@spaethtech/svelte-ui 0.21.1-dev.115.1af903e → 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/DataTable.svelte +4 -1
- package/dist/components/ThemeToggle.svelte +63 -15
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +1 -0
- package/docs/components.md +25 -4
- package/package.json +1 -1
|
@@ -347,7 +347,10 @@
|
|
|
347
347
|
style="border-color: {bodyBg}; border-left-color: {bodyBorder}; border-right-color: {bodyBorder}; background-color: {bodyBg}"
|
|
348
348
|
>
|
|
349
349
|
{#if rows.length === 0}
|
|
350
|
-
|
|
350
|
+
<!-- Marked role="row" (like data rows) so any consumer per-row styling — e.g. a
|
|
351
|
+
`[role=row]` top/bottom divider border — lands on the empty-state row too, keeping it the
|
|
352
|
+
same height as a data row by construction (svelte-ui draws no row border itself). -->
|
|
353
|
+
<div role="row" class="flex items-center {cellGap} {cellPad}" style="min-height: {heightVar}">
|
|
351
354
|
{#if hasLead}
|
|
352
355
|
<div class="invisible flex shrink-0 items-center gap-1" aria-hidden="true">
|
|
353
356
|
{#if selectable}<Checkbox {size} tabindex={-1} />{/if}
|
|
@@ -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
|
@@ -440,7 +440,10 @@ Config-driven, responsive 12-column data table over a `DataGrid`.
|
|
|
440
440
|
errored result never spins, and the row is **exactly one data row tall** in every state (its
|
|
441
441
|
invisible lead/tail spacers carry the control height), so the table never jumps between Loading,
|
|
442
442
|
rows, No results and error — and you don't need a separate page banner that contradicts a "No
|
|
443
|
-
results" table. (A long error wraps and grows the row; one line is the minimum, not a fixed height.)
|
|
443
|
+
results" table. (A long error wraps and grows the row; one line is the minimum, not a fixed height.)
|
|
444
|
+
The empty-state row is marked **`role="row"`** like data rows, so if you add your own per-row styling
|
|
445
|
+
(e.g. a `[role="row"]` divider border), it lands on the empty-state row too and heights stay matched
|
|
446
|
+
— svelte-ui itself draws no per-row border. With `localFetch`, drive this by
|
|
444
447
|
passing `loading`/`error` getters (below); a remote `query()` that exposes `.error` gets it for free.
|
|
445
448
|
(`DataGrid.error` is the query-*parse* error surfaced by `<Query>` — a bad filter — which is distinct
|
|
446
449
|
from this fetch/load error.)
|
|
@@ -682,9 +685,27 @@ Compact icon-button sibling of `ThemeSelector` — flips light ⇄ dark (☀ /
|
|
|
682
685
|
|
|
683
686
|
- **Location**: `src/lib/components/ThemeToggle.svelte`
|
|
684
687
|
- **Axes**: `size`, `variant` (default `ghost`)
|
|
685
|
-
- **
|
|
686
|
-
`src/lib/components/theme.ts`), follows the system preference until first toggled,
|
|
687
|
-
|
|
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
|
+
```
|
|
688
709
|
|
|
689
710
|
## Import Pattern
|
|
690
711
|
|