@poodle64/ui 2026.9.4 → 2026.9.6
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 +15 -1
- package/dist/components/ui/app-identity/app-identity.svelte +50 -1
- package/dist/components/ui/app-identity/app-identity.svelte.d.ts +62 -1
- package/dist/components/ui/app-shell/app-shell.svelte +13 -1
- package/dist/styles.css +47 -3
- package/package.json +1 -1
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -1164,4 +1164,18 @@ non-zero and naming the field on any difference.
|
|
|
1164
1164
|
|
|
1165
1165
|
1. Change a component; bump `version` in `package.json` (CalVer).
|
|
1166
1166
|
2. `pnpm build`, which runs `svelte-package` then `publint` (package.json/exports sanity).
|
|
1167
|
-
3. Commit, tag `ui-v<version>`, push the tag
|
|
1167
|
+
3. Commit, tag `ui-v<version>`, push the tag.
|
|
1168
|
+
4. `.github/workflows/publish.yaml` runs on that push and should publish via
|
|
1169
|
+
npm OIDC trusted publishing: check it with
|
|
1170
|
+
`gh run list --workflow=publish.yaml`. As of 2026.9.4 every run since
|
|
1171
|
+
`ui-v2026.9.2` fails at the `npm publish` step with
|
|
1172
|
+
`E404 Not Found - PUT .../@poodle64%2fui` (an OIDC/trusted-publisher
|
|
1173
|
+
binding issue, since `Build and test package` passes first). Until that
|
|
1174
|
+
is diagnosed and fixed, publish manually from `packages/ui/`:
|
|
1175
|
+
```bash
|
|
1176
|
+
printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > .npmrc
|
|
1177
|
+
signet exec --identity huginn-claude --broker https://portcullis.godswood.au \
|
|
1178
|
+
--credential npm-publish-token --env-var NODE_AUTH_TOKEN -- npm publish
|
|
1179
|
+
rm -f .npmrc
|
|
1180
|
+
```
|
|
1181
|
+
Confirm with `npm view @poodle64/ui version`.
|
|
@@ -30,7 +30,14 @@
|
|
|
30
30
|
* instead (operator ruling, 07/09/2026, master-project#291's org-of-one
|
|
31
31
|
* case). Wire it whenever the app has a members route, whether or not it
|
|
32
32
|
* also renders a switcher.
|
|
33
|
+
*
|
|
34
|
+
* `links` is the escape valve for everything the fixed entries have no room
|
|
35
|
+
* for — an Admin route, a support link — as a LIST, so every app's extra
|
|
36
|
+
* destinations render as the same row. Without it an app puts its own
|
|
37
|
+
* destination in AppShell's `actions` slot as an icon button (cadmus did,
|
|
38
|
+
* twice), and two apps do it two ways in the same bar.
|
|
33
39
|
*/
|
|
40
|
+
import type { Component } from 'svelte';
|
|
34
41
|
import { toggleMode } from 'mode-watcher';
|
|
35
42
|
import * as DropdownMenu from '../dropdown-menu/index.js';
|
|
36
43
|
import * as Avatar from '../avatar/index.js';
|
|
@@ -43,7 +50,8 @@
|
|
|
43
50
|
onSignOut,
|
|
44
51
|
onAccountSettings,
|
|
45
52
|
onManageMembers,
|
|
46
|
-
onSwitchTheme
|
|
53
|
+
onSwitchTheme,
|
|
54
|
+
links = []
|
|
47
55
|
}: {
|
|
48
56
|
user: { username: string; display_name: string | null; email: string | null };
|
|
49
57
|
/** The active workspace's name. Omit while none is chosen yet — the block hides rather than rendering empty. */
|
|
@@ -70,6 +78,22 @@
|
|
|
70
78
|
onManageMembers?: () => void;
|
|
71
79
|
/** Override the theme action. Defaults to mode-watcher's toggleMode, matching AppShell. */
|
|
72
80
|
onSwitchTheme?: () => void;
|
|
81
|
+
/**
|
|
82
|
+
* App destinations the fixed entries have no room for, rendered in their
|
|
83
|
+
* own group.
|
|
84
|
+
*
|
|
85
|
+
* A list of items, never a snippet: a snippet lets each app invent its own
|
|
86
|
+
* row shape, and two apps inventing two shapes in the same top bar is the
|
|
87
|
+
* drift this package exists to end. Cadmus is the measured case — its
|
|
88
|
+
* Admin entry had nowhere to go and became an icon button in AppShell's
|
|
89
|
+
* `actions` slot, and a second one (a quiet support link) was about to
|
|
90
|
+
* follow it there.
|
|
91
|
+
*
|
|
92
|
+
* No `href`: the callback shape matches every other action on this
|
|
93
|
+
* component, and a consumer routes with its own `goto`. The icon is
|
|
94
|
+
* optional — a label-only row stays aligned with any iconed row beside it.
|
|
95
|
+
*/
|
|
96
|
+
links?: { label: string; icon?: Component<{ class?: string }>; onSelect: () => void }[];
|
|
73
97
|
} = $props();
|
|
74
98
|
|
|
75
99
|
const shown = $derived(user.display_name ?? user.username);
|
|
@@ -110,6 +134,11 @@
|
|
|
110
134
|
user.display_name ? initialsOf(user.display_name) : handleInitials(user.username)
|
|
111
135
|
);
|
|
112
136
|
|
|
137
|
+
// Spacers only where the group actually has an icon to align to: a group of
|
|
138
|
+
// label-only rows sits flush with the fixed entries below it, exactly as it
|
|
139
|
+
// would if this prop had never existed.
|
|
140
|
+
const linksHaveIcons = $derived(links.some((l) => l.icon));
|
|
141
|
+
|
|
113
142
|
function switchTheme() {
|
|
114
143
|
if (onSwitchTheme) onSwitchTheme();
|
|
115
144
|
else toggleMode();
|
|
@@ -177,6 +206,26 @@
|
|
|
177
206
|
<DropdownMenu.Item onSelect={onManageMembers}>Members</DropdownMenu.Item>
|
|
178
207
|
{/if}
|
|
179
208
|
{/if}
|
|
209
|
+
{#if links.length}
|
|
210
|
+
<DropdownMenu.Separator />
|
|
211
|
+
<DropdownMenu.Group>
|
|
212
|
+
<!-- Keyed by position, not by label: nothing in the type makes a label
|
|
213
|
+
unique, and two apps' configs colliding on one would be a
|
|
214
|
+
duplicate-key crash rather than a duplicated row. This list is
|
|
215
|
+
app configuration, not data that reorders. -->
|
|
216
|
+
{#each links as link, i (i)}
|
|
217
|
+
{@const Icon = link.icon}
|
|
218
|
+
<DropdownMenu.Item onSelect={link.onSelect}>
|
|
219
|
+
{#if Icon}
|
|
220
|
+
<Icon class="size-4" />
|
|
221
|
+
{:else if linksHaveIcons}
|
|
222
|
+
<span class="size-4 flex-none" aria-hidden="true"></span>
|
|
223
|
+
{/if}
|
|
224
|
+
{link.label}
|
|
225
|
+
</DropdownMenu.Item>
|
|
226
|
+
{/each}
|
|
227
|
+
</DropdownMenu.Group>
|
|
228
|
+
{/if}
|
|
180
229
|
<DropdownMenu.Separator />
|
|
181
230
|
<DropdownMenu.Item onSelect={switchTheme}>Switch theme</DropdownMenu.Item>
|
|
182
231
|
<DropdownMenu.Item onSelect={onAccountSettings}>Account settings</DropdownMenu.Item>
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in user surface for AppShell's `identity` slot.
|
|
3
|
+
*
|
|
4
|
+
* Graduated from the shell lab (`packages/console/src/routes/lab/+page.svelte`,
|
|
5
|
+
* the `identity === 'avatar'` shape) — operator ruling 04/09/2026: avatar
|
|
6
|
+
* only at rest, no name, no chevron, and no prop reintroduces a variant.
|
|
7
|
+
* Opening it answers who is signed in, which workspace they act in, and the
|
|
8
|
+
* ways out — identity first, because a menu that opens on a list of actions
|
|
9
|
+
* makes you infer who you are from the avatar you just clicked.
|
|
10
|
+
*
|
|
11
|
+
* Prop names match the stamped template's auth store
|
|
12
|
+
* (`frontend/src/lib/auth.svelte.ts` — `User`, `Membership`) exactly, so a
|
|
13
|
+
* consumer wires this straight from `auth.user`, `auth.entitlements`,
|
|
14
|
+
* `auth.activeWorkspace` and `auth.activeRole` with no reshaping. A guessed
|
|
15
|
+
* shape needs rewriting the day it meets the real store, which is the whole
|
|
16
|
+
* reason this one didn't. `display_name` and `email` are both nullable
|
|
17
|
+
* there, and `activeWorkspace` is genuinely null until a caller with several
|
|
18
|
+
* memberships chooses one — this component is built against that, not a
|
|
19
|
+
* happy path.
|
|
20
|
+
*
|
|
21
|
+
* Sign-out and settings are the consuming app's concerns, never behaviour
|
|
22
|
+
* baked in here: `onSignOut` and `onAccountSettings` are callbacks. Theme
|
|
23
|
+
* follows AppShell's own `onToggleTheme` pattern — override it, or accept
|
|
24
|
+
* mode-watcher's `toggleMode` by default.
|
|
25
|
+
*
|
|
26
|
+
* `onManageMembers` exists for the caller a workspace switcher has nothing
|
|
27
|
+
* to show: `WorkspaceMenu` (the factory template) renders nothing in the
|
|
28
|
+
* bar for a single-member workspace, and this is where Members lands
|
|
29
|
+
* instead (operator ruling, 07/09/2026, master-project#291's org-of-one
|
|
30
|
+
* case). Wire it whenever the app has a members route, whether or not it
|
|
31
|
+
* also renders a switcher.
|
|
32
|
+
*
|
|
33
|
+
* `links` is the escape valve for everything the fixed entries have no room
|
|
34
|
+
* for — an Admin route, a support link — as a LIST, so every app's extra
|
|
35
|
+
* destinations render as the same row. Without it an app puts its own
|
|
36
|
+
* destination in AppShell's `actions` slot as an icon button (cadmus did,
|
|
37
|
+
* twice), and two apps do it two ways in the same bar.
|
|
38
|
+
*/
|
|
39
|
+
import type { Component } from 'svelte';
|
|
1
40
|
type $$ComponentProps = {
|
|
2
41
|
user: {
|
|
3
42
|
username: string;
|
|
@@ -28,7 +67,29 @@ type $$ComponentProps = {
|
|
|
28
67
|
onManageMembers?: () => void;
|
|
29
68
|
/** Override the theme action. Defaults to mode-watcher's toggleMode, matching AppShell. */
|
|
30
69
|
onSwitchTheme?: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* App destinations the fixed entries have no room for, rendered in their
|
|
72
|
+
* own group.
|
|
73
|
+
*
|
|
74
|
+
* A list of items, never a snippet: a snippet lets each app invent its own
|
|
75
|
+
* row shape, and two apps inventing two shapes in the same top bar is the
|
|
76
|
+
* drift this package exists to end. Cadmus is the measured case — its
|
|
77
|
+
* Admin entry had nowhere to go and became an icon button in AppShell's
|
|
78
|
+
* `actions` slot, and a second one (a quiet support link) was about to
|
|
79
|
+
* follow it there.
|
|
80
|
+
*
|
|
81
|
+
* No `href`: the callback shape matches every other action on this
|
|
82
|
+
* component, and a consumer routes with its own `goto`. The icon is
|
|
83
|
+
* optional — a label-only row stays aligned with any iconed row beside it.
|
|
84
|
+
*/
|
|
85
|
+
links?: {
|
|
86
|
+
label: string;
|
|
87
|
+
icon?: Component<{
|
|
88
|
+
class?: string;
|
|
89
|
+
}>;
|
|
90
|
+
onSelect: () => void;
|
|
91
|
+
}[];
|
|
31
92
|
};
|
|
32
|
-
declare const AppIdentity:
|
|
93
|
+
declare const AppIdentity: Component<$$ComponentProps, {}, "">;
|
|
33
94
|
type AppIdentity = ReturnType<typeof AppIdentity>;
|
|
34
95
|
export default AppIdentity;
|
|
@@ -575,7 +575,19 @@
|
|
|
575
575
|
{@render brandLockup(true)}
|
|
576
576
|
</span>
|
|
577
577
|
|
|
578
|
-
|
|
578
|
+
<!-- `min-w-0` is what lets a leading `context` snippet (a workspace
|
|
579
|
+
switcher, a breadcrumb) actually shrink under crowding — a flex
|
|
580
|
+
item's automatic `min-width: auto` otherwise floors it at its own
|
|
581
|
+
min-content size, so with search, actions and identity all also
|
|
582
|
+
competing for the bar the FIRST thing to run out of room was
|
|
583
|
+
whichever trailing control happened to sit last in source order
|
|
584
|
+
(theme, identity) — pushed edge-to-edge past the header's own
|
|
585
|
+
right edge and, with no horizontal scroll on the bar, permanently
|
|
586
|
+
unreachable rather than merely cramped (design-system#111). The
|
|
587
|
+
wordmark and the search button already carry their own shrink
|
|
588
|
+
behaviour (hiding text, going icon-only); `context` is the one
|
|
589
|
+
leading slot that never had to. -->
|
|
590
|
+
{#if context}<div class="min-w-0">{@render context()}</div>{/if}
|
|
579
591
|
|
|
580
592
|
{#if onSearch && searchPlacement === 'leading'}
|
|
581
593
|
{@render searchButton(
|
package/dist/styles.css
CHANGED
|
@@ -624,6 +624,15 @@
|
|
|
624
624
|
.ds-nav {
|
|
625
625
|
--ds-nav-ink: var(--ds-nav-chrome-ink, var(--foreground));
|
|
626
626
|
--ds-nav-ink-muted: var(--ds-nav-chrome-ink-muted, var(--muted-foreground));
|
|
627
|
+
/* Specified rather than left `visible` (the default): `overflow-y: auto`
|
|
628
|
+
otherwise forces the computed value of an unspecified overflow-x to
|
|
629
|
+
`auto` too (CSSOM's "if one axis is visible and the other is not, the
|
|
630
|
+
visible one becomes auto" rule), so a row wider than the nav — a long
|
|
631
|
+
label, a wide badge — got its own sideways scrollbar inside the drawer.
|
|
632
|
+
The drawer scrolls vertically only; a row that doesn't fit truncates
|
|
633
|
+
(`.ds-nav-item`'s label already carries `.truncate`) rather than
|
|
634
|
+
growing a second scroll axis on top of the menu's own (design-system#111). */
|
|
635
|
+
overflow-x: hidden;
|
|
627
636
|
}
|
|
628
637
|
|
|
629
638
|
.ds-shell-rail,
|
|
@@ -847,6 +856,21 @@
|
|
|
847
856
|
padding-inline: 0;
|
|
848
857
|
}
|
|
849
858
|
|
|
859
|
+
/* A hard ceiling on the whole shell, in the one unit that names the actual
|
|
860
|
+
drawn surface rather than a percentage of an ancestor. `.ds-shell` is
|
|
861
|
+
already `flex` with `overflow-hidden` in the component markup, which
|
|
862
|
+
clips an ordinary descendant that outgrows it — but a `position: fixed`
|
|
863
|
+
descendant (the open drawer, below) is excused from an ancestor's
|
|
864
|
+
`overflow: hidden` by definition, fixed positioning escapes every
|
|
865
|
+
clipping ancestor that is not itself a containing block for fixed
|
|
866
|
+
elements, and `.ds-shell` is not one. This is the backstop for that
|
|
867
|
+
whole class of leak: whatever grows past the shell's own box, on
|
|
868
|
+
whichever descendant, the SHELL still cannot be asked to draw wider than
|
|
869
|
+
the viewport it is actually in (design-system#111). */
|
|
870
|
+
.ds-shell {
|
|
871
|
+
max-width: 100dvw;
|
|
872
|
+
}
|
|
873
|
+
|
|
850
874
|
/* The rail is ONE element in two states, not two elements (see the comment in
|
|
851
875
|
app-shell.svelte): below md it is hidden until `data-drawer` turns it into
|
|
852
876
|
an overlay; at md and up it is the permanent column and `data-drawer` means
|
|
@@ -871,16 +895,35 @@
|
|
|
871
895
|
the last nav row at its bottom sit exactly where a notch or a home
|
|
872
896
|
indicator gesture bar would cover them. The rail's own sticky state
|
|
873
897
|
never needs this — it starts below the page's own top edge in normal
|
|
874
|
-
flow, not against the physical screen edge.
|
|
898
|
+
flow, not against the physical screen edge.
|
|
899
|
+
|
|
900
|
+
The width is a `min()` against the true viewport (`100dvw`, less the
|
|
901
|
+
left inset a landscape notch carves out of it) rather than the old
|
|
902
|
+
`max-width: 82vw` — `vw` is the LAYOUT viewport, which on a phone stays
|
|
903
|
+
the largest size the page could ever occupy whether or not the browser
|
|
904
|
+
chrome is actually showing, while `dvw` is the viewport the page is
|
|
905
|
+
ACTUALLY drawn into right now. The two rarely disagree on width the way
|
|
906
|
+
`vh`/`dvh` disagree on height, but disagreeing on principle was already
|
|
907
|
+
enough to make this the one place on the drawer that named the wrong
|
|
908
|
+
viewport (design-system#111): every other rule the drawer carries reads
|
|
909
|
+
`dvh`/`env(safe-area-inset-*)`, so `82vw` was the one dimension still
|
|
910
|
+
trusting a unit this component elsewhere treats as untrustworthy on a
|
|
911
|
+
phone. `min()` over `--ds-shell-rail-width` keeps the drawer at its
|
|
912
|
+
ordinary width on anything bigger than a narrow phone, and only the
|
|
913
|
+
viewport term binds below that — so the drawer can never be asked to be
|
|
914
|
+
wider than the screen it is drawn on, at any width, by construction
|
|
915
|
+
rather than by testing every content shape that could have pushed it
|
|
916
|
+
there. */
|
|
875
917
|
.ds-shell-rail[data-drawer='true'] {
|
|
876
918
|
display: flex;
|
|
877
919
|
position: fixed;
|
|
878
920
|
inset-block: 0;
|
|
879
921
|
left: 0;
|
|
880
922
|
z-index: 50;
|
|
881
|
-
|
|
923
|
+
width: min(var(--ds-shell-rail-width), 82dvw, 100dvw - env(safe-area-inset-left, 0px));
|
|
882
924
|
padding-top: env(safe-area-inset-top);
|
|
883
925
|
padding-bottom: env(safe-area-inset-bottom);
|
|
926
|
+
padding-left: env(safe-area-inset-left);
|
|
884
927
|
box-shadow:
|
|
885
928
|
0 10px 15px -3px oklch(0 0 0 / 0.1),
|
|
886
929
|
0 4px 6px -4px oklch(0 0 0 / 0.1);
|
|
@@ -896,9 +939,10 @@
|
|
|
896
939
|
.ds-shell-rail[data-drawer='true'] {
|
|
897
940
|
position: sticky;
|
|
898
941
|
z-index: auto;
|
|
899
|
-
|
|
942
|
+
width: var(--ds-shell-rail-width);
|
|
900
943
|
padding-top: 0;
|
|
901
944
|
padding-bottom: 0;
|
|
945
|
+
padding-left: 0;
|
|
902
946
|
box-shadow: none;
|
|
903
947
|
animation: none;
|
|
904
948
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.6",
|
|
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.9.
|
|
4
|
+
Generated from `@poodle64/ui@2026.9.6`. 56 components, 15 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
|
|