@marianmeres/stuic 3.87.0 → 3.89.0
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/API.md +41 -0
- package/dist/components/Checkout/CheckoutAddressForm.svelte +61 -0
- package/dist/components/Checkout/CheckoutAddressForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutGuestForm.svelte +57 -0
- package/dist/components/Checkout/CheckoutGuestForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +81 -6
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutLoginForm.svelte +36 -1
- package/dist/components/Checkout/CheckoutLoginForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutShippingStep.svelte +41 -0
- package/dist/components/Checkout/CheckoutShippingStep.svelte.d.ts +6 -1
- package/dist/components/LoginForm/LoginForm.svelte +7 -1
- package/dist/components/RegisterForm/RegisterForm.svelte +7 -1
- package/dist/components/UserAvatarMenu/README.md +188 -0
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +416 -0
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte.d.ts +143 -0
- package/dist/components/UserAvatarMenu/index.css +95 -0
- package/dist/components/UserAvatarMenu/index.d.ts +1 -0
- package/dist/components/UserAvatarMenu/index.js +1 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons/index.js +3 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/domains/components.md +59 -0
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# UserAvatarMenu
|
|
2
|
+
|
|
3
|
+
A thin, opinionated wrapper around [`Avatar`](../Avatar/) + [`DropdownMenu`](../DropdownMenu/) for the common "user avatar in the header that opens a small menu" pattern. Renders sensibly in both authenticated and unauthenticated states from the same trigger position.
|
|
4
|
+
|
|
5
|
+
The component composes existing primitives and adds convention, not behavior. It owns **no** auth state, router, or i18n — consumers pass in the user (or `null`) and the handlers.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Minimal — logged in
|
|
10
|
+
|
|
11
|
+
```svelte
|
|
12
|
+
<UserAvatarMenu
|
|
13
|
+
identity={{ email: user.email }}
|
|
14
|
+
actions={{
|
|
15
|
+
onProfile: () => goto("/me"),
|
|
16
|
+
onLogout: () => goto("/logout"),
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Minimal — logged out
|
|
22
|
+
|
|
23
|
+
```svelte
|
|
24
|
+
<UserAvatarMenu
|
|
25
|
+
actions={{
|
|
26
|
+
onLogin: () => openLoginModal(),
|
|
27
|
+
onRegister: () => openRegisterModal(),
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Full (i18n + extras)
|
|
33
|
+
|
|
34
|
+
```svelte
|
|
35
|
+
<UserAvatarMenu
|
|
36
|
+
identity={{
|
|
37
|
+
email: user.email,
|
|
38
|
+
roles: user.roles,
|
|
39
|
+
}}
|
|
40
|
+
showRoles
|
|
41
|
+
actions={{
|
|
42
|
+
onProfile: () => goto("/account/profile"),
|
|
43
|
+
onLogout: () => goto("/logout"),
|
|
44
|
+
}}
|
|
45
|
+
labels={{
|
|
46
|
+
viewProfile: t.raw("dd_view_profile"),
|
|
47
|
+
logout: t.raw("dd_logout"),
|
|
48
|
+
lightMode: t.raw("dd_light_mode"),
|
|
49
|
+
darkMode: t.raw("dd_dark_mode"),
|
|
50
|
+
}}
|
|
51
|
+
avatar={{ class: "size-[60px]", padding: "10px" }}
|
|
52
|
+
classDropdown="min-w-56 rounded-lg border-primary border-3 border-solid"
|
|
53
|
+
/>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Props
|
|
57
|
+
|
|
58
|
+
| Prop | Type | Default | Description |
|
|
59
|
+
| ---------------- | --------------------------------------------- | ------- | ------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `identity` | `UserAvatarMenuIdentity \| null` | `null` | Current user. `null` / `undefined` → unauthenticated mode. |
|
|
61
|
+
| `actions` | `UserAvatarMenuActions` | `{}` | Handlers: `onProfile`, `onSettings`, `onLogout`, `onLogin`, `onRegister`. Missing → hidden. |
|
|
62
|
+
| `labels` | `UserAvatarMenuLabels` | English | Translated strings for built-in items. |
|
|
63
|
+
| `colorScheme` | `boolean \| { enabled?; onToggle?; isDark? }` | `true` | Built-in dark/light toggle. `false` to disable. Object form overrides toggle / read. |
|
|
64
|
+
| `showHeaderTile` | `boolean` | `true` | Render the avatar + email header tile when authenticated. |
|
|
65
|
+
| `showRoles` | `boolean` | `false` | Render `identity.roles` under the email in the header tile. |
|
|
66
|
+
| `extraItems` | `DropdownMenuItem[]` | — | Appended to the standard item set. |
|
|
67
|
+
| `items` | `DropdownMenuItem[]` | — | Full override of the item list. Trigger + dropdown shell still render. |
|
|
68
|
+
| `avatar` | `Partial<AvatarProps>` | — | Forwarded to the default trigger Avatar (and the header-tile Avatar). |
|
|
69
|
+
| `position` | `DropdownMenuPosition` | — | Forwarded to `DropdownMenu`. |
|
|
70
|
+
| `offset` | `string` | — | Forwarded. |
|
|
71
|
+
| `maxHeight` | `string` | — | Forwarded. |
|
|
72
|
+
| `closeOnSelect` | `boolean` | — | Forwarded. |
|
|
73
|
+
| `classDropdown` | `string` | — | Forwarded. |
|
|
74
|
+
| `classTrigger` | `string` | — | Class merged onto the default trigger `<button>`. |
|
|
75
|
+
| `isOpen` | `boolean` | `false` | Bindable open state. |
|
|
76
|
+
| `trigger` | `Snippet<[{ isOpen; toggle; triggerProps }]>` | — | Custom trigger snippet; fully replaces the default. |
|
|
77
|
+
| `headerTile` | `Snippet<[{ identity }]>` | — | Custom header-tile snippet; replaces the default avatar+email tile inside the dropdown. |
|
|
78
|
+
| `unstyled` | `boolean` | `false` | Skip default styling. |
|
|
79
|
+
| `class` | `string` | — | Wrapper classes. |
|
|
80
|
+
| `el` | `HTMLDivElement` | — | Bindable wrapper element. |
|
|
81
|
+
|
|
82
|
+
### `UserAvatarMenuIdentity`
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
interface UserAvatarMenuIdentity {
|
|
86
|
+
email: string;
|
|
87
|
+
name?: string;
|
|
88
|
+
src?: string; // photo URL
|
|
89
|
+
roles?: string[];
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `UserAvatarMenuActions`
|
|
94
|
+
|
|
95
|
+
All handlers are optional. **Omitting a handler hides the corresponding item.** The three unauth handlers (`onLoginOrRegister`, `onLogin`, `onRegister`) are independent — pass any combination.
|
|
96
|
+
|
|
97
|
+
| Handler | Item | Visible in |
|
|
98
|
+
| ------------------- | ----------------- | --------------- |
|
|
99
|
+
| `onProfile` | View profile | Authenticated |
|
|
100
|
+
| `onSettings` | Settings | Authenticated |
|
|
101
|
+
| `onLogout` | Logout | Authenticated |
|
|
102
|
+
| `onLoginOrRegister` | Login or register | Unauthenticated |
|
|
103
|
+
| `onLogin` | Login | Unauthenticated |
|
|
104
|
+
| `onRegister` | Register | Unauthenticated |
|
|
105
|
+
|
|
106
|
+
### `UserAvatarMenuLabels`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
interface UserAvatarMenuLabels {
|
|
110
|
+
viewProfile?: string; // "View profile"
|
|
111
|
+
settings?: string; // "Settings"
|
|
112
|
+
logout?: string; // "Logout"
|
|
113
|
+
loginOrRegister?: string; // "Login or register"
|
|
114
|
+
login?: string; // "Login"
|
|
115
|
+
register?: string; // "Register"
|
|
116
|
+
lightMode?: string; // "Light mode"
|
|
117
|
+
darkMode?: string; // "Dark mode"
|
|
118
|
+
triggerAuthed?: string; // "User menu" (aria-label)
|
|
119
|
+
triggerAnon?: string; // "Sign in" (aria-label)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Default item order
|
|
124
|
+
|
|
125
|
+
Predictable so consumers can reason about position when adding `extraItems`.
|
|
126
|
+
|
|
127
|
+
### Authenticated
|
|
128
|
+
|
|
129
|
+
1. Header tile (when `showHeaderTile !== false`)
|
|
130
|
+
2. View profile (when `actions.onProfile`)
|
|
131
|
+
3. Settings (when `actions.onSettings`)
|
|
132
|
+
4. Color scheme toggle (when `colorScheme !== false`) — sun/moon icon, label flips
|
|
133
|
+
5. Divider (when there was at least one item above **and** `actions.onLogout`)
|
|
134
|
+
6. Logout (when `actions.onLogout`) — with `iconLogOut`
|
|
135
|
+
7. `extraItems` (appended)
|
|
136
|
+
|
|
137
|
+
### Unauthenticated
|
|
138
|
+
|
|
139
|
+
1. Login or register (when `actions.onLoginOrRegister`) — the typical case, wire to a `LoginOrRegisterFormModal`
|
|
140
|
+
2. Login (when `actions.onLogin`)
|
|
141
|
+
3. Register (when `actions.onRegister`)
|
|
142
|
+
4. Color scheme toggle
|
|
143
|
+
5. `extraItems`
|
|
144
|
+
|
|
145
|
+
No header tile in unauth mode.
|
|
146
|
+
|
|
147
|
+
### `items` override
|
|
148
|
+
|
|
149
|
+
When `items` is passed, the entire default set is bypassed. The trigger + shell still render.
|
|
150
|
+
|
|
151
|
+
## Opinionated decisions
|
|
152
|
+
|
|
153
|
+
- **Color scheme is the one built-in side effect.** The component calls `ColorScheme.toggle()` and re-reads `ColorScheme.getValue()` on select. Label/icon flip without consumer involvement. Opt out with `colorScheme={false}`, or override via the object form: `colorScheme={{ onToggle, isDark }}`.
|
|
154
|
+
- **English defaults.** Pass `labels` for i18n. The component never imports an i18n library; consumers pass already-translated strings.
|
|
155
|
+
- **No auth ownership.** No state observation, no log-out call, no router. All side effects are consumer callbacks.
|
|
156
|
+
- **No modal triggering.** "Login" doesn't open a `LoginOrRegisterFormModal` — that's your `onLogin` handler.
|
|
157
|
+
- **Trigger is just `Avatar`.** No wrapper element by default. In unauth mode `Avatar` falls back to `iconUser`. Use the `trigger` snippet for full control.
|
|
158
|
+
|
|
159
|
+
## CSS Tokens
|
|
160
|
+
|
|
161
|
+
Prefix: `--stuic-user-avatar-menu-*`
|
|
162
|
+
|
|
163
|
+
| Token | Default |
|
|
164
|
+
| ------------------------------------------------- | ------------------------------------- |
|
|
165
|
+
| `--stuic-user-avatar-menu-dropdown-width` | `16rem` |
|
|
166
|
+
| `--stuic-user-avatar-menu-trigger-radius` | `var(--stuic-radius)` |
|
|
167
|
+
| `--stuic-user-avatar-menu-trigger-opacity-hover` | `0.85` |
|
|
168
|
+
| `--stuic-user-avatar-menu-trigger-outline-color` | `var(--stuic-color-primary)` |
|
|
169
|
+
| `--stuic-user-avatar-menu-transition` | `var(--stuic-transition)` |
|
|
170
|
+
| `--stuic-user-avatar-menu-header-gap` | `0.5rem` |
|
|
171
|
+
| `--stuic-user-avatar-menu-header-padding` | `0.75rem 0.5rem` |
|
|
172
|
+
| `--stuic-user-avatar-menu-header-margin-bottom` | `0.25rem` |
|
|
173
|
+
| `--stuic-user-avatar-menu-header-bg` | `var(--stuic-color-muted)` |
|
|
174
|
+
| `--stuic-user-avatar-menu-header-color` | `var(--stuic-color-muted-foreground)` |
|
|
175
|
+
| `--stuic-user-avatar-menu-header-radius` | `var(--stuic-radius)` |
|
|
176
|
+
| `--stuic-user-avatar-menu-header-email-font-size` | `inherit` |
|
|
177
|
+
| `--stuic-user-avatar-menu-header-email-color` | `inherit` |
|
|
178
|
+
| `--stuic-user-avatar-menu-header-roles-font-size` | `0.75rem` |
|
|
179
|
+
| `--stuic-user-avatar-menu-header-roles-color` | `var(--stuic-color-muted-foreground)` |
|
|
180
|
+
| `--stuic-user-avatar-menu-header-roles-opacity` | `0.7` |
|
|
181
|
+
|
|
182
|
+
The dropdown has a fixed `width` so every instance opens at the same size, regardless of content. Long emails/names truncate with `text-overflow: ellipsis` against that fixed width. Override `--stuic-user-avatar-menu-dropdown-width` (or pass `classDropdown="!w-72"` for one-off overrides).
|
|
183
|
+
|
|
184
|
+
## See also
|
|
185
|
+
|
|
186
|
+
- [`Avatar`](../Avatar/) — trigger / header-tile primitive
|
|
187
|
+
- [`DropdownMenu`](../DropdownMenu/) — menu shell (item shapes, position values, search)
|
|
188
|
+
- [`ColorScheme`](../ColorScheme/) — class used by the built-in toggle
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type {
|
|
4
|
+
DropdownMenuItem,
|
|
5
|
+
DropdownMenuPosition,
|
|
6
|
+
} from "../DropdownMenu/DropdownMenu.svelte";
|
|
7
|
+
import type { Props as AvatarProps } from "../Avatar/Avatar.svelte";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Identity passed by the consumer. When `null` / `undefined`, the menu
|
|
11
|
+
* renders in unauthenticated mode. When provided, the menu renders the
|
|
12
|
+
* header tile (avatar + email) and the authenticated item set.
|
|
13
|
+
*/
|
|
14
|
+
export interface UserAvatarMenuIdentity {
|
|
15
|
+
/** Used for initials + auto-color hashing. Required when `identity` is set. */
|
|
16
|
+
email: string;
|
|
17
|
+
/** Optional display name (preferred over email in the header tile if present). */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** Optional photo URL (forwarded to Avatar `src`). */
|
|
20
|
+
src?: string;
|
|
21
|
+
/** Optional role list — rendered under the email if `showRoles` is enabled. */
|
|
22
|
+
roles?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard built-in actions. Consumers wire these by passing handlers;
|
|
27
|
+
* omitting a handler hides the corresponding item.
|
|
28
|
+
*
|
|
29
|
+
* The component never navigates, logs out, or toggles theme on its own —
|
|
30
|
+
* it only invokes callbacks. The only built-in side effect is
|
|
31
|
+
* `ColorScheme.toggle()` inside the color-scheme item (see `colorScheme`).
|
|
32
|
+
*/
|
|
33
|
+
export interface UserAvatarMenuActions {
|
|
34
|
+
/** "View profile" item. Hidden when not provided. (Auth state only.) */
|
|
35
|
+
onProfile?: () => void;
|
|
36
|
+
/** "Settings" item. Hidden when not provided. (Auth state only.) */
|
|
37
|
+
onSettings?: () => void;
|
|
38
|
+
/** "Logout" item. Hidden when not provided. (Auth state only.) */
|
|
39
|
+
onLogout?: () => void;
|
|
40
|
+
/**
|
|
41
|
+
* "Login or register" item — combined affordance, typically wired to
|
|
42
|
+
* open a `LoginOrRegisterFormModal`. Hidden when not provided.
|
|
43
|
+
* (Unauth state only.) Independent of `onLogin` / `onRegister`; pass
|
|
44
|
+
* any combination of the three.
|
|
45
|
+
*/
|
|
46
|
+
onLoginOrRegister?: () => void;
|
|
47
|
+
/** "Login" item. Hidden when not provided. (Unauth state only.) */
|
|
48
|
+
onLogin?: () => void;
|
|
49
|
+
/** "Register" item. Hidden when not provided. (Unauth state only.) */
|
|
50
|
+
onRegister?: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Labels for built-in items. All optional — defaults are English strings.
|
|
55
|
+
* Consumers running i18n pass already-translated strings here.
|
|
56
|
+
*/
|
|
57
|
+
export interface UserAvatarMenuLabels {
|
|
58
|
+
viewProfile?: string;
|
|
59
|
+
settings?: string;
|
|
60
|
+
logout?: string;
|
|
61
|
+
loginOrRegister?: string;
|
|
62
|
+
login?: string;
|
|
63
|
+
register?: string;
|
|
64
|
+
lightMode?: string;
|
|
65
|
+
darkMode?: string;
|
|
66
|
+
/** Trigger aria-label when authenticated. Default: "User menu". */
|
|
67
|
+
triggerAuthed?: string;
|
|
68
|
+
/** Trigger aria-label when unauthenticated. Default: "Sign in". */
|
|
69
|
+
triggerAnon?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Color-scheme item config. Defaults to enabled.
|
|
74
|
+
* Pass `false` to disable; pass an object to customize.
|
|
75
|
+
*/
|
|
76
|
+
export type UserAvatarMenuColorScheme =
|
|
77
|
+
| boolean
|
|
78
|
+
| {
|
|
79
|
+
enabled?: boolean;
|
|
80
|
+
/** Override the default `ColorScheme.toggle()` behavior. */
|
|
81
|
+
onToggle?: () => void;
|
|
82
|
+
/** Read current "is dark" state. Defaults to `ColorScheme.getValue() === "dark"`. */
|
|
83
|
+
isDark?: () => boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export interface Props {
|
|
87
|
+
/** Current user. `null` / `undefined` → unauthenticated mode. */
|
|
88
|
+
identity?: UserAvatarMenuIdentity | null;
|
|
89
|
+
|
|
90
|
+
/** Action handlers (see `UserAvatarMenuActions`). */
|
|
91
|
+
actions?: UserAvatarMenuActions;
|
|
92
|
+
|
|
93
|
+
/** Translated / customized labels. */
|
|
94
|
+
labels?: UserAvatarMenuLabels;
|
|
95
|
+
|
|
96
|
+
/** Color-scheme toggle config. Default: enabled. */
|
|
97
|
+
colorScheme?: UserAvatarMenuColorScheme;
|
|
98
|
+
|
|
99
|
+
/** Render the identity header tile (avatar + email) in the dropdown. Default: `true`. */
|
|
100
|
+
showHeaderTile?: boolean;
|
|
101
|
+
|
|
102
|
+
/** Render roles under the email in the header tile. Default: `false`. */
|
|
103
|
+
showRoles?: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extra items appended to the standard set (after Logout / Register).
|
|
107
|
+
* Use for app-specific actions ("Switch project", "Billing", etc.).
|
|
108
|
+
*/
|
|
109
|
+
extraItems?: DropdownMenuItem[];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Full override. When provided, the standard item set is IGNORED and the
|
|
113
|
+
* component renders exactly these items. `identity` is still consulted for
|
|
114
|
+
* the trigger; everything else (`actions`, `labels`, `colorScheme`,
|
|
115
|
+
* `showHeaderTile`, `showRoles`) is not.
|
|
116
|
+
*/
|
|
117
|
+
items?: DropdownMenuItem[];
|
|
118
|
+
|
|
119
|
+
/** Forwarded to the default `Avatar` trigger and header-tile avatar. */
|
|
120
|
+
avatar?: Partial<Omit<AvatarProps, "onclick" | "initials" | "src" | "el">>;
|
|
121
|
+
|
|
122
|
+
/** Forwarded to `DropdownMenu`. */
|
|
123
|
+
position?: DropdownMenuPosition;
|
|
124
|
+
offset?: string;
|
|
125
|
+
maxHeight?: string;
|
|
126
|
+
closeOnSelect?: boolean;
|
|
127
|
+
classDropdown?: string;
|
|
128
|
+
classTrigger?: string;
|
|
129
|
+
|
|
130
|
+
/** Bindable open state (forwarded to `DropdownMenu`). */
|
|
131
|
+
isOpen?: boolean;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Optional custom trigger snippet. Receives the same args as
|
|
135
|
+
* `DropdownMenu`'s `trigger` snippet — `isOpen`, `toggle`, `triggerProps`.
|
|
136
|
+
*/
|
|
137
|
+
trigger?: Snippet<
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
isOpen: boolean;
|
|
141
|
+
toggle: () => void;
|
|
142
|
+
triggerProps: {
|
|
143
|
+
id: string;
|
|
144
|
+
"aria-haspopup": "menu";
|
|
145
|
+
"aria-expanded": boolean;
|
|
146
|
+
"aria-controls": string;
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Optional custom header-tile snippet. Replaces the default
|
|
154
|
+
* avatar + email tile rendered at the top of the menu.
|
|
155
|
+
*/
|
|
156
|
+
headerTile?: Snippet<[{ identity: UserAvatarMenuIdentity }]>;
|
|
157
|
+
|
|
158
|
+
/** Skip default styling. */
|
|
159
|
+
unstyled?: boolean;
|
|
160
|
+
/** Additional CSS classes on the wrapper. */
|
|
161
|
+
class?: string;
|
|
162
|
+
/** Bindable wrapper element. */
|
|
163
|
+
el?: HTMLDivElement;
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<script lang="ts">
|
|
168
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
169
|
+
import { iconLogOut, iconMoon, iconSun } from "../../icons/index.js";
|
|
170
|
+
import Avatar from "../Avatar/Avatar.svelte";
|
|
171
|
+
import DropdownMenu from "../DropdownMenu/DropdownMenu.svelte";
|
|
172
|
+
import { ColorScheme } from "../ColorScheme/index.js";
|
|
173
|
+
|
|
174
|
+
let {
|
|
175
|
+
identity = null,
|
|
176
|
+
actions = {},
|
|
177
|
+
labels = {},
|
|
178
|
+
colorScheme = true,
|
|
179
|
+
showHeaderTile = true,
|
|
180
|
+
showRoles = false,
|
|
181
|
+
extraItems,
|
|
182
|
+
items: itemsOverride,
|
|
183
|
+
avatar: avatarOverrides,
|
|
184
|
+
position,
|
|
185
|
+
offset,
|
|
186
|
+
maxHeight,
|
|
187
|
+
closeOnSelect,
|
|
188
|
+
classDropdown,
|
|
189
|
+
classTrigger,
|
|
190
|
+
isOpen = $bindable(false),
|
|
191
|
+
trigger,
|
|
192
|
+
headerTile,
|
|
193
|
+
unstyled = false,
|
|
194
|
+
class: classProp,
|
|
195
|
+
el = $bindable(),
|
|
196
|
+
}: Props = $props();
|
|
197
|
+
|
|
198
|
+
const isAuthed = $derived(!!identity);
|
|
199
|
+
|
|
200
|
+
// Color-scheme config normalization
|
|
201
|
+
const cs = $derived(
|
|
202
|
+
typeof colorScheme === "object" && colorScheme !== null ? colorScheme : {}
|
|
203
|
+
);
|
|
204
|
+
const csEnabled = $derived(
|
|
205
|
+
colorScheme === false ? false : cs.enabled !== false /* default true */
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
function readIsDark(): boolean {
|
|
209
|
+
return cs.isDark ? cs.isDark() : ColorScheme.getValue() === "dark";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let isDark = $state(false);
|
|
213
|
+
$effect(() => {
|
|
214
|
+
if (csEnabled) isDark = readIsDark();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
function toggleColorScheme() {
|
|
218
|
+
if (cs.onToggle) {
|
|
219
|
+
cs.onToggle();
|
|
220
|
+
} else {
|
|
221
|
+
ColorScheme.toggle();
|
|
222
|
+
}
|
|
223
|
+
isDark = readIsDark();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Default labels (English)
|
|
227
|
+
const L = $derived({
|
|
228
|
+
viewProfile: labels.viewProfile ?? "View profile",
|
|
229
|
+
settings: labels.settings ?? "Settings",
|
|
230
|
+
logout: labels.logout ?? "Logout",
|
|
231
|
+
loginOrRegister: labels.loginOrRegister ?? "Login or register",
|
|
232
|
+
login: labels.login ?? "Login",
|
|
233
|
+
register: labels.register ?? "Register",
|
|
234
|
+
lightMode: labels.lightMode ?? "Light mode",
|
|
235
|
+
darkMode: labels.darkMode ?? "Dark mode",
|
|
236
|
+
triggerAuthed: labels.triggerAuthed ?? "User menu",
|
|
237
|
+
triggerAnon: labels.triggerAnon ?? "Sign in",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
function buildColorSchemeItem(): DropdownMenuItem {
|
|
241
|
+
return {
|
|
242
|
+
type: "action",
|
|
243
|
+
id: "color-scheme",
|
|
244
|
+
label: isDark ? L.lightMode : L.darkMode,
|
|
245
|
+
contentBefore: {
|
|
246
|
+
html: isDark ? iconSun({ size: 16 }) : iconMoon({ size: 16 }),
|
|
247
|
+
},
|
|
248
|
+
onSelect: toggleColorScheme,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const computedItems = $derived.by((): DropdownMenuItem[] => {
|
|
253
|
+
if (itemsOverride) return itemsOverride;
|
|
254
|
+
|
|
255
|
+
const out: DropdownMenuItem[] = [];
|
|
256
|
+
|
|
257
|
+
if (isAuthed) {
|
|
258
|
+
if (showHeaderTile) {
|
|
259
|
+
out.push({
|
|
260
|
+
type: "custom",
|
|
261
|
+
id: "header-tile",
|
|
262
|
+
content: { snippet: renderHeaderTile },
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let renderedAny = false;
|
|
267
|
+
if (actions.onProfile) {
|
|
268
|
+
out.push({
|
|
269
|
+
type: "action",
|
|
270
|
+
id: "profile",
|
|
271
|
+
label: L.viewProfile,
|
|
272
|
+
onSelect: actions.onProfile,
|
|
273
|
+
});
|
|
274
|
+
renderedAny = true;
|
|
275
|
+
}
|
|
276
|
+
if (actions.onSettings) {
|
|
277
|
+
out.push({
|
|
278
|
+
type: "action",
|
|
279
|
+
id: "settings",
|
|
280
|
+
label: L.settings,
|
|
281
|
+
onSelect: actions.onSettings,
|
|
282
|
+
});
|
|
283
|
+
renderedAny = true;
|
|
284
|
+
}
|
|
285
|
+
if (csEnabled) {
|
|
286
|
+
out.push(buildColorSchemeItem());
|
|
287
|
+
renderedAny = true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (renderedAny && actions.onLogout) {
|
|
291
|
+
out.push({ type: "divider", id: "div-logout" });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (actions.onLogout) {
|
|
295
|
+
out.push({
|
|
296
|
+
type: "action",
|
|
297
|
+
id: "logout",
|
|
298
|
+
label: L.logout,
|
|
299
|
+
contentBefore: { html: iconLogOut({ size: 16 }) },
|
|
300
|
+
onSelect: actions.onLogout,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
if (actions.onLoginOrRegister) {
|
|
305
|
+
out.push({
|
|
306
|
+
type: "action",
|
|
307
|
+
id: "login-or-register",
|
|
308
|
+
label: L.loginOrRegister,
|
|
309
|
+
onSelect: actions.onLoginOrRegister,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (actions.onLogin) {
|
|
313
|
+
out.push({
|
|
314
|
+
type: "action",
|
|
315
|
+
id: "login",
|
|
316
|
+
label: L.login,
|
|
317
|
+
onSelect: actions.onLogin,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (actions.onRegister) {
|
|
321
|
+
out.push({
|
|
322
|
+
type: "action",
|
|
323
|
+
id: "register",
|
|
324
|
+
label: L.register,
|
|
325
|
+
onSelect: actions.onRegister,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (csEnabled) out.push(buildColorSchemeItem());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (extraItems?.length) out.push(...extraItems);
|
|
332
|
+
return out;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const wrapperClass = $derived(
|
|
336
|
+
twMerge(!unstyled && "stuic-user-avatar-menu", classProp)
|
|
337
|
+
);
|
|
338
|
+
</script>
|
|
339
|
+
|
|
340
|
+
{#snippet defaultHeaderTile()}
|
|
341
|
+
{#if identity}
|
|
342
|
+
<div class={!unstyled ? "stuic-user-avatar-menu-header" : undefined}>
|
|
343
|
+
<Avatar
|
|
344
|
+
initials={identity.email}
|
|
345
|
+
initialsLength={1}
|
|
346
|
+
autoColor
|
|
347
|
+
hashSource={identity.email}
|
|
348
|
+
src={identity.src}
|
|
349
|
+
onclick={actions.onProfile}
|
|
350
|
+
{...avatarOverrides}
|
|
351
|
+
/>
|
|
352
|
+
<div class={!unstyled ? "stuic-user-avatar-menu-header-email" : undefined}>
|
|
353
|
+
{identity.name ?? identity.email}
|
|
354
|
+
</div>
|
|
355
|
+
{#if showRoles && identity.roles?.length}
|
|
356
|
+
<div class={!unstyled ? "stuic-user-avatar-menu-header-roles" : undefined}>
|
|
357
|
+
{identity.roles.join(", ")}
|
|
358
|
+
</div>
|
|
359
|
+
{/if}
|
|
360
|
+
</div>
|
|
361
|
+
{/if}
|
|
362
|
+
{/snippet}
|
|
363
|
+
|
|
364
|
+
{#snippet renderHeaderTile(_args: Record<string, any>)}
|
|
365
|
+
{#if identity}
|
|
366
|
+
{#if headerTile}
|
|
367
|
+
{@render headerTile({ identity })}
|
|
368
|
+
{:else}
|
|
369
|
+
{@render defaultHeaderTile()}
|
|
370
|
+
{/if}
|
|
371
|
+
{/if}
|
|
372
|
+
{/snippet}
|
|
373
|
+
|
|
374
|
+
{#snippet defaultTrigger({
|
|
375
|
+
toggle,
|
|
376
|
+
triggerProps,
|
|
377
|
+
}: {
|
|
378
|
+
isOpen: boolean;
|
|
379
|
+
toggle: () => void;
|
|
380
|
+
triggerProps: Record<string, any>;
|
|
381
|
+
})}
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
onclick={toggle}
|
|
385
|
+
aria-label={isAuthed ? L.triggerAuthed : L.triggerAnon}
|
|
386
|
+
class={twMerge(!unstyled && "stuic-user-avatar-menu-trigger", classTrigger)}
|
|
387
|
+
{...triggerProps}
|
|
388
|
+
>
|
|
389
|
+
{#if isAuthed && identity}
|
|
390
|
+
<Avatar
|
|
391
|
+
initials={identity.email}
|
|
392
|
+
initialsLength={1}
|
|
393
|
+
autoColor
|
|
394
|
+
hashSource={identity.email}
|
|
395
|
+
src={identity.src}
|
|
396
|
+
{...avatarOverrides}
|
|
397
|
+
/>
|
|
398
|
+
{:else}
|
|
399
|
+
<Avatar {...avatarOverrides} />
|
|
400
|
+
{/if}
|
|
401
|
+
</button>
|
|
402
|
+
{/snippet}
|
|
403
|
+
|
|
404
|
+
<div bind:this={el} class={wrapperClass}>
|
|
405
|
+
<DropdownMenu
|
|
406
|
+
items={computedItems}
|
|
407
|
+
bind:isOpen
|
|
408
|
+
{position}
|
|
409
|
+
{offset}
|
|
410
|
+
{maxHeight}
|
|
411
|
+
{closeOnSelect}
|
|
412
|
+
classDropdown={twMerge(!unstyled && "stuic-user-avatar-menu-dropdown", classDropdown)}
|
|
413
|
+
{unstyled}
|
|
414
|
+
trigger={trigger ?? defaultTrigger}
|
|
415
|
+
/>
|
|
416
|
+
</div>
|