@ruckc/truss-ui 1.2.0 → 1.4.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/README.md +6 -1
- package/dist/commands.svelte.d.ts +41 -0
- package/dist/commands.svelte.js +59 -0
- package/dist/components/Accordion.svelte +1 -1
- package/dist/components/Button.svelte +65 -8
- package/dist/components/Button.svelte.d.ts +2 -0
- package/dist/components/Calendar.svelte +2 -2
- package/dist/components/Card.svelte +1 -0
- package/dist/components/Card.svelte.d.ts +1 -0
- package/dist/components/Checkbox.svelte +1 -1
- package/dist/components/CommandPalette.svelte +207 -0
- package/dist/components/CommandPalette.svelte.d.ts +39 -0
- package/dist/components/DataTable.svelte +201 -27
- package/dist/components/DataTable.svelte.d.ts +50 -4
- package/dist/components/Dialog.svelte +2 -1
- package/dist/components/Drawer.svelte +1 -1
- package/dist/components/EditableText.svelte +2 -2
- package/dist/components/Eyebrow.svelte +1 -1
- package/dist/components/FileUpload.svelte +1 -1
- package/dist/components/Footer.svelte +29 -2
- package/dist/components/Footer.svelte.d.ts +8 -0
- package/dist/components/GridLayout.svelte +242 -0
- package/dist/components/GridLayout.svelte.d.ts +30 -0
- package/dist/components/HoverCard.svelte +158 -0
- package/dist/components/HoverCard.svelte.d.ts +46 -0
- package/dist/components/Kbd.svelte +36 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Link.svelte +1 -1
- package/dist/components/List.svelte +113 -0
- package/dist/components/List.svelte.d.ts +17 -0
- package/dist/components/ListItem.svelte +13 -0
- package/dist/components/ListItem.svelte.d.ts +9 -0
- package/dist/components/Menu.svelte +4 -3
- package/dist/components/MultiSelect.svelte +2 -2
- package/dist/components/Navbar.svelte +13 -3
- package/dist/components/Navbar.svelte.d.ts +5 -1
- package/dist/components/Panel.svelte +2 -2
- package/dist/components/Popover.svelte +22 -1
- package/dist/components/Popover.svelte.d.ts +2 -0
- package/dist/components/RadioGroup.svelte +1 -1
- package/dist/components/Select.svelte +1 -1
- package/dist/components/Shell.svelte +12 -1
- package/dist/components/Shell.svelte.d.ts +2 -0
- package/dist/components/ShortcutSheet.svelte +62 -0
- package/dist/components/ShortcutSheet.svelte.d.ts +9 -0
- package/dist/components/Sidebar.svelte +2 -2
- package/dist/components/Skeleton.svelte +80 -6
- package/dist/components/Skeleton.svelte.d.ts +8 -1
- package/dist/components/Slider.svelte +1 -1
- package/dist/components/Switch.svelte +1 -1
- package/dist/components/Tabs.svelte +252 -32
- package/dist/components/Tabs.svelte.d.ts +21 -9
- package/dist/components/Tag.svelte +7 -7
- package/dist/components/TagInput.svelte +1 -1
- package/dist/components/Text.svelte +1 -1
- package/dist/components/Toaster.svelte +33 -2
- package/dist/components/ToggleChip.svelte +1 -1
- package/dist/components/Toolbar.svelte +27 -0
- package/dist/components/Toolbar.svelte.d.ts +10 -0
- package/dist/components/ToolbarSeparator.svelte +15 -0
- package/dist/components/ToolbarSeparator.svelte.d.ts +3 -0
- package/dist/components/Tooltip.svelte +19 -28
- package/dist/components/Tooltip.svelte.d.ts +6 -1
- package/dist/components/TooltipTip.svelte +59 -0
- package/dist/components/TooltipTip.svelte.d.ts +12 -0
- package/dist/components/Tree.svelte +343 -44
- package/dist/components/Tree.svelte.d.ts +29 -2
- package/dist/grid-layout.d.ts +42 -0
- package/dist/grid-layout.js +100 -0
- package/dist/hotkeys.svelte.d.ts +45 -0
- package/dist/hotkeys.svelte.js +108 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +13 -0
- package/dist/keys.d.ts +25 -0
- package/dist/keys.js +88 -0
- package/dist/roving.d.ts +25 -0
- package/dist/roving.js +129 -0
- package/dist/styles/base.css +2 -0
- package/dist/styles/tokens.css +49 -0
- package/dist/table-export.d.ts +24 -0
- package/dist/table-export.js +130 -0
- package/dist/toast.svelte.d.ts +25 -1
- package/dist/toast.svelte.js +42 -4
- package/dist/types.d.ts +6 -0
- package/package.json +7 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout engine behind GridLayout: integer grid cells, collision resolution, vertical compaction,
|
|
3
|
+
* size clamping and responsive column mapping. Nothing here touches the DOM and no function mutates its input.
|
|
4
|
+
*/
|
|
5
|
+
const int = (n, fallback = 0) => (Number.isFinite(n) ? Math.round(n) : fallback);
|
|
6
|
+
const between = (n, lo, hi) => Math.min(Math.max(n, lo), Math.max(lo, hi));
|
|
7
|
+
/** Whether two items overlap (touching edges do not count). */
|
|
8
|
+
export function collides(a, b) {
|
|
9
|
+
return a.id !== b.id && a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
10
|
+
}
|
|
11
|
+
/** Number of rows the layout occupies. */
|
|
12
|
+
export function bottom(items) {
|
|
13
|
+
return items.reduce((m, i) => Math.max(m, i.y + i.h), 0);
|
|
14
|
+
}
|
|
15
|
+
/** Clamp one item into `cols` columns: size within its min/max (and the grid), position inside the grid. */
|
|
16
|
+
export function clampItem(item, cols) {
|
|
17
|
+
const c = Math.max(1, int(cols, 1));
|
|
18
|
+
const minW = between(int(item.minW ?? 1, 1), 1, c);
|
|
19
|
+
const maxW = between(int(item.maxW ?? c, c), minW, c);
|
|
20
|
+
const minH = Math.max(1, int(item.minH ?? 1, 1));
|
|
21
|
+
const maxH = Math.max(minH, int(item.maxH ?? Infinity, Infinity) || Infinity);
|
|
22
|
+
const w = between(int(item.w, minW), minW, maxW);
|
|
23
|
+
const h = between(int(item.h, minH), minH, maxH);
|
|
24
|
+
return { ...item, w, h, x: between(int(item.x), 0, c - w), y: Math.max(0, int(item.y)) };
|
|
25
|
+
}
|
|
26
|
+
const byPosition = (a, b) => a.y - b.y || a.x - b.x;
|
|
27
|
+
/**
|
|
28
|
+
* Settle a layout. Static items and `anchorId` keep their cells; every other item is pushed down past
|
|
29
|
+
* whatever it collides with, then pulled up as far as it can go (vertical compaction). The anchor itself
|
|
30
|
+
* is not compacted, so a tile being dragged stays where it was put. When the anchor moved down (`fromY` is where
|
|
31
|
+
* it started), a tile that was below that start and now collides with it hops above the anchor if there is
|
|
32
|
+
* room, so a tile can be dragged down past its neighbours (a swap) instead of shoving them ahead of it.
|
|
33
|
+
*/
|
|
34
|
+
function settle(layout, anchorId, fromY = Infinity) {
|
|
35
|
+
const anchor = layout.find((i) => i.id === anchorId);
|
|
36
|
+
const placed = layout.filter((i) => i.static || i.id === anchorId);
|
|
37
|
+
const rest = layout.filter((i) => !(i.static || i.id === anchorId)).sort(byPosition);
|
|
38
|
+
const hit = (it) => placed.find((p) => collides(it, p));
|
|
39
|
+
for (const it of rest) {
|
|
40
|
+
const first = hit(it);
|
|
41
|
+
if (first && first === anchor && it.y >= fromY && anchor.y > fromY && anchor.y - it.h >= 0) {
|
|
42
|
+
const y = it.y;
|
|
43
|
+
it.y = anchor.y - it.h;
|
|
44
|
+
if (hit(it))
|
|
45
|
+
it.y = y;
|
|
46
|
+
}
|
|
47
|
+
for (let c = hit(it); c; c = hit(it))
|
|
48
|
+
it.y = c.y + c.h;
|
|
49
|
+
while (it.y > 0) {
|
|
50
|
+
it.y -= 1;
|
|
51
|
+
if (hit(it)) {
|
|
52
|
+
it.y += 1;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
placed.push(it);
|
|
57
|
+
}
|
|
58
|
+
return layout;
|
|
59
|
+
}
|
|
60
|
+
/** Clamp every item and compact the layout vertically. */
|
|
61
|
+
export function compact(items, cols) {
|
|
62
|
+
return settle(items.map((i) => clampItem(i, cols)));
|
|
63
|
+
}
|
|
64
|
+
/** Fit a layout to a (possibly narrower) column count: clamp, resolve collisions, compact. */
|
|
65
|
+
export const fitToCols = compact;
|
|
66
|
+
function change(items, id, cols, edit) {
|
|
67
|
+
const layout = items.map((i) => clampItem(i, cols));
|
|
68
|
+
const at = layout.findIndex((i) => i.id === id);
|
|
69
|
+
if (at < 0 || layout[at].static)
|
|
70
|
+
return settle(layout);
|
|
71
|
+
const from = layout[at].y;
|
|
72
|
+
const next = clampItem(edit(layout[at]), cols);
|
|
73
|
+
if (layout.some((o) => o.static && collides(next, o)))
|
|
74
|
+
return settle(layout); // a static tile is an obstacle, not something to push
|
|
75
|
+
layout[at] = next;
|
|
76
|
+
return settle(layout, id, from);
|
|
77
|
+
}
|
|
78
|
+
/** Move an item to (x, y): clamped to the grid, colliding items are pushed down, the rest compact around it. */
|
|
79
|
+
export function moveItem(items, id, x, y, cols) {
|
|
80
|
+
return change(items, id, cols, (i) => ({ ...i, x, y }));
|
|
81
|
+
}
|
|
82
|
+
/** Resize an item to w by h cells, within its min/max and the grid edge; colliding items are pushed down. */
|
|
83
|
+
export function resizeItem(items, id, w, h, cols) {
|
|
84
|
+
return change(items, id, cols, (i) => ({ ...i, w: Math.min(w, Math.max(1, cols - i.x)), h })); // grow towards the edge, never shift
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Columns for a container width. `breakpoints` maps a minimum width in px to a column count; the largest
|
|
88
|
+
* key not above `width` wins, and a width below every key uses the smallest key. Without breakpoints it is `fallback`.
|
|
89
|
+
*/
|
|
90
|
+
export function colsFor(width, breakpoints, fallback) {
|
|
91
|
+
const keys = Object.keys(breakpoints ?? {}).map(Number).filter(Number.isFinite).sort((a, b) => a - b);
|
|
92
|
+
if (!breakpoints || !keys.length)
|
|
93
|
+
return fallback;
|
|
94
|
+
const key = [...keys].reverse().find((k) => k <= width) ?? keys[0];
|
|
95
|
+
return Math.max(1, int(breakpoints[key], fallback));
|
|
96
|
+
}
|
|
97
|
+
/** Whether two layouts place every item identically. */
|
|
98
|
+
export function sameLayout(a, b) {
|
|
99
|
+
return a.length === b.length && a.every((i, n) => i.id === b[n].id && i.x === b[n].x && i.y === b[n].y && i.w === b[n].w && i.h === b[n].h);
|
|
100
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Combo } from './keys.js';
|
|
2
|
+
export type HotkeyOptions = {
|
|
3
|
+
/** Where the binding is live. `'global'` (default) is suppressed while a modal scope is on top (see `hotkeys.pushScope`). */
|
|
4
|
+
scope?: string;
|
|
5
|
+
/** Fire even while the user is typing in an input, textarea, select or contenteditable element. Off by default. */
|
|
6
|
+
allowInInput?: boolean;
|
|
7
|
+
/** Human-readable label; bindings with one are listed by `<ShortcutSheet>`. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Heading to list the shortcut under in `<ShortcutSheet>`. */
|
|
10
|
+
group?: string;
|
|
11
|
+
/** Checked on every key press; return false to leave the binding inactive. */
|
|
12
|
+
when?: () => boolean;
|
|
13
|
+
/** Call `preventDefault()` on the event when the binding fires. Default true. */
|
|
14
|
+
preventDefault?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type Hotkey = Required<Pick<HotkeyOptions, 'scope' | 'allowInInput' | 'preventDefault'>> & Pick<HotkeyOptions, 'description' | 'group' | 'when'> & {
|
|
17
|
+
id: number;
|
|
18
|
+
/** The shortcut as registered, e.g. `mod+k` or `g h`. */
|
|
19
|
+
shortcut: string;
|
|
20
|
+
steps: Combo[];
|
|
21
|
+
handler: (e: KeyboardEvent) => void;
|
|
22
|
+
};
|
|
23
|
+
/** Whether keystrokes from this element are text entry. */
|
|
24
|
+
export declare function isTyping(t: EventTarget | null): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The single keyboard shortcut registry. One `keydown` listener on `window` is installed when the first binding is registered
|
|
27
|
+
* and removed with the last. Safe to import during SSR: nothing touches `window` until `register` runs in the browser.
|
|
28
|
+
*/
|
|
29
|
+
declare class HotkeyStore {
|
|
30
|
+
#private;
|
|
31
|
+
/** Every live binding, in registration order. */
|
|
32
|
+
bindings: Hotkey[];
|
|
33
|
+
/** Modal scopes, innermost last. Empty means only `'global'` bindings are live. */
|
|
34
|
+
scopes: string[];
|
|
35
|
+
/** The scope currently receiving shortcuts. */
|
|
36
|
+
get scope(): string;
|
|
37
|
+
/** Bindings that have a description, for display (`<ShortcutSheet>`). */
|
|
38
|
+
get described(): Hotkey[];
|
|
39
|
+
/** Bind a shortcut (`mod+k`, `shift+/`, or a sequence like `g h`). Returns the function that removes it. */
|
|
40
|
+
register(shortcut: string, handler: (e: KeyboardEvent) => void, o?: HotkeyOptions): () => void;
|
|
41
|
+
/** Make `name` the active scope (for a modal, say): `'global'` bindings pause until the returned function is called. */
|
|
42
|
+
pushScope(name: string): () => void;
|
|
43
|
+
}
|
|
44
|
+
export declare const hotkeys: HotkeyStore;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
import { matches, parseSequence } from './keys.js';
|
|
3
|
+
/** How long a sequence waits for its next key. */
|
|
4
|
+
const SEQUENCE_TIMEOUT = 1000;
|
|
5
|
+
const MODIFIER_KEYS = new Set(['Shift', 'Control', 'Alt', 'Meta', 'AltGraph', 'CapsLock']);
|
|
6
|
+
const snap = (e) => ({ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, altKey: e.altKey, shiftKey: e.shiftKey });
|
|
7
|
+
/** Whether keystrokes from this element are text entry. */
|
|
8
|
+
export function isTyping(t) {
|
|
9
|
+
if (!(t instanceof HTMLElement))
|
|
10
|
+
return false;
|
|
11
|
+
if (t.isContentEditable)
|
|
12
|
+
return true;
|
|
13
|
+
if (t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement)
|
|
14
|
+
return true;
|
|
15
|
+
return t instanceof HTMLInputElement && !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file', 'image'].includes(t.type);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The single keyboard shortcut registry. One `keydown` listener on `window` is installed when the first binding is registered
|
|
19
|
+
* and removed with the last. Safe to import during SSR: nothing touches `window` until `register` runs in the browser.
|
|
20
|
+
*/
|
|
21
|
+
class HotkeyStore {
|
|
22
|
+
/** Every live binding, in registration order. */
|
|
23
|
+
bindings = $state.raw([]);
|
|
24
|
+
/** Modal scopes, innermost last. Empty means only `'global'` bindings are live. */
|
|
25
|
+
scopes = $state.raw([]);
|
|
26
|
+
#next = 1;
|
|
27
|
+
#buffer = [];
|
|
28
|
+
#timer;
|
|
29
|
+
#installed = false;
|
|
30
|
+
/** The scope currently receiving shortcuts. */
|
|
31
|
+
get scope() { return this.scopes[this.scopes.length - 1] ?? 'global'; }
|
|
32
|
+
/** Bindings that have a description, for display (`<ShortcutSheet>`). */
|
|
33
|
+
get described() { return this.bindings.filter((b) => b.description); }
|
|
34
|
+
/** Bind a shortcut (`mod+k`, `shift+/`, or a sequence like `g h`). Returns the function that removes it. */
|
|
35
|
+
register(shortcut, handler, o = {}) {
|
|
36
|
+
const steps = parseSequence(shortcut);
|
|
37
|
+
if (!steps.length || steps.some((s) => !s.key))
|
|
38
|
+
throw new Error(`Invalid shortcut: "${shortcut}"`);
|
|
39
|
+
const b = { id: this.#next++, shortcut, steps, handler, scope: o.scope ?? 'global', allowInInput: o.allowInInput ?? false, preventDefault: o.preventDefault ?? true, description: o.description, group: o.group, when: o.when };
|
|
40
|
+
const key = JSON.stringify(steps);
|
|
41
|
+
// register/pushScope are called from effects: reading the lists they then write must not subscribe the caller
|
|
42
|
+
const cur = untrack(() => this.bindings);
|
|
43
|
+
if (isDev() && cur.some((x) => x.scope === b.scope && JSON.stringify(x.steps) === key))
|
|
44
|
+
console.warn(`[truss] shortcut "${shortcut}" is already bound in scope "${b.scope}"; the most recent binding wins.`);
|
|
45
|
+
this.bindings = [...cur, b];
|
|
46
|
+
this.#install();
|
|
47
|
+
return () => {
|
|
48
|
+
this.bindings = untrack(() => this.bindings).filter((x) => x !== b);
|
|
49
|
+
if (!this.bindings.length)
|
|
50
|
+
this.#uninstall();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Make `name` the active scope (for a modal, say): `'global'` bindings pause until the returned function is called. */
|
|
54
|
+
pushScope(name) {
|
|
55
|
+
this.scopes = [...untrack(() => this.scopes), name];
|
|
56
|
+
return () => {
|
|
57
|
+
const cur = untrack(() => this.scopes);
|
|
58
|
+
const i = cur.lastIndexOf(name);
|
|
59
|
+
if (i >= 0)
|
|
60
|
+
this.scopes = cur.filter((_, j) => j !== i);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
#install() {
|
|
64
|
+
if (this.#installed || typeof window === 'undefined')
|
|
65
|
+
return;
|
|
66
|
+
window.addEventListener('keydown', this.#onKey);
|
|
67
|
+
this.#installed = true;
|
|
68
|
+
}
|
|
69
|
+
#uninstall() {
|
|
70
|
+
if (!this.#installed)
|
|
71
|
+
return;
|
|
72
|
+
window.removeEventListener('keydown', this.#onKey);
|
|
73
|
+
this.#installed = false;
|
|
74
|
+
this.#reset();
|
|
75
|
+
}
|
|
76
|
+
#reset() { clearTimeout(this.#timer); this.#buffer = []; }
|
|
77
|
+
#onKey = (e) => {
|
|
78
|
+
if (e.isComposing || e.repeat || MODIFIER_KEYS.has(e.key))
|
|
79
|
+
return;
|
|
80
|
+
const typing = isTyping(e.target);
|
|
81
|
+
const scope = this.scope;
|
|
82
|
+
const live = this.bindings.filter((b) => b.scope === scope && (!typing || b.allowInInput) && (!b.when || b.when()));
|
|
83
|
+
const candidates = (buf) => live.filter((b) => b.steps.length >= buf.length && buf.every((s, i) => matches(s, b.steps[i])));
|
|
84
|
+
let buf = [...this.#buffer, snap(e)];
|
|
85
|
+
let found = candidates(buf);
|
|
86
|
+
if (!found.length && this.#buffer.length) {
|
|
87
|
+
buf = [snap(e)];
|
|
88
|
+
found = candidates(buf);
|
|
89
|
+
} // a stray key restarts the sequence
|
|
90
|
+
clearTimeout(this.#timer);
|
|
91
|
+
const exact = found.filter((b) => b.steps.length === buf.length);
|
|
92
|
+
if (exact.length) {
|
|
93
|
+
this.#buffer = [];
|
|
94
|
+
const b = exact[exact.length - 1];
|
|
95
|
+
if (b.preventDefault)
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
b.handler(e);
|
|
98
|
+
}
|
|
99
|
+
else if (found.length) {
|
|
100
|
+
this.#buffer = buf;
|
|
101
|
+
this.#timer = setTimeout(() => (this.#buffer = []), SEQUENCE_TIMEOUT);
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
this.#buffer = [];
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const isDev = () => !!import.meta.env?.DEV;
|
|
108
|
+
export const hotkeys = new HotkeyStore();
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export { default as Tabs } from './components/Tabs.svelte';
|
|
|
23
23
|
export { default as Accordion } from './components/Accordion.svelte';
|
|
24
24
|
export { default as SplitPane } from './components/SplitPane.svelte';
|
|
25
25
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
26
|
+
export { default as Kbd } from './components/Kbd.svelte';
|
|
27
|
+
export { default as HoverCard } from './components/HoverCard.svelte';
|
|
26
28
|
export { default as Spinner } from './components/Spinner.svelte';
|
|
27
29
|
export { default as ProgressBar } from './components/ProgressBar.svelte';
|
|
28
30
|
export { default as Skeleton } from './components/Skeleton.svelte';
|
|
@@ -41,16 +43,22 @@ export { default as Image } from './components/Image.svelte';
|
|
|
41
43
|
export { default as DescriptionList } from './components/DescriptionList.svelte';
|
|
42
44
|
export { default as DescriptionTerm } from './components/DescriptionTerm.svelte';
|
|
43
45
|
export { default as DescriptionDetail } from './components/DescriptionDetail.svelte';
|
|
46
|
+
export { default as List } from './components/List.svelte';
|
|
47
|
+
export { default as ListItem } from './components/ListItem.svelte';
|
|
44
48
|
export { default as Container } from './components/Container.svelte';
|
|
45
49
|
export { default as Stack } from './components/Stack.svelte';
|
|
46
50
|
export { default as Cluster } from './components/Cluster.svelte';
|
|
47
51
|
export { default as Grid } from './components/Grid.svelte';
|
|
52
|
+
export { default as GridLayout } from './components/GridLayout.svelte';
|
|
53
|
+
export type { GridItem } from './grid-layout';
|
|
48
54
|
export { default as Band } from './components/Band.svelte';
|
|
49
55
|
export { default as Hero } from './components/Hero.svelte';
|
|
50
56
|
export { default as Avatar } from './components/Avatar.svelte';
|
|
51
57
|
export { default as AvatarGroup } from './components/AvatarGroup.svelte';
|
|
52
58
|
export { default as ButtonGroup } from './components/ButtonGroup.svelte';
|
|
53
59
|
export { default as SegmentedControl } from './components/SegmentedControl.svelte';
|
|
60
|
+
export { default as Toolbar } from './components/Toolbar.svelte';
|
|
61
|
+
export { default as ToolbarSeparator } from './components/ToolbarSeparator.svelte';
|
|
54
62
|
export { default as Card } from './components/Card.svelte';
|
|
55
63
|
export { default as Panel } from './components/Panel.svelte';
|
|
56
64
|
/** @deprecated use Panel */
|
|
@@ -84,12 +92,23 @@ export { default as DataTable } from './components/DataTable.svelte';
|
|
|
84
92
|
export { default as Dialog } from './components/Dialog.svelte';
|
|
85
93
|
export { default as Toaster } from './components/Toaster.svelte';
|
|
86
94
|
export { default as DialogHost } from './components/DialogHost.svelte';
|
|
95
|
+
export { default as CommandPalette } from './components/CommandPalette.svelte';
|
|
96
|
+
export { default as ShortcutSheet } from './components/ShortcutSheet.svelte';
|
|
87
97
|
export { default as Icon } from './icons/Icon.svelte';
|
|
88
98
|
export { toasts } from './toast.svelte.js';
|
|
89
99
|
export { dialogs } from './dialogs.svelte.js';
|
|
100
|
+
export { hotkeys } from './hotkeys.svelte.js';
|
|
101
|
+
export { commands, registerCommand, fuzzyScore } from './commands.svelte.js';
|
|
102
|
+
export { parseCombo, matches, formatCombo, formatShortcut, ariaKeyShortcuts, isApple } from './keys.js';
|
|
90
103
|
export type { IconName, IconDef } from './icons/paths.js';
|
|
91
|
-
export type { ColumnFilters, SortState, TreeNode, UploadItem, MenuEntry, Option, DateRange, Column, MenuItem, Intent, IconSource } from './types.js';
|
|
92
|
-
export
|
|
104
|
+
export type { ColumnFilters, SortState, TreeNode, UploadItem, MenuEntry, Option, DateRange, Column, ColumnType, MenuItem, Intent, IconSource } from './types.js';
|
|
105
|
+
export { serializeRows, toCsv, toTsv, toJson, displayValue } from './table-export.js';
|
|
106
|
+
export type { ExportFormat, ExportOptions, ExportSerializer } from './table-export.js';
|
|
107
|
+
export type { Combo } from './keys.js';
|
|
108
|
+
export type { Hotkey, HotkeyOptions } from './hotkeys.svelte.js';
|
|
109
|
+
export type { Command } from './commands.svelte.js';
|
|
110
|
+
export type { PaletteItem, PaletteProvider } from './components/CommandPalette.svelte';
|
|
111
|
+
export type { Toast, ToastIntent, ToastAction } from './toast.svelte.js';
|
|
93
112
|
export type { AlertOptions, ConfirmOptions, PromptOptions, DialogIntent, DialogKind } from './dialogs.svelte.js';
|
|
94
113
|
export { parseDuration, formatDuration } from './duration.js';
|
|
95
114
|
export type { DurationUnit } from './duration.js';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export { default as Tabs } from './components/Tabs.svelte';
|
|
|
23
23
|
export { default as Accordion } from './components/Accordion.svelte';
|
|
24
24
|
export { default as SplitPane } from './components/SplitPane.svelte';
|
|
25
25
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
26
|
+
export { default as Kbd } from './components/Kbd.svelte';
|
|
27
|
+
export { default as HoverCard } from './components/HoverCard.svelte';
|
|
26
28
|
export { default as Spinner } from './components/Spinner.svelte';
|
|
27
29
|
export { default as ProgressBar } from './components/ProgressBar.svelte';
|
|
28
30
|
export { default as Skeleton } from './components/Skeleton.svelte';
|
|
@@ -41,16 +43,21 @@ export { default as Image } from './components/Image.svelte';
|
|
|
41
43
|
export { default as DescriptionList } from './components/DescriptionList.svelte';
|
|
42
44
|
export { default as DescriptionTerm } from './components/DescriptionTerm.svelte';
|
|
43
45
|
export { default as DescriptionDetail } from './components/DescriptionDetail.svelte';
|
|
46
|
+
export { default as List } from './components/List.svelte';
|
|
47
|
+
export { default as ListItem } from './components/ListItem.svelte';
|
|
44
48
|
export { default as Container } from './components/Container.svelte';
|
|
45
49
|
export { default as Stack } from './components/Stack.svelte';
|
|
46
50
|
export { default as Cluster } from './components/Cluster.svelte';
|
|
47
51
|
export { default as Grid } from './components/Grid.svelte';
|
|
52
|
+
export { default as GridLayout } from './components/GridLayout.svelte';
|
|
48
53
|
export { default as Band } from './components/Band.svelte';
|
|
49
54
|
export { default as Hero } from './components/Hero.svelte';
|
|
50
55
|
export { default as Avatar } from './components/Avatar.svelte';
|
|
51
56
|
export { default as AvatarGroup } from './components/AvatarGroup.svelte';
|
|
52
57
|
export { default as ButtonGroup } from './components/ButtonGroup.svelte';
|
|
53
58
|
export { default as SegmentedControl } from './components/SegmentedControl.svelte';
|
|
59
|
+
export { default as Toolbar } from './components/Toolbar.svelte';
|
|
60
|
+
export { default as ToolbarSeparator } from './components/ToolbarSeparator.svelte';
|
|
54
61
|
export { default as Card } from './components/Card.svelte';
|
|
55
62
|
export { default as Panel } from './components/Panel.svelte';
|
|
56
63
|
/** @deprecated use Panel */
|
|
@@ -84,8 +91,14 @@ export { default as DataTable } from './components/DataTable.svelte';
|
|
|
84
91
|
export { default as Dialog } from './components/Dialog.svelte';
|
|
85
92
|
export { default as Toaster } from './components/Toaster.svelte';
|
|
86
93
|
export { default as DialogHost } from './components/DialogHost.svelte';
|
|
94
|
+
export { default as CommandPalette } from './components/CommandPalette.svelte';
|
|
95
|
+
export { default as ShortcutSheet } from './components/ShortcutSheet.svelte';
|
|
87
96
|
export { default as Icon } from './icons/Icon.svelte';
|
|
88
97
|
export { toasts } from './toast.svelte.js';
|
|
89
98
|
export { dialogs } from './dialogs.svelte.js';
|
|
99
|
+
export { hotkeys } from './hotkeys.svelte.js';
|
|
100
|
+
export { commands, registerCommand, fuzzyScore } from './commands.svelte.js';
|
|
101
|
+
export { parseCombo, matches, formatCombo, formatShortcut, ariaKeyShortcuts, isApple } from './keys.js';
|
|
102
|
+
export { serializeRows, toCsv, toTsv, toJson, displayValue } from './table-export.js';
|
|
90
103
|
export { parseDuration, formatDuration } from './duration.js';
|
|
91
104
|
export { parseHex, toHex, contrast } from './color.js';
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** A single parsed key combination, e.g. `mod+shift+k`. `mod` is Cmd on Apple platforms and Ctrl elsewhere. */
|
|
2
|
+
export type Combo = {
|
|
3
|
+
key: string;
|
|
4
|
+
mod: boolean;
|
|
5
|
+
ctrl: boolean;
|
|
6
|
+
meta: boolean;
|
|
7
|
+
alt: boolean;
|
|
8
|
+
shift: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** True on macOS and iOS. SSR-safe: false when there is no `navigator`. */
|
|
11
|
+
export declare function isApple(): boolean;
|
|
12
|
+
/** Parse `'mod+shift+k'` (case-insensitive; `esc`, `space`, `up`... are accepted). A lone `+` key is written `plus`. */
|
|
13
|
+
export declare function parseCombo(combo: string): Combo;
|
|
14
|
+
/** Split a shortcut into its steps: `'g h'` is a two-step sequence, `'mod+k'` a single step. */
|
|
15
|
+
export declare const parseSequence: (shortcut: string) => Combo[];
|
|
16
|
+
/** Whether a keyboard event is exactly this combo. Shift is not required for symbol keys such as `?`. */
|
|
17
|
+
export declare function matches(e: Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>, combo: Combo | string, apple?: boolean): boolean;
|
|
18
|
+
type Platform = 'apple' | 'other' | boolean;
|
|
19
|
+
/** The labels to show for a combo, one per key: `['⌘', '⇧', 'K']` on Apple, `['Ctrl', 'Shift', 'K']` elsewhere. */
|
|
20
|
+
export declare function formatCombo(combo: Combo | string, platform?: Platform): string[];
|
|
21
|
+
/** Format a whole shortcut: one array of key labels per sequence step. */
|
|
22
|
+
export declare const formatShortcut: (shortcut: string, platform?: Platform) => string[][];
|
|
23
|
+
/** The `aria-keyshortcuts` value for a shortcut (`Control+K`, `Meta+Shift+K`), or undefined for sequences, which ARIA cannot express. */
|
|
24
|
+
export declare function ariaKeyShortcuts(shortcut: string, platform?: Platform): string | undefined;
|
|
25
|
+
export {};
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const ALIASES = {
|
|
2
|
+
esc: 'escape', return: 'enter', space: ' ', spacebar: ' ', del: 'delete', up: 'arrowup', down: 'arrowdown', left: 'arrowleft', right: 'arrowright',
|
|
3
|
+
plus: '+', comma: ',', period: '.', slash: '/', cmd: 'meta', command: 'meta', win: 'meta', super: 'meta', control: 'ctrl', option: 'alt', opt: 'alt'
|
|
4
|
+
};
|
|
5
|
+
const MODIFIERS = new Set(['mod', 'ctrl', 'meta', 'alt', 'shift']);
|
|
6
|
+
/** True on macOS and iOS. SSR-safe: false when there is no `navigator`. */
|
|
7
|
+
export function isApple() {
|
|
8
|
+
if (typeof navigator === 'undefined')
|
|
9
|
+
return false;
|
|
10
|
+
const p = navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent ?? '';
|
|
11
|
+
return /mac|iphone|ipad|ipod/i.test(p);
|
|
12
|
+
}
|
|
13
|
+
/** Parse `'mod+shift+k'` (case-insensitive; `esc`, `space`, `up`... are accepted). A lone `+` key is written `plus`. */
|
|
14
|
+
export function parseCombo(combo) {
|
|
15
|
+
const c = { key: '', mod: false, ctrl: false, meta: false, alt: false, shift: false };
|
|
16
|
+
const parts = combo.trim().split('+');
|
|
17
|
+
for (let i = 0; i < parts.length; i++) {
|
|
18
|
+
let p = parts[i].trim().toLowerCase();
|
|
19
|
+
if (!p && i === parts.length - 1 && i > 0)
|
|
20
|
+
p = '+'; // "ctrl++": the key is "+"
|
|
21
|
+
if (!p)
|
|
22
|
+
continue;
|
|
23
|
+
p = ALIASES[p] ?? p;
|
|
24
|
+
if (MODIFIERS.has(p) && i < parts.length - 1)
|
|
25
|
+
c[p] = true;
|
|
26
|
+
else
|
|
27
|
+
c.key = p;
|
|
28
|
+
}
|
|
29
|
+
return c;
|
|
30
|
+
}
|
|
31
|
+
/** Split a shortcut into its steps: `'g h'` is a two-step sequence, `'mod+k'` a single step. */
|
|
32
|
+
export const parseSequence = (shortcut) => shortcut.trim().split(/\s+/).filter(Boolean).map(parseCombo);
|
|
33
|
+
/** Whether a keyboard event is exactly this combo. Shift is not required for symbol keys such as `?`. */
|
|
34
|
+
export function matches(e, combo, apple = isApple()) {
|
|
35
|
+
const c = typeof combo === 'string' ? parseCombo(combo) : combo;
|
|
36
|
+
if (!c.key)
|
|
37
|
+
return false;
|
|
38
|
+
const wantCtrl = c.ctrl || (c.mod && !apple);
|
|
39
|
+
const wantMeta = c.meta || (c.mod && apple);
|
|
40
|
+
if (e.ctrlKey !== wantCtrl || e.metaKey !== wantMeta || e.altKey !== c.alt)
|
|
41
|
+
return false;
|
|
42
|
+
const symbol = c.key.length === 1 && !/[a-z0-9]/i.test(c.key) && c.key !== ' ';
|
|
43
|
+
if (!symbol && e.shiftKey !== c.shift)
|
|
44
|
+
return false;
|
|
45
|
+
if (symbol && c.shift && !e.shiftKey)
|
|
46
|
+
return false;
|
|
47
|
+
return (ALIASES[e.key.toLowerCase()] ?? e.key.toLowerCase()) === c.key;
|
|
48
|
+
}
|
|
49
|
+
const GLYPHS = { arrowup: '↑', arrowdown: '↓', arrowleft: '←', arrowright: '→', enter: '↵', escape: 'Esc', ' ': 'Space', backspace: '⌫', delete: 'Del', tab: 'Tab' };
|
|
50
|
+
const isMac = (p) => p === true || p === 'apple';
|
|
51
|
+
/** The labels to show for a combo, one per key: `['⌘', '⇧', 'K']` on Apple, `['Ctrl', 'Shift', 'K']` elsewhere. */
|
|
52
|
+
export function formatCombo(combo, platform = isApple()) {
|
|
53
|
+
const c = typeof combo === 'string' ? parseCombo(combo) : combo;
|
|
54
|
+
const apple = isMac(platform);
|
|
55
|
+
const out = [];
|
|
56
|
+
if (c.ctrl || (c.mod && !apple))
|
|
57
|
+
out.push(apple ? '⌃' : 'Ctrl');
|
|
58
|
+
if (c.alt)
|
|
59
|
+
out.push(apple ? '⌥' : 'Alt');
|
|
60
|
+
if (c.shift)
|
|
61
|
+
out.push(apple ? '⇧' : 'Shift');
|
|
62
|
+
if (c.meta || (c.mod && apple))
|
|
63
|
+
out.push(apple ? '⌘' : 'Win');
|
|
64
|
+
if (c.key)
|
|
65
|
+
out.push(GLYPHS[c.key] ?? (c.key.length === 1 ? c.key.toUpperCase() : c.key));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/** Format a whole shortcut: one array of key labels per sequence step. */
|
|
69
|
+
export const formatShortcut = (shortcut, platform) => parseSequence(shortcut).map((c) => formatCombo(c, platform));
|
|
70
|
+
/** The `aria-keyshortcuts` value for a shortcut (`Control+K`, `Meta+Shift+K`), or undefined for sequences, which ARIA cannot express. */
|
|
71
|
+
export function ariaKeyShortcuts(shortcut, platform = isApple()) {
|
|
72
|
+
const steps = parseSequence(shortcut);
|
|
73
|
+
if (steps.length !== 1 || !steps[0].key)
|
|
74
|
+
return undefined;
|
|
75
|
+
const c = steps[0];
|
|
76
|
+
const apple = isMac(platform);
|
|
77
|
+
const out = [];
|
|
78
|
+
if (c.ctrl || (c.mod && !apple))
|
|
79
|
+
out.push('Control');
|
|
80
|
+
if (c.meta || (c.mod && apple))
|
|
81
|
+
out.push('Meta');
|
|
82
|
+
if (c.alt)
|
|
83
|
+
out.push('Alt');
|
|
84
|
+
if (c.shift)
|
|
85
|
+
out.push('Shift');
|
|
86
|
+
out.push(c.key === ' ' ? 'Space' : c.key.length === 1 ? c.key.toUpperCase() : c.key[0].toUpperCase() + c.key.slice(1));
|
|
87
|
+
return out.join('+');
|
|
88
|
+
}
|
package/dist/roving.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roving tabindex over the focusable descendants of a container: exactly one item is in the tab order,
|
|
3
|
+
* arrow keys / Home / End move between items, disabled and hidden items are skipped.
|
|
4
|
+
* Shared by Toolbar and the document variant of Tabs.
|
|
5
|
+
*/
|
|
6
|
+
export type RovingOptions = {
|
|
7
|
+
orientation?: 'horizontal' | 'vertical';
|
|
8
|
+
/** Which descendants are items (default: every focusable element, minus `[data-roving-skip]`). */
|
|
9
|
+
selector?: string;
|
|
10
|
+
/** Wrap from the last item to the first (default true). */
|
|
11
|
+
wrap?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** The items in DOM order. */
|
|
14
|
+
export declare function rovingItems(root: HTMLElement, o?: RovingOptions): HTMLElement[];
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a keydown to the item that should receive focus (null when the key is not ours).
|
|
17
|
+
* Does not move focus; callers do that, so they can also activate.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rovingTarget(e: KeyboardEvent, root: HTMLElement, o?: RovingOptions): HTMLElement | null;
|
|
20
|
+
/** Make `active` the only tab stop among the items (all others get tabindex -1). */
|
|
21
|
+
export declare function setRovingActive(root: HTMLElement, active: HTMLElement | undefined, o?: RovingOptions): void;
|
|
22
|
+
/** Svelte action: roving focus for a container (keyboard, focus tracking, and re-sync when its content changes). */
|
|
23
|
+
export declare function roving(node: HTMLElement, getOptions?: () => RovingOptions): {
|
|
24
|
+
destroy(): void;
|
|
25
|
+
};
|
package/dist/roving.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const FOCUSABLE = 'a[href], button, input:not([type="hidden"]), select, textarea, summary, [tabindex], [contenteditable=""], [contenteditable="true"]';
|
|
2
|
+
/** Nested composite widgets keep their own arrow-key handling; the container treats each as one item. */
|
|
3
|
+
const COMPOSITE = '[role="radiogroup"], [role="tablist"], [role="listbox"]';
|
|
4
|
+
const usable = (el) => !el.matches(':disabled, [aria-disabled="true"], [data-roving-skip], [inert] *, [hidden] *') && !el.hidden && el.getClientRects().length > 0;
|
|
5
|
+
/** The items in DOM order. */
|
|
6
|
+
export function rovingItems(root, o = {}) {
|
|
7
|
+
const all = [...root.querySelectorAll(o.selector ?? FOCUSABLE)].filter(usable);
|
|
8
|
+
if (o.selector)
|
|
9
|
+
return all;
|
|
10
|
+
// one representative per nested composite widget (its selected member, else the first)
|
|
11
|
+
const reps = new Map();
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const el of all) {
|
|
14
|
+
const group = el.parentElement?.closest(COMPOSITE);
|
|
15
|
+
if (!group || !root.contains(group) || group === root) {
|
|
16
|
+
out.push(el);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const rep = reps.get(group);
|
|
20
|
+
if (!rep) {
|
|
21
|
+
reps.set(group, el);
|
|
22
|
+
out.push(el);
|
|
23
|
+
}
|
|
24
|
+
else if (el.matches('[aria-checked="true"], [aria-selected="true"]')) {
|
|
25
|
+
out[out.indexOf(rep)] = el;
|
|
26
|
+
reps.set(group, el);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
const isText = (el) => el.isContentEditable ||
|
|
32
|
+
el instanceof HTMLTextAreaElement ||
|
|
33
|
+
(el instanceof HTMLInputElement && !/^(button|checkbox|radio|range|color|file|image|reset|submit)$/.test(el.type));
|
|
34
|
+
/** True when the caret sits at the edge of a text field in the direction of travel (the arrow key has nothing left to do there). */
|
|
35
|
+
function textEdge(el, dir) {
|
|
36
|
+
if (el.isContentEditable)
|
|
37
|
+
return false;
|
|
38
|
+
const f = el;
|
|
39
|
+
try {
|
|
40
|
+
if (f.selectionStart !== f.selectionEnd)
|
|
41
|
+
return false;
|
|
42
|
+
return dir < 0 ? f.selectionStart === 0 : f.selectionStart === f.value.length;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false; // input types without a caret (number, email…) never give the arrow key up
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a keydown to the item that should receive focus (null when the key is not ours).
|
|
50
|
+
* Does not move focus; callers do that, so they can also activate.
|
|
51
|
+
*/
|
|
52
|
+
export function rovingTarget(e, root, o = {}) {
|
|
53
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)
|
|
54
|
+
return null;
|
|
55
|
+
const vertical = o.orientation === 'vertical';
|
|
56
|
+
const rtl = !vertical && getComputedStyle(root).direction === 'rtl';
|
|
57
|
+
const next = vertical ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
58
|
+
const prev = vertical ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
59
|
+
if (e.key !== next && e.key !== prev && e.key !== 'Home' && e.key !== 'End')
|
|
60
|
+
return null;
|
|
61
|
+
const els = rovingItems(root, o);
|
|
62
|
+
const target = e.target;
|
|
63
|
+
const from = els.findIndex((x) => x === target || x.contains(target));
|
|
64
|
+
if (from < 0)
|
|
65
|
+
return null;
|
|
66
|
+
const dir = e.key === next ? 1 : -1;
|
|
67
|
+
// text fields keep Home/End and their own caret movement
|
|
68
|
+
if (isText(target) && (e.key === 'Home' || e.key === 'End' || !textEdge(target, dir)))
|
|
69
|
+
return null;
|
|
70
|
+
const n = els.length;
|
|
71
|
+
let to;
|
|
72
|
+
if (e.key === 'Home')
|
|
73
|
+
to = 0;
|
|
74
|
+
else if (e.key === 'End')
|
|
75
|
+
to = n - 1;
|
|
76
|
+
else if (o.wrap === false)
|
|
77
|
+
to = Math.min(n - 1, Math.max(0, from + dir));
|
|
78
|
+
else
|
|
79
|
+
to = (from + dir + n) % n;
|
|
80
|
+
return els[to] ?? null;
|
|
81
|
+
}
|
|
82
|
+
/** Make `active` the only tab stop among the items (all others get tabindex -1). */
|
|
83
|
+
export function setRovingActive(root, active, o = {}) {
|
|
84
|
+
const els = rovingItems(root, o);
|
|
85
|
+
const keep = active && els.includes(active) ? active : els[0];
|
|
86
|
+
for (const el of els)
|
|
87
|
+
el.tabIndex = el === keep ? 0 : -1;
|
|
88
|
+
// non-representative members of nested composites stay out of the tab order too
|
|
89
|
+
if (!o.selector)
|
|
90
|
+
for (const g of root.querySelectorAll(COMPOSITE))
|
|
91
|
+
for (const el of g.querySelectorAll(FOCUSABLE))
|
|
92
|
+
if (!els.includes(el))
|
|
93
|
+
el.tabIndex = -1;
|
|
94
|
+
}
|
|
95
|
+
/** Svelte action: roving focus for a container (keyboard, focus tracking, and re-sync when its content changes). */
|
|
96
|
+
export function roving(node, getOptions = () => ({})) {
|
|
97
|
+
let active;
|
|
98
|
+
const sync = () => setRovingActive(node, active && node.contains(active) ? active : undefined, getOptions());
|
|
99
|
+
const onKey = (e) => {
|
|
100
|
+
const to = rovingTarget(e, node, getOptions());
|
|
101
|
+
if (!to)
|
|
102
|
+
return;
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
active = to;
|
|
105
|
+
sync();
|
|
106
|
+
to.focus();
|
|
107
|
+
};
|
|
108
|
+
const onFocusIn = (e) => {
|
|
109
|
+
const t = e.target;
|
|
110
|
+
const item = rovingItems(node, getOptions()).find((x) => x === t || x.contains(t));
|
|
111
|
+
if (!item)
|
|
112
|
+
return;
|
|
113
|
+
active = item;
|
|
114
|
+
sync();
|
|
115
|
+
t.tabIndex = 0; // a nested composite's own focused member stays reachable
|
|
116
|
+
};
|
|
117
|
+
node.addEventListener('keydown', onKey);
|
|
118
|
+
node.addEventListener('focusin', onFocusIn);
|
|
119
|
+
const mo = new MutationObserver(sync);
|
|
120
|
+
mo.observe(node, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled', 'hidden', 'aria-disabled'] });
|
|
121
|
+
sync();
|
|
122
|
+
return {
|
|
123
|
+
destroy() {
|
|
124
|
+
node.removeEventListener('keydown', onKey);
|
|
125
|
+
node.removeEventListener('focusin', onFocusIn);
|
|
126
|
+
mo.disconnect();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
package/dist/styles/base.css
CHANGED