@poodle64/ui 2026.10.4 → 2026.10.5

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.
Files changed (46) hide show
  1. package/README.md +100 -4
  2. package/dist/components/ui/context-column/context-column.svelte +118 -21
  3. package/dist/components/ui/context-column/context-column.svelte.d.ts +24 -1
  4. package/dist/components/ui/document-pane/document-pane.svelte +136 -0
  5. package/dist/components/ui/document-pane/document-pane.svelte.d.ts +43 -0
  6. package/dist/components/ui/document-pane/index.d.ts +2 -0
  7. package/dist/components/ui/document-pane/index.js +2 -0
  8. package/dist/components/ui/ledger/head-cell.svelte +73 -0
  9. package/dist/components/ui/ledger/head-cell.svelte.d.ts +19 -0
  10. package/dist/components/ui/ledger/index.d.ts +1 -1
  11. package/dist/components/ui/ledger/ledger-columns.svelte +29 -21
  12. package/dist/components/ui/ledger/ledger-columns.svelte.d.ts +50 -13
  13. package/dist/components/ui/ledger/ledger.d.ts +6 -0
  14. package/dist/components/ui/ledger/ledger.js +9 -1
  15. package/dist/components/ui/ledger/ledger.svelte +58 -78
  16. package/dist/components/ui/ledger/ledger.svelte.d.ts +6 -1
  17. package/dist/components/ui/ledger/look.d.ts +32 -0
  18. package/dist/components/ui/ledger/look.js +32 -0
  19. package/dist/components/ui/ledger/types.d.ts +21 -11
  20. package/dist/components/ui/list-toolbar/icon-action.svelte +42 -0
  21. package/dist/components/ui/list-toolbar/icon-action.svelte.d.ts +7 -0
  22. package/dist/components/ui/list-toolbar/index.d.ts +3 -0
  23. package/dist/components/ui/list-toolbar/index.js +2 -0
  24. package/dist/components/ui/list-toolbar/list-toolbar.svelte +85 -0
  25. package/dist/components/ui/list-toolbar/list-toolbar.svelte.d.ts +32 -0
  26. package/dist/components/ui/list-toolbar/types.d.ts +18 -0
  27. package/dist/components/ui/list-toolbar/types.js +1 -0
  28. package/dist/components/ui/panel/panel.svelte +8 -1
  29. package/dist/components/ui/panel/panel.svelte.d.ts +6 -0
  30. package/dist/components/ui/record-list/index.d.ts +3 -0
  31. package/dist/components/ui/record-list/index.js +2 -0
  32. package/dist/components/ui/record-list/record-list.svelte +489 -0
  33. package/dist/components/ui/record-list/record-list.svelte.d.ts +89 -0
  34. package/dist/components/ui/record-list/thumbnail.svelte +32 -0
  35. package/dist/components/ui/record-list/thumbnail.svelte.d.ts +14 -0
  36. package/dist/components/ui/record-list/types.d.ts +40 -0
  37. package/dist/components/ui/record-list/types.js +1 -0
  38. package/dist/components/ui/record-switcher/index.d.ts +3 -0
  39. package/dist/components/ui/record-switcher/index.js +2 -0
  40. package/dist/components/ui/record-switcher/record-switcher.svelte +167 -0
  41. package/dist/components/ui/record-switcher/record-switcher.svelte.d.ts +28 -0
  42. package/dist/components/ui/record-switcher/types.d.ts +22 -0
  43. package/dist/components/ui/record-switcher/types.js +1 -0
  44. package/package.json +1 -1
  45. package/registry/component-map.json +34 -6
  46. package/registry/component-map.md +8 -4
@@ -1,8 +1,9 @@
1
- <script lang="ts">
1
+ <script lang="ts" generics="P extends { columns: string[]; period?: LedgerPeriod }">
2
2
  /**
3
3
  * The viewer's Columns menu: every column the module offers as a tick (the
4
- * running balance one more, always last), then the period rows group by.
5
- * An icon button named by its tooltip.
4
+ * ledger's running balance one more, always last), then, where the list
5
+ * groups by a period, the period. An icon button named by its tooltip.
6
+ * The RecordList's menu is this one without the period.
6
7
  */
7
8
  import Columns3 from '@lucide/svelte/icons/columns-3';
8
9
  import { Button, buttonVariants } from '../button/index.js';
@@ -12,20 +13,23 @@
12
13
  import * as Tooltip from '../tooltip/index.js';
13
14
  import { cn } from '../../../utils.js';
14
15
  import { LEDGER_PERIODS } from './ledger.js';
15
- import type { LedgerPeriod, LedgerPreferences } from './types.js';
16
+ import type { LedgerPeriod } from './types.js';
16
17
 
17
18
  let {
18
19
  offered,
19
20
  preferences,
20
21
  note,
21
22
  onchange,
22
- onreset
23
+ onreset,
24
+ owner = 'ledger'
23
25
  }: {
24
26
  offered: { key: string; label: string; locked: boolean }[];
25
- preferences: LedgerPreferences;
27
+ preferences: P;
26
28
  note?: string;
27
- onchange: (next: LedgerPreferences) => void;
29
+ onchange: (next: P) => void;
28
30
  onreset: () => void;
31
+ /** The `data-slot` prefix: the list that owns the menu. */
32
+ owner?: string;
29
33
  } = $props();
30
34
 
31
35
  let open = $state(false);
@@ -48,6 +52,8 @@
48
52
  }
49
53
 
50
54
  const eyebrow = 'text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase';
55
+ /** The ledger's menu also holds its running balance and its grouping. */
56
+ const name = $derived(preferences.period ? 'Columns, balance and grouping' : 'Columns');
51
57
  </script>
52
58
 
53
59
  <Popover.Root bind:open>
@@ -58,17 +64,17 @@
58
64
  <Popover.Trigger
59
65
  {...props}
60
66
  class={buttonVariants({ variant: open ? 'outline' : 'ghost', size: 'icon-sm' })}
61
- aria-label="Columns, balance and grouping"
62
- data-slot="ledger-columns-trigger"
67
+ aria-label={name}
68
+ data-slot="{owner}-columns-trigger"
63
69
  >
64
70
  <Columns3 />
65
71
  </Popover.Trigger>
66
72
  {/snippet}
67
73
  </Tooltip.Trigger>
68
- <Tooltip.Content side="bottom">Columns, balance and grouping</Tooltip.Content>
74
+ <Tooltip.Content side="bottom">{name}</Tooltip.Content>
69
75
  </Tooltip.Root>
70
76
  </Tooltip.Provider>
71
- <Popover.Content align="end" class="w-80 p-0" data-slot="ledger-columns">
77
+ <Popover.Content align="end" class="w-80 p-0" data-slot="{owner}-columns">
72
78
  <div class="flex flex-col px-2 pt-2.5 pb-2">
73
79
  <span class={cn(eyebrow, 'px-1.5 pb-1.5')}>Columns</span>
74
80
  {#each offered as c (c.key)}
@@ -89,16 +95,18 @@
89
95
  </label>
90
96
  {/each}
91
97
  </div>
92
- <div class="border-border flex flex-col gap-2 border-t px-3.5 pt-2.5 pb-3">
93
- <span class={eyebrow}>Group by</span>
94
- <Segmented
95
- label="Group by"
96
- options={LEDGER_PERIODS}
97
- bind:value={() => preferences.period, (period) => onchange({ ...preferences, period })}
98
- class="grid w-full grid-cols-2"
99
- />
100
- <span class="text-muted-foreground text-xs">{HINT[preferences.period]}</span>
101
- </div>
98
+ {#if preferences.period}
99
+ <div class="border-border flex flex-col gap-2 border-t px-3.5 pt-2.5 pb-3">
100
+ <span class={eyebrow}>Group by</span>
101
+ <Segmented
102
+ label="Group by"
103
+ options={LEDGER_PERIODS}
104
+ bind:value={() => preferences.period!, (period) => onchange({ ...preferences, period })}
105
+ class="grid w-full grid-cols-2"
106
+ />
107
+ <span class="text-muted-foreground text-xs">{HINT[preferences.period]}</span>
108
+ </div>
109
+ {/if}
102
110
  <div class="bg-surface-2 border-border flex items-center gap-2 border-t py-2 pr-2 pl-3.5">
103
111
  <span class="text-muted-foreground min-w-0 flex-1 text-xs">{note ?? ''}</span>
104
112
  <Button variant="ghost" size="sm" onclick={onreset}>Reset</Button>
@@ -1,15 +1,52 @@
1
- import type { LedgerPreferences } from './types.js';
2
- type $$ComponentProps = {
3
- offered: {
4
- key: string;
5
- label: string;
6
- locked: boolean;
7
- }[];
8
- preferences: LedgerPreferences;
9
- note?: string;
10
- onchange: (next: LedgerPreferences) => void;
11
- onreset: () => void;
1
+ import type { LedgerPeriod } from './types.js';
2
+ declare function $$render<P extends {
3
+ columns: string[];
4
+ period?: LedgerPeriod;
5
+ }>(): {
6
+ props: {
7
+ offered: {
8
+ key: string;
9
+ label: string;
10
+ locked: boolean;
11
+ }[];
12
+ preferences: P;
13
+ note?: string;
14
+ onchange: (next: P) => void;
15
+ onreset: () => void;
16
+ /** The `data-slot` prefix: the list that owns the menu. */
17
+ owner?: string;
18
+ };
19
+ exports: {};
20
+ bindings: "";
21
+ slots: {};
22
+ events: {};
12
23
  };
13
- declare const LedgerColumns: import("svelte").Component<$$ComponentProps, {}, "">;
14
- type LedgerColumns = ReturnType<typeof LedgerColumns>;
24
+ declare class __sveltets_Render<P extends {
25
+ columns: string[];
26
+ period?: LedgerPeriod;
27
+ }> {
28
+ props(): ReturnType<typeof $$render<P>>['props'];
29
+ events(): ReturnType<typeof $$render<P>>['events'];
30
+ slots(): ReturnType<typeof $$render<P>>['slots'];
31
+ bindings(): "";
32
+ exports(): {};
33
+ }
34
+ interface $$IsomorphicComponent {
35
+ new <P extends {
36
+ columns: string[];
37
+ period?: LedgerPeriod;
38
+ }>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<P>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<P>['props']>, ReturnType<__sveltets_Render<P>['events']>, ReturnType<__sveltets_Render<P>['slots']>> & {
39
+ $$bindings?: ReturnType<__sveltets_Render<P>['bindings']>;
40
+ } & ReturnType<__sveltets_Render<P>['exports']>;
41
+ <P extends {
42
+ columns: string[];
43
+ period?: LedgerPeriod;
44
+ }>(internal: unknown, props: ReturnType<__sveltets_Render<P>['props']> & {}): ReturnType<__sveltets_Render<P>['exports']>;
45
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
46
+ }
47
+ declare const LedgerColumns: $$IsomorphicComponent;
48
+ type LedgerColumns<P extends {
49
+ columns: string[];
50
+ period?: LedgerPeriod;
51
+ }> = InstanceType<typeof LedgerColumns<P>>;
15
52
  export default LedgerColumns;
@@ -21,6 +21,12 @@ export declare function groupRows<R extends LedgerRow>(rows: readonly R[], perio
21
21
  * rows without a value come last either way, and equal values keep newest first.
22
22
  */
23
23
  export declare function sortRows<R extends LedgerRow>(rows: readonly R[], value: (row: R) => number | string | null | undefined, dir: 1 | -1): R[];
24
+ /**
25
+ * Any list's rows ordered by a value, `dir` 1 ascending and -1 descending:
26
+ * numbers as numbers, text with its numerals read as numbers. Rows without a
27
+ * value come last either way, and equal values keep the order they came in.
28
+ */
29
+ export declare function orderBy<R>(rows: readonly R[], value: (row: R) => number | string | null | undefined, dir: 1 | -1): R[];
24
30
  /** Whole cents, so a group's sum carries no float error. */
25
31
  export declare function cents(value: number | string | null | undefined): number;
26
32
  /** Money as the ledger prints it: in plain, out in brackets. */
@@ -127,7 +127,15 @@ export function groupRows(rows, period, fyStart = 7) {
127
127
  * rows without a value come last either way, and equal values keep newest first.
128
128
  */
129
129
  export function sortRows(rows, value, dir) {
130
- const keyed = (groupRows(rows, 'none')[0]?.rows ?? []).map((row) => ({ row, v: value(row) }));
130
+ return orderBy(groupRows(rows, 'none')[0]?.rows ?? [], value, dir);
131
+ }
132
+ /**
133
+ * Any list's rows ordered by a value, `dir` 1 ascending and -1 descending:
134
+ * numbers as numbers, text with its numerals read as numbers. Rows without a
135
+ * value come last either way, and equal values keep the order they came in.
136
+ */
137
+ export function orderBy(rows, value, dir) {
138
+ const keyed = rows.map((row) => ({ row, v: value(row) }));
131
139
  const missing = (v) => v === null || v === undefined || v === '';
132
140
  keyed.sort((a, b) => {
133
141
  if (missing(a.v) || missing(b.v))
@@ -18,14 +18,32 @@
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';
22
21
  import Paperclip from '@lucide/svelte/icons/paperclip';
23
22
  import X from '@lucide/svelte/icons/x';
24
23
  import Button from '../button/button.svelte';
25
24
  import { Checkbox } from '../checkbox/index.js';
26
25
  import EmptyState from '../empty-state/empty-state.svelte';
27
26
  import StatusBadge from '../status-badge/status-badge.svelte';
27
+ import ListToolbar from '../list-toolbar/list-toolbar.svelte';
28
+ import type { ListAction, ListIconAction } from '../list-toolbar/types.js';
29
+ import HeadCell from './head-cell.svelte';
28
30
  import LedgerColumns from './ledger-columns.svelte';
31
+ import {
32
+ CARD,
33
+ CELL,
34
+ END,
35
+ GROUP,
36
+ GROUP_LABEL,
37
+ HEAD,
38
+ HEAD_GROUND,
39
+ RING,
40
+ RING_INSET,
41
+ ROW_HEIGHT,
42
+ ROW_TEXT,
43
+ RULE,
44
+ THROUGH,
45
+ groupSpacing
46
+ } from './look.js';
29
47
  import { AU_LOCALE } from '../../../format.js';
30
48
  import { cn } from '../../../utils.js';
31
49
  import {
@@ -58,6 +76,8 @@
58
76
  title,
59
77
  meta,
60
78
  actions,
79
+ tools = [],
80
+ action,
61
81
  selected = $bindable([]),
62
82
  bulkActions,
63
83
  open = $bindable(null),
@@ -89,8 +109,12 @@
89
109
  title?: string;
90
110
  /** Beside the title. Defaults to the row count. */
91
111
  meta?: string;
92
- /** The toolbar's own actions, after the Columns menu: export, add. */
112
+ /** The toolbar's own controls, after the Columns menu and before `tools`. */
93
113
  actions?: Snippet;
114
+ /** The toolbar's icon actions, each named by its tooltip: export, print. */
115
+ tools?: ListIconAction[];
116
+ /** The toolbar's one labelled action, last: add. */
117
+ action?: ListAction;
94
118
  /** The ticked rows' ids. Ticking needs `bulkActions`. */
95
119
  selected?: Id[];
96
120
  /** What the ticked rows allow, in the bar that replaces the toolbar. */
@@ -135,21 +159,6 @@
135
159
  /** Ticking adds a checkbox to the gutter, so it widens to hold the chevron beside it. */
136
160
  const GUTTER = '2.5rem';
137
161
  const GUTTER_TICKS = '3.5rem';
138
- const CELL = 'min-w-0 truncate px-2';
139
- const END = 'pr-5 text-right';
140
- /** The row's height rides the package's density ramp: 3.5rem, 3rem compact. */
141
- const ROW_HEIGHT = 'min-height: calc(var(--ds-control-height-md) + 1rem);';
142
- /** The house focus ring, and the same ring drawn inside a control that fills a clipped card. */
143
- const RING = 'focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-none';
144
- const RING_INSET =
145
- 'focus-visible:inset-ring-ring/50 focus-visible:inset-ring-3 focus-visible:outline-none';
146
- /**
147
- * A cell over an openable row lets a click through to the row beneath it, and
148
- * gives one back to any control a module's cell renders, so a link or a button
149
- * in a cell is never silently inert.
150
- */
151
- const THROUGH =
152
- 'pointer-events-none [&_:is(a,button,input,select,textarea,label,summary,[role=button],[role=link],[role=checkbox],[tabindex])]:pointer-events-auto';
153
162
 
154
163
  // ── Columns: the ledger's own three, the module's, the balance last ──
155
164
  const resolved = $derived.by(() => {
@@ -177,7 +186,9 @@
177
186
  });
178
187
 
179
188
  const defaults = $derived<LedgerPreferences>({
180
- columns: resolved.filter((c) => !c.locked && c.on).map((c) => c.key),
189
+ columns: resolved
190
+ .filter((c) => !c.locked && (c.on === true || (c.on === 'wide' && WIDE)))
191
+ .map((c) => c.key),
181
192
  period: 'month'
182
193
  });
183
194
  /** What the consumer kept, read field by field: a stale or foreign key falls back. */
@@ -638,7 +649,7 @@
638
649
  <div
639
650
  class={cn(
640
651
  'relative grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 px-3 py-2 text-sm',
641
- !first && 'border-border/60 border-t',
652
+ !first && RULE,
642
653
  openable && 'hover:bg-accent/40'
643
654
  )}
644
655
  style={ROW_HEIGHT}
@@ -677,8 +688,9 @@
677
688
  {:else}
678
689
  <div
679
690
  class={cn(
680
- 'relative grid items-stretch text-[0.8125rem]',
681
- !first && 'border-border/60 border-t',
691
+ 'relative grid items-stretch',
692
+ ROW_TEXT,
693
+ !first && RULE,
682
694
  on && 'bg-primary/9',
683
695
  openable && !on && 'hover:bg-accent/40'
684
696
  )}
@@ -726,12 +738,13 @@
726
738
  {@const net = single ? '' : money(t.net)}
727
739
  {@const netTone = t.net > 0 ? 'text-status-success' : 'text-muted-foreground'}
728
740
  {@const klass = cn(
729
- 'text-muted-foreground h-6.5 w-full text-left text-xs',
741
+ GROUP,
742
+ 'w-full text-left',
730
743
  PHONE ? 'flex items-center gap-2 px-0.5' : 'grid items-center px-px'
731
744
  )}
732
745
  <!-- The spacing is padding, not margin, so the measured height is the height it takes. -->
733
746
  <div
734
- class={cn('pb-1.5', gi > 0 && 'pt-4.5')}
747
+ class={groupSpacing(gi)}
735
748
  {@attach measure(`h:${key}`, gi > 0 ? 'head' : undefined)}
736
749
  >
737
750
  <!-- A day heads its rows and does not close; any other period's label opens and closes it. -->
@@ -770,7 +783,7 @@
770
783
  {#if !BY_DAY}<ChevronRight
771
784
  class={cn('size-3.5 flex-none transition-transform', isOpen && 'rotate-90')}
772
785
  />{/if}
773
- <span class="text-[0.8125rem] font-semibold whitespace-nowrap">{label}</span>
786
+ <span class={cn(GROUP_LABEL, 'whitespace-nowrap')}>{label}</span>
774
787
  <span class="min-w-0 flex-1 truncate">{metaText}</span>
775
788
  <span class={cn('tabular-nums', netTone)}>{net}</span>
776
789
  {:else}
@@ -786,7 +799,7 @@
786
799
  class="flex min-w-0 items-baseline gap-3 overflow-hidden px-2 whitespace-nowrap"
787
800
  style="grid-column: 2 / {cAmount};"
788
801
  >
789
- <span class="text-[0.8125rem] font-semibold">{label}</span>
802
+ <span class={GROUP_LABEL}>{label}</span>
790
803
  <span class="min-w-0 truncate">{metaText}</span>
791
804
  </span>
792
805
  <span class={cn(CELL, END, 'tabular-nums', netTone)} style="grid-column: {cAmount};">{net}</span
@@ -835,18 +848,15 @@
835
848
  </Button>
836
849
  </div>
837
850
  {:else}
838
- <div
839
- role="toolbar"
840
- aria-label="This ledger"
841
- class="flex min-h-11 flex-none items-center gap-2 px-2 pb-2"
842
- style:padding-right={edge}
851
+ <ListToolbar
852
+ {title}
853
+ meta={meta ?? count(rows.length)}
854
+ {tools}
855
+ {action}
856
+ label="This ledger"
857
+ style="padding-right: {edge};"
843
858
  data-slot="ledger-toolbar"
844
859
  >
845
- <span class="flex min-w-0 items-baseline gap-2.5 overflow-hidden whitespace-nowrap">
846
- {#if title}<span class="text-body flex-none font-semibold">{title}</span>{/if}
847
- <span class="text-muted-foreground truncate text-sm">{meta ?? count(rows.length)}</span>
848
- </span>
849
- <span class="min-w-0 flex-1"></span>
850
860
  <LedgerColumns
851
861
  offered={resolved.map((c) => ({ key: c.key, label: c.label, locked: c.locked }))}
852
862
  preferences={prefs}
@@ -855,7 +865,7 @@
855
865
  onreset={() => setPreferences(defaults)}
856
866
  />
857
867
  {@render actions?.()}
858
- </div>
868
+ </ListToolbar>
859
869
  {/if}
860
870
 
861
871
  <div
@@ -868,9 +878,9 @@
868
878
  data-slot="ledger-rows"
869
879
  >
870
880
  {#if !PHONE}
871
- <div class="bg-background sticky top-0 z-3 pb-3" bind:offsetHeight={headHeight}>
881
+ <div class={HEAD_GROUND} bind:offsetHeight={headHeight}>
872
882
  <div
873
- class="bg-card border-border grid h-9 items-center rounded-lg border"
883
+ class={HEAD}
874
884
  style:grid-template-columns={template}
875
885
  data-slot="ledger-head"
876
886
  >
@@ -905,46 +915,16 @@
905
915
  {/if}
906
916
  </span>
907
917
  {#each shown as c (c.key)}
908
- {@const sortable = !!sorter(c)}
909
918
  {@const mine = activeSort?.key === c.key ? activeSort : null}
910
- {@const label = c.head ?? c.label}
911
- {@const headClass = cn(
912
- CELL,
913
- 'text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase',
914
- c.align === 'end' && END
915
- )}
916
- {#if sortable}
917
- <button
918
- type="button"
919
- class={cn(
920
- headClass,
921
- 'group flex cursor-pointer items-center gap-0.5 rounded-md hover:text-foreground',
922
- RING_INSET,
923
- c.align === 'end' && 'justify-end',
924
- mine && 'text-foreground'
925
- )}
926
- aria-label={mine
927
- ? `Sorted by ${c.label}, ${mine.dir === -1 ? 'descending' : 'ascending'}. Click ${mine.dir === -1 ? 'to reverse' : 'for the default order'}`
928
- : `Sort by ${c.label}`}
929
- onclick={() => sortBy(c.key)}
930
- data-slot="ledger-sort"
931
- data-sort={mine ? (mine.dir === -1 ? 'desc' : 'asc') : undefined}
932
- >
933
- <span class="truncate">{label}</span>
934
- {#if mine?.dir === 1}
935
- <ChevronUp class="size-3 flex-none" />
936
- {:else}
937
- <ChevronDown
938
- class={cn(
939
- 'size-3 flex-none',
940
- !mine && 'opacity-0 group-hover:opacity-50 group-focus-visible:opacity-50'
941
- )}
942
- />
943
- {/if}
944
- </button>
945
- {:else}
946
- <span class={headClass}>{label}</span>
947
- {/if}
919
+ <HeadCell
920
+ label={c.head ?? c.label}
921
+ name={c.label}
922
+ align={c.align}
923
+ sortable={!!sorter(c)}
924
+ sort={mine ? (mine.dir === -1 ? 'desc' : 'asc') : null}
925
+ onsort={() => sortBy(c.key)}
926
+ owner="ledger"
927
+ />
948
928
  {/each}
949
929
  </div>
950
930
  </div>
@@ -974,7 +954,7 @@
974
954
  <!-- A card the window cuts keeps no edge at the cut, which is out of view. -->
975
955
  <div
976
956
  class={cn(
977
- 'bg-card border-border overflow-hidden rounded-lg border',
957
+ CARD,
978
958
  block.cutTop && 'rounded-t-none border-t-0',
979
959
  block.cutBottom && 'rounded-b-none border-b-0'
980
960
  )}
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { Snippet } from 'svelte';
14
14
  import type { HTMLAttributes } from 'svelte/elements';
15
+ import type { ListAction, ListIconAction } from '../list-toolbar/types.js';
15
16
  import type { LedgerColumn, LedgerOpenContext, LedgerPreferences, LedgerRow } from './types.js';
16
17
  declare function $$render<R extends LedgerRow>(): {
17
18
  props: Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
@@ -33,8 +34,12 @@ declare function $$render<R extends LedgerRow>(): {
33
34
  title?: string;
34
35
  /** Beside the title. Defaults to the row count. */
35
36
  meta?: string;
36
- /** The toolbar's own actions, after the Columns menu: export, add. */
37
+ /** The toolbar's own controls, after the Columns menu and before `tools`. */
37
38
  actions?: Snippet;
39
+ /** The toolbar's icon actions, each named by its tooltip: export, print. */
40
+ tools?: ListIconAction[];
41
+ /** The toolbar's one labelled action, last: add. */
42
+ action?: ListAction;
38
43
  /** The ticked rows' ids. Ticking needs `bulkActions`. */
39
44
  selected?: R["id"][];
40
45
  /** What the ticked rows allow, in the bar that replaces the toolbar. */
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The ledger's look, held once for every list that wears it: the Ledger and
3
+ * the RecordList. A head card, row cards with hairlines between rows, and each
4
+ * group's label on the page ground.
5
+ */
6
+ export declare const CELL = "min-w-0 truncate px-2";
7
+ export declare const END = "pr-5 text-right";
8
+ /** The row's height rides the package's density ramp: 3.5rem, 3rem compact. */
9
+ export declare const ROW_HEIGHT = "min-height: calc(var(--ds-control-height-md) + 1rem);";
10
+ /** The row text's size, between the body and the small print. */
11
+ export declare const ROW_TEXT = "text-[0.8125rem]";
12
+ /** The hairline between two rows of one card. */
13
+ export declare const RULE = "border-border/60 border-t";
14
+ /** The card that holds a group's rows. */
15
+ export declare const CARD = "bg-card border-border overflow-hidden rounded-lg border";
16
+ /** The head card, and the ground behind it that hides the rows scrolling under it. */
17
+ export declare const HEAD_GROUND = "bg-background sticky top-0 z-3 pb-3";
18
+ export declare const HEAD = "bg-card border-border grid h-9 items-center rounded-lg border";
19
+ export declare const HEAD_TEXT = "text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase";
20
+ /** A group's label line on the ground; the spacing is padding, so a measured height is the height it takes. */
21
+ export declare const GROUP = "text-muted-foreground h-6.5 text-xs";
22
+ export declare const GROUP_LABEL = "text-[0.8125rem] font-semibold";
23
+ export declare const groupSpacing: (index: number) => "pt-4.5 pb-1.5" | "pb-1.5";
24
+ /** The house focus ring, and the same ring drawn inside a control that fills a clipped card. */
25
+ export declare const RING = "focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-none";
26
+ export declare const RING_INSET = "focus-visible:inset-ring-ring/50 focus-visible:inset-ring-3 focus-visible:outline-none";
27
+ /**
28
+ * A cell over an openable row lets a click through to the row beneath it, and
29
+ * gives one back to any control a module's cell renders, so a link or a button
30
+ * in a cell is never silently inert.
31
+ */
32
+ export declare const THROUGH = "pointer-events-none [&_:is(a,button,input,select,textarea,label,summary,[role=button],[role=link],[role=checkbox],[tabindex])]:pointer-events-auto";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The ledger's look, held once for every list that wears it: the Ledger and
3
+ * the RecordList. A head card, row cards with hairlines between rows, and each
4
+ * group's label on the page ground.
5
+ */
6
+ export const CELL = 'min-w-0 truncate px-2';
7
+ export const END = 'pr-5 text-right';
8
+ /** The row's height rides the package's density ramp: 3.5rem, 3rem compact. */
9
+ export const ROW_HEIGHT = 'min-height: calc(var(--ds-control-height-md) + 1rem);';
10
+ /** The row text's size, between the body and the small print. */
11
+ export const ROW_TEXT = 'text-[0.8125rem]';
12
+ /** The hairline between two rows of one card. */
13
+ export const RULE = 'border-border/60 border-t';
14
+ /** The card that holds a group's rows. */
15
+ export const CARD = 'bg-card border-border overflow-hidden rounded-lg border';
16
+ /** The head card, and the ground behind it that hides the rows scrolling under it. */
17
+ export const HEAD_GROUND = 'bg-background sticky top-0 z-3 pb-3';
18
+ export const HEAD = 'bg-card border-border grid h-9 items-center rounded-lg border';
19
+ export const HEAD_TEXT = 'text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase';
20
+ /** A group's label line on the ground; the spacing is padding, so a measured height is the height it takes. */
21
+ export const GROUP = 'text-muted-foreground h-6.5 text-xs';
22
+ export const GROUP_LABEL = 'text-[0.8125rem] font-semibold';
23
+ export const groupSpacing = (index) => (index > 0 ? 'pt-4.5 pb-1.5' : 'pb-1.5');
24
+ /** The house focus ring, and the same ring drawn inside a control that fills a clipped card. */
25
+ export const RING = 'focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-none';
26
+ export const RING_INSET = 'focus-visible:inset-ring-ring/50 focus-visible:inset-ring-3 focus-visible:outline-none';
27
+ /**
28
+ * A cell over an openable row lets a click through to the row beneath it, and
29
+ * gives one back to any control a module's cell renders, so a link or a button
30
+ * in a cell is never silently inert.
31
+ */
32
+ export const THROUGH = 'pointer-events-none [&_:is(a,button,input,select,textarea,label,summary,[role=button],[role=link],[role=checkbox],[tabindex])]:pointer-events-auto';
@@ -45,30 +45,31 @@ export interface LedgerRow {
45
45
  } | null;
46
46
  }
47
47
  /**
48
- * A column the module offers. The keys `date`, `title`, `amount` and `balance`
49
- * are the ledger's own cells: list one to rename or resize it (and, for
50
- * `balance`, to offer the running balance at all, with `text` saying what it
51
- * counts). Date, title and amount always show, first and in that order; the
52
- * balance is always last. Every other key is the module's, in its order.
48
+ * A column a module offers a list in the ledger's look: the Ledger, and the
49
+ * RecordList for rows that are not money.
53
50
  */
54
- export interface LedgerColumn<R extends LedgerRow = LedgerRow> {
51
+ export interface ListColumn<R> {
55
52
  key: string;
56
53
  /** Its name in the Columns menu, and its head unless `head` is shorter. */
57
54
  label: string;
58
55
  head?: string;
59
- /** A grid track, or one for a narrow ledger and one for a wide one. */
56
+ /** A grid track, or one for a narrow list and one for a wide one (1200px and up). */
60
57
  width?: string | {
61
58
  narrow: string;
62
59
  wide: string;
63
60
  };
64
61
  align?: 'start' | 'end';
65
- /** Ticked until the viewer chooses. Defaults to true, and to false for `balance`. */
66
- on?: boolean;
62
+ /**
63
+ * Ticked until the viewer chooses. Defaults to true. `'wide'` is ticked
64
+ * only on a wide list, so extra width shows more columns and the viewer
65
+ * can still tick it back on a narrow one.
66
+ */
67
+ on?: boolean | 'wide';
67
68
  /**
68
69
  * What a click on the head sorts this column by: the real value, a number
69
70
  * 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.
71
+ * that text and a `cell`-only column does not sort. The ledger's running
72
+ * balance never sorts, since it depends on the order.
72
73
  */
73
74
  sort?: (row: R) => number | string | null | undefined;
74
75
  /** A plain cell. */
@@ -79,6 +80,15 @@ export interface LedgerColumn<R extends LedgerRow = LedgerRow> {
79
80
  */
80
81
  cell?: Snippet<[R]>;
81
82
  }
83
+ /**
84
+ * A column the module offers the ledger. The keys `date`, `title`, `amount`
85
+ * and `balance` are the ledger's own cells: list one to rename or resize it
86
+ * (and, for `balance`, to offer the running balance at all, with `text` saying
87
+ * what it counts). Date, title and amount always show, first and in that
88
+ * order; the balance is always last, and off until ticked. Every other key is
89
+ * the module's, in its order.
90
+ */
91
+ export type LedgerColumn<R extends LedgerRow = LedgerRow> = ListColumn<R>;
82
92
  /** The viewer's own choices, which the consumer persists. */
83
93
  export interface LedgerPreferences {
84
94
  /** The ticked columns' keys, `balance` included. */
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ /** An icon button, or an icon link, named by its tooltip. Inside a `Tooltip.Provider`. */
3
+ import { mergeProps } from 'bits-ui';
4
+ import { buttonVariants } from '../button/index.js';
5
+ import * as Tooltip from '../tooltip/index.js';
6
+ import type { ListIconAction } from './types.js';
7
+
8
+ let { action }: { action: ListIconAction } = $props();
9
+ const Icon = $derived(action.icon);
10
+ const klass = buttonVariants({ variant: 'ghost', size: 'icon-sm' });
11
+
12
+ /** The trigger's own handlers (a click closes the tooltip) chained with the action's. */
13
+ const merged = (props: Record<string, unknown>) =>
14
+ mergeProps(props, { onclick: action.onclick, class: klass, 'aria-label': action.label });
15
+ /** A link takes no button `type`. */
16
+ function asLink(props: Record<string, unknown>) {
17
+ const { type: _type, ...rest } = merged(props);
18
+ return rest;
19
+ }
20
+ </script>
21
+
22
+ <Tooltip.Root>
23
+ <Tooltip.Trigger>
24
+ {#snippet child({ props })}
25
+ {#if action.href && !action.disabled}
26
+ <a
27
+ {...asLink(props)}
28
+ href={action.href}
29
+ download={action.download === true ? '' : action.download || undefined}
30
+ data-slot="list-action"
31
+ >
32
+ <Icon />
33
+ </a>
34
+ {:else}
35
+ <button {...merged(props)} type="button" disabled={action.disabled} data-slot="list-action">
36
+ <Icon />
37
+ </button>
38
+ {/if}
39
+ {/snippet}
40
+ </Tooltip.Trigger>
41
+ <Tooltip.Content side="bottom">{action.label}</Tooltip.Content>
42
+ </Tooltip.Root>
@@ -0,0 +1,7 @@
1
+ import type { ListIconAction } from './types.js';
2
+ type $$ComponentProps = {
3
+ action: ListIconAction;
4
+ };
5
+ declare const IconAction: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type IconAction = ReturnType<typeof IconAction>;
7
+ export default IconAction;
@@ -0,0 +1,3 @@
1
+ export { default as ListToolbar } from './list-toolbar.svelte';
2
+ export { default } from './list-toolbar.svelte';
3
+ export type { ListAction, ListIconAction } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { default as ListToolbar } from './list-toolbar.svelte';
2
+ export { default } from './list-toolbar.svelte';