@spaethtech/svelte-ui 0.21.1-dev.116.7683168 → 0.21.1-dev.117.092ebb0
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/ThemeToggle.svelte +63 -15
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +1 -0
- package/docs/components.md +21 -3
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
}: {
|
|
23
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
60
|
-
aria-pressed={effective === "dark"}
|
|
61
|
-
|
|
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
|
-
|
|
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
package/docs/components.md
CHANGED
|
@@ -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
|
-
- **
|
|
689
|
-
`src/lib/components/theme.ts`), follows the system preference until first toggled,
|
|
690
|
-
|
|
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
|
|