@marianmeres/stuic 3.92.0 → 3.94.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.
@@ -18,7 +18,9 @@ A flexible avatar component that displays user photos, initials, or icons with a
18
18
  | `bg` | `string` | - | Background color (Tailwind class). Ignored if `autoColor=true` |
19
19
  | `textColor` | `string` | - | Text color (Tailwind class). Ignored if `autoColor=true` |
20
20
  | `autoColor` | `boolean` | `false` | Generate deterministic pastel colors from hashSource/initials |
21
+ | `padding` | `string` | - | CSS padding around the visual circle. Outer keeps its size, inner circle shrinks (e.g. `"4px"`, `"0.25rem"`). Useful for larger tap targets. |
21
22
  | `class` | `string` | - | Additional CSS classes |
23
+ | `classInner` | `string` | - | Additional CSS classes for the inner element (only used when `padding` is set) |
22
24
  | `el` | `HTMLElement` | - | Bindable element reference |
23
25
 
24
26
  ## Usage
@@ -82,6 +84,18 @@ Generate deterministic pastel colors based on a hash source:
82
84
  <Avatar src="/photo.jpg" onclick={() => console.log("clicked")} />
83
85
  ```
84
86
 
87
+ ### Padded (larger tap target / visually smaller circle)
88
+
89
+ Use `padding` to keep the avatar's outer footprint (e.g. a 44px tap target) while shrinking the visible colored circle. The padded ring is transparent.
90
+
91
+ ```svelte
92
+ <!-- Outer stays at size="md" (2.75rem), circle shrinks by 4px on each side -->
93
+ <Avatar size="md" initials="AB" padding="4px" />
94
+ <Avatar size="md" autoColor initials="john@example.com" padding="0.25rem" onclick={() => {}} />
95
+ ```
96
+
97
+ When `padding` is set, `bg`, `textColor`, and `autoColor` automatically apply to the inner circle (not the outer element).
98
+
85
99
  ### Custom Colors
86
100
 
87
101
  ```svelte
@@ -107,6 +121,7 @@ Override globally in `:root` or locally via `style` prop:
107
121
  | `--stuic-avatar-fg` | `--stuic-color-muted-foreground` | Default text/icon color |
108
122
  | `--stuic-avatar-ring-width` | `3px` | Focus ring width (button mode) |
109
123
  | `--stuic-avatar-ring-color` | `--stuic-color-ring` | Focus ring color |
124
+ | `--stuic-avatar-padding` | - | Set by the `padding` prop; can also be driven directly via CSS |
110
125
 
111
126
  ### Size Tokens
112
127
 
@@ -148,6 +163,7 @@ The component uses data attributes for CSS styling:
148
163
  | ------------------ | ----------------------------- | ----------------------------------- |
149
164
  | `data-size` | `sm`, `md`, `lg`, `xl`, `2xl` | Size preset (only for preset sizes) |
150
165
  | `data-interactive` | `true` | Present when `onclick` is provided |
166
+ | `data-padded` | `""` | Present when `padding` is set |
151
167
 
152
168
  ## Theming
153
169
 
@@ -16,6 +16,11 @@
16
16
  Similar to ColorSchemeSystemAware, except that it never reads window.matchMedia and only
17
17
  relies on the local userland setting.
18
18
 
19
+ If no preference is stored, this bootstrap leaves the `<html>` class
20
+ alone — meaning the app's SSR'd default wins. Ship `<html class="dark">`
21
+ from your `app.html` / SSR to default the app to dark; users can still
22
+ override via `ColorScheme.toggle()` later.
23
+
19
24
  Uses the hardcoded default storage key "stuic-color-scheme". Apps that
20
25
  override the runtime key via `ColorScheme.configure({ key })` should ship
21
26
  their own inline bootstrap in `app.html` if FOUC-free hydration matters.
@@ -24,6 +29,9 @@
24
29
  <script>
25
30
  const KEY = window.__COLOR_SCHEME_KEY__ ?? "stuic-color-scheme";
26
31
  const cls = window.document.documentElement.classList;
27
- localStorage.getItem(KEY) === "dark" ? cls.add("dark") : cls.remove("dark");
32
+ const v = localStorage.getItem(KEY);
33
+ if (v === "dark") cls.add("dark");
34
+ else if (v === "light") cls.remove("dark");
35
+ // else: no stored preference — leave the SSR'd class alone.
28
36
  </script>
29
37
  </svelte:head>
@@ -13,7 +13,10 @@
13
13
  </script>
14
14
 
15
15
  <!--
16
- If you do not wish to take the system preference into account use ColorSchemeLocal sibling.
16
+ Explicit opt-in for OS-aware first paint. The `ColorScheme` runtime itself
17
+ never auto-derives from `prefers-color-scheme` — only this bootstrap does,
18
+ and only at hydration when no preference is stored. If you do not want
19
+ system preference taken into account, use the ColorSchemeLocal sibling.
17
20
 
18
21
  Uses the hardcoded default storage key "stuic-color-scheme". Apps that
19
22
  override the runtime key via `ColorScheme.configure({ key })` should ship
@@ -1,31 +1,62 @@
1
1
  # ColorScheme
2
2
 
3
- Hydration components for dark mode support. Add/remove the `dark` class on the document root based on localStorage and optionally system preference.
3
+ Hydration components and a small runtime for dark mode support. Add/remove the `dark` class on the `<html>` element based on a stored user preference. The runtime has **no opinion**: when `localStorage` is empty, it defers to whatever class the SSR'd `<html>` already has — so apps can ship dark-by-default just by SSR'ing `<html class="dark">`.
4
+
5
+ ## Contract
6
+
7
+ - `localStorage["stuic-color-scheme"]` is `"dark"` or `"light"` → that wins.
8
+ - Otherwise → defer to the current `<html>` class (set by SSR, `app.html`, or your own bootstrap).
9
+ - `prefers-color-scheme` is **never** consulted implicitly. It is read only when:
10
+ - You explicitly mount `<ColorSchemeSystemAware />` (which reads it once, at first paint, to seed the DOM when no preference is stored), or
11
+ - You call `ColorScheme.getSystemValue()` from your own UI (e.g. a "Use system preference" button).
4
12
 
5
13
  ## Components
6
14
 
7
15
  ### ColorSchemeLocal
8
16
 
9
- Uses only the localStorage setting to determine dark mode. Does not check system preference.
17
+ Uses only the localStorage setting. If no preference is stored, the inline bootstrap leaves the `<html>` class alone — the SSR'd default wins.
10
18
 
11
19
  ### ColorSchemeSystemAware
12
20
 
13
- Checks localStorage first, then falls back to system preference (`prefers-color-scheme: dark`).
21
+ Explicit opt-in for OS-aware first paint. Checks localStorage first; if empty, falls back to `prefers-color-scheme: dark`. Use this if you want the OS to influence first paint.
14
22
 
15
23
  ## Props
16
24
 
17
- Both components have **no props** - they are pure hydration components.
25
+ Both components have **no props** they are pure hydration components.
18
26
 
19
27
  ## Storage Key
20
28
 
21
- Both components use the localStorage key: `stuic-color-scheme`
29
+ Default localStorage key: `stuic-color-scheme`.
22
30
 
23
31
  - Value `"dark"` → adds `dark` class to `<html>`
24
- - Any other value → removes `dark` class
32
+ - Value `"light"` → removes `dark` class
33
+ - Anything else / absent → no DOM change (defers to existing class)
34
+
35
+ Override via `ColorScheme.configure({ key: "myapp:color-scheme" })`. Call early in app boot. For FOUC-free hydration with a custom key, ship your own inline bootstrap in `app.html`.
25
36
 
26
37
  ## Usage
27
38
 
28
- ### System-Aware (Recommended)
39
+ ### Local (no system fallback)
40
+
41
+ ```svelte
42
+ <script lang="ts">
43
+ import { ColorSchemeLocal } from "stuic";
44
+ </script>
45
+
46
+ <ColorSchemeLocal />
47
+ ```
48
+
49
+ ### Dark by default
50
+
51
+ Just SSR the `dark` class on `<html>` (e.g. in `src/app.html`):
52
+
53
+ ```html
54
+ <html lang="en" class="dark"></html>
55
+ ```
56
+
57
+ With `<ColorSchemeLocal />` mounted, the app starts dark; toggling persists `"light"` to localStorage; calling `ColorScheme.reset()` clears the preference (the next reload restores the dark default).
58
+
59
+ ### System-aware
29
60
 
30
61
  ```svelte
31
62
  <script lang="ts">
@@ -35,30 +66,40 @@ Both components use the localStorage key: `stuic-color-scheme`
35
66
  <ColorSchemeSystemAware />
36
67
  ```
37
68
 
38
- ### Local Only
69
+ ### Toggle / reset / read
39
70
 
40
71
  ```svelte
41
72
  <script lang="ts">
42
- import { ColorSchemeLocal } from "stuic";
73
+ import { ColorScheme, ColorSchemeLocal } from "stuic";
43
74
  </script>
44
75
 
45
76
  <ColorSchemeLocal />
77
+
78
+ <button onclick={() => ColorScheme.toggle()}>
79
+ Current: {ColorScheme.current}
80
+ </button>
81
+
82
+ <!-- Clear persisted preference. Visual stays as-is until next reload. -->
83
+ <button onclick={() => ColorScheme.reset()}>Reset</button>
46
84
  ```
47
85
 
48
- ### Toggle Dark Mode
86
+ ### Apply system preference on demand
49
87
 
50
88
  ```svelte
51
89
  <script lang="ts">
52
- import { ColorSchemeSystemAware } from "stuic";
90
+ import { ColorScheme } from "stuic";
53
91
 
54
- function toggleDarkMode() {
55
- const root = document.documentElement;
56
- const isDark = root.classList.toggle("dark");
57
- localStorage.setItem("stuic-color-scheme", isDark ? "dark" : "light");
92
+ function useSystem() {
93
+ const v = ColorScheme.getSystemValue();
94
+ localStorage.setItem(ColorScheme.KEY, v);
95
+ document.documentElement.classList.toggle("dark", v === "dark");
96
+ ColorScheme.syncFromDom();
58
97
  }
59
98
  </script>
60
99
 
61
- <ColorSchemeSystemAware />
62
-
63
- <button onclick={toggleDarkMode}>Toggle Dark Mode</button>
100
+ <button onclick={useSystem}>Use system preference</button>
64
101
  ```
102
+
103
+ ## Notes on `reset()`
104
+
105
+ `ColorScheme.reset()` removes the persisted preference from `localStorage`. It does **not** change the visual state in the current session — `toggle()` already painted the DOM, and the lib does not track an "original SSR default" to revert to. The next page load will paint from the SSR'd `<html>` class. If you need a same-session revert, persist your default explicitly (e.g. `localStorage.setItem(ColorScheme.KEY, "dark")` then call your toggle code).
@@ -30,7 +30,7 @@ declare global {
30
30
  * // Toggle between light and dark
31
31
  * ColorScheme.toggle();
32
32
  *
33
- * // Reset to system preference
33
+ * // Clear persisted preference (visual reverts on next reload)
34
34
  * ColorScheme.reset();
35
35
  *
36
36
  * // Use a custom localStorage key (call early in app boot):
@@ -43,6 +43,11 @@ declare global {
43
43
  * - Works with Tailwind's `darkMode: 'class'` configuration
44
44
  * - Pair with `<ColorSchemeLocal />` or `<ColorSchemeSystemAware />` for
45
45
  * FOUC-free initial paint
46
+ * - Runtime never auto-derives from `prefers-color-scheme`. When no
47
+ * preference is stored, it defers to whatever class the SSR'd `<html>`
48
+ * already has. To support OS-aware paint, mount
49
+ * `<ColorSchemeSystemAware />` (opt-in) or call `getSystemValue()` from
50
+ * your own UI.
46
51
  */
47
52
  export declare class ColorScheme {
48
53
  static readonly DARK: "dark";
@@ -77,7 +82,12 @@ export declare class ColorScheme {
77
82
  */
78
83
  static syncFromDom(): void;
79
84
  /**
80
- * Reads the `prefers-color-scheme` system setting
85
+ * Reads the `prefers-color-scheme` system setting. Manual-only utility —
86
+ * the runtime never invokes this implicitly. Wire it to a custom button if
87
+ * your app wants OS-aware behavior, e.g.
88
+ * `localStorage.setItem(ColorScheme.KEY, ColorScheme.getSystemValue())`
89
+ * followed by a re-sync, or mount `<ColorSchemeSystemAware />` for
90
+ * OS-aware first paint.
81
91
  */
82
92
  static getSystemValue(): Scheme;
83
93
  /**
@@ -93,8 +103,12 @@ export declare class ColorScheme {
93
103
  */
94
104
  static toggle(): void;
95
105
  /**
96
- * Resets color scheme to system preference by removing the localStorage value
97
- * and re-applying the resolved scheme.
106
+ * Clears the persisted preference from `localStorage`. Does NOT change the
107
+ * current visual state in the active session — `_sync()` will read the
108
+ * (now empty) storage and the DOM (last applied class) and find no change.
109
+ * The visual revert to the app's SSR'd default happens on the next page
110
+ * load. The runtime never consults `prefers-color-scheme` on its own; call
111
+ * `ColorScheme.getSystemValue()` explicitly if you want that behavior.
98
112
  */
99
113
  static reset(): void;
100
114
  }
@@ -5,7 +5,12 @@ function _compute() {
5
5
  const local = globalThis.localStorage?.getItem(_key);
6
6
  if (local === "dark" || local === "light")
7
7
  return local;
8
- return ColorScheme.getSystemValue();
8
+ // No persisted preference: defer to whatever the DOM (SSR'd <html> class
9
+ // or a prior bootstrap) currently shows. The runtime has no opinion of
10
+ // its own and never auto-derives from prefers-color-scheme.
11
+ return globalThis?.document?.documentElement.classList.contains(ColorScheme.DARK)
12
+ ? ColorScheme.DARK
13
+ : ColorScheme.LIGHT;
9
14
  }
10
15
  function _applyDom(scheme) {
11
16
  globalThis?.document?.documentElement.classList.toggle(ColorScheme.DARK, scheme === ColorScheme.DARK);
@@ -35,7 +40,7 @@ function _sync() {
35
40
  * // Toggle between light and dark
36
41
  * ColorScheme.toggle();
37
42
  *
38
- * // Reset to system preference
43
+ * // Clear persisted preference (visual reverts on next reload)
39
44
  * ColorScheme.reset();
40
45
  *
41
46
  * // Use a custom localStorage key (call early in app boot):
@@ -48,6 +53,11 @@ function _sync() {
48
53
  * - Works with Tailwind's `darkMode: 'class'` configuration
49
54
  * - Pair with `<ColorSchemeLocal />` or `<ColorSchemeSystemAware />` for
50
55
  * FOUC-free initial paint
56
+ * - Runtime never auto-derives from `prefers-color-scheme`. When no
57
+ * preference is stored, it defers to whatever class the SSR'd `<html>`
58
+ * already has. To support OS-aware paint, mount
59
+ * `<ColorSchemeSystemAware />` (opt-in) or call `getSystemValue()` from
60
+ * your own UI.
51
61
  */
52
62
  export class ColorScheme {
53
63
  static DARK = "dark";
@@ -99,7 +109,12 @@ export class ColorScheme {
99
109
  _current = next;
100
110
  }
101
111
  /**
102
- * Reads the `prefers-color-scheme` system setting
112
+ * Reads the `prefers-color-scheme` system setting. Manual-only utility —
113
+ * the runtime never invokes this implicitly. Wire it to a custom button if
114
+ * your app wants OS-aware behavior, e.g.
115
+ * `localStorage.setItem(ColorScheme.KEY, ColorScheme.getSystemValue())`
116
+ * followed by a re-sync, or mount `<ColorSchemeSystemAware />` for
117
+ * OS-aware first paint.
103
118
  */
104
119
  static getSystemValue() {
105
120
  return globalThis.matchMedia?.(`(prefers-color-scheme: ${ColorScheme.DARK})`).matches
@@ -128,8 +143,12 @@ export class ColorScheme {
128
143
  _current = next;
129
144
  }
130
145
  /**
131
- * Resets color scheme to system preference by removing the localStorage value
132
- * and re-applying the resolved scheme.
146
+ * Clears the persisted preference from `localStorage`. Does NOT change the
147
+ * current visual state in the active session — `_sync()` will read the
148
+ * (now empty) storage and the DOM (last applied class) and find no change.
149
+ * The visual revert to the app's SSR'd default happens on the next page
150
+ * load. The runtime never consults `prefers-color-scheme` on its own; call
151
+ * `ColorScheme.getSystemValue()` explicitly if you want that behavior.
133
152
  */
134
153
  static reset() {
135
154
  globalThis.localStorage?.removeItem(_key);
@@ -142,9 +161,11 @@ if (typeof window !== "undefined") {
142
161
  // the same key the bootstrap painted from.
143
162
  if (window.__COLOR_SCHEME_KEY__)
144
163
  _key = window.__COLOR_SCHEME_KEY__;
145
- // Seed from localStorage (with system-pref fallback). Reading from the DOM
146
- // here is unreliable: module init runs before the hydration component's
147
- // inline <script> is appended to <head>, so the dark class isn't there yet.
164
+ // Seed from localStorage; if empty, fall back to the current <html> class
165
+ // (set by SSR, app.html, or a bootstrap <script> in `<svelte:head>` that
166
+ // has already executed inline). In CSR-only setups the bootstrap may run
167
+ // after this read — that is fine, the hydration component's `$effect`
168
+ // later calls `syncFromDom()` to reconcile.
148
169
  _current = _compute();
149
170
  window.addEventListener("storage", (e) => {
150
171
  if (e.key === _key)
@@ -53,6 +53,19 @@ The component composes existing primitives and adds convention, not behavior. It
53
53
  />
54
54
  ```
55
55
 
56
+ ### Separate trigger vs header-tile avatar styling
57
+
58
+ `avatar` is the shared base; `avatarHeader` overrides keys on the header-tile Avatar only (shallow merge).
59
+
60
+ ```svelte
61
+ <UserAvatarMenu
62
+ identity={{ email: user.email }}
63
+ actions={{ onProfile: () => goto("/me"), onLogout: () => goto("/logout") }}
64
+ avatar={{ class: "size-8" }}
65
+ avatarHeader={{ class: "size-16", padding: "10px" }}
66
+ />
67
+ ```
68
+
56
69
  ## Props
57
70
 
58
71
  | Prop | Type | Default | Description |
@@ -65,7 +78,8 @@ The component composes existing primitives and adds convention, not behavior. It
65
78
  | `showRoles` | `boolean` | `false` | Render `identity.roles` under the email in the header tile. |
66
79
  | `extraItems` | `DropdownMenuItem[]` | — | Appended to the standard item set. |
67
80
  | `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). |
81
+ | `avatar` | `Partial<AvatarProps>` | — | Forwarded to BOTH the trigger Avatar and the header-tile Avatar. |
82
+ | `avatarHeader` | `Partial<AvatarProps>` | — | Overrides applied on top of `avatar` for the header-tile Avatar only. Shallow merge. |
69
83
  | `position` | `DropdownMenuPosition` | — | Forwarded to `DropdownMenu`. |
70
84
  | `offset` | `string` | — | Forwarded. |
71
85
  | `maxHeight` | `string` | — | Forwarded. |
@@ -116,9 +116,16 @@
116
116
  */
117
117
  items?: DropdownMenuItem[];
118
118
 
119
- /** Forwarded to the default `Avatar` trigger and header-tile avatar. */
119
+ /** Forwarded to BOTH the trigger `Avatar` and the header-tile `Avatar`. */
120
120
  avatar?: Partial<Omit<AvatarProps, "onclick" | "initials" | "src" | "el">>;
121
121
 
122
+ /**
123
+ * Overrides applied on top of `avatar` for the header-tile `Avatar` only
124
+ * (the larger avatar shown above the menu items inside the popup).
125
+ * Shallow-merged: keys present here win over `avatar`.
126
+ */
127
+ avatarHeader?: Partial<Omit<AvatarProps, "onclick" | "initials" | "src" | "el">>;
128
+
122
129
  /** Forwarded to `DropdownMenu`. */
123
130
  position?: DropdownMenuPosition;
124
131
  offset?: string;
@@ -181,6 +188,7 @@
181
188
  extraItems,
182
189
  items: itemsOverride,
183
190
  avatar: avatarOverrides,
191
+ avatarHeader: avatarHeaderOverrides,
184
192
  position,
185
193
  offset,
186
194
  maxHeight,
@@ -197,6 +205,11 @@
197
205
 
198
206
  const isAuthed = $derived(!!identity);
199
207
 
208
+ const avatarHeaderMerged = $derived({
209
+ ...(avatarOverrides ?? {}),
210
+ ...(avatarHeaderOverrides ?? {}),
211
+ });
212
+
200
213
  // Color-scheme config normalization
201
214
  const cs = $derived(
202
215
  typeof colorScheme === "object" && colorScheme !== null ? colorScheme : {}
@@ -345,7 +358,7 @@
345
358
  hashSource={identity.email}
346
359
  src={identity.src}
347
360
  onclick={actions.onProfile}
348
- {...avatarOverrides}
361
+ {...avatarHeaderMerged}
349
362
  />
350
363
  <div class={!unstyled ? "stuic-user-avatar-menu-header-email" : undefined}>
351
364
  {identity.name ?? identity.email}
@@ -97,8 +97,14 @@ export interface Props {
97
97
  * `showHeaderTile`, `showRoles`) is not.
98
98
  */
99
99
  items?: DropdownMenuItem[];
100
- /** Forwarded to the default `Avatar` trigger and header-tile avatar. */
100
+ /** Forwarded to BOTH the trigger `Avatar` and the header-tile `Avatar`. */
101
101
  avatar?: Partial<Omit<AvatarProps, "onclick" | "initials" | "src" | "el">>;
102
+ /**
103
+ * Overrides applied on top of `avatar` for the header-tile `Avatar` only
104
+ * (the larger avatar shown above the menu items inside the popup).
105
+ * Shallow-merged: keys present here win over `avatar`.
106
+ */
107
+ avatarHeader?: Partial<Omit<AvatarProps, "onclick" | "initials" | "src" | "el">>;
102
108
  /** Forwarded to `DropdownMenu`. */
103
109
  position?: DropdownMenuPosition;
104
110
  offset?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.92.0",
3
+ "version": "3.94.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",