@joewinke/jatui 0.42.0 → 0.43.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 (45) hide show
  1. package/CLAUDE.md +23 -3
  2. package/THIRD_PARTY_NOTICES.md +14 -0
  3. package/dist/actions/dismissOutside.d.ts +52 -0
  4. package/dist/actions/dismissOutside.js +80 -0
  5. package/dist/components/FileDropzone.svelte +165 -3
  6. package/dist/components/FileDropzone.svelte.d.ts +9 -0
  7. package/dist/components/TextInput.svelte +71 -20
  8. package/dist/components/TextInput.svelte.d.ts +7 -0
  9. package/dist/components/ThumbnailGrid.svelte +106 -1
  10. package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
  11. package/dist/components/basic/Alert.svelte +113 -16
  12. package/dist/components/basic/Alert.svelte.d.ts +17 -1
  13. package/dist/components/basic/Checkbox.svelte +53 -1
  14. package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
  15. package/dist/components/basic/RadioGroup.svelte +59 -3
  16. package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
  17. package/dist/components/basic/Slider.svelte +257 -9
  18. package/dist/components/basic/Slider.svelte.d.ts +14 -2
  19. package/dist/components/basic/sliderMath.d.ts +29 -0
  20. package/dist/components/basic/sliderMath.js +64 -0
  21. package/dist/components/field/FieldHint.svelte +29 -0
  22. package/dist/components/field/FieldHint.svelte.d.ts +17 -0
  23. package/dist/components/field/FieldLabel.svelte +100 -0
  24. package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
  25. package/dist/components/form-cards/CheckGlyph.svelte +138 -0
  26. package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
  27. package/dist/components/form-cards/CheckboxCard.svelte +120 -0
  28. package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
  29. package/dist/components/form-cards/ChoiceCard.svelte +156 -0
  30. package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
  31. package/dist/components/form-cards/SwitchCard.svelte +114 -0
  32. package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
  33. package/dist/components/toast/ToastHost.svelte +56 -3
  34. package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
  35. package/dist/components/toast/ToastItem.svelte +75 -23
  36. package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
  37. package/dist/components/uploadProgress.d.ts +30 -0
  38. package/dist/components/uploadProgress.js +47 -0
  39. package/dist/index.d.ts +12 -2
  40. package/dist/index.js +11 -1
  41. package/dist/stores/toast.d.ts +35 -0
  42. package/dist/stores/toast.js +15 -1
  43. package/dist/styles/motion.css +72 -0
  44. package/dist/styles/rail.css +51 -0
  45. package/package.json +1 -1
@@ -1,21 +1,74 @@
1
+ <script module lang="ts">
2
+ export type ToastPosition =
3
+ | 'bottom-end'
4
+ | 'bottom-start'
5
+ | 'bottom-center'
6
+ | 'top-end'
7
+ | 'top-start'
8
+ | 'top-center';
9
+ </script>
10
+
1
11
  <script lang="ts">
2
12
  import { getToasts } from '../../stores/toast.js';
3
13
  import ToastItem from './ToastItem.svelte';
4
14
 
15
+ interface Props {
16
+ /**
17
+ * Viewport corner/edge. Logical: `end` is the right edge in LTR and the
18
+ * left edge in RTL. Default 'bottom-end' (the original bottom-right).
19
+ */
20
+ position?: ToastPosition;
21
+ }
22
+
23
+ let { position = 'bottom-end' }: Props = $props();
24
+
5
25
  const toasts = $derived(getToasts());
26
+ const edge = $derived<'top' | 'bottom'>(position.startsWith('top') ? 'top' : 'bottom');
6
27
  </script>
7
28
 
8
29
  <!--
9
30
  Mount once in your app root (e.g. +layout.svelte).
10
- Fixed bottom-right, pointer-events only on the toast list.
31
+ Fixed to a viewport corner, pointer-events only on the toast list.
32
+ Newest toast sits on top of the stack; at top positions that puts it
33
+ nearest the edge, so the stack reads newest-first downward.
11
34
  -->
12
35
  <div
13
36
  aria-label="Notifications"
14
- class="toast-host pointer-events-none fixed right-4 bottom-4 z-[9999] flex flex-col-reverse items-end gap-2 w-80 max-w-[calc(100vw-2rem)]"
37
+ class="toast-host toast-host--{position} pointer-events-none fixed z-[9999] flex flex-col-reverse gap-2 w-80 max-w-[calc(100vw-2rem)]"
38
+ data-position={position}
15
39
  >
16
40
  <ul class="contents">
17
41
  {#each toasts as toast, i (toast.id)}
18
- <ToastItem {toast} index={i} total={toasts.length} />
42
+ <ToastItem {toast} index={i} total={toasts.length} {edge} />
19
43
  {/each}
20
44
  </ul>
21
45
  </div>
46
+
47
+ <style>
48
+ .toast-host--bottom-end,
49
+ .toast-host--bottom-start,
50
+ .toast-host--bottom-center {
51
+ bottom: 1rem;
52
+ }
53
+ .toast-host--top-end,
54
+ .toast-host--top-start,
55
+ .toast-host--top-center {
56
+ top: 1rem;
57
+ }
58
+ .toast-host--bottom-end,
59
+ .toast-host--top-end {
60
+ inset-inline-end: 1rem;
61
+ align-items: flex-end;
62
+ }
63
+ .toast-host--bottom-start,
64
+ .toast-host--top-start {
65
+ inset-inline-start: 1rem;
66
+ align-items: flex-start;
67
+ }
68
+ .toast-host--bottom-center,
69
+ .toast-host--top-center {
70
+ inset-inline: 0;
71
+ margin-inline: auto;
72
+ align-items: center;
73
+ }
74
+ </style>
@@ -1,3 +1,11 @@
1
- declare const ToastHost: import("svelte").Component<Record<string, never>, {}, "">;
1
+ export type ToastPosition = 'bottom-end' | 'bottom-start' | 'bottom-center' | 'top-end' | 'top-start' | 'top-center';
2
+ interface Props {
3
+ /**
4
+ * Viewport corner/edge. Logical: `end` is the right edge in LTR and the
5
+ * left edge in RTL. Default 'bottom-end' (the original bottom-right).
6
+ */
7
+ position?: ToastPosition;
8
+ }
9
+ declare const ToastHost: import("svelte").Component<Props, {}, "">;
2
10
  type ToastHost = ReturnType<typeof ToastHost>;
3
11
  export default ToastHost;
@@ -1,13 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { dismissToast, type Toast } from '../../stores/toast.js';
3
+ import CloseButton from '../CloseButton.svelte';
3
4
 
4
5
  type Props = {
5
6
  toast: Toast;
6
7
  index: number;
7
8
  total: number;
9
+ /** Which viewport edge the host is pinned to — sets the entrance direction. */
10
+ edge?: 'top' | 'bottom';
8
11
  };
9
12
 
10
- let { toast, index, total }: Props = $props();
13
+ let { toast, index, total, edge = 'bottom' }: Props = $props();
14
+ const enterSign = $derived(edge === 'top' ? -1 : 1);
11
15
 
12
16
  // Stack compression: older toasts (lower index) scale down and fade behind newer ones
13
17
  // index 0 = oldest (back), index total-1 = newest (front)
@@ -172,10 +176,10 @@ const detailsId = $derived(`toast-details-${toast.id}`);
172
176
  isDragging
173
177
  ? `translateX(${dragX}px) rotate(${dragX * 0.02}deg)`
174
178
  : exiting
175
- ? `translateY(20px) scale(0.9)`
179
+ ? `translateY(${20 * enterSign}px) scale(0.9)`
176
180
  : mounted
177
181
  ? `translateX(0) rotate(0deg)`
178
- : `translateY(40px) scale(0.92)`
182
+ : `translateY(${40 * enterSign}px) scale(0.92)`
179
183
  );
180
184
 
181
185
  const itemOpacity = $derived(
@@ -192,13 +196,25 @@ const detailsId = $derived(`toast-details-${toast.id}`);
192
196
  isDragging
193
197
  ? 'none'
194
198
  : exiting
195
- ? 'transform var(--motion-enter) var(--motion-glide), opacity var(--motion-enter) var(--motion-glide)'
196
- : 'transform var(--motion-enter) var(--motion-spring), opacity var(--motion-fast) var(--motion-glide)'
199
+ ? 'transform var(--motion-enter) var(--motion-glide), opacity var(--motion-enter) var(--motion-glide), filter var(--motion-enter) var(--motion-glide)'
200
+ : 'transform var(--motion-enter) var(--motion-spring), opacity var(--motion-fast) var(--motion-glide), filter var(--motion-enter) var(--motion-glide)'
201
+ );
202
+
203
+ // Blur on enter/exit (BoardUI notification idea). The class only marks the
204
+ // hidden state; CSS drops the blur under data-motion="calm" and reduced motion.
205
+ const veiled = $derived(!isDragging && (exiting || !mounted));
206
+
207
+ const titleId = $derived(`toast-title-${toast.id}`);
208
+ // Legacy toasts from an older store shape may lack the normalised list.
209
+ const actionList = $derived(
210
+ toast.actions ?? (toast.action ? [{ ...toast.action, variant: 'ghost' as const }] : [])
197
211
  );
198
212
  </script>
199
213
 
200
214
  <li
201
215
  class="toast-item pointer-events-auto w-full"
216
+ class:toast-veiled={veiled}
217
+ aria-labelledby={toast.title ? titleId : undefined}
202
218
  style:transform={itemTransform}
203
219
  style:opacity={itemOpacity}
204
220
  style:transition={dragTransition}
@@ -219,6 +235,27 @@ const detailsId = $derived(`toast-details-${toast.id}`);
219
235
  style:transition="transform var(--motion-move) var(--motion-glide), opacity var(--motion-move) var(--motion-glide)"
220
236
  >
221
237
  <!-- Undo countdown bar -->
238
+ {#if toast.showProgress && !toast.onUndo}
239
+ <span
240
+ class="toast-progress absolute bottom-0 left-0 h-0.5 w-full origin-left bg-current opacity-40"
241
+ style:animation-duration="{toast.durationMs}ms"
242
+ style:animation-play-state={paused ? 'paused' : 'running'}
243
+ aria-hidden="true"
244
+ ></span>
245
+ {/if}
246
+
247
+ {#if toast.avatar}
248
+ <img
249
+ src={toast.avatar.src}
250
+ alt={toast.avatar.alt}
251
+ class="toast-avatar size-8 shrink-0 self-start rounded-full object-cover"
252
+ />
253
+ {:else if toast.icon}
254
+ <span class="toast-icon flex size-5 shrink-0 items-center justify-center self-start" aria-hidden="true">
255
+ {#if typeof toast.icon === 'string'}{toast.icon}{:else}{@render toast.icon()}{/if}
256
+ </span>
257
+ {/if}
258
+
222
259
  {#if toast.onUndo && !undoDone}
223
260
  <span
224
261
  class="toast-progress absolute bottom-0 left-0 h-0.5 w-full origin-left bg-current opacity-40"
@@ -228,7 +265,10 @@ const detailsId = $derived(`toast-details-${toast.id}`);
228
265
  {/if}
229
266
 
230
267
  <div class="flex-1 min-w-0">
231
- <span class="text-sm">{toast.message}</span>
268
+ {#if toast.title}
269
+ <p id={titleId} class="toast-title text-sm font-semibold">{toast.title}</p>
270
+ {/if}
271
+ <span class="text-sm" class:opacity-80={!!toast.title}>{toast.message}</span>
232
272
 
233
273
  {#if toast.details}
234
274
  <button
@@ -270,26 +310,26 @@ const detailsId = $derived(`toast-details-${toast.id}`);
270
310
  >
271
311
  Undo
272
312
  </button>
273
- {:else if toast.action}
274
- <button
275
- type="button"
276
- class="btn btn-ghost btn-xs font-semibold"
277
- onclick={() => { toast.action!.onClick(); triggerDismiss(); }}
278
- >
279
- {toast.action.label}
280
- </button>
313
+ {:else if actionList.length > 0}
314
+ <div class="toast-actions flex shrink-0 flex-wrap items-center gap-1">
315
+ {#each actionList as act, ai (ai)}
316
+ <button
317
+ type="button"
318
+ class="btn btn-xs font-semibold {act.variant === 'primary' ? 'btn-primary' : 'btn-ghost'}"
319
+ onclick={() => { act.onClick(); triggerDismiss(); }}
320
+ >
321
+ {act.label}
322
+ </button>
323
+ {/each}
324
+ </div>
281
325
  {/if}
282
326
 
283
- <button
284
- type="button"
285
- class="btn btn-ghost btn-xs btn-square"
286
- aria-label="Dismiss notification"
327
+ <CloseButton
328
+ label="Dismiss notification"
329
+ size="xs"
330
+ shape="square"
287
331
  onclick={triggerDismiss}
288
- >
289
- <svg xmlns="http://www.w3.org/2000/svg" class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
290
- <path d="M18 6L6 18M6 6l12 12"/>
291
- </svg>
292
- </button>
332
+ />
293
333
  </div>
294
334
  </li>
295
335
 
@@ -300,6 +340,14 @@ const detailsId = $derived(`toast-details-${toast.id}`);
300
340
  user-select: none;
301
341
  }
302
342
 
343
+ .toast-item.toast-veiled {
344
+ filter: blur(4px);
345
+ }
346
+
347
+ :global([data-motion='calm']) .toast-item.toast-veiled {
348
+ filter: none;
349
+ }
350
+
303
351
  .toast-progress {
304
352
  animation-name: drain;
305
353
  animation-timing-function: linear;
@@ -328,5 +376,9 @@ const detailsId = $derived(`toast-details-${toast.id}`);
328
376
  .toast-item > div {
329
377
  transition: none !important;
330
378
  }
379
+
380
+ .toast-item.toast-veiled {
381
+ filter: none;
382
+ }
331
383
  }
332
384
  </style>
@@ -3,6 +3,8 @@ type Props = {
3
3
  toast: Toast;
4
4
  index: number;
5
5
  total: number;
6
+ /** Which viewport edge the host is pinned to — sets the entrance direction. */
7
+ edge?: 'top' | 'bottom';
6
8
  };
7
9
  declare const ToastItem: import("svelte").Component<Props, {}, "">;
8
10
  type ToastItem = ReturnType<typeof ToastItem>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pure math behind the upload-progress strokes on FileDropzone (perimeter
3
+ * border) and ThumbnailGrid (per-tile ring). DOM-free so it is unit-testable
4
+ * under node:test (tests/unit/uploadProgress.test.ts).
5
+ */
6
+ /** Clamp a 0–100 progress value; `null`/`undefined`/NaN mean "no progress shown". */
7
+ export declare function clampProgress(value: number | null | undefined): number | null;
8
+ /** True while an upload is in flight (a progress value below 100). */
9
+ export declare function isInFlight(value: number | null | undefined): boolean;
10
+ /** Whole-number percent for the badge / aria-valuenow. Floors so 99.6 never reads "100". */
11
+ export declare function progressPercent(value: number | null | undefined): number;
12
+ /**
13
+ * stroke-dashoffset for a path drawn with `pathLength={length}` and
14
+ * `stroke-dasharray={length}`: 0 = fully drawn, `length` = empty.
15
+ */
16
+ export declare function dashOffset(value: number | null | undefined, length?: number): number;
17
+ export interface PerimeterRect {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ rx: number;
23
+ }
24
+ /**
25
+ * Geometry for an SVG `<rect>` whose stroke sits exactly on a CSS border of
26
+ * width `stroke` around a `width`×`height` border-box with `radius` corners.
27
+ * The stroke is centred on the path, so the rect is inset by half a stroke and
28
+ * the corner radius shrinks by the same amount (never below 0).
29
+ */
30
+ export declare function perimeterRect(width: number, height: number, stroke?: number, radius?: number): PerimeterRect;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Pure math behind the upload-progress strokes on FileDropzone (perimeter
3
+ * border) and ThumbnailGrid (per-tile ring). DOM-free so it is unit-testable
4
+ * under node:test (tests/unit/uploadProgress.test.ts).
5
+ */
6
+ /** Clamp a 0–100 progress value; `null`/`undefined`/NaN mean "no progress shown". */
7
+ export function clampProgress(value) {
8
+ if (value == null || Number.isNaN(value))
9
+ return null;
10
+ return Math.min(100, Math.max(0, value));
11
+ }
12
+ /** True while an upload is in flight (a progress value below 100). */
13
+ export function isInFlight(value) {
14
+ const p = clampProgress(value);
15
+ return p !== null && p < 100;
16
+ }
17
+ /** Whole-number percent for the badge / aria-valuenow. Floors so 99.6 never reads "100". */
18
+ export function progressPercent(value) {
19
+ const p = clampProgress(value);
20
+ if (p === null)
21
+ return 0;
22
+ return p >= 100 ? 100 : Math.floor(p);
23
+ }
24
+ /**
25
+ * stroke-dashoffset for a path drawn with `pathLength={length}` and
26
+ * `stroke-dasharray={length}`: 0 = fully drawn, `length` = empty.
27
+ */
28
+ export function dashOffset(value, length = 100) {
29
+ const p = clampProgress(value) ?? 0;
30
+ return length - (p / 100) * length;
31
+ }
32
+ /**
33
+ * Geometry for an SVG `<rect>` whose stroke sits exactly on a CSS border of
34
+ * width `stroke` around a `width`×`height` border-box with `radius` corners.
35
+ * The stroke is centred on the path, so the rect is inset by half a stroke and
36
+ * the corner radius shrinks by the same amount (never below 0).
37
+ */
38
+ export function perimeterRect(width, height, stroke = 2, radius = 8) {
39
+ const inset = stroke / 2;
40
+ return {
41
+ x: inset,
42
+ y: inset,
43
+ width: Math.max(0, width - stroke),
44
+ height: Math.max(0, height - stroke),
45
+ rx: Math.max(0, radius - inset)
46
+ };
47
+ }
package/dist/index.d.ts CHANGED
@@ -252,8 +252,9 @@ export { copyText, readClipboardText } from './utils/clipboard';
252
252
  export { splitText } from './utils/splitText';
253
253
  export type { SplitBy, SplitTextOptions, SplitTextResult } from './utils/splitText';
254
254
  export { default as ToastHost } from './components/toast/ToastHost.svelte';
255
- export { showToast, dismissToast, getToasts } from './stores/toast';
256
- export type { Toast, ToastType, ToastOptions, DismissHandle } from './stores/toast';
255
+ export { showToast, dismissToast, getToasts, normalizeToastActions } from './stores/toast';
256
+ export type { Toast, ToastType, ToastOptions, DismissHandle, ToastAction, ToastAvatar } from './stores/toast';
257
+ export type { ToastPosition } from './components/toast/ToastHost.svelte';
257
258
  export { default as Skeleton } from './components/Skeleton.svelte';
258
259
  export { default as ProgressBar } from './components/progress/ProgressBar.svelte';
259
260
  export { default as ProgressRing } from './components/progress/ProgressRing.svelte';
@@ -302,6 +303,15 @@ export { default as SectionNav } from './components/basic/SectionNav.svelte';
302
303
  export { computeActiveSection, DEFAULT_ACTIVATION_RATIO } from './components/basic/sectionNavSpy';
303
304
  export type { SectionRect, SpyViewport, SpyScrollState } from './components/basic/sectionNavSpy';
304
305
  export { default as Slider } from './components/basic/Slider.svelte';
306
+ export { snapToStep, moveThumb, normalizePair, percentOf, bubbleLeft } from './components/basic/sliderMath';
307
+ export { default as CheckGlyph } from './components/form-cards/CheckGlyph.svelte';
308
+ export { default as ChoiceCard } from './components/form-cards/ChoiceCard.svelte';
309
+ export { default as CheckboxCard } from './components/form-cards/CheckboxCard.svelte';
310
+ export { default as SwitchCard } from './components/form-cards/SwitchCard.svelte';
311
+ export { default as FieldLabel } from './components/field/FieldLabel.svelte';
312
+ export { default as FieldHint } from './components/field/FieldHint.svelte';
313
+ export { dismissOutside, createDismissOutside, isInsidePath } from './actions/dismissOutside';
314
+ export type { DismissOutsideOptions, DismissOutsideController, DismissReason } from './actions/dismissOutside';
305
315
  export { default as Pagination } from './components/basic/Pagination.svelte';
306
316
  export { MAX_UNDO_DEPTH, createHistory, pushState, beginChange, endChange, untracked, undo, redo, clearHistory } from './history';
307
317
  export type { History } from './history';
package/dist/index.js CHANGED
@@ -294,7 +294,7 @@ export { copyText, readClipboardText } from './utils/clipboard';
294
294
  export { splitText } from './utils/splitText';
295
295
  // Toast system
296
296
  export { default as ToastHost } from './components/toast/ToastHost.svelte';
297
- export { showToast, dismissToast, getToasts } from './stores/toast';
297
+ export { showToast, dismissToast, getToasts, normalizeToastActions } from './stores/toast';
298
298
  // Loading & progress family
299
299
  export { default as Skeleton } from './components/Skeleton.svelte';
300
300
  export { default as ProgressBar } from './components/progress/ProgressBar.svelte';
@@ -351,6 +351,16 @@ export { default as Separator } from './components/basic/Separator.svelte';
351
351
  export { default as SectionNav } from './components/basic/SectionNav.svelte';
352
352
  export { computeActiveSection, DEFAULT_ACTIVATION_RATIO } from './components/basic/sectionNavSpy';
353
353
  export { default as Slider } from './components/basic/Slider.svelte';
354
+ export { snapToStep, moveThumb, normalizePair, percentOf, bubbleLeft } from './components/basic/sliderMath';
355
+ // BoardUI wave 5 — form polish (jatui-bns3w): choice cards + animated tick,
356
+ // shared field label/hint parts, dismissOutside.
357
+ export { default as CheckGlyph } from './components/form-cards/CheckGlyph.svelte';
358
+ export { default as ChoiceCard } from './components/form-cards/ChoiceCard.svelte';
359
+ export { default as CheckboxCard } from './components/form-cards/CheckboxCard.svelte';
360
+ export { default as SwitchCard } from './components/form-cards/SwitchCard.svelte';
361
+ export { default as FieldLabel } from './components/field/FieldLabel.svelte';
362
+ export { default as FieldHint } from './components/field/FieldHint.svelte';
363
+ export { dismissOutside, createDismissOutside, isInsidePath } from './actions/dismissOutside';
354
364
  export { default as Pagination } from './components/basic/Pagination.svelte';
355
365
  // Snapshot undo/redo history (wayflow-core) — also importable as 'jatui/history' (jatui-00jfh).
356
366
  export { MAX_UNDO_DEPTH, createHistory, pushState, beginChange, endChange, untracked, undo, redo, clearHistory } from './history';
@@ -1,36 +1,71 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export type ToastType = 'info' | 'success' | 'warning' | 'error';
3
+ /** One button on a toast. `variant` 'primary' renders a filled button; the
4
+ * default 'ghost' matches the legacy single-action button. */
5
+ export type ToastAction = {
6
+ label: string;
7
+ onClick: () => void;
8
+ variant?: 'primary' | 'ghost';
9
+ };
10
+ /** Image shown in place of the status icon (e.g. the sender of a message). */
11
+ export type ToastAvatar = {
12
+ src: string;
13
+ alt: string;
14
+ };
2
15
  export type ToastOptions = {
3
16
  type?: ToastType;
4
17
  message: string;
18
+ /** Bold heading above `message`. Optional — without it the toast renders
19
+ * exactly as before. */
20
+ title?: string;
21
+ /** Leading icon. A string renders as a glyph/emoji; a Snippet renders as-is.
22
+ * Ignored when `avatar` is set. */
23
+ icon?: Snippet | string;
24
+ /** Leading avatar image (takes precedence over `icon`). */
25
+ avatar?: ToastAvatar;
5
26
  /** Extended detail text (e.g. a stack trace or error chain) — rendered by
6
27
  * ToastHost behind a collapsible "Details" toggle. Multiline-safe. */
7
28
  details?: string;
8
29
  /** Arbitrary structured context carried on the Toast for programmatic
9
30
  * access (getToasts()); not rendered by ToastHost. */
10
31
  meta?: Record<string, unknown>;
32
+ /** Legacy single action. Still supported; folded into `actions`. */
11
33
  action?: {
12
34
  label: string;
13
35
  onClick: () => void;
14
36
  };
37
+ /** Several action buttons, rendered in order after the message. */
38
+ actions?: ToastAction[];
15
39
  durationMs?: number;
16
40
  onUndo?: () => void;
41
+ /** Show a draining countdown bar on a timed (non-undo) toast. It pauses on
42
+ * hover/focus together with the timer. Default false. */
43
+ showProgress?: boolean;
17
44
  };
18
45
  export type Toast = {
19
46
  id: number;
20
47
  type: ToastType;
21
48
  message: string;
49
+ title?: string;
50
+ icon?: Snippet | string;
51
+ avatar?: ToastAvatar;
22
52
  details?: string;
23
53
  meta?: Record<string, unknown>;
24
54
  action?: {
25
55
  label: string;
26
56
  onClick: () => void;
27
57
  };
58
+ /** Normalised list: `actions` followed by the legacy `action` (if any). */
59
+ actions: ToastAction[];
28
60
  durationMs: number;
29
61
  onUndo?: () => void;
62
+ showProgress: boolean;
30
63
  };
31
64
  export type DismissHandle = {
32
65
  dismiss: () => void;
33
66
  };
34
67
  export declare function getToasts(): Toast[];
68
+ /** Merge the legacy single `action` with `actions[]` (pure; exported for tests). */
69
+ export declare function normalizeToastActions(options: Pick<ToastOptions, 'action' | 'actions'>): ToastAction[];
35
70
  export declare function showToast(options: ToastOptions): DismissHandle;
36
71
  export declare function dismissToast(id: number): void;
@@ -8,6 +8,15 @@ export function getToasts() {
8
8
  return $.get(toasts);
9
9
  }
10
10
 
11
+ /** Merge the legacy single `action` with `actions[]` (pure; exported for tests). */
12
+ export function normalizeToastActions(options) {
13
+ const list = [...options.actions ?? []];
14
+
15
+ if (options.action) list.push({ ...options.action, variant: 'ghost' });
16
+
17
+ return list;
18
+ }
19
+
11
20
  export function showToast(options) {
12
21
  const id = ++nextId;
13
22
 
@@ -15,11 +24,16 @@ export function showToast(options) {
15
24
  id,
16
25
  type: options.type ?? 'info',
17
26
  message: options.message,
27
+ title: options.title,
28
+ icon: options.icon,
29
+ avatar: options.avatar,
18
30
  details: options.details,
19
31
  meta: options.meta,
20
32
  action: options.action,
33
+ actions: normalizeToastActions(options),
21
34
  durationMs: options.durationMs ?? 5000,
22
- onUndo: options.onUndo
35
+ onUndo: options.onUndo,
36
+ showProgress: options.showProgress ?? false
23
37
  };
24
38
 
25
39
  $.set(toasts, [...$.get(toasts), toast], true);
@@ -223,3 +223,75 @@
223
223
  opacity: 1;
224
224
  }
225
225
  }
226
+
227
+ /*
228
+ * One-shot utility animations (BoardUI wave 5, jatui-bns3w). All on the
229
+ * tokens above, so they scale with data-motion and collapse under reduced
230
+ * motion. Ideas after BoardUI's globals.css (MIT © 2026 Mertcan Dundar
231
+ * Esmergul).
232
+ *
233
+ * .jatui-check-draw — an SVG stroke with pathLength="1" draws itself on
234
+ * (the CheckGlyph tick carries its own copy).
235
+ * .jatui-row-exit — a list row slides aside and collapses its height;
236
+ * remove it on `animationend` (bulk-delete flows).
237
+ * .jatui-label-out / .jatui-label-in — a button label swap: old text
238
+ * lifts out, new text rises in.
239
+ */
240
+ .jatui-check-draw {
241
+ stroke-dasharray: 1;
242
+ stroke-dashoffset: 1;
243
+ animation: jatui-check-draw var(--motion-fast) var(--motion-glide) forwards;
244
+ }
245
+
246
+ @keyframes jatui-check-draw {
247
+ to {
248
+ stroke-dashoffset: 0;
249
+ }
250
+ }
251
+
252
+ .jatui-row-exit {
253
+ overflow: hidden;
254
+ animation: jatui-row-exit var(--motion-move) var(--motion-standard) forwards;
255
+ }
256
+
257
+ @keyframes jatui-row-exit {
258
+ 0% {
259
+ opacity: 1;
260
+ transform: translateX(0);
261
+ max-height: var(--jatui-row-h, 4rem);
262
+ }
263
+ 50% {
264
+ opacity: 0;
265
+ transform: translateX(calc(var(--motion-travel) * 1.5));
266
+ max-height: var(--jatui-row-h, 4rem);
267
+ }
268
+ 100% {
269
+ opacity: 0;
270
+ transform: translateX(calc(var(--motion-travel) * 1.5));
271
+ max-height: 0;
272
+ padding-block: 0;
273
+ margin-block: 0;
274
+ }
275
+ }
276
+
277
+ .jatui-label-out {
278
+ animation: jatui-label-out var(--motion-fast) var(--motion-glide) forwards;
279
+ }
280
+
281
+ .jatui-label-in {
282
+ animation: jatui-label-in var(--motion-enter) var(--motion-glide) both;
283
+ }
284
+
285
+ @keyframes jatui-label-out {
286
+ to {
287
+ opacity: 0;
288
+ transform: translateY(calc(var(--motion-travel) * -0.5));
289
+ }
290
+ }
291
+
292
+ @keyframes jatui-label-in {
293
+ from {
294
+ opacity: 0;
295
+ transform: translateY(calc(var(--motion-travel) * 0.5));
296
+ }
297
+ }
@@ -74,3 +74,54 @@
74
74
  line-height: 1rem; /* 16px */
75
75
  letter-spacing: 0.01em;
76
76
  }
77
+
78
+ /* ─── Type ramp — composite text utilities (BoardUI wave 5) ──────────────
79
+ Each class sets size + line-height + tracking + weight together, so a
80
+ caption is one class instead of `text-xs font-medium tracking-wide`.
81
+ Deliberately small (six steps), not a Figma matrix. Colour is left to
82
+ the caller (text-base-content/60 etc.). Idea after BoardUI's
83
+ typography.css (MIT © 2026 Mertcan Dundar Esmergul).
84
+ ─────────────────────────────────────────────────────────────────────── */
85
+
86
+ .text-jatui-caption {
87
+ font-size: 0.75rem; /* 12px */
88
+ line-height: 1rem;
89
+ letter-spacing: 0.01em;
90
+ font-weight: 400;
91
+ }
92
+
93
+ .text-jatui-label {
94
+ font-size: 0.6875rem; /* 11px — uppercase instrument label */
95
+ line-height: 1rem;
96
+ letter-spacing: 0.08em;
97
+ font-weight: 600;
98
+ text-transform: uppercase;
99
+ }
100
+
101
+ .text-jatui-body {
102
+ font-size: 0.875rem; /* 14px */
103
+ line-height: 1.25rem;
104
+ letter-spacing: 0;
105
+ font-weight: 400;
106
+ }
107
+
108
+ .text-jatui-body-strong {
109
+ font-size: 0.875rem;
110
+ line-height: 1.25rem;
111
+ letter-spacing: 0;
112
+ font-weight: 600;
113
+ }
114
+
115
+ .text-jatui-title {
116
+ font-size: 1.125rem; /* 18px */
117
+ line-height: 1.5rem;
118
+ letter-spacing: -0.01em;
119
+ font-weight: 600;
120
+ }
121
+
122
+ .text-jatui-display {
123
+ font-size: 1.875rem; /* 30px */
124
+ line-height: 2.25rem;
125
+ letter-spacing: -0.02em;
126
+ font-weight: 700;
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",