@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.
Files changed (209) hide show
  1. package/CLAUDE.md +22 -0
  2. package/dist/actions/attention.d.ts +24 -0
  3. package/dist/actions/attention.js +94 -0
  4. package/dist/actions/floating.d.ts +53 -0
  5. package/dist/actions/floating.js +158 -0
  6. package/dist/actions/staggerEntrance.d.ts +28 -0
  7. package/dist/actions/staggerEntrance.js +71 -0
  8. package/dist/actions/tooltip.d.ts +28 -0
  9. package/dist/actions/tooltip.js +186 -0
  10. package/dist/assets/sounds/README.md +47 -0
  11. package/dist/assets/sounds/error.d.ts +1 -0
  12. package/dist/assets/sounds/error.js +4 -0
  13. package/dist/assets/sounds/notify.d.ts +1 -0
  14. package/dist/assets/sounds/notify.js +4 -0
  15. package/dist/assets/sounds/pop.d.ts +1 -0
  16. package/dist/assets/sounds/pop.js +4 -0
  17. package/dist/assets/sounds/send.d.ts +1 -0
  18. package/dist/assets/sounds/send.js +4 -0
  19. package/dist/assets/sounds/success.d.ts +1 -0
  20. package/dist/assets/sounds/success.js +4 -0
  21. package/dist/assets/sounds/toggle.d.ts +1 -0
  22. package/dist/assets/sounds/toggle.js +4 -0
  23. package/dist/components/AnimatedNumber.svelte +112 -0
  24. package/dist/components/AnimatedNumber.svelte.d.ts +8 -0
  25. package/dist/components/Badge.svelte +75 -4
  26. package/dist/components/Badge.svelte.d.ts +6 -1
  27. package/dist/components/Button.svelte +196 -21
  28. package/dist/components/Button.svelte.d.ts +10 -1
  29. package/dist/components/Carousel.svelte +265 -0
  30. package/dist/components/Carousel.svelte.d.ts +21 -0
  31. package/dist/components/ChipInput.svelte +63 -3
  32. package/dist/components/Collapse.svelte +70 -14
  33. package/dist/components/Collapse.svelte.d.ts +11 -0
  34. package/dist/components/ColorSelector.svelte +21 -24
  35. package/dist/components/CommandPalette.svelte +475 -0
  36. package/dist/components/CommandPalette.svelte.d.ts +3 -0
  37. package/dist/components/ContextMenu.svelte +9 -73
  38. package/dist/components/CopyButton.svelte +156 -0
  39. package/dist/components/CopyButton.svelte.d.ts +12 -0
  40. package/dist/components/HoldToConfirm.svelte +183 -0
  41. package/dist/components/HoldToConfirm.svelte.d.ts +11 -0
  42. package/dist/components/KeyboardShortcutsOverlay.svelte +54 -8
  43. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +3 -0
  44. package/dist/components/PasswordMeter.svelte +87 -0
  45. package/dist/components/PasswordMeter.svelte.d.ts +8 -0
  46. package/dist/components/PinInput.svelte +186 -0
  47. package/dist/components/PinInput.svelte.d.ts +13 -0
  48. package/dist/components/QuantityStepper.svelte +185 -0
  49. package/dist/components/QuantityStepper.svelte.d.ts +12 -0
  50. package/dist/components/Skeleton.svelte +123 -0
  51. package/dist/components/Skeleton.svelte.d.ts +12 -0
  52. package/dist/components/SpeechForm.svelte +37 -0
  53. package/dist/components/StatusBadge.svelte +31 -3
  54. package/dist/components/StatusBadge.svelte.d.ts +0 -5
  55. package/dist/components/Tabs.svelte +270 -0
  56. package/dist/components/Tabs.svelte.d.ts +15 -0
  57. package/dist/components/TextInput.svelte +138 -38
  58. package/dist/components/TextInput.svelte.d.ts +9 -1
  59. package/dist/components/VoicePlayer.svelte +51 -0
  60. package/dist/components/basic/Alert.svelte +36 -0
  61. package/dist/components/basic/Alert.svelte.d.ts +9 -0
  62. package/dist/components/basic/Breadcrumb.svelte +27 -0
  63. package/dist/components/basic/Breadcrumb.svelte.d.ts +11 -0
  64. package/dist/components/basic/ButtonGroup.svelte +15 -0
  65. package/dist/components/basic/ButtonGroup.svelte.d.ts +8 -0
  66. package/dist/components/basic/Checkbox.svelte +85 -0
  67. package/dist/components/basic/Checkbox.svelte.d.ts +15 -0
  68. package/dist/components/basic/Empty.svelte +17 -0
  69. package/dist/components/basic/Empty.svelte.d.ts +8 -0
  70. package/dist/components/basic/Kbd.svelte +16 -0
  71. package/dist/components/basic/Kbd.svelte.d.ts +8 -0
  72. package/dist/components/basic/Pagination.svelte +104 -0
  73. package/dist/components/basic/Pagination.svelte.d.ts +12 -0
  74. package/dist/components/basic/RadioGroup.svelte +71 -0
  75. package/dist/components/basic/RadioGroup.svelte.d.ts +18 -0
  76. package/dist/components/basic/Separator.svelte +19 -0
  77. package/dist/components/basic/Separator.svelte.d.ts +8 -0
  78. package/dist/components/basic/Slider.svelte +72 -0
  79. package/dist/components/basic/Slider.svelte.d.ts +17 -0
  80. package/dist/components/basic/Switch.svelte +88 -0
  81. package/dist/components/basic/Switch.svelte.d.ts +14 -0
  82. package/dist/components/basic/Toggle.svelte +43 -0
  83. package/dist/components/basic/Toggle.svelte.d.ts +14 -0
  84. package/dist/components/graph/GraphCanvas.svelte +103 -0
  85. package/dist/components/graph/GraphCanvas.svelte.d.ts +22 -0
  86. package/dist/components/graph/JatuiNode.svelte +128 -0
  87. package/dist/components/graph/JatuiNode.svelte.d.ts +14 -0
  88. package/dist/components/graph/theme.d.ts +27 -0
  89. package/dist/components/graph/theme.js +136 -0
  90. package/dist/components/messaging/MessageItem.svelte +92 -0
  91. package/dist/components/messaging/MessageItem.svelte.d.ts +5 -0
  92. package/dist/components/messaging/TypingIndicator.svelte +63 -0
  93. package/dist/components/messaging/TypingIndicator.svelte.d.ts +7 -0
  94. package/dist/components/pipeline/PipelineColumn.svelte +44 -3
  95. package/dist/components/progress/ProgressBar.svelte +189 -0
  96. package/dist/components/progress/ProgressBar.svelte.d.ts +17 -0
  97. package/dist/components/progress/ProgressRing.svelte +138 -0
  98. package/dist/components/progress/ProgressRing.svelte.d.ts +12 -0
  99. package/dist/components/progress/Steps.svelte +195 -0
  100. package/dist/components/progress/Steps.svelte.d.ts +14 -0
  101. package/dist/components/shell/NavigationMenu.svelte +422 -0
  102. package/dist/components/shell/NavigationMenu.svelte.d.ts +43 -0
  103. package/dist/components/shell/Sidebar.svelte +276 -0
  104. package/dist/components/shell/Sidebar.svelte.d.ts +32 -0
  105. package/dist/components/shell/SidebarGroup.svelte +180 -0
  106. package/dist/components/shell/SidebarGroup.svelte.d.ts +19 -0
  107. package/dist/components/shell/SidebarItem.svelte +170 -0
  108. package/dist/components/shell/SidebarItem.svelte.d.ts +24 -0
  109. package/dist/components/shell/shell.d.ts +30 -0
  110. package/dist/components/shell/shell.js +65 -0
  111. package/dist/components/table/DataTable.svelte +364 -0
  112. package/dist/components/table/DataTable.svelte.d.ts +4 -0
  113. package/dist/components/table/flexRender.d.ts +31 -0
  114. package/dist/components/table/flexRender.js +32 -0
  115. package/dist/components/table/types.d.ts +28 -0
  116. package/dist/components/table/types.js +1 -0
  117. package/dist/components/toast/ToastHost.svelte +21 -0
  118. package/dist/components/toast/ToastHost.svelte.d.ts +3 -0
  119. package/dist/components/toast/ToastItem.svelte +287 -0
  120. package/dist/components/toast/ToastItem.svelte.d.ts +9 -0
  121. package/dist/index.d.ts +59 -0
  122. package/dist/index.js +67 -0
  123. package/dist/showcase/AnimatedBeam.svelte +183 -0
  124. package/dist/showcase/AnimatedBeam.svelte.d.ts +26 -0
  125. package/dist/showcase/AuroraDrift.svelte +74 -0
  126. package/dist/showcase/AuroraDrift.svelte.d.ts +9 -0
  127. package/dist/showcase/BentoCard.svelte +50 -0
  128. package/dist/showcase/BentoCard.svelte.d.ts +14 -0
  129. package/dist/showcase/BentoGrid.svelte +43 -0
  130. package/dist/showcase/BentoGrid.svelte.d.ts +12 -0
  131. package/dist/showcase/BorderBeam.svelte +79 -0
  132. package/dist/showcase/BorderBeam.svelte.d.ts +12 -0
  133. package/dist/showcase/ClipWipe.svelte +82 -0
  134. package/dist/showcase/ClipWipe.svelte.d.ts +12 -0
  135. package/dist/showcase/Confetti.svelte +71 -0
  136. package/dist/showcase/Confetti.svelte.d.ts +10 -0
  137. package/dist/showcase/CursorSpotlight.svelte +85 -0
  138. package/dist/showcase/CursorSpotlight.svelte.d.ts +9 -0
  139. package/dist/showcase/DeviceMock.svelte +186 -0
  140. package/dist/showcase/DeviceMock.svelte.d.ts +11 -0
  141. package/dist/showcase/Dock.svelte +123 -0
  142. package/dist/showcase/Dock.svelte.d.ts +17 -0
  143. package/dist/showcase/FlipCard.svelte +103 -0
  144. package/dist/showcase/FlipCard.svelte.d.ts +10 -0
  145. package/dist/showcase/FocusReveal.svelte +83 -0
  146. package/dist/showcase/FocusReveal.svelte.d.ts +11 -0
  147. package/dist/showcase/Globe.svelte +202 -0
  148. package/dist/showcase/Globe.svelte.d.ts +16 -0
  149. package/dist/showcase/KenBurns.svelte +105 -0
  150. package/dist/showcase/KenBurns.svelte.d.ts +13 -0
  151. package/dist/showcase/Marquee.svelte +89 -0
  152. package/dist/showcase/Marquee.svelte.d.ts +9 -0
  153. package/dist/showcase/ParallaxTilt.svelte +98 -0
  154. package/dist/showcase/ParallaxTilt.svelte.d.ts +10 -0
  155. package/dist/showcase/ScrambleText.svelte +98 -0
  156. package/dist/showcase/ScrambleText.svelte.d.ts +11 -0
  157. package/dist/showcase/ShineSweep.svelte +81 -0
  158. package/dist/showcase/ShineSweep.svelte.d.ts +9 -0
  159. package/dist/showcase/Terminal.svelte +230 -0
  160. package/dist/showcase/Terminal.svelte.d.ts +18 -0
  161. package/dist/showcase/TextSplitReveal.svelte +146 -0
  162. package/dist/showcase/TextSplitReveal.svelte.d.ts +13 -0
  163. package/dist/showcase/TextWave.svelte +55 -0
  164. package/dist/showcase/TextWave.svelte.d.ts +8 -0
  165. package/dist/showcase/TypewriterText.svelte +130 -0
  166. package/dist/showcase/TypewriterText.svelte.d.ts +9 -0
  167. package/dist/showcase/backdrop/GrainGradient.svelte +126 -0
  168. package/dist/showcase/backdrop/GrainGradient.svelte.d.ts +23 -0
  169. package/dist/showcase/backdrop/MeshGradient.svelte +104 -0
  170. package/dist/showcase/backdrop/MeshGradient.svelte.d.ts +20 -0
  171. package/dist/showcase/backdrop/Metaballs.svelte +116 -0
  172. package/dist/showcase/backdrop/Metaballs.svelte.d.ts +18 -0
  173. package/dist/showcase/backdrop/NeuroNoise.svelte +115 -0
  174. package/dist/showcase/backdrop/NeuroNoise.svelte.d.ts +20 -0
  175. package/dist/showcase/backdrop/ShowcaseBackdrop.svelte +48 -0
  176. package/dist/showcase/backdrop/ShowcaseBackdrop.svelte.d.ts +12 -0
  177. package/dist/showcase/generative/GenerativeBackdrop.svelte +234 -0
  178. package/dist/showcase/generative/GenerativeBackdrop.svelte.d.ts +9 -0
  179. package/dist/showcase/generative/intent.d.ts +7 -0
  180. package/dist/showcase/generative/intent.js +67 -0
  181. package/dist/showcase/generative/manifest.d.ts +9 -0
  182. package/dist/showcase/generative/manifest.js +97 -0
  183. package/dist/showcase/generative/types.d.ts +48 -0
  184. package/dist/showcase/generative/types.js +8 -0
  185. package/dist/showcase/index.d.ts +36 -0
  186. package/dist/showcase/index.js +46 -0
  187. package/dist/showcase/scroll/SmoothScroll.svelte +49 -0
  188. package/dist/showcase/scroll/SmoothScroll.svelte.d.ts +12 -0
  189. package/dist/showcase/scroll/scrollProgress.d.ts +24 -0
  190. package/dist/showcase/scroll/scrollProgress.js +65 -0
  191. package/dist/showcase/scroll/scrollReveal.d.ts +30 -0
  192. package/dist/showcase/scroll/scrollReveal.js +70 -0
  193. package/dist/stores/commandPalette.svelte.d.ts +24 -0
  194. package/dist/stores/commandPalette.svelte.js +82 -0
  195. package/dist/stores/toast.svelte.d.ts +28 -0
  196. package/dist/stores/toast.svelte.js +33 -0
  197. package/dist/styles/motion.css +46 -0
  198. package/dist/styles/tokens.css +31 -0
  199. package/dist/transitions/glide.d.ts +31 -0
  200. package/dist/transitions/glide.js +58 -0
  201. package/dist/utils/sound.d.ts +6 -0
  202. package/dist/utils/sound.js +113 -0
  203. package/dist/utils/splitText.d.ts +25 -0
  204. package/dist/utils/splitText.js +94 -0
  205. package/dist/utils/themeWipe.d.ts +21 -0
  206. package/dist/utils/themeWipe.js +94 -0
  207. package/dist/utils/viewTransition.d.ts +23 -0
  208. package/dist/utils/viewTransition.js +75 -0
  209. 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
- /** When true, show an animated success check before the label (transitions.dev "Success check"). */
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: 'btn-primary',
59
+ primary: 'btn-primary',
38
60
  secondary: 'btn-secondary',
39
- outline: 'btn-outline',
40
- ghost: 'btn-ghost',
41
- error: 'btn-error',
42
- warning: 'btn-warning',
43
- success: 'btn-success',
44
- info: 'btn-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(`btn ${variantClass} ${sizeClass} ${className}`.trim());
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 {type} disabled={disabled || loading} aria-busy={loading || undefined} class={buttonClass} {onclick}>
68
- {#if loading}
69
- <span class="loading loading-spinner loading-xs"></span>
70
- {:else if success}
71
- <span class="t-success-check" data-state="in" aria-hidden="true">
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
- /** When true, show an animated success check before the label (transitions.dev "Success check"). */
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={handleKeydown}
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>