@poodle64/ui 2026.10.4 → 2026.10.6
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 +101 -5
- package/dist/components/ui/context-column/context-column.svelte +118 -21
- package/dist/components/ui/context-column/context-column.svelte.d.ts +24 -1
- package/dist/components/ui/document-pane/document-pane.svelte +136 -0
- package/dist/components/ui/document-pane/document-pane.svelte.d.ts +43 -0
- package/dist/components/ui/document-pane/index.d.ts +2 -0
- package/dist/components/ui/document-pane/index.js +2 -0
- package/dist/components/ui/ledger/head-cell.svelte +73 -0
- package/dist/components/ui/ledger/head-cell.svelte.d.ts +19 -0
- package/dist/components/ui/ledger/index.d.ts +1 -1
- package/dist/components/ui/ledger/ledger-columns.svelte +29 -21
- package/dist/components/ui/ledger/ledger-columns.svelte.d.ts +50 -13
- package/dist/components/ui/ledger/ledger.d.ts +6 -0
- package/dist/components/ui/ledger/ledger.js +9 -1
- package/dist/components/ui/ledger/ledger.svelte +61 -88
- package/dist/components/ui/ledger/ledger.svelte.d.ts +7 -2
- package/dist/components/ui/ledger/look.d.ts +41 -0
- package/dist/components/ui/ledger/look.js +43 -0
- package/dist/components/ui/ledger/types.d.ts +21 -11
- package/dist/components/ui/list-toolbar/icon-action.svelte +42 -0
- package/dist/components/ui/list-toolbar/icon-action.svelte.d.ts +7 -0
- package/dist/components/ui/list-toolbar/index.d.ts +3 -0
- package/dist/components/ui/list-toolbar/index.js +2 -0
- package/dist/components/ui/list-toolbar/list-toolbar.svelte +85 -0
- package/dist/components/ui/list-toolbar/list-toolbar.svelte.d.ts +32 -0
- package/dist/components/ui/list-toolbar/types.d.ts +18 -0
- package/dist/components/ui/list-toolbar/types.js +1 -0
- package/dist/components/ui/panel/panel.svelte +8 -1
- package/dist/components/ui/panel/panel.svelte.d.ts +6 -0
- package/dist/components/ui/record-list/index.d.ts +3 -0
- package/dist/components/ui/record-list/index.js +2 -0
- package/dist/components/ui/record-list/record-list.svelte +482 -0
- package/dist/components/ui/record-list/record-list.svelte.d.ts +89 -0
- package/dist/components/ui/record-list/thumbnail.svelte +32 -0
- package/dist/components/ui/record-list/thumbnail.svelte.d.ts +14 -0
- package/dist/components/ui/record-list/types.d.ts +40 -0
- package/dist/components/ui/record-list/types.js +1 -0
- package/dist/components/ui/record-switcher/index.d.ts +3 -0
- package/dist/components/ui/record-switcher/index.js +2 -0
- package/dist/components/ui/record-switcher/record-switcher.svelte +167 -0
- package/dist/components/ui/record-switcher/record-switcher.svelte.d.ts +28 -0
- package/dist/components/ui/record-switcher/types.d.ts +22 -0
- package/dist/components/ui/record-switcher/types.js +1 -0
- package/package.json +1 -1
- package/registry/component-map.json +34 -6
- 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
|
|
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
|
|
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:
|
|
27
|
+
preferences: P;
|
|
26
28
|
note?: string;
|
|
27
|
-
onchange: (next:
|
|
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=
|
|
62
|
-
data-slot="
|
|
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">
|
|
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="
|
|
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
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
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))
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* It fills its parent's height and scrolls its own rows, so the head card
|
|
10
10
|
* stays at the top: give it a bounded parent (`flex-1 min-h-0` in a
|
|
11
|
-
* full-height page). Width picks the layout: a phone's list below
|
|
11
|
+
* full-height page). Width picks the layout: a phone's list below 480px, the
|
|
12
12
|
* wide column widths from 1200px.
|
|
13
13
|
*/
|
|
14
14
|
import type { Snippet } from 'svelte';
|
|
@@ -18,14 +18,33 @@
|
|
|
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
|
+
listLayout
|
|
47
|
+
} from './look.js';
|
|
29
48
|
import { AU_LOCALE } from '../../../format.js';
|
|
30
49
|
import { cn } from '../../../utils.js';
|
|
31
50
|
import {
|
|
@@ -58,6 +77,8 @@
|
|
|
58
77
|
title,
|
|
59
78
|
meta,
|
|
60
79
|
actions,
|
|
80
|
+
tools = [],
|
|
81
|
+
action,
|
|
61
82
|
selected = $bindable([]),
|
|
62
83
|
bulkActions,
|
|
63
84
|
open = $bindable(null),
|
|
@@ -89,8 +110,12 @@
|
|
|
89
110
|
title?: string;
|
|
90
111
|
/** Beside the title. Defaults to the row count. */
|
|
91
112
|
meta?: string;
|
|
92
|
-
/** The toolbar's own
|
|
113
|
+
/** The toolbar's own controls, after the Columns menu and before `tools`. */
|
|
93
114
|
actions?: Snippet;
|
|
115
|
+
/** The toolbar's icon actions, each named by its tooltip: export, print. */
|
|
116
|
+
tools?: ListIconAction[];
|
|
117
|
+
/** The toolbar's one labelled action, last: add. */
|
|
118
|
+
action?: ListAction;
|
|
94
119
|
/** The ticked rows' ids. Ticking needs `bulkActions`. */
|
|
95
120
|
selected?: Id[];
|
|
96
121
|
/** What the ticked rows allow, in the bar that replaces the toolbar. */
|
|
@@ -135,21 +160,6 @@
|
|
|
135
160
|
/** Ticking adds a checkbox to the gutter, so it widens to hold the chevron beside it. */
|
|
136
161
|
const GUTTER = '2.5rem';
|
|
137
162
|
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
163
|
|
|
154
164
|
// ── Columns: the ledger's own three, the module's, the balance last ──
|
|
155
165
|
const resolved = $derived.by(() => {
|
|
@@ -177,7 +187,9 @@
|
|
|
177
187
|
});
|
|
178
188
|
|
|
179
189
|
const defaults = $derived<LedgerPreferences>({
|
|
180
|
-
columns: resolved
|
|
190
|
+
columns: resolved
|
|
191
|
+
.filter((c) => !c.locked && (c.on === true || (c.on === 'wide' && WIDE)))
|
|
192
|
+
.map((c) => c.key),
|
|
181
193
|
period: 'month'
|
|
182
194
|
});
|
|
183
195
|
/** What the consumer kept, read field by field: a stale or foreign key falls back. */
|
|
@@ -201,15 +213,7 @@
|
|
|
201
213
|
/** The bars above the list end where its cards do: clear of its scroll bar's gutter. */
|
|
202
214
|
const edge = $derived(`calc(0.5rem + ${Math.max(0, listOuter - listInner)}px)`);
|
|
203
215
|
const mode = $derived(
|
|
204
|
-
layout !== 'auto'
|
|
205
|
-
? layout
|
|
206
|
-
: width === 0
|
|
207
|
-
? 'narrow'
|
|
208
|
-
: width < 600
|
|
209
|
-
? 'phone'
|
|
210
|
-
: width >= 1200
|
|
211
|
-
? 'wide'
|
|
212
|
-
: 'narrow'
|
|
216
|
+
layout !== 'auto' ? layout : listLayout(width)
|
|
213
217
|
);
|
|
214
218
|
const WIDE = $derived(mode === 'wide');
|
|
215
219
|
const PHONE = $derived(mode === 'phone');
|
|
@@ -638,7 +642,7 @@
|
|
|
638
642
|
<div
|
|
639
643
|
class={cn(
|
|
640
644
|
'relative grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 px-3 py-2 text-sm',
|
|
641
|
-
!first &&
|
|
645
|
+
!first && RULE,
|
|
642
646
|
openable && 'hover:bg-accent/40'
|
|
643
647
|
)}
|
|
644
648
|
style={ROW_HEIGHT}
|
|
@@ -677,8 +681,9 @@
|
|
|
677
681
|
{:else}
|
|
678
682
|
<div
|
|
679
683
|
class={cn(
|
|
680
|
-
'relative grid items-stretch
|
|
681
|
-
|
|
684
|
+
'relative grid items-stretch',
|
|
685
|
+
ROW_TEXT,
|
|
686
|
+
!first && RULE,
|
|
682
687
|
on && 'bg-primary/9',
|
|
683
688
|
openable && !on && 'hover:bg-accent/40'
|
|
684
689
|
)}
|
|
@@ -726,12 +731,13 @@
|
|
|
726
731
|
{@const net = single ? '' : money(t.net)}
|
|
727
732
|
{@const netTone = t.net > 0 ? 'text-status-success' : 'text-muted-foreground'}
|
|
728
733
|
{@const klass = cn(
|
|
729
|
-
|
|
734
|
+
GROUP,
|
|
735
|
+
'w-full text-left',
|
|
730
736
|
PHONE ? 'flex items-center gap-2 px-0.5' : 'grid items-center px-px'
|
|
731
737
|
)}
|
|
732
738
|
<!-- The spacing is padding, not margin, so the measured height is the height it takes. -->
|
|
733
739
|
<div
|
|
734
|
-
class={
|
|
740
|
+
class={groupSpacing(gi)}
|
|
735
741
|
{@attach measure(`h:${key}`, gi > 0 ? 'head' : undefined)}
|
|
736
742
|
>
|
|
737
743
|
<!-- A day heads its rows and does not close; any other period's label opens and closes it. -->
|
|
@@ -770,7 +776,7 @@
|
|
|
770
776
|
{#if !BY_DAY}<ChevronRight
|
|
771
777
|
class={cn('size-3.5 flex-none transition-transform', isOpen && 'rotate-90')}
|
|
772
778
|
/>{/if}
|
|
773
|
-
<span class=
|
|
779
|
+
<span class={cn(GROUP_LABEL, 'whitespace-nowrap')}>{label}</span>
|
|
774
780
|
<span class="min-w-0 flex-1 truncate">{metaText}</span>
|
|
775
781
|
<span class={cn('tabular-nums', netTone)}>{net}</span>
|
|
776
782
|
{:else}
|
|
@@ -786,7 +792,7 @@
|
|
|
786
792
|
class="flex min-w-0 items-baseline gap-3 overflow-hidden px-2 whitespace-nowrap"
|
|
787
793
|
style="grid-column: 2 / {cAmount};"
|
|
788
794
|
>
|
|
789
|
-
<span class=
|
|
795
|
+
<span class={GROUP_LABEL}>{label}</span>
|
|
790
796
|
<span class="min-w-0 truncate">{metaText}</span>
|
|
791
797
|
</span>
|
|
792
798
|
<span class={cn(CELL, END, 'tabular-nums', netTone)} style="grid-column: {cAmount};">{net}</span
|
|
@@ -835,18 +841,15 @@
|
|
|
835
841
|
</Button>
|
|
836
842
|
</div>
|
|
837
843
|
{:else}
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
844
|
+
<ListToolbar
|
|
845
|
+
{title}
|
|
846
|
+
meta={meta ?? count(rows.length)}
|
|
847
|
+
{tools}
|
|
848
|
+
{action}
|
|
849
|
+
label="This ledger"
|
|
850
|
+
style="padding-right: {edge};"
|
|
843
851
|
data-slot="ledger-toolbar"
|
|
844
852
|
>
|
|
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
853
|
<LedgerColumns
|
|
851
854
|
offered={resolved.map((c) => ({ key: c.key, label: c.label, locked: c.locked }))}
|
|
852
855
|
preferences={prefs}
|
|
@@ -855,7 +858,7 @@
|
|
|
855
858
|
onreset={() => setPreferences(defaults)}
|
|
856
859
|
/>
|
|
857
860
|
{@render actions?.()}
|
|
858
|
-
</
|
|
861
|
+
</ListToolbar>
|
|
859
862
|
{/if}
|
|
860
863
|
|
|
861
864
|
<div
|
|
@@ -868,9 +871,9 @@
|
|
|
868
871
|
data-slot="ledger-rows"
|
|
869
872
|
>
|
|
870
873
|
{#if !PHONE}
|
|
871
|
-
<div class=
|
|
874
|
+
<div class={HEAD_GROUND} bind:offsetHeight={headHeight}>
|
|
872
875
|
<div
|
|
873
|
-
class=
|
|
876
|
+
class={HEAD}
|
|
874
877
|
style:grid-template-columns={template}
|
|
875
878
|
data-slot="ledger-head"
|
|
876
879
|
>
|
|
@@ -905,46 +908,16 @@
|
|
|
905
908
|
{/if}
|
|
906
909
|
</span>
|
|
907
910
|
{#each shown as c (c.key)}
|
|
908
|
-
{@const sortable = !!sorter(c)}
|
|
909
911
|
{@const mine = activeSort?.key === c.key ? activeSort : null}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
c
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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}
|
|
912
|
+
<HeadCell
|
|
913
|
+
label={c.head ?? c.label}
|
|
914
|
+
name={c.label}
|
|
915
|
+
align={c.align}
|
|
916
|
+
sortable={!!sorter(c)}
|
|
917
|
+
sort={mine ? (mine.dir === -1 ? 'desc' : 'asc') : null}
|
|
918
|
+
onsort={() => sortBy(c.key)}
|
|
919
|
+
owner="ledger"
|
|
920
|
+
/>
|
|
948
921
|
{/each}
|
|
949
922
|
</div>
|
|
950
923
|
</div>
|
|
@@ -974,7 +947,7 @@
|
|
|
974
947
|
<!-- A card the window cuts keeps no edge at the cut, which is out of view. -->
|
|
975
948
|
<div
|
|
976
949
|
class={cn(
|
|
977
|
-
|
|
950
|
+
CARD,
|
|
978
951
|
block.cutTop && 'rounded-t-none border-t-0',
|
|
979
952
|
block.cutBottom && 'rounded-b-none border-b-0'
|
|
980
953
|
)}
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
*
|
|
8
8
|
* It fills its parent's height and scrolls its own rows, so the head card
|
|
9
9
|
* stays at the top: give it a bounded parent (`flex-1 min-h-0` in a
|
|
10
|
-
* full-height page). Width picks the layout: a phone's list below
|
|
10
|
+
* full-height page). Width picks the layout: a phone's list below 480px, the
|
|
11
11
|
* wide column widths from 1200px.
|
|
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
|
|
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,41 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* The layout a list's own width picks: a phone's list below 480px, the wide
|
|
8
|
+
* tracks from 1200px, the narrow tracks between. The list's width, not the
|
|
9
|
+
* window's: beside a ContextColumn at 1280px with the rail open it is 588px,
|
|
10
|
+
* which is a laptop's list, not a phone's.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PHONE_BELOW = 480;
|
|
13
|
+
export declare const WIDE_FROM = 1200;
|
|
14
|
+
export declare function listLayout(width: number): 'phone' | 'narrow' | 'wide';
|
|
15
|
+
export declare const CELL = "min-w-0 truncate px-2";
|
|
16
|
+
export declare const END = "pr-5 text-right";
|
|
17
|
+
/** The row's height rides the package's density ramp: 3.5rem, 3rem compact. */
|
|
18
|
+
export declare const ROW_HEIGHT = "min-height: calc(var(--ds-control-height-md) + 1rem);";
|
|
19
|
+
/** The row text's size, between the body and the small print. */
|
|
20
|
+
export declare const ROW_TEXT = "text-[0.8125rem]";
|
|
21
|
+
/** The hairline between two rows of one card. */
|
|
22
|
+
export declare const RULE = "border-border/60 border-t";
|
|
23
|
+
/** The card that holds a group's rows. */
|
|
24
|
+
export declare const CARD = "bg-card border-border overflow-hidden rounded-lg border";
|
|
25
|
+
/** The head card, and the ground behind it that hides the rows scrolling under it. */
|
|
26
|
+
export declare const HEAD_GROUND = "bg-background sticky top-0 z-3 pb-3";
|
|
27
|
+
export declare const HEAD = "bg-card border-border grid h-9 items-center rounded-lg border";
|
|
28
|
+
export declare const HEAD_TEXT = "text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase";
|
|
29
|
+
/** A group's label line on the ground; the spacing is padding, so a measured height is the height it takes. */
|
|
30
|
+
export declare const GROUP = "text-muted-foreground h-6.5 text-xs";
|
|
31
|
+
export declare const GROUP_LABEL = "text-[0.8125rem] font-semibold";
|
|
32
|
+
export declare const groupSpacing: (index: number) => "pt-4.5 pb-1.5" | "pb-1.5";
|
|
33
|
+
/** The house focus ring, and the same ring drawn inside a control that fills a clipped card. */
|
|
34
|
+
export declare const RING = "focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-none";
|
|
35
|
+
export declare const RING_INSET = "focus-visible:inset-ring-ring/50 focus-visible:inset-ring-3 focus-visible:outline-none";
|
|
36
|
+
/**
|
|
37
|
+
* A cell over an openable row lets a click through to the row beneath it, and
|
|
38
|
+
* gives one back to any control a module's cell renders, so a link or a button
|
|
39
|
+
* in a cell is never silently inert.
|
|
40
|
+
*/
|
|
41
|
+
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,43 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* The layout a list's own width picks: a phone's list below 480px, the wide
|
|
8
|
+
* tracks from 1200px, the narrow tracks between. The list's width, not the
|
|
9
|
+
* window's: beside a ContextColumn at 1280px with the rail open it is 588px,
|
|
10
|
+
* which is a laptop's list, not a phone's.
|
|
11
|
+
*/
|
|
12
|
+
export const PHONE_BELOW = 480;
|
|
13
|
+
export const WIDE_FROM = 1200;
|
|
14
|
+
export function listLayout(width) {
|
|
15
|
+
return width === 0 ? 'narrow' : width < PHONE_BELOW ? 'phone' : width >= WIDE_FROM ? 'wide' : 'narrow';
|
|
16
|
+
}
|
|
17
|
+
export const CELL = 'min-w-0 truncate px-2';
|
|
18
|
+
export const END = 'pr-5 text-right';
|
|
19
|
+
/** The row's height rides the package's density ramp: 3.5rem, 3rem compact. */
|
|
20
|
+
export const ROW_HEIGHT = 'min-height: calc(var(--ds-control-height-md) + 1rem);';
|
|
21
|
+
/** The row text's size, between the body and the small print. */
|
|
22
|
+
export const ROW_TEXT = 'text-[0.8125rem]';
|
|
23
|
+
/** The hairline between two rows of one card. */
|
|
24
|
+
export const RULE = 'border-border/60 border-t';
|
|
25
|
+
/** The card that holds a group's rows. */
|
|
26
|
+
export const CARD = 'bg-card border-border overflow-hidden rounded-lg border';
|
|
27
|
+
/** The head card, and the ground behind it that hides the rows scrolling under it. */
|
|
28
|
+
export const HEAD_GROUND = 'bg-background sticky top-0 z-3 pb-3';
|
|
29
|
+
export const HEAD = 'bg-card border-border grid h-9 items-center rounded-lg border';
|
|
30
|
+
export const HEAD_TEXT = 'text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase';
|
|
31
|
+
/** A group's label line on the ground; the spacing is padding, so a measured height is the height it takes. */
|
|
32
|
+
export const GROUP = 'text-muted-foreground h-6.5 text-xs';
|
|
33
|
+
export const GROUP_LABEL = 'text-[0.8125rem] font-semibold';
|
|
34
|
+
export const groupSpacing = (index) => (index > 0 ? 'pt-4.5 pb-1.5' : 'pb-1.5');
|
|
35
|
+
/** The house focus ring, and the same ring drawn inside a control that fills a clipped card. */
|
|
36
|
+
export const RING = 'focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-none';
|
|
37
|
+
export const RING_INSET = 'focus-visible:inset-ring-ring/50 focus-visible:inset-ring-3 focus-visible:outline-none';
|
|
38
|
+
/**
|
|
39
|
+
* A cell over an openable row lets a click through to the row beneath it, and
|
|
40
|
+
* gives one back to any control a module's cell renders, so a link or a button
|
|
41
|
+
* in a cell is never silently inert.
|
|
42
|
+
*/
|
|
43
|
+
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
|
|
49
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
66
|
-
|
|
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
|
|
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. */
|