@joewinke/jatui 0.38.6 → 0.40.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 (98) hide show
  1. package/CLAUDE.md +54 -5
  2. package/THIRD_PARTY_NOTICES.md +67 -0
  3. package/dist/actions/scrollEdge.d.ts +55 -0
  4. package/dist/actions/scrollEdge.js +144 -0
  5. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
  6. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
  7. package/dist/components/BottomSheet.svelte +7 -6
  8. package/dist/components/BottomSheet.svelte.d.ts +2 -0
  9. package/dist/components/BulkActionBar.svelte +32 -2
  10. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  11. package/dist/components/CloseButton.svelte +75 -0
  12. package/dist/components/CloseButton.svelte.d.ts +29 -0
  13. package/dist/components/Drawer.svelte +10 -11
  14. package/dist/components/Drawer.svelte.d.ts +2 -8
  15. package/dist/components/FloatingActionBar.svelte +102 -34
  16. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  17. package/dist/components/IconButton.svelte +200 -0
  18. package/dist/components/IconButton.svelte.d.ts +46 -0
  19. package/dist/components/LinkButton.svelte +117 -0
  20. package/dist/components/LinkButton.svelte.d.ts +32 -0
  21. package/dist/components/MediaExplorer.svelte +249 -0
  22. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  23. package/dist/components/MediaLightbox.svelte +2 -7
  24. package/dist/components/Modal.svelte +2 -10
  25. package/dist/components/NotificationCenter.svelte +481 -0
  26. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  27. package/dist/components/PinInput.svelte +94 -31
  28. package/dist/components/PinInput.svelte.d.ts +4 -0
  29. package/dist/components/SearchDropdown.svelte +54 -8
  30. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  31. package/dist/components/TextArea.svelte +67 -5
  32. package/dist/components/TextArea.svelte.d.ts +4 -6
  33. package/dist/components/ThemeSelector.svelte +29 -9
  34. package/dist/components/ThemeSelector.svelte.d.ts +6 -0
  35. package/dist/components/ThemeToggle.svelte +28 -3
  36. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  37. package/dist/components/agent/AgentLog.svelte +217 -0
  38. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  39. package/dist/components/agent/AgentLogRow.svelte +281 -0
  40. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  41. package/dist/components/agent/AgentTaskList.svelte +607 -0
  42. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  43. package/dist/components/agent/AgentThinking.svelte +280 -0
  44. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  45. package/dist/components/agent/ComposerGlow.svelte +265 -0
  46. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  47. package/dist/components/agent/ContextMeter.svelte +277 -0
  48. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  49. package/dist/components/agent/Questionnaire.svelte +767 -0
  50. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  51. package/dist/components/agent/ShimmerText.svelte +100 -0
  52. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  53. package/dist/components/agent/StreamedText.svelte +135 -0
  54. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  55. package/dist/components/agent/agentLog.d.ts +22 -0
  56. package/dist/components/agent/agentLog.js +26 -0
  57. package/dist/components/agent/agentTaskList.d.ts +90 -0
  58. package/dist/components/agent/agentTaskList.js +93 -0
  59. package/dist/components/agent/agentThinking.d.ts +46 -0
  60. package/dist/components/agent/agentThinking.js +78 -0
  61. package/dist/components/agent/contextMeter.d.ts +97 -0
  62. package/dist/components/agent/contextMeter.js +156 -0
  63. package/dist/components/agent/questionnaire.d.ts +104 -0
  64. package/dist/components/agent/questionnaire.js +225 -0
  65. package/dist/components/agent/revealCore.d.ts +74 -0
  66. package/dist/components/agent/revealCore.js +160 -0
  67. package/dist/components/agent/revealTicker.d.ts +34 -0
  68. package/dist/components/agent/revealTicker.js +73 -0
  69. package/dist/components/agent/streamedText.d.ts +28 -0
  70. package/dist/components/agent/streamedText.js +59 -0
  71. package/dist/components/basic/Breadcrumb.svelte +226 -11
  72. package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
  73. package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
  74. package/dist/components/basic/breadcrumbCollapse.js +12 -0
  75. package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
  76. package/dist/components/messaging/MessageInput.svelte +34 -4
  77. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  78. package/dist/components/messaging/MessageItem.svelte +2 -2
  79. package/dist/components/notificationCenter.d.ts +65 -0
  80. package/dist/components/notificationCenter.js +110 -0
  81. package/dist/components/progress/Steps.svelte +269 -5
  82. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  83. package/dist/components/progress/stepsSummary.d.ts +14 -0
  84. package/dist/components/progress/stepsSummary.js +32 -0
  85. package/dist/index.d.ts +48 -1
  86. package/dist/index.js +39 -1
  87. package/dist/selection/clickSelection.d.ts +89 -0
  88. package/dist/selection/clickSelection.js +152 -0
  89. package/dist/selection/createClickSelection.d.ts +79 -0
  90. package/dist/selection/createClickSelection.js +134 -0
  91. package/dist/styles/tokens.css +80 -1
  92. package/dist/utils/pinInput.d.ts +33 -0
  93. package/dist/utils/pinInput.js +63 -0
  94. package/dist/utils/textareaRows.d.ts +23 -0
  95. package/dist/utils/textareaRows.js +13 -0
  96. package/dist/utils/themeWipe.d.ts +36 -17
  97. package/dist/utils/themeWipe.js +145 -35
  98. package/package.json +3 -2
@@ -0,0 +1,134 @@
1
+ /* createClickSelection.svelte.js generated by Svelte v5.53.11 */
2
+ import * as $ from 'svelte/internal/client';
3
+ import { SvelteSet } from 'svelte/reactivity';
4
+
5
+ import {
6
+ applyClick,
7
+ clearSelection,
8
+ extendTo,
9
+ inOrder,
10
+ inPickOrder,
11
+ prune,
12
+ selectAll,
13
+ selectionKey,
14
+ selectOnly,
15
+ toggleOne
16
+ } from './clickSelection';
17
+
18
+ export function createClickSelection(options) {
19
+ const selected = new SvelteSet();
20
+ let anchor = $.state(null);
21
+ let head = $.state(null);
22
+
23
+ const state = {
24
+ selected,
25
+ get anchor() {
26
+ return $.get(anchor);
27
+ },
28
+
29
+ set anchor(v) {
30
+ $.set(anchor, v, true);
31
+ },
32
+
33
+ get head() {
34
+ return $.get(head);
35
+ },
36
+
37
+ set head(v) {
38
+ $.set(head, v, true);
39
+ }
40
+ };
41
+
42
+ const ids = () => options.ids();
43
+ const changed = () => options.onChange?.(inOrder(selected, ids()));
44
+
45
+ return {
46
+ selected,
47
+ get anchor() {
48
+ return $.get(anchor);
49
+ },
50
+
51
+ get head() {
52
+ return $.get(head);
53
+ },
54
+
55
+ get ordered() {
56
+ return inOrder(selected, ids());
57
+ },
58
+
59
+ get picked() {
60
+ return inPickOrder(selected, ids());
61
+ },
62
+
63
+ get count() {
64
+ return inOrder(selected, ids()).length;
65
+ },
66
+ has: (id) => selected.has(id) && ids().includes(id),
67
+ click(id, mods = {}) {
68
+ applyClick(state, ids(), id, mods);
69
+ changed();
70
+ },
71
+
72
+ only(id) {
73
+ prune(state, ids());
74
+ selectOnly(state, id);
75
+ changed();
76
+ },
77
+
78
+ toggle(id) {
79
+ prune(state, ids());
80
+ toggleOne(state, id);
81
+ changed();
82
+ },
83
+
84
+ extendTo(id, additive = false) {
85
+ const all = ids();
86
+
87
+ prune(state, all);
88
+ extendTo(state, all, id, additive);
89
+ changed();
90
+ },
91
+
92
+ selectAll() {
93
+ selectAll(state, ids());
94
+ changed();
95
+ },
96
+
97
+ clear() {
98
+ clearSelection(state);
99
+ changed();
100
+ },
101
+
102
+ prune() {
103
+ if (prune(state, ids())) changed();
104
+ },
105
+
106
+ handleKeydown(e, opts = {}) {
107
+ const all = ids();
108
+ const action = selectionKey(e, inOrder(selected, all).length > 0);
109
+
110
+ if (!action) return false;
111
+
112
+ if (action.kind === 'extend') {
113
+ if (!opts.next) return false;
114
+
115
+ prune(state, all);
116
+
117
+ const to = opts.next($.get(head) ?? $.get(anchor), action.key);
118
+
119
+ if (to === null || to === undefined) {
120
+ e.preventDefault?.();
121
+
122
+ return true;
123
+ }
124
+
125
+ extendTo(state, all, to, action.additive);
126
+ } else if (action.kind === 'selectAll') selectAll(state, all); else clearSelection(state);
127
+
128
+ e.preventDefault?.();
129
+ changed();
130
+
131
+ return true;
132
+ }
133
+ };
134
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * jatui design tokens — radius, spacing, border.
2
+ * jatui design tokens — radius, spacing, border, colour intent.
3
3
  *
4
4
  * Sibling of motion.css: jatui owns the token layer; DaisyUI theme
5
5
  * variables FEED INTO it (never the other way around). Components use
@@ -29,3 +29,82 @@
29
29
  /* Border — depth comes from tonal surfaces + 1px borders, not shadows */
30
30
  --jatui-border-w: 1px;
31
31
  }
32
+
33
+ /*
34
+ * Colour-intent tokens (BoardUI wave 1, jatui-owg2z).
35
+ *
36
+ * A THIN component-intent layer: every value is DaisyUI's semantic colour
37
+ * vars mixed with `color-mix`, never a hex or a raw palette step, so a theme
38
+ * swap (and light ↔ dark) re-derives all of them for free. Mixing against
39
+ * --color-base-100 / --color-base-content is what makes the pairs flip in
40
+ * dark mode: the surface and the ink trade places, the mix follows.
41
+ *
42
+ * Declared on `:root` AND every `[data-theme]` element. Custom properties
43
+ * that contain var() are resolved where they are DECLARED, then inherited as
44
+ * finished colours — declared on :root alone, a nested `data-theme="dark"`
45
+ * panel would inherit the light root's values. Re-declaring them on each
46
+ * themed subtree makes them recompute against that subtree's palette.
47
+ *
48
+ * Components consume them with an inline fallback
49
+ * (`var(--jatui-focus-ring, …)`) so a host that never imports tokens.css
50
+ * still renders the same thing.
51
+ */
52
+ :root,
53
+ [data-theme] {
54
+ /* Focus — one ring for every interactive part */
55
+ --jatui-focus-ring: color-mix(in oklab, var(--color-primary) 70%, transparent);
56
+ --jatui-focus-ring-width: 2px;
57
+ --jatui-focus-ring-offset: 2px;
58
+
59
+ /* Surfaces + quiet structure */
60
+ --jatui-surface: var(--color-base-100); /* what a scroll-edge fade blends into */
61
+ --jatui-hover-bg: color-mix(in oklab, var(--color-base-content) 6%, transparent);
62
+ --jatui-pressed-bg: color-mix(in oklab, var(--color-base-content) 10%, transparent);
63
+ --jatui-separator: color-mix(in oklab, var(--color-base-content) 12%, transparent);
64
+
65
+ /* Selection — selected rows, range bands, picked cells */
66
+ --jatui-selection-bg: color-mix(in oklab, var(--color-primary) 14%, transparent);
67
+ --jatui-selection-edge: color-mix(in oklab, var(--color-primary) 55%, transparent);
68
+ --jatui-selection-solid: var(--color-primary); /* range endpoints, the picked day */
69
+ --jatui-selection-solid-fg: var(--color-primary-content);
70
+
71
+ /* Chart — categorical series 1..5, plus the quiet chart furniture */
72
+ --jatui-chart-1: var(--color-primary);
73
+ --jatui-chart-2: var(--color-secondary);
74
+ --jatui-chart-3: var(--color-accent);
75
+ --jatui-chart-4: var(--color-info);
76
+ --jatui-chart-5: var(--color-warning);
77
+ --jatui-chart-grid: color-mix(in oklab, var(--color-base-content) 10%, transparent);
78
+ --jatui-chart-axis: color-mix(in oklab, var(--color-base-content) 55%, transparent);
79
+
80
+ /*
81
+ * Event hues 1..6 — calendar chips, timeline bars, tags. Each hue is a
82
+ * triplet: a tinted chip surface, legible ink, and a solid edge/dot.
83
+ * bg mixes the hue into the surface; fg mixes it into the ink — both
84
+ * flip with the theme.
85
+ */
86
+ --jatui-event-1: var(--color-primary);
87
+ --jatui-event-2: var(--color-secondary);
88
+ --jatui-event-3: var(--color-accent);
89
+ --jatui-event-4: var(--color-info);
90
+ --jatui-event-5: var(--color-success);
91
+ --jatui-event-6: var(--color-warning);
92
+ --jatui-event-1-bg: color-mix(in oklab, var(--jatui-event-1) 16%, var(--color-base-100));
93
+ --jatui-event-2-bg: color-mix(in oklab, var(--jatui-event-2) 16%, var(--color-base-100));
94
+ --jatui-event-3-bg: color-mix(in oklab, var(--jatui-event-3) 16%, var(--color-base-100));
95
+ --jatui-event-4-bg: color-mix(in oklab, var(--jatui-event-4) 16%, var(--color-base-100));
96
+ --jatui-event-5-bg: color-mix(in oklab, var(--jatui-event-5) 16%, var(--color-base-100));
97
+ --jatui-event-6-bg: color-mix(in oklab, var(--jatui-event-6) 16%, var(--color-base-100));
98
+ --jatui-event-1-fg: color-mix(in oklab, var(--jatui-event-1) 55%, var(--color-base-content));
99
+ --jatui-event-2-fg: color-mix(in oklab, var(--jatui-event-2) 55%, var(--color-base-content));
100
+ --jatui-event-3-fg: color-mix(in oklab, var(--jatui-event-3) 55%, var(--color-base-content));
101
+ --jatui-event-4-fg: color-mix(in oklab, var(--jatui-event-4) 55%, var(--color-base-content));
102
+ --jatui-event-5-fg: color-mix(in oklab, var(--jatui-event-5) 55%, var(--color-base-content));
103
+ --jatui-event-6-fg: color-mix(in oklab, var(--jatui-event-6) 55%, var(--color-base-content));
104
+
105
+ /* Status intents — availability dots, deltas, soft banners */
106
+ --jatui-status-positive: var(--color-success);
107
+ --jatui-status-caution: var(--color-warning);
108
+ --jatui-status-negative: var(--color-error);
109
+ --jatui-status-neutral: color-mix(in oklab, var(--color-base-content) 40%, transparent);
110
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure cell logic behind PinInput (jatui-owg2z). No DOM — unit-tested.
3
+ *
4
+ * One rule set covers typing, paste and OS one-time-code autofill:
5
+ * - a single character replaces the cell and advances;
6
+ * - several characters DISTRIBUTE from the cell they landed in (paste from
7
+ * the caret);
8
+ * - a whole code (≥ length characters) fills from the FIRST cell no matter
9
+ * which cell received it — that is what iOS/Android SMS autofill and
10
+ * Chrome's WebOTP do: they drop the full code into the focused field.
11
+ */
12
+ export interface PinCellOptions {
13
+ length: number;
14
+ numeric?: boolean;
15
+ }
16
+ export interface PinCellResult {
17
+ cells: string[];
18
+ /** Index to focus next. */
19
+ focus: number;
20
+ }
21
+ /** Strip whitespace and separators; digits only when numeric. */
22
+ export declare function sanitizePin(raw: string, numeric?: boolean): string;
23
+ /** Normalise an external value into exactly `length` cells. */
24
+ export declare function cellsFromValue(value: string, length: number, numeric?: boolean): string[];
25
+ /**
26
+ * Apply what the browser put into cell `index`. `raw` is the cell's whole
27
+ * new value (with no maxlength, a typed character arrives NEXT TO the old
28
+ * one — "3" + "7" → "37" — so the previous char is dropped first).
29
+ */
30
+ export declare function applyCellInput(cells: readonly string[], index: number, raw: string, { length, numeric }: PinCellOptions): PinCellResult;
31
+ /** Paste / autofill: spread `text` across cells starting at `start`. */
32
+ export declare function distribute(cells: readonly string[], start: number, text: string, { length, numeric }: PinCellOptions): PinCellResult;
33
+ export declare function isPinComplete(cells: readonly string[], length: number): boolean;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Pure cell logic behind PinInput (jatui-owg2z). No DOM — unit-tested.
3
+ *
4
+ * One rule set covers typing, paste and OS one-time-code autofill:
5
+ * - a single character replaces the cell and advances;
6
+ * - several characters DISTRIBUTE from the cell they landed in (paste from
7
+ * the caret);
8
+ * - a whole code (≥ length characters) fills from the FIRST cell no matter
9
+ * which cell received it — that is what iOS/Android SMS autofill and
10
+ * Chrome's WebOTP do: they drop the full code into the focused field.
11
+ */
12
+ /** Strip whitespace and separators; digits only when numeric. */
13
+ export function sanitizePin(raw, numeric = false) {
14
+ const s = raw.replace(/[\s\-–—·.]/g, '');
15
+ return numeric ? s.replace(/\D/g, '') : s;
16
+ }
17
+ /** Normalise an external value into exactly `length` cells. */
18
+ export function cellsFromValue(value, length, numeric = false) {
19
+ const chars = [...sanitizePin(value ?? '', numeric)].slice(0, length);
20
+ return Array.from({ length }, (_, i) => chars[i] ?? '');
21
+ }
22
+ /**
23
+ * Apply what the browser put into cell `index`. `raw` is the cell's whole
24
+ * new value (with no maxlength, a typed character arrives NEXT TO the old
25
+ * one — "3" + "7" → "37" — so the previous char is dropped first).
26
+ */
27
+ export function applyCellInput(cells, index, raw, { length, numeric = false }) {
28
+ const next = Array.from({ length }, (_, i) => cells[i] ?? '');
29
+ let text = sanitizePin(raw, numeric);
30
+ const prev = next[index];
31
+ // Typed over an existing character: keep only what is new.
32
+ if (prev && [...text].length === 2 && text.includes(prev)) {
33
+ text = text.startsWith(prev) ? text.slice(prev.length) : text.slice(0, -prev.length);
34
+ }
35
+ const chars = [...text];
36
+ if (chars.length === 0) {
37
+ next[index] = '';
38
+ return { cells: next, focus: index };
39
+ }
40
+ if (chars.length === 1) {
41
+ next[index] = chars[0];
42
+ return { cells: next, focus: Math.min(index + 1, length - 1) };
43
+ }
44
+ return distribute(next, chars.length >= length ? 0 : index, text, { length, numeric });
45
+ }
46
+ /** Paste / autofill: spread `text` across cells starting at `start`. */
47
+ export function distribute(cells, start, text, { length, numeric = false }) {
48
+ const next = Array.from({ length }, (_, i) => cells[i] ?? '');
49
+ const chars = [...sanitizePin(text, numeric)];
50
+ const from = chars.length >= length ? 0 : start;
51
+ let i = from;
52
+ for (const c of chars) {
53
+ if (i >= length)
54
+ break;
55
+ next[i++] = c;
56
+ }
57
+ const firstEmpty = next.findIndex((c) => !c);
58
+ const focus = firstEmpty === -1 ? length - 1 : Math.min(i, length - 1);
59
+ return { cells: next, focus };
60
+ }
61
+ export function isPinComplete(cells, length) {
62
+ return cells.length >= length && cells.slice(0, length).every((c) => c !== '');
63
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Row-quantized auto-grow for TextArea (jatui-owg2z, BoardUI wave 1). Pure.
3
+ *
4
+ * Grows a whole line at a time between `minRows` and `maxRows`, then
5
+ * scrolls — no sub-line jitter while typing. `chrome` is the vertical
6
+ * padding + border in px (the element is border-box).
7
+ */
8
+ export interface TextareaRowsInput {
9
+ /** The element's scrollHeight measured at `height: auto` (content + padding). */
10
+ scrollHeight: number;
11
+ lineHeight: number;
12
+ /** Vertical padding + border, px. */
13
+ chrome: number;
14
+ /** Vertical padding only, px (scrollHeight includes padding, not border). */
15
+ padding: number;
16
+ minRows: number;
17
+ maxRows?: number;
18
+ }
19
+ export declare function textareaRowsHeight(m: TextareaRowsInput): {
20
+ rows: number;
21
+ height: number;
22
+ overflow: 'hidden' | 'auto';
23
+ };
@@ -0,0 +1,13 @@
1
+ export function textareaRowsHeight(m) {
2
+ const lh = m.lineHeight > 0 ? m.lineHeight : 20;
3
+ const min = Math.max(1, Math.floor(m.minRows));
4
+ const max = m.maxRows != null ? Math.max(min, Math.floor(m.maxRows)) : Infinity;
5
+ // Content lines; a hair of tolerance so 3.0001 lines is still 3.
6
+ const contentLines = Math.ceil((m.scrollHeight - m.padding) / lh - 0.01);
7
+ const rows = Math.min(max, Math.max(min, contentLines));
8
+ return {
9
+ rows,
10
+ height: Math.round(rows * lh + m.chrome),
11
+ overflow: contentLines > rows ? 'auto' : 'hidden'
12
+ };
13
+ }
@@ -1,21 +1,40 @@
1
+ export interface ThemeWipeOptions {
2
+ /** 'hard' (default) clip-path circle, or 'soft' — a feathered mask edge. */
3
+ edge?: 'hard' | 'soft';
4
+ /** Standard-level duration in ms; scaled by the motion level. Default 500. */
5
+ duration?: number;
6
+ /** Explicit origin: a point, or an element whose centre is used. Overrides the event. */
7
+ origin?: {
8
+ x: number;
9
+ y: number;
10
+ } | Element | null;
11
+ }
12
+ type Point = {
13
+ x: number;
14
+ y: number;
15
+ };
1
16
  /**
2
- * themeWipe — circular View Transition reveal from a click origin.
3
- *
4
- * Wraps document.startViewTransition, computes a covering radius from the
5
- * click point, and animates ::view-transition-new(root) with a clip-path
6
- * circle via WAAPI (~500ms, var(--motion-glide)).
7
- *
8
- * Falls back gracefully: if startViewTransition is unsupported or
9
- * prefers-reduced-motion is set, applyTheme() runs synchronously with no
10
- * animation.
17
+ * Where the wipe grows from. Pure apart from reading the element rect.
18
+ * Keyboard-synthesised clicks report clientX = clientY = 0; those use the
19
+ * element's centre instead of the top-left corner.
11
20
  */
21
+ export declare function wipeOrigin(event: Pick<MouseEvent, 'clientX' | 'clientY' | 'currentTarget' | 'target'> | null, viewport: {
22
+ width: number;
23
+ height: number;
24
+ }, origin?: ThemeWipeOptions['origin']): Point;
25
+ /** Radius that covers the whole viewport from `p`. Pure. */
26
+ export declare function coverRadius(p: Point, viewport: {
27
+ width: number;
28
+ height: number;
29
+ }): number;
30
+ /** WAAPI keyframes for the soft (mask) edge. Pure. */
31
+ export declare function softWipeKeyframes(p: Point, radius: number): Keyframe[];
12
32
  /**
13
- * @param event - The click (or pointer) event that triggered the theme toggle.
14
- * Used to anchor the reveal circle. Pass null to default to
15
- * screen center.
16
- * @param applyTheme - Callback that performs the actual theme mutation
17
- * (e.g. set data-theme, update localStorage). Called
18
- * inside the startViewTransition callback so the snapshot
19
- * captures the old state and the new DOM is recorded after.
33
+ * @param event - The click/pointer/keyboard event that triggered the toggle
34
+ * (anchors the circle). Pass null for the viewport centre.
35
+ * @param applyTheme - Performs the theme mutation (data-theme, storage). Runs
36
+ * inside startViewTransition so the old state is captured
37
+ * before it.
20
38
  */
21
- export declare function themeWipe(event: MouseEvent | PointerEvent | null, applyTheme: () => void): Promise<void>;
39
+ export declare function themeWipe(event: MouseEvent | PointerEvent | KeyboardEvent | null, applyTheme: () => void, options?: ThemeWipeOptions): Promise<void>;
40
+ export {};
@@ -2,48 +2,158 @@
2
2
  * themeWipe — circular View Transition reveal from a click origin.
3
3
  *
4
4
  * Wraps document.startViewTransition, computes a covering radius from the
5
- * click point, and animates ::view-transition-new(root) with a clip-path
6
- * circle via WAAPI (~500ms, var(--motion-glide)).
5
+ * origin, and grows ::view-transition-new(root) from it with WAAPI.
7
6
  *
8
- * Falls back gracefully: if startViewTransition is unsupported or
9
- * prefers-reduced-motion is set, applyTheme() runs synchronously with no
10
- * animation.
7
+ * themeWipe(event, () => setTheme(next)) hard edge
8
+ * themeWipe(event, () => setTheme(next), { edge: 'soft' }) feathered edge
9
+ *
10
+ * Behaviour (BoardUI wave 1 fixes, jatui-owg2z):
11
+ * - Origin: the pointer position; a KEYBOARD activation (clientX/Y both 0)
12
+ * uses the centre of the element that fired it (`event.currentTarget`,
13
+ * or `options.origin`), never the top-left corner. No event → viewport
14
+ * centre.
15
+ * - Single-flight: a second call while a wipe is running skips the running
16
+ * transition and applies the new theme at once — no overlapping
17
+ * transitions on a double-click.
18
+ * - Motion tokens: duration is level-scaled (`data-motion` calm ×0.75,
19
+ * lively ×1.25) and the easing is `--motion-glide`. Reduced motion (or no
20
+ * View Transitions support) applies the theme synchronously.
21
+ * - The UA's default root cross-fade is suppressed for the duration, so the
22
+ * circle is the only thing that moves.
23
+ *
24
+ * The returned promise resolves once the theme is applied and any animation
25
+ * has finished.
11
26
  */
12
- const DURATION = 500;
27
+ import { getMotionContext, motionDuration } from '../motion/level';
13
28
  const EASING = 'cubic-bezier(0.16, 1, 0.3, 1)'; // --motion-glide fallback
29
+ /** Standard-level duration, ms (scaled by the motion level). */
30
+ const BASE_DURATION = 500;
31
+ /** Soft edge: the opaque core is this fraction of the mask circle's radius. */
32
+ const SOFT_CORE = 0.82;
33
+ function centreOf(el) {
34
+ const r = el.getBoundingClientRect();
35
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
36
+ }
14
37
  /**
15
- * @param event - The click (or pointer) event that triggered the theme toggle.
16
- * Used to anchor the reveal circle. Pass null to default to
17
- * screen center.
18
- * @param applyTheme - Callback that performs the actual theme mutation
19
- * (e.g. set data-theme, update localStorage). Called
20
- * inside the startViewTransition callback so the snapshot
21
- * captures the old state and the new DOM is recorded after.
38
+ * Where the wipe grows from. Pure apart from reading the element rect.
39
+ * Keyboard-synthesised clicks report clientX = clientY = 0; those use the
40
+ * element's centre instead of the top-left corner.
22
41
  */
23
- export async function themeWipe(event, applyTheme) {
24
- const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
25
- if (prefersReduced || !document.startViewTransition) {
42
+ export function wipeOrigin(event, viewport, origin) {
43
+ if (origin) {
44
+ if ('getBoundingClientRect' in origin)
45
+ return centreOf(origin);
46
+ return origin;
47
+ }
48
+ if (event) {
49
+ // KeyboardEvents carry no clientX/Y at all; synthesised clicks carry 0,0.
50
+ const cx = typeof event.clientX === 'number' ? event.clientX : 0;
51
+ const cy = typeof event.clientY === 'number' ? event.clientY : 0;
52
+ if (cx !== 0 || cy !== 0)
53
+ return { x: cx, y: cy };
54
+ const el = (event.currentTarget ?? event.target);
55
+ if (el && typeof el.getBoundingClientRect === 'function')
56
+ return centreOf(el);
57
+ }
58
+ return { x: viewport.width / 2, y: viewport.height / 2 };
59
+ }
60
+ /** Radius that covers the whole viewport from `p`. Pure. */
61
+ export function coverRadius(p, viewport) {
62
+ return Math.hypot(Math.max(p.x, viewport.width - p.x), Math.max(p.y, viewport.height - p.y));
63
+ }
64
+ /** WAAPI keyframes for the soft (mask) edge. Pure. */
65
+ export function softWipeKeyframes(p, radius) {
66
+ const end = (2 * radius) / SOFT_CORE;
67
+ const frame = (size) => ({
68
+ maskSize: `${size}px ${size}px`,
69
+ maskPosition: `${p.x - size / 2}px ${p.y - size / 2}px`,
70
+ webkitMaskSize: `${size}px ${size}px`,
71
+ webkitMaskPosition: `${p.x - size / 2}px ${p.y - size / 2}px`
72
+ });
73
+ return [frame(0), frame(end)];
74
+ }
75
+ let running = null;
76
+ const STYLE_ID = 'jatui-theme-wipe-style';
77
+ function setWipeStyle(edge) {
78
+ document.getElementById(STYLE_ID)?.remove();
79
+ if (!edge)
80
+ return;
81
+ const style = document.createElement('style');
82
+ style.id = STYLE_ID;
83
+ const mask = edge === 'soft'
84
+ ? `mask-image: radial-gradient(closest-side, #000 ${SOFT_CORE * 100}%, transparent);
85
+ -webkit-mask-image: radial-gradient(closest-side, #000 ${SOFT_CORE * 100}%, transparent);
86
+ mask-repeat: no-repeat; -webkit-mask-repeat: no-repeat;
87
+ mask-size: 0 0; -webkit-mask-size: 0 0;`
88
+ : '';
89
+ style.textContent = `
90
+ ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
91
+ ::view-transition-new(root) { ${mask} }
92
+ `;
93
+ document.head.appendChild(style);
94
+ }
95
+ /**
96
+ * @param event - The click/pointer/keyboard event that triggered the toggle
97
+ * (anchors the circle). Pass null for the viewport centre.
98
+ * @param applyTheme - Performs the theme mutation (data-theme, storage). Runs
99
+ * inside startViewTransition so the old state is captured
100
+ * before it.
101
+ */
102
+ export async function themeWipe(event, applyTheme, options = {}) {
103
+ if (typeof document === 'undefined') {
104
+ applyTheme();
105
+ return;
106
+ }
107
+ // Single-flight: finish the running wipe instantly, then apply this one plainly.
108
+ if (running) {
109
+ const prev = running;
110
+ running = null;
111
+ prev.skipTransition();
112
+ // Its update callback still runs (asynchronously) — let it land first,
113
+ // or it would overwrite the theme applied here.
114
+ await prev.updateCallbackDone.catch(() => { });
26
115
  applyTheme();
27
116
  return;
28
117
  }
29
- // Origin: click point or viewport center
30
- const x = event ? event.clientX : window.innerWidth / 2;
31
- const y = event ? event.clientY : window.innerHeight / 2;
32
- // Radius that covers the entire viewport from the click origin
33
- const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));
118
+ const ctx = getMotionContext();
119
+ const duration = motionDuration(options.duration ?? BASE_DURATION, ctx);
120
+ if (ctx.reduced || duration <= 0 || !document.startViewTransition) {
121
+ applyTheme();
122
+ return;
123
+ }
124
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
125
+ const p = wipeOrigin(event, viewport, options.origin);
126
+ const radius = coverRadius(p, viewport);
127
+ const edge = options.edge ?? 'hard';
128
+ setWipeStyle(edge);
34
129
  const transition = document.startViewTransition(applyTheme);
35
- // Wait until both snapshots are captured before animating
36
- await transition.ready;
37
- // Read the resolved --motion-glide easing from CSS custom properties
38
- const resolvedEasing = getComputedStyle(document.documentElement)
39
- .getPropertyValue('--motion-glide')
40
- .trim() || EASING;
41
- document.documentElement.animate([
42
- { clipPath: `circle(0px at ${x}px ${y}px)` },
43
- { clipPath: `circle(${endRadius}px at ${x}px ${y}px)` }
44
- ], {
45
- duration: DURATION,
46
- easing: resolvedEasing,
47
- pseudoElement: '::view-transition-new(root)'
48
- });
130
+ running = transition;
131
+ try {
132
+ await transition.ready;
133
+ const easing = getComputedStyle(document.documentElement).getPropertyValue('--motion-glide').trim() ||
134
+ EASING;
135
+ const keyframes = edge === 'soft'
136
+ ? softWipeKeyframes(p, radius)
137
+ : [
138
+ { clipPath: `circle(0px at ${p.x}px ${p.y}px)` },
139
+ { clipPath: `circle(${radius}px at ${p.x}px ${p.y}px)` }
140
+ ];
141
+ document.documentElement.animate(keyframes, {
142
+ duration,
143
+ easing,
144
+ fill: 'forwards',
145
+ pseudoElement: '::view-transition-new(root)'
146
+ });
147
+ await transition.finished;
148
+ }
149
+ catch {
150
+ // Skipped or aborted — the theme callback has still run.
151
+ }
152
+ finally {
153
+ if (running === transition)
154
+ running = null;
155
+ // A newer wipe may own the style element by now — leave it alone.
156
+ if (!running)
157
+ setWipeStyle(null);
158
+ }
49
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.38.6",
3
+ "version": "0.40.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -84,7 +84,8 @@
84
84
  "dist",
85
85
  "CLAUDE.md",
86
86
  "README.md",
87
- "LICENSE"
87
+ "LICENSE",
88
+ "THIRD_PARTY_NOTICES.md"
88
89
  ],
89
90
  "scripts": {
90
91
  "dev": "npm run package && vite dev --port 5180",