feat: streamline portfolio UI and support atomic record edits
This commit is contained in:
1 parent
932a864ed6
commit
9adf7fdbc5
26 files changed
+988
-112
No files matched your search
@@ -51,11 +51,12 @@ export function AccountPicker({
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
<div className="actions">
|
||||
<button type="button" aria-pressed={!grouped} onClick={() => setGrouped(false)}>
|
||||
{tr('全部账户')}
|
||||
</button>
|
||||
<button type="button" aria-pressed={grouped} onClick={() => setGrouped(true)}>
|
||||
{tr('按分组显示')}
|
||||
<button
|
||||
className="secondary picker-view-toggle"
|
||||
type="button"
|
||||
onClick={() => setGrouped(!grouped)}
|
||||
>
|
||||
{tr(grouped ? '显示全部账户' : '按分组显示')} <span aria-hidden="true">⇄</span>
|
||||
</button>
|
||||
</div>
|
||||
{groups.map((group) => (
|
||||
|
||||
+205
-58
@@ -51,6 +51,7 @@ const labels: Record<string, string> = {
|
||||
debt: '债务',
|
||||
history: '变化记录',
|
||||
calendar: '收支日历',
|
||||
schedules: '定时计划',
|
||||
settings: '设置与备份',
|
||||
};
|
||||
const categories: Record<string, [string, string][]> = {
|
||||
@@ -221,6 +222,7 @@ export default function App() {
|
||||
kind: string;
|
||||
p?: Position;
|
||||
h?: History;
|
||||
transfer?: Transfer;
|
||||
operation?: DebtOperation;
|
||||
} | null>(null),
|
||||
[busy, setBusy] = useState(false),
|
||||
@@ -340,6 +342,9 @@ export default function App() {
|
||||
return;
|
||||
}
|
||||
setOverview({ ...o, trend: t.items, recent: recent.items });
|
||||
} else if (page === 'schedules') {
|
||||
const rows = await api<Position[]>('/positions?kind=account');
|
||||
if (active()) setPositions(rows);
|
||||
} else if (['account', 'asset', 'debt'].includes(page)) {
|
||||
if (page === 'account' && !rangeOnly) {
|
||||
const result = await api<{
|
||||
@@ -741,6 +746,29 @@ export default function App() {
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
async function correctHistory(h: History) {
|
||||
if (h.reason.startsWith('transfer_') || h.reason.startsWith('loan_')) {
|
||||
setSelected(null);
|
||||
setPage('history');
|
||||
return;
|
||||
}
|
||||
const session = sessionGeneration.current,
|
||||
view = viewRef.current;
|
||||
try {
|
||||
const p = await api<Position>('/positions/' + h.positionId);
|
||||
if (session !== sessionGeneration.current || view !== viewRef.current) return;
|
||||
setModal({ kind: 'correction', p, h });
|
||||
} catch (e) {
|
||||
if (session === sessionGeneration.current && view === viewRef.current) report(e);
|
||||
}
|
||||
}
|
||||
function deleteHistory(h: History) {
|
||||
if (!window.confirm(tr('删除这条余额记录并重算后续余额?'))) return;
|
||||
void act(
|
||||
() => api('/positions/' + h.positionId + '/revisions/' + h.id, 'DELETE'),
|
||||
tr('记录已删除,余额和日历已更新'),
|
||||
);
|
||||
}
|
||||
const nav = [
|
||||
['overview', LayoutDashboard],
|
||||
['account', Wallet],
|
||||
@@ -748,6 +776,7 @@ export default function App() {
|
||||
['debt', HandCoins],
|
||||
['history', HistoryIcon],
|
||||
['calendar', CalendarDays],
|
||||
['schedules', HistoryIcon],
|
||||
['settings', Settings],
|
||||
] as const;
|
||||
const visibleNav = nav.filter(([key]) => !(user.hiddenMenus || []).includes(key));
|
||||
@@ -882,42 +911,6 @@ export default function App() {
|
||||
{tr('正在刷新数据…')}
|
||||
</p>
|
||||
)}
|
||||
{page === 'overview' && (
|
||||
<section className="panel">
|
||||
{' '}
|
||||
<div className="actions">
|
||||
<label>
|
||||
{tr('开始')}
|
||||
<input
|
||||
type="date"
|
||||
value={rangeFrom}
|
||||
max={rangeTo}
|
||||
onChange={(e) => setRangeFrom(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{tr('结束')}
|
||||
<input
|
||||
type="date"
|
||||
value={rangeTo}
|
||||
min={rangeFrom}
|
||||
max={today()}
|
||||
onChange={(e) => setRangeTo(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<select value={grain} onChange={(e) => setGrain(e.target.value)}>
|
||||
<option value="day">{tr('日(最多366天)')}</option>
|
||||
<option value="week">{tr('周(最多3年)')}</option>
|
||||
<option value="month">{tr('月(最多10年)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className="muted">
|
||||
{tr(
|
||||
'默认最近90天;周一至周日及自然月取期末余额,变化归因累计区间内每日变化,首尾不足周期按实际范围。',
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
{page === 'overview' && overview && (
|
||||
<>
|
||||
{!overview.complete && (
|
||||
@@ -967,6 +960,41 @@ export default function App() {
|
||||
<h2>{tr('净资产轨迹')}</h2>
|
||||
<span className="badge">{tr('按业务日期')}</span>
|
||||
</div>
|
||||
<div className="actions overview-filters">
|
||||
<label>
|
||||
{tr('开始')}
|
||||
<input
|
||||
type="date"
|
||||
value={rangeFrom}
|
||||
max={rangeTo}
|
||||
onChange={(e) => setRangeFrom(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{tr('结束')}
|
||||
<input
|
||||
type="date"
|
||||
value={rangeTo}
|
||||
min={rangeFrom}
|
||||
max={today()}
|
||||
onChange={(e) => setRangeTo(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<select
|
||||
aria-label={tr('统计粒度')}
|
||||
value={grain}
|
||||
onChange={(e) => setGrain(e.target.value)}
|
||||
>
|
||||
<option value="day">{tr('日(最多366天)')}</option>
|
||||
<option value="week">{tr('周(最多3年)')}</option>
|
||||
<option value="month">{tr('月(最多10年)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className="muted">
|
||||
{tr(
|
||||
'默认最近90天;周一至周日及自然月取期末余额,变化归因累计区间内每日变化,首尾不足周期按实际范围。',
|
||||
)}
|
||||
</p>
|
||||
<Chart rows={overview.trend} base={user.baseCurrency} />
|
||||
<p className="muted">
|
||||
{tr('按当时可用汇率重放;更正历史后重新计算。归档项目仍计入。')}
|
||||
@@ -1014,7 +1042,7 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
<HistoryTable
|
||||
rows={overview.recent}
|
||||
rows={overview.recent.slice(0, 6)}
|
||||
open={(id) => {
|
||||
setSelected(id);
|
||||
setPage(overview.recent.find((h) => h.positionId === id)?.kind || 'account');
|
||||
@@ -1034,8 +1062,8 @@ export default function App() {
|
||||
{tr('· 每日尝试更新,失败保留已有数据;旧日期汇率可能产生换算偏差。')}
|
||||
</div>
|
||||
{overview.trend.length > 1 && (
|
||||
<section className="panel">
|
||||
<h2>{tr('变化归因')}</h2>
|
||||
<details className="panel attribution">
|
||||
<summary>{tr('变化归因')}</summary>
|
||||
<div className="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -1068,7 +1096,7 @@ export default function App() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</details>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -1211,7 +1239,8 @@ export default function App() {
|
||||
</p>
|
||||
<HistoryTable
|
||||
rows={historyRows}
|
||||
correct={p.archived ? undefined : (h) => setModal({ kind: 'correction', p, h })}
|
||||
correct={p.archived ? undefined : (h) => void correctHistory(h)}
|
||||
remove={p.archived ? undefined : deleteHistory}
|
||||
/>
|
||||
{historyCursor && (
|
||||
<button disabled={loading} onClick={() => void more('history')}>
|
||||
@@ -1252,12 +1281,6 @@ export default function App() {
|
||||
</select>
|
||||
</div>
|
||||
{quickMode && <p className="muted">{tr('点击账户卡片,直接填写当前余额。')}</p>}
|
||||
<SchedulePanel
|
||||
accounts={positions.filter(
|
||||
(p) => p.kind === 'account' && p.side === 'asset' && !p.archived,
|
||||
)}
|
||||
refresh={() => load(false, true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{positions.some((p) => p.kind === page) ? (
|
||||
@@ -1392,6 +1415,28 @@ export default function App() {
|
||||
{money(t.fee, t.sourceCurrency)}
|
||||
</p>
|
||||
{t.notes && <p className="muted notes-display">{t.notes}</p>}
|
||||
<div className="actions record-actions">
|
||||
<button
|
||||
className="text"
|
||||
disabled={busy}
|
||||
onClick={() => setModal({ kind: 'transfer-edit', transfer: t })}
|
||||
>
|
||||
{tr('修改')}
|
||||
</button>
|
||||
<button
|
||||
className="text danger-text"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (window.confirm(tr('删除这笔资金往来并重算双方余额?')))
|
||||
void act(
|
||||
() => api('/transfers/' + t.id, 'DELETE'),
|
||||
tr('记录已删除,余额和日历已更新'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{tr('删除')}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
@@ -1409,6 +1454,8 @@ export default function App() {
|
||||
</p>
|
||||
<HistoryTable
|
||||
rows={historyRows}
|
||||
correct={(h) => void correctHistory(h)}
|
||||
remove={deleteHistory}
|
||||
open={(id) => {
|
||||
setSelected(id);
|
||||
setPage(historyRows.find((h) => h.positionId === id)?.kind || 'account');
|
||||
@@ -1422,6 +1469,16 @@ export default function App() {
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
{page === 'schedules' && (
|
||||
<section className="panel">
|
||||
<SchedulePanel
|
||||
accounts={positions.filter(
|
||||
(p) => p.kind === 'account' && p.side === 'asset' && !p.archived,
|
||||
)}
|
||||
refresh={() => load(false, true)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
{page === 'calendar' && (
|
||||
<Calendar revealed={!!user.revealed} refreshVersion={calendarRefresh} />
|
||||
)}
|
||||
@@ -1471,9 +1528,15 @@ export default function App() {
|
||||
() =>
|
||||
api('/settings', 'PATCH', {
|
||||
baseCurrency: f.get('baseCurrency'),
|
||||
hiddenMenus: ['overview', 'account', 'asset', 'debt', 'history'].filter(
|
||||
(key) => !f.has('menu_' + key),
|
||||
),
|
||||
hiddenMenus: [
|
||||
'overview',
|
||||
'account',
|
||||
'asset',
|
||||
'debt',
|
||||
'history',
|
||||
'calendar',
|
||||
'schedules',
|
||||
].filter((key) => !f.has('menu_' + key)),
|
||||
idleMinutes: Number(f.get('idleMinutes')),
|
||||
showNotes: f.has('showNotes'),
|
||||
}),
|
||||
@@ -1485,7 +1548,15 @@ export default function App() {
|
||||
{tr('选择需要显示的菜单项。设置入口始终保留,隐藏仅影响导航,不删除数据。')}
|
||||
</p>
|
||||
<div className="menu-preferences">
|
||||
{['overview', 'account', 'asset', 'debt', 'history'].map((key) => (
|
||||
{[
|
||||
'overview',
|
||||
'account',
|
||||
'asset',
|
||||
'debt',
|
||||
'history',
|
||||
'calendar',
|
||||
'schedules',
|
||||
].map((key) => (
|
||||
<label key={key} className="switch-row">
|
||||
<span>{tr(labels[key])}</span>
|
||||
<input
|
||||
@@ -1865,7 +1936,76 @@ export default function App() {
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
{modal && (
|
||||
{modal?.kind === 'transfer-edit' && modal.transfer && (
|
||||
<Modal
|
||||
title={tr('修改资金往来')}
|
||||
close={() => {
|
||||
if (!busy) setModal(null);
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const t = modal.transfer!,
|
||||
f = Object.fromEntries(new FormData(e.currentTarget));
|
||||
void act(
|
||||
() =>
|
||||
api('/transfers/' + t.id, 'PUT', {
|
||||
...f,
|
||||
sourceId: t.sourceId,
|
||||
targetId: t.targetId,
|
||||
operation: t.operation,
|
||||
}),
|
||||
tr('记录已修改,余额和日历已更新'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
{modal.transfer.source.name} ↔ {modal.transfer.target.name}
|
||||
</p>
|
||||
<Field label={tr('转出金额(') + modal.transfer.sourceCurrency + ')'}>
|
||||
<input
|
||||
name="amount"
|
||||
required
|
||||
inputMode="decimal"
|
||||
defaultValue={modal.transfer.amount}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={tr('实际到账(') + modal.transfer.targetCurrency + ')'}>
|
||||
<input
|
||||
name="received"
|
||||
required
|
||||
inputMode="decimal"
|
||||
defaultValue={modal.transfer.received}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={tr('手续费 / 优惠(') + modal.transfer.sourceCurrency + ')'}>
|
||||
<input name="fee" required inputMode="decimal" defaultValue={modal.transfer.fee} />
|
||||
</Field>
|
||||
<Field label={tr('记录时间(UTC+8)')}>
|
||||
<input
|
||||
name="date"
|
||||
type="datetime-local"
|
||||
required
|
||||
max={nowMinute()}
|
||||
defaultValue={modal.transfer.date}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={tr('备注')}>
|
||||
<textarea name="notes" maxLength={2000} defaultValue={modal.transfer.notes} />
|
||||
</Field>
|
||||
{error && (
|
||||
<p role="alert" className="notice error">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<button className="primary" disabled={busy}>
|
||||
{tr(busy ? '正在保存…' : '保存')}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
)}
|
||||
{modal && modal.kind !== 'transfer-edit' && (
|
||||
<Modal
|
||||
title={
|
||||
modal.kind === 'debt-payment'
|
||||
@@ -2215,10 +2355,12 @@ function HistoryTable({
|
||||
rows,
|
||||
open,
|
||||
correct,
|
||||
remove,
|
||||
}: {
|
||||
rows: History[];
|
||||
open?: (id: string) => void;
|
||||
correct?: (h: History) => void;
|
||||
remove?: (h: History) => void;
|
||||
}) {
|
||||
return rows.length ? (
|
||||
<div className="table-wrap">
|
||||
@@ -2307,13 +2449,18 @@ function HistoryTable({
|
||||
<td className="notes-cell">{h.notes || '—'}</td>
|
||||
{correct && (
|
||||
<td>
|
||||
<button
|
||||
className="text"
|
||||
disabled={h.reason.startsWith('transfer_') || h.reason.startsWith('loan_')}
|
||||
onClick={() => correct(h)}
|
||||
>
|
||||
{tr('更正')}
|
||||
<button className="text" onClick={() => correct(h)}>
|
||||
{tr(
|
||||
h.reason.startsWith('transfer_') || h.reason.startsWith('loan_')
|
||||
? '查看资金往来'
|
||||
: '更正',
|
||||
)}
|
||||
</button>
|
||||
{remove && !h.reason.startsWith('transfer_') && !h.reason.startsWith('loan_') && (
|
||||
<button className="text danger-text" onClick={() => remove(h)}>
|
||||
{tr('删除')}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
|
||||
@@ -61,12 +61,12 @@ export function SchedulePanel({
|
||||
}
|
||||
}
|
||||
return (
|
||||
<details className="schedule-panel">
|
||||
<summary>
|
||||
<section className="schedule-panel">
|
||||
<h2>
|
||||
{tr('定时计划')} ({rows.length})
|
||||
</summary>
|
||||
</h2>
|
||||
<p className="muted">
|
||||
{tr('打开账户页时处理到期计划。每次最多执行20项,离线期间的计划会逐次补记。')}
|
||||
{tr('打开账户页或点击执行时处理到期计划。每次最多执行20项,离线期间的计划会逐次补记。')}
|
||||
</p>
|
||||
<div className="actions">
|
||||
<button type="button" disabled={busy || !accounts.length} onClick={() => setOpen(!open)}>
|
||||
@@ -227,6 +227,6 @@ export function SchedulePanel({
|
||||
</p>
|
||||
)}
|
||||
{status && <p role="status">{status}</p>}
|
||||
</details>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -426,5 +426,23 @@
|
||||
"六": "Sat",
|
||||
"日": "Sun",
|
||||
"已完成": "Completed",
|
||||
"一次性计划已完成,请新建计划": "This one-time plan is complete. Create a new plan."
|
||||
"一次性计划已完成,请新建计划": "This one-time plan is complete. Create a new plan.",
|
||||
"显示全部账户": "Show all accounts",
|
||||
"删除这条余额记录并重算后续余额?": "Delete this balance record and recalculate later balances?",
|
||||
"删除这笔资金往来并重算双方余额?": "Delete this movement and recalculate both balances?",
|
||||
"记录已删除,余额和日历已更新": "Record deleted. Balances and calendar updated.",
|
||||
"记录已修改,余额和日历已更新": "Record updated. Balances and calendar updated.",
|
||||
"修改资金往来": "Edit movement",
|
||||
"查看资金往来": "View movements",
|
||||
"打开账户页或点击执行时处理到期计划。每次最多执行20项,离线期间的计划会逐次补记。": "Due plans run when opening Accounts or clicking Run. Each batch processes up to 20 plans, including overdue occurrences.",
|
||||
"配对记录不完整,无法重算": "Paired records are incomplete. Recalculation failed.",
|
||||
"资金操作之前必须保留一条余额记录": "Keep a balance record before the first movement.",
|
||||
"修改后债务或资产金额不能为负数": "The edited debt or asset value cannot be negative.",
|
||||
"重算后的金额超出支持范围": "The recalculated amount exceeds the supported range.",
|
||||
"请在资金往来中修改完整配对记录": "Edit the complete pair in Movements.",
|
||||
"请在资金往来中删除完整配对记录": "Delete the complete pair in Movements.",
|
||||
"资金往来记录不存在": "Movement not found.",
|
||||
"修改记录不能更换账户或操作类型,请删除后重新创建": "To change accounts or the operation type, delete the movement and create it again.",
|
||||
"修改": "Edit",
|
||||
"统计粒度": "Time interval"
|
||||
}
|
||||
@@ -426,5 +426,23 @@
|
||||
"六": "六",
|
||||
"日": "日",
|
||||
"已完成": "已完成",
|
||||
"一次性计划已完成,请新建计划": "一次性計劃已完成,請新建計劃"
|
||||
"一次性计划已完成,请新建计划": "一次性計劃已完成,請新建計劃",
|
||||
"显示全部账户": "顯示全部賬戶",
|
||||
"删除这条余额记录并重算后续余额?": "刪除這條餘額記錄並重算後續餘額?",
|
||||
"删除这笔资金往来并重算双方余额?": "刪除這筆資金往來並重算雙方餘額?",
|
||||
"记录已删除,余额和日历已更新": "記錄已刪除,餘額和日曆已更新",
|
||||
"记录已修改,余额和日历已更新": "記錄已修改,餘額和日曆已更新",
|
||||
"修改资金往来": "修改資金往來",
|
||||
"查看资金往来": "查看資金往來",
|
||||
"打开账户页或点击执行时处理到期计划。每次最多执行20项,离线期间的计划会逐次补记。": "打開賬戶頁或點擊執行時處理到期計劃。每次最多執行20項,離線期間的計劃會逐次補記。",
|
||||
"配对记录不完整,无法重算": "配對記錄不完整,無法重算",
|
||||
"资金操作之前必须保留一条余额记录": "資金操作之前必須保留一條餘額記錄",
|
||||
"修改后债务或资产金额不能为负数": "修改後債務或資產金額不能為負數",
|
||||
"重算后的金额超出支持范围": "重算後的金額超出支援範圍",
|
||||
"请在资金往来中修改完整配对记录": "請在資金往來中修改完整配對記錄",
|
||||
"请在资金往来中删除完整配对记录": "請在資金往來中刪除完整配對記錄",
|
||||
"资金往来记录不存在": "資金往來記錄不存在",
|
||||
"修改记录不能更换账户或操作类型,请删除后重新创建": "修改記錄不能更換賬戶或操作類型,請刪除後重新建立",
|
||||
"修改": "修改",
|
||||
"统计粒度": "統計粒度"
|
||||
}
|
||||
@@ -1335,3 +1335,155 @@ footer {
|
||||
.calendar-panel h2 {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Compact cards and a calmer overview hierarchy. */
|
||||
body {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
[contenteditable='true'] {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
.position-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.position-card {
|
||||
padding: 14px 16px;
|
||||
position: relative;
|
||||
}
|
||||
.position-card > div {
|
||||
float: left;
|
||||
margin: 0 12px 8px 0;
|
||||
}
|
||||
.position-card > div > svg {
|
||||
display: none;
|
||||
}
|
||||
.position-icon {
|
||||
padding: 7px;
|
||||
}
|
||||
.position-card h3 {
|
||||
font-size: 14px;
|
||||
margin: 0 0 2px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.position-card p {
|
||||
margin: 2px 0 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.position-card strong {
|
||||
clear: both;
|
||||
font-size: 19px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.position-card small {
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.position-card .badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.picker-view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.picker-dialog .actions {
|
||||
margin: 12px 0;
|
||||
}
|
||||
.metric {
|
||||
padding: 20px;
|
||||
}
|
||||
.metric.net {
|
||||
background: #e9f3ed;
|
||||
border-color: #a9cbb9;
|
||||
}
|
||||
.metric strong {
|
||||
font-size: clamp(22px, 2.5vw, 32px);
|
||||
}
|
||||
.dashboard-grid {
|
||||
grid-template-columns: minmax(0, 2fr) minmax(230px, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.dashboard-grid .panel {
|
||||
padding: 20px;
|
||||
}
|
||||
.dashboard-grid .privacy {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.attribution summary {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.attribution[open] summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.record-actions {
|
||||
gap: 16px;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.position-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(165px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.position-card {
|
||||
padding: 12px;
|
||||
}
|
||||
.position-card strong {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.metric.net {
|
||||
order: -1;
|
||||
}
|
||||
.metrics {
|
||||
grid-template-columns: 1.2fr 1fr 1fr;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.metric.net {
|
||||
color: #234c3b;
|
||||
}
|
||||
.metric.net > div,
|
||||
.metric.net small {
|
||||
color: #557867;
|
||||
}
|
||||
.overview-filters {
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.overview-filters label {
|
||||
flex: 1;
|
||||
min-width: 130px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.overview-filters input {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.overview-filters select {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
min-width: 130px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.overview-filters + .muted {
|
||||
font-size: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
Reference in new issue
Block a user