@poodle64/ui 2026.10.1 → 2026.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/dist/components/ui/ledger/ledger.d.ts +5 -0
- package/dist/components/ui/ledger/ledger.js +17 -0
- package/dist/components/ui/ledger/ledger.svelte +126 -35
- package/dist/components/ui/ledger/types.d.ts +17 -1
- package/package.json +1 -1
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -886,8 +886,8 @@ no module rebuilds them and a need one module finds becomes a toggle for all.
|
|
|
886
886
|
|
|
887
887
|
| Prop | Purpose |
|
|
888
888
|
| --- | --- |
|
|
889
|
-
| `rows` | `LedgerRow`s, extended with whatever the module's columns read: `id`, `date` (ISO), `title`, `note`, `amount` (signed), `balance`, `review` (`true` or a `Status`), `attachment` (its name), `origins` (a synthetic row's statement lines). Pass every row the view covers, filtered: the ledger sorts newest first, keeps a day's rows in the order given, and mounts only those near the view. |
|
|
890
|
-
| `columns` | The module's columns, in order after the amount. The keys `date`, `title`, `amount` and `balance` are the ledger's own cells: list one to rename or resize it (the title reads "Description" until the module names it), and list `balance` to offer the running balance at all, with `text` saying what it counts (money, a coin, units). A `cell` snippet's links and buttons take their own clicks; the rest of the cell opens the row. |
|
|
889
|
+
| `rows` | `LedgerRow`s, extended with whatever the module's columns read: `id`, `date` (ISO), `title`, `note`, `amount` (signed), `balance`, `review` (`true` or a `Status`) with `reviewLabel` (the chip's words, "Review" by default), `conversion` (a swap inside the account: plain amount, in no total), `attachment` (its name), `origins` (a synthetic row's statement lines). Pass every row the view covers, filtered: the ledger sorts newest first, keeps a day's rows in the order given, and mounts only those near the view. |
|
|
890
|
+
| `columns` | The module's columns, in order after the amount. The keys `date`, `title`, `amount` and `balance` are the ledger's own cells: list one to rename or resize it (the title reads "Description" until the module names it), and list `balance` to offer the running balance at all, with `text` saying what it counts (money, a coin, units). A `cell` snippet's links and buttons take their own clicks; the rest of the cell opens the row. A column's `sort` gives the real value its head sorts by (a number, not its text); a `text` column sorts by its text without one, a `cell`-only column does not sort, and the balance never does. |
|
|
891
891
|
| `preferences`, `onPreferencesChange` | The viewer's `{ columns, period }`. Missing fields take the module's defaults (each column's `on`, the balance off, grouped by month); an unknown column key is ignored. The ledger stores nothing. |
|
|
892
892
|
| `fyStart` | The month a financial year starts, 1–12. July by default; anything else groups from July and logs an error in development. |
|
|
893
893
|
| `locale`, `currency` | Every figure, amounts, nets and counts, in this locale (`AU_LOCALE` by default) and currency (AUD). Dates keep the drawn "15 Sep": the ledger's words are English. |
|
|
@@ -900,16 +900,22 @@ no module rebuilds them and a need one module finds becomes a toggle for all.
|
|
|
900
900
|
Settled here, the same in every module, and not props:
|
|
901
901
|
|
|
902
902
|
- **Two lines a row**, the note under the title, the pair centred on the row; a
|
|
903
|
-
row with no note centres its title. The checkbox, the review
|
|
903
|
+
row with no note centres its title. The checkbox, the review chip, the date and
|
|
904
904
|
every single-value cell centre too.
|
|
905
|
-
- **One mark per kind.** A review
|
|
906
|
-
chip; a synthetic row's "Not as printed" badge leads its second line and
|
|
905
|
+
- **One mark per kind.** A review chip (StatusBadge's dot and label on a tint)
|
|
906
|
+
beside the title on line one, which truncates before the chip does; a synthetic row's "Not as printed" badge leads its second line and
|
|
907
907
|
opens the row in place to the lines it was made from.
|
|
908
908
|
- **PocketSmith's look.** No panel around the list; a head card exactly as wide
|
|
909
909
|
as the group cards, its columns on theirs; one rounded card per period with
|
|
910
910
|
hairlines between rows; the period's label, count and net on the page ground
|
|
911
911
|
above its card; figures in the body face with tabular numerals, money out
|
|
912
912
|
bracketed and money in green (`formatCurrency(v, { negative: 'brackets' })`).
|
|
913
|
+
- **Sorting and the groups' one control.** Click a column head to sort by it,
|
|
914
|
+
largest or newest first; click again to reverse. A sorted list is one flat
|
|
915
|
+
run with no group heads and shows the Date column even by day. Until the
|
|
916
|
+
viewer sorts, rows sit newest first in their groups. A chevron in the head's
|
|
917
|
+
gutter opens or closes every group at once; where rows tick, the gutter
|
|
918
|
+
widens to hold it beside the select-all checkbox.
|
|
913
919
|
- **The Columns menu** holds every column as a tick, the three fixed ones
|
|
914
920
|
locked and the running balance last, then the period: financial year,
|
|
915
921
|
calendar year, month, week (from Monday), day (the date heads the group and
|
|
@@ -16,6 +16,11 @@ export declare function groupRows<R extends LedgerRow>(rows: readonly R[], perio
|
|
|
16
16
|
key: string;
|
|
17
17
|
rows: R[];
|
|
18
18
|
}[];
|
|
19
|
+
/**
|
|
20
|
+
* The rows ordered by a column's value, `dir` 1 ascending and -1 descending;
|
|
21
|
+
* rows without a value come last either way, and equal values keep newest first.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sortRows<R extends LedgerRow>(rows: readonly R[], value: (row: R) => number | string | null | undefined, dir: 1 | -1): R[];
|
|
19
24
|
/** Whole cents, so a group's sum carries no float error. */
|
|
20
25
|
export declare function cents(value: number | string | null | undefined): number;
|
|
21
26
|
/** Money as the ledger prints it: in plain, out in brackets. */
|
|
@@ -122,6 +122,23 @@ export function groupRows(rows, period, fyStart = 7) {
|
|
|
122
122
|
}
|
|
123
123
|
return out;
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The rows ordered by a column's value, `dir` 1 ascending and -1 descending;
|
|
127
|
+
* rows without a value come last either way, and equal values keep newest first.
|
|
128
|
+
*/
|
|
129
|
+
export function sortRows(rows, value, dir) {
|
|
130
|
+
const keyed = (groupRows(rows, 'none')[0]?.rows ?? []).map((row) => ({ row, v: value(row) }));
|
|
131
|
+
const missing = (v) => v === null || v === undefined || v === '';
|
|
132
|
+
keyed.sort((a, b) => {
|
|
133
|
+
if (missing(a.v) || missing(b.v))
|
|
134
|
+
return Number(missing(a.v)) - Number(missing(b.v));
|
|
135
|
+
const cmp = typeof a.v === 'number' && typeof b.v === 'number'
|
|
136
|
+
? a.v - b.v
|
|
137
|
+
: String(a.v).localeCompare(String(b.v), undefined, { numeric: true, sensitivity: 'base' });
|
|
138
|
+
return dir * cmp;
|
|
139
|
+
});
|
|
140
|
+
return keyed.map((k) => k.row);
|
|
141
|
+
}
|
|
125
142
|
/** Whole cents, so a group's sum carries no float error. */
|
|
126
143
|
export function cents(value) {
|
|
127
144
|
const n = Number(value);
|
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
import { DEV } from 'esm-env';
|
|
19
19
|
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
|
20
20
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
21
|
+
import ChevronUp from '@lucide/svelte/icons/chevron-up';
|
|
21
22
|
import Paperclip from '@lucide/svelte/icons/paperclip';
|
|
22
23
|
import X from '@lucide/svelte/icons/x';
|
|
23
24
|
import Button from '../button/button.svelte';
|
|
24
25
|
import { Checkbox } from '../checkbox/index.js';
|
|
25
26
|
import EmptyState from '../empty-state/empty-state.svelte';
|
|
27
|
+
import StatusBadge from '../status-badge/status-badge.svelte';
|
|
26
28
|
import LedgerColumns from './ledger-columns.svelte';
|
|
27
29
|
import { AU_LOCALE } from '../../../format.js';
|
|
28
30
|
import { cn } from '../../../utils.js';
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
ledgerItems,
|
|
37
39
|
ledgerMoney,
|
|
38
40
|
periodLabel,
|
|
41
|
+
sortRows,
|
|
39
42
|
windowOf
|
|
40
43
|
} from './ledger.js';
|
|
41
44
|
import type { LedgerColumn, LedgerOpenContext, LedgerPreferences, LedgerRow } from './types.js';
|
|
@@ -109,12 +112,18 @@
|
|
|
109
112
|
} = $props();
|
|
110
113
|
|
|
111
114
|
const BUILT_IN: Record<string, Omit<LedgerColumn<R>, 'key'>> = {
|
|
112
|
-
date: { label: 'Date', width: { narrow: '4.25rem', wide: '7rem' } },
|
|
115
|
+
date: { label: 'Date', width: { narrow: '4.25rem', wide: '7rem' }, sort: (r) => r.date.slice(0, 10) },
|
|
113
116
|
title: {
|
|
114
117
|
label: 'Description',
|
|
118
|
+
sort: (r) => r.title,
|
|
115
119
|
width: { narrow: 'minmax(0, 1fr)', wide: 'minmax(16rem, 1.5fr)' }
|
|
116
120
|
},
|
|
117
|
-
amount: {
|
|
121
|
+
amount: {
|
|
122
|
+
label: 'Amount',
|
|
123
|
+
width: { narrow: '7.25rem', wide: '9.5rem' },
|
|
124
|
+
align: 'end',
|
|
125
|
+
sort: (r) => cents(r.amount)
|
|
126
|
+
},
|
|
118
127
|
balance: {
|
|
119
128
|
label: 'Running balance',
|
|
120
129
|
head: 'Balance',
|
|
@@ -123,7 +132,9 @@
|
|
|
123
132
|
on: false
|
|
124
133
|
}
|
|
125
134
|
};
|
|
135
|
+
/** Ticking adds a checkbox to the gutter, so it widens to hold the chevron beside it. */
|
|
126
136
|
const GUTTER = '2.5rem';
|
|
137
|
+
const GUTTER_TICKS = '3.5rem';
|
|
127
138
|
const CELL = 'min-w-0 truncate px-2';
|
|
128
139
|
const END = 'pr-5 text-right';
|
|
129
140
|
/** The row's height rides the package's density ramp: 3.5rem, 3rem compact. */
|
|
@@ -202,7 +213,27 @@
|
|
|
202
213
|
);
|
|
203
214
|
const WIDE = $derived(mode === 'wide');
|
|
204
215
|
const PHONE = $derived(mode === 'phone');
|
|
205
|
-
|
|
216
|
+
|
|
217
|
+
const selectable = $derived(!!bulkActions && !PHONE);
|
|
218
|
+
// ── Sorting: the viewer's own, kept here until a head is clicked again ──
|
|
219
|
+
let sort = $state<{ key: string; dir: 1 | -1 } | null>(null);
|
|
220
|
+
/** What a column sorts by; the running balance depends on the order, so never it. */
|
|
221
|
+
const sorter = (c: { key: string; sort?: LedgerColumn<R>['sort']; text?: LedgerColumn<R>['text'] }) =>
|
|
222
|
+
c.key === 'balance' ? undefined : (c.sort ?? c.text);
|
|
223
|
+
/** Click a head to sort by it, largest or newest first; again to reverse. */
|
|
224
|
+
function sortBy(key: string) {
|
|
225
|
+
sort = sort?.key === key ? { key, dir: sort.dir === 1 ? -1 : 1 } : { key, dir: -1 };
|
|
226
|
+
}
|
|
227
|
+
/** The sort in force: a column the viewer has since unticked no longer sorts. */
|
|
228
|
+
const activeSort = $derived.by(() => {
|
|
229
|
+
const col = sort && resolved.find((c) => c.key === sort!.key);
|
|
230
|
+
return sort && col && sorter(col) && (col.locked || prefs.columns.includes(col.key))
|
|
231
|
+
? sort
|
|
232
|
+
: null;
|
|
233
|
+
});
|
|
234
|
+
/** Until the viewer sorts, rows sit newest first in their period's groups. */
|
|
235
|
+
const grouped = $derived(prefs.period !== 'none' && !activeSort);
|
|
236
|
+
const BY_DAY = $derived(grouped && prefs.period === 'day');
|
|
206
237
|
|
|
207
238
|
const shown = $derived(
|
|
208
239
|
resolved.filter(
|
|
@@ -210,7 +241,7 @@
|
|
|
210
241
|
)
|
|
211
242
|
);
|
|
212
243
|
const template = $derived(
|
|
213
|
-
`${GUTTER} ${shown.map((c) => (WIDE ? c.track.wide : c.track.narrow)).join(' ')}`
|
|
244
|
+
`${selectable ? GUTTER_TICKS : GUTTER} ${shown.map((c) => (WIDE ? c.track.wide : c.track.narrow)).join(' ')}`
|
|
214
245
|
);
|
|
215
246
|
/** A column's grid line, the gutter being 1; 0 when it is not shown. */
|
|
216
247
|
const column = (key: string) => {
|
|
@@ -229,7 +260,11 @@
|
|
|
229
260
|
if (DEV && !validStart)
|
|
230
261
|
console.error(`Ledger: fyStart is a month from 1 to 12, not ${fyStart}; grouping from July.`);
|
|
231
262
|
});
|
|
232
|
-
const groups = $derived(
|
|
263
|
+
const groups = $derived.by(() => {
|
|
264
|
+
const by = activeSort && sorter(resolved.find((c) => c.key === activeSort.key)!);
|
|
265
|
+
if (activeSort && by) return [{ key: '', rows: sortRows(rows, by, activeSort.dir) }];
|
|
266
|
+
return groupRows(rows, prefs.period, fy);
|
|
267
|
+
});
|
|
233
268
|
/** Closed groups, keyed with their period so a new period starts open. */
|
|
234
269
|
const closed = new SvelteSet<string>();
|
|
235
270
|
const groupId = (key: string) => `${prefs.period}:${key}`;
|
|
@@ -241,6 +276,17 @@
|
|
|
241
276
|
else set.add(value);
|
|
242
277
|
}
|
|
243
278
|
|
|
279
|
+
const allOpen = $derived(groups.every((g) => !closed.has(groupId(g.key))));
|
|
280
|
+
/** One control for every group: open them all, or close them all. */
|
|
281
|
+
function toggleAll() {
|
|
282
|
+
const closing = allOpen;
|
|
283
|
+
if (closing && open !== null) setOpen(null);
|
|
284
|
+
for (const g of groups) {
|
|
285
|
+
if (closing) closed.add(groupId(g.key));
|
|
286
|
+
else closed.delete(groupId(g.key));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
244
290
|
/** Closing a group closes the row open in it, and says so. */
|
|
245
291
|
function toggleGroup(g: { key: string; rows: readonly R[] }) {
|
|
246
292
|
const id = groupId(g.key);
|
|
@@ -249,7 +295,6 @@
|
|
|
249
295
|
}
|
|
250
296
|
|
|
251
297
|
// ── Selection and the opened row ──
|
|
252
|
-
const selectable = $derived(!!bulkActions && !PHONE);
|
|
253
298
|
const openable = $derived(!!editor);
|
|
254
299
|
const ticked = $derived(new Set<Id>(selected));
|
|
255
300
|
const tickedRows = $derived(groups.flatMap((g) => g.rows).filter((r) => ticked.has(r.id)));
|
|
@@ -287,7 +332,7 @@
|
|
|
287
332
|
// bar, the cards and the head keep their true geometry.
|
|
288
333
|
const items = $derived(
|
|
289
334
|
ledgerItems(groups, {
|
|
290
|
-
heads:
|
|
335
|
+
heads: grouped,
|
|
291
336
|
closed: (key) => closed.has(groupId(key)),
|
|
292
337
|
open: openable ? open : null
|
|
293
338
|
})
|
|
@@ -301,7 +346,7 @@
|
|
|
301
346
|
let measured = $state(0);
|
|
302
347
|
let rowEstimate = $state(REM * 3.5);
|
|
303
348
|
let headEstimate = $state(REM * 3.125);
|
|
304
|
-
const sizeKey = (key: string) => `${mode}|${prefs.period}|${key}`;
|
|
349
|
+
const sizeKey = (key: string) => `${mode}|${grouped ? prefs.period : 'flat'}|${key}`;
|
|
305
350
|
|
|
306
351
|
const starts = $derived.by(() => {
|
|
307
352
|
void measured;
|
|
@@ -367,6 +412,7 @@
|
|
|
367
412
|
let inn = 0;
|
|
368
413
|
let out = 0;
|
|
369
414
|
for (const r of rs) {
|
|
415
|
+
if (r.conversion) continue;
|
|
370
416
|
const c = cents(r.amount);
|
|
371
417
|
if (c > 0) inn += c;
|
|
372
418
|
else out += c;
|
|
@@ -378,6 +424,8 @@
|
|
|
378
424
|
const number = (n: number) => n.toLocaleString(locale);
|
|
379
425
|
const count = (n: number) => `${number(n)} ${n === 1 ? noun[0] : noun[1]}`;
|
|
380
426
|
const income = (value: number | string | null | undefined) => cents(value) > 0;
|
|
427
|
+
/** Green for money in; a conversion is neither in nor out, so plain. */
|
|
428
|
+
const isIncome = (r: R) => !r.conversion && income(r.amount);
|
|
381
429
|
const reviewOf = (r: R) => (r.review === true ? 'warning' : r.review || null);
|
|
382
430
|
const balanceText = (r: R) =>
|
|
383
431
|
balanceColumn?.text ? balanceColumn.text(r) : r.balance == null ? '' : money(r.balance);
|
|
@@ -431,19 +479,12 @@
|
|
|
431
479
|
</button>
|
|
432
480
|
{/snippet}
|
|
433
481
|
|
|
434
|
-
{#snippet
|
|
482
|
+
{#snippet chip(r: R)}
|
|
435
483
|
{@const status = reviewOf(r)}
|
|
436
484
|
{#if status}
|
|
437
|
-
<span
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
`ds-dot-${status}`,
|
|
441
|
-
beside && 'absolute top-1/2 right-[calc(50%+0.75rem)] -translate-y-1/2'
|
|
442
|
-
)}
|
|
443
|
-
role="img"
|
|
444
|
-
aria-label="Needs review"
|
|
445
|
-
data-slot="ledger-review"
|
|
446
|
-
></span>
|
|
485
|
+
<span class="flex-none" data-slot="ledger-review">
|
|
486
|
+
<StatusBadge {status} label={r.reviewLabel ?? 'Review'} />
|
|
487
|
+
</span>
|
|
447
488
|
{/if}
|
|
448
489
|
{/snippet}
|
|
449
490
|
|
|
@@ -467,6 +508,7 @@
|
|
|
467
508
|
<span class="flex min-w-0 items-center gap-1.5">
|
|
468
509
|
<span class="min-w-0 truncate">{r.title}</span>
|
|
469
510
|
{#if r.attachment}{@render clip(r)}{/if}
|
|
511
|
+
{@render chip(r)}
|
|
470
512
|
</span>
|
|
471
513
|
{#if r.note || r.origins}
|
|
472
514
|
<span class="text-muted-foreground flex min-w-0 items-center gap-2 text-xs font-normal">
|
|
@@ -482,7 +524,7 @@
|
|
|
482
524
|
END,
|
|
483
525
|
through,
|
|
484
526
|
'self-center tabular-nums',
|
|
485
|
-
|
|
527
|
+
isIncome(r) && 'text-status-success'
|
|
486
528
|
)}
|
|
487
529
|
{style}
|
|
488
530
|
data-slot="ledger-amount"
|
|
@@ -611,9 +653,9 @@
|
|
|
611
653
|
{/if}
|
|
612
654
|
<span class="pointer-events-none flex min-w-0 flex-col justify-center gap-0.5">
|
|
613
655
|
<span class="flex min-w-0 items-center gap-1.5">
|
|
614
|
-
{@render dot(r, false)}
|
|
615
656
|
<span class="min-w-0 truncate font-medium">{r.title}</span>
|
|
616
657
|
{#if r.attachment}{@render clip(r)}{/if}
|
|
658
|
+
{@render chip(r)}
|
|
617
659
|
</span>
|
|
618
660
|
{#if two || r.origins}
|
|
619
661
|
<span class="text-muted-foreground flex min-w-0 items-center gap-2 text-xs">
|
|
@@ -623,7 +665,7 @@
|
|
|
623
665
|
{/if}
|
|
624
666
|
</span>
|
|
625
667
|
<span class="pointer-events-none flex flex-col items-end justify-center gap-0.5 tabular-nums">
|
|
626
|
-
<span class={cn(
|
|
668
|
+
<span class={cn(isIncome(r) && 'text-status-success')} data-slot="ledger-amount"
|
|
627
669
|
>{money(r.amount)}</span
|
|
628
670
|
>
|
|
629
671
|
{#if balanceOn && balanceText(r)}<span class="text-muted-foreground text-xs"
|
|
@@ -643,7 +685,6 @@
|
|
|
643
685
|
data-slot="ledger-row"
|
|
644
686
|
>
|
|
645
687
|
<span class="relative flex items-center justify-center" style="grid-column: 1; grid-row: 1;">
|
|
646
|
-
{@render dot(r, selectable)}
|
|
647
688
|
{#if selectable}
|
|
648
689
|
<Checkbox
|
|
649
690
|
checked={on}
|
|
@@ -732,7 +773,10 @@
|
|
|
732
773
|
<span class="min-w-0 flex-1 truncate">{metaText}</span>
|
|
733
774
|
<span class={cn('tabular-nums', netTone)}>{net}</span>
|
|
734
775
|
{:else}
|
|
735
|
-
<span
|
|
776
|
+
<span
|
|
777
|
+
class={cn('grid place-items-center', selectable && 'justify-items-start pl-[5px]')}
|
|
778
|
+
style="grid-column: 1;"
|
|
779
|
+
>
|
|
736
780
|
{#if !BY_DAY}<ChevronRight
|
|
737
781
|
class={cn('size-3.5 transition-transform', isOpen && 'rotate-90')}
|
|
738
782
|
/>{/if}
|
|
@@ -776,7 +820,7 @@
|
|
|
776
820
|
<span class="text-sm font-semibold whitespace-nowrap">{number(tickedRows.length)} ticked</span
|
|
777
821
|
>
|
|
778
822
|
<span class="text-muted-foreground mr-2 text-xs whitespace-nowrap tabular-nums">
|
|
779
|
-
net {money(tickedRows.reduce((s, r) => s + cents(r.amount), 0) / 100)}
|
|
823
|
+
net {money(tickedRows.reduce((s, r) => s + (r.conversion ? 0 : cents(r.amount)), 0) / 100)}
|
|
780
824
|
</span>
|
|
781
825
|
{@render bulkActions?.(tickedRows)}
|
|
782
826
|
<span class="flex-1"></span>
|
|
@@ -829,7 +873,23 @@
|
|
|
829
873
|
style:grid-template-columns={template}
|
|
830
874
|
data-slot="ledger-head"
|
|
831
875
|
>
|
|
832
|
-
<span class="grid place-items-center">
|
|
876
|
+
<span class="relative grid place-items-center">
|
|
877
|
+
{#if grouped && !BY_DAY && groups.length}
|
|
878
|
+
<button
|
|
879
|
+
type="button"
|
|
880
|
+
class={cn(
|
|
881
|
+
'text-muted-foreground hover:text-foreground absolute top-1/2 z-10 grid size-4 -translate-y-1/2 cursor-pointer place-items-center rounded-md',
|
|
882
|
+
selectable ? 'left-1' : 'left-1/2 -translate-x-1/2',
|
|
883
|
+
RING
|
|
884
|
+
)}
|
|
885
|
+
aria-label={allOpen ? 'Collapse every group' : 'Expand every group'}
|
|
886
|
+
aria-expanded={allOpen}
|
|
887
|
+
onclick={toggleAll}
|
|
888
|
+
data-slot="ledger-all-groups"
|
|
889
|
+
>
|
|
890
|
+
<ChevronRight class={cn('size-3.5 transition-transform', allOpen && 'rotate-90')} />
|
|
891
|
+
</button>
|
|
892
|
+
{/if}
|
|
833
893
|
{#if selectable}
|
|
834
894
|
<Checkbox
|
|
835
895
|
checked={allTicked}
|
|
@@ -844,15 +904,46 @@
|
|
|
844
904
|
{/if}
|
|
845
905
|
</span>
|
|
846
906
|
{#each shown as c (c.key)}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
907
|
+
{@const sortable = !!sorter(c)}
|
|
908
|
+
{@const mine = activeSort?.key === c.key ? activeSort : null}
|
|
909
|
+
{@const label = c.head ?? c.label}
|
|
910
|
+
{@const headClass = cn(
|
|
911
|
+
CELL,
|
|
912
|
+
'text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase',
|
|
913
|
+
c.align === 'end' && END
|
|
914
|
+
)}
|
|
915
|
+
{#if sortable}
|
|
916
|
+
<button
|
|
917
|
+
type="button"
|
|
918
|
+
class={cn(
|
|
919
|
+
headClass,
|
|
920
|
+
'group flex cursor-pointer items-center gap-0.5 rounded-md hover:text-foreground',
|
|
921
|
+
RING_INSET,
|
|
922
|
+
c.align === 'end' && 'justify-end',
|
|
923
|
+
mine && 'text-foreground'
|
|
924
|
+
)}
|
|
925
|
+
aria-label={mine
|
|
926
|
+
? `Sorted by ${c.label}, ${mine.dir === -1 ? 'descending' : 'ascending'}. Click to reverse`
|
|
927
|
+
: `Sort by ${c.label}`}
|
|
928
|
+
onclick={() => sortBy(c.key)}
|
|
929
|
+
data-slot="ledger-sort"
|
|
930
|
+
data-sort={mine ? (mine.dir === -1 ? 'desc' : 'asc') : undefined}
|
|
931
|
+
>
|
|
932
|
+
<span class="truncate">{label}</span>
|
|
933
|
+
{#if mine?.dir === 1}
|
|
934
|
+
<ChevronUp class="size-3 flex-none" />
|
|
935
|
+
{:else}
|
|
936
|
+
<ChevronDown
|
|
937
|
+
class={cn(
|
|
938
|
+
'size-3 flex-none',
|
|
939
|
+
!mine && 'opacity-0 group-hover:opacity-50 group-focus-visible:opacity-50'
|
|
940
|
+
)}
|
|
941
|
+
/>
|
|
942
|
+
{/if}
|
|
943
|
+
</button>
|
|
944
|
+
{:else}
|
|
945
|
+
<span class={headClass}>{label}</span>
|
|
946
|
+
{/if}
|
|
856
947
|
{/each}
|
|
857
948
|
</div>
|
|
858
949
|
</div>
|
|
@@ -25,8 +25,17 @@ export interface LedgerRow {
|
|
|
25
25
|
amount: number | string;
|
|
26
26
|
/** What the running balance counts after this row: money, a coin, units. */
|
|
27
27
|
balance?: number | string | null;
|
|
28
|
-
/** A
|
|
28
|
+
/** A chip beside the title, on line one. `true` is a warning. */
|
|
29
29
|
review?: boolean | Status;
|
|
30
|
+
/** The review chip's words, in the module's own voice. Defaults to "Review". */
|
|
31
|
+
reviewLabel?: string;
|
|
32
|
+
/**
|
|
33
|
+
* The amount is a conversion inside the account (dollars swapped for a
|
|
34
|
+
* coin, so what the account is worth does not change). It shows plain,
|
|
35
|
+
* neither income green nor the muted outgoing tone, and counts in no
|
|
36
|
+
* group's in, out or net, nor the ticked rows' net.
|
|
37
|
+
*/
|
|
38
|
+
conversion?: boolean;
|
|
30
39
|
/** The attachment's name: a paperclip after the title, the name on hover. */
|
|
31
40
|
attachment?: string | null;
|
|
32
41
|
/** A row not as printed: the statement lines it was made from. */
|
|
@@ -55,6 +64,13 @@ export interface LedgerColumn<R extends LedgerRow = LedgerRow> {
|
|
|
55
64
|
align?: 'start' | 'end';
|
|
56
65
|
/** Ticked until the viewer chooses. Defaults to true, and to false for `balance`. */
|
|
57
66
|
on?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* What a click on the head sorts this column by: the real value, a number
|
|
69
|
+
* rather than its display text. Without it a column with `text` sorts by
|
|
70
|
+
* that text and a `cell`-only column does not sort. The running balance
|
|
71
|
+
* never sorts, since it depends on the order.
|
|
72
|
+
*/
|
|
73
|
+
sort?: (row: R) => number | string | null | undefined;
|
|
58
74
|
/** A plain cell. */
|
|
59
75
|
text?: (row: R) => string;
|
|
60
76
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.2",
|
|
4
4
|
"description": "Household shared component layer: shadcn-svelte primitives (bits-ui) plus the composed page chrome every app builds its routes from, restyled by each app's @poodle64/design-tokens alias layer. One fix reaches every app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @poodle64/ui — situation → component map
|
|
2
2
|
|
|
3
3
|
<!-- GENERATED by scripts/generate-registry.mjs from scripts/situations.json + package source. DO NOT EDIT. -->
|
|
4
|
-
Generated from `@poodle64/ui@2026.10.
|
|
4
|
+
Generated from `@poodle64/ui@2026.10.2`. 62 components, 18 situations.
|
|
5
5
|
|
|
6
6
|
**Read this before writing a `<div>`.** Find the SITUATION you are in below, then compose the component named for it — do not hand-build it from raw `Card` or utility classes. Import is `import { Name } from '<import path>'`. Props marked `?` are optional. This map is the retrieval step the [`frontend-design` skill] makes mandatory; the [CHI 2026 study] measured composing-from-a-registry at 95% design-system compliance against 71% for writing the CSS from a prose style guide.
|
|
7
7
|
|