@poodle64/ui 2026.9.5 → 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.
@@ -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: import("svelte").Component<$$ComponentProps, {}, "">;
93
+ declare const AppIdentity: Component<$$ComponentProps, {}, "">;
33
94
  type AppIdentity = ReturnType<typeof AppIdentity>;
34
95
  export default AppIdentity;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/ui",
3
- "version": "2026.9.5",
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
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.9.5",
4
+ "version": "2026.9.6",
5
5
  "generatedBy": "scripts/generate-registry.mjs",
6
6
  "source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
7
7
  "componentCount": 56,
@@ -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.5`. 56 components, 15 situations.
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