@joewinke/jatui 0.39.0 → 0.41.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 (75) hide show
  1. package/CLAUDE.md +52 -5
  2. package/THIRD_PARTY_NOTICES.md +31 -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/AnimatedNumber.svelte +42 -1
  8. package/dist/components/AnimatedNumber.svelte.d.ts +7 -0
  9. package/dist/components/BottomSheet.svelte +7 -6
  10. package/dist/components/BottomSheet.svelte.d.ts +2 -0
  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/IconButton.svelte +200 -0
  16. package/dist/components/IconButton.svelte.d.ts +46 -0
  17. package/dist/components/LinkButton.svelte +117 -0
  18. package/dist/components/LinkButton.svelte.d.ts +32 -0
  19. package/dist/components/MediaLightbox.svelte +2 -7
  20. package/dist/components/Modal.svelte +2 -10
  21. package/dist/components/PinInput.svelte +94 -31
  22. package/dist/components/PinInput.svelte.d.ts +4 -0
  23. package/dist/components/SegmentedControl.svelte +263 -0
  24. package/dist/components/SegmentedControl.svelte.d.ts +48 -0
  25. package/dist/components/TextArea.svelte +67 -5
  26. package/dist/components/TextArea.svelte.d.ts +4 -6
  27. package/dist/components/ThemeSelector.svelte +29 -9
  28. package/dist/components/ThemeSelector.svelte.d.ts +6 -0
  29. package/dist/components/ThemeToggle.svelte +28 -3
  30. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  31. package/dist/components/basic/Breadcrumb.svelte +226 -11
  32. package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
  33. package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
  34. package/dist/components/basic/breadcrumbCollapse.js +12 -0
  35. package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
  36. package/dist/components/settings/SettingsRow.svelte +177 -0
  37. package/dist/components/settings/SettingsRow.svelte.d.ts +32 -0
  38. package/dist/components/settings/SettingsSection.svelte +139 -0
  39. package/dist/components/settings/SettingsSection.svelte.d.ts +21 -0
  40. package/dist/components/stats/ActivityHeatmap.svelte +412 -0
  41. package/dist/components/stats/ActivityHeatmap.svelte.d.ts +72 -0
  42. package/dist/components/stats/BarList.svelte +262 -0
  43. package/dist/components/stats/BarList.svelte.d.ts +57 -0
  44. package/dist/components/stats/ChartCard.svelte +230 -0
  45. package/dist/components/stats/ChartCard.svelte.d.ts +63 -0
  46. package/dist/components/stats/StatCard.svelte +387 -0
  47. package/dist/components/stats/StatCard.svelte.d.ts +37 -0
  48. package/dist/components/stats/StatGroup.svelte +67 -0
  49. package/dist/components/stats/StatGroup.svelte.d.ts +14 -0
  50. package/dist/components/stats/heatmap.d.ts +76 -0
  51. package/dist/components/stats/heatmap.js +151 -0
  52. package/dist/components/tree/TreeView.svelte +680 -0
  53. package/dist/components/tree/TreeView.svelte.d.ts +123 -0
  54. package/dist/components/tree/flattenTree.d.ts +57 -0
  55. package/dist/components/tree/flattenTree.js +91 -0
  56. package/dist/components/tree/treeKeys.d.ts +46 -0
  57. package/dist/components/tree/treeKeys.js +80 -0
  58. package/dist/data/grid/cells.d.ts +40 -0
  59. package/dist/data/grid/cells.js +89 -0
  60. package/dist/data/grid/clipboard.d.ts +46 -0
  61. package/dist/data/grid/clipboard.js +131 -0
  62. package/dist/data/grid/edits.d.ts +148 -0
  63. package/dist/data/grid/edits.js +222 -0
  64. package/dist/data/grid/index.d.ts +12 -0
  65. package/dist/data/grid/index.js +12 -0
  66. package/dist/index.d.ts +36 -1
  67. package/dist/index.js +31 -1
  68. package/dist/styles/tokens.css +80 -1
  69. package/dist/utils/pinInput.d.ts +33 -0
  70. package/dist/utils/pinInput.js +63 -0
  71. package/dist/utils/textareaRows.d.ts +23 -0
  72. package/dist/utils/textareaRows.js +13 -0
  73. package/dist/utils/themeWipe.d.ts +36 -17
  74. package/dist/utils/themeWipe.js +145 -35
  75. package/package.json +5 -1
package/dist/index.js CHANGED
@@ -112,6 +112,10 @@ export { default as InfiniteSearchGrid } from './components/InfiniteSearchGrid.s
112
112
  export { createInfiniteLoader } from './components/infiniteGridLoader';
113
113
  // Components — from Flush
114
114
  export { default as Button } from './components/Button.svelte';
115
+ // Icon-only / close / text-link buttons (BoardUI wave 1, jatui-owg2z)
116
+ export { default as IconButton } from './components/IconButton.svelte';
117
+ export { default as CloseButton } from './components/CloseButton.svelte';
118
+ export { default as LinkButton } from './components/LinkButton.svelte';
115
119
  export { default as Badge } from './components/Badge.svelte';
116
120
  export { default as Card } from './components/Card.svelte';
117
121
  export { default as Drawer } from './components/Drawer.svelte';
@@ -120,6 +124,7 @@ export { default as LoadingSpinner } from './components/LoadingSpinner.svelte';
120
124
  export { default as StatusBadge } from './components/StatusBadge.svelte';
121
125
  export { default as TextInput } from './components/TextInput.svelte';
122
126
  export { default as TextArea } from './components/TextArea.svelte';
127
+ export { textareaRowsHeight } from './utils/textareaRows';
123
128
  export { default as SelectInput } from './components/SelectInput.svelte';
124
129
  export { default as PhoneInput } from './components/PhoneInput.svelte';
125
130
  // Components — from Chimaro
@@ -245,6 +250,8 @@ export { loadGoogleMapsAPI, isGoogleMapsLoaded, isPlacesLoaded, isGeometryLoaded
245
250
  // ============================================================
246
251
  // Motion primitives
247
252
  export { tooltip } from './actions/tooltip';
253
+ // Progressive-blur scroll-region edge (BoardUI wave 1, jatui-owg2z)
254
+ export { scrollEdge, edgeOpacity, edgeDistances } from './actions/scrollEdge';
248
255
  export { staggerEntrance } from './actions/staggerEntrance';
249
256
  export { attention } from './actions/attention';
250
257
  // Scroll-reveal (jatui-m4uz3.3, chimaro port): IntersectionObserver
@@ -258,7 +265,7 @@ export { default as MediaExplorer } from "./components/MediaExplorer.svelte";
258
265
  export { floating, makeVirtualAnchor } from './actions/floating';
259
266
  export { glide } from './transitions/glide';
260
267
  // View Transitions kit
261
- export { themeWipe } from './utils/themeWipe';
268
+ export { themeWipe, wipeOrigin, coverRadius } from './utils/themeWipe';
262
269
  export { viewTransition } from './utils/viewTransition';
263
270
  // Motion level kit (jatui-j6833): data-motion="calm|standard|lively" scales
264
271
  // the --motion-* tokens per surface (styles/motion.css); these helpers,
@@ -292,6 +299,7 @@ export { default as CopyButton } from './components/CopyButton.svelte';
292
299
  export { default as HoldToConfirm } from './components/HoldToConfirm.svelte';
293
300
  // Form gaps
294
301
  export { default as PinInput } from './components/PinInput.svelte';
302
+ export { applyCellInput as applyPinCellInput, distribute as distributePin, cellsFromValue as pinCellsFromValue, isPinComplete, sanitizePin } from './utils/pinInput';
295
303
  export { default as PasswordMeter } from './components/PasswordMeter.svelte';
296
304
  export { default as QuantityStepper } from './components/QuantityStepper.svelte';
297
305
  // Messaging
@@ -321,6 +329,7 @@ export { createStickToBottom } from './actions/stickToBottom';
321
329
  // ANSI terminal output → HTML (upstreamed from the JAT IDE, U5 — jatui-rl7w6.3).
322
330
  export { ansiToHtml, ansiToHtmlStateful, ansiToHtmlWithLinks, ansiToHtmlWithHierarchy, linkifyUrls, stripAnsi, classifyLine, parseOutputLines, createDefaultAnsiState } from './utils/ansiToHtml';
323
331
  export { default as Breadcrumb } from './components/basic/Breadcrumb.svelte';
332
+ export { collapseBreadcrumb } from './components/basic/breadcrumbCollapse';
324
333
  export { default as Alert } from './components/basic/Alert.svelte';
325
334
  export { default as ButtonGroup } from './components/basic/ButtonGroup.svelte';
326
335
  export { default as Switch } from './components/basic/Switch.svelte';
@@ -384,3 +393,24 @@ export { splitStreamedText } from './components/agent/streamedText';
384
393
  export { stepsSummary, stepsRemaining, isStepsFinished, clampStepProgress } from './components/progress/stepsSummary';
385
394
  export { default as NotificationCenter } from './components/NotificationCenter.svelte';
386
395
  export { DEFAULT_NOTIFICATION_TABS, DATE_GROUP_LABELS as NOTIFICATION_DATE_GROUP_LABELS, dateGroupKey as notificationDateGroupKey, groupNotificationsByDate, toTimestampMs as notificationTimestampMs, isNotificationUnread, unreadIds as unreadNotificationIds, filterByTab as filterNotificationsByTab, tabCounts as notificationTabCounts, unreadSummary as notificationUnreadSummary } from './components/notificationCenter';
396
+ // BoardUI wave 3 (jatui-6kitr) — tree, data and dashboards.
397
+ // TreeView renders flattenTree's flat row list (role="tree", aria-level/
398
+ // posinset/setsize, connector guides, full APG keys + typeahead, optional
399
+ // jatui/virtual windowing). Stats: StatCard/StatGroup (AnimatedNumber's new
400
+ // mode="tween"), ChartCard (headline driven by chart hover, no chart lib),
401
+ // BarList, ActivityHeatmap. SegmentedControl = radio semantics (Tabs switch
402
+ // panels; this sets a value). SettingsSection/SettingsRow = settings pages.
403
+ // Pure grid helpers also live on the 'jatui/grid' subpath.
404
+ export { default as TreeView } from './components/tree/TreeView.svelte';
405
+ export { flattenTree, collectBranchIds, pathToNode } from './components/tree/flattenTree';
406
+ export { resolveTreeKey, treeTypeahead } from './components/tree/treeKeys';
407
+ export { default as StatCard } from './components/stats/StatCard.svelte';
408
+ export { default as StatGroup } from './components/stats/StatGroup.svelte';
409
+ export { default as ChartCard } from './components/stats/ChartCard.svelte';
410
+ export { default as BarList } from './components/stats/BarList.svelte';
411
+ export { default as ActivityHeatmap } from './components/stats/ActivityHeatmap.svelte';
412
+ export { buildHeatmap, moveHeatFocus, isoDay as heatmapIsoDay, defaultLevelOf as heatmapLevelOf } from './components/stats/heatmap';
413
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
414
+ export { default as SettingsSection } from './components/settings/SettingsSection.svelte';
415
+ export { default as SettingsRow } from './components/settings/SettingsRow.svelte';
416
+ export * from './data/grid/index';
@@ -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.39.0",
3
+ "version": "0.41.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",
@@ -34,6 +34,10 @@
34
34
  "svelte": "./dist/components/table/index.js",
35
35
  "default": "./dist/components/table/index.js"
36
36
  },
37
+ "./grid": {
38
+ "types": "./dist/data/grid/index.d.ts",
39
+ "default": "./dist/data/grid/index.js"
40
+ },
37
41
  "./virtual": {
38
42
  "types": "./dist/virtual/index.d.ts",
39
43
  "svelte": "./dist/virtual/index.js",