@poodle64/ui 2026.8.9 → 2026.8.11
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 +16 -10
- package/dist/components/ui/app-shell/app-nav.svelte +12 -12
- package/dist/components/ui/app-shell/app-shell.svelte +14 -12
- package/dist/components/ui/app-shell/app-shell.svelte.d.ts +14 -6
- package/dist/components/ui/app-shell/types.d.ts +20 -3
- package/dist/components/ui/app-shell/types.js +29 -5
- package/dist/components/ui/page-header/page-header.svelte +10 -1
- package/package.json +1 -1
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,6 @@ That is the whole minimum. Everything below is optional.
|
|
|
147
147
|
| `identity` | The signed-in surface. Rendered once, at the end of the top bar. |
|
|
148
148
|
| `context`, `actions` | Leading and trailing top-bar slots: a store/tenant switcher, app-level action buttons. |
|
|
149
149
|
| `banner` | Full-width region under the bar: reconnect notices, trial warnings. |
|
|
150
|
-
| `sidebar` | A secondary, route-scoped column between the nav and the page body. |
|
|
151
150
|
| `onSearch`, `searchLabel`, `searchShortcut` | Provide `onSearch` to render the search affordance at all. |
|
|
152
151
|
| `themeToggle`, `onToggleTheme` | Defaults to `mode-watcher`. Set `themeToggle={false}` when the app puts theming inside its own user menu. |
|
|
153
152
|
| `measure` | How wide the page body may get, from a named scale. Defaults to `full` (no cap). |
|
|
@@ -176,10 +175,15 @@ it did before the field existed.
|
|
|
176
175
|
|
|
177
176
|
It exists because the alternative was two left-hand columns: an app whose
|
|
178
177
|
sections have inner navigation had nowhere to put it in the rail, so it put
|
|
179
|
-
modules in `nav` and the current section's pages in `sidebar
|
|
180
|
-
(modules along the top bar, rail for the current module) was rejected on
|
|
181
|
-
it leaves two navigation surfaces that both need collapsing and both want
|
|
182
|
-
same hamburger. One nested tree collapses to one drawer.
|
|
178
|
+
modules in `nav` and the current section's pages in a `sidebar` slot. The other
|
|
179
|
+
way out (modules along the top bar, rail for the current module) was rejected on
|
|
180
|
+
mobile: it leaves two navigation surfaces that both need collapsing and both want
|
|
181
|
+
the same hamburger. One nested tree collapses to one drawer.
|
|
182
|
+
|
|
183
|
+
Since **2026.8.11 it is the only way in**: the `sidebar` slot is gone. What it
|
|
184
|
+
produced was the shell shape differing per app, and in one app per MODULE — two
|
|
185
|
+
of its sections rendered their pages beside the rail and the rest rendered them
|
|
186
|
+
inside it. Operator ruling, 21/08/2026: no app supports an additional sidebar.
|
|
183
187
|
|
|
184
188
|
The behaviour, and why:
|
|
185
189
|
|
|
@@ -213,9 +217,11 @@ deep, so a section whose own navigation is itself grouped under sub-headings, or
|
|
|
213
217
|
whose rows disclose a third level, does not lift into the rail whole. The rail is
|
|
214
218
|
not the place to fix that. At 15.5rem a third level leaves roughly 128px for the
|
|
215
219
|
label, which is about fifteen characters. A section that deep keeps its deepest
|
|
216
|
-
level on its own page, where there is width for it
|
|
217
|
-
|
|
218
|
-
|
|
220
|
+
level on its own page, where there is width for it. A column that is not
|
|
221
|
+
`NavItem`-shaped at all — a document tree, a table of contents, a filter panel —
|
|
222
|
+
belongs in the page too, as a sibling of the article it serves, and not in the
|
|
223
|
+
shell: it is part of that page's own reading surface, it wants that page's
|
|
224
|
+
breakpoints, and no other route should be paying rail width for it.
|
|
219
225
|
|
|
220
226
|
`NavItem` / `NavGroup` are exported so an app types its own config against them.
|
|
221
227
|
They carry **no notion of who may see an item**: two surveyed apps gate
|
|
@@ -260,8 +266,8 @@ palette:
|
|
|
260
266
|
}
|
|
261
267
|
```
|
|
262
268
|
|
|
263
|
-
`AppNav` used outside the chrome (
|
|
264
|
-
page's ink, so inverting the rail does not drag
|
|
269
|
+
`AppNav` used outside the chrome (a navigation list inside a page) keeps the
|
|
270
|
+
page's ink, so inverting the rail does not drag it along.
|
|
265
271
|
|
|
266
272
|
### The content measure
|
|
267
273
|
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
* The vertical navigation list.
|
|
4
4
|
*
|
|
5
5
|
* Rendered by AppShell in the rail/drawer (one element, two states — see
|
|
6
|
-
* app-shell.svelte) and exported in its own right
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the collapse behaviour independently of its own top bar.
|
|
6
|
+
* app-shell.svelte), and exported in its own right for a navigation list that
|
|
7
|
+
* belongs in a PAGE — so it is built from the same affordance rather than
|
|
8
|
+
* hand-copied. That was the concrete shape of the drift in the estate: one
|
|
9
|
+
* app's inner nav had reimplemented the active indicator, the group
|
|
10
|
+
* separators and the collapse behaviour independently of its own top bar.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
* is
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* and
|
|
18
|
-
*
|
|
12
|
+
* It is no longer how an app gets a second navigation COLUMN, because there
|
|
13
|
+
* is no longer such a thing: AppShell's `sidebar` slot was removed in
|
|
14
|
+
* 2026.8.11 (operator ruling, 21/08/2026 — no app supports an additional
|
|
15
|
+
* sidebar). An item carrying `children` discloses them in place, beneath
|
|
16
|
+
* itself, and that is where a section's own pages go at every width. See
|
|
17
|
+
* `types.ts` on `children` for why the nested rail won over
|
|
18
|
+
* modules-on-the-top-bar, and why the depth cap is one.
|
|
19
19
|
*/
|
|
20
20
|
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
21
21
|
import {
|
|
@@ -56,10 +56,20 @@
|
|
|
56
56
|
* remembered. `collapsible` is on for the same reason.
|
|
57
57
|
*
|
|
58
58
|
* Everything else the surveyed apps differed on turned out to be a slot, not
|
|
59
|
-
* a variant: the brand, the identity surface, a context switcher, a banner
|
|
60
|
-
*
|
|
61
|
-
* no app
|
|
62
|
-
*
|
|
59
|
+
* a variant: the brand, the identity surface, a context switcher, a banner.
|
|
60
|
+
* Those are snippets, so this package imports no app store, no app route and
|
|
61
|
+
* no app brand — the coupling that made the best shell in the estate
|
|
62
|
+
* unliftable in the first place.
|
|
63
|
+
*
|
|
64
|
+
* There is NO second navigation column, and there is no slot to make one.
|
|
65
|
+
* `sidebar` was such a slot until 2026.8.11, and what it produced was two
|
|
66
|
+
* apps rendering a module's own pages beside the rail while the other seven
|
|
67
|
+
* rendered them inside it — the shell shape differing per app, and in one app
|
|
68
|
+
* per MODULE, which is the thing this component exists to stop. Operator
|
|
69
|
+
* ruling, 21/08/2026, on seeing one app's two modules disagree: no app
|
|
70
|
+
* supports an additional sidebar; a section's own pages roll out beneath it
|
|
71
|
+
* in the rail. That is `NavItem.children` (see types.ts), which the rail
|
|
72
|
+
* already discloses at every width and folds into the one drawer on a phone.
|
|
63
73
|
*
|
|
64
74
|
* The minimum useful call is two props:
|
|
65
75
|
*
|
|
@@ -96,7 +106,6 @@
|
|
|
96
106
|
context,
|
|
97
107
|
actions,
|
|
98
108
|
identity,
|
|
99
|
-
sidebar,
|
|
100
109
|
onSearch,
|
|
101
110
|
searchLabel = 'Search…',
|
|
102
111
|
searchShortcut = '⌘K',
|
|
@@ -154,8 +163,6 @@
|
|
|
154
163
|
actions?: Snippet;
|
|
155
164
|
/** The signed-in user surface. Rendered once, at the end of the top bar. */
|
|
156
165
|
identity?: Snippet;
|
|
157
|
-
/** A secondary, route-scoped column between the nav and the page body. */
|
|
158
|
-
sidebar?: Snippet;
|
|
159
166
|
/** Provide to render the search affordance. Usually opens a CommandPalette. */
|
|
160
167
|
onSearch?: () => void;
|
|
161
168
|
searchLabel?: string;
|
|
@@ -590,11 +597,6 @@
|
|
|
590
597
|
{#if banner}{@render banner()}{/if}
|
|
591
598
|
|
|
592
599
|
<div class="flex min-h-0 min-w-0 flex-1">
|
|
593
|
-
{#if sidebar}
|
|
594
|
-
<div class="border-border hidden min-h-0 flex-none border-r md:flex">
|
|
595
|
-
{@render sidebar()}
|
|
596
|
-
</div>
|
|
597
|
-
{/if}
|
|
598
600
|
<!--
|
|
599
601
|
The scrolling content region, and the element a consumer's
|
|
600
602
|
"no horizontal overflow" test must measure.
|
|
@@ -55,10 +55,20 @@
|
|
|
55
55
|
* remembered. `collapsible` is on for the same reason.
|
|
56
56
|
*
|
|
57
57
|
* Everything else the surveyed apps differed on turned out to be a slot, not
|
|
58
|
-
* a variant: the brand, the identity surface, a context switcher, a banner
|
|
59
|
-
*
|
|
60
|
-
* no app
|
|
61
|
-
*
|
|
58
|
+
* a variant: the brand, the identity surface, a context switcher, a banner.
|
|
59
|
+
* Those are snippets, so this package imports no app store, no app route and
|
|
60
|
+
* no app brand — the coupling that made the best shell in the estate
|
|
61
|
+
* unliftable in the first place.
|
|
62
|
+
*
|
|
63
|
+
* There is NO second navigation column, and there is no slot to make one.
|
|
64
|
+
* `sidebar` was such a slot until 2026.8.11, and what it produced was two
|
|
65
|
+
* apps rendering a module's own pages beside the rail while the other seven
|
|
66
|
+
* rendered them inside it — the shell shape differing per app, and in one app
|
|
67
|
+
* per MODULE, which is the thing this component exists to stop. Operator
|
|
68
|
+
* ruling, 21/08/2026, on seeing one app's two modules disagree: no app
|
|
69
|
+
* supports an additional sidebar; a section's own pages roll out beneath it
|
|
70
|
+
* in the rail. That is `NavItem.children` (see types.ts), which the rail
|
|
71
|
+
* already discloses at every width and folds into the one drawer on a phone.
|
|
62
72
|
*
|
|
63
73
|
* The minimum useful call is two props:
|
|
64
74
|
*
|
|
@@ -116,8 +126,6 @@ type $$ComponentProps = {
|
|
|
116
126
|
actions?: Snippet;
|
|
117
127
|
/** The signed-in user surface. Rendered once, at the end of the top bar. */
|
|
118
128
|
identity?: Snippet;
|
|
119
|
-
/** A secondary, route-scoped column between the nav and the page body. */
|
|
120
|
-
sidebar?: Snippet;
|
|
121
129
|
/** Provide to render the search affordance. Usually opens a CommandPalette. */
|
|
122
130
|
onSearch?: () => void;
|
|
123
131
|
searchLabel?: string;
|
|
@@ -57,10 +57,12 @@ export interface NavItem {
|
|
|
57
57
|
*
|
|
58
58
|
* Without this, an app whose sections have their own inner navigation has
|
|
59
59
|
* nowhere to put it inside the rail. The observed workaround was modules in
|
|
60
|
-
* `nav` and the current module's pages in
|
|
60
|
+
* `nav` and the current module's pages in AppShell's `sidebar` snippet — two
|
|
61
61
|
* left-hand columns on a desktop, and, the reason this shape won over moving
|
|
62
62
|
* modules to a top bar, two surfaces on a phone that both want the same
|
|
63
|
-
* hamburger. One nested tree collapses to one drawer.
|
|
63
|
+
* hamburger. One nested tree collapses to one drawer. That slot is gone as
|
|
64
|
+
* of 2026.8.11 and this is the only way in, so a section's navigation is in
|
|
65
|
+
* the same place in every app and on every route.
|
|
64
66
|
*
|
|
65
67
|
* Named `children`, not `items`, for a mechanical reason as well as a
|
|
66
68
|
* readable one: `isNavGroup` narrows on `'items' in entry`, so an item
|
|
@@ -105,11 +107,26 @@ export declare function toGroups(nav: NavSource | undefined): NavGroup[];
|
|
|
105
107
|
export declare function navChildren(item: NavItem): readonly NavChildItem[];
|
|
106
108
|
/**
|
|
107
109
|
* Every item in source order, ignoring grouping — a parent immediately followed
|
|
108
|
-
* by its own children. Used by the command
|
|
110
|
+
* by its own children, each destination appearing ONCE. Used by the command
|
|
111
|
+
* palette.
|
|
109
112
|
*
|
|
110
113
|
* Children are included because they are destinations like any other, and the
|
|
111
114
|
* palette is the fastest route to a page three levels into a section. An item
|
|
112
115
|
* with no children flattens exactly as it always did.
|
|
116
|
+
*
|
|
117
|
+
* The de-duplication is not tidiness. A section that discloses its own pages
|
|
118
|
+
* names its landing page twice by nature — the parent row goes there, and the
|
|
119
|
+
* section's first child row is that same page under its own name ("Property",
|
|
120
|
+
* then "Dashboard"). Both rows are wanted in the rail, where they render in
|
|
121
|
+
* different `{#each}` blocks; flattened they collide, and every consumer keys
|
|
122
|
+
* this list by `href`, so Svelte throws `each_key_duplicate` and the throw
|
|
123
|
+
* takes the palette's whole content with it. Measured in a consumer: ⌘K opened
|
|
124
|
+
* an empty sheet on every module route until the app hand-filtered its own nav
|
|
125
|
+
* before passing it. An app should not have to know that.
|
|
126
|
+
*
|
|
127
|
+
* First occurrence wins, so the surviving entry is the parent's — the row
|
|
128
|
+
* carrying the section's own name, which is what someone typing into a palette
|
|
129
|
+
* is looking for.
|
|
113
130
|
*/
|
|
114
131
|
export declare function toItems(nav: NavSource | undefined): NavItem[];
|
|
115
132
|
/**
|
|
@@ -35,19 +35,43 @@ export function navChildren(item) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Every item in source order, ignoring grouping — a parent immediately followed
|
|
38
|
-
* by its own children. Used by the command
|
|
38
|
+
* by its own children, each destination appearing ONCE. Used by the command
|
|
39
|
+
* palette.
|
|
39
40
|
*
|
|
40
41
|
* Children are included because they are destinations like any other, and the
|
|
41
42
|
* palette is the fastest route to a page three levels into a section. An item
|
|
42
43
|
* with no children flattens exactly as it always did.
|
|
44
|
+
*
|
|
45
|
+
* The de-duplication is not tidiness. A section that discloses its own pages
|
|
46
|
+
* names its landing page twice by nature — the parent row goes there, and the
|
|
47
|
+
* section's first child row is that same page under its own name ("Property",
|
|
48
|
+
* then "Dashboard"). Both rows are wanted in the rail, where they render in
|
|
49
|
+
* different `{#each}` blocks; flattened they collide, and every consumer keys
|
|
50
|
+
* this list by `href`, so Svelte throws `each_key_duplicate` and the throw
|
|
51
|
+
* takes the palette's whole content with it. Measured in a consumer: ⌘K opened
|
|
52
|
+
* an empty sheet on every module route until the app hand-filtered its own nav
|
|
53
|
+
* before passing it. An app should not have to know that.
|
|
54
|
+
*
|
|
55
|
+
* First occurrence wins, so the surviving entry is the parent's — the row
|
|
56
|
+
* carrying the section's own name, which is what someone typing into a palette
|
|
57
|
+
* is looking for.
|
|
43
58
|
*/
|
|
44
59
|
export function toItems(nav) {
|
|
45
60
|
if (!nav)
|
|
46
61
|
return [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const items = [];
|
|
64
|
+
for (const entry of nav) {
|
|
65
|
+
for (const item of isNavGroup(entry) ? entry.items : [entry]) {
|
|
66
|
+
for (const candidate of [item, ...navChildren(item)]) {
|
|
67
|
+
if (seen.has(candidate.href))
|
|
68
|
+
continue;
|
|
69
|
+
seen.add(candidate.href);
|
|
70
|
+
items.push(candidate);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return items;
|
|
51
75
|
}
|
|
52
76
|
/** Prefix-match `path` against `prefix`, without running past a path segment. */
|
|
53
77
|
function matchesPrefix(path, prefix) {
|
|
@@ -95,6 +95,15 @@
|
|
|
95
95
|
{/if}
|
|
96
96
|
</div>
|
|
97
97
|
{#if actions}
|
|
98
|
-
|
|
98
|
+
<!--
|
|
99
|
+
`min-w-0` is load-bearing, not tidying. This row is a flex ITEM of the
|
|
100
|
+
header, so it inherits `min-width: auto` — a floor at its own min-content
|
|
101
|
+
width, which its `flex-wrap` cannot get below because wrapping shrinks a
|
|
102
|
+
flex CONTAINER, never its item floor. Measured 2026-08-20 at 360px: an
|
|
103
|
+
actions row rendered 408px inside a 328px parent and pushed 64px of
|
|
104
|
+
sideways scroll into the shell content region, on every consumer using
|
|
105
|
+
PageHeader with actions.
|
|
106
|
+
-->
|
|
107
|
+
<div class="flex min-w-0 flex-wrap items-center justify-end gap-2">{@render actions()}</div>
|
|
99
108
|
{/if}
|
|
100
109
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.11",
|
|
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,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.8.
|
|
4
|
+
Generated from `@poodle64/ui@2026.8.11`. 49 components, 12 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
|
|