@poodle64/ui 2026.9.20 → 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 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
 
@@ -404,14 +406,6 @@ wrapper: the pane stacks them on the package's section rhythm itself.
404
406
 
405
407
  ### A working list beside a page's own content
406
408
 
407
- AppShell has no second-column slot, on purpose: a module's own **pages**
408
- belong in the rail (`NavItem.children`), never a hand-rolled column beside it.
409
- A module's **data** (a queue, an inbox, a working list of things waiting on
410
- the user) is a different situation, not navigation, so it belongs in the page
411
- beside the content it serves, exactly as the settings destination does.
412
- `ListShell` is that shape: a foldable list, in the rail's own chrome, that
413
- vanishes entirely when closed and hands the freed width back to the page.
414
-
415
409
  ```svelte
416
410
  <script lang="ts">
417
411
  import AppShell from '@poodle64/ui/app-shell';
@@ -454,12 +448,6 @@ vanishes entirely when closed and hands the freed width back to the page.
454
448
  | `label` | The landmark's accessible name. Defaults to `title`. |
455
449
  | `actions` | Leading header actions beside the title: an "Add" button, a filter. |
456
450
 
457
- Width is fixed at 25rem (400px) and not a prop, the same anti-drift discipline
458
- `SettingsShell` applies to its own 240px: one width, chosen once, rather than
459
- every module guessing its own. Below `md` this never renders, at either state,
460
- since a 400px column has nowhere to go beside a rail that is already a drawer
461
- there; a module's phone view is its own route.
462
-
463
451
  ### The content measure
464
452
 
465
453
  How wide the page body may get, chosen once in the layout from a scale of four:
@@ -1334,6 +1322,19 @@ routes by situation, so it stands outside the situation→component registry
1334
1322
  for the same reason. `getSessionId()` reading null when telemetry is off is
1335
1323
  ordinary, not an error: the report sends with a null `session_id`.
1336
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
+
1337
1338
  ## The gate: `ds-check-colour-surface`
1338
1339
 
1339
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;
@@ -0,0 +1,2 @@
1
+ import AlertBell, { type Alert } from './alert-bell.svelte';
2
+ export { AlertBell, AlertBell as default, type Alert };
@@ -0,0 +1,2 @@
1
+ import AlertBell, {} from './alert-bell.svelte';
2
+ export { AlertBell, AlertBell as default };
@@ -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, {}, "open" | "ref" | "value">;
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" | "week";
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;
@@ -1,79 +1,15 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * ListShell — a module's working list, folded away and brought back by the
4
- * app that owns it, beside the content it serves.
5
- *
6
- * AppShell carries no second-column slot for this on purpose: `sidebar` was
7
- * such a slot until 2026.8.11, and what it produced was two apps rendering a
8
- * module's own PAGES beside the rail while the other seven rendered them
9
- * inside it — the shell shape differing per app, and in one app per module.
10
- * Operator ruling, 21/08/2026: no app supports an additional sidebar; a
11
- * section's own pages roll out beneath it in the rail (`NavItem.children`).
12
- * That ruling is about NAVIGATION. A working list is not navigation — it is
13
- * data the module owns (a queue of things waiting on the user), and
14
- * `20-sveltekit-frontend.md` already answers where that belongs: "a column
15
- * that is not NavItem-shaped … belongs in the page beside the content it
16
- * serves." `ContextColumn` is the household's other proof this is a real,
17
- * sanctioned shape — a page-level flanking `<aside>`, not a shell slot.
18
- *
19
- * So this is a companion to AppShell, exactly as `SettingsShell` is: the
20
- * shell provides ONE chrome (the rail, the bar, the content frame), and any
21
- * module reaches for this to add a foldable list beside its own content,
22
- * inside AppShell's content area. Every module gets the same list, not a
23
- * hand-rolled column per module — which is the actual thing the 21/08/2026
24
- * ruling exists to prevent, still true one level down.
25
- *
26
- * USE IT WITH `padded={false}` on AppShell, exactly as `SettingsShell`
27
- * requires: the list has to reach the content area's edge — flush against
28
- * the rail, with no gap — or it reads as a floating card rather than chrome.
29
- * It paints itself in the rail's OWN chrome tokens (`bg-shell`,
30
- * `text-shell-foreground`) for the same reason: a list that is chrome reads
31
- * as chrome, not as a panel someone built inside the page.
32
- *
33
- * Width is fixed at 25rem (400px), measured off the operator's approved
34
- * mockup (radar-hooves/godswood#839, Focus-List.dc.html) rather than picked,
35
- * and not exposed as a prop — the same anti-drift discipline `SettingsShell`
36
- * applies to its own 240px: one width, chosen once, or ten modules each
37
- * guess their own.
38
- *
39
- * Open/closed is the CALLER's state (`open`, bindable), the same way
40
- * AppShell's own `collapsed` is: bind it to persist the choice across
41
- * reloads (a module-scoped `$state` in a `.svelte.ts` file, synced to
42
- * `localStorage` — `svelte-basics.md` §shared state — or the app's own
43
- * settings store). This component holds no storage of its own, on purpose:
44
- * a module that already persists user preferences server-side should not
45
- * gain a second, disagreeing mechanism here.
46
- *
47
- * When `open` is false this renders NOTHING — no placeholder, no rail — so
48
- * the content beside it gets the freed width back, exactly like the
49
- * operator's mockup (Focus-Run.dc.html). Reopening it is deliberately NOT
50
- * this component's job: the fold toggle only exists while the list is open
51
- * (inside its own header, where the mockup puts it); the reopen affordance
52
- * — a "Waiting on you 338" pill, a bare icon, wherever the module wants it —
53
- * is the CALLER's own markup, driving the same bound `open`. Fixing that
54
- * button's position here would be exactly the one-shape-per-module drift
55
- * this file exists to end; the module's own page already earned that
56
- * decision by owning the content beside this list. A caller's reopen
57
- * control should carry `aria-expanded={open}` itself, so the two controls
58
- * (this one, and the caller's) read as one continuous toggle to a screen
59
- * reader even though only one is ever in the document at a time:
60
- *
61
- * <button onclick={() => (open = true)} aria-expanded={open}>
62
- * Waiting on you <Badge>{waitingCount}</Badge>
63
- * </button>
64
- *
65
- * Below `md` this never renders, at either state: the rail is already a
66
- * drawer there, and a 400px column has nowhere to go beside it. Godswood's
67
- * own answer is a dedicated phone route for the same list (Phone.dc.html);
68
- * that choice is the app's, not this component's — it only gets out of the
69
- * way so the app's own mobile route can render undisturbed.
70
- *
71
- * The header is chrome (title, the fold toggle, and an optional leading
72
- * `actions` snippet for a module-specific control — an "Add" button in the
73
- * mockup). Everything below it — tabs, grouped headings, the rows
74
- * themselves — is the module's own `children`, exactly as `SettingsShell`
75
- * hands its list body to the caller rather than describing a row shape of
76
- * its own. A queue is not the only thing this list will ever hold.
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.
77
13
  */
78
14
  import type { Snippet } from 'svelte';
79
15
  import type { HTMLAttributes } from 'svelte/elements';
@@ -1,78 +1,14 @@
1
1
  /**
2
- * ListShell — a module's working list, folded away and brought back by the
3
- * app that owns it, beside the content it serves.
4
- *
5
- * AppShell carries no second-column slot for this on purpose: `sidebar` was
6
- * such a slot until 2026.8.11, and what it produced was two apps rendering a
7
- * module's own PAGES beside the rail while the other seven rendered them
8
- * inside it — the shell shape differing per app, and in one app per module.
9
- * Operator ruling, 21/08/2026: no app supports an additional sidebar; a
10
- * section's own pages roll out beneath it in the rail (`NavItem.children`).
11
- * That ruling is about NAVIGATION. A working list is not navigation — it is
12
- * data the module owns (a queue of things waiting on the user), and
13
- * `20-sveltekit-frontend.md` already answers where that belongs: "a column
14
- * that is not NavItem-shaped … belongs in the page beside the content it
15
- * serves." `ContextColumn` is the household's other proof this is a real,
16
- * sanctioned shape — a page-level flanking `<aside>`, not a shell slot.
17
- *
18
- * So this is a companion to AppShell, exactly as `SettingsShell` is: the
19
- * shell provides ONE chrome (the rail, the bar, the content frame), and any
20
- * module reaches for this to add a foldable list beside its own content,
21
- * inside AppShell's content area. Every module gets the same list, not a
22
- * hand-rolled column per module — which is the actual thing the 21/08/2026
23
- * ruling exists to prevent, still true one level down.
24
- *
25
- * USE IT WITH `padded={false}` on AppShell, exactly as `SettingsShell`
26
- * requires: the list has to reach the content area's edge — flush against
27
- * the rail, with no gap — or it reads as a floating card rather than chrome.
28
- * It paints itself in the rail's OWN chrome tokens (`bg-shell`,
29
- * `text-shell-foreground`) for the same reason: a list that is chrome reads
30
- * as chrome, not as a panel someone built inside the page.
31
- *
32
- * Width is fixed at 25rem (400px), measured off the operator's approved
33
- * mockup (radar-hooves/godswood#839, Focus-List.dc.html) rather than picked,
34
- * and not exposed as a prop — the same anti-drift discipline `SettingsShell`
35
- * applies to its own 240px: one width, chosen once, or ten modules each
36
- * guess their own.
37
- *
38
- * Open/closed is the CALLER's state (`open`, bindable), the same way
39
- * AppShell's own `collapsed` is: bind it to persist the choice across
40
- * reloads (a module-scoped `$state` in a `.svelte.ts` file, synced to
41
- * `localStorage` — `svelte-basics.md` §shared state — or the app's own
42
- * settings store). This component holds no storage of its own, on purpose:
43
- * a module that already persists user preferences server-side should not
44
- * gain a second, disagreeing mechanism here.
45
- *
46
- * When `open` is false this renders NOTHING — no placeholder, no rail — so
47
- * the content beside it gets the freed width back, exactly like the
48
- * operator's mockup (Focus-Run.dc.html). Reopening it is deliberately NOT
49
- * this component's job: the fold toggle only exists while the list is open
50
- * (inside its own header, where the mockup puts it); the reopen affordance
51
- * — a "Waiting on you 338" pill, a bare icon, wherever the module wants it —
52
- * is the CALLER's own markup, driving the same bound `open`. Fixing that
53
- * button's position here would be exactly the one-shape-per-module drift
54
- * this file exists to end; the module's own page already earned that
55
- * decision by owning the content beside this list. A caller's reopen
56
- * control should carry `aria-expanded={open}` itself, so the two controls
57
- * (this one, and the caller's) read as one continuous toggle to a screen
58
- * reader even though only one is ever in the document at a time:
59
- *
60
- * <button onclick={() => (open = true)} aria-expanded={open}>
61
- * Waiting on you <Badge>{waitingCount}</Badge>
62
- * </button>
63
- *
64
- * Below `md` this never renders, at either state: the rail is already a
65
- * drawer there, and a 400px column has nowhere to go beside it. Godswood's
66
- * own answer is a dedicated phone route for the same list (Phone.dc.html);
67
- * that choice is the app's, not this component's — it only gets out of the
68
- * way so the app's own mobile route can render undisturbed.
69
- *
70
- * The header is chrome (title, the fold toggle, and an optional leading
71
- * `actions` snippet for a module-specific control — an "Add" button in the
72
- * mockup). Everything below it — tabs, grouped headings, the rows
73
- * themselves — is the module's own `children`, exactly as `SettingsShell`
74
- * hands its list body to the caller rather than describing a row shape of
75
- * its own. A queue is not the only thing this list will ever hold.
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.
76
12
  */
77
13
  import type { Snippet } from 'svelte';
78
14
  import type { HTMLAttributes } from 'svelte/elements';
@@ -98,6 +34,6 @@ type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLElement>> & {
98
34
  /** The list body: tabs, group headings, rows — entirely the module's own. */
99
35
  children: Snippet;
100
36
  };
101
- declare const ListShell: import("svelte").Component<$$ComponentProps, {}, "open" | "ref">;
37
+ declare const ListShell: import("svelte").Component<$$ComponentProps, {}, "ref" | "open">;
102
38
  type ListShell = ReturnType<typeof ListShell>;
103
39
  export default ListShell;
@@ -1,3 +1,3 @@
1
- declare const Select: import("svelte").Component<import("bits-ui").SelectRootPropsWithoutHTML, {}, "open" | "value">;
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.20",
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,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.9.20",
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
7
  "componentCount": 61,
@@ -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.20`. 61 components, 17 situations.
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