@joewinke/jatui 0.29.0 → 0.30.0

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/CLAUDE.md CHANGED
@@ -310,6 +310,7 @@ const callbacks: AnnotationCallbacks = {
310
310
  | Component | Purpose | Key Props |
311
311
  |-----------|---------|-----------|
312
312
  | `ThemeSelector` | DaisyUI theme switcher with preview dots | `themes`, `defaultTheme`, `dropUp`, `storageKey`, `onThemeChange` |
313
+ | `ThemeToggle` | Icon-only sun<->moon light/dark toggle with an animated morph (transform+opacity on motion tokens; instant under reduced-motion; spring flattens on `data-motion="calm"`). A pure control — flips the bindable `theme` and fires `onchange`; storage stays with the consumer. | `theme` (bindable), `lightTheme`, `darkTheme`, `onchange`, `size` |
313
314
 
314
315
  ### Keyboard Navigation
315
316
 
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ThemeToggle — icon-only light/dark switch with an animated sun<->moon morph
4
+ * (mos-p6o2d). The public-site counterpart to ThemeSelector: where a site
5
+ * offers exactly two themes, a dropdown with visible "Dark mode" text is
6
+ * chrome noise — this is a single icon button instead.
7
+ *
8
+ * The component is a CONTROL, not a theme manager: it flips the bindable
9
+ * `theme` between `lightTheme` and `darkTheme` and fires `onchange`. It does
10
+ * not touch localStorage, `document.documentElement` or any storage key —
11
+ * persistence, pre-paint system-preference behaviour and DB theme overrides
12
+ * stay with the consumer (see JST's PublicThemeToggle wrapper, which owns
13
+ * the exact storage contract ThemeSelector established).
14
+ *
15
+ * Motion: the morph uses transform + opacity only, driven by the jatui
16
+ * motion tokens (`--motion-fast` duration, spring for the settle, glide for
17
+ * the cross-fade). Honours `data-motion="calm"` (tokens re-declared there —
18
+ * springs flatten) and `prefers-reduced-motion` (instant swap, both via the
19
+ * motion.css global floor and a component-local guard, since not every
20
+ * consumer loads motion.css). Colours are token-only — no literals.
21
+ */
22
+ let {
23
+ theme = $bindable('light'),
24
+ lightTheme = 'light',
25
+ darkTheme = 'dark',
26
+ onchange,
27
+ size = 'md'
28
+ }: {
29
+ /** Current theme name (bindable). Anything that is not `darkTheme` reads as light. */
30
+ theme?: string;
31
+ /** Theme name considered "light" (sun shown; the target when dark). */
32
+ lightTheme?: string;
33
+ /** Theme name considered "dark" (moon shown; the target when light). */
34
+ darkTheme?: string;
35
+ /** Fired after the toggle flips the bound theme. */
36
+ onchange?: (theme: string) => void;
37
+ /** Hit-target size — sm 40px, md 44px, lg 48px. Never below the 40px touch floor. */
38
+ size?: 'sm' | 'md' | 'lg';
39
+ } = $props();
40
+
41
+ let isDark = $derived(theme === darkTheme);
42
+
43
+ const BUTTON_SIZES = {
44
+ sm: 'size-10',
45
+ md: 'size-11',
46
+ lg: 'size-12'
47
+ } as const;
48
+
49
+ const ICON_SIZES = { sm: 18, md: 20, lg: 24 } as const;
50
+
51
+ const label = $derived(isDark ? 'Switch to light mode' : 'Switch to dark mode');
52
+
53
+ function toggle() {
54
+ const next = isDark ? lightTheme : darkTheme;
55
+ theme = next;
56
+ onchange?.(next);
57
+ }
58
+ </script>
59
+
60
+ <button
61
+ type="button"
62
+ class="jatui-theme-toggle inline-flex shrink-0 items-center justify-center rounded-full
63
+ text-base-content transition-colors duration-[var(--motion-instant,80ms)]
64
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
65
+ hover:bg-base-content/10 focus-visible:bg-base-content/10
66
+ active:bg-base-content/15 disabled:pointer-events-none disabled:opacity-50
67
+ {BUTTON_SIZES[size]}"
68
+ class:is-dark={isDark}
69
+ aria-label={label}
70
+ aria-pressed={isDark}
71
+ title={label}
72
+ onclick={toggle}
73
+ >
74
+ <span
75
+ class="glyphs relative block"
76
+ style="width: {ICON_SIZES[size]}px; height: {ICON_SIZES[size]}px"
77
+ aria-hidden="true"
78
+ >
79
+ <!-- Sun: ring + eight rays, lucide geometry. -->
80
+ <svg
81
+ xmlns="http://www.w3.org/2000/svg"
82
+ viewBox="0 0 24 24"
83
+ fill="none"
84
+ stroke="currentColor"
85
+ stroke-width="2"
86
+ stroke-linecap="round"
87
+ stroke-linejoin="round"
88
+ class="glyph glyph-sun absolute inset-0"
89
+ width={ICON_SIZES[size]}
90
+ height={ICON_SIZES[size]}
91
+ >
92
+ <circle cx="12" cy="12" r="4" />
93
+ <path d="M12 2v2" />
94
+ <path d="M12 20v2" />
95
+ <path d="m4.93 4.93 1.41 1.41" />
96
+ <path d="m17.66 17.66 1.41 1.41" />
97
+ <path d="M2 12h2" />
98
+ <path d="M20 12h2" />
99
+ <path d="m6.34 17.66-1.41 1.41" />
100
+ <path d="m19.07 4.93-1.41 1.41" />
101
+ </svg>
102
+ <!-- Moon: crescent, lucide geometry. -->
103
+ <svg
104
+ xmlns="http://www.w3.org/2000/svg"
105
+ viewBox="0 0 24 24"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ stroke-width="2"
109
+ stroke-linecap="round"
110
+ stroke-linejoin="round"
111
+ class="glyph glyph-moon absolute inset-0"
112
+ width={ICON_SIZES[size]}
113
+ height={ICON_SIZES[size]}
114
+ >
115
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
116
+ </svg>
117
+ </span>
118
+ </button>
119
+
120
+ <style>
121
+ /*
122
+ * Morph: transform + opacity ONLY (compositor-friendly, no layout thrash).
123
+ * The outgoing glyph rotates away and shrinks while fading; the incoming
124
+ * one rotates in. Spring carries the small-element settle; glide the fade.
125
+ */
126
+ .glyph {
127
+ transition:
128
+ transform var(--motion-fast, 150ms) var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1)),
129
+ opacity var(--motion-fast, 150ms) var(--motion-glide, cubic-bezier(0.16, 1, 0.3, 1));
130
+ }
131
+
132
+ .glyph-sun {
133
+ opacity: 1;
134
+ transform: rotate(0deg) scale(1);
135
+ }
136
+
137
+ .glyph-moon {
138
+ opacity: 0;
139
+ transform: rotate(-90deg) scale(0.5);
140
+ }
141
+
142
+ .is-dark .glyph-sun {
143
+ opacity: 0;
144
+ transform: rotate(90deg) scale(0.5);
145
+ }
146
+
147
+ .is-dark .glyph-moon {
148
+ opacity: 1;
149
+ transform: rotate(0deg) scale(1);
150
+ }
151
+
152
+ /* Component-local reduced-motion guard — not every consumer loads motion.css. */
153
+ @media (prefers-reduced-motion: reduce) {
154
+ .glyph {
155
+ transition: none;
156
+ }
157
+ }
158
+ </style>
@@ -0,0 +1,15 @@
1
+ type $$ComponentProps = {
2
+ /** Current theme name (bindable). Anything that is not `darkTheme` reads as light. */
3
+ theme?: string;
4
+ /** Theme name considered "light" (sun shown; the target when dark). */
5
+ lightTheme?: string;
6
+ /** Theme name considered "dark" (moon shown; the target when light). */
7
+ darkTheme?: string;
8
+ /** Fired after the toggle flips the bound theme. */
9
+ onchange?: (theme: string) => void;
10
+ /** Hit-target size — sm 40px, md 44px, lg 48px. Never below the 40px touch floor. */
11
+ size?: 'sm' | 'md' | 'lg';
12
+ };
13
+ declare const ThemeToggle: import("svelte").Component<$$ComponentProps, {}, "theme">;
14
+ type ThemeToggle = ReturnType<typeof ThemeToggle>;
15
+ export default ThemeToggle;
package/dist/index.d.ts CHANGED
@@ -93,6 +93,7 @@ export type { AudioRecordingResult } from './utils/audioRecorderCore';
93
93
  export { createAudioRecorder } from './utils/audioRecorder';
94
94
  export type { AudioRecorder, AudioRecorderCore, AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecorderSnapshot } from './utils/audioRecorderCore';
95
95
  export { default as ThemeSelector } from './components/ThemeSelector.svelte';
96
+ export { default as ThemeToggle } from './components/ThemeToggle.svelte';
96
97
  export { default as Sparkline } from './components/Sparkline.svelte';
97
98
  export { default as BurndownChart } from './components/BurndownChart.svelte';
98
99
  export { default as AvatarUpload } from './components/AvatarUpload.svelte';
package/dist/index.js CHANGED
@@ -127,6 +127,7 @@ export { default as AudioRecorder } from './components/AudioRecorder.svelte';
127
127
  export { createAudioRecorder } from './utils/audioRecorder';
128
128
  // Components — from Headcount
129
129
  export { default as ThemeSelector } from './components/ThemeSelector.svelte';
130
+ export { default as ThemeToggle } from './components/ThemeToggle.svelte';
130
131
  export { default as Sparkline } from './components/Sparkline.svelte';
131
132
  export { default as BurndownChart } from './components/BurndownChart.svelte';
132
133
  // Components — from JST
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",