@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.
- package/CLAUDE.md +54 -5
- package/THIRD_PARTY_NOTICES.md +67 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/BulkActionBar.svelte +32 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -1
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/FloatingActionBar.svelte +102 -34
- package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaExplorer.svelte +249 -0
- package/dist/components/MediaExplorer.svelte.d.ts +37 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/NotificationCenter.svelte +481 -0
- package/dist/components/NotificationCenter.svelte.d.ts +29 -0
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SearchDropdown.svelte +54 -8
- package/dist/components/SearchDropdown.svelte.d.ts +12 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/agent/AgentLog.svelte +217 -0
- package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
- package/dist/components/agent/AgentLogRow.svelte +281 -0
- package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
- package/dist/components/agent/AgentTaskList.svelte +607 -0
- package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
- package/dist/components/agent/AgentThinking.svelte +280 -0
- package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
- package/dist/components/agent/ComposerGlow.svelte +265 -0
- package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
- package/dist/components/agent/ContextMeter.svelte +277 -0
- package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
- package/dist/components/agent/Questionnaire.svelte +767 -0
- package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
- package/dist/components/agent/ShimmerText.svelte +100 -0
- package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
- package/dist/components/agent/StreamedText.svelte +135 -0
- package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
- package/dist/components/agent/agentLog.d.ts +22 -0
- package/dist/components/agent/agentLog.js +26 -0
- package/dist/components/agent/agentTaskList.d.ts +90 -0
- package/dist/components/agent/agentTaskList.js +93 -0
- package/dist/components/agent/agentThinking.d.ts +46 -0
- package/dist/components/agent/agentThinking.js +78 -0
- package/dist/components/agent/contextMeter.d.ts +97 -0
- package/dist/components/agent/contextMeter.js +156 -0
- package/dist/components/agent/questionnaire.d.ts +104 -0
- package/dist/components/agent/questionnaire.js +225 -0
- package/dist/components/agent/revealCore.d.ts +74 -0
- package/dist/components/agent/revealCore.js +160 -0
- package/dist/components/agent/revealTicker.d.ts +34 -0
- package/dist/components/agent/revealTicker.js +73 -0
- package/dist/components/agent/streamedText.d.ts +28 -0
- package/dist/components/agent/streamedText.js +59 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/components/messaging/MessageInput.svelte +34 -4
- package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
- package/dist/components/messaging/MessageItem.svelte +2 -2
- package/dist/components/notificationCenter.d.ts +65 -0
- package/dist/components/notificationCenter.js +110 -0
- package/dist/components/progress/Steps.svelte +269 -5
- package/dist/components/progress/Steps.svelte.d.ts +16 -1
- package/dist/components/progress/stepsSummary.d.ts +14 -0
- package/dist/components/progress/stepsSummary.js +32 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.js +39 -1
- package/dist/selection/clickSelection.d.ts +89 -0
- package/dist/selection/clickSelection.js +152 -0
- package/dist/selection/createClickSelection.d.ts +79 -0
- package/dist/selection/createClickSelection.js +134 -0
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- 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
|
+
}
|
package/dist/styles/tokens.css
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 {};
|
package/dist/utils/themeWipe.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
* circle via WAAPI (~500ms, var(--motion-glide)).
|
|
5
|
+
* origin, and grows ::view-transition-new(root) from it with WAAPI.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
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",
|