@poodle64/ui 2026.9.1 → 2026.9.2

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.
@@ -0,0 +1,138 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The signed-in user surface for AppShell's `identity` slot.
4
+ *
5
+ * Graduated from the shell lab (`packages/console/src/routes/lab/+page.svelte`,
6
+ * the `identity === 'avatar'` shape) — operator ruling 04/09/2026: avatar
7
+ * only at rest, no name, no chevron, and no prop reintroduces a variant.
8
+ * Opening it answers who is signed in, which workspace they act in, and the
9
+ * ways out — identity first, because a menu that opens on a list of actions
10
+ * makes you infer who you are from the avatar you just clicked.
11
+ *
12
+ * Prop names match the stamped template's auth store
13
+ * (`frontend/src/lib/auth.svelte.ts` — `User`, `Membership`) exactly, so a
14
+ * consumer wires this straight from `auth.user`, `auth.entitlements`,
15
+ * `auth.activeWorkspace` and `auth.activeRole` with no reshaping. A guessed
16
+ * shape needs rewriting the day it meets the real store, which is the whole
17
+ * reason this one didn't. `display_name` and `email` are both nullable
18
+ * there, and `activeWorkspace` is genuinely null until a caller with several
19
+ * memberships chooses one — this component is built against that, not a
20
+ * happy path.
21
+ *
22
+ * Sign-out and settings are the consuming app's concerns, never behaviour
23
+ * baked in here: `onSignOut` and `onAccountSettings` are callbacks. Theme
24
+ * follows AppShell's own `onToggleTheme` pattern — override it, or accept
25
+ * mode-watcher's `toggleMode` by default.
26
+ */
27
+ import { toggleMode } from 'mode-watcher';
28
+ import * as DropdownMenu from '../dropdown-menu/index.js';
29
+ import * as Avatar from '../avatar/index.js';
30
+
31
+ let {
32
+ user,
33
+ workspace = null,
34
+ role = null,
35
+ entitlements = [],
36
+ onSignOut,
37
+ onAccountSettings,
38
+ onSwitchTheme
39
+ }: {
40
+ user: { username: string; display_name: string | null; email: string | null };
41
+ /** The active workspace's name. Omit while none is chosen yet — the block hides rather than rendering empty. */
42
+ workspace?: string | null;
43
+ /** The caller's standing in `workspace`. Ignored while `workspace` is unset. */
44
+ role?: 'owner' | 'member' | null;
45
+ entitlements?: string[];
46
+ /** Required — the whole reason this slot exists is a stamped app rendering no way to sign out. */
47
+ onSignOut: () => void;
48
+ /** Optional until the app has an account-settings destination to send it to. */
49
+ onAccountSettings?: () => void;
50
+ /** Override the theme action. Defaults to mode-watcher's toggleMode, matching AppShell. */
51
+ onSwitchTheme?: () => void;
52
+ } = $props();
53
+
54
+ const shown = $derived(user.display_name ?? user.username);
55
+ // Shown separately only when it says something the display name doesn't —
56
+ // an audit log and a support question use the username, and it is not
57
+ // always the display name.
58
+ const showUsername = $derived(!!user.display_name && user.display_name !== user.username);
59
+
60
+ function initialsOf(name: string): string {
61
+ const parts = name.trim().split(/\s+/).filter(Boolean);
62
+ if (parts.length === 0) return '';
63
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
64
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
65
+ }
66
+ const initials = $derived(initialsOf(shown));
67
+
68
+ function switchTheme() {
69
+ if (onSwitchTheme) onSwitchTheme();
70
+ else toggleMode();
71
+ }
72
+ </script>
73
+
74
+ <DropdownMenu.Root>
75
+ <DropdownMenu.Trigger>
76
+ {#snippet child({ props })}
77
+ <button
78
+ {...props}
79
+ aria-label="Account"
80
+ class="hover:bg-surface-2 grid size-[34px] cursor-pointer place-items-center rounded-md"
81
+ >
82
+ <Avatar.Root class="size-6.5 flex-none">
83
+ <Avatar.Fallback class="bg-primary text-primary-foreground text-[11px] font-bold">
84
+ {initials}
85
+ </Avatar.Fallback>
86
+ </Avatar.Root>
87
+ </button>
88
+ {/snippet}
89
+ </DropdownMenu.Trigger>
90
+ <DropdownMenu.Content align="end" class="w-72">
91
+ <div class="flex items-start gap-3 px-2 py-1.5">
92
+ <Avatar.Root class="size-9 flex-none">
93
+ <Avatar.Fallback class="bg-primary text-primary-foreground text-[13px] font-bold">
94
+ {initials}
95
+ </Avatar.Fallback>
96
+ </Avatar.Root>
97
+ <div class="min-w-0">
98
+ <div class="truncate text-[13.5px] font-semibold">{shown}</div>
99
+ {#if showUsername}
100
+ <div class="text-muted-foreground truncate font-mono text-[11px]">{user.username}</div>
101
+ {/if}
102
+ {#if user.email}
103
+ <div class="text-muted-foreground truncate text-[12px]">{user.email}</div>
104
+ {/if}
105
+ </div>
106
+ </div>
107
+ {#if workspace}
108
+ <DropdownMenu.Separator />
109
+ <DropdownMenu.Label class="text-muted-foreground text-[10px] tracking-[0.1em] uppercase">
110
+ Workspace
111
+ </DropdownMenu.Label>
112
+ <div class="flex items-center justify-between gap-2 px-2 pb-1.5">
113
+ <span class="truncate text-[13px]">{workspace}</span>
114
+ {#if role}
115
+ <span
116
+ class="border-border text-muted-foreground rounded-full border px-1.5 py-px text-[10px] capitalize"
117
+ >
118
+ {role}
119
+ </span>
120
+ {/if}
121
+ </div>
122
+ {#if entitlements.length}
123
+ <div class="flex flex-wrap gap-1 px-2 pb-2">
124
+ {#each entitlements as e (e)}
125
+ <span class="bg-surface-2 text-muted-foreground font-mono rounded px-1.5 py-px text-[10px]">
126
+ {e}
127
+ </span>
128
+ {/each}
129
+ </div>
130
+ {/if}
131
+ {/if}
132
+ <DropdownMenu.Separator />
133
+ <DropdownMenu.Item onSelect={switchTheme}>Switch theme</DropdownMenu.Item>
134
+ <DropdownMenu.Item onSelect={onAccountSettings}>Account settings</DropdownMenu.Item>
135
+ <DropdownMenu.Separator />
136
+ <DropdownMenu.Item class="text-status-error" onSelect={onSignOut}>Sign out</DropdownMenu.Item>
137
+ </DropdownMenu.Content>
138
+ </DropdownMenu.Root>
@@ -0,0 +1,21 @@
1
+ type $$ComponentProps = {
2
+ user: {
3
+ username: string;
4
+ display_name: string | null;
5
+ email: string | null;
6
+ };
7
+ /** The active workspace's name. Omit while none is chosen yet — the block hides rather than rendering empty. */
8
+ workspace?: string | null;
9
+ /** The caller's standing in `workspace`. Ignored while `workspace` is unset. */
10
+ role?: 'owner' | 'member' | null;
11
+ entitlements?: string[];
12
+ /** Required — the whole reason this slot exists is a stamped app rendering no way to sign out. */
13
+ onSignOut: () => void;
14
+ /** Optional until the app has an account-settings destination to send it to. */
15
+ onAccountSettings?: () => void;
16
+ /** Override the theme action. Defaults to mode-watcher's toggleMode, matching AppShell. */
17
+ onSwitchTheme?: () => void;
18
+ };
19
+ declare const AppIdentity: import("svelte").Component<$$ComponentProps, {}, "">;
20
+ type AppIdentity = ReturnType<typeof AppIdentity>;
21
+ export default AppIdentity;
@@ -0,0 +1,2 @@
1
+ export { default as AppIdentity } from './app-identity.svelte';
2
+ export { default } from './app-identity.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as AppIdentity } from './app-identity.svelte';
2
+ export { default } from './app-identity.svelte';
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@poodle64/ui",
3
- "version": "2026.9.1",
3
+ "version": "2026.9.2",
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",
7
7
  "repository": "github:poodle64/design-system",
8
8
  "publishConfig": {
9
9
  "registry": "https://registry.npmjs.org",
10
- "access": "public"
10
+ "access": "public",
11
+ "provenance": false
11
12
  },
12
13
  "files": [
13
14
  "bin",
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.9.1",
4
+ "version": "2026.9.2",
5
5
  "generatedBy": "scripts/generate-registry.mjs",
6
6
  "source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
7
- "componentCount": 55,
7
+ "componentCount": 56,
8
8
  "situationCount": 15
9
9
  },
10
10
  "situations": [
@@ -299,6 +299,13 @@
299
299
  "import": "@poodle64/ui/context-column",
300
300
  "props": "stats, statsTitle?, statsInfo?, detail?, ariaLabel?",
301
301
  "insteadOf": "a hand-built right-hand <aside> with a stat card and a detail pane"
302
+ },
303
+ {
304
+ "name": "AppIdentity",
305
+ "dir": "app-identity",
306
+ "import": "@poodle64/ui/app-identity",
307
+ "props": "user, workspace?, role?, entitlements?, onSignOut, onAccountSettings?, onSwitchTheme?",
308
+ "insteadOf": "a hand-rolled account/avatar dropdown wired to raw DropdownMenu.* + Avatar.*"
302
309
  }
303
310
  ]
304
311
  },
@@ -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.1`. 55 components, 15 situations.
4
+ Generated from `@poodle64/ui@2026.9.2`. 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
 
@@ -127,6 +127,7 @@ _The standing chrome — the app shell and its nav, the one page-title treatment
127
127
  | `AppShell` | `@poodle64/ui/app-shell` | `nav, currentPath, brandTitle?, actions?, context?, children` | a bespoke nav rail + header layout per app |
128
128
  | `PageHeader` | `@poodle64/ui/page-header` | `title?, eyebrow?, breadcrumbs?, icon?, subtitle?, info?, meta?, actions?` | a hand-written <h1> and title bar (the drift gate fails this) |
129
129
  | `ContextColumn` | `@poodle64/ui/context-column` | `stats, statsTitle?, statsInfo?, detail?, ariaLabel?` | a hand-built right-hand <aside> with a stat card and a detail pane |
130
+ | `AppIdentity` | `@poodle64/ui/app-identity` | `user, workspace?, role?, entitlements?, onSignOut, onAccountSettings?, onSwitchTheme?` | a hand-rolled account/avatar dropdown wired to raw DropdownMenu.* + Avatar.* |
130
131
 
131
132
  ## A dialog or overlay
132
133