@spaethtech/svelte-ui 0.21.1-dev.116.7683168 → 0.21.1-dev.118.553a240

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.
@@ -1,11 +1,22 @@
1
1
  <script lang="ts">
2
2
  /**
3
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.
4
+ *
5
+ * Two ways to use it:
6
+ * - **Self-managed (default)** — reads/writes `localStorage` (`svelte-ui-theme`), applies the
7
+ * `<html>` class, and toggles on click. JS-only; shares state with `ThemeSelector`.
8
+ * - **Form-owned (`type="submit"` / `"reset"`)** — attaches NO click handler and does NOT touch
9
+ * `localStorage` or `<html>`; `type`/`name`/`value` forward to the native `<button>` so a
10
+ * surrounding `<form>` owns the change. Works with JS off and before hydration.
11
+ *
12
+ * The icon is always chosen by **CSS** from the applied `<html>` theme class (`.theme-light` /
13
+ * `.theme-dark`, plus the system preference for "auto"), NOT from JS state — so it's correct in
14
+ * SSR, before hydration, and with JS off. The accessible name is a static `aria-label` (also
15
+ * correct in SSR); `aria-pressed` is a hydrated enhancement in self-managed mode.
7
16
  */
8
17
  import Button from "./Button.svelte";
18
+ import { uiI18n } from "../i18n.js";
19
+ const i18n = uiI18n();
9
20
  import type { Size } from "../types/sizes.js";
10
21
  import type { Variant } from "../types/variants.js";
11
22
  import type { Responsive } from "../types/responsive.js";
@@ -18,19 +29,28 @@
18
29
  let {
19
30
  size = "md",
20
31
  variant = "ghost",
32
+ type = "button",
21
33
  ...rest
22
- }: { size?: Responsive<Size>; variant?: Variant } & Omit<HTMLButtonAttributes, "class"> =
23
- $props();
34
+ }: {
35
+ size?: Responsive<Size>;
36
+ variant?: Variant;
37
+ } & Omit<HTMLButtonAttributes, "class"> = $props();
38
+
39
+ // Self-managed only when it's a plain button. A submit/reset button is form-owned: no localStorage,
40
+ // no <html> apply, no click toggle — the consumer's <form> handles the change (JS-off friendly).
41
+ const managed = $derived(type !== "submit" && type !== "reset");
24
42
 
25
43
  // The stored value ("auto" until the user picks) and the tracked system preference.
26
44
  let theme = $state<ThemeValue>("auto");
27
45
  let system = $state<"light" | "dark">("light");
28
46
 
29
- // The theme actually showing — "auto" resolved against the system preference.
47
+ // The theme actually showing — "auto" resolved against the system preference. Used only for
48
+ // `aria-pressed` (the visible icon is CSS-driven); meaningful only in self-managed mode.
30
49
  const effective = $derived(theme === "auto" ? system : theme);
31
50
 
32
- // Load stored theme + track the system preference (so the icon stays right in "auto" mode).
51
+ // Load stored theme + track the system preference — self-managed mode only.
33
52
  $effect(() => {
53
+ if (!managed) return;
34
54
  theme = readStoredTheme();
35
55
  system = resolveTheme("auto");
36
56
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
@@ -39,8 +59,9 @@
39
59
  return () => mq.removeEventListener("change", onChange);
40
60
  });
41
61
 
42
- // Apply (and persist) whenever the theme changes.
62
+ // Apply (and persist) whenever the theme changes — self-managed mode only.
43
63
  $effect(() => {
64
+ if (!managed) return;
44
65
  applyTheme(theme);
45
66
  });
46
67
 
@@ -50,17 +71,44 @@
50
71
  }
51
72
  </script>
52
73
 
53
- <!-- rest is fully typed on this component's own Props (below); the cast only sidesteps a TS
54
- "union too complex" limit when spreading a wide attrs type into Button's ButtonProps|AnchorProps. -->
74
+ <!-- rest is fully typed on this component's own Props (above); the cast only sidesteps a TS
75
+ "union too complex" limit when spreading a wide attrs type into Button's ButtonProps|AnchorProps.
76
+ In form-owned mode the consumer's own `onclick` (if any) is preserved; otherwise we toggle. -->
55
77
  <Button
56
- {...rest as Record<string, unknown>}
78
+ {...(managed ? { ...rest, onclick: toggle } : rest) as Record<string, unknown>}
57
79
  {size}
58
80
  {variant}
59
- onclick={toggle}
60
- aria-pressed={effective === "dark"}
61
- title={effective === "dark" ? "Switch to light theme" : "Switch to dark theme"}
81
+ {type}
82
+ aria-pressed={managed ? effective === "dark" : undefined}
83
+ aria-label={i18n.messages.toggleTheme}
84
+ title={i18n.messages.toggleTheme}
62
85
  >
63
86
  {#snippet icon()}
64
- {#if effective === "dark"}<IconMoon />{:else}<IconSun />{/if}
87
+ <span class="ui-theme-toggle-sun"><IconSun /></span><span class="ui-theme-toggle-moon"
88
+ ><IconMoon /></span
89
+ >
65
90
  {/snippet}
66
91
  </Button>
92
+
93
+ <style>
94
+ /* The visible icon tracks the APPLIED theme (the <html> class) + the system preference for "auto",
95
+ so it's right in SSR / before hydration / with JS off — never from JS state. Mirrors theme.css:
96
+ `.theme-light`/`.theme-dark` force a theme; no class → the prefers-color-scheme media query. */
97
+ .ui-theme-toggle-moon {
98
+ display: none;
99
+ }
100
+ :global(:root.theme-dark) .ui-theme-toggle-sun {
101
+ display: none;
102
+ }
103
+ :global(:root.theme-dark) .ui-theme-toggle-moon {
104
+ display: contents;
105
+ }
106
+ @media (prefers-color-scheme: dark) {
107
+ :global(:root:not(.theme-light)) .ui-theme-toggle-sun {
108
+ display: none;
109
+ }
110
+ :global(:root:not(.theme-light)) .ui-theme-toggle-moon {
111
+ display: contents;
112
+ }
113
+ }
114
+ </style>
package/dist/i18n.d.ts CHANGED
@@ -15,6 +15,8 @@ export type UiMessages = {
15
15
  copyToClipboard: Msg;
16
16
  done: Msg;
17
17
  noResults: Msg;
18
+ /** ThemeToggle accessible name (static, SSR-correct) — also its tooltip. */
19
+ toggleTheme: Msg;
18
20
  /** DataTable empty-state while the source is still arriving. */
19
21
  loading: Msg;
20
22
  /** DataTable empty-state when the fetch result carries a non-string error. */
package/dist/i18n.js CHANGED
@@ -16,6 +16,7 @@ export const defaultUiMessages = {
16
16
  copyToClipboard: "Copy to clipboard",
17
17
  done: "Done",
18
18
  noResults: "No results",
19
+ toggleTheme: "Toggle light/dark theme",
19
20
  loading: "Loading…",
20
21
  loadFailed: "Failed to load",
21
22
  previous: "Previous",
@@ -685,9 +685,27 @@ Compact icon-button sibling of `ThemeSelector` — flips light ⇄ dark (☀ /
685
685
 
686
686
  - **Location**: `src/lib/components/ThemeToggle.svelte`
687
687
  - **Axes**: `size`, `variant` (default `ghost`)
688
- - **Features**: shares `ThemeSelector`'s state (same `localStorage` key + `<html>` application, via
689
- `src/lib/components/theme.ts`), follows the system preference until first toggled, icon reflects the
690
- effective theme
688
+ - **Self-managed (default)**: shares `ThemeSelector`'s state (same `localStorage` key + `<html>`
689
+ application, via `src/lib/components/theme.ts`), follows the system preference until first toggled,
690
+ and toggles on click.
691
+ - **Form-owned mode (`type="submit"` / `"reset"`)**: attaches **no** click handler and does **not**
692
+ touch `localStorage` or `<html>`; `type`/`name`/`value` forward to the native `<button>` so a
693
+ surrounding `<form>` owns the change. Works with **JS off** and before hydration (a consumer's own
694
+ `onclick`, if passed, is preserved).
695
+ - **SSR-correct icon**: the visible icon is chosen by **CSS** from the applied `<html>` theme class
696
+ (`.theme-light`/`.theme-dark`, plus `prefers-color-scheme` for "auto") — not from JS state — so
697
+ light/dark/system-dark users get the right icon on first paint, before hydration, and with JS off.
698
+ - **Accessible name**: a static `aria-label` (localized `toggleTheme`, "Toggle light/dark theme") that
699
+ is correct in SSR; `aria-pressed` is a hydrated enhancement in self-managed mode. (Note: `Button`'s
700
+ `title` drives the client **tooltip** action, not an SSR attribute — use `aria-label` for the name,
701
+ as this does.)
702
+
703
+ ```svelte
704
+ <!-- Form-owned (works with JS off): a preference <form> posts the change, ThemeToggle is just the button -->
705
+ <form method="POST" action="/preference"><ThemeToggle type="submit" name="theme" /></form>
706
+ <!-- Self-managed (JS): localStorage + <html>, toggles on click -->
707
+ <ThemeToggle />
708
+ ```
691
709
 
692
710
  ## Import Pattern
693
711
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.21.1-dev.116.7683168",
3
+ "version": "0.21.1-dev.118.553a240",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"