@poodle64/ui 2026.9.16 → 2026.9.17
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 +124 -24
- package/dist/components/ui/app-shell/app-shell.svelte +2 -2
- package/dist/components/ui/app-shell/padding.d.ts +20 -0
- package/dist/components/ui/app-shell/padding.js +20 -0
- package/dist/components/ui/panel/panel.svelte +97 -5
- package/dist/components/ui/panel/panel.svelte.d.ts +36 -1
- package/dist/components/ui/settings-shell/index.d.ts +2 -0
- package/dist/components/ui/settings-shell/index.js +2 -0
- package/dist/components/ui/settings-shell/settings-shell.svelte +111 -0
- package/dist/components/ui/settings-shell/settings-shell.svelte.d.ts +78 -0
- package/dist/styles.css +68 -13
- package/package.json +1 -1
- package/registry/component-map.json +19 -5
- package/registry/component-map.md +10 -2
package/README.md
CHANGED
|
@@ -85,30 +85,31 @@ app — the page chrome is. These are the cross-cutting surfaces every route
|
|
|
85
85
|
composes from, so a household app gets its layout language from the package
|
|
86
86
|
rather than rebuilding it:
|
|
87
87
|
|
|
88
|
-
| Import | What it is
|
|
89
|
-
| ----------------------------------------------- |
|
|
90
|
-
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon` snippets, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar.
|
|
91
|
-
| `panel` | The generic titled card: optional icon, subtitle
|
|
92
|
-
| `detail-panel` | The entity-detail surface: header with icon/eyebrow/title/`StatusBadge`/close, scrollable body, footer of actions.
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `dialog
|
|
96
|
-
| `
|
|
97
|
-
| `stat-
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
88
|
+
| Import | What it is |
|
|
89
|
+
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon` snippets, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar. |
|
|
91
|
+
| `panel` | The generic titled card, and the settings section: optional icon, a clamped one-line `subtitle` or a wrapping `description`, trailing `action`s, a body that can opt out of padding, an optional `footer` strip for Save/Cancel, and `tone="destructive"` for a danger zone. There is no `SettingsSection`; see [The settings destination](#the-settings-destination). |
|
|
92
|
+
| `detail-panel` | The entity-detail surface: header with icon/eyebrow/title/`StatusBadge`/close, scrollable body, footer of actions. |
|
|
93
|
+
| `settings-shell` | The settings destination: a grouped section list of `NavItem` rows beside the section you are reading, both scrolling independently from `md`, stacked below it. Render it inside `AppShell` with `padded={false}`. |
|
|
94
|
+
| `context-column` | The persistent right-hand column: a standing `StatList` plus an optional detail that flows in on select. Below `xl` (1280px), where the column has nowhere to sit, the same content opens from a floating trigger instead of disappearing. |
|
|
95
|
+
| `app-dialog` | The dialogue frame: titled header, scrollable body, footer action bar, five sizes (`xs`…`xl`), and an `onOpenChange` for the dismissals the caller did not drive. |
|
|
96
|
+
| `dialog-section` | One section of a dialogue body; adjacent sections are divided automatically. |
|
|
97
|
+
| `stat-card` | A single metric that earns its space (label, value, unit, sub, status dot, and `valueTone` to colour the figure itself). |
|
|
98
|
+
| `stat-list` | A route's low-context integers as a label→value list. Zero-aware: `muted` keeps a healthy zero quiet. |
|
|
99
|
+
| `arc-gauge` | A radial capacity/percentage ring for a single 0–100 metric, in a footprint too compact for a `stat-card`. |
|
|
100
|
+
| `bar-row` | A labelled horizontal bar with a trailing tabular value, for a ranked list (usage, rank, token burn). |
|
|
101
|
+
| `scorecard` | A compact 0/1/2 dot-row health strip for several independent checks read at a glance. |
|
|
102
|
+
| `sparkline` | An inline multi-series area+line trend for a row or card with room for a trend but not a full chart. |
|
|
103
|
+
| `status` / `status-badge` | The fixed five-state vocabulary (`success \| warning \| error \| info \| neutral`) and the one state chip, with `pulse` for a state still in motion and `class` for placement. `status-badge` alone also accepts `'primary'`, a brand-emphasis extension outside the shared vocabulary — `stat-card`, `stat-list` and `data-table-toolbar` never see it. |
|
|
104
|
+
| `empty-state` / `error-state` / `loading-state` | The shared blank, error and loading surfaces. Never hand-roll one. |
|
|
105
|
+
| `info-tip` | One tooltip pattern: a small info trigger, or wrap an existing affordance as children. |
|
|
106
|
+
| `data-table-toolbar` | Search field plus filter-chip groups for a TanStack table. Owns no state; fires callbacks. |
|
|
107
|
+
| `schema-form` | The renderer for a config object the server described: a JSON Schema plus a JSON Forms UI Schema in, a form out. Anything it cannot dispatch renders flagged, never blank; see [Server-described forms](#server-described-forms) below. |
|
|
108
|
+
| `data-table-tanstack` | The TanStack-backed table: global search, column filters, master-detail row select, opt-in bulk selection, responsive column hiding, a first-class empty branch. |
|
|
109
|
+
| `library-browse` | The faceted catalogue index: search, facet rail, active filter chips, the document table and a pager, all over plain props (`LibraryDocument[]`, `LibraryFacet[]`). The page fetches, maps and routes; the component renders. Library data is fixed components because its shape is stable; configuration is `schema-form` because its shape changes (#30). |
|
|
110
|
+
| `collection-detail` | One collection's surface: identity (`detail-panel`), an at-a-glance `stat-list`, and the documents it holds, with `actions` and `children` slots for the app-specific rest. |
|
|
111
|
+
| `document-detail` | One document's surface: identity fields, locations, tags and collection memberships, each section present exactly when its data is. Membership links are the app's own via `collectionHref`. |
|
|
112
|
+
| `search-results` | A ranked retrieval answer: title, the matched passage with accent-tinted highlights, source chip, mapped state and a mono relevance figure per hit; plus the before-any-search and matched-nothing empties. |
|
|
112
113
|
|
|
113
114
|
**The application shell** (`app-shell`, `command-palette`). Page chrome is not
|
|
114
115
|
what makes an app feel like an app either — the shell is. Five household
|
|
@@ -302,6 +303,105 @@ palette:
|
|
|
302
303
|
`AppNav` used outside the chrome (a navigation list inside a page) keeps the
|
|
303
304
|
page's ink, so inverting the rail does not drag it along.
|
|
304
305
|
|
|
306
|
+
### The settings destination
|
|
307
|
+
|
|
308
|
+
Settings is the one destination with a second column, and it takes one shape in
|
|
309
|
+
every app. `SettingsShell` is that shape: a grouped section list beside the
|
|
310
|
+
section you are reading, both panes scrolling independently from `md`.
|
|
311
|
+
|
|
312
|
+
```svelte
|
|
313
|
+
<script lang="ts">
|
|
314
|
+
import { page } from '$app/state';
|
|
315
|
+
import AppShell from '@poodle64/ui/app-shell';
|
|
316
|
+
import type { NavSource } from '@poodle64/ui/app-shell';
|
|
317
|
+
import SettingsShell from '@poodle64/ui/settings-shell';
|
|
318
|
+
import Users from '@lucide/svelte/icons/users';
|
|
319
|
+
import Gavel from '@lucide/svelte/icons/gavel';
|
|
320
|
+
|
|
321
|
+
// Grouped by WHOSE setting it is: the person's, then the workspace's. It is
|
|
322
|
+
// the only split a reader can predict: "yours" changes what you see, "this
|
|
323
|
+
// company" changes what everyone sees. A group with no items renders nothing,
|
|
324
|
+
// so an app with no personal sections yet can leave it in the list.
|
|
325
|
+
const sections: NavSource = [
|
|
326
|
+
{ heading: 'Yours', items: [] },
|
|
327
|
+
{
|
|
328
|
+
heading: 'This company',
|
|
329
|
+
items: [
|
|
330
|
+
{ label: 'Users', href: '/settings/users', icon: Users },
|
|
331
|
+
{ label: 'Delegation limits', href: '/settings/delegation-limits', icon: Gavel }
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
];
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<!-- padded={false}: the list's rule and tint have to reach the content area's
|
|
338
|
+
edges, and SettingsShell pads its own content pane to the same rhythm. -->
|
|
339
|
+
<AppShell {nav} currentPath={page.url.pathname} settingsHref="/settings" padded={false}>
|
|
340
|
+
<SettingsShell {sections} currentPath={page.url.pathname}>
|
|
341
|
+
{@render children()}
|
|
342
|
+
</SettingsShell>
|
|
343
|
+
</AppShell>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| Prop | Purpose |
|
|
347
|
+
| ------------- | ------------------------------------------------------------------------------ |
|
|
348
|
+
| `sections` | Groups of `NavItem`, bare items, or a mix: the rail's own vocabulary. |
|
|
349
|
+
| `currentPath` | Which row is the page. Prefix-matched on the segment boundary, as the rail is. |
|
|
350
|
+
| `listLabel` | The list's accessible name. Defaults to `Settings sections`. |
|
|
351
|
+
|
|
352
|
+
A row at the settings ROOT (`/settings`) needs `exact: true`, or its prefix match
|
|
353
|
+
keeps it lit on every section beneath it alongside the section's own row.
|
|
354
|
+
|
|
355
|
+
Three things are settled here and are not props:
|
|
356
|
+
|
|
357
|
+
**Rows are icon plus label, with nothing beneath.** A description long enough to
|
|
358
|
+
be worth reading does not fit a 240px column; every one of them truncated to an
|
|
359
|
+
ellipsis and told the reader less than the label already did.
|
|
360
|
+
|
|
361
|
+
**There is no "Settings" heading above the list.** The bar already names the
|
|
362
|
+
section you are on, so a heading here is the same word twice.
|
|
363
|
+
|
|
364
|
+
**Below `md` the list stacks above the content and the page scrolls as one.** A
|
|
365
|
+
horizontally scrollable strip of the same rows was the alternative and loses on
|
|
366
|
+
the requirement it was meant to serve: it fits about two and a half rows at
|
|
367
|
+
375px, so the rest are behind a sideways swipe with no scrollbar advertising
|
|
368
|
+
them — a swipe _then_ a tap, where a stack is a scroll the reader is already
|
|
369
|
+
doing. It also has to drop the group labels for width.
|
|
370
|
+
|
|
371
|
+
Density works here as it does in the rail, because the list _is_ the rail's
|
|
372
|
+
list: the controls inside a section ride the `--ds-control-*` ramp and shrink
|
|
373
|
+
under `data-ds-density="compact"`, while a section row keeps `AppNav`'s own
|
|
374
|
+
36px whatever the density. A row that shrank here and not in the rail would be
|
|
375
|
+
the drift, not the fix.
|
|
376
|
+
|
|
377
|
+
#### The sections themselves are `Panel`s
|
|
378
|
+
|
|
379
|
+
There is no `SettingsSection`, and deliberately so: it would have been `Panel`
|
|
380
|
+
with a different name (same surface, same header, same title), and the estate
|
|
381
|
+
would then hold two answers to "a titled section of a route", which is the drift
|
|
382
|
+
this package exists to end. What settings actually needed was three props, and
|
|
383
|
+
all three serve any route:
|
|
384
|
+
|
|
385
|
+
```svelte
|
|
386
|
+
<Panel title="Delegation limits"
|
|
387
|
+
description="Who can approve a bill, and up to what value.">
|
|
388
|
+
<!-- controls -->
|
|
389
|
+
{#snippet footer()}
|
|
390
|
+
<Button variant="ghost" size="sm">Cancel</Button>
|
|
391
|
+
<Button size="sm">Save</Button>
|
|
392
|
+
{/snippet}
|
|
393
|
+
</Panel>
|
|
394
|
+
|
|
395
|
+
<Panel title="Delete this workspace" tone="destructive" icon={Trash2}
|
|
396
|
+
description="Every project, document and filing decision goes with it.">
|
|
397
|
+
<Button variant="destructive" size="sm">Delete</Button>
|
|
398
|
+
</Panel>
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`description` wraps; `subtitle` is the one-line qualifier beside the title and
|
|
402
|
+
truncates. Carry one or the other. A page writes a run of `Panel`s and no
|
|
403
|
+
wrapper: the pane stacks them on the package's section rhythm itself.
|
|
404
|
+
|
|
305
405
|
### The content measure
|
|
306
406
|
|
|
307
407
|
How wide the page body may get, chosen once in the layout from a scale of four:
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
*/
|
|
80
80
|
import type { Snippet } from 'svelte';
|
|
81
81
|
import type { ShellMeasure } from './measure.js';
|
|
82
|
+
import { CONTENT_PADDING } from './padding.js';
|
|
82
83
|
import type { ShellTexture } from './texture.js';
|
|
83
84
|
import { toggleMode } from 'mode-watcher';
|
|
84
85
|
import Menu from '@lucide/svelte/icons/menu';
|
|
@@ -917,8 +918,7 @@
|
|
|
917
918
|
'flex min-h-0 min-w-0 flex-1 flex-col',
|
|
918
919
|
'w-full',
|
|
919
920
|
capped && 'ds-shell-measure',
|
|
920
|
-
padded &&
|
|
921
|
-
'px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12'
|
|
921
|
+
padded && CONTENT_PADDING
|
|
922
922
|
)}
|
|
923
923
|
data-measure={capped ?? undefined}
|
|
924
924
|
>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page padding the shell gives its content area.
|
|
3
|
+
*
|
|
4
|
+
* A constant rather than a literal in app-shell.svelte, because it is no longer
|
|
5
|
+
* declared in one place: SettingsShell runs inside an UNPADDED content area (its
|
|
6
|
+
* section list has to reach the area's edges to read as a pane rather than a
|
|
7
|
+
* floating card) and then pads its own content pane, which must land on the same
|
|
8
|
+
* rhythm as every other page or the settings destination is the one route whose
|
|
9
|
+
* text sits somewhere else.
|
|
10
|
+
*
|
|
11
|
+
* Utilities rather than a class in styles.css, deliberately: both consumers pass
|
|
12
|
+
* this through `cn()`, so an app overriding the padding — `mainClass="px-0"`, a
|
|
13
|
+
* flush table — still resolves through tailwind-merge. A base-layer class would
|
|
14
|
+
* be invisible to it, and the utility would win by layer order instead of by
|
|
15
|
+
* being last, which is the cascade trap recorded against `.ds-nav`'s ink.
|
|
16
|
+
*
|
|
17
|
+
* The bottom pad carries `env(safe-area-inset-bottom)` so the last row of a page
|
|
18
|
+
* clears a home-indicator gesture bar.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CONTENT_PADDING = "px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page padding the shell gives its content area.
|
|
3
|
+
*
|
|
4
|
+
* A constant rather than a literal in app-shell.svelte, because it is no longer
|
|
5
|
+
* declared in one place: SettingsShell runs inside an UNPADDED content area (its
|
|
6
|
+
* section list has to reach the area's edges to read as a pane rather than a
|
|
7
|
+
* floating card) and then pads its own content pane, which must land on the same
|
|
8
|
+
* rhythm as every other page or the settings destination is the one route whose
|
|
9
|
+
* text sits somewhere else.
|
|
10
|
+
*
|
|
11
|
+
* Utilities rather than a class in styles.css, deliberately: both consumers pass
|
|
12
|
+
* this through `cn()`, so an app overriding the padding — `mainClass="px-0"`, a
|
|
13
|
+
* flush table — still resolves through tailwind-merge. A base-layer class would
|
|
14
|
+
* be invisible to it, and the utility would win by layer order instead of by
|
|
15
|
+
* being last, which is the cascade trap recorded against `.ds-nav`'s ink.
|
|
16
|
+
*
|
|
17
|
+
* The bottom pad carries `env(safe-area-inset-bottom)` so the last row of a page
|
|
18
|
+
* clears a home-indicator gesture bar.
|
|
19
|
+
*/
|
|
20
|
+
export const CONTENT_PADDING = 'px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] sm:px-6 md:px-8 md:pt-7 md:pb-[calc(1.75rem+env(safe-area-inset-bottom))] 2xl:px-12';
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet, Component } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
import type
|
|
4
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
5
5
|
|
|
6
6
|
// The generic titled card. Where DetailPanel is the *entity* surface (and owns
|
|
7
7
|
// a status and a close affordance), Panel is the plain sectioning card any
|
|
8
8
|
// route reaches for: an optional header with icon, subtitle and trailing
|
|
9
9
|
// actions, over a body that can opt out of padding for a flush table.
|
|
10
|
+
//
|
|
11
|
+
// It is also the settings SECTION, and there is no second component for that.
|
|
12
|
+
// A `SettingsSection` would have been this card with a different name — same
|
|
13
|
+
// surface, same header, same title — and the estate would then hold two
|
|
14
|
+
// answers to "a titled section of a route", which is the drift this package
|
|
15
|
+
// exists to end. What settings actually needed that this did not have is three
|
|
16
|
+
// props, and all three serve any route: a description that wraps, a footer for
|
|
17
|
+
// the Save/Cancel pair, and a destructive tone for a danger zone.
|
|
10
18
|
let {
|
|
11
19
|
title,
|
|
12
20
|
subtitle,
|
|
21
|
+
description,
|
|
13
22
|
icon: Icon,
|
|
14
23
|
action,
|
|
24
|
+
footer,
|
|
25
|
+
tone = 'default',
|
|
15
26
|
children,
|
|
16
27
|
pad = true,
|
|
17
28
|
class: klass = '',
|
|
@@ -19,29 +30,94 @@
|
|
|
19
30
|
...restProps
|
|
20
31
|
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
21
32
|
title?: string;
|
|
33
|
+
/**
|
|
34
|
+
* A one-line qualifier beside the title. It TRUNCATES, and every existing
|
|
35
|
+
* call site is written against that: a panel header is one rhythm, and a
|
|
36
|
+
* subtitle that wrapped would change it everywhere at once.
|
|
37
|
+
*
|
|
38
|
+
* For the sentence or two that explains what the section is for, use
|
|
39
|
+
* `description`, which wraps. Carry one or the other, not both.
|
|
40
|
+
*/
|
|
22
41
|
subtitle?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The sentence or two under the title, wrapped, explaining what this
|
|
44
|
+
* section does or what changing it costs.
|
|
45
|
+
*
|
|
46
|
+
* PageHeader deliberately has no equivalent — a page's long explanation
|
|
47
|
+
* goes in its `info` tip so no route carries a standing explainer banner —
|
|
48
|
+
* and that position does not carry down to here. A settings section's
|
|
49
|
+
* description IS the setting's meaning ("who can approve a bill, and up to
|
|
50
|
+
* what value"), and hiding it behind an (i) on the one destination whose
|
|
51
|
+
* whole job is explaining consequential toggles is worse than the banner
|
|
52
|
+
* that rule was written against. GitHub, Linear, Vercel and Stripe all
|
|
53
|
+
* wrap a description under the section title.
|
|
54
|
+
*/
|
|
55
|
+
description?: string;
|
|
23
56
|
icon?: Component<{ class?: string }>;
|
|
24
57
|
action?: Snippet;
|
|
58
|
+
/**
|
|
59
|
+
* The section's actions — a Save/Cancel pair — on a top-bordered muted
|
|
60
|
+
* strip at the foot, right-aligned. Same treatment as CardFooter's, so a
|
|
61
|
+
* panel's foot and a card's foot are the same foot.
|
|
62
|
+
*/
|
|
63
|
+
footer?: Snippet;
|
|
64
|
+
/**
|
|
65
|
+
* `destructive` is the danger zone: a section whose controls delete or
|
|
66
|
+
* revoke something. It moves the rule and the title to the destructive
|
|
67
|
+
* ink — the treatment Alert and Button already use for it — and nothing
|
|
68
|
+
* else, so the section is marked without being shouted.
|
|
69
|
+
*/
|
|
70
|
+
tone?: 'default' | 'destructive';
|
|
25
71
|
children: Snippet;
|
|
26
72
|
pad?: boolean;
|
|
27
73
|
} = $props();
|
|
74
|
+
|
|
75
|
+
const destructive = $derived(tone === 'destructive');
|
|
28
76
|
</script>
|
|
29
77
|
|
|
30
78
|
<section
|
|
31
79
|
bind:this={ref}
|
|
32
|
-
|
|
80
|
+
data-tone={tone === 'default' ? undefined : tone}
|
|
81
|
+
class={cn(
|
|
82
|
+
'bg-card ds-edge flex flex-col overflow-hidden rounded-lg border',
|
|
83
|
+
destructive ? 'border-destructive/40' : 'border-border',
|
|
84
|
+
klass
|
|
85
|
+
)}
|
|
33
86
|
{...restProps}
|
|
34
87
|
>
|
|
35
88
|
{#if title}
|
|
36
|
-
|
|
89
|
+
<!--
|
|
90
|
+
`items-start` once a description is present: a wrapping description makes
|
|
91
|
+
the title block taller than the action row, and centring the actions
|
|
92
|
+
against three lines of prose floats them in the middle of the header.
|
|
93
|
+
-->
|
|
94
|
+
<header
|
|
95
|
+
class={cn(
|
|
96
|
+
'flex gap-2.5 border-b px-4 py-3',
|
|
97
|
+
description ? 'items-start' : 'items-center',
|
|
98
|
+
destructive ? 'border-destructive/25' : 'border-border'
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
37
101
|
{#if Icon}
|
|
38
|
-
<Icon
|
|
102
|
+
<Icon
|
|
103
|
+
class={cn('size-4', destructive ? 'text-destructive' : 'text-muted-foreground')}
|
|
104
|
+
/>
|
|
39
105
|
{/if}
|
|
40
106
|
<div class="min-w-0">
|
|
41
|
-
<h2
|
|
107
|
+
<h2
|
|
108
|
+
class={cn(
|
|
109
|
+
'text-body leading-tight font-semibold tracking-tight',
|
|
110
|
+
destructive && 'text-destructive'
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
{title}
|
|
114
|
+
</h2>
|
|
42
115
|
{#if subtitle}
|
|
43
116
|
<p class="text-muted-foreground truncate text-xs">{subtitle}</p>
|
|
44
117
|
{/if}
|
|
118
|
+
{#if description}
|
|
119
|
+
<p class="text-muted-foreground mt-1 text-sm">{description}</p>
|
|
120
|
+
{/if}
|
|
45
121
|
</div>
|
|
46
122
|
{#if action}
|
|
47
123
|
<div class="ml-auto flex items-center gap-1.5">{@render action()}</div>
|
|
@@ -51,4 +127,20 @@
|
|
|
51
127
|
<div class={pad ? 'p-4' : ''}>
|
|
52
128
|
{@render children()}
|
|
53
129
|
</div>
|
|
130
|
+
{#if footer}
|
|
131
|
+
<!--
|
|
132
|
+
`mt-auto` so a footer sits at the bottom of a panel that has been given a
|
|
133
|
+
height (a fixed-height column, a grid row) rather than floating under
|
|
134
|
+
short content.
|
|
135
|
+
-->
|
|
136
|
+
<div
|
|
137
|
+
class={cn(
|
|
138
|
+
'bg-muted/50 mt-auto flex items-center justify-end gap-2 border-t px-4 py-3',
|
|
139
|
+
destructive ? 'border-destructive/25' : 'border-border'
|
|
140
|
+
)}
|
|
141
|
+
data-slot="panel-footer"
|
|
142
|
+
>
|
|
143
|
+
{@render footer()}
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
54
146
|
</section>
|
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
import type { Snippet, Component } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
-
import type
|
|
3
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
4
4
|
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
5
5
|
title?: string;
|
|
6
|
+
/**
|
|
7
|
+
* A one-line qualifier beside the title. It TRUNCATES, and every existing
|
|
8
|
+
* call site is written against that: a panel header is one rhythm, and a
|
|
9
|
+
* subtitle that wrapped would change it everywhere at once.
|
|
10
|
+
*
|
|
11
|
+
* For the sentence or two that explains what the section is for, use
|
|
12
|
+
* `description`, which wraps. Carry one or the other, not both.
|
|
13
|
+
*/
|
|
6
14
|
subtitle?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The sentence or two under the title, wrapped, explaining what this
|
|
17
|
+
* section does or what changing it costs.
|
|
18
|
+
*
|
|
19
|
+
* PageHeader deliberately has no equivalent — a page's long explanation
|
|
20
|
+
* goes in its `info` tip so no route carries a standing explainer banner —
|
|
21
|
+
* and that position does not carry down to here. A settings section's
|
|
22
|
+
* description IS the setting's meaning ("who can approve a bill, and up to
|
|
23
|
+
* what value"), and hiding it behind an (i) on the one destination whose
|
|
24
|
+
* whole job is explaining consequential toggles is worse than the banner
|
|
25
|
+
* that rule was written against. GitHub, Linear, Vercel and Stripe all
|
|
26
|
+
* wrap a description under the section title.
|
|
27
|
+
*/
|
|
28
|
+
description?: string;
|
|
7
29
|
icon?: Component<{
|
|
8
30
|
class?: string;
|
|
9
31
|
}>;
|
|
10
32
|
action?: Snippet;
|
|
33
|
+
/**
|
|
34
|
+
* The section's actions — a Save/Cancel pair — on a top-bordered muted
|
|
35
|
+
* strip at the foot, right-aligned. Same treatment as CardFooter's, so a
|
|
36
|
+
* panel's foot and a card's foot are the same foot.
|
|
37
|
+
*/
|
|
38
|
+
footer?: Snippet;
|
|
39
|
+
/**
|
|
40
|
+
* `destructive` is the danger zone: a section whose controls delete or
|
|
41
|
+
* revoke something. It moves the rule and the title to the destructive
|
|
42
|
+
* ink — the treatment Alert and Button already use for it — and nothing
|
|
43
|
+
* else, so the section is marked without being shouted.
|
|
44
|
+
*/
|
|
45
|
+
tone?: 'default' | 'destructive';
|
|
11
46
|
children: Snippet;
|
|
12
47
|
pad?: boolean;
|
|
13
48
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The settings destination: a grouped section list beside the section you are
|
|
4
|
+
* reading, inside AppShell's content area.
|
|
5
|
+
*
|
|
6
|
+
* Two PANES, not a panel and not a drawer. The distinction is load-bearing:
|
|
7
|
+
* a panel overlays and closes, and this package carries a standing directive
|
|
8
|
+
* against side panels for work you are actually doing. These panes divide and
|
|
9
|
+
* persist, each scrolling on its own, so a long settings page never pushes the
|
|
10
|
+
* section list out of reach.
|
|
11
|
+
*
|
|
12
|
+
* The list renders through AppNav rather than a row renderer of its own, and
|
|
13
|
+
* that is the whole reason this component is small. AppNav already owns the
|
|
14
|
+
* group eyebrow, the active row's leading edge and tint and weight, the
|
|
15
|
+
* `aria-current="page"`, the segment-safe prefix match, truncation, and the
|
|
16
|
+
* density ramp — and it is exported in its own right precisely so a navigation
|
|
17
|
+
* list that belongs in a PAGE is built from the same affordance rather than
|
|
18
|
+
* hand-copied. The estate has already paid for the alternative once: one app's
|
|
19
|
+
* inner nav had reimplemented the active indicator independently of its own
|
|
20
|
+
* top bar. A second row renderer here would re-commit that drift one
|
|
21
|
+
* destination down, on the one surface every app is about to adopt at once.
|
|
22
|
+
*
|
|
23
|
+
* Grouping is by WHOSE setting it is — the person's, then the workspace's —
|
|
24
|
+
* because that is the only split a reader can predict: "yours" changes what
|
|
25
|
+
* you see, "this company" changes what everyone sees. A group whose `items`
|
|
26
|
+
* is empty renders nothing (`toGroups` drops it), so an app with no personal
|
|
27
|
+
* sections yet can leave the group in its list rather than stubbing a row that
|
|
28
|
+
* leads nowhere.
|
|
29
|
+
*
|
|
30
|
+
* One label per row, nothing beneath it. A description long enough to be worth
|
|
31
|
+
* reading does not fit a 240px column: every one of them truncated to an
|
|
32
|
+
* ellipsis and told the reader less than the label already did (measured
|
|
33
|
+
* 16/09/2026 in a consumer, removed there the same day). The page introduces
|
|
34
|
+
* its own sections — that is what Panel's `description` is for.
|
|
35
|
+
*
|
|
36
|
+
* No "Settings" heading. The shell's bar names the section it is on as of
|
|
37
|
+
* 2026.9.16, so a heading here is the same word twice, one above the other.
|
|
38
|
+
*
|
|
39
|
+
* BELOW md the list STACKS above the content and the page scrolls as one.
|
|
40
|
+
* A horizontally scrollable strip of the same rows was the obvious
|
|
41
|
+
* alternative and loses on the requirement it was meant to serve — every
|
|
42
|
+
* section reachable in one tap. A strip fits about two and a half rows at
|
|
43
|
+
* 375px, so the rest are behind a sideways swipe with no scrollbar to
|
|
44
|
+
* advertise them: a swipe THEN a tap, where a stack is a scroll the reader is
|
|
45
|
+
* already doing. It also has to drop the group labels for width, which throws
|
|
46
|
+
* away the one split the reader could predict, and it needs a second row
|
|
47
|
+
* renderer in a horizontal mode — the drift this component exists to avoid.
|
|
48
|
+
*
|
|
49
|
+
* USE IT WITH `padded={false}` on AppShell. The list's border and tint have to
|
|
50
|
+
* reach the content area's edges or the two panes read as a floating card, and
|
|
51
|
+
* this component pads its own content pane to the shell's own rhythm
|
|
52
|
+
* (`CONTENT_PADDING`) so the settings destination is not the one route whose
|
|
53
|
+
* text sits somewhere else.
|
|
54
|
+
*/
|
|
55
|
+
import type { Snippet } from 'svelte';
|
|
56
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
57
|
+
import AppNav from '../app-shell/app-nav.svelte';
|
|
58
|
+
import { CONTENT_PADDING } from '../app-shell/padding.js';
|
|
59
|
+
import type { NavSource } from '../app-shell/types.js';
|
|
60
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
61
|
+
|
|
62
|
+
let {
|
|
63
|
+
sections,
|
|
64
|
+
currentPath,
|
|
65
|
+
listLabel = 'Settings sections',
|
|
66
|
+
children,
|
|
67
|
+
ref = $bindable(null),
|
|
68
|
+
class: className,
|
|
69
|
+
...restProps
|
|
70
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
71
|
+
/**
|
|
72
|
+
* The sections, as the package's own navigation vocabulary: groups of
|
|
73
|
+
* `NavItem` (`{ heading, items }`), bare items, or a mix. The same type the
|
|
74
|
+
* rail takes, so a row is the same row everywhere in the household and an
|
|
75
|
+
* app types its settings list against something it already has.
|
|
76
|
+
*
|
|
77
|
+
* A row at the settings ROOT (`/settings`) needs `exact: true`, or its
|
|
78
|
+
* prefix match keeps it lit on every section beneath it alongside the
|
|
79
|
+
* section's own row.
|
|
80
|
+
*/
|
|
81
|
+
sections: NavSource;
|
|
82
|
+
/** The active path. Apps pass `page.url.pathname`, as they do to AppShell. */
|
|
83
|
+
currentPath?: string;
|
|
84
|
+
/** The list's accessible name. */
|
|
85
|
+
listLabel?: string;
|
|
86
|
+
children: Snippet;
|
|
87
|
+
} = $props();
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<div bind:this={ref} class={cn('ds-settings', className)} {...restProps}>
|
|
91
|
+
<!--
|
|
92
|
+
Width, tint and rule as utilities rather than in styles.css, because AppNav
|
|
93
|
+
declares `flex-1` on this same element: a base-layer `flex: none` loses to
|
|
94
|
+
a utility by layer, whatever order it is written in, while a utility passed
|
|
95
|
+
through `cn()` is resolved by tailwind-merge — `flex-none` and `flex-1` are
|
|
96
|
+
one group, so the last one wins and it is this one.
|
|
97
|
+
|
|
98
|
+
15rem (240px) is measured, not picked: it is the width at which a
|
|
99
|
+
two-word section label ("Overhead suppliers", "Delegation limits") sits on
|
|
100
|
+
one line beside its icon at comfortable density.
|
|
101
|
+
-->
|
|
102
|
+
<AppNav
|
|
103
|
+
nav={sections}
|
|
104
|
+
{currentPath}
|
|
105
|
+
label={listLabel}
|
|
106
|
+
class="border-border bg-muted/30 flex-none border-b md:w-60 md:border-b-0 md:border-e"
|
|
107
|
+
/>
|
|
108
|
+
<div class={cn('ds-settings-pane', CONTENT_PADDING)}>
|
|
109
|
+
{@render children()}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings destination: a grouped section list beside the section you are
|
|
3
|
+
* reading, inside AppShell's content area.
|
|
4
|
+
*
|
|
5
|
+
* Two PANES, not a panel and not a drawer. The distinction is load-bearing:
|
|
6
|
+
* a panel overlays and closes, and this package carries a standing directive
|
|
7
|
+
* against side panels for work you are actually doing. These panes divide and
|
|
8
|
+
* persist, each scrolling on its own, so a long settings page never pushes the
|
|
9
|
+
* section list out of reach.
|
|
10
|
+
*
|
|
11
|
+
* The list renders through AppNav rather than a row renderer of its own, and
|
|
12
|
+
* that is the whole reason this component is small. AppNav already owns the
|
|
13
|
+
* group eyebrow, the active row's leading edge and tint and weight, the
|
|
14
|
+
* `aria-current="page"`, the segment-safe prefix match, truncation, and the
|
|
15
|
+
* density ramp — and it is exported in its own right precisely so a navigation
|
|
16
|
+
* list that belongs in a PAGE is built from the same affordance rather than
|
|
17
|
+
* hand-copied. The estate has already paid for the alternative once: one app's
|
|
18
|
+
* inner nav had reimplemented the active indicator independently of its own
|
|
19
|
+
* top bar. A second row renderer here would re-commit that drift one
|
|
20
|
+
* destination down, on the one surface every app is about to adopt at once.
|
|
21
|
+
*
|
|
22
|
+
* Grouping is by WHOSE setting it is — the person's, then the workspace's —
|
|
23
|
+
* because that is the only split a reader can predict: "yours" changes what
|
|
24
|
+
* you see, "this company" changes what everyone sees. A group whose `items`
|
|
25
|
+
* is empty renders nothing (`toGroups` drops it), so an app with no personal
|
|
26
|
+
* sections yet can leave the group in its list rather than stubbing a row that
|
|
27
|
+
* leads nowhere.
|
|
28
|
+
*
|
|
29
|
+
* One label per row, nothing beneath it. A description long enough to be worth
|
|
30
|
+
* reading does not fit a 240px column: every one of them truncated to an
|
|
31
|
+
* ellipsis and told the reader less than the label already did (measured
|
|
32
|
+
* 16/09/2026 in a consumer, removed there the same day). The page introduces
|
|
33
|
+
* its own sections — that is what Panel's `description` is for.
|
|
34
|
+
*
|
|
35
|
+
* No "Settings" heading. The shell's bar names the section it is on as of
|
|
36
|
+
* 2026.9.16, so a heading here is the same word twice, one above the other.
|
|
37
|
+
*
|
|
38
|
+
* BELOW md the list STACKS above the content and the page scrolls as one.
|
|
39
|
+
* A horizontally scrollable strip of the same rows was the obvious
|
|
40
|
+
* alternative and loses on the requirement it was meant to serve — every
|
|
41
|
+
* section reachable in one tap. A strip fits about two and a half rows at
|
|
42
|
+
* 375px, so the rest are behind a sideways swipe with no scrollbar to
|
|
43
|
+
* advertise them: a swipe THEN a tap, where a stack is a scroll the reader is
|
|
44
|
+
* already doing. It also has to drop the group labels for width, which throws
|
|
45
|
+
* away the one split the reader could predict, and it needs a second row
|
|
46
|
+
* renderer in a horizontal mode — the drift this component exists to avoid.
|
|
47
|
+
*
|
|
48
|
+
* USE IT WITH `padded={false}` on AppShell. The list's border and tint have to
|
|
49
|
+
* reach the content area's edges or the two panes read as a floating card, and
|
|
50
|
+
* this component pads its own content pane to the shell's own rhythm
|
|
51
|
+
* (`CONTENT_PADDING`) so the settings destination is not the one route whose
|
|
52
|
+
* text sits somewhere else.
|
|
53
|
+
*/
|
|
54
|
+
import type { Snippet } from 'svelte';
|
|
55
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
56
|
+
import type { NavSource } from '../app-shell/types.js';
|
|
57
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
58
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
59
|
+
/**
|
|
60
|
+
* The sections, as the package's own navigation vocabulary: groups of
|
|
61
|
+
* `NavItem` (`{ heading, items }`), bare items, or a mix. The same type the
|
|
62
|
+
* rail takes, so a row is the same row everywhere in the household and an
|
|
63
|
+
* app types its settings list against something it already has.
|
|
64
|
+
*
|
|
65
|
+
* A row at the settings ROOT (`/settings`) needs `exact: true`, or its
|
|
66
|
+
* prefix match keeps it lit on every section beneath it alongside the
|
|
67
|
+
* section's own row.
|
|
68
|
+
*/
|
|
69
|
+
sections: NavSource;
|
|
70
|
+
/** The active path. Apps pass `page.url.pathname`, as they do to AppShell. */
|
|
71
|
+
currentPath?: string;
|
|
72
|
+
/** The list's accessible name. */
|
|
73
|
+
listLabel?: string;
|
|
74
|
+
children: Snippet;
|
|
75
|
+
};
|
|
76
|
+
declare const SettingsShell: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
77
|
+
type SettingsShell = ReturnType<typeof SettingsShell>;
|
|
78
|
+
export default SettingsShell;
|
package/dist/styles.css
CHANGED
|
@@ -770,27 +770,24 @@
|
|
|
770
770
|
color: var(--ds-nav-ink);
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
-
/* The disclosed rows. The indent is measured against the parent's own
|
|
774
|
-
geometry rather than picked: .ds-nav-item pads 0.625rem and the icon is
|
|
775
|
-
1.125rem, so the icon's centre line sits at 1.1875rem — where the guide
|
|
776
|
-
border goes — and 0.625rem of padding here plus the child's own 0.625rem
|
|
777
|
-
lands its label at 2.5rem, exactly under the parent's. The border is a real
|
|
778
|
-
affordance at fifteen rows, not decoration: it is what makes the run
|
|
779
|
-
scannable as one section. */
|
|
780
773
|
/* The disclosed rows carry no guide rule. Indentation and the children's own
|
|
781
774
|
icons already read as one group, and a line down the side is a second
|
|
782
|
-
signal for a grouping that was never ambiguous.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
775
|
+
signal for a grouping that was never ambiguous. 8f39b02 removed the rule
|
|
776
|
+
but not the 1px it was contributing to the indent (design-system#38): the
|
|
777
|
+
border sat between this margin and this padding, so the child label
|
|
778
|
+
landed 1px further right than the padding alone now gives it. Restored
|
|
779
|
+
here instead, so the geometry — not a rule nobody can see — still carries
|
|
780
|
+
it: `.ds-nav-item` pads 0.625rem and the icon is 1.125rem, so 1.1875rem
|
|
781
|
+
(margin) + 0.625rem plus the former border's 1px (this padding) +
|
|
782
|
+
0.625rem (the child's own padding) lands its label at 2.5rem, exactly
|
|
783
|
+
under the parent's. */
|
|
787
784
|
.ds-nav-children {
|
|
788
785
|
display: flex;
|
|
789
786
|
flex-direction: column;
|
|
790
787
|
gap: 0.125rem;
|
|
791
788
|
margin-top: 0.125rem;
|
|
792
789
|
margin-left: 1.1875rem;
|
|
793
|
-
padding-left: 0.625rem;
|
|
790
|
+
padding-left: calc(0.625rem + 1px);
|
|
794
791
|
}
|
|
795
792
|
|
|
796
793
|
/* The rail's foot: where you configure the app, and which build you are on.
|
|
@@ -881,6 +878,64 @@
|
|
|
881
878
|
padding-inline: 0;
|
|
882
879
|
}
|
|
883
880
|
|
|
881
|
+
/* The settings destination's two panes (SettingsShell).
|
|
882
|
+
|
|
883
|
+
Only the SHAPE is here; the list's width, tint and rule are utilities on the
|
|
884
|
+
AppNav element itself, because AppNav declares `flex-1` there and a
|
|
885
|
+
base-layer `flex: none` would lose to it by layer order rather than by
|
|
886
|
+
position — the cascade trap recorded against `.ds-nav`'s ink. What is left is
|
|
887
|
+
the axis swap and the scroll bounding, which nothing else declares.
|
|
888
|
+
|
|
889
|
+
48rem is Tailwind's own --breakpoint-md, written literally for the reason
|
|
890
|
+
`.ds-shell-rail` writes it literally: a media query cannot read a custom
|
|
891
|
+
property, so an app that retunes md retunes this with it. */
|
|
892
|
+
.ds-settings {
|
|
893
|
+
display: flex;
|
|
894
|
+
flex-direction: column;
|
|
895
|
+
flex: 1 1 auto;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/* `min-height: 0` is what buys the two independent scrollers, and it is
|
|
899
|
+
deliberately NOT set below md. A flex item is floored at its content height
|
|
900
|
+
unless it is allowed to shrink past it, so without this neither pane ever
|
|
901
|
+
scrolls and the whole content area does instead. Below md that floor is
|
|
902
|
+
exactly what is wanted: the list stacks above the content and the page
|
|
903
|
+
scrolls as one, which is the only sane arrangement on a phone — two nested
|
|
904
|
+
scrollers at 375px is the sideways-scroll hazard this package already carries
|
|
905
|
+
a gate for. */
|
|
906
|
+
@media (min-width: 48rem) {
|
|
907
|
+
.ds-settings {
|
|
908
|
+
flex-direction: row;
|
|
909
|
+
min-height: 0;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.ds-settings-pane {
|
|
914
|
+
flex: 1 1 auto;
|
|
915
|
+
min-width: 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* The pane stacks its sections itself, so a settings page writes a run of
|
|
919
|
+
Panels and no wrapper — the framing markup this surface exists to stop a
|
|
920
|
+
consumer writing. 1.25rem is the package's existing section rhythm, the same
|
|
921
|
+
gap `.ds-dialog-section` puts between adjacent sections.
|
|
922
|
+
|
|
923
|
+
A margin rule rather than `display: flex; gap`, because at md and up this box
|
|
924
|
+
is a height-bounded scroller: as a flex column its children carry
|
|
925
|
+
`flex-shrink: 1` and get squashed toward min-content before the scrollbar is
|
|
926
|
+
ever reached, so a tall section would compress instead of scrolling. Block
|
|
927
|
+
flow overflows, which is what a scroller wants. */
|
|
928
|
+
.ds-settings-pane > * + * {
|
|
929
|
+
margin-top: 1.25rem;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
@media (min-width: 48rem) {
|
|
933
|
+
.ds-settings-pane {
|
|
934
|
+
min-height: 0;
|
|
935
|
+
overflow-y: auto;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
884
939
|
/* A hard ceiling on the whole shell, in the one unit that names the actual
|
|
885
940
|
drawn surface rather than a percentage of an ancestor. `.ds-shell` is
|
|
886
941
|
already `flex` with `overflow-hidden` in the component markup, which
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.17",
|
|
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,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"package": "@poodle64/ui",
|
|
4
|
-
"version": "2026.9.
|
|
4
|
+
"version": "2026.9.17",
|
|
5
5
|
"generatedBy": "scripts/generate-registry.mjs",
|
|
6
6
|
"source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
|
|
7
|
-
"componentCount":
|
|
8
|
-
"situationCount":
|
|
7
|
+
"componentCount": 58,
|
|
8
|
+
"situationCount": 16
|
|
9
9
|
},
|
|
10
10
|
"situations": [
|
|
11
11
|
{
|
|
@@ -150,8 +150,8 @@
|
|
|
150
150
|
"name": "Panel",
|
|
151
151
|
"dir": "panel",
|
|
152
152
|
"import": "@poodle64/ui/panel",
|
|
153
|
-
"props": "children, title?, subtitle?, icon?, action?, pad?",
|
|
154
|
-
"insteadOf": "raw Card + CardHeader + CardTitle assembled by hand on every route"
|
|
153
|
+
"props": "children, title?, subtitle?, description?, icon?, action?, footer?, tone?, pad?",
|
|
154
|
+
"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"
|
|
155
155
|
}
|
|
156
156
|
]
|
|
157
157
|
},
|
|
@@ -323,6 +323,20 @@
|
|
|
323
323
|
}
|
|
324
324
|
]
|
|
325
325
|
},
|
|
326
|
+
{
|
|
327
|
+
"key": "settings-destination",
|
|
328
|
+
"title": "The settings destination",
|
|
329
|
+
"description": "The one settings surface: a grouped section list beside the section you are reading, inside AppShell’s content area (give AppShell padded={false}). Sections are grouped by whose setting it is — the person’s, then the workspace’s. The sections themselves are Panels.",
|
|
330
|
+
"components": [
|
|
331
|
+
{
|
|
332
|
+
"name": "SettingsShell",
|
|
333
|
+
"dir": "settings-shell",
|
|
334
|
+
"import": "@poodle64/ui/settings-shell",
|
|
335
|
+
"props": "sections, children, currentPath?, listLabel?",
|
|
336
|
+
"insteadOf": "a hand-built two-pane settings layout, or a card of tab links in a grid, per app"
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
},
|
|
326
340
|
{
|
|
327
341
|
"key": "dialog-and-overlay",
|
|
328
342
|
"title": "A dialog or overlay",
|
|
@@ -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.17`. 58 components, 16 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
|
|
|
@@ -81,7 +81,7 @@ _The generic titled card every route reaches for — an optional header with ico
|
|
|
81
81
|
|
|
82
82
|
| Component | Import | Key props | Reach for it instead of |
|
|
83
83
|
| --- | --- | --- | --- |
|
|
84
|
-
| `Panel` | `@poodle64/ui/panel` | `children, title?, subtitle?, icon?, action?, pad?` | raw Card + CardHeader + CardTitle assembled by hand on every route |
|
|
84
|
+
| `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 |
|
|
85
85
|
|
|
86
86
|
## A form the app itself describes
|
|
87
87
|
|
|
@@ -131,6 +131,14 @@ _The standing chrome — the app shell and its nav, the one page-title treatment
|
|
|
131
131
|
| `ContextColumn` | `@poodle64/ui/context-column` | `stats, statsTitle?, statsInfo?, detail?, ariaLabel?` | a hand-built right-hand <aside> with a stat card and a detail pane |
|
|
132
132
|
| `AppIdentity` | `@poodle64/ui/app-identity` | `user, workspace?, role?, entitlements?, onSignOut, onAccountSettings?, onSwitchTheme?` | a hand-rolled account/avatar dropdown wired to raw DropdownMenu.* + Avatar.* |
|
|
133
133
|
|
|
134
|
+
## The settings destination
|
|
135
|
+
|
|
136
|
+
_The one settings surface: a grouped section list beside the section you are reading, inside AppShell’s content area (give AppShell padded={false}). Sections are grouped by whose setting it is — the person’s, then the workspace’s. The sections themselves are Panels._
|
|
137
|
+
|
|
138
|
+
| Component | Import | Key props | Reach for it instead of |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| `SettingsShell` | `@poodle64/ui/settings-shell` | `sections, children, currentPath?, listLabel?` | a hand-built two-pane settings layout, or a card of tab links in a grid, per app |
|
|
141
|
+
|
|
134
142
|
## A dialog or overlay
|
|
135
143
|
|
|
136
144
|
_A modal task and the pieces around it — the dialog frame, a section within its body, a ⌘K palette, an inline (i) hint._
|