@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.
- package/dist/components/ui/app-identity/app-identity.svelte +138 -0
- package/dist/components/ui/app-identity/app-identity.svelte.d.ts +21 -0
- package/dist/components/ui/app-identity/index.d.ts +2 -0
- package/dist/components/ui/app-identity/index.js +2 -0
- package/package.json +3 -2
- package/registry/component-map.json +9 -2
- package/registry/component-map.md +2 -1
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.9.
|
|
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.
|
|
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":
|
|
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.
|
|
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
|
|