@poodle64/ui 2026.9.19 → 2026.9.21
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 +59 -0
- package/dist/components/alerts/alert-bell.svelte +177 -0
- package/dist/components/alerts/alert-bell.svelte.d.ts +16 -0
- package/dist/components/alerts/index.d.ts +2 -0
- package/dist/components/alerts/index.js +2 -0
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/list-shell/index.d.ts +2 -0
- package/dist/components/ui/list-shell/index.js +2 -0
- package/dist/components/ui/list-shell/list-shell.svelte +85 -0
- package/dist/components/ui/list-shell/list-shell.svelte.d.ts +39 -0
- package/dist/components/ui/select/select.svelte.d.ts +1 -1
- package/package.json +5 -1
- package/registry/component-map.json +10 -3
- package/registry/component-map.md +3 -2
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ src/lib/
|
|
|
42
42
|
page-chrome components built on top of them
|
|
43
43
|
components/feedback/ ReportWidget: the fixed bottom-right "report a problem"
|
|
44
44
|
trigger and dialogue every app mounts (see below)
|
|
45
|
+
components/alerts/ AlertBell: the top-bar bell over the app factory's alerts
|
|
46
|
+
routes (see below)
|
|
45
47
|
dist/ generated by `pnpm build` (@sveltejs/package); never edit
|
|
46
48
|
```
|
|
47
49
|
|
|
@@ -402,6 +404,50 @@ all three serve any route:
|
|
|
402
404
|
truncates. Carry one or the other. A page writes a run of `Panel`s and no
|
|
403
405
|
wrapper: the pane stacks them on the package's section rhythm itself.
|
|
404
406
|
|
|
407
|
+
### A working list beside a page's own content
|
|
408
|
+
|
|
409
|
+
```svelte
|
|
410
|
+
<script lang="ts">
|
|
411
|
+
import AppShell from '@poodle64/ui/app-shell';
|
|
412
|
+
import ListShell from '@poodle64/ui/list-shell';
|
|
413
|
+
|
|
414
|
+
// Bind it to whatever already persists a user preference for this module:
|
|
415
|
+
// a module-scoped `.svelte.ts` store backed by localStorage, or the app's
|
|
416
|
+
// own settings. ListShell holds no storage of its own.
|
|
417
|
+
let listOpen = $state(true);
|
|
418
|
+
</script>
|
|
419
|
+
|
|
420
|
+
<!-- padded={false}: the list has to reach the content area's edge, flush
|
|
421
|
+
against the rail, or it reads as a floating card. -->
|
|
422
|
+
<AppShell {nav} currentPath={page.url.pathname} padded={false}>
|
|
423
|
+
<div class="flex min-h-0 flex-1">
|
|
424
|
+
<ListShell title="Waiting on you" bind:open={listOpen}>
|
|
425
|
+
{#snippet actions()}
|
|
426
|
+
<Button size="icon" variant="outline" aria-label="Add documents"><Plus /></Button>
|
|
427
|
+
{/snippet}
|
|
428
|
+
<!-- tabs, group headings, rows: entirely this module's own -->
|
|
429
|
+
</ListShell>
|
|
430
|
+
<div class="min-w-0 flex-1">
|
|
431
|
+
{#if !listOpen}
|
|
432
|
+
<!-- The reopen control is this module's own markup, at whatever
|
|
433
|
+
position it chooses; ListShell never places it. -->
|
|
434
|
+
<Button onclick={() => (listOpen = true)} aria-expanded={listOpen}>
|
|
435
|
+
Waiting on you <Badge>{waitingCount}</Badge>
|
|
436
|
+
</Button>
|
|
437
|
+
{/if}
|
|
438
|
+
<!-- the module's own content -->
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</AppShell>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
| Prop | Purpose |
|
|
445
|
+
| ---------- | ----------------------------------------------------------------------------- |
|
|
446
|
+
| `title` | The list's heading, in the shell's display face. |
|
|
447
|
+
| `open` | Bindable. Not persisted by this component; bind it to what already persists a preference for this module. |
|
|
448
|
+
| `label` | The landmark's accessible name. Defaults to `title`. |
|
|
449
|
+
| `actions` | Leading header actions beside the title: an "Add" button, a filter. |
|
|
450
|
+
|
|
405
451
|
### The content measure
|
|
406
452
|
|
|
407
453
|
How wide the page body may get, chosen once in the layout from a scale of four:
|
|
@@ -1276,6 +1322,19 @@ routes by situation, so it stands outside the situation→component registry
|
|
|
1276
1322
|
for the same reason. `getSessionId()` reading null when telemetry is off is
|
|
1277
1323
|
ordinary, not an error: the report sends with a null `session_id`.
|
|
1278
1324
|
|
|
1325
|
+
## The alerts bell
|
|
1326
|
+
|
|
1327
|
+
`@poodle64/ui/alerts` (`AlertBell`) is app machinery of the same kind: the app
|
|
1328
|
+
factory's layout mounts it once, in the shell's `actions` slot, for callers
|
|
1329
|
+
holding `admin`. It shows what machines raised through the app's `/mcp` `alerts`
|
|
1330
|
+
tool, from the factory's routes under the `endpoint` prop (default
|
|
1331
|
+
`/api/alerts`): `GET <endpoint>/` lists `{ alerts, total, unread }`, `POST
|
|
1332
|
+
<endpoint>/<id>/read` marks one read, `DELETE <endpoint>/<id>` dismisses it.
|
|
1333
|
+
`src/test/alert-bell.test.ts` asserts each call. It renders nothing while no
|
|
1334
|
+
alert is open, polls once a minute and again as its panel opens, and reports a
|
|
1335
|
+
failed read or dismissal with `toast.error`, so `svelte-sonner` is required
|
|
1336
|
+
here. The app factory's `docs/design/alerts.md` carries the design.
|
|
1337
|
+
|
|
1279
1338
|
## The gate: `ds-check-colour-surface`
|
|
1280
1339
|
|
|
1281
1340
|
The package ships the gate that proves its own surface actually resolves in the
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/** One alert as the endpoint lists it: the app factory's `AlertRead`. */
|
|
3
|
+
export type Alert = {
|
|
4
|
+
id: number;
|
|
5
|
+
key: string;
|
|
6
|
+
title: string;
|
|
7
|
+
body: string | null;
|
|
8
|
+
link: string | null;
|
|
9
|
+
raised_at: string;
|
|
10
|
+
read_at: string | null;
|
|
11
|
+
};
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import Bell from '@lucide/svelte/icons/bell';
|
|
16
|
+
import X from '@lucide/svelte/icons/x';
|
|
17
|
+
import { onMount } from 'svelte';
|
|
18
|
+
import { toast } from 'svelte-sonner';
|
|
19
|
+
import { Badge } from '../ui/badge/index.js';
|
|
20
|
+
import { Button } from '../ui/button/index.js';
|
|
21
|
+
import * as Popover from '../ui/popover/index.js';
|
|
22
|
+
import { cn } from '../../utils';
|
|
23
|
+
|
|
24
|
+
// The alerts bell: what machines told this deployment's operator, in the
|
|
25
|
+
// shell's top-bar `actions` slot. App machinery like ReportWidget, mounted
|
|
26
|
+
// once by the layout and talking to the app factory's own routes: GET
|
|
27
|
+
// `<endpoint>/` lists, POST `<endpoint>/<id>/read` marks read, DELETE
|
|
28
|
+
// `<endpoint>/<id>` dismisses.
|
|
29
|
+
//
|
|
30
|
+
// It renders NOTHING while no alert is open, so an app no producer writes
|
|
31
|
+
// to carries no bell. The layout decides who may see it; the server's gate
|
|
32
|
+
// is the real one.
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
/** The app's alerts route, defaulting to the one every factory app serves. */
|
|
36
|
+
endpoint = '/api/alerts'
|
|
37
|
+
}: { endpoint?: string } = $props();
|
|
38
|
+
|
|
39
|
+
const POLL_MS = 60_000;
|
|
40
|
+
const TIMEOUT_MS = 30_000;
|
|
41
|
+
const since = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
|
42
|
+
|
|
43
|
+
let alerts: Alert[] = $state([]);
|
|
44
|
+
let total = $state(0);
|
|
45
|
+
let unread = $state(0);
|
|
46
|
+
let open = $state(false);
|
|
47
|
+
|
|
48
|
+
function call(path: string, method = 'GET'): Promise<Response | null> {
|
|
49
|
+
return fetch(`${endpoint}/${path}`, { method, signal: AbortSignal.timeout(TIMEOUT_MS) }).catch(
|
|
50
|
+
() => null
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A failed poll keeps what the bell last showed and is not worth a toast:
|
|
55
|
+
// the next one, a minute on, tries again.
|
|
56
|
+
async function load() {
|
|
57
|
+
const response = await call('');
|
|
58
|
+
if (!response?.ok) return;
|
|
59
|
+
const data = (await response.json()) as { alerts: Alert[]; total: number; unread: number };
|
|
60
|
+
alerts = data.alerts;
|
|
61
|
+
total = data.total;
|
|
62
|
+
unread = data.unread;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function act(path: string, method: string, failure: string) {
|
|
66
|
+
const response = await call(path, method);
|
|
67
|
+
if (!response?.ok) toast.error(failure);
|
|
68
|
+
await load();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function markRead(alert: Alert) {
|
|
72
|
+
if (alert.read_at) return;
|
|
73
|
+
void act(`${alert.id}/read`, 'POST', 'Could not mark the alert read');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Following a link closes the panel; the read is recorded on the way out.
|
|
77
|
+
function follow(alert: Alert) {
|
|
78
|
+
open = false;
|
|
79
|
+
markRead(alert);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function age(iso: string): string {
|
|
83
|
+
const minutes = Math.round((new Date(iso).getTime() - Date.now()) / 60_000);
|
|
84
|
+
if (minutes > -60) return since.format(minutes, 'minute');
|
|
85
|
+
if (minutes > -60 * 24) return since.format(Math.round(minutes / 60), 'hour');
|
|
86
|
+
return since.format(Math.round(minutes / (60 * 24)), 'day');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onMount(() => {
|
|
90
|
+
void load();
|
|
91
|
+
const timer = setInterval(() => void load(), POLL_MS);
|
|
92
|
+
return () => clearInterval(timer);
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
{#if total > 0}
|
|
97
|
+
<Popover.Root bind:open onOpenChange={(opened) => opened && void load()}>
|
|
98
|
+
<Popover.Trigger>
|
|
99
|
+
{#snippet child({ props })}
|
|
100
|
+
<!-- size-9 is the shell's own top-bar control size (its search and
|
|
101
|
+
theme buttons), which size="icon" follows at one density only. -->
|
|
102
|
+
<Button
|
|
103
|
+
{...props}
|
|
104
|
+
variant="outline"
|
|
105
|
+
size="icon"
|
|
106
|
+
class="relative size-9"
|
|
107
|
+
aria-label={unread > 0 ? `Alerts, ${unread} unread` : 'Alerts'}
|
|
108
|
+
data-testid="alert-bell"
|
|
109
|
+
>
|
|
110
|
+
<Bell />
|
|
111
|
+
{#if unread > 0}
|
|
112
|
+
<Badge
|
|
113
|
+
class="absolute -top-1.5 -right-1.5 h-4 min-w-4 px-1"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
data-testid="alert-bell-unread"
|
|
116
|
+
>
|
|
117
|
+
{unread > 99 ? '99+' : unread}
|
|
118
|
+
</Badge>
|
|
119
|
+
{/if}
|
|
120
|
+
</Button>
|
|
121
|
+
{/snippet}
|
|
122
|
+
</Popover.Trigger>
|
|
123
|
+
|
|
124
|
+
<Popover.Content align="end" class="w-96 p-0">
|
|
125
|
+
<p class="border-b px-4 py-3 text-sm font-semibold">Alerts</p>
|
|
126
|
+
<ul class="divide-y">
|
|
127
|
+
{#each alerts as alert (alert.id)}
|
|
128
|
+
<li class="flex items-start gap-2 px-4 py-3" data-testid="alert-item">
|
|
129
|
+
<span
|
|
130
|
+
class={cn('mt-1.5 size-2 shrink-0 rounded-full', !alert.read_at && 'bg-primary')}
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
></span>
|
|
133
|
+
{#if alert.link}
|
|
134
|
+
<a
|
|
135
|
+
href={alert.link}
|
|
136
|
+
class="min-w-0 flex-1 text-left"
|
|
137
|
+
target={alert.link.startsWith('/') ? undefined : '_blank'}
|
|
138
|
+
rel={alert.link.startsWith('/') ? undefined : 'noreferrer'}
|
|
139
|
+
onclick={() => follow(alert)}
|
|
140
|
+
>
|
|
141
|
+
{@render content(alert)}
|
|
142
|
+
</a>
|
|
143
|
+
{:else}
|
|
144
|
+
<button type="button" class="min-w-0 flex-1 text-left" onclick={() => markRead(alert)}>
|
|
145
|
+
{@render content(alert)}
|
|
146
|
+
</button>
|
|
147
|
+
{/if}
|
|
148
|
+
<Button
|
|
149
|
+
variant="ghost"
|
|
150
|
+
size="icon-xs"
|
|
151
|
+
aria-label="Dismiss"
|
|
152
|
+
onclick={() => void act(String(alert.id), 'DELETE', 'Could not dismiss the alert')}
|
|
153
|
+
data-testid="alert-dismiss"
|
|
154
|
+
>
|
|
155
|
+
<X />
|
|
156
|
+
</Button>
|
|
157
|
+
</li>
|
|
158
|
+
{/each}
|
|
159
|
+
</ul>
|
|
160
|
+
{#if total > alerts.length}
|
|
161
|
+
<p class="text-muted-foreground border-t px-4 py-2 text-xs">
|
|
162
|
+
Showing the newest {alerts.length} of {total}.
|
|
163
|
+
</p>
|
|
164
|
+
{/if}
|
|
165
|
+
</Popover.Content>
|
|
166
|
+
</Popover.Root>
|
|
167
|
+
{/if}
|
|
168
|
+
|
|
169
|
+
{#snippet content(alert: Alert)}
|
|
170
|
+
<span class={cn('block text-sm leading-tight', !alert.read_at && 'font-medium')}>
|
|
171
|
+
{alert.title}
|
|
172
|
+
</span>
|
|
173
|
+
{#if alert.body}
|
|
174
|
+
<span class="text-muted-foreground mt-1 block text-xs whitespace-pre-line">{alert.body}</span>
|
|
175
|
+
{/if}
|
|
176
|
+
<span class="text-muted-foreground mt-1 block text-xs">{age(alert.raised_at)}</span>
|
|
177
|
+
{/snippet}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One alert as the endpoint lists it: the app factory's `AlertRead`. */
|
|
2
|
+
export type Alert = {
|
|
3
|
+
id: number;
|
|
4
|
+
key: string;
|
|
5
|
+
title: string;
|
|
6
|
+
body: string | null;
|
|
7
|
+
link: string | null;
|
|
8
|
+
raised_at: string;
|
|
9
|
+
read_at: string | null;
|
|
10
|
+
};
|
|
11
|
+
type $$ComponentProps = {
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const AlertBell: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
15
|
+
type AlertBell = ReturnType<typeof AlertBell>;
|
|
16
|
+
export default AlertBell;
|
|
@@ -9,6 +9,6 @@ type $$ComponentProps = WithoutChildrenOrChild<DialogPrimitive.RootProps> & With
|
|
|
9
9
|
showCloseButton?: boolean;
|
|
10
10
|
class?: string;
|
|
11
11
|
};
|
|
12
|
-
declare const CommandDialog: import("svelte").Component<$$ComponentProps, {}, "
|
|
12
|
+
declare const CommandDialog: import("svelte").Component<$$ComponentProps, {}, "ref" | "value" | "open">;
|
|
13
13
|
type CommandDialog = ReturnType<typeof CommandDialog>;
|
|
14
14
|
export default CommandDialog;
|
|
@@ -2,7 +2,7 @@ declare const InputGroupInput: import("svelte").Component<(Omit<import("svelte/e
|
|
|
2
2
|
type: "file";
|
|
3
3
|
files?: FileList;
|
|
4
4
|
} | {
|
|
5
|
-
type?: "number" | "month" | "url" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "email" | "date" | "time" | "image" | "datetime-local" | "password" | "range"
|
|
5
|
+
type?: "number" | "month" | "url" | "hidden" | "color" | "submit" | "reset" | "button" | "search" | "checkbox" | "radio" | (string & {}) | "text" | "tel" | "email" | "week" | "date" | "time" | "image" | "datetime-local" | "password" | "range";
|
|
6
6
|
files?: undefined;
|
|
7
7
|
})) & {
|
|
8
8
|
ref?: HTMLElement | null | undefined;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ListShell — a module's working list, folded away and brought back by
|
|
4
|
+
* the app that owns it, beside the content it serves. Not an AppShell
|
|
5
|
+
* slot: `sidebar` was removed 2026.8.11 after two apps disagreed on
|
|
6
|
+
* where a module's own PAGES render (operator ruling 21/08/2026); a
|
|
7
|
+
* working list is data, not navigation, so it is a page-level companion
|
|
8
|
+
* like `SettingsShell`/`ContextColumn` — pair with `padded={false}` on
|
|
9
|
+
* AppShell, as `SettingsShell` requires. `open` is the caller's own
|
|
10
|
+
* bindable state, never persisted here; closed, this renders nothing,
|
|
11
|
+
* and reopening is the caller's own control, placed wherever the module
|
|
12
|
+
* chooses. Hidden below `md` at every state.
|
|
13
|
+
*/
|
|
14
|
+
import type { Snippet } from 'svelte';
|
|
15
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
16
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
17
|
+
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
title,
|
|
21
|
+
label = title,
|
|
22
|
+
open = $bindable(true),
|
|
23
|
+
actions,
|
|
24
|
+
children,
|
|
25
|
+
ref = $bindable(null),
|
|
26
|
+
class: className,
|
|
27
|
+
...restProps
|
|
28
|
+
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
29
|
+
/** The list's heading, in the shell's display face. */
|
|
30
|
+
title: string;
|
|
31
|
+
/**
|
|
32
|
+
* The landmark's accessible name. Defaults to `title` — the visible
|
|
33
|
+
* heading already says what this is, so a module only sets `label`
|
|
34
|
+
* separately when the heading text would not stand alone as a name
|
|
35
|
+
* (WCAG 2.4.1), e.g. a heading that leads with a live count.
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the list is open. Bindable, and NOT persisted by this
|
|
40
|
+
* component — bind it to whatever the module already uses to persist a
|
|
41
|
+
* user preference across a reload.
|
|
42
|
+
*/
|
|
43
|
+
open?: boolean;
|
|
44
|
+
/** Leading header actions beside the title — an "Add" button, a filter. */
|
|
45
|
+
actions?: Snippet;
|
|
46
|
+
/** The list body: tabs, group headings, rows — entirely the module's own. */
|
|
47
|
+
children: Snippet;
|
|
48
|
+
} = $props();
|
|
49
|
+
|
|
50
|
+
const listId = $props.id();
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#if open}
|
|
54
|
+
<aside
|
|
55
|
+
bind:this={ref}
|
|
56
|
+
id={listId}
|
|
57
|
+
aria-label={label}
|
|
58
|
+
class={cn(
|
|
59
|
+
'bg-shell text-shell-foreground border-border hidden w-[25rem] flex-none flex-col border-e md:flex',
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...restProps}
|
|
63
|
+
>
|
|
64
|
+
<div class="flex flex-none items-center gap-2 px-5 pt-5 pb-3">
|
|
65
|
+
<h2 class="font-display text-display flex-1 truncate font-semibold tracking-tight">
|
|
66
|
+
{title}
|
|
67
|
+
</h2>
|
|
68
|
+
{#if actions}{@render actions()}{/if}
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onclick={() => (open = false)}
|
|
72
|
+
aria-expanded="true"
|
|
73
|
+
aria-controls={listId}
|
|
74
|
+
aria-label={`Fold ${label} away`}
|
|
75
|
+
title={`Fold ${label} away`}
|
|
76
|
+
class="text-shell-muted-foreground hover:text-shell-foreground border-border grid size-9 flex-none place-items-center rounded-md border transition-colors"
|
|
77
|
+
>
|
|
78
|
+
<PanelLeftClose class="size-4" />
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
|
82
|
+
{@render children()}
|
|
83
|
+
</div>
|
|
84
|
+
</aside>
|
|
85
|
+
{/if}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListShell — a module's working list, folded away and brought back by
|
|
3
|
+
* the app that owns it, beside the content it serves. Not an AppShell
|
|
4
|
+
* slot: `sidebar` was removed 2026.8.11 after two apps disagreed on
|
|
5
|
+
* where a module's own PAGES render (operator ruling 21/08/2026); a
|
|
6
|
+
* working list is data, not navigation, so it is a page-level companion
|
|
7
|
+
* like `SettingsShell`/`ContextColumn` — pair with `padded={false}` on
|
|
8
|
+
* AppShell, as `SettingsShell` requires. `open` is the caller's own
|
|
9
|
+
* bindable state, never persisted here; closed, this renders nothing,
|
|
10
|
+
* and reopening is the caller's own control, placed wherever the module
|
|
11
|
+
* chooses. Hidden below `md` at every state.
|
|
12
|
+
*/
|
|
13
|
+
import type { Snippet } from 'svelte';
|
|
14
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
15
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
16
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
17
|
+
/** The list's heading, in the shell's display face. */
|
|
18
|
+
title: string;
|
|
19
|
+
/**
|
|
20
|
+
* The landmark's accessible name. Defaults to `title` — the visible
|
|
21
|
+
* heading already says what this is, so a module only sets `label`
|
|
22
|
+
* separately when the heading text would not stand alone as a name
|
|
23
|
+
* (WCAG 2.4.1), e.g. a heading that leads with a live count.
|
|
24
|
+
*/
|
|
25
|
+
label?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the list is open. Bindable, and NOT persisted by this
|
|
28
|
+
* component — bind it to whatever the module already uses to persist a
|
|
29
|
+
* user preference across a reload.
|
|
30
|
+
*/
|
|
31
|
+
open?: boolean;
|
|
32
|
+
/** Leading header actions beside the title — an "Add" button, a filter. */
|
|
33
|
+
actions?: Snippet;
|
|
34
|
+
/** The list body: tabs, group headings, rows — entirely the module's own. */
|
|
35
|
+
children: Snippet;
|
|
36
|
+
};
|
|
37
|
+
declare const ListShell: import("svelte").Component<$$ComponentProps, {}, "ref" | "open">;
|
|
38
|
+
type ListShell = ReturnType<typeof ListShell>;
|
|
39
|
+
export default ListShell;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const Select: import("svelte").Component<import("bits-ui").SelectRootPropsWithoutHTML, {}, "
|
|
1
|
+
declare const Select: import("svelte").Component<import("bits-ui").SelectRootPropsWithoutHTML, {}, "value" | "open">;
|
|
2
2
|
type Select = ReturnType<typeof Select>;
|
|
3
3
|
export default Select;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.21",
|
|
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",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./dist/components/feedback/index.d.ts",
|
|
38
38
|
"svelte": "./dist/components/feedback/index.js"
|
|
39
39
|
},
|
|
40
|
+
"./alerts": {
|
|
41
|
+
"types": "./dist/components/alerts/index.d.ts",
|
|
42
|
+
"svelte": "./dist/components/alerts/index.js"
|
|
43
|
+
},
|
|
40
44
|
"./*": {
|
|
41
45
|
"types": "./dist/components/ui/*/index.d.ts",
|
|
42
46
|
"svelte": "./dist/components/ui/*/index.js"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"package": "@poodle64/ui",
|
|
4
|
-
"version": "2026.9.
|
|
4
|
+
"version": "2026.9.21",
|
|
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": 61,
|
|
8
8
|
"situationCount": 17
|
|
9
9
|
},
|
|
10
10
|
"situations": [
|
|
@@ -298,7 +298,7 @@
|
|
|
298
298
|
{
|
|
299
299
|
"key": "page-frame",
|
|
300
300
|
"title": "The frame every route shares",
|
|
301
|
-
"description": "The standing chrome — the app shell and its nav, the one page-title treatment, the persistent right-hand context column.",
|
|
301
|
+
"description": "The standing chrome — the app shell and its nav, the one page-title treatment, the persistent right-hand context column, a foldable working list beside the page's own content.",
|
|
302
302
|
"components": [
|
|
303
303
|
{
|
|
304
304
|
"name": "AppShell",
|
|
@@ -335,6 +335,13 @@
|
|
|
335
335
|
"props": "stats, statsTitle?, statsInfo?, detail?, ariaLabel?",
|
|
336
336
|
"insteadOf": "a hand-built right-hand <aside> with a stat card and a detail pane"
|
|
337
337
|
},
|
|
338
|
+
{
|
|
339
|
+
"name": "ListShell",
|
|
340
|
+
"dir": "list-shell",
|
|
341
|
+
"import": "@poodle64/ui/list-shell",
|
|
342
|
+
"props": "title, children, label?, open?, actions?",
|
|
343
|
+
"insteadOf": "a module hand-rolling its own foldable second column beside the rail — a queue, an inbox, a working list"
|
|
344
|
+
},
|
|
338
345
|
{
|
|
339
346
|
"name": "AppIdentity",
|
|
340
347
|
"dir": "app-identity",
|
|
@@ -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.9.
|
|
4
|
+
Generated from `@poodle64/ui@2026.9.21`. 61 components, 17 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
|
|
|
@@ -129,7 +129,7 @@ _One page at a time, fitted to its pane both ways so an A4 statement and a tall
|
|
|
129
129
|
|
|
130
130
|
## The frame every route shares
|
|
131
131
|
|
|
132
|
-
_The standing chrome — the app shell and its nav, the one page-title treatment, the persistent right-hand context column._
|
|
132
|
+
_The standing chrome — the app shell and its nav, the one page-title treatment, the persistent right-hand context column, a foldable working list beside the page's own content._
|
|
133
133
|
|
|
134
134
|
| Component | Import | Key props | Reach for it instead of |
|
|
135
135
|
| --- | --- | --- | --- |
|
|
@@ -138,6 +138,7 @@ _The standing chrome — the app shell and its nav, the one page-title treatment
|
|
|
138
138
|
| `Segmented` | `@poodle64/ui/segmented` | `value, options, label, size?` | a hand-built row of aria-pressed buttons for a small fixed choice |
|
|
139
139
|
| `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) |
|
|
140
140
|
| `ContextColumn` | `@poodle64/ui/context-column` | `stats, statsTitle?, statsInfo?, detail?, ariaLabel?` | a hand-built right-hand <aside> with a stat card and a detail pane |
|
|
141
|
+
| `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 |
|
|
141
142
|
| `AppIdentity` | `@poodle64/ui/app-identity` | `user, workspace?, role?, entitlements?, onSignOut, onAccountSettings?, onSwitchTheme?` | a hand-rolled account/avatar dropdown wired to raw DropdownMenu.* + Avatar.* |
|
|
142
143
|
|
|
143
144
|
## The settings destination
|