@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.
- package/README.md +100 -4
- 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 +58 -78
- package/dist/components/ui/ledger/ledger.svelte.d.ts +6 -1
- package/dist/components/ui/ledger/look.d.ts +32 -0
- package/dist/components/ui/ledger/look.js +32 -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 +489 -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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger's look for a list that is not money: properties, managers,
|
|
3
|
+
* tools, the lines of a profit and loss. The same bar, head card, row cards
|
|
4
|
+
* and group labels on the ground as the Ledger, so every list in an app
|
|
5
|
+
* reads one way. The consumer brings its rows and the columns it offers; the
|
|
6
|
+
* list owns the look, the grouping, the sort and the viewer's columns,
|
|
7
|
+
* which it emits for the consumer to keep.
|
|
8
|
+
*
|
|
9
|
+
* A row is its thumbnail, its title and a second line of flags and a note,
|
|
10
|
+
* then the module's columns. It opens by its link (`href`) or by `onOpen`,
|
|
11
|
+
* which marks the row `open` names, for a detail beside the list.
|
|
12
|
+
*
|
|
13
|
+
* It fills its parent's height and scrolls its own rows, so the head card
|
|
14
|
+
* stays at the top: give it a bounded parent (`flex-1 min-h-0`). Width picks
|
|
15
|
+
* the layout, as the Ledger's does: a phone's list below 600px, the wide
|
|
16
|
+
* tracks and the `'wide'` columns from 1200px. On a phone a row is its
|
|
17
|
+
* thumbnail, title and lines, with the first end-aligned column's value
|
|
18
|
+
* beside the title.
|
|
19
|
+
*/
|
|
20
|
+
import type { Snippet } from 'svelte';
|
|
21
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
22
|
+
import type { ListAction, ListIconAction } from '../list-toolbar/types.js';
|
|
23
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
24
|
+
import type { RecordListColumn, RecordListPreferences, RecordListRow } from './types.js';
|
|
25
|
+
declare function $$render<R extends RecordListRow>(): {
|
|
26
|
+
props: Omit<HTMLAttributes<HTMLDivElement>, "title"> & {
|
|
27
|
+
rows: readonly R[];
|
|
28
|
+
/** The module's columns, in order after the title. See `RecordListColumn`. */
|
|
29
|
+
columns?: RecordListColumn<R>[];
|
|
30
|
+
/** The viewer's columns. Missing, the module's defaults. */
|
|
31
|
+
preferences?: Partial<RecordListPreferences>;
|
|
32
|
+
onPreferencesChange?: (next: RecordListPreferences) => void;
|
|
33
|
+
/** The Columns menu's footnote: where the choice is kept. */
|
|
34
|
+
preferencesNote?: string;
|
|
35
|
+
/** What a row is, one and many: "property", "properties". */
|
|
36
|
+
noun?: [string, string];
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Beside the title. Defaults to the row count. */
|
|
39
|
+
meta?: string;
|
|
40
|
+
/** Before the title: a switch between views of the same list. */
|
|
41
|
+
leading?: Snippet;
|
|
42
|
+
/** The bar's own controls, after the Columns menu. */
|
|
43
|
+
toolbar?: Snippet;
|
|
44
|
+
/** The bar's icon actions, each named by its tooltip. */
|
|
45
|
+
tools?: ListIconAction[];
|
|
46
|
+
/** The bar's one labelled action, last. */
|
|
47
|
+
action?: ListAction;
|
|
48
|
+
/** A row's group, by its label: groups show in the order their first rows come. */
|
|
49
|
+
group?: (row: R) => string;
|
|
50
|
+
/** Names a card after the rows with each column's total over every row: "All three". */
|
|
51
|
+
totalLabel?: string;
|
|
52
|
+
/** The head card. A list whose columns need no heads drops it. */
|
|
53
|
+
head?: boolean;
|
|
54
|
+
/** The tile a row without a photo or its own icon shows. With it every row has a thumbnail. */
|
|
55
|
+
icon?: IconComponent;
|
|
56
|
+
/** Where a row leads. */
|
|
57
|
+
href?: (row: R) => string | undefined;
|
|
58
|
+
/** A row's click, for a detail beside the list. */
|
|
59
|
+
onOpen?: (row: R) => void;
|
|
60
|
+
/** The row whose detail is open, marked. */
|
|
61
|
+
open?: R["id"] | null;
|
|
62
|
+
empty?: Snippet;
|
|
63
|
+
/** After the last row, inside the scroll. */
|
|
64
|
+
footer?: Snippet;
|
|
65
|
+
layout?: "auto" | "phone" | "narrow" | "wide";
|
|
66
|
+
class?: string;
|
|
67
|
+
};
|
|
68
|
+
exports: {};
|
|
69
|
+
bindings: "preferences";
|
|
70
|
+
slots: {};
|
|
71
|
+
events: {};
|
|
72
|
+
};
|
|
73
|
+
declare class __sveltets_Render<R extends RecordListRow> {
|
|
74
|
+
props(): ReturnType<typeof $$render<R>>['props'];
|
|
75
|
+
events(): ReturnType<typeof $$render<R>>['events'];
|
|
76
|
+
slots(): ReturnType<typeof $$render<R>>['slots'];
|
|
77
|
+
bindings(): "preferences";
|
|
78
|
+
exports(): {};
|
|
79
|
+
}
|
|
80
|
+
interface $$IsomorphicComponent {
|
|
81
|
+
new <R extends RecordListRow>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<R>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<R>['props']>, ReturnType<__sveltets_Render<R>['events']>, ReturnType<__sveltets_Render<R>['slots']>> & {
|
|
82
|
+
$$bindings?: ReturnType<__sveltets_Render<R>['bindings']>;
|
|
83
|
+
} & ReturnType<__sveltets_Render<R>['exports']>;
|
|
84
|
+
<R extends RecordListRow>(internal: unknown, props: ReturnType<__sveltets_Render<R>['props']> & {}): ReturnType<__sveltets_Render<R>['exports']>;
|
|
85
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
86
|
+
}
|
|
87
|
+
declare const RecordList: $$IsomorphicComponent;
|
|
88
|
+
type RecordList<R extends RecordListRow> = InstanceType<typeof RecordList<R>>;
|
|
89
|
+
export default RecordList;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A record's picture at a fixed size: its photo, or a quiet tile with the
|
|
4
|
+
* module's icon when it has none or the photo fails to load. Decorative:
|
|
5
|
+
* the record's name always sits beside it.
|
|
6
|
+
*/
|
|
7
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
8
|
+
import { cn } from '../../../utils.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
src,
|
|
12
|
+
icon: Icon,
|
|
13
|
+
class: className
|
|
14
|
+
}: { src?: string | null; icon?: IconComponent; class?: string } = $props();
|
|
15
|
+
|
|
16
|
+
let failed = $state<string | null>(null);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<span
|
|
20
|
+
class={cn(
|
|
21
|
+
'bg-surface-2 border-border/60 grid flex-none place-items-center overflow-hidden rounded-md border',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
data-slot="record-thumbnail"
|
|
26
|
+
>
|
|
27
|
+
{#if src && failed !== src}
|
|
28
|
+
<img {src} alt="" loading="lazy" class="size-full object-cover" onerror={() => (failed = src)} />
|
|
29
|
+
{:else if Icon}
|
|
30
|
+
<Icon class="text-primary size-1/2" />
|
|
31
|
+
{/if}
|
|
32
|
+
</span>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A record's picture at a fixed size: its photo, or a quiet tile with the
|
|
3
|
+
* module's icon when it has none or the photo fails to load. Decorative:
|
|
4
|
+
* the record's name always sits beside it.
|
|
5
|
+
*/
|
|
6
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
src?: string | null;
|
|
9
|
+
icon?: IconComponent;
|
|
10
|
+
class?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const Thumbnail: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type Thumbnail = ReturnType<typeof Thumbnail>;
|
|
14
|
+
export default Thumbnail;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
2
|
+
import type { ListColumn } from '../ledger/types.js';
|
|
3
|
+
import type { Status } from '../status/index.js';
|
|
4
|
+
/** Something on file that wants the viewer: a chip on the row's second line. */
|
|
5
|
+
export interface RecordFlag {
|
|
6
|
+
status: Status;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
/** What the list reads off a row. A consumer extends it with its own fields for its columns. */
|
|
10
|
+
export interface RecordListRow {
|
|
11
|
+
id: string | number;
|
|
12
|
+
/** Line one: the record's name. */
|
|
13
|
+
title: string;
|
|
14
|
+
/** Line two, after any flags. */
|
|
15
|
+
note?: string | null;
|
|
16
|
+
/** A thumbnail before the title. Without one, `icon` or the list's `icon` stands in. */
|
|
17
|
+
image?: string | null;
|
|
18
|
+
/** The row's own tile, where each row is a kind of thing (a tool) rather than a photo. */
|
|
19
|
+
icon?: IconComponent;
|
|
20
|
+
/**
|
|
21
|
+
* What wants the viewer, first first. Line two shows the first as a chip
|
|
22
|
+
* and "+N more" naming the rest on hover; a phone's row shows every one.
|
|
23
|
+
*/
|
|
24
|
+
flags?: readonly RecordFlag[] | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A column the module offers. The key `title` is the list's own cell (the
|
|
28
|
+
* thumbnail, the title, the flags and the note): list it to rename or resize
|
|
29
|
+
* it, or to draw it yourself with `cell`. It always shows, first. Every other
|
|
30
|
+
* key is the module's, in its order.
|
|
31
|
+
*/
|
|
32
|
+
export interface RecordListColumn<R extends RecordListRow = RecordListRow> extends ListColumn<R> {
|
|
33
|
+
/** The column's total over some rows: each group's on its label, all rows' in the total card. */
|
|
34
|
+
total?: (rows: readonly R[]) => string;
|
|
35
|
+
}
|
|
36
|
+
/** The viewer's own choices, which the consumer persists. */
|
|
37
|
+
export interface RecordListPreferences {
|
|
38
|
+
/** The ticked columns' keys. */
|
|
39
|
+
columns: string[];
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The open record's whole name as the top bar's control: a raised button
|
|
4
|
+
* with its chevron inside, after the section's name. Its menu finds a
|
|
5
|
+
* record by name, lists them under their headings (owned, then sold) with
|
|
6
|
+
* a thumbnail each, and ends with the way to all of them, so it still
|
|
7
|
+
* works at fifty. The record's own pages follow as tabs.
|
|
8
|
+
*
|
|
9
|
+
* It places nothing itself: put it inside `ShellControls` with whatever
|
|
10
|
+
* else the page sets in the bar.
|
|
11
|
+
*
|
|
12
|
+
* <ShellControls>
|
|
13
|
+
* <RecordSwitcher name="4. Banksia" current={4} groups={[owned, sold]}
|
|
14
|
+
* noun="property" icon={House} all={{ label: 'All properties', href: '/property' }}
|
|
15
|
+
* pages={[{ label: 'Overview', href: '/property/4', current: true }, …]} />
|
|
16
|
+
* </ShellControls>
|
|
17
|
+
*/
|
|
18
|
+
import { Command as CommandPrimitive, computeCommandScore } from 'bits-ui';
|
|
19
|
+
import Check from '@lucide/svelte/icons/check';
|
|
20
|
+
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
|
21
|
+
import { buttonVariants } from '../button/index.js';
|
|
22
|
+
import * as Command from '../command/index.js';
|
|
23
|
+
import * as Popover from '../popover/index.js';
|
|
24
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
25
|
+
import Thumbnail from '../record-list/thumbnail.svelte';
|
|
26
|
+
import { cn } from '../../../utils.js';
|
|
27
|
+
import type { RecordPage, RecordSwitcherGroup, RecordSwitcherItem } from './types.js';
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
name,
|
|
31
|
+
current,
|
|
32
|
+
groups,
|
|
33
|
+
noun = 'record',
|
|
34
|
+
placeholder = `Find a ${noun}`,
|
|
35
|
+
icon,
|
|
36
|
+
all,
|
|
37
|
+
pages = [],
|
|
38
|
+
separator = true,
|
|
39
|
+
open = $bindable(false),
|
|
40
|
+
class: className
|
|
41
|
+
}: {
|
|
42
|
+
/** The open record's whole name. */
|
|
43
|
+
name: string;
|
|
44
|
+
/** The open record's id, ticked in the menu. */
|
|
45
|
+
current?: string | number;
|
|
46
|
+
groups: readonly RecordSwitcherGroup[];
|
|
47
|
+
/** What a record is, for the control's name and the search's prompt. */
|
|
48
|
+
noun?: string;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
/** The tile a record without a photo shows. With it every record has a thumbnail. */
|
|
51
|
+
icon?: IconComponent;
|
|
52
|
+
/** The menu's last line: every record, on its list. */
|
|
53
|
+
all?: { label: string; href: string };
|
|
54
|
+
/** The open record's own pages, as tabs after its name. */
|
|
55
|
+
pages?: readonly RecordPage[];
|
|
56
|
+
/** The "/" after the section's name. Off where it is not in the bar. */
|
|
57
|
+
separator?: boolean;
|
|
58
|
+
open?: boolean;
|
|
59
|
+
class?: string;
|
|
60
|
+
} = $props();
|
|
61
|
+
|
|
62
|
+
const thumbs = $derived(!!icon || groups.some((g) => g.items.some((i) => i.image)));
|
|
63
|
+
/** An item's key in the menu; the search reads its name and note, never this. */
|
|
64
|
+
const valueOf = (item: RecordSwitcherItem) => `record:${item.id}`;
|
|
65
|
+
const words = $derived(
|
|
66
|
+
new Map(groups.flatMap((g) => g.items).map((i) => [valueOf(i), `${i.label} ${i.note ?? ''}`]))
|
|
67
|
+
);
|
|
68
|
+
const filter = (value: string, search: string) =>
|
|
69
|
+
computeCommandScore(words.get(value) ?? value, search);
|
|
70
|
+
const here = () => {
|
|
71
|
+
const item = groups.flatMap((g) => g.items).find((i) => i.id === current);
|
|
72
|
+
return item ? valueOf(item) : '';
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The highlighted record: the open one, set as the menu opens, before the
|
|
76
|
+
* list mounts, or the list would start on its first row.
|
|
77
|
+
*/
|
|
78
|
+
// svelte-ignore state_referenced_locally
|
|
79
|
+
let highlighted = $state(here());
|
|
80
|
+
const eyebrow = 'text-muted-foreground text-2xs tracking-eyebrow font-semibold uppercase';
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class={cn('flex min-w-0 items-center gap-2 sm:gap-3', className)} data-slot="record-switcher">
|
|
84
|
+
{#if separator}<span class="text-shell-muted-foreground flex-none" aria-hidden="true">/</span>{/if}
|
|
85
|
+
<Popover.Root bind:open onOpenChange={(o) => o && (highlighted = here())}>
|
|
86
|
+
<Popover.Trigger
|
|
87
|
+
class={cn(
|
|
88
|
+
buttonVariants({ variant: 'outline', size: 'sm' }),
|
|
89
|
+
'bg-card min-w-0 gap-1.5 text-sm font-semibold'
|
|
90
|
+
)}
|
|
91
|
+
aria-label="{name}: switch to another {noun}"
|
|
92
|
+
data-slot="record-switcher-trigger"
|
|
93
|
+
>
|
|
94
|
+
<span class="truncate">{name}</span>
|
|
95
|
+
<ChevronDown class="text-muted-foreground" />
|
|
96
|
+
</Popover.Trigger>
|
|
97
|
+
<Popover.Content align="start" class="w-[min(24rem,calc(100vw-2rem))] p-0">
|
|
98
|
+
<Command.Root class="rounded-md!" label={placeholder} {filter} loop bind:value={highlighted}>
|
|
99
|
+
<Command.Input {placeholder} />
|
|
100
|
+
<Command.List class="max-h-[min(28rem,60vh)] px-1 pt-1">
|
|
101
|
+
<Command.Empty class="text-muted-foreground py-5">No {noun} by that name</Command.Empty>
|
|
102
|
+
{#each groups as g, gi (g.label ?? gi)}
|
|
103
|
+
<CommandPrimitive.Group class="pb-1" value={g.label ?? `group-${gi}`}>
|
|
104
|
+
{#if g.label}
|
|
105
|
+
<CommandPrimitive.GroupHeading class={cn(eyebrow, 'px-2 pt-2 pb-1.5')}>
|
|
106
|
+
{g.label}
|
|
107
|
+
</CommandPrimitive.GroupHeading>
|
|
108
|
+
{/if}
|
|
109
|
+
<CommandPrimitive.GroupItems>
|
|
110
|
+
{#each g.items as item (item.id)}
|
|
111
|
+
{@const on = item.id === current}
|
|
112
|
+
<Command.LinkItem
|
|
113
|
+
href={item.href}
|
|
114
|
+
value={valueOf(item)}
|
|
115
|
+
onSelect={() => (open = false)}
|
|
116
|
+
onclick={() => (open = false)}
|
|
117
|
+
aria-current={on ? 'page' : undefined}
|
|
118
|
+
class={cn('gap-3 rounded-md py-1.5', on && 'bg-primary/10')}
|
|
119
|
+
data-slot="record-switcher-item"
|
|
120
|
+
>
|
|
121
|
+
{#if thumbs}<Thumbnail src={item.image} {icon} class="size-10" />{/if}
|
|
122
|
+
<span class="flex min-w-0 flex-1 flex-col">
|
|
123
|
+
<span class="text-foreground truncate font-medium">{item.label}</span>
|
|
124
|
+
{#if item.note}
|
|
125
|
+
<span class="text-muted-foreground truncate text-xs">{item.note}</span>
|
|
126
|
+
{/if}
|
|
127
|
+
</span>
|
|
128
|
+
{#if on}<Check class="text-primary size-4" /><span class="sr-only">, open now</span>{/if}
|
|
129
|
+
</Command.LinkItem>
|
|
130
|
+
{/each}
|
|
131
|
+
</CommandPrimitive.GroupItems>
|
|
132
|
+
</CommandPrimitive.Group>
|
|
133
|
+
{/each}
|
|
134
|
+
</Command.List>
|
|
135
|
+
{#if all}
|
|
136
|
+
<div class="border-border border-t p-1">
|
|
137
|
+
<a
|
|
138
|
+
href={all.href}
|
|
139
|
+
class="text-foreground hover:bg-accent focus-visible:ring-ring/50 block rounded-md px-2 py-2 text-sm underline underline-offset-4 focus-visible:ring-3 focus-visible:outline-none"
|
|
140
|
+
onclick={() => (open = false)}
|
|
141
|
+
data-slot="record-switcher-all">{all.label}</a
|
|
142
|
+
>
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
145
|
+
</Command.Root>
|
|
146
|
+
</Popover.Content>
|
|
147
|
+
</Popover.Root>
|
|
148
|
+
|
|
149
|
+
{#if pages.length}
|
|
150
|
+
<nav aria-label="Pages of {name}" class="ml-1 flex items-stretch gap-1 self-stretch sm:ml-3">
|
|
151
|
+
{#each pages as p (p.href)}
|
|
152
|
+
<a
|
|
153
|
+
href={p.href}
|
|
154
|
+
aria-current={p.current ? 'page' : undefined}
|
|
155
|
+
class={cn(
|
|
156
|
+
'flex items-center border-b-2 px-2 text-sm transition-colors sm:px-3',
|
|
157
|
+
p.current
|
|
158
|
+
? 'border-primary text-foreground font-semibold'
|
|
159
|
+
: 'text-muted-foreground hover:text-foreground border-transparent'
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{p.label}
|
|
163
|
+
</a>
|
|
164
|
+
{/each}
|
|
165
|
+
</nav>
|
|
166
|
+
{/if}
|
|
167
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
2
|
+
import type { RecordPage, RecordSwitcherGroup } from './types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
/** The open record's whole name. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** The open record's id, ticked in the menu. */
|
|
7
|
+
current?: string | number;
|
|
8
|
+
groups: readonly RecordSwitcherGroup[];
|
|
9
|
+
/** What a record is, for the control's name and the search's prompt. */
|
|
10
|
+
noun?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
/** The tile a record without a photo shows. With it every record has a thumbnail. */
|
|
13
|
+
icon?: IconComponent;
|
|
14
|
+
/** The menu's last line: every record, on its list. */
|
|
15
|
+
all?: {
|
|
16
|
+
label: string;
|
|
17
|
+
href: string;
|
|
18
|
+
};
|
|
19
|
+
/** The open record's own pages, as tabs after its name. */
|
|
20
|
+
pages?: readonly RecordPage[];
|
|
21
|
+
/** The "/" after the section's name. Off where it is not in the bar. */
|
|
22
|
+
separator?: boolean;
|
|
23
|
+
open?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
};
|
|
26
|
+
declare const RecordSwitcher: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
27
|
+
type RecordSwitcher = ReturnType<typeof RecordSwitcher>;
|
|
28
|
+
export default RecordSwitcher;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A record another can be switched to. */
|
|
2
|
+
export interface RecordSwitcherItem {
|
|
3
|
+
id: string | number;
|
|
4
|
+
/** Its whole name, as the switcher shows it when it is the one open. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Under the name: where it is, what it is worth, when it was sold. */
|
|
7
|
+
note?: string | null;
|
|
8
|
+
href: string;
|
|
9
|
+
/** Its thumbnail. Without one the switcher's `icon` stands in. */
|
|
10
|
+
image?: string | null;
|
|
11
|
+
}
|
|
12
|
+
/** A run of records under a heading: owned, then sold. */
|
|
13
|
+
export interface RecordSwitcherGroup {
|
|
14
|
+
label?: string;
|
|
15
|
+
items: readonly RecordSwitcherItem[];
|
|
16
|
+
}
|
|
17
|
+
/** One of the open record's own pages, a tab beside its name. */
|
|
18
|
+
export interface RecordPage {
|
|
19
|
+
label: string;
|
|
20
|
+
href: string;
|
|
21
|
+
current?: boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.5",
|
|
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,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"package": "@poodle64/ui",
|
|
4
|
-
"version": "2026.10.
|
|
4
|
+
"version": "2026.10.5",
|
|
5
5
|
"generatedBy": "scripts/generate-registry.mjs",
|
|
6
6
|
"source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
|
|
7
|
-
"componentCount":
|
|
7
|
+
"componentCount": 66,
|
|
8
8
|
"situationCount": 18
|
|
9
9
|
},
|
|
10
10
|
"situations": [
|
|
@@ -125,6 +125,20 @@
|
|
|
125
125
|
"title": "A catalogue of like things",
|
|
126
126
|
"description": "An index of many peer items — a responsive tile wall, or a sortable/filterable table of records.",
|
|
127
127
|
"components": [
|
|
128
|
+
{
|
|
129
|
+
"name": "RecordList",
|
|
130
|
+
"dir": "record-list",
|
|
131
|
+
"import": "@poodle64/ui/record-list",
|
|
132
|
+
"props": "rows, columns?, preferences?, onPreferencesChange?, title?, tools?, action?, group?, totalLabel?, icon?, href?, onOpen?, open?",
|
|
133
|
+
"insteadOf": "a DataTableTanstack, or a boxed Table, for a module's own records (properties, managers, tools, a P&L's lines): the ledger's look (head card, row cards, group labels on the ground) for rows that are not money, so every list in an app reads one way"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "ListToolbar",
|
|
137
|
+
"dir": "list-toolbar",
|
|
138
|
+
"import": "@poodle64/ui/list-toolbar",
|
|
139
|
+
"props": "title?, meta?, leading?, children?, tools?, action?",
|
|
140
|
+
"insteadOf": "a hand-built title, count and button row above a list; the Ledger and the RecordList draw theirs with it"
|
|
141
|
+
},
|
|
128
142
|
{
|
|
129
143
|
"name": "TileGrid",
|
|
130
144
|
"dir": "tile-grid",
|
|
@@ -157,7 +171,7 @@
|
|
|
157
171
|
"name": "Ledger",
|
|
158
172
|
"dir": "ledger",
|
|
159
173
|
"import": "@poodle64/ui/ledger",
|
|
160
|
-
"props": "rows, columns?, preferences?, onPreferencesChange?, title?, actions?, selected?, bulkActions?, open?, editor?",
|
|
174
|
+
"props": "rows, columns?, preferences?, onPreferencesChange?, title?, actions?, tools?, action?, selected?, bulkActions?, open?, editor?",
|
|
161
175
|
"insteadOf": "a DataTableTanstack of transactions, or a hand-built grouped list with its own period headers and column picker"
|
|
162
176
|
}
|
|
163
177
|
]
|
|
@@ -171,7 +185,7 @@
|
|
|
171
185
|
"name": "Panel",
|
|
172
186
|
"dir": "panel",
|
|
173
187
|
"import": "@poodle64/ui/panel",
|
|
174
|
-
"props": "children, title?, subtitle?, description?, icon?, action?, footer?, tone?, pad?",
|
|
188
|
+
"props": "children, title?, subtitle?, description?, icon?, action?, footer?, tone?, pad?, scroll?",
|
|
175
189
|
"insteadOf": "raw Card + CardHeader + CardTitle assembled by hand on every route; also the settings section — there is no separate SettingsSection, `description` wraps, `footer` carries Save/Cancel, `tone=\"destructive\"` is the danger zone"
|
|
176
190
|
}
|
|
177
191
|
]
|
|
@@ -306,6 +320,13 @@
|
|
|
306
320
|
"import": "@poodle64/ui/page-canvas",
|
|
307
321
|
"props": "pageCount, page, currentPage?, regions?, lookedRegions?, activeRegionId?, focusedRegionId?, zoom?",
|
|
308
322
|
"insteadOf": "a hand-built page viewer per app (godswood's DocumentViewer, pebblestone's PdfViewer) each re-deriving fit/zoom/pan, now with no way to outline what was read (#839)"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"name": "DocumentPane",
|
|
326
|
+
"dir": "document-pane",
|
|
327
|
+
"import": "@poodle64/ui/document-pane",
|
|
328
|
+
"props": "title, subtitle?, pages?, pageCount?, page?, currentPage?, zoom?, actions?, onClose?",
|
|
329
|
+
"insteadOf": "a PageCanvas framed by hand with its own title row, download and close; the pane the record page opens a document in, beside the column or in its place"
|
|
309
330
|
}
|
|
310
331
|
]
|
|
311
332
|
},
|
|
@@ -342,12 +363,19 @@
|
|
|
342
363
|
"props": "title?, eyebrow?, breadcrumbs?, icon?, subtitle?, info?, meta?, actions?",
|
|
343
364
|
"insteadOf": "a hand-written <h1> and title bar (the drift gate fails this)"
|
|
344
365
|
},
|
|
366
|
+
{
|
|
367
|
+
"name": "RecordSwitcher",
|
|
368
|
+
"dir": "record-switcher",
|
|
369
|
+
"import": "@poodle64/ui/record-switcher",
|
|
370
|
+
"props": "name, groups, current?, noun?, placeholder?, icon?, all?, pages?, separator?",
|
|
371
|
+
"insteadOf": "a bare record name beside a small chevrons button in the top bar, and a dropdown of names with no search, groups or thumbnails; the record's own page tabs come with it"
|
|
372
|
+
},
|
|
345
373
|
{
|
|
346
374
|
"name": "ContextColumn",
|
|
347
375
|
"dir": "context-column",
|
|
348
376
|
"import": "@poodle64/ui/context-column",
|
|
349
|
-
"props": "stats
|
|
350
|
-
"insteadOf": "a hand-built right-hand <aside> with a stat card and a detail pane"
|
|
377
|
+
"props": "stats?, statsTitle?, statsInfo?, detail?, title?, subtitle?, onClose?, children?, documentPane?, ariaLabel?",
|
|
378
|
+
"insteadOf": "a hand-built right-hand <aside> with a stat card and a detail pane; on a record page, the one opened item (title, close, an inner scroll) and its DocumentPane, in the column's place or beside it once the page's row has room"
|
|
351
379
|
},
|
|
352
380
|
{
|
|
353
381
|
"name": "ListShell",
|
|
@@ -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.5`. 66 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
|
|
|
@@ -72,6 +72,8 @@ _An index of many peer items — a responsive tile wall, or a sortable/filterabl
|
|
|
72
72
|
|
|
73
73
|
| Component | Import | Key props | Reach for it instead of |
|
|
74
74
|
| --- | --- | --- | --- |
|
|
75
|
+
| `RecordList` | `@poodle64/ui/record-list` | `rows, columns?, preferences?, onPreferencesChange?, title?, tools?, action?, group?, totalLabel?, icon?, href?, onOpen?, open?` | a DataTableTanstack, or a boxed Table, for a module's own records (properties, managers, tools, a P&L's lines): the ledger's look (head card, row cards, group labels on the ground) for rows that are not money, so every list in an app reads one way |
|
|
76
|
+
| `ListToolbar` | `@poodle64/ui/list-toolbar` | `title?, meta?, leading?, children?, tools?, action?` | a hand-built title, count and button row above a list; the Ledger and the RecordList draw theirs with it |
|
|
75
77
|
| `TileGrid` | `@poodle64/ui/tile-grid` | `children, min?, gap?, tag?` | a grid with fixed sm:grid-cols-* breakpoints you maintain per page |
|
|
76
78
|
| `DataTableTanstack` | `@poodle64/ui/data-table-tanstack` | `table, selectable?, selectedId?, onSelect?, emptyMessage?` | a hand-built <table>, or the raw Table primitive wired to TanStack by hand |
|
|
77
79
|
| `DataTableToolbar` | `@poodle64/ui/data-table-toolbar` | `searchValue, onSearch, searchPlaceholder?, chipGroups?, trailing?` | a hand-built search + filter-chip bar above a table |
|
|
@@ -82,7 +84,7 @@ _Dated rows of money, coins or units moving — a bank account, a property's boo
|
|
|
82
84
|
|
|
83
85
|
| Component | Import | Key props | Reach for it instead of |
|
|
84
86
|
| --- | --- | --- | --- |
|
|
85
|
-
| `Ledger` | `@poodle64/ui/ledger` | `rows, columns?, preferences?, onPreferencesChange?, title?, actions?, selected?, bulkActions?, open?, editor?` | a DataTableTanstack of transactions, or a hand-built grouped list with its own period headers and column picker |
|
|
87
|
+
| `Ledger` | `@poodle64/ui/ledger` | `rows, columns?, preferences?, onPreferencesChange?, title?, actions?, tools?, action?, selected?, bulkActions?, open?, editor?` | a DataTableTanstack of transactions, or a hand-built grouped list with its own period headers and column picker |
|
|
86
88
|
|
|
87
89
|
## A titled section of a route
|
|
88
90
|
|
|
@@ -90,7 +92,7 @@ _The generic titled card every route reaches for — an optional header with ico
|
|
|
90
92
|
|
|
91
93
|
| Component | Import | Key props | Reach for it instead of |
|
|
92
94
|
| --- | --- | --- | --- |
|
|
93
|
-
| `Panel` | `@poodle64/ui/panel` | `children, title?, subtitle?, description?, icon?, action?, footer?, tone?, pad?` | raw Card + CardHeader + CardTitle assembled by hand on every route; also the settings section — there is no separate SettingsSection, `description` wraps, `footer` carries Save/Cancel, `tone="destructive"` is the danger zone |
|
|
95
|
+
| `Panel` | `@poodle64/ui/panel` | `children, title?, subtitle?, description?, icon?, action?, footer?, tone?, pad?, scroll?` | raw Card + CardHeader + CardTitle assembled by hand on every route; also the settings section — there is no separate SettingsSection, `description` wraps, `footer` carries Save/Cancel, `tone="destructive"` is the danger zone |
|
|
94
96
|
|
|
95
97
|
## A form the app itself describes
|
|
96
98
|
|
|
@@ -134,6 +136,7 @@ _One page at a time, fitted to its pane both ways so an A4 statement and a tall
|
|
|
134
136
|
| Component | Import | Key props | Reach for it instead of |
|
|
135
137
|
| --- | --- | --- | --- |
|
|
136
138
|
| `PageCanvas` | `@poodle64/ui/page-canvas` | `pageCount, page, currentPage?, regions?, lookedRegions?, activeRegionId?, focusedRegionId?, zoom?` | a hand-built page viewer per app (godswood's DocumentViewer, pebblestone's PdfViewer) each re-deriving fit/zoom/pan, now with no way to outline what was read (#839) |
|
|
139
|
+
| `DocumentPane` | `@poodle64/ui/document-pane` | `title, subtitle?, pages?, pageCount?, page?, currentPage?, zoom?, actions?, onClose?` | a PageCanvas framed by hand with its own title row, download and close; the pane the record page opens a document in, beside the column or in its place |
|
|
137
140
|
|
|
138
141
|
## The frame every route shares
|
|
139
142
|
|
|
@@ -145,7 +148,8 @@ _The standing chrome — the app shell and its nav, the one page-title treatment
|
|
|
145
148
|
| `ShellControls` | `@poodle64/ui/app-shell` | `children` | a scope row (entity, financial year, chart/table) hand-placed above the page body; the shell's top bar carries it |
|
|
146
149
|
| `Segmented` | `@poodle64/ui/segmented` | `value, options, label, size?` | a hand-built row of aria-pressed buttons for a small fixed choice |
|
|
147
150
|
| `PageHeader` | `@poodle64/ui/page-header` | `title?, eyebrow?, breadcrumbs?, icon?, subtitle?, info?, meta?, actions?` | a hand-written <h1> and title bar (the drift gate fails this) |
|
|
148
|
-
| `
|
|
151
|
+
| `RecordSwitcher` | `@poodle64/ui/record-switcher` | `name, groups, current?, noun?, placeholder?, icon?, all?, pages?, separator?` | a bare record name beside a small chevrons button in the top bar, and a dropdown of names with no search, groups or thumbnails; the record's own page tabs come with it |
|
|
152
|
+
| `ContextColumn` | `@poodle64/ui/context-column` | `stats?, statsTitle?, statsInfo?, detail?, title?, subtitle?, onClose?, children?, documentPane?, ariaLabel?` | a hand-built right-hand <aside> with a stat card and a detail pane; on a record page, the one opened item (title, close, an inner scroll) and its DocumentPane, in the column's place or beside it once the page's row has room |
|
|
149
153
|
| `ListShell` | `@poodle64/ui/list-shell` | `title, children, label?, open?, actions?` | a module hand-rolling its own foldable second column beside the rail — a queue, an inbox, a working list |
|
|
150
154
|
| `AppIdentity` | `@poodle64/ui/app-identity` | `user, workspace?, role?, entitlements?, onSignOut, onAccountSettings?, onSwitchTheme?` | a hand-rolled account/avatar dropdown wired to raw DropdownMenu.* + Avatar.* |
|
|
151
155
|
|