@spaethtech/svelte-ui 0.4.1-dev.14.d54aa31 → 0.4.1-dev.17.75f0491

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.
@@ -108,7 +108,8 @@ examples):
108
108
  - **Navigation:** `TabStrip` `SideBarMenu`
109
109
  - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast`
110
110
  - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter`
111
- - **Display / theme:** `NotesEditor` `ThemeSelector`
111
+ - **Display / theme:** `NotesEditor` · `ThemeSelector` (Auto/Light/Dark Select) · `ThemeToggle`
112
+ (compact ☀/☾ icon button, same persisted state)
112
113
  - **Types / utils:** `Variant` `Size` `TabDefinition` (types), `responsiveClasses` / `resolveScalar`,
113
114
  `BREAKPOINT_PX`.
114
115
 
@@ -3,6 +3,7 @@
3
3
  import type { Size } from "../types/sizes.js";
4
4
  import { responsiveClasses, type Responsive } from "../types/responsive.js";
5
5
  import type { Variant } from "../types/variants.js";
6
+ import { applyTheme, readStoredTheme, type ThemeValue } from "./theme.js";
6
7
 
7
8
  // Shared axes, forwarded to the underlying Select (and the label text tracks size).
8
9
  let { size = "md", variant }: { size?: Responsive<Size>; variant?: Variant } = $props();
@@ -15,54 +16,18 @@
15
16
  { label: "Dark", value: "dark" },
16
17
  ];
17
18
 
18
- const themeClasses = {
19
- light: "theme-light",
20
- dark: "theme-dark",
21
- };
22
-
19
+ // Shared theme state (localStorage-backed, applied to <html>) — see ./theme.ts. Kept as a plain
20
+ // string for the Select `bind:value`; the values are constrained to the options above.
23
21
  let selectedTheme = $state("auto");
24
22
 
25
- // Load saved theme on mount
23
+ // Load the saved theme on mount (defaults to "auto" when nothing is stored).
26
24
  $effect(() => {
27
- if (typeof window !== "undefined") {
28
- const savedTheme = localStorage.getItem("svelte-ui-theme");
29
- if (savedTheme && options.some((opt) => opt.value === savedTheme)) {
30
- selectedTheme = savedTheme;
31
- }
32
- }
25
+ selectedTheme = readStoredTheme();
33
26
  });
34
27
 
35
- // Apply theme to <html> (documentElement) NOT <body>.
36
- // The theme tokens (incl. --ui-color-hover/active) are defined at :root, and app.html's pre-hydration
37
- // script forces the class onto <html>. If we applied it to <body> instead, <html> would stay on the
38
- // system @media(prefers-color-scheme) path, so tokens that reference other tokens (--ui-color-hover =
39
- // color-mix(var(--ui-color-text)…)) would resolve against the WRONG theme at :root and inherit down
40
- // stale (e.g. a white hover-tint on a light page → invisible hover). Keep the class on :root.
41
- function applyTheme(themeValue: string) {
42
- const body = document.body;
43
- const html = document.documentElement;
44
-
45
- // Remove all existing theme classes from both body and html (clear any stray body class too)
46
- Object.values(themeClasses).forEach((className) => {
47
- body.classList.remove(className);
48
- html.classList.remove(className);
49
- });
50
-
51
- // Add the selected theme class to <html>
52
- const themeClass = themeClasses[themeValue as keyof typeof themeClasses];
53
- if (themeClass) {
54
- html.classList.add(themeClass);
55
- }
56
-
57
- // Save theme to localStorage
58
- if (typeof window !== "undefined") {
59
- localStorage.setItem("svelte-ui-theme", themeValue);
60
- }
61
- }
62
-
63
- // Apply theme when selection changes
28
+ // Apply (and persist) whenever the selection changes.
64
29
  $effect(() => {
65
- applyTheme(selectedTheme);
30
+ applyTheme(selectedTheme as ThemeValue);
66
31
  });
67
32
  </script>
68
33
 
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ThemeToggle — an icon button that flips between light and dark, sibling to `ThemeSelector`.
4
+ * Shares the same persisted state (`svelte-ui-theme` in localStorage, applied to `<html>`), so the
5
+ * two stay in sync. When nothing is stored it follows the system preference; the icon reflects the
6
+ * effective theme (☀ light / ☾ dark), and a click sets the explicit opposite.
7
+ */
8
+ import Button from "./Button.svelte";
9
+ import type { Size } from "../types/sizes.js";
10
+ import type { Variant } from "../types/variants.js";
11
+ import type { Responsive } from "../types/responsive.js";
12
+ import { applyTheme, readStoredTheme, resolveTheme, type ThemeValue } from "./theme.js";
13
+ import IconSun from "~icons/mdi/weather-sunny";
14
+ import IconMoon from "~icons/mdi/weather-night";
15
+
16
+ // Shared axes, forwarded to the underlying Button. Icon-only, so it defaults to the chromeless ghost.
17
+ let { size = "md", variant = "ghost" }: { size?: Responsive<Size>; variant?: Variant } = $props();
18
+
19
+ // The stored value ("auto" until the user picks) and the tracked system preference.
20
+ let theme = $state<ThemeValue>("auto");
21
+ let system = $state<"light" | "dark">("light");
22
+
23
+ // The theme actually showing — "auto" resolved against the system preference.
24
+ const effective = $derived(theme === "auto" ? system : theme);
25
+
26
+ // Load stored theme + track the system preference (so the icon stays right in "auto" mode).
27
+ $effect(() => {
28
+ theme = readStoredTheme();
29
+ system = resolveTheme("auto");
30
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
31
+ const onChange = (e: MediaQueryListEvent) => (system = e.matches ? "dark" : "light");
32
+ mq.addEventListener("change", onChange);
33
+ return () => mq.removeEventListener("change", onChange);
34
+ });
35
+
36
+ // Apply (and persist) whenever the theme changes.
37
+ $effect(() => {
38
+ applyTheme(theme);
39
+ });
40
+
41
+ // Flip to the explicit opposite of whatever is currently in effect.
42
+ function toggle() {
43
+ theme = effective === "dark" ? "light" : "dark";
44
+ }
45
+ </script>
46
+
47
+ <Button
48
+ {size}
49
+ {variant}
50
+ onclick={toggle}
51
+ aria-pressed={effective === "dark"}
52
+ title={effective === "dark" ? "Switch to light theme" : "Switch to dark theme"}
53
+ >
54
+ {#snippet icon()}
55
+ {#if effective === "dark"}<IconMoon />{:else}<IconSun />{/if}
56
+ {/snippet}
57
+ </Button>
@@ -0,0 +1,10 @@
1
+ import type { Size } from "../types/sizes.js";
2
+ import type { Variant } from "../types/variants.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ type $$ComponentProps = {
5
+ size?: Responsive<Size>;
6
+ variant?: Variant;
7
+ };
8
+ declare const ThemeToggle: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type ThemeToggle = ReturnType<typeof ThemeToggle>;
10
+ export default ThemeToggle;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared theme state — single source for the persisted theme, used by `ThemeSelector` and
3
+ * `ThemeToggle` so the localStorage key, the `<html>` class application, and system detection can't
4
+ * drift between them.
5
+ */
6
+ export declare const THEME_STORAGE_KEY = "svelte-ui-theme";
7
+ export type ThemeValue = "auto" | "light" | "dark";
8
+ /** The saved theme, or `"auto"` when nothing valid is stored (or on the server). */
9
+ export declare function readStoredTheme(): ThemeValue;
10
+ /** The OS/browser preference, used to resolve `"auto"` to a concrete light/dark. */
11
+ export declare function systemTheme(): "light" | "dark";
12
+ /** The concrete theme in effect — `"auto"` resolved against the system preference. */
13
+ export declare function resolveTheme(value: ThemeValue): "light" | "dark";
14
+ /**
15
+ * Apply a theme to `<html>` (documentElement) — NOT `<body>` — and persist it.
16
+ * The tokens (incl. --ui-color-hover/active, which color-mix from other tokens) are defined at
17
+ * `:root`; app.html's pre-hydration script forces the class onto `<html>`. Applying it to `<body>`
18
+ * instead would leave `<html>` on the system `@media(prefers-color-scheme)` path, so token-references
19
+ * would resolve against the WRONG theme at `:root` and inherit down stale (e.g. a white hover-tint on
20
+ * a light page → invisible hover). Keep the class on `:root`. `"auto"` clears both classes so the
21
+ * `@media` path takes over.
22
+ */
23
+ export declare function applyTheme(value: ThemeValue): void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared theme state — single source for the persisted theme, used by `ThemeSelector` and
3
+ * `ThemeToggle` so the localStorage key, the `<html>` class application, and system detection can't
4
+ * drift between them.
5
+ */
6
+ export const THEME_STORAGE_KEY = "svelte-ui-theme";
7
+ const THEME_CLASSES = {
8
+ light: "theme-light",
9
+ dark: "theme-dark",
10
+ };
11
+ /** The saved theme, or `"auto"` when nothing valid is stored (or on the server). */
12
+ export function readStoredTheme() {
13
+ if (typeof window === "undefined")
14
+ return "auto";
15
+ const v = localStorage.getItem(THEME_STORAGE_KEY);
16
+ return v === "light" || v === "dark" || v === "auto" ? v : "auto";
17
+ }
18
+ /** The OS/browser preference, used to resolve `"auto"` to a concrete light/dark. */
19
+ export function systemTheme() {
20
+ if (typeof window === "undefined")
21
+ return "light";
22
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
23
+ }
24
+ /** The concrete theme in effect — `"auto"` resolved against the system preference. */
25
+ export function resolveTheme(value) {
26
+ return value === "auto" ? systemTheme() : value;
27
+ }
28
+ /**
29
+ * Apply a theme to `<html>` (documentElement) — NOT `<body>` — and persist it.
30
+ * The tokens (incl. --ui-color-hover/active, which color-mix from other tokens) are defined at
31
+ * `:root`; app.html's pre-hydration script forces the class onto `<html>`. Applying it to `<body>`
32
+ * instead would leave `<html>` on the system `@media(prefers-color-scheme)` path, so token-references
33
+ * would resolve against the WRONG theme at `:root` and inherit down stale (e.g. a white hover-tint on
34
+ * a light page → invisible hover). Keep the class on `:root`. `"auto"` clears both classes so the
35
+ * `@media` path takes over.
36
+ */
37
+ export function applyTheme(value) {
38
+ if (typeof document === "undefined")
39
+ return;
40
+ const { body, documentElement: html } = document;
41
+ Object.values(THEME_CLASSES).forEach((className) => {
42
+ body.classList.remove(className);
43
+ html.classList.remove(className);
44
+ });
45
+ if (value === "light" || value === "dark")
46
+ html.classList.add(THEME_CLASSES[value]);
47
+ if (typeof window !== "undefined")
48
+ localStorage.setItem(THEME_STORAGE_KEY, value);
49
+ }
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export { default as TimeSpinner } from "./components/TimeSpinner.svelte";
28
28
  export { default as TimePicker } from "./components/TimePicker.svelte";
29
29
  export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
30
30
  export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
31
+ export { default as ThemeToggle } from "./components/ThemeToggle.svelte";
31
32
  export { default as Popup } from "./components/Popup.svelte";
32
33
  export { default as Menu } from "./components/Menu.svelte";
33
34
  export { default as Checkbox } from "./components/Checkbox.svelte";
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ export { default as TimePicker } from "./components/TimePicker.svelte";
32
32
  export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
33
33
  // Theme Components
34
34
  export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
35
+ export { default as ThemeToggle } from "./components/ThemeToggle.svelte";
35
36
  // Utility Components
36
37
  export { default as Popup } from "./components/Popup.svelte";
37
38
  export { default as Menu } from "./components/Menu.svelte";
@@ -231,9 +231,20 @@ Status indicator / label.
231
231
  Theme switcher (Auto / Light / Dark), built on `Select`.
232
232
 
233
233
  - **Location**: `src/lib/components/ThemeSelector.svelte`
234
- - **Features**: applies `.theme-light` / `.theme-dark` to `<html>`/`<body>`, `auto` follows the
234
+ - **Axes**: `size`, `variant` (forwarded to the Select)
235
+ - **Features**: applies `.theme-light` / `.theme-dark` to `<html>`, `auto` follows the
235
236
  system preference, persists to `localStorage` (`svelte-ui-theme`)
236
237
 
238
+ ### ThemeToggle
239
+
240
+ Compact icon-button sibling of `ThemeSelector` — flips light ⇄ dark (☀ / ☾), built on `Button`.
241
+
242
+ - **Location**: `src/lib/components/ThemeToggle.svelte`
243
+ - **Axes**: `size`, `variant` (default `ghost`)
244
+ - **Features**: shares `ThemeSelector`'s state (same `localStorage` key + `<html>` application, via
245
+ `src/lib/components/theme.ts`), follows the system preference until first toggled, icon reflects the
246
+ effective theme
247
+
237
248
  ## Import Pattern
238
249
 
239
250
  ```typescript
package/docs/usage.md CHANGED
@@ -454,12 +454,16 @@ See the live demo for the full DataTable + Query walkthrough — http://localhos
454
454
  </style>
455
455
  ```
456
456
 
457
- ### Theme Selector
457
+ ### Theme Selector / Toggle
458
458
 
459
459
  ```svelte
460
460
  <script>
461
- import { ThemeSelector } from "@spaethtech/svelte-ui";
461
+ import { ThemeSelector, ThemeToggle } from "@spaethtech/svelte-ui";
462
462
  </script>
463
463
 
464
+ <!-- Full Auto/Light/Dark dropdown -->
464
465
  <ThemeSelector />
466
+
467
+ <!-- Compact ☀/☾ icon toggle (light ⇄ dark) — same persisted state, follows system until toggled -->
468
+ <ThemeToggle />
465
469
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.4.1-dev.14.d54aa31",
3
+ "version": "0.4.1-dev.17.75f0491",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"