@a4ui/core 0.30.0 → 0.31.1
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/elements.css +3 -0
- package/dist/full.css +3 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3090 -2890
- package/dist/ui/MorphPresets.d.ts +126 -0
- package/package.json +1 -1
- package/src/index.ts +13 -1
- package/src/ui/CardSpread.tsx +25 -17
- package/src/ui/Carousel3D.tsx +28 -6
- package/src/ui/MorphPresets.tsx +218 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
import { IconMorphButtonVariant } from './IconMorphButton';
|
|
3
|
+
export interface CopyButtonProps {
|
|
4
|
+
/** Text copied to the clipboard when the button is pressed. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Optional label shown next to the icon before copying. */
|
|
7
|
+
label?: JSX.Element;
|
|
8
|
+
/** Optional label shown next to the icon once copied. */
|
|
9
|
+
copiedLabel?: JSX.Element;
|
|
10
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
11
|
+
variant?: IconMorphButtonVariant;
|
|
12
|
+
class?: string;
|
|
13
|
+
'aria-label'?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Copy-to-clipboard button that morphs Copy → Check and auto-reverts after
|
|
17
|
+
* 1.5s.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <CopyButton value="npm install @a4ui/core" aria-label="Copy install command" />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function CopyButton(props: CopyButtonProps): JSX.Element;
|
|
25
|
+
export interface PlayPauseButtonProps {
|
|
26
|
+
/** Controlled playing state. Omit to let the button manage its own state. */
|
|
27
|
+
pressed?: boolean;
|
|
28
|
+
/** Initial playing state when uncontrolled. Defaults to `false`. */
|
|
29
|
+
defaultPressed?: boolean;
|
|
30
|
+
/** Fired with the next playing state, controlled or uncontrolled. */
|
|
31
|
+
onChange?: (pressed: boolean) => void;
|
|
32
|
+
/** Optional label shown next to the icon while paused. */
|
|
33
|
+
label?: JSX.Element;
|
|
34
|
+
/** Optional label shown next to the icon while playing. */
|
|
35
|
+
activeLabel?: JSX.Element;
|
|
36
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
37
|
+
variant?: IconMorphButtonVariant;
|
|
38
|
+
class?: string;
|
|
39
|
+
'aria-label'?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Play/pause toggle button whose icon morphs Play → Pause.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <PlayPauseButton defaultPressed={false} aria-label="Play" />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function PlayPauseButton(props: PlayPauseButtonProps): JSX.Element;
|
|
50
|
+
export interface MuteButtonProps {
|
|
51
|
+
/** Controlled muted state. Omit to let the button manage its own state. */
|
|
52
|
+
pressed?: boolean;
|
|
53
|
+
/** Initial muted state when uncontrolled. Defaults to `false`. */
|
|
54
|
+
defaultPressed?: boolean;
|
|
55
|
+
/** Fired with the next muted state, controlled or uncontrolled. */
|
|
56
|
+
onChange?: (pressed: boolean) => void;
|
|
57
|
+
/** Optional label shown next to the icon while unmuted. */
|
|
58
|
+
label?: JSX.Element;
|
|
59
|
+
/** Optional label shown next to the icon while muted. */
|
|
60
|
+
activeLabel?: JSX.Element;
|
|
61
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
62
|
+
variant?: IconMorphButtonVariant;
|
|
63
|
+
class?: string;
|
|
64
|
+
'aria-label'?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Mute toggle button whose icon morphs Volume2 → VolumeX.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <MuteButton defaultPressed={false} aria-label="Mute" />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function MuteButton(props: MuteButtonProps): JSX.Element;
|
|
75
|
+
export interface LockButtonProps {
|
|
76
|
+
/** Controlled locked state. Omit to let the button manage its own state. */
|
|
77
|
+
pressed?: boolean;
|
|
78
|
+
/** Initial locked state when uncontrolled. Defaults to `false`. */
|
|
79
|
+
defaultPressed?: boolean;
|
|
80
|
+
/** Fired with the next locked state, controlled or uncontrolled. */
|
|
81
|
+
onChange?: (pressed: boolean) => void;
|
|
82
|
+
/** Optional label shown next to the icon while unlocked. */
|
|
83
|
+
label?: JSX.Element;
|
|
84
|
+
/** Optional label shown next to the icon while locked. */
|
|
85
|
+
activeLabel?: JSX.Element;
|
|
86
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
87
|
+
variant?: IconMorphButtonVariant;
|
|
88
|
+
class?: string;
|
|
89
|
+
'aria-label'?: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Lock toggle button whose icon morphs Unlock → Lock.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```tsx
|
|
96
|
+
* <LockButton defaultPressed={false} aria-label="Lock" />
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function LockButton(props: LockButtonProps): JSX.Element;
|
|
100
|
+
export interface ThemeMorphButtonProps {
|
|
101
|
+
/** Controlled dark-mode state. Omit to let the button manage its own state. */
|
|
102
|
+
pressed?: boolean;
|
|
103
|
+
/** Initial dark-mode state when uncontrolled. Defaults to `false`. */
|
|
104
|
+
defaultPressed?: boolean;
|
|
105
|
+
/** Fired with the next dark-mode state, controlled or uncontrolled. */
|
|
106
|
+
onChange?: (pressed: boolean) => void;
|
|
107
|
+
/** Optional label shown next to the icon in light mode. */
|
|
108
|
+
label?: JSX.Element;
|
|
109
|
+
/** Optional label shown next to the icon in dark mode. */
|
|
110
|
+
activeLabel?: JSX.Element;
|
|
111
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
112
|
+
variant?: IconMorphButtonVariant;
|
|
113
|
+
class?: string;
|
|
114
|
+
'aria-label'?: string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Purely presentational theme toggle whose icon morphs Sun → Moon. Does not
|
|
118
|
+
* touch the theme system itself — wire `pressed`/`onChange` to your theme
|
|
119
|
+
* state.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* <ThemeMorphButton pressed={isDark()} onChange={setIsDark} aria-label="Toggle theme" />
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare function ThemeMorphButton(props: ThemeMorphButtonProps): JSX.Element;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import '@a4ui/core/styles.css'
|
|
9
9
|
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
10
|
|
|
11
|
-
export const A4UI_VERSION = '0.
|
|
11
|
+
export const A4UI_VERSION = '0.31.1'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -179,6 +179,18 @@ export { FocusBlurGroup, type FocusBlurGroupProps } from './ui/FocusBlurGroup'
|
|
|
179
179
|
export { CardSpread, type CardSpreadProps, type CardSpreadLayout } from './ui/CardSpread'
|
|
180
180
|
export { Carousel3D, type Carousel3DProps, type Carousel3DVariant } from './ui/Carousel3D'
|
|
181
181
|
export { TimeMachineStack, type TimeMachineStackProps } from './ui/TimeMachineStack'
|
|
182
|
+
export {
|
|
183
|
+
CopyButton,
|
|
184
|
+
type CopyButtonProps,
|
|
185
|
+
PlayPauseButton,
|
|
186
|
+
type PlayPauseButtonProps,
|
|
187
|
+
MuteButton,
|
|
188
|
+
type MuteButtonProps,
|
|
189
|
+
LockButton,
|
|
190
|
+
type LockButtonProps,
|
|
191
|
+
ThemeMorphButton,
|
|
192
|
+
type ThemeMorphButtonProps,
|
|
193
|
+
} from './ui/MorphPresets'
|
|
182
194
|
|
|
183
195
|
// Phase 2 — code, search, navigation, master-detail.
|
|
184
196
|
export { CodeTabs, type CodeTabsProps, type CodeTab } from './ui/CodeTabs'
|
package/src/ui/CardSpread.tsx
CHANGED
|
@@ -48,10 +48,12 @@ function pseudoRandom(i: number): number {
|
|
|
48
48
|
return (s - Math.floor(s)) * 2 - 1
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function restTransform(i: number): CardTransform {
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
51
|
+
function restTransform(i: number, n: number): CardTransform {
|
|
52
|
+
// Resting deck: a gentle symmetric peek-fan (cards' edges show) so the pile
|
|
53
|
+
// reads as multiple cards even before it opens, without stealing the reveal.
|
|
54
|
+
const mid = (n - 1) / 2
|
|
55
|
+
const d = i - mid
|
|
56
|
+
return { x: d * 7, y: Math.abs(d) * 2.5, rotate: d * 2.5, origin: 'bottom center' }
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
function openTransform(layout: CardSpreadLayout, i: number, n: number, spread: number): CardTransform {
|
|
@@ -131,7 +133,7 @@ export function CardSpread(props: CardSpreadProps): JSX.Element {
|
|
|
131
133
|
|
|
132
134
|
cardRefs.forEach((card, i) => {
|
|
133
135
|
if (!card) return
|
|
134
|
-
const t = open ? openTransform(layout(), i, n, spread()) : restTransform(i)
|
|
136
|
+
const t = open ? openTransform(layout(), i, n, spread()) : restTransform(i, n)
|
|
135
137
|
card.style.transformOrigin = t.origin
|
|
136
138
|
card.style.zIndex = String(open ? i : n - i)
|
|
137
139
|
|
|
@@ -174,21 +176,27 @@ export function CardSpread(props: CardSpreadProps): JSX.Element {
|
|
|
174
176
|
<div
|
|
175
177
|
role="group"
|
|
176
178
|
aria-label={props['aria-label']}
|
|
177
|
-
class={cn('relative
|
|
179
|
+
class={cn('relative mx-auto h-96 w-full max-w-lg', props.class)}
|
|
178
180
|
onPointerEnter={handleEnter}
|
|
179
181
|
onPointerLeave={handleLeave}
|
|
180
182
|
>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
{/* The cards fan around this fixed, centered anchor while the wider root
|
|
184
|
+
above stays the (stable) hover zone: a fanned spread reaches far past
|
|
185
|
+
a single card's box, so anchoring the hit-area to one card made the
|
|
186
|
+
cards leave it and flicker enter/leave. */}
|
|
187
|
+
<div class="absolute left-1/2 top-1/2 h-56 w-40 -translate-x-1/2 -translate-y-1/2">
|
|
188
|
+
<For each={props.items}>
|
|
189
|
+
{(item, i) => (
|
|
190
|
+
<div
|
|
191
|
+
ref={(el) => (cardRefs[i()] = el)}
|
|
192
|
+
class="absolute inset-0 rounded-xl border border-border bg-card text-card-foreground shadow-sm will-change-transform"
|
|
193
|
+
style={{ transform: transformString(restTransform(i(), props.items.length)) }}
|
|
194
|
+
>
|
|
195
|
+
{item}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</For>
|
|
199
|
+
</div>
|
|
192
200
|
</div>
|
|
193
201
|
)
|
|
194
202
|
}
|
package/src/ui/Carousel3D.tsx
CHANGED
|
@@ -201,19 +201,31 @@ export function Carousel3D(props: Carousel3DProps): JSX.Element {
|
|
|
201
201
|
|
|
202
202
|
// --- pointer drag: horizontal movement past a threshold advances one slide
|
|
203
203
|
// per threshold crossed, so a long drag can page through several slides. --
|
|
204
|
-
|
|
204
|
+
// Capture is DEFERRED until the pointer actually moves past a small
|
|
205
|
+
// threshold. Capturing on pointerdown (the old behaviour) retargeted the
|
|
206
|
+
// pointer to the stage and swallowed the `click` on the chevron buttons and
|
|
207
|
+
// any interactive slide content. A plain click no longer starts a drag, so
|
|
208
|
+
// those clicks work again.
|
|
205
209
|
let startX = 0
|
|
210
|
+
let pointerId: number | null = null
|
|
211
|
+
let captured = false
|
|
212
|
+
const CAPTURE_THRESHOLD = 8
|
|
206
213
|
const DRAG_THRESHOLD = 60
|
|
207
214
|
|
|
208
215
|
const onPointerDown = (e: PointerEvent): void => {
|
|
209
216
|
if (count() < 2) return
|
|
210
217
|
startX = e.clientX
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
pointerId = e.pointerId
|
|
219
|
+
captured = false
|
|
213
220
|
}
|
|
214
221
|
const onPointerMove = (e: PointerEvent): void => {
|
|
215
|
-
if (
|
|
222
|
+
if (pointerId === null) return
|
|
216
223
|
const dx = e.clientX - startX
|
|
224
|
+
if (!captured) {
|
|
225
|
+
if (Math.abs(dx) < CAPTURE_THRESHOLD) return
|
|
226
|
+
captured = true
|
|
227
|
+
;(e.currentTarget as HTMLElement).setPointerCapture(pointerId)
|
|
228
|
+
}
|
|
217
229
|
if (dx <= -DRAG_THRESHOLD) {
|
|
218
230
|
next()
|
|
219
231
|
startX = e.clientX
|
|
@@ -222,8 +234,16 @@ export function Carousel3D(props: Carousel3DProps): JSX.Element {
|
|
|
222
234
|
startX = e.clientX
|
|
223
235
|
}
|
|
224
236
|
}
|
|
225
|
-
const endDrag = (): void => {
|
|
226
|
-
|
|
237
|
+
const endDrag = (e: PointerEvent): void => {
|
|
238
|
+
if (pointerId !== null && captured) {
|
|
239
|
+
try {
|
|
240
|
+
;(e.currentTarget as HTMLElement).releasePointerCapture(pointerId)
|
|
241
|
+
} catch {
|
|
242
|
+
/* already released */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
pointerId = null
|
|
246
|
+
captured = false
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
return (
|
|
@@ -266,6 +286,7 @@ export function Carousel3D(props: Carousel3DProps): JSX.Element {
|
|
|
266
286
|
type="button"
|
|
267
287
|
aria-label="Previous slide"
|
|
268
288
|
disabled={count() === 0 || active() === 0}
|
|
289
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
269
290
|
onClick={prev}
|
|
270
291
|
class="absolute left-2 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-primary-foreground shadow transition-opacity hover:opacity-90 disabled:pointer-events-none disabled:opacity-40"
|
|
271
292
|
>
|
|
@@ -275,6 +296,7 @@ export function Carousel3D(props: Carousel3DProps): JSX.Element {
|
|
|
275
296
|
type="button"
|
|
276
297
|
aria-label="Next slide"
|
|
277
298
|
disabled={count() === 0 || active() === count() - 1}
|
|
299
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
278
300
|
onClick={next}
|
|
279
301
|
class="absolute right-2 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-primary text-primary-foreground shadow transition-opacity hover:opacity-90 disabled:pointer-events-none disabled:opacity-40"
|
|
280
302
|
>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// MorphPresets — thin, opinionated wrappers over IconMorphButton for common
|
|
2
|
+
// two-state icon toggles (copy, play/pause, mute, lock, theme). Each preset
|
|
3
|
+
// only supplies icons + sensible defaults; the morph animation itself lives
|
|
4
|
+
// in IconMorphButton and is not reimplemented here.
|
|
5
|
+
import type { JSX } from 'solid-js'
|
|
6
|
+
import { Check, Copy, Lock, Moon, Pause, Play, Sun, Unlock, Volume2, VolumeX } from 'lucide-solid'
|
|
7
|
+
|
|
8
|
+
import { IconMorphButton, type IconMorphButtonVariant } from './IconMorphButton'
|
|
9
|
+
|
|
10
|
+
export interface CopyButtonProps {
|
|
11
|
+
/** Text copied to the clipboard when the button is pressed. */
|
|
12
|
+
value: string
|
|
13
|
+
/** Optional label shown next to the icon before copying. */
|
|
14
|
+
label?: JSX.Element
|
|
15
|
+
/** Optional label shown next to the icon once copied. */
|
|
16
|
+
copiedLabel?: JSX.Element
|
|
17
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
18
|
+
variant?: IconMorphButtonVariant
|
|
19
|
+
class?: string
|
|
20
|
+
'aria-label'?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Copy-to-clipboard button that morphs Copy → Check and auto-reverts after
|
|
25
|
+
* 1.5s.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <CopyButton value="npm install @a4ui/core" aria-label="Copy install command" />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function CopyButton(props: CopyButtonProps): JSX.Element {
|
|
33
|
+
return (
|
|
34
|
+
<IconMorphButton
|
|
35
|
+
inactive={<Copy size={18} />}
|
|
36
|
+
active={<Check size={18} />}
|
|
37
|
+
label={props.label}
|
|
38
|
+
activeLabel={props.copiedLabel}
|
|
39
|
+
variant={props.variant}
|
|
40
|
+
class={props.class}
|
|
41
|
+
aria-label={props['aria-label']}
|
|
42
|
+
revertAfter={1500}
|
|
43
|
+
onChange={(pressed) => {
|
|
44
|
+
if (pressed) void navigator.clipboard?.writeText(props.value)
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PlayPauseButtonProps {
|
|
51
|
+
/** Controlled playing state. Omit to let the button manage its own state. */
|
|
52
|
+
pressed?: boolean
|
|
53
|
+
/** Initial playing state when uncontrolled. Defaults to `false`. */
|
|
54
|
+
defaultPressed?: boolean
|
|
55
|
+
/** Fired with the next playing state, controlled or uncontrolled. */
|
|
56
|
+
onChange?: (pressed: boolean) => void
|
|
57
|
+
/** Optional label shown next to the icon while paused. */
|
|
58
|
+
label?: JSX.Element
|
|
59
|
+
/** Optional label shown next to the icon while playing. */
|
|
60
|
+
activeLabel?: JSX.Element
|
|
61
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
62
|
+
variant?: IconMorphButtonVariant
|
|
63
|
+
class?: string
|
|
64
|
+
'aria-label'?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Play/pause toggle button whose icon morphs Play → Pause.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <PlayPauseButton defaultPressed={false} aria-label="Play" />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function PlayPauseButton(props: PlayPauseButtonProps): JSX.Element {
|
|
76
|
+
return (
|
|
77
|
+
<IconMorphButton
|
|
78
|
+
inactive={<Play size={18} />}
|
|
79
|
+
active={<Pause size={18} />}
|
|
80
|
+
pressed={props.pressed}
|
|
81
|
+
defaultPressed={props.defaultPressed}
|
|
82
|
+
onChange={props.onChange}
|
|
83
|
+
label={props.label}
|
|
84
|
+
activeLabel={props.activeLabel}
|
|
85
|
+
variant={props.variant}
|
|
86
|
+
class={props.class}
|
|
87
|
+
aria-label={props['aria-label']}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MuteButtonProps {
|
|
93
|
+
/** Controlled muted state. Omit to let the button manage its own state. */
|
|
94
|
+
pressed?: boolean
|
|
95
|
+
/** Initial muted state when uncontrolled. Defaults to `false`. */
|
|
96
|
+
defaultPressed?: boolean
|
|
97
|
+
/** Fired with the next muted state, controlled or uncontrolled. */
|
|
98
|
+
onChange?: (pressed: boolean) => void
|
|
99
|
+
/** Optional label shown next to the icon while unmuted. */
|
|
100
|
+
label?: JSX.Element
|
|
101
|
+
/** Optional label shown next to the icon while muted. */
|
|
102
|
+
activeLabel?: JSX.Element
|
|
103
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
104
|
+
variant?: IconMorphButtonVariant
|
|
105
|
+
class?: string
|
|
106
|
+
'aria-label'?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Mute toggle button whose icon morphs Volume2 → VolumeX.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* <MuteButton defaultPressed={false} aria-label="Mute" />
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function MuteButton(props: MuteButtonProps): JSX.Element {
|
|
118
|
+
return (
|
|
119
|
+
<IconMorphButton
|
|
120
|
+
inactive={<Volume2 size={18} />}
|
|
121
|
+
active={<VolumeX size={18} />}
|
|
122
|
+
pressed={props.pressed}
|
|
123
|
+
defaultPressed={props.defaultPressed}
|
|
124
|
+
onChange={props.onChange}
|
|
125
|
+
label={props.label}
|
|
126
|
+
activeLabel={props.activeLabel}
|
|
127
|
+
variant={props.variant}
|
|
128
|
+
class={props.class}
|
|
129
|
+
aria-label={props['aria-label']}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface LockButtonProps {
|
|
135
|
+
/** Controlled locked state. Omit to let the button manage its own state. */
|
|
136
|
+
pressed?: boolean
|
|
137
|
+
/** Initial locked state when uncontrolled. Defaults to `false`. */
|
|
138
|
+
defaultPressed?: boolean
|
|
139
|
+
/** Fired with the next locked state, controlled or uncontrolled. */
|
|
140
|
+
onChange?: (pressed: boolean) => void
|
|
141
|
+
/** Optional label shown next to the icon while unlocked. */
|
|
142
|
+
label?: JSX.Element
|
|
143
|
+
/** Optional label shown next to the icon while locked. */
|
|
144
|
+
activeLabel?: JSX.Element
|
|
145
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
146
|
+
variant?: IconMorphButtonVariant
|
|
147
|
+
class?: string
|
|
148
|
+
'aria-label'?: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Lock toggle button whose icon morphs Unlock → Lock.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```tsx
|
|
156
|
+
* <LockButton defaultPressed={false} aria-label="Lock" />
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export function LockButton(props: LockButtonProps): JSX.Element {
|
|
160
|
+
return (
|
|
161
|
+
<IconMorphButton
|
|
162
|
+
inactive={<Unlock size={18} />}
|
|
163
|
+
active={<Lock size={18} />}
|
|
164
|
+
pressed={props.pressed}
|
|
165
|
+
defaultPressed={props.defaultPressed}
|
|
166
|
+
onChange={props.onChange}
|
|
167
|
+
label={props.label}
|
|
168
|
+
activeLabel={props.activeLabel}
|
|
169
|
+
variant={props.variant}
|
|
170
|
+
class={props.class}
|
|
171
|
+
aria-label={props['aria-label']}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ThemeMorphButtonProps {
|
|
177
|
+
/** Controlled dark-mode state. Omit to let the button manage its own state. */
|
|
178
|
+
pressed?: boolean
|
|
179
|
+
/** Initial dark-mode state when uncontrolled. Defaults to `false`. */
|
|
180
|
+
defaultPressed?: boolean
|
|
181
|
+
/** Fired with the next dark-mode state, controlled or uncontrolled. */
|
|
182
|
+
onChange?: (pressed: boolean) => void
|
|
183
|
+
/** Optional label shown next to the icon in light mode. */
|
|
184
|
+
label?: JSX.Element
|
|
185
|
+
/** Optional label shown next to the icon in dark mode. */
|
|
186
|
+
activeLabel?: JSX.Element
|
|
187
|
+
/** Visual style. Defaults to `'ghost'`. */
|
|
188
|
+
variant?: IconMorphButtonVariant
|
|
189
|
+
class?: string
|
|
190
|
+
'aria-label'?: string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Purely presentational theme toggle whose icon morphs Sun → Moon. Does not
|
|
195
|
+
* touch the theme system itself — wire `pressed`/`onChange` to your theme
|
|
196
|
+
* state.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```tsx
|
|
200
|
+
* <ThemeMorphButton pressed={isDark()} onChange={setIsDark} aria-label="Toggle theme" />
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function ThemeMorphButton(props: ThemeMorphButtonProps): JSX.Element {
|
|
204
|
+
return (
|
|
205
|
+
<IconMorphButton
|
|
206
|
+
inactive={<Sun size={18} />}
|
|
207
|
+
active={<Moon size={18} />}
|
|
208
|
+
pressed={props.pressed}
|
|
209
|
+
defaultPressed={props.defaultPressed}
|
|
210
|
+
onChange={props.onChange}
|
|
211
|
+
label={props.label}
|
|
212
|
+
activeLabel={props.activeLabel}
|
|
213
|
+
variant={props.variant}
|
|
214
|
+
class={props.class}
|
|
215
|
+
aria-label={props['aria-label']}
|
|
216
|
+
/>
|
|
217
|
+
)
|
|
218
|
+
}
|