@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 +1 -0
- package/dist/components/ThemeToggle.svelte +158 -0
- package/dist/components/ThemeToggle.svelte.d.ts +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
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
|