@joewinke/jatui 0.42.0 → 0.44.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 (49) hide show
  1. package/CLAUDE.md +24 -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/ImageReveal.svelte +353 -0
  8. package/dist/components/ImageReveal.svelte.d.ts +31 -0
  9. package/dist/components/TextInput.svelte +71 -20
  10. package/dist/components/TextInput.svelte.d.ts +7 -0
  11. package/dist/components/ThumbnailGrid.svelte +106 -1
  12. package/dist/components/ThumbnailGrid.svelte.d.ts +7 -0
  13. package/dist/components/basic/Alert.svelte +113 -16
  14. package/dist/components/basic/Alert.svelte.d.ts +17 -1
  15. package/dist/components/basic/Checkbox.svelte +53 -1
  16. package/dist/components/basic/Checkbox.svelte.d.ts +6 -0
  17. package/dist/components/basic/RadioGroup.svelte +59 -3
  18. package/dist/components/basic/RadioGroup.svelte.d.ts +15 -0
  19. package/dist/components/basic/Slider.svelte +257 -9
  20. package/dist/components/basic/Slider.svelte.d.ts +14 -2
  21. package/dist/components/basic/sliderMath.d.ts +29 -0
  22. package/dist/components/basic/sliderMath.js +64 -0
  23. package/dist/components/field/FieldHint.svelte +29 -0
  24. package/dist/components/field/FieldHint.svelte.d.ts +17 -0
  25. package/dist/components/field/FieldLabel.svelte +100 -0
  26. package/dist/components/field/FieldLabel.svelte.d.ts +29 -0
  27. package/dist/components/form-cards/CheckGlyph.svelte +138 -0
  28. package/dist/components/form-cards/CheckGlyph.svelte.d.ts +29 -0
  29. package/dist/components/form-cards/CheckboxCard.svelte +120 -0
  30. package/dist/components/form-cards/CheckboxCard.svelte.d.ts +33 -0
  31. package/dist/components/form-cards/ChoiceCard.svelte +156 -0
  32. package/dist/components/form-cards/ChoiceCard.svelte.d.ts +42 -0
  33. package/dist/components/form-cards/SwitchCard.svelte +114 -0
  34. package/dist/components/form-cards/SwitchCard.svelte.d.ts +28 -0
  35. package/dist/components/imageReveal.d.ts +51 -0
  36. package/dist/components/imageReveal.js +83 -0
  37. package/dist/components/toast/ToastHost.svelte +56 -3
  38. package/dist/components/toast/ToastHost.svelte.d.ts +9 -1
  39. package/dist/components/toast/ToastItem.svelte +75 -23
  40. package/dist/components/toast/ToastItem.svelte.d.ts +2 -0
  41. package/dist/components/uploadProgress.d.ts +30 -0
  42. package/dist/components/uploadProgress.js +47 -0
  43. package/dist/index.d.ts +15 -2
  44. package/dist/index.js +13 -1
  45. package/dist/stores/toast.d.ts +35 -0
  46. package/dist/stores/toast.js +15 -1
  47. package/dist/styles/motion.css +72 -0
  48. package/dist/styles/rail.css +51 -0
  49. package/package.json +1 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Pure helpers behind ImageReveal: which phase the frame is in, the initials
3
+ * fallback, and the size / shape / tone lookups. No DOM, so
4
+ * tests/unit/imageReveal.test.ts runs them in node.
5
+ *
6
+ * Clean-room (jatui-vui9q): BoardUI's "AI Image Generation" item is in the
7
+ * Pro tier, so no BoardUI source was read or copied.
8
+ */
9
+ /**
10
+ * generating — no src yet (`null`/`undefined`): the ripple runs.
11
+ * loading — a src arrived but the bytes haven't decoded: the ripple keeps
12
+ * running, the image waits underneath at blur + opacity 0.
13
+ * revealed — the image loaded: blur-to-sharp, the ripple fades out.
14
+ * fallback — `src === ''` or the image failed: calm initials / empty glyph.
15
+ */
16
+ export type ImageRevealPhase = 'generating' | 'loading' | 'revealed' | 'fallback';
17
+ export type ImageRevealShape = 'square' | 'rounded' | 'circle';
18
+ export type ImageRevealSizeName = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'fill';
19
+ /** DaisyUI semantic colour names accepted as a `tone`. */
20
+ export type ImageRevealToneName = 'primary' | 'secondary' | 'accent' | 'neutral' | 'info' | 'success' | 'warning' | 'error';
21
+ export interface ImageRevealState {
22
+ src: string | null | undefined;
23
+ /** The src whose load event fired (null if none has). */
24
+ loadedSrc: string | null;
25
+ /** The src whose error event fired (null if none has). */
26
+ failedSrc: string | null;
27
+ }
28
+ /**
29
+ * Phase from the current src plus what the browser reported for it. Load and
30
+ * error are keyed by src, so a new src starts over at `loading` without the
31
+ * component having to reset flags.
32
+ */
33
+ export declare function imageRevealPhase({ src, loadedSrc, failedSrc }: ImageRevealState): ImageRevealPhase;
34
+ /** True while the frame is still waiting for something to show. */
35
+ export declare function imageRevealBusy(phase: ImageRevealPhase): boolean;
36
+ /**
37
+ * Up to two letters from the alt text: "Acme Corp" → "AC", "acme" → "A",
38
+ * "the-north-face" → "TN". Symbols and digits-only words are skipped, so a
39
+ * logo alt like "Acme logo" gives "AL" — callers who want "A" pass
40
+ * `initials` explicitly.
41
+ */
42
+ export declare function imageRevealInitials(alt: string | null | undefined): string;
43
+ export declare const IMAGE_REVEAL_SIZES: Record<Exclude<ImageRevealSizeName, 'fill'>, number>;
44
+ /**
45
+ * A CSS length for the frame's width. Named sizes and numbers are px; any
46
+ * other string (`'10rem'`, `'50%'`) passes through; `'fill'` is 100%.
47
+ */
48
+ export declare function imageRevealSize(size: ImageRevealSizeName | number | (string & {})): string;
49
+ export declare const IMAGE_REVEAL_RADIUS: Record<ImageRevealShape, string>;
50
+ /** A semantic name → its DaisyUI variable; any other string is a CSS colour. */
51
+ export declare function imageRevealTone(tone: ImageRevealToneName | (string & {})): string;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Pure helpers behind ImageReveal: which phase the frame is in, the initials
3
+ * fallback, and the size / shape / tone lookups. No DOM, so
4
+ * tests/unit/imageReveal.test.ts runs them in node.
5
+ *
6
+ * Clean-room (jatui-vui9q): BoardUI's "AI Image Generation" item is in the
7
+ * Pro tier, so no BoardUI source was read or copied.
8
+ */
9
+ /**
10
+ * Phase from the current src plus what the browser reported for it. Load and
11
+ * error are keyed by src, so a new src starts over at `loading` without the
12
+ * component having to reset flags.
13
+ */
14
+ export function imageRevealPhase({ src, loadedSrc, failedSrc }) {
15
+ if (src == null)
16
+ return 'generating';
17
+ if (src.trim() === '' || failedSrc === src)
18
+ return 'fallback';
19
+ if (loadedSrc === src)
20
+ return 'revealed';
21
+ return 'loading';
22
+ }
23
+ /** True while the frame is still waiting for something to show. */
24
+ export function imageRevealBusy(phase) {
25
+ return phase === 'generating' || phase === 'loading';
26
+ }
27
+ /**
28
+ * Up to two letters from the alt text: "Acme Corp" → "AC", "acme" → "A",
29
+ * "the-north-face" → "TN". Symbols and digits-only words are skipped, so a
30
+ * logo alt like "Acme logo" gives "AL" — callers who want "A" pass
31
+ * `initials` explicitly.
32
+ */
33
+ export function imageRevealInitials(alt) {
34
+ if (!alt)
35
+ return '';
36
+ const words = alt
37
+ .split(/[\s\-_./]+/)
38
+ .map((w) => w.match(/\p{L}/u)?.[0] ?? '')
39
+ .filter(Boolean);
40
+ return words
41
+ .slice(0, 2)
42
+ .join('')
43
+ .toLocaleUpperCase();
44
+ }
45
+ export const IMAGE_REVEAL_SIZES = {
46
+ xs: 32,
47
+ sm: 48,
48
+ md: 64,
49
+ lg: 96,
50
+ xl: 128
51
+ };
52
+ /**
53
+ * A CSS length for the frame's width. Named sizes and numbers are px; any
54
+ * other string (`'10rem'`, `'50%'`) passes through; `'fill'` is 100%.
55
+ */
56
+ export function imageRevealSize(size) {
57
+ if (typeof size === 'number')
58
+ return `${size}px`;
59
+ if (size === 'fill')
60
+ return '100%';
61
+ if (size in IMAGE_REVEAL_SIZES)
62
+ return `${IMAGE_REVEAL_SIZES[size]}px`;
63
+ return size;
64
+ }
65
+ export const IMAGE_REVEAL_RADIUS = {
66
+ square: '0',
67
+ rounded: 'var(--jatui-radius-box, var(--radius-box, 0.5rem))',
68
+ circle: '9999px'
69
+ };
70
+ const TONE_NAMES = new Set([
71
+ 'primary',
72
+ 'secondary',
73
+ 'accent',
74
+ 'neutral',
75
+ 'info',
76
+ 'success',
77
+ 'warning',
78
+ 'error'
79
+ ]);
80
+ /** A semantic name → its DaisyUI variable; any other string is a CSS colour. */
81
+ export function imageRevealTone(tone) {
82
+ return TONE_NAMES.has(tone) ? `var(--color-${tone})` : tone;
83
+ }
@@ -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';
@@ -390,3 +400,6 @@ export { default as SettingsSection } from './components/settings/SettingsSectio
390
400
  export { default as SettingsRow } from './components/settings/SettingsRow.svelte';
391
401
  export type { SettingsRowContext } from './components/settings/SettingsRow.svelte';
392
402
  export * from './data/grid/index';
403
+ export { default as ImageReveal } from './components/ImageReveal.svelte';
404
+ export { imageRevealPhase, imageRevealBusy, imageRevealInitials, imageRevealSize, imageRevealTone, IMAGE_REVEAL_SIZES, IMAGE_REVEAL_RADIUS } from './components/imageReveal';
405
+ export type { ImageRevealPhase, ImageRevealShape, ImageRevealSizeName, ImageRevealToneName, ImageRevealState } from './components/imageReveal';
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';
@@ -425,3 +435,5 @@ export { default as SegmentedControl } from './components/SegmentedControl.svelt
425
435
  export { default as SettingsSection } from './components/settings/SettingsSection.svelte';
426
436
  export { default as SettingsRow } from './components/settings/SettingsRow.svelte';
427
437
  export * from './data/grid/index';
438
+ export { default as ImageReveal } from './components/ImageReveal.svelte';
439
+ export { imageRevealPhase, imageRevealBusy, imageRevealInitials, imageRevealSize, imageRevealTone, IMAGE_REVEAL_SIZES, IMAGE_REVEAL_RADIUS } from './components/imageReveal';
@@ -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);