@poodle64/ui 2026.10.11 → 2026.10.13
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 +13 -0
- package/dist/components/ui/app-dialog/app-dialog.svelte +1 -1
- package/dist/components/ui/app-shell/app-nav.svelte +60 -23
- package/dist/components/ui/app-shell/app-nav.svelte.d.ts +9 -0
- package/dist/components/ui/app-shell/app-shell.svelte +1 -1
- package/dist/components/ui/list-shell/list-shell.svelte +1 -1
- package/dist/components/ui/page-header/page-header.svelte +1 -1
- package/dist/components/ui/settings-row/index.d.ts +2 -0
- package/dist/components/ui/settings-row/index.js +2 -0
- package/dist/components/ui/settings-row/settings-row.svelte +52 -0
- package/dist/components/ui/settings-row/settings-row.svelte.d.ts +15 -0
- package/dist/components/ui/settings-shell/index.d.ts +1 -0
- package/dist/components/ui/settings-shell/settings-shell.svelte +72 -19
- package/dist/components/ui/settings-shell/settings-shell.svelte.d.ts +42 -13
- package/dist/components/ui/stat-list/stat-list.svelte +1 -1
- package/dist/styles.css +16 -1
- package/package.json +1 -1
- package/registry/component-map.json +10 -3
- package/registry/component-map.md +3 -2
package/README.md
CHANGED
|
@@ -357,6 +357,19 @@ section you are reading, both panes scrolling independently from `md`.
|
|
|
357
357
|
| `sections` | Groups of `NavItem`, bare items, or a mix: the rail's own vocabulary. |
|
|
358
358
|
| `currentPath` | Which row is the page. Prefix-matched on the segment boundary, as the rail is. |
|
|
359
359
|
| `listLabel` | The list's accessible name. Defaults to `Settings sections`. |
|
|
360
|
+
| `selected`, `onSelect` | A single-route app (no `/settings/*` routes): instead of `currentPath`, pass sections as `{ id, label, icon? }`, the current `id` and a callback. Rows become buttons; the app owns the state. |
|
|
361
|
+
|
|
362
|
+
A setting inside a section is a `SettingsRow`: `label`, optional `description`, the control as children (a Switch, a Select, a Button), and `controlId` to tie the label to the control. Use it in `<Panel pad={false}>`; the row carries its own padding and the rule between rows.
|
|
363
|
+
|
|
364
|
+
```svelte
|
|
365
|
+
<SettingsShell sections={[{ items: [{ id: 'library', label: 'Library' }] }]} selected={active} onSelect={(id) => (active = id)}>
|
|
366
|
+
<Panel title="Library" pad={false}>
|
|
367
|
+
<SettingsRow label="Rescan on start" description="Look for new files at launch." controlId="rescan">
|
|
368
|
+
<Switch id="rescan" bind:checked={rescan} />
|
|
369
|
+
</SettingsRow>
|
|
370
|
+
</Panel>
|
|
371
|
+
</SettingsShell>
|
|
372
|
+
```
|
|
360
373
|
|
|
361
374
|
A row at the settings ROOT (`/settings`) needs `exact: true`, or its prefix match
|
|
362
375
|
keeps it lit on every section beneath it alongside the section's own row.
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
</span>
|
|
82
82
|
{/if}
|
|
83
83
|
<div class="min-w-0 flex-1">
|
|
84
|
-
<Dialog.Title class="font-display text-lg leading-tight font-semibold tracking-
|
|
84
|
+
<Dialog.Title class="font-display text-lg leading-tight font-semibold tracking-[var(--ds-font-display-tracking,-0.025em)]"
|
|
85
85
|
>{title}</Dialog.Title
|
|
86
86
|
>
|
|
87
87
|
{#if subtitle}
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
collapsed = false,
|
|
36
36
|
label = 'Primary',
|
|
37
37
|
onNavigate,
|
|
38
|
+
selected,
|
|
39
|
+
onSelect,
|
|
38
40
|
firstLink = $bindable(null),
|
|
39
41
|
class: className
|
|
40
42
|
}: {
|
|
@@ -47,6 +49,15 @@
|
|
|
47
49
|
label?: string;
|
|
48
50
|
/** Fired after any nav link activates — AppShell uses it to shut the drawer. */
|
|
49
51
|
onNavigate?: () => void;
|
|
52
|
+
/**
|
|
53
|
+
* State-driven mode, for a single-route app whose sections swap in place.
|
|
54
|
+
* With `onSelect` set each row is a BUTTON that calls it with the item's
|
|
55
|
+
* `href` (the key; it is never navigated to), and the row whose key equals
|
|
56
|
+
* `selected` is the current one. Children are not disclosed in this mode.
|
|
57
|
+
* SettingsShell is the caller; an app passes `id`s there, not hrefs.
|
|
58
|
+
*/
|
|
59
|
+
selected?: string;
|
|
60
|
+
onSelect?: (key: string) => void;
|
|
50
61
|
/**
|
|
51
62
|
* The first rendered link, bound out. AppShell moves focus here when this
|
|
52
63
|
* list is the content of an overlay, so a keyboard user lands inside the
|
|
@@ -167,28 +178,10 @@
|
|
|
167
178
|
still the active row. A childless item never reaches this at all —
|
|
168
179
|
`hasActiveNavChild` is false for it — so the flat case is untouched.
|
|
169
180
|
-->
|
|
170
|
-
{@const
|
|
171
|
-
{@const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
() => firstLink,
|
|
175
|
-
(node) => {
|
|
176
|
-
if (opts.first) firstLink = node;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
id={opts.labelId}
|
|
180
|
-
href={item.href}
|
|
181
|
-
class="ds-nav-item"
|
|
182
|
-
data-active={active ? 'true' : undefined}
|
|
183
|
-
data-within={within ? 'true' : undefined}
|
|
184
|
-
data-child={opts.child ? 'true' : undefined}
|
|
185
|
-
aria-current={active ? 'page' : undefined}
|
|
186
|
-
title={collapsed ? item.label : undefined}
|
|
187
|
-
target={item.external ? '_blank' : undefined}
|
|
188
|
-
rel={item.external ? 'noreferrer noopener' : undefined}
|
|
189
|
-
onclick={onNavigate}
|
|
190
|
-
onkeydown={onNavKeydown}
|
|
191
|
-
>
|
|
181
|
+
{@const stateful = onSelect !== undefined}
|
|
182
|
+
{@const within = !stateful && hasActiveNavChild(item, currentPath)}
|
|
183
|
+
{@const active = stateful ? item.href === selected : !within && isNavItemActive(item, currentPath)}
|
|
184
|
+
{#snippet body()}
|
|
192
185
|
{#if active}
|
|
193
186
|
<span class="ds-nav-indicator"></span>
|
|
194
187
|
{/if}
|
|
@@ -205,7 +198,51 @@
|
|
|
205
198
|
{#if item.badge !== undefined && !collapsed}
|
|
206
199
|
<span class="ds-nav-badge">{item.badge}</span>
|
|
207
200
|
{/if}
|
|
208
|
-
|
|
201
|
+
{/snippet}
|
|
202
|
+
{#if stateful}
|
|
203
|
+
<button
|
|
204
|
+
bind:this={
|
|
205
|
+
() => firstLink,
|
|
206
|
+
(node) => {
|
|
207
|
+
if (opts.first) firstLink = node;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
type="button"
|
|
211
|
+
class="ds-nav-item w-full text-start"
|
|
212
|
+
data-active={active ? 'true' : undefined}
|
|
213
|
+
aria-current={active ? 'true' : undefined}
|
|
214
|
+
title={collapsed ? item.label : undefined}
|
|
215
|
+
onclick={() => {
|
|
216
|
+
onSelect(item.href);
|
|
217
|
+
onNavigate?.();
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{@render body()}
|
|
221
|
+
</button>
|
|
222
|
+
{:else}
|
|
223
|
+
<a
|
|
224
|
+
bind:this={
|
|
225
|
+
() => firstLink,
|
|
226
|
+
(node) => {
|
|
227
|
+
if (opts.first) firstLink = node;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
id={opts.labelId}
|
|
231
|
+
href={item.href}
|
|
232
|
+
class="ds-nav-item"
|
|
233
|
+
data-active={active ? 'true' : undefined}
|
|
234
|
+
data-within={within ? 'true' : undefined}
|
|
235
|
+
data-child={opts.child ? 'true' : undefined}
|
|
236
|
+
aria-current={active ? 'page' : undefined}
|
|
237
|
+
title={collapsed ? item.label : undefined}
|
|
238
|
+
target={item.external ? '_blank' : undefined}
|
|
239
|
+
rel={item.external ? 'noreferrer noopener' : undefined}
|
|
240
|
+
onclick={onNavigate}
|
|
241
|
+
onkeydown={onNavKeydown}
|
|
242
|
+
>
|
|
243
|
+
{@render body()}
|
|
244
|
+
</a>
|
|
245
|
+
{/if}
|
|
209
246
|
{/snippet}
|
|
210
247
|
|
|
211
248
|
<nav
|
|
@@ -9,6 +9,15 @@ type $$ComponentProps = {
|
|
|
9
9
|
label?: string;
|
|
10
10
|
/** Fired after any nav link activates — AppShell uses it to shut the drawer. */
|
|
11
11
|
onNavigate?: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* State-driven mode, for a single-route app whose sections swap in place.
|
|
14
|
+
* With `onSelect` set each row is a BUTTON that calls it with the item's
|
|
15
|
+
* `href` (the key; it is never navigated to), and the row whose key equals
|
|
16
|
+
* `selected` is the current one. Children are not disclosed in this mode.
|
|
17
|
+
* SettingsShell is the caller; an app passes `id`s there, not hrefs.
|
|
18
|
+
*/
|
|
19
|
+
selected?: string;
|
|
20
|
+
onSelect?: (key: string) => void;
|
|
12
21
|
/**
|
|
13
22
|
* The first rendered link, bound out. AppShell moves focus here when this
|
|
14
23
|
* list is the content of an overlay, so a keyboard user lands inside the
|
|
@@ -457,7 +457,7 @@
|
|
|
457
457
|
slot to re-earn a behaviour the shell could give everyone is the shell
|
|
458
458
|
failing to ship something good.
|
|
459
459
|
-->
|
|
460
|
-
<span class="font-display text-body hidden font-semibold tracking-
|
|
460
|
+
<span class="font-display text-body hidden font-semibold tracking-[var(--ds-font-display-tracking,-0.025em)] sm:inline"
|
|
461
461
|
>{brandTitle}</span
|
|
462
462
|
>
|
|
463
463
|
{/if}
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
{...restProps}
|
|
63
63
|
>
|
|
64
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-
|
|
65
|
+
<h2 class="font-display text-display flex-1 truncate font-semibold tracking-[var(--ds-font-display-tracking,-0.025em)]">
|
|
66
66
|
{title}
|
|
67
67
|
</h2>
|
|
68
68
|
{#if actions}{@render actions()}{/if}
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
{/if}
|
|
96
96
|
{#if title}
|
|
97
97
|
<div class="flex items-center gap-2">
|
|
98
|
-
<h1 class="font-display text-display leading-tight font-semibold tracking-[-0.02em]">
|
|
98
|
+
<h1 class="font-display text-display leading-tight font-semibold tracking-[var(--ds-font-display-tracking,-0.02em)]">
|
|
99
99
|
{title}
|
|
100
100
|
</h1>
|
|
101
101
|
{#if info}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
5
|
+
|
|
6
|
+
// One setting: its name and what it does on the left, its control on the
|
|
7
|
+
// right (a Switch, a Select, a Button). Every settings page in the estate
|
|
8
|
+
// hand-built this, and each spaced it differently.
|
|
9
|
+
//
|
|
10
|
+
// Put rows in a Panel with `pad={false}`: the row carries its own padding so
|
|
11
|
+
// the rule between adjacent rows runs edge to edge, and the first row has no
|
|
12
|
+
// rule above it.
|
|
13
|
+
let {
|
|
14
|
+
label,
|
|
15
|
+
description,
|
|
16
|
+
controlId,
|
|
17
|
+
children,
|
|
18
|
+
ref = $bindable(null),
|
|
19
|
+
class: className,
|
|
20
|
+
...restProps
|
|
21
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
22
|
+
label: string;
|
|
23
|
+
/** What the setting does or costs; wraps beneath the label. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** The control's `id`: the label then names it (click focuses, screen reader announces). */
|
|
26
|
+
controlId?: string;
|
|
27
|
+
/** The control. */
|
|
28
|
+
children: Snippet;
|
|
29
|
+
} = $props();
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div
|
|
33
|
+
bind:this={ref}
|
|
34
|
+
data-slot="settings-row"
|
|
35
|
+
class={cn(
|
|
36
|
+
'border-border flex flex-wrap items-center justify-between gap-x-6 gap-y-2 border-t px-4 py-3 first:border-t-0',
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...restProps}
|
|
40
|
+
>
|
|
41
|
+
<div class="min-w-48 flex-1">
|
|
42
|
+
{#if controlId}
|
|
43
|
+
<label for={controlId} class="text-body block leading-tight font-medium">{label}</label>
|
|
44
|
+
{:else}
|
|
45
|
+
<div class="text-body leading-tight font-medium">{label}</div>
|
|
46
|
+
{/if}
|
|
47
|
+
{#if description}
|
|
48
|
+
<p class="text-muted-foreground mt-1 text-sm">{description}</p>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
<div class="flex flex-none items-center gap-2">{@render children()}</div>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
4
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
5
|
+
label: string;
|
|
6
|
+
/** What the setting does or costs; wraps beneath the label. */
|
|
7
|
+
description?: string;
|
|
8
|
+
/** The control's `id`: the label then names it (click focuses, screen reader announces). */
|
|
9
|
+
controlId?: string;
|
|
10
|
+
/** The control. */
|
|
11
|
+
children: Snippet;
|
|
12
|
+
};
|
|
13
|
+
declare const SettingsRow: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
14
|
+
type SettingsRow = ReturnType<typeof SettingsRow>;
|
|
15
|
+
export default SettingsRow;
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
3
|
+
|
|
4
|
+
/** A section of a single-route app: identified by `id`, switched by state. */
|
|
5
|
+
export interface SettingsSection {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: IconComponent;
|
|
9
|
+
badge?: string | number;
|
|
10
|
+
}
|
|
11
|
+
export interface SettingsSectionGroup {
|
|
12
|
+
heading?: string;
|
|
13
|
+
items: SettingsSection[];
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
1
17
|
<script lang="ts">
|
|
2
18
|
/**
|
|
3
19
|
* The settings destination: a grouped section list beside the section you are
|
|
@@ -59,32 +75,67 @@
|
|
|
59
75
|
import type { NavSource } from '../app-shell/types.js';
|
|
60
76
|
import { cn, type WithElementRef } from '../../../utils.js';
|
|
61
77
|
|
|
78
|
+
type Shared = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
79
|
+
/** The list's accessible name. */
|
|
80
|
+
listLabel?: string;
|
|
81
|
+
children: Snippet;
|
|
82
|
+
};
|
|
83
|
+
|
|
62
84
|
let {
|
|
63
85
|
sections,
|
|
64
86
|
currentPath,
|
|
87
|
+
selected,
|
|
88
|
+
onSelect,
|
|
65
89
|
listLabel = 'Settings sections',
|
|
66
90
|
children,
|
|
67
91
|
ref = $bindable(null),
|
|
68
92
|
class: className,
|
|
69
93
|
...restProps
|
|
70
|
-
}:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
}: Shared &
|
|
95
|
+
(
|
|
96
|
+
| {
|
|
97
|
+
/**
|
|
98
|
+
* ROUTED: the sections, as the package's own navigation vocabulary:
|
|
99
|
+
* groups of `NavItem` (`{ heading, items }`), bare items, or a mix.
|
|
100
|
+
* The same type the rail takes, so a row is the same row everywhere
|
|
101
|
+
* in the household.
|
|
102
|
+
*
|
|
103
|
+
* A row at the settings ROOT (`/settings`) needs `exact: true`, or
|
|
104
|
+
* its prefix match keeps it lit on every section beneath it.
|
|
105
|
+
*/
|
|
106
|
+
sections: NavSource;
|
|
107
|
+
/** The active path. Apps pass `page.url.pathname`, as to AppShell. */
|
|
108
|
+
currentPath?: string;
|
|
109
|
+
selected?: undefined;
|
|
110
|
+
onSelect?: undefined;
|
|
111
|
+
}
|
|
112
|
+
| {
|
|
113
|
+
/**
|
|
114
|
+
* STATE-DRIVEN, for a single-route app whose sections swap in place
|
|
115
|
+
* (canonical-app-shape §Applicability): groups of `SettingsSection`
|
|
116
|
+
* (`{ id, label, icon? }`) rather than `NavItem`, since nothing is
|
|
117
|
+
* navigated to. Rows are buttons; `selected` is the current `id` and
|
|
118
|
+
* `onSelect` fires with the `id` that was chosen. The app owns the
|
|
119
|
+
* state, so a deep link or a failure that opens a section just sets
|
|
120
|
+
* `selected`.
|
|
121
|
+
*/
|
|
122
|
+
sections: readonly (SettingsSection | SettingsSectionGroup)[];
|
|
123
|
+
selected: string;
|
|
124
|
+
onSelect: (id: string) => void;
|
|
125
|
+
currentPath?: undefined;
|
|
126
|
+
}
|
|
127
|
+
) = $props();
|
|
128
|
+
|
|
129
|
+
/** The state-driven sections as the rail's vocabulary, the `id` standing as the key. */
|
|
130
|
+
const nav = $derived<NavSource>(
|
|
131
|
+
onSelect
|
|
132
|
+
? (sections as readonly (SettingsSection | SettingsSectionGroup)[]).map((entry) =>
|
|
133
|
+
'items' in entry
|
|
134
|
+
? { heading: entry.heading, items: entry.items.map(({ id, ...rest }) => ({ ...rest, href: id })) }
|
|
135
|
+
: (({ id, ...rest }) => ({ ...rest, href: id }))(entry)
|
|
136
|
+
)
|
|
137
|
+
: (sections as NavSource)
|
|
138
|
+
);
|
|
88
139
|
</script>
|
|
89
140
|
|
|
90
141
|
<div bind:this={ref} class={cn('ds-settings', className)} {...restProps}>
|
|
@@ -100,8 +151,10 @@
|
|
|
100
151
|
one line beside its icon at comfortable density.
|
|
101
152
|
-->
|
|
102
153
|
<AppNav
|
|
103
|
-
nav
|
|
154
|
+
{nav}
|
|
104
155
|
{currentPath}
|
|
156
|
+
{selected}
|
|
157
|
+
{onSelect}
|
|
105
158
|
label={listLabel}
|
|
106
159
|
class="border-border bg-muted/30 flex-none border-b md:w-60 md:border-b-0 md:border-e"
|
|
107
160
|
/>
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import type { IconComponent } from '../app-shell/types.js';
|
|
2
|
+
/** A section of a single-route app: identified by `id`, switched by state. */
|
|
3
|
+
export interface SettingsSection {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: IconComponent;
|
|
7
|
+
badge?: string | number;
|
|
8
|
+
}
|
|
9
|
+
export interface SettingsSectionGroup {
|
|
10
|
+
heading?: string;
|
|
11
|
+
items: SettingsSection[];
|
|
12
|
+
}
|
|
1
13
|
/**
|
|
2
14
|
* The settings destination: a grouped section list beside the section you are
|
|
3
15
|
* reading, inside AppShell's content area.
|
|
@@ -55,24 +67,41 @@ import type { Snippet } from 'svelte';
|
|
|
55
67
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
56
68
|
import type { NavSource } from '../app-shell/types.js';
|
|
57
69
|
import { type WithElementRef } from '../../../utils.js';
|
|
58
|
-
type
|
|
70
|
+
type Shared = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
71
|
+
/** The list's accessible name. */
|
|
72
|
+
listLabel?: string;
|
|
73
|
+
children: Snippet;
|
|
74
|
+
};
|
|
75
|
+
type $$ComponentProps = Shared & ({
|
|
59
76
|
/**
|
|
60
|
-
*
|
|
61
|
-
* `NavItem` (`{ heading, items }`), bare items, or a mix.
|
|
62
|
-
* rail takes, so a row is the same row everywhere
|
|
63
|
-
*
|
|
77
|
+
* ROUTED: the sections, as the package's own navigation vocabulary:
|
|
78
|
+
* groups of `NavItem` (`{ heading, items }`), bare items, or a mix.
|
|
79
|
+
* The same type the rail takes, so a row is the same row everywhere
|
|
80
|
+
* in the household.
|
|
64
81
|
*
|
|
65
|
-
* A row at the settings ROOT (`/settings`) needs `exact: true`, or
|
|
66
|
-
* prefix match keeps it lit on every section beneath it
|
|
67
|
-
* section's own row.
|
|
82
|
+
* A row at the settings ROOT (`/settings`) needs `exact: true`, or
|
|
83
|
+
* its prefix match keeps it lit on every section beneath it.
|
|
68
84
|
*/
|
|
69
85
|
sections: NavSource;
|
|
70
|
-
/** The active path. Apps pass `page.url.pathname`, as
|
|
86
|
+
/** The active path. Apps pass `page.url.pathname`, as to AppShell. */
|
|
71
87
|
currentPath?: string;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
selected?: undefined;
|
|
89
|
+
onSelect?: undefined;
|
|
90
|
+
} | {
|
|
91
|
+
/**
|
|
92
|
+
* STATE-DRIVEN, for a single-route app whose sections swap in place
|
|
93
|
+
* (canonical-app-shape §Applicability): groups of `SettingsSection`
|
|
94
|
+
* (`{ id, label, icon? }`) rather than `NavItem`, since nothing is
|
|
95
|
+
* navigated to. Rows are buttons; `selected` is the current `id` and
|
|
96
|
+
* `onSelect` fires with the `id` that was chosen. The app owns the
|
|
97
|
+
* state, so a deep link or a failure that opens a section just sets
|
|
98
|
+
* `selected`.
|
|
99
|
+
*/
|
|
100
|
+
sections: readonly (SettingsSection | SettingsSectionGroup)[];
|
|
101
|
+
selected: string;
|
|
102
|
+
onSelect: (id: string) => void;
|
|
103
|
+
currentPath?: undefined;
|
|
104
|
+
});
|
|
76
105
|
declare const SettingsShell: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
77
106
|
type SettingsShell = ReturnType<typeof SettingsShell>;
|
|
78
107
|
export default SettingsShell;
|
package/dist/styles.css
CHANGED
|
@@ -446,6 +446,19 @@
|
|
|
446
446
|
var(--ds-shadow-sm);
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
/**
|
|
450
|
+
* How the display face is set. An app says it once in its palette
|
|
451
|
+
* (`--ds-font-display-transform: uppercase; --ds-font-display-tracking:
|
|
452
|
+
* 0.04em`) and every place the factory sets --ds-font-display follows: this
|
|
453
|
+
* rule for `.font-display`, the components' own tracking, .ds-prose and the
|
|
454
|
+
* librarian's markdown. Unset, each falls back to `inherit`, which is what the
|
|
455
|
+
* property did before, so an app that sets neither renders as it always did.
|
|
456
|
+
*/
|
|
457
|
+
.font-display {
|
|
458
|
+
text-transform: var(--ds-font-display-transform, inherit);
|
|
459
|
+
letter-spacing: var(--ds-font-display-tracking, inherit);
|
|
460
|
+
}
|
|
461
|
+
|
|
449
462
|
/** Tabular figures, so a column of numbers aligns. */
|
|
450
463
|
.ds-tabular {
|
|
451
464
|
font-variant-numeric: tabular-nums;
|
|
@@ -737,7 +750,7 @@
|
|
|
737
750
|
form. The literal keeps this rule correct on its own; the var keeps an
|
|
738
751
|
app's override flowing through. */
|
|
739
752
|
letter-spacing: var(--tracking-eyebrow, 0.06em);
|
|
740
|
-
color:
|
|
753
|
+
color: var(--ds-nav-ink-muted);
|
|
741
754
|
}
|
|
742
755
|
|
|
743
756
|
/* A count badge is small text on a tint, so it carries the 4.5:1 text floor
|
|
@@ -1144,6 +1157,8 @@
|
|
|
1144
1157
|
.ds-prose h3,
|
|
1145
1158
|
.ds-prose h4 {
|
|
1146
1159
|
font-family: var(--ds-font-display);
|
|
1160
|
+
text-transform: var(--ds-font-display-transform, inherit);
|
|
1161
|
+
letter-spacing: var(--ds-font-display-tracking, inherit);
|
|
1147
1162
|
font-weight: 600;
|
|
1148
1163
|
line-height: 1.25;
|
|
1149
1164
|
margin-block: var(--ds-spacing-lg) var(--ds-spacing-sm);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.13",
|
|
4
4
|
"description": "Household shared component layer: shadcn-svelte primitives (bits-ui) plus the composed page chrome every app builds its routes from, restyled by each app's @poodle64/design-tokens alias layer. One fix reaches every app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"package": "@poodle64/ui",
|
|
4
|
-
"version": "2026.10.
|
|
4
|
+
"version": "2026.10.13",
|
|
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": 69,
|
|
8
8
|
"situationCount": 19
|
|
9
9
|
},
|
|
10
10
|
"situations": [
|
|
@@ -416,9 +416,16 @@
|
|
|
416
416
|
"name": "SettingsShell",
|
|
417
417
|
"dir": "settings-shell",
|
|
418
418
|
"import": "@poodle64/ui/settings-shell",
|
|
419
|
-
"props": "sections, children, currentPath?, listLabel?",
|
|
419
|
+
"props": "sections, children, currentPath?, selected?, onSelect?, listLabel?",
|
|
420
420
|
"insteadOf": "a hand-built two-pane settings layout, or a card of tab links in a grid, per app"
|
|
421
421
|
},
|
|
422
|
+
{
|
|
423
|
+
"name": "SettingsRow",
|
|
424
|
+
"dir": "settings-row",
|
|
425
|
+
"import": "@poodle64/ui/settings-row",
|
|
426
|
+
"props": "label, description?, controlId?, children",
|
|
427
|
+
"insteadOf": "a hand-built label + description + control row inside a settings Panel (use Panel pad={false})"
|
|
428
|
+
},
|
|
422
429
|
{
|
|
423
430
|
"name": "About",
|
|
424
431
|
"dir": "about",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @poodle64/ui — situation → component map
|
|
2
2
|
|
|
3
3
|
<!-- GENERATED by scripts/generate-registry.mjs from scripts/situations.json + package source. DO NOT EDIT. -->
|
|
4
|
-
Generated from `@poodle64/ui@2026.10.
|
|
4
|
+
Generated from `@poodle64/ui@2026.10.13`. 69 components, 19 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
|
|
|
@@ -167,7 +167,8 @@ _The one settings surface: a grouped section list beside the section you are rea
|
|
|
167
167
|
|
|
168
168
|
| Component | Import | Key props | Reach for it instead of |
|
|
169
169
|
| --- | --- | --- | --- |
|
|
170
|
-
| `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 |
|
|
170
|
+
| `SettingsShell` | `@poodle64/ui/settings-shell` | `sections, children, currentPath?, selected?, onSelect?, listLabel?` | a hand-built two-pane settings layout, or a card of tab links in a grid, per app |
|
|
171
|
+
| `SettingsRow` | `@poodle64/ui/settings-row` | `label, description?, controlId?, children` | a hand-built label + description + control row inside a settings Panel (use Panel pad={false}) |
|
|
171
172
|
| `About` | `@poodle64/ui/about` | `name, version, description?, brand?, links?, diagnostics?, writeClipboard?` | a per-app About page or dialogue with duplicated version, links and diagnostic copying |
|
|
172
173
|
|
|
173
174
|
## A dialog or overlay
|