@dorsk/tsumikit 0.53.0 → 0.55.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 +29 -2
- package/dist/anchor.d.ts +10 -0
- package/dist/anchor.js +1 -0
- package/dist/components/layouts/Toolbar.svelte +83 -2
- package/dist/components/layouts/Toolbar.svelte.d.ts +4 -0
- package/dist/components/layouts/toolbar-roving.d.ts +53 -0
- package/dist/components/layouts/toolbar-roving.js +104 -0
- package/dist/components/molecules/FilterInput.svelte +7 -2
- package/dist/components/molecules/FilterInput.svelte.d.ts +3 -1
- package/dist/components/molecules/Menu.svelte +7 -0
- package/dist/components/molecules/Menu.svelte.d.ts +6 -0
- package/dist/components/molecules/Popover.svelte +36 -31
- package/dist/components/molecules/Popover.svelte.d.ts +3 -1
- package/dist/components/molecules/Tabs.svelte +14 -3
- package/dist/components/molecules/Tabs.svelte.d.ts +9 -1
- package/dist/components/molecules/ThemePicker.svelte +7 -2
- package/dist/components/molecules/ThemePicker.svelte.d.ts +3 -1
- package/dist/components/organisms/FilterSearchBar.svelte +7 -2
- package/dist/components/organisms/FilterSearchBar.svelte.d.ts +3 -1
- package/dist/index.d.ts +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -222,6 +222,23 @@ renders, and the trigger belongs to `Popover`, so `.toolbar .my-trigger` compile
|
|
|
222
222
|
to `.toolbar.svelte-x .my-trigger.svelte-x` and matches nothing. Use `style` and
|
|
223
223
|
the properties above instead.
|
|
224
224
|
|
|
225
|
+
### Anchoring to a component
|
|
226
|
+
|
|
227
|
+
Components that render a single addressable element forward rest props onto it,
|
|
228
|
+
so `data-journey`, `data-testid` and analytics attributes land where you expect.
|
|
229
|
+
`Popover` puts them on its **trigger** (so `Menu`, `ThemePicker` and
|
|
230
|
+
`FontScalePicker` forward through to it), `Tabs` on its root — with a per-tab
|
|
231
|
+
`attrs` bag on each `TabItem` — `FilterInput` on its root, and `FilterSearchBar`
|
|
232
|
+
forwards onto that. `Menu` items take an `attrs` bag. Components that forward
|
|
233
|
+
into another component take `AnchorAttributes` (`data-*` only), which cannot
|
|
234
|
+
collide with the child's own props; the rest take full `HTMLAttributes`. The
|
|
235
|
+
component's own `role`, `aria-*`, `class` and keyboard wiring are applied after
|
|
236
|
+
the spread and always win, so an anchor can never break behaviour; `class` in a
|
|
237
|
+
rest/`attrs` bag is therefore dropped — use `class` / `triggerClass`.
|
|
238
|
+
|
|
239
|
+
Never target the private `data-tsu="…"` markers: they are internal, unguarded by
|
|
240
|
+
any test, and change without a major.
|
|
241
|
+
|
|
225
242
|
## Components
|
|
226
243
|
|
|
227
244
|
**Atoms:** Text, Heading, Button, Input (`icon` inset leading glyph,
|
|
@@ -241,7 +258,8 @@ always/hover/none, `align`), Icon (open registry — pass a `children` snippet f
|
|
|
241
258
|
any custom SVG).
|
|
242
259
|
|
|
243
260
|
**Molecules:** Field (`grow`), IconButton, SelectButton, Toggle, OptionButton, Modal,
|
|
244
|
-
Popover, Menu (items take a free-form trailing `tag` + `tagTone`, or a `tag` snippet
|
|
261
|
+
Popover, Menu (items take a free-form trailing `tag` + `tagTone`, or a `tag` snippet,
|
|
262
|
+
and an `attrs` object for `data-*`/test ids on the row),
|
|
245
263
|
Tabs, RadioGroup (`variant="rows"`: bordered rows, per-option `note`/`description`,
|
|
246
264
|
`action(option)` trailing control that never toggles, `below(option)` inline panel),
|
|
247
265
|
Tooltip, Accordion, CopyButton, FileButton,
|
|
@@ -523,6 +541,14 @@ triggers, and `SegmentedControl`. Each renders an exact
|
|
|
523
541
|
original ghost icon-button default. Use `control` on `Button` or `Popover` when
|
|
524
542
|
the roomier shared `--control-height` composer contract is required.
|
|
525
543
|
|
|
544
|
+
`Toolbar` implements the keyboard contract its `role="toolbar"` announces: the
|
|
545
|
+
bar is a **single tab stop**, and `←`/`→` (plus `Home`/`End`) move between the
|
|
546
|
+
controls that are actually visible — collapsed `data-overflow` children drop out
|
|
547
|
+
of the ring and the `…` trigger is its last stop. Controls that own the arrow
|
|
548
|
+
keys themselves (text inputs, `select`, sliders) keep them. Pass
|
|
549
|
+
`roving={false}` to opt out; that drops the role along with the promise, leaving
|
|
550
|
+
every child an ordinary tab stop.
|
|
551
|
+
|
|
526
552
|
Button and Popover share the same semantic tones. For a confirmed positive
|
|
527
553
|
action, `tone="success"` gives neutral controls a success tint; combine it with
|
|
528
554
|
`variant="primary"` for a filled success action without consumer CSS.
|
|
@@ -610,7 +636,8 @@ label, copy, line numbers, wrap, scroll) and takes code three ways: plain
|
|
|
610
636
|
`disabled` and native events pass through.
|
|
611
637
|
- Visible `:focus-visible` rings; ARIA patterns implemented for switch, menu
|
|
612
638
|
(`role=menu` + roving focus), tabs (`tablist` + arrow keys), radiogroup,
|
|
613
|
-
dialog; polite live region for
|
|
639
|
+
toolbar (`role=toolbar` + roving tabindex), dialog; polite live region for
|
|
640
|
+
toasts; `.sr-only`.
|
|
614
641
|
- Mobile-first: one `min-width: 640px` breakpoint, bottom-sheet→centered-modal,
|
|
615
642
|
safe-area insets, 16px-min inputs (no iOS zoom).
|
|
616
643
|
- A verified color-blind-safe theme (Okabe-Ito); meaning never relies on hue
|
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `data-*` attributes for addressing a component later — tour anchors, test ids.
|
|
3
|
+
*
|
|
4
|
+
* Components rendering their own element take a full `HTMLAttributes` rest
|
|
5
|
+
* instead; this narrower bag is for those that forward into another component,
|
|
6
|
+
* where a full attribute type collides with the child's narrower props.
|
|
7
|
+
*/
|
|
8
|
+
export type AnchorAttributes = {
|
|
9
|
+
[key: `data-${string}`]: string | number | boolean | null | undefined;
|
|
10
|
+
};
|
package/dist/anchor.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
// every child marked `data-overflow` is hidden and a `…` button opens a Menu
|
|
4
4
|
// with `items` (or the `overflow` snippet in a Popover) that stands in for
|
|
5
5
|
// them. `sticky` pins the bar under `stickyOffset`.
|
|
6
|
+
//
|
|
7
|
+
// `role="toolbar"` is a promise of keyboard behaviour, so the bar keeps it:
|
|
8
|
+
// roving tabindex makes it one tab stop and ←/→/Home/End move between the
|
|
9
|
+
// controls actually visible, the `…` trigger included. Children are arbitrary
|
|
10
|
+
// snippet content, so the ring is read from the DOM rather than a list prop.
|
|
6
11
|
import type { Snippet } from 'svelte';
|
|
7
12
|
import IconButton from '../molecules/IconButton.svelte';
|
|
8
13
|
import Menu, { type MenuItem } from '../molecules/Menu.svelte';
|
|
9
14
|
import Popover from '../molecules/Popover.svelte';
|
|
15
|
+
import {
|
|
16
|
+
consumesArrowKeys,
|
|
17
|
+
isToolbarNavKey,
|
|
18
|
+
nextToolbarStop,
|
|
19
|
+
TOOLBAR_STOP_ATTR,
|
|
20
|
+
toolbarStops
|
|
21
|
+
} from './toolbar-roving.js';
|
|
10
22
|
|
|
11
23
|
let {
|
|
12
24
|
children,
|
|
@@ -18,6 +30,7 @@
|
|
|
18
30
|
density = 'default',
|
|
19
31
|
overflowLabel = 'More',
|
|
20
32
|
label,
|
|
33
|
+
roving = true,
|
|
21
34
|
class: klass = '',
|
|
22
35
|
style: styleProp = ''
|
|
23
36
|
}: {
|
|
@@ -34,6 +47,10 @@
|
|
|
34
47
|
overflowLabel?: string;
|
|
35
48
|
/** Accessible name of the bar (`role="toolbar"`). */
|
|
36
49
|
label?: string;
|
|
50
|
+
/** Roving tabindex: the bar is one tab stop and ←/→ (plus Home/End) move
|
|
51
|
+
* between its controls. `false` drops `role="toolbar"` along with the
|
|
52
|
+
* keyboard contract it promises, leaving children as ordinary tab stops. */
|
|
53
|
+
roving?: boolean;
|
|
37
54
|
class?: string;
|
|
38
55
|
style?: string;
|
|
39
56
|
} = $props();
|
|
@@ -64,13 +81,77 @@
|
|
|
64
81
|
});
|
|
65
82
|
|
|
66
83
|
const hasOverflow = $derived(!!items?.length || !!overflow);
|
|
84
|
+
|
|
85
|
+
let activeStop: HTMLElement | null = null;
|
|
86
|
+
|
|
87
|
+
// Collapsed `data-overflow` children are `display: none`, so they have no box —
|
|
88
|
+
// that, not a class check, is what takes them out of the ring.
|
|
89
|
+
const stops = () => toolbarStops(el, (node) => node.getClientRects().length === 0);
|
|
90
|
+
|
|
91
|
+
function rove(list: HTMLElement[], index: number) {
|
|
92
|
+
activeStop = list[index] ?? null;
|
|
93
|
+
list.forEach((node, i) => {
|
|
94
|
+
node.setAttribute(TOOLBAR_STOP_ATTR, '');
|
|
95
|
+
node.tabIndex = i === index ? 0 : -1;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sync() {
|
|
100
|
+
const list = stops();
|
|
101
|
+
if (!list.length) return;
|
|
102
|
+
const current = activeStop ? list.indexOf(activeStop) : -1;
|
|
103
|
+
rove(list, current < 0 ? 0 : current);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
$effect(() => {
|
|
107
|
+
if (!roving || !el) return;
|
|
108
|
+
const node = el;
|
|
109
|
+
sync();
|
|
110
|
+
// Only attributes the caller owns are observed: writing tabindex and
|
|
111
|
+
// TOOLBAR_STOP_ATTR back must not re-enter this.
|
|
112
|
+
const mo = new MutationObserver(sync);
|
|
113
|
+
mo.observe(node, {
|
|
114
|
+
childList: true,
|
|
115
|
+
subtree: true,
|
|
116
|
+
attributes: true,
|
|
117
|
+
attributeFilter: ['disabled', 'hidden', 'aria-hidden', 'class', 'style']
|
|
118
|
+
});
|
|
119
|
+
return () => mo.disconnect();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function onKeydown(event: KeyboardEvent) {
|
|
123
|
+
if (!roving || event.defaultPrevented) return;
|
|
124
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
125
|
+
if (!isToolbarNavKey(event.key)) return;
|
|
126
|
+
const target = event.target as HTMLElement | null;
|
|
127
|
+
if (consumesArrowKeys(target)) return;
|
|
128
|
+
const list = stops();
|
|
129
|
+
const next = nextToolbarStop(
|
|
130
|
+
list.length,
|
|
131
|
+
list.findIndex((node) => node.contains(target)),
|
|
132
|
+
event.key
|
|
133
|
+
);
|
|
134
|
+
if (next === undefined) return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
rove(list, next);
|
|
137
|
+
list[next].focus();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function onFocusin(event: FocusEvent) {
|
|
141
|
+
if (!roving) return;
|
|
142
|
+
const list = stops();
|
|
143
|
+
const index = list.findIndex((node) => node.contains(event.target as Node));
|
|
144
|
+
if (index >= 0 && list[index] !== activeStop) rove(list, index);
|
|
145
|
+
}
|
|
67
146
|
</script>
|
|
68
147
|
|
|
69
148
|
<div
|
|
70
149
|
bind:this={el}
|
|
71
150
|
data-tsu="Toolbar"
|
|
72
|
-
role=
|
|
73
|
-
aria-label={label}
|
|
151
|
+
role={roving ? 'toolbar' : undefined}
|
|
152
|
+
aria-label={roving ? label : undefined}
|
|
153
|
+
onkeydown={onKeydown}
|
|
154
|
+
onfocusin={onFocusin}
|
|
74
155
|
class="toolbar density-{density} {klass}"
|
|
75
156
|
class:sticky
|
|
76
157
|
class:collapsed
|
|
@@ -14,6 +14,10 @@ type $$ComponentProps = {
|
|
|
14
14
|
overflowLabel?: string;
|
|
15
15
|
/** Accessible name of the bar (`role="toolbar"`). */
|
|
16
16
|
label?: string;
|
|
17
|
+
/** Roving tabindex: the bar is one tab stop and ←/→ (plus Home/End) move
|
|
18
|
+
* between its controls. `false` drops `role="toolbar"` along with the
|
|
19
|
+
* keyboard contract it promises, leaving children as ordinary tab stops. */
|
|
20
|
+
roving?: boolean;
|
|
17
21
|
class?: string;
|
|
18
22
|
style?: string;
|
|
19
23
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** @param {string} key */
|
|
2
|
+
export function isToolbarNavKey(key: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Controls that own the arrow keys themselves (caret, spinner, native listbox):
|
|
5
|
+
* the toolbar must not steal them.
|
|
6
|
+
*
|
|
7
|
+
* @param {{ tagName?: string; type?: string; isContentEditable?: boolean } | null | undefined} el
|
|
8
|
+
*/
|
|
9
|
+
export function consumesArrowKeys(el: {
|
|
10
|
+
tagName?: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
isContentEditable?: boolean;
|
|
13
|
+
} | null | undefined): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* @param {{
|
|
16
|
+
* disabled?: boolean;
|
|
17
|
+
* hidden?: boolean;
|
|
18
|
+
* ariaHidden?: boolean;
|
|
19
|
+
* inPopover?: boolean;
|
|
20
|
+
* tabindex?: string | null;
|
|
21
|
+
* claimed?: boolean;
|
|
22
|
+
* }} el
|
|
23
|
+
*/
|
|
24
|
+
export function acceptsRovingStop(el: {
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
hidden?: boolean;
|
|
27
|
+
ariaHidden?: boolean;
|
|
28
|
+
inPopover?: boolean;
|
|
29
|
+
tabindex?: string | null;
|
|
30
|
+
claimed?: boolean;
|
|
31
|
+
}): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The ring, in DOM order. `isHidden` is injected because "has no box" is the only
|
|
34
|
+
* honest test for a collapsed child and it cannot be computed without layout.
|
|
35
|
+
*
|
|
36
|
+
* @param {Element | null} container
|
|
37
|
+
* @param {(node: HTMLElement) => boolean} isHidden
|
|
38
|
+
* @returns {HTMLElement[]}
|
|
39
|
+
*/
|
|
40
|
+
export function toolbarStops(container: Element | null, isHidden: (node: HTMLElement) => boolean): HTMLElement[];
|
|
41
|
+
/**
|
|
42
|
+
* Arrows wrap; Home/End jump to the ends. `undefined` means "not ours" — the
|
|
43
|
+
* caller must leave the event alone.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} count
|
|
46
|
+
* @param {number} current
|
|
47
|
+
* @param {string} key
|
|
48
|
+
* @returns {number | undefined}
|
|
49
|
+
*/
|
|
50
|
+
export function nextToolbarStop(count: number, current: number, key: string): number | undefined;
|
|
51
|
+
export const TOOLBAR_STOP_SELECTOR: "button, a[href], input, select, textarea, summary, [tabindex]";
|
|
52
|
+
/** Marks a control the toolbar has taken over, so its `-1` reads as ours, not the author's. */
|
|
53
|
+
export const TOOLBAR_STOP_ATTR: "data-toolbar-stop";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const TOOLBAR_STOP_SELECTOR =
|
|
2
|
+
'button, a[href], input, select, textarea, summary, [tabindex]';
|
|
3
|
+
|
|
4
|
+
/** Marks a control the toolbar has taken over, so its `-1` reads as ours, not the author's. */
|
|
5
|
+
export const TOOLBAR_STOP_ATTR = 'data-toolbar-stop';
|
|
6
|
+
|
|
7
|
+
const NAV_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
|
|
8
|
+
|
|
9
|
+
const ARROW_CONSUMING_INPUTS = new Set([
|
|
10
|
+
'text',
|
|
11
|
+
'search',
|
|
12
|
+
'email',
|
|
13
|
+
'url',
|
|
14
|
+
'tel',
|
|
15
|
+
'password',
|
|
16
|
+
'number',
|
|
17
|
+
'date',
|
|
18
|
+
'datetime-local',
|
|
19
|
+
'month',
|
|
20
|
+
'week',
|
|
21
|
+
'time',
|
|
22
|
+
'range',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** @param {string} key */
|
|
26
|
+
export function isToolbarNavKey(key) {
|
|
27
|
+
return NAV_KEYS.has(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Controls that own the arrow keys themselves (caret, spinner, native listbox):
|
|
32
|
+
* the toolbar must not steal them.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ tagName?: string; type?: string; isContentEditable?: boolean } | null | undefined} el
|
|
35
|
+
*/
|
|
36
|
+
export function consumesArrowKeys(el) {
|
|
37
|
+
if (!el) return false;
|
|
38
|
+
if (el.isContentEditable) return true;
|
|
39
|
+
const tag = (el.tagName ?? '').toLowerCase();
|
|
40
|
+
if (tag === 'textarea' || tag === 'select') return true;
|
|
41
|
+
if (tag !== 'input') return false;
|
|
42
|
+
return ARROW_CONSUMING_INPUTS.has((el.type ?? 'text').toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {{
|
|
47
|
+
* disabled?: boolean;
|
|
48
|
+
* hidden?: boolean;
|
|
49
|
+
* ariaHidden?: boolean;
|
|
50
|
+
* inPopover?: boolean;
|
|
51
|
+
* tabindex?: string | null;
|
|
52
|
+
* claimed?: boolean;
|
|
53
|
+
* }} el
|
|
54
|
+
*/
|
|
55
|
+
export function acceptsRovingStop(el) {
|
|
56
|
+
if (el.disabled || el.hidden || el.ariaHidden || el.inPopover) return false;
|
|
57
|
+
return el.claimed === true || el.tabindex !== '-1';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The ring, in DOM order. `isHidden` is injected because "has no box" is the only
|
|
62
|
+
* honest test for a collapsed child and it cannot be computed without layout.
|
|
63
|
+
*
|
|
64
|
+
* @param {Element | null} container
|
|
65
|
+
* @param {(node: HTMLElement) => boolean} isHidden
|
|
66
|
+
* @returns {HTMLElement[]}
|
|
67
|
+
*/
|
|
68
|
+
export function toolbarStops(container, isHidden) {
|
|
69
|
+
if (!container) return [];
|
|
70
|
+
return [
|
|
71
|
+
.../** @type {NodeListOf<HTMLElement>} */ (container.querySelectorAll(TOOLBAR_STOP_SELECTOR)),
|
|
72
|
+
].filter((node) =>
|
|
73
|
+
acceptsRovingStop({
|
|
74
|
+
disabled: /** @type {HTMLButtonElement} */ (node).disabled,
|
|
75
|
+
hidden: node.hidden || isHidden(node),
|
|
76
|
+
ariaHidden: node.getAttribute('aria-hidden') === 'true',
|
|
77
|
+
inPopover: node.closest('[popover]') !== null,
|
|
78
|
+
tabindex: node.getAttribute('tabindex'),
|
|
79
|
+
claimed: node.hasAttribute(TOOLBAR_STOP_ATTR),
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Arrows wrap; Home/End jump to the ends. `undefined` means "not ours" — the
|
|
86
|
+
* caller must leave the event alone.
|
|
87
|
+
*
|
|
88
|
+
* @param {number} count
|
|
89
|
+
* @param {number} current
|
|
90
|
+
* @param {string} key
|
|
91
|
+
* @returns {number | undefined}
|
|
92
|
+
*/
|
|
93
|
+
export function nextToolbarStop(count, current, key) {
|
|
94
|
+
if (count <= 0 || current < 0 || current >= count) return undefined;
|
|
95
|
+
if (key === 'Home') return 0;
|
|
96
|
+
if (key === 'End') return count - 1;
|
|
97
|
+
|
|
98
|
+
let direction;
|
|
99
|
+
if (key === 'ArrowRight' || key === 'ArrowDown') direction = 1;
|
|
100
|
+
else if (key === 'ArrowLeft' || key === 'ArrowUp') direction = -1;
|
|
101
|
+
else return undefined;
|
|
102
|
+
|
|
103
|
+
return (((current + direction) % count) + count) % count;
|
|
104
|
+
}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// `ValueProvider`s). No `fetch` lives in here; the bar owns the QUERY string.
|
|
39
39
|
// ───────────────────────────────────────────────────────────────────────
|
|
40
40
|
import type { Snippet } from 'svelte';
|
|
41
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
41
42
|
import Icon, { type IconName } from '../atoms/Icon.svelte';
|
|
42
43
|
import { filters, freeText } from '../../query/ast';
|
|
43
44
|
import { autoQuoteEdit, backspaceEmptyQuotes, closingQuoteExit } from '../../query/edit';
|
|
@@ -70,7 +71,10 @@
|
|
|
70
71
|
below,
|
|
71
72
|
class: klass = '',
|
|
72
73
|
style: styleProp = '',
|
|
73
|
-
|
|
74
|
+
...rest
|
|
75
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
76
|
+
|
|
77
|
+
type Own = {
|
|
74
78
|
schema: Schema;
|
|
75
79
|
/**
|
|
76
80
|
* Single-key mode: the name (or alias) of the ONE schema field being
|
|
@@ -127,7 +131,7 @@
|
|
|
127
131
|
below?: Snippet<[FilterInputContext]>;
|
|
128
132
|
class?: string;
|
|
129
133
|
style?: string;
|
|
130
|
-
}
|
|
134
|
+
};
|
|
131
135
|
|
|
132
136
|
const field = getFieldContext();
|
|
133
137
|
let el = $state<HTMLInputElement | null>(null);
|
|
@@ -335,6 +339,7 @@
|
|
|
335
339
|
</script>
|
|
336
340
|
|
|
337
341
|
<div
|
|
342
|
+
{...rest}
|
|
338
343
|
class="fi {klass}"
|
|
339
344
|
style={styleProp}
|
|
340
345
|
class:fi--sm={size === 'sm'}
|
|
@@ -20,9 +20,10 @@ export interface FilterInputContext {
|
|
|
20
20
|
}
|
|
21
21
|
import type { ControlSize } from '../../size';
|
|
22
22
|
import type { Snippet } from 'svelte';
|
|
23
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
23
24
|
import { type IconName } from '../atoms/Icon.svelte';
|
|
24
25
|
import { type Schema } from '../../query/schema';
|
|
25
|
-
type
|
|
26
|
+
type Own = {
|
|
26
27
|
schema: Schema;
|
|
27
28
|
/**
|
|
28
29
|
* Single-key mode: the name (or alias) of the ONE schema field being
|
|
@@ -80,6 +81,7 @@ type $$ComponentProps = {
|
|
|
80
81
|
class?: string;
|
|
81
82
|
style?: string;
|
|
82
83
|
};
|
|
84
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
83
85
|
declare const FilterInput: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
84
86
|
type FilterInput = ReturnType<typeof FilterInput>;
|
|
85
87
|
export default FilterInput;
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
pressed?: boolean;
|
|
13
13
|
/** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
|
|
14
14
|
content?: import('svelte').Snippet<[MenuItem]>;
|
|
15
|
+
/**
|
|
16
|
+
* Extra attributes for the rendered row — `data-*`, `title`, `aria-describedby`…
|
|
17
|
+
* The row's own semantics (`role`, `aria-checked`, `disabled`, `class`, `onclick`)
|
|
18
|
+
* are applied after and win.
|
|
19
|
+
*/
|
|
20
|
+
attrs?: import('svelte/elements').HTMLButtonAttributes;
|
|
15
21
|
}
|
|
16
22
|
</script>
|
|
17
23
|
|
|
@@ -165,6 +171,7 @@
|
|
|
165
171
|
{#each items as item (item.label)}
|
|
166
172
|
<button
|
|
167
173
|
type="button"
|
|
174
|
+
{...item.attrs}
|
|
168
175
|
role={item.pressed === undefined ? 'menuitem' : 'menuitemcheckbox'}
|
|
169
176
|
aria-checked={item.pressed === undefined ? undefined : item.pressed}
|
|
170
177
|
class="menu-item"
|
|
@@ -11,6 +11,12 @@ export interface MenuItem {
|
|
|
11
11
|
pressed?: boolean;
|
|
12
12
|
/** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
|
|
13
13
|
content?: import('svelte').Snippet<[MenuItem]>;
|
|
14
|
+
/**
|
|
15
|
+
* Extra attributes for the rendered row — `data-*`, `title`, `aria-describedby`…
|
|
16
|
+
* The row's own semantics (`role`, `aria-checked`, `disabled`, `class`, `onclick`)
|
|
17
|
+
* are applied after and win.
|
|
18
|
+
*/
|
|
19
|
+
attrs?: import('svelte/elements').HTMLButtonAttributes;
|
|
14
20
|
}
|
|
15
21
|
import type { ComponentProps, Snippet } from 'svelte';
|
|
16
22
|
import Popover from './Popover.svelte';
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// ships beyond Chromium; the popover semantics above are the hard part and
|
|
11
11
|
// are broadly supported today.)
|
|
12
12
|
import { tick, type Snippet } from 'svelte';
|
|
13
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
13
14
|
import { place } from '../../floating';
|
|
14
15
|
import { HOVER_CLOSE_GRACE, HOVER_OPEN_DELAY, createHoverIntent, opensOnHover } from './popover-hover.js';
|
|
15
16
|
|
|
@@ -20,36 +21,7 @@
|
|
|
20
21
|
type PanelRole = 'dialog' | 'menu' | 'listbox' | 'group';
|
|
21
22
|
type HasPopup = 'menu' | 'dialog' | 'listbox' | true;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
placement = 'bottom-start',
|
|
25
|
-
gap = 6,
|
|
26
|
-
label,
|
|
27
|
-
trigger,
|
|
28
|
-
children,
|
|
29
|
-
triggerClass = '',
|
|
30
|
-
bare = false,
|
|
31
|
-
variant,
|
|
32
|
-
tone = 'none',
|
|
33
|
-
size,
|
|
34
|
-
box,
|
|
35
|
-
pill = false,
|
|
36
|
-
control = false,
|
|
37
|
-
block = false,
|
|
38
|
-
hitArea = 'auto',
|
|
39
|
-
disabled = false,
|
|
40
|
-
openOn = 'click',
|
|
41
|
-
hoverDelay = HOVER_OPEN_DELAY,
|
|
42
|
-
as = 'button',
|
|
43
|
-
href,
|
|
44
|
-
role = 'dialog',
|
|
45
|
-
haspopup = 'dialog',
|
|
46
|
-
onopen,
|
|
47
|
-
onclose,
|
|
48
|
-
class: klass = '',
|
|
49
|
-
style: styleProp = '',
|
|
50
|
-
panelClass = '',
|
|
51
|
-
panelStyle = '',
|
|
52
|
-
}: {
|
|
24
|
+
type Own = {
|
|
53
25
|
placement?: Placement;
|
|
54
26
|
gap?: number;
|
|
55
27
|
/** Accessible name for the trigger. */
|
|
@@ -106,7 +78,39 @@
|
|
|
106
78
|
/** Class / inline style on the floating panel. */
|
|
107
79
|
panelClass?: string;
|
|
108
80
|
panelStyle?: string;
|
|
109
|
-
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let {
|
|
84
|
+
placement = 'bottom-start',
|
|
85
|
+
gap = 6,
|
|
86
|
+
label,
|
|
87
|
+
trigger,
|
|
88
|
+
children,
|
|
89
|
+
triggerClass = '',
|
|
90
|
+
bare = false,
|
|
91
|
+
variant,
|
|
92
|
+
tone = 'none',
|
|
93
|
+
size,
|
|
94
|
+
box,
|
|
95
|
+
pill = false,
|
|
96
|
+
control = false,
|
|
97
|
+
block = false,
|
|
98
|
+
hitArea = 'auto',
|
|
99
|
+
disabled = false,
|
|
100
|
+
openOn = 'click',
|
|
101
|
+
hoverDelay = HOVER_OPEN_DELAY,
|
|
102
|
+
as = 'button',
|
|
103
|
+
href,
|
|
104
|
+
role = 'dialog',
|
|
105
|
+
haspopup = 'dialog',
|
|
106
|
+
onopen,
|
|
107
|
+
onclose,
|
|
108
|
+
class: klass = '',
|
|
109
|
+
style: styleProp = '',
|
|
110
|
+
panelClass = '',
|
|
111
|
+
panelStyle = '',
|
|
112
|
+
...rest
|
|
113
|
+
}: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
|
|
110
114
|
|
|
111
115
|
const canonicalChrome = $derived(
|
|
112
116
|
variant !== undefined || tone !== 'none' || size !== undefined || control || block
|
|
@@ -219,6 +223,7 @@
|
|
|
219
223
|
<svelte:element
|
|
220
224
|
this={as}
|
|
221
225
|
bind:this={triggerEl}
|
|
226
|
+
{...rest}
|
|
222
227
|
data-tsu="Popover"
|
|
223
228
|
class="pop-trigger {triggerClass} {klass}"
|
|
224
229
|
style={styleProp}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
3
|
type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
3
4
|
type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger';
|
|
4
5
|
type TriggerTone = 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
|
|
5
6
|
type TriggerSize = 'sm' | 'md' | 'lg';
|
|
6
7
|
type PanelRole = 'dialog' | 'menu' | 'listbox' | 'group';
|
|
7
8
|
type HasPopup = 'menu' | 'dialog' | 'listbox' | true;
|
|
8
|
-
type
|
|
9
|
+
type Own = {
|
|
9
10
|
placement?: Placement;
|
|
10
11
|
gap?: number;
|
|
11
12
|
/** Accessible name for the trigger. */
|
|
@@ -65,6 +66,7 @@ type $$ComponentProps = {
|
|
|
65
66
|
panelClass?: string;
|
|
66
67
|
panelStyle?: string;
|
|
67
68
|
};
|
|
69
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own;
|
|
68
70
|
declare const Popover: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
69
71
|
type Popover = ReturnType<typeof Popover>;
|
|
70
72
|
export default Popover;
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
disabled?: boolean;
|
|
8
8
|
/** Trailing count badge. */
|
|
9
9
|
count?: number | string;
|
|
10
|
+
/**
|
|
11
|
+
* Extra attributes for this tab's button — `data-*`, `title`…
|
|
12
|
+
* The tab's own semantics (`role`, `aria-*`, `id`, `class`, `disabled`,
|
|
13
|
+
* `tabindex`, `onclick`) are applied after and win.
|
|
14
|
+
*/
|
|
15
|
+
attrs?: import('svelte/elements').HTMLButtonAttributes;
|
|
10
16
|
}
|
|
11
17
|
</script>
|
|
12
18
|
|
|
@@ -18,6 +24,7 @@
|
|
|
18
24
|
// the Tab order. `value` is bindable; `panel` is a snippet that receives the
|
|
19
25
|
// active id so the caller renders the matching content.
|
|
20
26
|
import type { Snippet } from 'svelte';
|
|
27
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
21
28
|
import Icon from '../atoms/Icon.svelte';
|
|
22
29
|
|
|
23
30
|
let {
|
|
@@ -29,7 +36,10 @@
|
|
|
29
36
|
style: styleProp = '',
|
|
30
37
|
panelClass = '',
|
|
31
38
|
panelPadding = 'md',
|
|
32
|
-
|
|
39
|
+
...rest
|
|
40
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
41
|
+
|
|
42
|
+
type Own = {
|
|
33
43
|
tabs: TabItem[];
|
|
34
44
|
value?: string;
|
|
35
45
|
label?: string;
|
|
@@ -38,7 +48,7 @@
|
|
|
38
48
|
style?: string;
|
|
39
49
|
panelClass?: string;
|
|
40
50
|
panelPadding?: 'none' | 'sm' | 'md';
|
|
41
|
-
}
|
|
51
|
+
};
|
|
42
52
|
|
|
43
53
|
// Default to the first selectable tab when no value is supplied.
|
|
44
54
|
$effect(() => {
|
|
@@ -84,11 +94,12 @@
|
|
|
84
94
|
}
|
|
85
95
|
</script>
|
|
86
96
|
|
|
87
|
-
<div class="tabs {klass}" style={styleProp} data-tsu="Tabs">
|
|
97
|
+
<div {...rest} class="tabs {klass}" style={styleProp} data-tsu="Tabs">
|
|
88
98
|
<div bind:this={listEl} role="tablist" aria-label={label} tabindex="-1" class="tablist" {onkeydown}>
|
|
89
99
|
{#each tabs as t (t.id)}
|
|
90
100
|
<button
|
|
91
101
|
type="button"
|
|
102
|
+
{...t.attrs}
|
|
92
103
|
role="tab"
|
|
93
104
|
id="{baseId}-tab-{t.id}"
|
|
94
105
|
aria-selected={value === t.id}
|
|
@@ -6,9 +6,16 @@ export interface TabItem {
|
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
/** Trailing count badge. */
|
|
8
8
|
count?: number | string;
|
|
9
|
+
/**
|
|
10
|
+
* Extra attributes for this tab's button — `data-*`, `title`…
|
|
11
|
+
* The tab's own semantics (`role`, `aria-*`, `id`, `class`, `disabled`,
|
|
12
|
+
* `tabindex`, `onclick`) are applied after and win.
|
|
13
|
+
*/
|
|
14
|
+
attrs?: import('svelte/elements').HTMLButtonAttributes;
|
|
9
15
|
}
|
|
10
16
|
import type { Snippet } from 'svelte';
|
|
11
|
-
type
|
|
17
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
18
|
+
type Own = {
|
|
12
19
|
tabs: TabItem[];
|
|
13
20
|
value?: string;
|
|
14
21
|
label?: string;
|
|
@@ -18,6 +25,7 @@ type $$ComponentProps = {
|
|
|
18
25
|
panelClass?: string;
|
|
19
26
|
panelPadding?: 'none' | 'sm' | 'md';
|
|
20
27
|
};
|
|
28
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
21
29
|
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
22
30
|
type Tabs = ReturnType<typeof Tabs>;
|
|
23
31
|
export default Tabs;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// resolved once at :root and would not re-scope). The root default theme
|
|
7
7
|
// has no [data-theme] block, so its swatch carries the :root values.
|
|
8
8
|
import type { ComponentProps } from 'svelte';
|
|
9
|
+
import type { AnchorAttributes } from '../../anchor';
|
|
9
10
|
import Popover from './Popover.svelte';
|
|
10
11
|
import { type ThemeDef, theme } from '../../stores/theme.svelte';
|
|
11
12
|
import { AUTO_THEME } from '../../theme-mode';
|
|
@@ -34,7 +35,10 @@
|
|
|
34
35
|
darkLabel = 'Dark',
|
|
35
36
|
class: klass = '',
|
|
36
37
|
style: styleProp = '',
|
|
37
|
-
|
|
38
|
+
...rest
|
|
39
|
+
}: AnchorAttributes & TriggerChrome & Own = $props();
|
|
40
|
+
|
|
41
|
+
type Own = {
|
|
38
42
|
/** Offer an "auto" row that follows `prefers-color-scheme`, remembering
|
|
39
43
|
* one light and one dark theme. */
|
|
40
44
|
auto?: boolean;
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
darkLabel?: string;
|
|
45
49
|
class?: string;
|
|
46
50
|
style?: string;
|
|
47
|
-
}
|
|
51
|
+
};
|
|
48
52
|
|
|
49
53
|
let hovered = $state<ThemeDef | null>(null);
|
|
50
54
|
let hoveredAuto = $state(false);
|
|
@@ -74,6 +78,7 @@
|
|
|
74
78
|
{/snippet}
|
|
75
79
|
|
|
76
80
|
<Popover
|
|
81
|
+
{...rest}
|
|
77
82
|
label={title}
|
|
78
83
|
{placement}
|
|
79
84
|
{box}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ComponentProps } from 'svelte';
|
|
2
|
+
import type { AnchorAttributes } from '../../anchor';
|
|
2
3
|
import Popover from './Popover.svelte';
|
|
3
4
|
type TriggerChrome = Pick<ComponentProps<typeof Popover>, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'>;
|
|
4
|
-
type
|
|
5
|
+
type Own = {
|
|
5
6
|
/** Offer an "auto" row that follows `prefers-color-scheme`, remembering
|
|
6
7
|
* one light and one dark theme. */
|
|
7
8
|
auto?: boolean;
|
|
@@ -12,6 +13,7 @@ type $$ComponentProps = TriggerChrome & {
|
|
|
12
13
|
class?: string;
|
|
13
14
|
style?: string;
|
|
14
15
|
};
|
|
16
|
+
type $$ComponentProps = AnchorAttributes & TriggerChrome & Own;
|
|
15
17
|
declare const ThemePicker: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
18
|
type ThemePicker = ReturnType<typeof ThemePicker>;
|
|
17
19
|
export default ThemePicker;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// and forwards onchange/onsubmit. Everything project-specific is INJECTED via
|
|
15
15
|
// `schema` (with per-field async `ValueProvider`s).
|
|
16
16
|
// ───────────────────────────────────────────────────────────────────────
|
|
17
|
+
import type { AnchorAttributes } from '../../anchor';
|
|
17
18
|
import Badge from '../atoms/Badge.svelte';
|
|
18
19
|
import FilterInput from '../molecules/FilterInput.svelte';
|
|
19
20
|
import type { Query } from '../../query/ast';
|
|
@@ -35,7 +36,10 @@
|
|
|
35
36
|
onsubmit,
|
|
36
37
|
class: klass = '',
|
|
37
38
|
style: styleProp = '',
|
|
38
|
-
|
|
39
|
+
...rest
|
|
40
|
+
}: AnchorAttributes & Own = $props();
|
|
41
|
+
|
|
42
|
+
type Own = {
|
|
39
43
|
schema: Schema;
|
|
40
44
|
/** The raw textual query (two-way bindable). */
|
|
41
45
|
value?: string;
|
|
@@ -61,7 +65,7 @@
|
|
|
61
65
|
onsubmit?: (value: string) => void;
|
|
62
66
|
class?: string;
|
|
63
67
|
style?: string;
|
|
64
|
-
}
|
|
68
|
+
};
|
|
65
69
|
|
|
66
70
|
function labelFor(fieldName: string): string {
|
|
67
71
|
return findField(schema, fieldName)?.label ?? fieldName;
|
|
@@ -69,6 +73,7 @@
|
|
|
69
73
|
</script>
|
|
70
74
|
|
|
71
75
|
<FilterInput
|
|
76
|
+
{...rest}
|
|
72
77
|
{schema}
|
|
73
78
|
bind:value
|
|
74
79
|
{placeholder}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ControlSize } from '../../size';
|
|
2
|
+
import type { AnchorAttributes } from '../../anchor';
|
|
2
3
|
import type { Query } from '../../query/ast';
|
|
3
4
|
import { type Schema } from '../../query/schema';
|
|
4
|
-
type
|
|
5
|
+
type Own = {
|
|
5
6
|
schema: Schema;
|
|
6
7
|
/** The raw textual query (two-way bindable). */
|
|
7
8
|
value?: string;
|
|
@@ -28,6 +29,7 @@ type $$ComponentProps = {
|
|
|
28
29
|
class?: string;
|
|
29
30
|
style?: string;
|
|
30
31
|
};
|
|
32
|
+
type $$ComponentProps = AnchorAttributes & Own;
|
|
31
33
|
declare const FilterSearchBar: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
32
34
|
type FilterSearchBar = ReturnType<typeof FilterSearchBar>;
|
|
33
35
|
export default FilterSearchBar;
|
package/dist/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dorsk/tsumikit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"@sveltejs/kit": "^2.22.0",
|
|
74
74
|
"@sveltejs/package": "^2.3.0",
|
|
75
75
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
76
|
+
"@types/jsdom": "^30.0.0",
|
|
76
77
|
"@types/node": "^25.9.3",
|
|
77
78
|
"esbuild": "^0.28.2",
|
|
78
79
|
"esbuild-svelte": "^0.9.5",
|