@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.
@@ -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
- <div class="flex items-center {cellGap} {cellPad}" style="min-height: {heightVar}">
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
- * 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",
@@ -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.) With `localFetch`, drive this by
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
- - **Features**: shares `ThemeSelector`'s state (same `localStorage` key + `<html>` application, via
686
- `src/lib/components/theme.ts`), follows the system preference until first toggled, icon reflects the
687
- 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
+ ```
688
709
 
689
710
  ## Import Pattern
690
711
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.21.1-dev.115.1af903e",
3
+ "version": "0.21.1-dev.117.092ebb0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"