@joewinke/jatui 0.1.33 → 0.2.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 +22 -0
- package/dist/actions/attention.d.ts +24 -0
- package/dist/actions/attention.js +94 -0
- package/dist/actions/floating.d.ts +53 -0
- package/dist/actions/floating.js +158 -0
- package/dist/actions/staggerEntrance.d.ts +28 -0
- package/dist/actions/staggerEntrance.js +71 -0
- package/dist/actions/tooltip.d.ts +28 -0
- package/dist/actions/tooltip.js +186 -0
- package/dist/assets/sounds/README.md +47 -0
- package/dist/assets/sounds/error.d.ts +1 -0
- package/dist/assets/sounds/error.js +4 -0
- package/dist/assets/sounds/notify.d.ts +1 -0
- package/dist/assets/sounds/notify.js +4 -0
- package/dist/assets/sounds/pop.d.ts +1 -0
- package/dist/assets/sounds/pop.js +4 -0
- package/dist/assets/sounds/send.d.ts +1 -0
- package/dist/assets/sounds/send.js +4 -0
- package/dist/assets/sounds/success.d.ts +1 -0
- package/dist/assets/sounds/success.js +4 -0
- package/dist/assets/sounds/toggle.d.ts +1 -0
- package/dist/assets/sounds/toggle.js +4 -0
- package/dist/components/AnimatedNumber.svelte +112 -0
- package/dist/components/AnimatedNumber.svelte.d.ts +8 -0
- package/dist/components/Badge.svelte +75 -4
- package/dist/components/Badge.svelte.d.ts +6 -1
- package/dist/components/Button.svelte +196 -21
- package/dist/components/Button.svelte.d.ts +10 -1
- package/dist/components/Carousel.svelte +265 -0
- package/dist/components/Carousel.svelte.d.ts +21 -0
- package/dist/components/ChipInput.svelte +63 -3
- package/dist/components/Collapse.svelte +70 -14
- package/dist/components/Collapse.svelte.d.ts +11 -0
- package/dist/components/ColorSelector.svelte +21 -24
- package/dist/components/CommandPalette.svelte +475 -0
- package/dist/components/CommandPalette.svelte.d.ts +3 -0
- package/dist/components/ContextMenu.svelte +9 -73
- package/dist/components/CopyButton.svelte +156 -0
- package/dist/components/CopyButton.svelte.d.ts +12 -0
- package/dist/components/HoldToConfirm.svelte +183 -0
- package/dist/components/HoldToConfirm.svelte.d.ts +11 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +54 -8
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +3 -0
- package/dist/components/PasswordMeter.svelte +87 -0
- package/dist/components/PasswordMeter.svelte.d.ts +8 -0
- package/dist/components/PinInput.svelte +186 -0
- package/dist/components/PinInput.svelte.d.ts +13 -0
- package/dist/components/QuantityStepper.svelte +185 -0
- package/dist/components/QuantityStepper.svelte.d.ts +12 -0
- package/dist/components/Skeleton.svelte +123 -0
- package/dist/components/Skeleton.svelte.d.ts +12 -0
- package/dist/components/SpeechForm.svelte +37 -0
- package/dist/components/StatusBadge.svelte +31 -3
- package/dist/components/StatusBadge.svelte.d.ts +0 -5
- package/dist/components/Tabs.svelte +270 -0
- package/dist/components/Tabs.svelte.d.ts +15 -0
- package/dist/components/TextInput.svelte +138 -38
- package/dist/components/TextInput.svelte.d.ts +9 -1
- package/dist/components/VoicePlayer.svelte +51 -0
- package/dist/components/basic/Alert.svelte +36 -0
- package/dist/components/basic/Alert.svelte.d.ts +9 -0
- package/dist/components/basic/Breadcrumb.svelte +27 -0
- package/dist/components/basic/Breadcrumb.svelte.d.ts +11 -0
- package/dist/components/basic/ButtonGroup.svelte +15 -0
- package/dist/components/basic/ButtonGroup.svelte.d.ts +8 -0
- package/dist/components/basic/Checkbox.svelte +85 -0
- package/dist/components/basic/Checkbox.svelte.d.ts +15 -0
- package/dist/components/basic/Empty.svelte +17 -0
- package/dist/components/basic/Empty.svelte.d.ts +8 -0
- package/dist/components/basic/Kbd.svelte +16 -0
- package/dist/components/basic/Kbd.svelte.d.ts +8 -0
- package/dist/components/basic/Pagination.svelte +104 -0
- package/dist/components/basic/Pagination.svelte.d.ts +12 -0
- package/dist/components/basic/RadioGroup.svelte +71 -0
- package/dist/components/basic/RadioGroup.svelte.d.ts +18 -0
- package/dist/components/basic/Separator.svelte +19 -0
- package/dist/components/basic/Separator.svelte.d.ts +8 -0
- package/dist/components/basic/Slider.svelte +72 -0
- package/dist/components/basic/Slider.svelte.d.ts +17 -0
- package/dist/components/basic/Switch.svelte +88 -0
- package/dist/components/basic/Switch.svelte.d.ts +14 -0
- package/dist/components/basic/Toggle.svelte +43 -0
- package/dist/components/basic/Toggle.svelte.d.ts +14 -0
- package/dist/components/graph/GraphCanvas.svelte +103 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +22 -0
- package/dist/components/graph/JatuiNode.svelte +128 -0
- package/dist/components/graph/JatuiNode.svelte.d.ts +14 -0
- package/dist/components/graph/theme.d.ts +27 -0
- package/dist/components/graph/theme.js +136 -0
- package/dist/components/messaging/MessageItem.svelte +92 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +5 -0
- package/dist/components/messaging/TypingIndicator.svelte +63 -0
- package/dist/components/messaging/TypingIndicator.svelte.d.ts +7 -0
- package/dist/components/pipeline/PipelineColumn.svelte +44 -3
- package/dist/components/progress/ProgressBar.svelte +189 -0
- package/dist/components/progress/ProgressBar.svelte.d.ts +17 -0
- package/dist/components/progress/ProgressRing.svelte +138 -0
- package/dist/components/progress/ProgressRing.svelte.d.ts +12 -0
- package/dist/components/progress/Steps.svelte +195 -0
- package/dist/components/progress/Steps.svelte.d.ts +14 -0
- package/dist/components/shell/NavigationMenu.svelte +422 -0
- package/dist/components/shell/NavigationMenu.svelte.d.ts +43 -0
- package/dist/components/shell/Sidebar.svelte +276 -0
- package/dist/components/shell/Sidebar.svelte.d.ts +32 -0
- package/dist/components/shell/SidebarGroup.svelte +180 -0
- package/dist/components/shell/SidebarGroup.svelte.d.ts +19 -0
- package/dist/components/shell/SidebarItem.svelte +170 -0
- package/dist/components/shell/SidebarItem.svelte.d.ts +24 -0
- package/dist/components/shell/shell.d.ts +30 -0
- package/dist/components/shell/shell.js +65 -0
- package/dist/components/table/DataTable.svelte +364 -0
- package/dist/components/table/DataTable.svelte.d.ts +4 -0
- package/dist/components/table/flexRender.d.ts +31 -0
- package/dist/components/table/flexRender.js +32 -0
- package/dist/components/table/types.d.ts +28 -0
- package/dist/components/table/types.js +1 -0
- package/dist/components/toast/ToastHost.svelte +21 -0
- package/dist/components/toast/ToastHost.svelte.d.ts +3 -0
- package/dist/components/toast/ToastItem.svelte +287 -0
- package/dist/components/toast/ToastItem.svelte.d.ts +9 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +67 -0
- package/dist/showcase/AnimatedBeam.svelte +183 -0
- package/dist/showcase/AnimatedBeam.svelte.d.ts +26 -0
- package/dist/showcase/AuroraDrift.svelte +74 -0
- package/dist/showcase/AuroraDrift.svelte.d.ts +9 -0
- package/dist/showcase/BentoCard.svelte +50 -0
- package/dist/showcase/BentoCard.svelte.d.ts +14 -0
- package/dist/showcase/BentoGrid.svelte +43 -0
- package/dist/showcase/BentoGrid.svelte.d.ts +12 -0
- package/dist/showcase/BorderBeam.svelte +79 -0
- package/dist/showcase/BorderBeam.svelte.d.ts +12 -0
- package/dist/showcase/ClipWipe.svelte +82 -0
- package/dist/showcase/ClipWipe.svelte.d.ts +12 -0
- package/dist/showcase/Confetti.svelte +71 -0
- package/dist/showcase/Confetti.svelte.d.ts +10 -0
- package/dist/showcase/CursorSpotlight.svelte +85 -0
- package/dist/showcase/CursorSpotlight.svelte.d.ts +9 -0
- package/dist/showcase/DeviceMock.svelte +186 -0
- package/dist/showcase/DeviceMock.svelte.d.ts +11 -0
- package/dist/showcase/Dock.svelte +123 -0
- package/dist/showcase/Dock.svelte.d.ts +17 -0
- package/dist/showcase/FlipCard.svelte +103 -0
- package/dist/showcase/FlipCard.svelte.d.ts +10 -0
- package/dist/showcase/FocusReveal.svelte +83 -0
- package/dist/showcase/FocusReveal.svelte.d.ts +11 -0
- package/dist/showcase/Globe.svelte +202 -0
- package/dist/showcase/Globe.svelte.d.ts +16 -0
- package/dist/showcase/KenBurns.svelte +105 -0
- package/dist/showcase/KenBurns.svelte.d.ts +13 -0
- package/dist/showcase/Marquee.svelte +89 -0
- package/dist/showcase/Marquee.svelte.d.ts +9 -0
- package/dist/showcase/ParallaxTilt.svelte +98 -0
- package/dist/showcase/ParallaxTilt.svelte.d.ts +10 -0
- package/dist/showcase/ScrambleText.svelte +98 -0
- package/dist/showcase/ScrambleText.svelte.d.ts +11 -0
- package/dist/showcase/ShineSweep.svelte +81 -0
- package/dist/showcase/ShineSweep.svelte.d.ts +9 -0
- package/dist/showcase/Terminal.svelte +230 -0
- package/dist/showcase/Terminal.svelte.d.ts +18 -0
- package/dist/showcase/TextSplitReveal.svelte +146 -0
- package/dist/showcase/TextSplitReveal.svelte.d.ts +13 -0
- package/dist/showcase/TextWave.svelte +55 -0
- package/dist/showcase/TextWave.svelte.d.ts +8 -0
- package/dist/showcase/TypewriterText.svelte +130 -0
- package/dist/showcase/TypewriterText.svelte.d.ts +9 -0
- package/dist/showcase/backdrop/GrainGradient.svelte +126 -0
- package/dist/showcase/backdrop/GrainGradient.svelte.d.ts +23 -0
- package/dist/showcase/backdrop/MeshGradient.svelte +104 -0
- package/dist/showcase/backdrop/MeshGradient.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/Metaballs.svelte +116 -0
- package/dist/showcase/backdrop/Metaballs.svelte.d.ts +18 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte +115 -0
- package/dist/showcase/backdrop/NeuroNoise.svelte.d.ts +20 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte +48 -0
- package/dist/showcase/backdrop/ShowcaseBackdrop.svelte.d.ts +12 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte +234 -0
- package/dist/showcase/generative/GenerativeBackdrop.svelte.d.ts +9 -0
- package/dist/showcase/generative/intent.d.ts +7 -0
- package/dist/showcase/generative/intent.js +67 -0
- package/dist/showcase/generative/manifest.d.ts +9 -0
- package/dist/showcase/generative/manifest.js +97 -0
- package/dist/showcase/generative/types.d.ts +48 -0
- package/dist/showcase/generative/types.js +8 -0
- package/dist/showcase/index.d.ts +36 -0
- package/dist/showcase/index.js +46 -0
- package/dist/showcase/scroll/SmoothScroll.svelte +49 -0
- package/dist/showcase/scroll/SmoothScroll.svelte.d.ts +12 -0
- package/dist/showcase/scroll/scrollProgress.d.ts +24 -0
- package/dist/showcase/scroll/scrollProgress.js +65 -0
- package/dist/showcase/scroll/scrollReveal.d.ts +30 -0
- package/dist/showcase/scroll/scrollReveal.js +70 -0
- package/dist/stores/commandPalette.svelte.d.ts +24 -0
- package/dist/stores/commandPalette.svelte.js +82 -0
- package/dist/stores/toast.svelte.d.ts +28 -0
- package/dist/stores/toast.svelte.js +33 -0
- package/dist/styles/motion.css +46 -0
- package/dist/styles/tokens.css +31 -0
- package/dist/transitions/glide.d.ts +31 -0
- package/dist/transitions/glide.js +58 -0
- package/dist/utils/sound.d.ts +6 -0
- package/dist/utils/sound.js +113 -0
- package/dist/utils/splitText.d.ts +25 -0
- package/dist/utils/splitText.js +94 -0
- package/dist/utils/themeWipe.d.ts +21 -0
- package/dist/utils/themeWipe.js +94 -0
- package/dist/utils/viewTransition.d.ts +23 -0
- package/dist/utils/viewTransition.js +75 -0
- package/package.json +50 -3
|
@@ -3,17 +3,27 @@
|
|
|
3
3
|
* Button Component
|
|
4
4
|
*
|
|
5
5
|
* Generic DaisyUI button with variant, size, loading state, and optional href.
|
|
6
|
+
* Adds: press squish, submit state machine (idle/loading/success/error),
|
|
7
|
+
* success check animation, error shake → auto-idle.
|
|
6
8
|
*/
|
|
7
9
|
|
|
10
|
+
type SubmitState = 'idle' | 'loading' | 'success' | 'error';
|
|
11
|
+
|
|
8
12
|
interface Props {
|
|
9
13
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'error' | 'warning' | 'success' | 'info';
|
|
10
14
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
11
15
|
href?: string;
|
|
12
16
|
type?: 'button' | 'submit' | 'reset';
|
|
13
17
|
disabled?: boolean;
|
|
18
|
+
/** @deprecated Use `state="loading"` instead. */
|
|
14
19
|
loading?: boolean;
|
|
15
|
-
/**
|
|
20
|
+
/** @deprecated Use `state="success"` instead. */
|
|
16
21
|
success?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Submit state machine. Drives loading dots, success check draw-on, error shake.
|
|
24
|
+
* Error auto-returns to idle after the shake animation completes.
|
|
25
|
+
*/
|
|
26
|
+
state?: SubmitState;
|
|
17
27
|
class?: string;
|
|
18
28
|
onclick?: (e: MouseEvent) => void;
|
|
19
29
|
children?: import('svelte').Snippet;
|
|
@@ -27,21 +37,33 @@
|
|
|
27
37
|
disabled = false,
|
|
28
38
|
loading = false,
|
|
29
39
|
success = false,
|
|
40
|
+
state: submitStateProp = 'idle',
|
|
30
41
|
class: className = '',
|
|
31
42
|
onclick,
|
|
32
43
|
children
|
|
33
44
|
}: Props = $props();
|
|
34
45
|
|
|
46
|
+
// Merge legacy boolean props into the state machine
|
|
47
|
+
const effectiveState = $derived<SubmitState>(
|
|
48
|
+
loading ? 'loading' :
|
|
49
|
+
success ? 'success' :
|
|
50
|
+
submitStateProp
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const isLoading = $derived(effectiveState === 'loading');
|
|
54
|
+
const isSuccess = $derived(effectiveState === 'success');
|
|
55
|
+
const isError = $derived(effectiveState === 'error');
|
|
56
|
+
|
|
35
57
|
const variantClass = $derived(
|
|
36
58
|
{
|
|
37
|
-
primary:
|
|
59
|
+
primary: 'btn-primary',
|
|
38
60
|
secondary: 'btn-secondary',
|
|
39
|
-
outline:
|
|
40
|
-
ghost:
|
|
41
|
-
error:
|
|
42
|
-
warning:
|
|
43
|
-
success:
|
|
44
|
-
info:
|
|
61
|
+
outline: 'btn-outline',
|
|
62
|
+
ghost: 'btn-ghost',
|
|
63
|
+
error: 'btn-error',
|
|
64
|
+
warning: 'btn-warning',
|
|
65
|
+
success: 'btn-success',
|
|
66
|
+
info: 'btn-info'
|
|
45
67
|
}[variant] || 'btn-primary'
|
|
46
68
|
);
|
|
47
69
|
|
|
@@ -54,28 +76,181 @@
|
|
|
54
76
|
}[size]
|
|
55
77
|
);
|
|
56
78
|
|
|
57
|
-
const buttonClass = $derived(
|
|
79
|
+
const buttonClass = $derived(
|
|
80
|
+
[
|
|
81
|
+
'btn jatui-btn',
|
|
82
|
+
variantClass,
|
|
83
|
+
sizeClass,
|
|
84
|
+
isError ? 'jatui-btn-shake' : '',
|
|
85
|
+
className
|
|
86
|
+
]
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join(' ')
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
let btnEl: HTMLButtonElement | undefined = $state();
|
|
92
|
+
let prevError = false;
|
|
93
|
+
|
|
94
|
+
// Re-trigger shake when state transitions back to error repeatedly
|
|
95
|
+
$effect(() => {
|
|
96
|
+
const nowError = isError;
|
|
97
|
+
if (nowError && !prevError && btnEl) {
|
|
98
|
+
const reduce =
|
|
99
|
+
typeof window !== 'undefined' &&
|
|
100
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
101
|
+
if (!reduce) {
|
|
102
|
+
btnEl.classList.remove('jatui-btn-shake');
|
|
103
|
+
void btnEl.offsetWidth;
|
|
104
|
+
btnEl.classList.add('jatui-btn-shake');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
prevError = nowError;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function onShakeEnd(e: AnimationEvent) {
|
|
111
|
+
if (e.animationName === 'jatui-shake-x' && isError) {
|
|
112
|
+
// Auto-return to idle via the parent's state binding is not possible
|
|
113
|
+
// from inside the component — we fire a custom event so the parent can react.
|
|
114
|
+
btnEl?.dispatchEvent(new CustomEvent('errorshakeend', { bubbles: true }));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
58
117
|
</script>
|
|
59
118
|
|
|
60
119
|
{#if href}
|
|
61
120
|
<a {href} class={buttonClass} role="button" tabindex="0">
|
|
62
|
-
{#if children}
|
|
63
|
-
{@render children()}
|
|
64
|
-
{/if}
|
|
121
|
+
{#if children}{@render children()}{/if}
|
|
65
122
|
</a>
|
|
66
123
|
{:else}
|
|
67
|
-
<button
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
|
|
124
|
+
<button
|
|
125
|
+
bind:this={btnEl}
|
|
126
|
+
{type}
|
|
127
|
+
disabled={disabled || isLoading}
|
|
128
|
+
aria-busy={isLoading || undefined}
|
|
129
|
+
class={buttonClass}
|
|
130
|
+
{onclick}
|
|
131
|
+
onanimationend={onShakeEnd}
|
|
132
|
+
>
|
|
133
|
+
{#if isLoading}
|
|
134
|
+
<span class="jatui-btn-dots" aria-hidden="true">
|
|
135
|
+
<i></i><i></i><i></i>
|
|
136
|
+
</span>
|
|
137
|
+
{:else if isSuccess}
|
|
138
|
+
<span class="jatui-btn-check" aria-hidden="true">
|
|
72
139
|
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
73
|
-
<path pathLength="1" d="M5 13l4 4L19 7" />
|
|
140
|
+
<path class="jatui-check-path" pathLength="1" d="M5 13l4 4L19 7" />
|
|
141
|
+
</svg>
|
|
142
|
+
</span>
|
|
143
|
+
{:else if isError}
|
|
144
|
+
<span class="jatui-btn-icon-error" aria-hidden="true">
|
|
145
|
+
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
146
|
+
<path d="M18 6L6 18M6 6l12 12"/>
|
|
74
147
|
</svg>
|
|
75
148
|
</span>
|
|
76
149
|
{/if}
|
|
77
|
-
{#if children}
|
|
78
|
-
{@render children()}
|
|
79
|
-
{/if}
|
|
150
|
+
{#if children}{@render children()}{/if}
|
|
80
151
|
</button>
|
|
81
152
|
{/if}
|
|
153
|
+
|
|
154
|
+
<style>
|
|
155
|
+
/* ── Press Squish ─────────────────────────────────────────────
|
|
156
|
+
Quick down on :active, spring back on release. Transform-only.
|
|
157
|
+
Local @media guard — motion.css is the backstop but not guaranteed. */
|
|
158
|
+
.jatui-btn {
|
|
159
|
+
transition: transform var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1)) 500ms;
|
|
160
|
+
will-change: transform;
|
|
161
|
+
}
|
|
162
|
+
.jatui-btn:active:not(:disabled) {
|
|
163
|
+
transform: scale(0.97);
|
|
164
|
+
transition: transform var(--motion-instant, 80ms) ease-out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@media (prefers-reduced-motion: reduce) {
|
|
168
|
+
.jatui-btn {
|
|
169
|
+
transition: none;
|
|
170
|
+
}
|
|
171
|
+
.jatui-btn:active:not(:disabled) {
|
|
172
|
+
transform: none;
|
|
173
|
+
transition: none;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ── Loading dots ─────────────────────────────────────────────
|
|
178
|
+
Three dots bounce with staggered delays. Transform-only. */
|
|
179
|
+
.jatui-btn-dots {
|
|
180
|
+
display: inline-flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 3px;
|
|
183
|
+
}
|
|
184
|
+
.jatui-btn-dots i {
|
|
185
|
+
display: inline-block;
|
|
186
|
+
width: 0.3em;
|
|
187
|
+
height: 0.3em;
|
|
188
|
+
border-radius: 50%;
|
|
189
|
+
background: currentColor;
|
|
190
|
+
animation: jatui-bounce var(--motion-move, 300ms) ease-in-out infinite;
|
|
191
|
+
}
|
|
192
|
+
.jatui-btn-dots i:nth-child(2) { animation-delay: 0.12s; }
|
|
193
|
+
.jatui-btn-dots i:nth-child(3) { animation-delay: 0.24s; }
|
|
194
|
+
|
|
195
|
+
@keyframes jatui-bounce {
|
|
196
|
+
0%, 100% { transform: translateY(0); }
|
|
197
|
+
50% { transform: translateY(-4px); }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@media (prefers-reduced-motion: reduce) {
|
|
201
|
+
.jatui-btn-dots i {
|
|
202
|
+
animation: none;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* ── Success check draw-on ────────────────────────────────────
|
|
207
|
+
stroke-dashoffset 1→0 using --motion-glide + --motion-fast.
|
|
208
|
+
Confirm spring on the check itself: --motion-spring. */
|
|
209
|
+
.jatui-btn-check {
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
}
|
|
213
|
+
.jatui-check-path {
|
|
214
|
+
stroke-dasharray: 1;
|
|
215
|
+
stroke-dashoffset: 1;
|
|
216
|
+
transition:
|
|
217
|
+
stroke-dashoffset var(--motion-fast, 150ms) var(--motion-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
218
|
+
}
|
|
219
|
+
/* Trigger the draw-in as soon as the element is rendered (data-state handled via Svelte) */
|
|
220
|
+
.jatui-btn-check .jatui-check-path {
|
|
221
|
+
stroke-dashoffset: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@media (prefers-reduced-motion: reduce) {
|
|
225
|
+
.jatui-check-path {
|
|
226
|
+
transition: none;
|
|
227
|
+
stroke-dashoffset: 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── Error shake ──────────────────────────────────────────────
|
|
232
|
+
Decaying horizontal oscillation, retriggerable via class remove+add.
|
|
233
|
+
Width is reserved — no layout shift. */
|
|
234
|
+
@keyframes jatui-shake-x {
|
|
235
|
+
10%, 90% { transform: translateX(-1px) scale(1); }
|
|
236
|
+
20%, 80% { transform: translateX(2px) scale(1); }
|
|
237
|
+
30%, 50%, 70% { transform: translateX(-4px) scale(1); }
|
|
238
|
+
40%, 60% { transform: translateX(4px) scale(1); }
|
|
239
|
+
100% { transform: translateX(0) scale(1); }
|
|
240
|
+
}
|
|
241
|
+
.jatui-btn-shake {
|
|
242
|
+
animation: jatui-shake-x 450ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@media (prefers-reduced-motion: reduce) {
|
|
246
|
+
.jatui-btn-shake {
|
|
247
|
+
animation: none;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* Error icon */
|
|
252
|
+
.jatui-btn-icon-error {
|
|
253
|
+
display: inline-flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
}
|
|
256
|
+
</style>
|
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
* Button Component
|
|
3
3
|
*
|
|
4
4
|
* Generic DaisyUI button with variant, size, loading state, and optional href.
|
|
5
|
+
* Adds: press squish, submit state machine (idle/loading/success/error),
|
|
6
|
+
* success check animation, error shake → auto-idle.
|
|
5
7
|
*/
|
|
8
|
+
type SubmitState = 'idle' | 'loading' | 'success' | 'error';
|
|
6
9
|
interface Props {
|
|
7
10
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'error' | 'warning' | 'success' | 'info';
|
|
8
11
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
9
12
|
href?: string;
|
|
10
13
|
type?: 'button' | 'submit' | 'reset';
|
|
11
14
|
disabled?: boolean;
|
|
15
|
+
/** @deprecated Use `state="loading"` instead. */
|
|
12
16
|
loading?: boolean;
|
|
13
|
-
/**
|
|
17
|
+
/** @deprecated Use `state="success"` instead. */
|
|
14
18
|
success?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Submit state machine. Drives loading dots, success check draw-on, error shake.
|
|
21
|
+
* Error auto-returns to idle after the shake animation completes.
|
|
22
|
+
*/
|
|
23
|
+
state?: SubmitState;
|
|
15
24
|
class?: string;
|
|
16
25
|
onclick?: (e: MouseEvent) => void;
|
|
17
26
|
children?: import('svelte').Snippet;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import emblaCarouselSvelte from 'embla-carousel-svelte';
|
|
3
|
+
import type { EmblaCarouselType } from 'embla-carousel';
|
|
4
|
+
import Button from './Button.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Gap between slides (CSS value, e.g. "1rem", "16px") */
|
|
8
|
+
gap?: string;
|
|
9
|
+
/** Number of slides visible at a time */
|
|
10
|
+
slidesPerView?: number;
|
|
11
|
+
/** Whether the carousel loops infinitely */
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
/** Alignment of selected slide within the viewport */
|
|
14
|
+
align?: 'start' | 'center' | 'end';
|
|
15
|
+
/** Enable autoplay; pauses on hover/focus/reduced-motion */
|
|
16
|
+
autoplay?: boolean;
|
|
17
|
+
/** Autoplay interval in ms */
|
|
18
|
+
autoplayInterval?: number;
|
|
19
|
+
/** Accessible label for the carousel landmark */
|
|
20
|
+
label?: string;
|
|
21
|
+
class?: string;
|
|
22
|
+
children?: import('svelte').Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
gap = '1rem',
|
|
27
|
+
slidesPerView = 1,
|
|
28
|
+
loop = false,
|
|
29
|
+
align = 'start',
|
|
30
|
+
autoplay = false,
|
|
31
|
+
autoplayInterval = 4000,
|
|
32
|
+
label = 'Carousel',
|
|
33
|
+
class: className = '',
|
|
34
|
+
children
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
let emblaApi = $state<EmblaCarouselType | null>(null);
|
|
38
|
+
let selectedIndex = $state(0);
|
|
39
|
+
let slideCount = $state(0);
|
|
40
|
+
let canScrollPrev = $state(false);
|
|
41
|
+
let canScrollNext = $state(false);
|
|
42
|
+
let autoplayTimer = $state<ReturnType<typeof setInterval> | null>(null);
|
|
43
|
+
let isHovered = $state(false);
|
|
44
|
+
let isFocused = $state(false);
|
|
45
|
+
|
|
46
|
+
const reducedMotion =
|
|
47
|
+
typeof window !== 'undefined'
|
|
48
|
+
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
49
|
+
: false;
|
|
50
|
+
|
|
51
|
+
const emblaOptions = $derived({
|
|
52
|
+
loop,
|
|
53
|
+
align,
|
|
54
|
+
duration: reducedMotion ? 0 : 25
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function onEmblaInit(evt: CustomEvent<EmblaCarouselType>) {
|
|
58
|
+
emblaApi = evt.detail;
|
|
59
|
+
slideCount = emblaApi.slideNodes().length;
|
|
60
|
+
updateState();
|
|
61
|
+
emblaApi.on('select', updateState);
|
|
62
|
+
emblaApi.on('reInit', updateState);
|
|
63
|
+
if (autoplay && !reducedMotion) startAutoplay();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function updateState() {
|
|
67
|
+
if (!emblaApi) return;
|
|
68
|
+
selectedIndex = emblaApi.selectedScrollSnap();
|
|
69
|
+
canScrollPrev = emblaApi.canScrollPrev();
|
|
70
|
+
canScrollNext = emblaApi.canScrollNext();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scrollPrev() {
|
|
74
|
+
emblaApi?.scrollPrev();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function scrollNext() {
|
|
78
|
+
emblaApi?.scrollNext();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scrollTo(index: number) {
|
|
82
|
+
emblaApi?.scrollTo(index);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function startAutoplay() {
|
|
86
|
+
stopAutoplay();
|
|
87
|
+
autoplayTimer = setInterval(() => {
|
|
88
|
+
if (!emblaApi) return;
|
|
89
|
+
if (loop || emblaApi.canScrollNext()) {
|
|
90
|
+
emblaApi.scrollNext();
|
|
91
|
+
} else {
|
|
92
|
+
emblaApi.scrollTo(0);
|
|
93
|
+
}
|
|
94
|
+
}, autoplayInterval);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopAutoplay() {
|
|
98
|
+
if (autoplayTimer !== null) {
|
|
99
|
+
clearInterval(autoplayTimer);
|
|
100
|
+
autoplayTimer = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$effect(() => {
|
|
105
|
+
if (!autoplay || reducedMotion || !emblaApi) return;
|
|
106
|
+
if (isHovered || isFocused) {
|
|
107
|
+
stopAutoplay();
|
|
108
|
+
} else {
|
|
109
|
+
startAutoplay();
|
|
110
|
+
}
|
|
111
|
+
return stopAutoplay;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
115
|
+
if (e.key === 'ArrowLeft') {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
scrollPrev();
|
|
118
|
+
} else if (e.key === 'ArrowRight') {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
scrollNext();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
126
|
+
<section
|
|
127
|
+
aria-label={label}
|
|
128
|
+
aria-roledescription="carousel"
|
|
129
|
+
class="jatui-carousel {className}"
|
|
130
|
+
onmouseenter={() => (isHovered = true)}
|
|
131
|
+
onmouseleave={() => (isHovered = false)}
|
|
132
|
+
onfocusin={() => (isFocused = true)}
|
|
133
|
+
onfocusout={() => (isFocused = false)}
|
|
134
|
+
onkeydown={handleKeyDown}
|
|
135
|
+
>
|
|
136
|
+
<div
|
|
137
|
+
class="jatui-carousel-viewport"
|
|
138
|
+
use:emblaCarouselSvelte={{ options: emblaOptions, plugins: [] }}
|
|
139
|
+
onemblaInit={onEmblaInit}
|
|
140
|
+
>
|
|
141
|
+
<div
|
|
142
|
+
class="jatui-carousel-track"
|
|
143
|
+
style:gap
|
|
144
|
+
style:--jatui-carousel-spv={slidesPerView}
|
|
145
|
+
style:--jatui-carousel-gap={gap}
|
|
146
|
+
>
|
|
147
|
+
{#if children}{@render children()}{/if}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{#if slideCount > 1}
|
|
152
|
+
<div class="jatui-carousel-controls" aria-label="Carousel controls">
|
|
153
|
+
<Button
|
|
154
|
+
variant="ghost"
|
|
155
|
+
size="sm"
|
|
156
|
+
onclick={scrollPrev}
|
|
157
|
+
disabled={!loop && !canScrollPrev}
|
|
158
|
+
>
|
|
159
|
+
<span class="sr-only">Previous slide</span>
|
|
160
|
+
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
161
|
+
<path d="M15 18l-6-6 6-6"/>
|
|
162
|
+
</svg>
|
|
163
|
+
</Button>
|
|
164
|
+
|
|
165
|
+
<div class="jatui-carousel-dots" role="tablist" aria-label="Slides">
|
|
166
|
+
{#each Array(slideCount) as _, i}
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
role="tab"
|
|
170
|
+
aria-selected={selectedIndex === i}
|
|
171
|
+
aria-label="Go to slide {i + 1}"
|
|
172
|
+
class="jatui-carousel-dot"
|
|
173
|
+
class:jatui-carousel-dot--active={selectedIndex === i}
|
|
174
|
+
onclick={() => scrollTo(i)}
|
|
175
|
+
></button>
|
|
176
|
+
{/each}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<Button
|
|
180
|
+
variant="ghost"
|
|
181
|
+
size="sm"
|
|
182
|
+
onclick={scrollNext}
|
|
183
|
+
disabled={!loop && !canScrollNext}
|
|
184
|
+
>
|
|
185
|
+
<span class="sr-only">Next slide</span>
|
|
186
|
+
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
187
|
+
<path d="M9 18l6-6-6-6"/>
|
|
188
|
+
</svg>
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
{/if}
|
|
192
|
+
</section>
|
|
193
|
+
|
|
194
|
+
<style>
|
|
195
|
+
.jatui-carousel {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
gap: var(--jatui-space-lg);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.jatui-carousel-viewport {
|
|
202
|
+
overflow: hidden;
|
|
203
|
+
border-radius: var(--jatui-radius-box);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.jatui-carousel-track {
|
|
207
|
+
display: flex;
|
|
208
|
+
touch-action: pan-y pinch-zoom;
|
|
209
|
+
/* gap is set via inline style from the gap prop */
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── Slide sizing ─────────────────────────────────────────────
|
|
213
|
+
Each direct child of the track is a slide. We let consumers
|
|
214
|
+
wrap content in whatever element they like; we size them via
|
|
215
|
+
the CSS custom property that the component owner sets via
|
|
216
|
+
slidesPerView. Because slidesPerView is a prop, we compute
|
|
217
|
+
the flex-basis here via an inline var. We expose it as a
|
|
218
|
+
CSS var on the track so slides can inherit it. */
|
|
219
|
+
.jatui-carousel-track > :global(*) {
|
|
220
|
+
flex: 0 0 calc((100% - (var(--jatui-carousel-spv, 1) - 1) * var(--jatui-carousel-gap, 1rem)) / var(--jatui-carousel-spv, 1));
|
|
221
|
+
min-width: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ── Controls ─────────────────────────────────────────────────*/
|
|
225
|
+
.jatui-carousel-controls {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
gap: var(--jatui-space-lg);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ── Dot indicators ───────────────────────────────────────────*/
|
|
233
|
+
.jatui-carousel-dots {
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
gap: var(--jatui-space-sm);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.jatui-carousel-dot {
|
|
240
|
+
width: 6px;
|
|
241
|
+
height: 6px;
|
|
242
|
+
border-radius: 9999px;
|
|
243
|
+
border: none;
|
|
244
|
+
padding: 0;
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
background: oklch(from var(--color-base-content, currentColor) l c h / 0.25);
|
|
247
|
+
transition:
|
|
248
|
+
width var(--motion-move) var(--motion-glide),
|
|
249
|
+
background var(--motion-fast) var(--motion-glide);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.jatui-carousel-dot--active {
|
|
253
|
+
width: 18px;
|
|
254
|
+
background: oklch(from var(--color-base-content, currentColor) l c h / 0.75);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@media (prefers-reduced-motion: reduce) {
|
|
258
|
+
.jatui-carousel-dot {
|
|
259
|
+
transition: none;
|
|
260
|
+
}
|
|
261
|
+
.jatui-carousel-dot--active {
|
|
262
|
+
outline: 2px solid currentColor;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** Gap between slides (CSS value, e.g. "1rem", "16px") */
|
|
3
|
+
gap?: string;
|
|
4
|
+
/** Number of slides visible at a time */
|
|
5
|
+
slidesPerView?: number;
|
|
6
|
+
/** Whether the carousel loops infinitely */
|
|
7
|
+
loop?: boolean;
|
|
8
|
+
/** Alignment of selected slide within the viewport */
|
|
9
|
+
align?: 'start' | 'center' | 'end';
|
|
10
|
+
/** Enable autoplay; pauses on hover/focus/reduced-motion */
|
|
11
|
+
autoplay?: boolean;
|
|
12
|
+
/** Autoplay interval in ms */
|
|
13
|
+
autoplayInterval?: number;
|
|
14
|
+
/** Accessible label for the carousel landmark */
|
|
15
|
+
label?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
children?: import('svelte').Snippet;
|
|
18
|
+
}
|
|
19
|
+
declare const Carousel: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type Carousel = ReturnType<typeof Carousel>;
|
|
21
|
+
export default Carousel;
|
|
@@ -317,10 +317,10 @@
|
|
|
317
317
|
sel.removeAllRanges();
|
|
318
318
|
sel.addRange(newRange);
|
|
319
319
|
} else {
|
|
320
|
-
// Create chip element
|
|
320
|
+
// Create chip element — spring pop on add
|
|
321
321
|
const chip = document.createElement('span');
|
|
322
322
|
chip.contentEditable = 'false';
|
|
323
|
-
chip.className = chipInfo.className;
|
|
323
|
+
chip.className = chipInfo.className + ' chip-enter';
|
|
324
324
|
chip.dataset.chipValue = suggestion.value;
|
|
325
325
|
for (const [key, val] of Object.entries(chipInfo.dataAttrs)) {
|
|
326
326
|
chip.dataset[key] = val;
|
|
@@ -399,6 +399,41 @@
|
|
|
399
399
|
|
|
400
400
|
import { fly } from 'svelte/transition';
|
|
401
401
|
import { cubicOut } from 'svelte/easing';
|
|
402
|
+
|
|
403
|
+
// Animate chip removal: add exit class, wait for animation, then remove node
|
|
404
|
+
function animateChipRemove(chip: HTMLElement) {
|
|
405
|
+
chip.classList.add('chip-exit');
|
|
406
|
+
chip.addEventListener('animationend', () => chip.remove(), { once: true });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Override Backspace to animate chip removal before DOM removal
|
|
410
|
+
function handleKeydownWithChipAnim(e: KeyboardEvent) {
|
|
411
|
+
if (e.key === 'Backspace' && editableRef) {
|
|
412
|
+
const sel = window.getSelection();
|
|
413
|
+
if (sel && sel.rangeCount > 0) {
|
|
414
|
+
const range = sel.getRangeAt(0);
|
|
415
|
+
if (range.collapsed) {
|
|
416
|
+
const container = range.startContainer;
|
|
417
|
+
const offset = range.startOffset;
|
|
418
|
+
// Check if cursor is right after a chip span
|
|
419
|
+
let prevNode: Node | null = null;
|
|
420
|
+
if (container.nodeType === Node.TEXT_NODE && offset === 0) {
|
|
421
|
+
prevNode = container.previousSibling;
|
|
422
|
+
} else if (container.nodeType !== Node.TEXT_NODE) {
|
|
423
|
+
prevNode = (container as Element).childNodes[offset - 1] ?? null;
|
|
424
|
+
}
|
|
425
|
+
if (prevNode instanceof HTMLElement && prevNode.dataset.chipValue !== undefined) {
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
animateChipRemove(prevNode);
|
|
428
|
+
// Defer value sync until after animation
|
|
429
|
+
setTimeout(() => syncValue(), 250);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
handleKeydown(e);
|
|
436
|
+
}
|
|
402
437
|
</script>
|
|
403
438
|
|
|
404
439
|
<div class="chip-input-wrapper relative">
|
|
@@ -417,7 +452,7 @@
|
|
|
417
452
|
{disabled ? 'opacity: 0.5; cursor: not-allowed;' : ''}
|
|
418
453
|
"
|
|
419
454
|
oninput={handleInput}
|
|
420
|
-
onkeydown={
|
|
455
|
+
onkeydown={handleKeydownWithChipAnim}
|
|
421
456
|
onpaste={handlePaste}
|
|
422
457
|
onfocus={onfocus}
|
|
423
458
|
onblur={handleBlur}
|
|
@@ -606,4 +641,29 @@
|
|
|
606
641
|
border-radius: 2px;
|
|
607
642
|
font-size: 10px;
|
|
608
643
|
}
|
|
644
|
+
|
|
645
|
+
/* Chip kinetics: spring pop on add, glide exit on remove */
|
|
646
|
+
:global(.chip-enter) {
|
|
647
|
+
animation: chip-pop var(--motion-fast) var(--motion-spring);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
:global(.chip-exit) {
|
|
651
|
+
animation: chip-out var(--motion-fast) var(--motion-glide) forwards;
|
|
652
|
+
pointer-events: none;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
@keyframes chip-pop {
|
|
656
|
+
from { transform: scale(0.4); opacity: 0; }
|
|
657
|
+
to { transform: scale(1); opacity: 1; }
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
@keyframes chip-out {
|
|
661
|
+
from { transform: scale(1); opacity: 1; }
|
|
662
|
+
to { transform: scale(0.4); opacity: 0; }
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
@media (prefers-reduced-motion: reduce) {
|
|
666
|
+
:global(.chip-enter) { animation: none; }
|
|
667
|
+
:global(.chip-exit) { animation: none; opacity: 0; }
|
|
668
|
+
}
|
|
609
669
|
</style>
|