@dorsk/tsumikit 0.71.0 → 0.72.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 +33 -5
- package/dist/autoresize.js +13 -0
- package/dist/clipboard.js +12 -0
- package/dist/components/atoms/Artwork.svelte +4 -2
- package/dist/components/atoms/Avatar.svelte +4 -3
- package/dist/components/atoms/Badge.svelte +13 -0
- package/dist/components/atoms/Button.svelte +25 -9
- package/dist/components/atoms/Card.svelte +2 -0
- package/dist/components/atoms/Dot.svelte +1 -1
- package/dist/components/atoms/Icon.svelte +2 -0
- package/dist/components/atoms/Icon.svelte.d.ts +2 -0
- package/dist/components/atoms/Kbd.svelte +28 -16
- package/dist/components/atoms/Link.svelte +6 -4
- package/dist/components/atoms/Scrim.svelte +18 -1
- package/dist/components/atoms/Slider.svelte +11 -1
- package/dist/components/atoms/Swatch.svelte +12 -0
- package/dist/components/layouts/AppShell.svelte +70 -39
- package/dist/components/layouts/Container.svelte +25 -3
- package/dist/components/layouts/MasterDetail.svelte +59 -33
- package/dist/components/layouts/MasterDetail.svelte.d.ts +1 -1
- package/dist/components/layouts/NavItem.svelte +8 -4
- package/dist/components/layouts/NavSection.svelte +8 -2
- package/dist/components/layouts/ResizablePanel.svelte +14 -0
- package/dist/components/layouts/TabBar.svelte +217 -0
- package/dist/components/layouts/TabBar.svelte.d.ts +27 -0
- package/dist/components/layouts/Toolbar.svelte +5 -0
- package/dist/components/layouts/toolbar-roving.d.ts +16 -0
- package/dist/components/layouts/toolbar-roving.js +47 -1
- package/dist/components/molecules/FilterInput.svelte +7 -0
- package/dist/components/molecules/FilterInput.svelte.d.ts +2 -0
- package/dist/components/molecules/PullToRefresh.svelte +205 -0
- package/dist/components/molecules/PullToRefresh.svelte.d.ts +18 -0
- package/dist/components/molecules/SwipeRow.svelte +349 -0
- package/dist/components/molecules/SwipeRow.svelte.d.ts +37 -0
- package/dist/components/organisms/ConversationFeed.svelte +22 -8
- package/dist/components/organisms/ConversationFeed.svelte.d.ts +2 -0
- package/dist/components/organisms/DataTable.svelte +22 -5
- package/dist/components/organisms/DataTable.svelte.d.ts +4 -2
- package/dist/components/organisms/FilterSearchBar.svelte +4 -0
- package/dist/components/organisms/FilterSearchBar.svelte.d.ts +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/markdown.js +15 -7
- package/dist/styles/reset.css +30 -0
- package/dist/styles/syntax.css +0 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ individually, in this order:
|
|
|
102
102
|
| --- | --- |
|
|
103
103
|
| `styles/tokens.css` | `:root` only — every token the kit reads (`--c-*` palette, `--bg`/`--text`/`--accent` aliases, type scale, spacing, radii, shadows, fonts, `--control-height`, …). Theme-less. |
|
|
104
104
|
| `styles/themes.css` | the built-in `[data-theme="id"]` blocks, one per `THEMES` entry. |
|
|
105
|
-
| `styles/reset.css` | reset + element defaults. |
|
|
105
|
+
| `styles/reset.css` | reset + element defaults, global reduced-motion / forced-colors / prefers-contrast rules. |
|
|
106
106
|
| `styles/utilities.css` | `.container`, `.stack`, `.row`, `.sr-only`, icon sizing, … |
|
|
107
107
|
| `styles/syntax.css` | highlight.js / Prism class → `--syn-*` mapping. |
|
|
108
108
|
|
|
@@ -273,7 +273,7 @@ else aria-hidden, `interactive` renders a button with `onclick` + `aria-pressed`
|
|
|
273
273
|
from `selected`), Divider (standalone
|
|
274
274
|
horizontal/vertical hairline, `tone`, `spacing`, optional centred label,
|
|
275
275
|
`decorative` to drop the `separator` role), Link (`tone`, `underline`
|
|
276
|
-
always/hover/none, `align`), Icon (open registry — pass a `children` snippet for
|
|
276
|
+
always/hover/none, default hover for navigation and titles; pass `always` inside prose, `align`), Icon (open registry — pass a `children` snippet for
|
|
277
277
|
any custom SVG).
|
|
278
278
|
|
|
279
279
|
**Molecules:** Field (`grow`), IconButton (`filled` forwards to `Icon`, for a
|
|
@@ -344,7 +344,8 @@ replaces a header cell (a select-all checkbox);
|
|
|
344
344
|
paints a left accent bar + `data-tone`, `rowClass(row)`, `rowActions` snippet
|
|
345
345
|
for a hover/focus-revealed trailing cell (always visible on touch; under
|
|
346
346
|
`layout="fixed"` its column is `--dt-actions-w`, default 3.5rem),
|
|
347
|
-
`stickyOffset` for the sticky header's `top
|
|
347
|
+
`stickyOffset` for the sticky header's `top` (it sticks within the table's own
|
|
348
|
+
scroller, so give the table a bounded height), `size="sm"`, `loading`,
|
|
348
349
|
`onloadmore` footer button, `empty` as string or snippet; `data-part`
|
|
349
350
|
hooks on head/row/cell; `responsive="stack"` turns rows into cards below
|
|
350
351
|
`stackBelow` (48rem, measured on the table's own box) using `Column.role`
|
|
@@ -355,7 +356,7 @@ literal `inherit` falls back to mono), stacked tables use `table-layout: fixed`,
|
|
|
355
356
|
`stackActions="end"` closes every card with its actions, cells that render
|
|
356
357
|
nothing get `data-empty` and are hidden and skipped by the `·` separators, the `<table>`
|
|
357
358
|
stays a table for assistive tech), FilterSearchBar /
|
|
358
|
-
FilterInput (`size="sm"` compact bar on `--control-height-compact`, text
|
|
359
|
+
FilterInput (`font="sans"` for a plain search box, `size="sm"` compact bar on `--control-height-compact`, text
|
|
359
360
|
floored at 16px under `pointer: coarse` so iOS does not zoom on focus,
|
|
360
361
|
`shape="pill"`, `surface`, `hotkey="/"` focuses the input from anywhere
|
|
361
362
|
outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`;
|
|
@@ -383,7 +384,8 @@ insets it as an elevated rounded card), NavItem
|
|
|
383
384
|
(collapses to an icon rail when the sidebar is narrow; `icon` from the
|
|
384
385
|
registry, `iconPath` / `iconChildren` for custom glyphs, `activeStyle="bar"`
|
|
385
386
|
for the inset-bar active look), MasterDetail (list + detail columns that
|
|
386
|
-
become two pages — one pane at a time with a sticky 44px back header
|
|
387
|
+
become two pages — one pane at a time with a sticky 44px back header, keeping the
|
|
388
|
+
list's scroll and focus — below
|
|
387
389
|
their own `breakpoint`, so `selected` can be driven from the URL; `gap` spaces
|
|
388
390
|
the columns and `divider="none"` drops the seam, both also settable as the
|
|
389
391
|
`--md-gap` / `--md-divider` custom properties), Container, Stack
|
|
@@ -865,6 +867,32 @@ proportionally with no code. That's the recommended path for magnification.
|
|
|
865
867
|
text tokens only) for reading-dense apps that want larger body text while
|
|
866
868
|
keeping chrome compact. It isn't wired into AppShell or any default.
|
|
867
869
|
|
|
870
|
+
### Mobile: TabBar, SwipeRow, PullToRefresh
|
|
871
|
+
|
|
872
|
+
`TabBar` is a phone bottom tab bar: `items: { id, label, icon?, href?, badge? }[]`,
|
|
873
|
+
`bind:value` (the selected `id`), `onselect(id, item)`. Items without `href` are a
|
|
874
|
+
`tablist` with roving focus (←/→/Home/End); items with `href` are links with
|
|
875
|
+
`aria-current="page"`. It is fixed to the bottom with `env(safe-area-inset-bottom)`
|
|
876
|
+
(`placement="inline"` flows with the page) and, while mounted as a bar, publishes its
|
|
877
|
+
height as `--tabbar-h` on `<html>`, so content clears it with
|
|
878
|
+
`padding-bottom: var(--tabbar-h)`. Targets are at least 48px; numeric badges cap at `99+`.
|
|
879
|
+
|
|
880
|
+
`SwipeRow` wraps a list row with one action per side: `leadingAction` on a rightward
|
|
881
|
+
swipe, `trailingAction` on a leftward one, each `{ label, icon?, tone?, onaction }`
|
|
882
|
+
(or `leading` / `trailing` snippets receiving the action and an `armed` flag).
|
|
883
|
+
Releasing past `threshold` (0.35 of the width) fires `onaction`. Drags that are more
|
|
884
|
+
vertical than horizontal are left to the scroller (`touch-action: pan-y`). The same
|
|
885
|
+
actions are visually hidden buttons for keyboard and screen-reader users.
|
|
886
|
+
|
|
887
|
+
`PullToRefresh` is a scroll container with pull-down-to-refresh for touch and pen.
|
|
888
|
+
`onrefresh` returns a promise; the Spinner holds and the container is `aria-busy`
|
|
889
|
+
with `label` ("Refreshing…") announced politely until it settles. It starts only at
|
|
890
|
+
`scrollTop === 0`, fires once past `threshold` (72px), and contains overscroll so the
|
|
891
|
+
browser's own pull does not fight it. Keep a visible refresh control for mouse users.
|
|
892
|
+
|
|
893
|
+
AppShell exposes `--shell-header-h` (the header plus the top safe-area inset) for
|
|
894
|
+
sticky offsets under `viewport-fit=cover`.
|
|
895
|
+
|
|
868
896
|
### Toolbar control heights
|
|
869
897
|
|
|
870
898
|
`size="sm"` is the shared compact-toolbar contract for `Button`, `Popover`
|
package/dist/autoresize.js
CHANGED
|
@@ -32,6 +32,18 @@ export function autoresize(node, _value) {
|
|
|
32
32
|
};
|
|
33
33
|
resize();
|
|
34
34
|
node.addEventListener('input', resize);
|
|
35
|
+
// Re-wrap on width changes, and measure once a hidden textarea is revealed.
|
|
36
|
+
let width = node.clientWidth;
|
|
37
|
+
const ro = typeof ResizeObserver === 'undefined'
|
|
38
|
+
? undefined
|
|
39
|
+
: new ResizeObserver(([entry]) => {
|
|
40
|
+
const w = entry.contentRect.width;
|
|
41
|
+
if (w === width)
|
|
42
|
+
return;
|
|
43
|
+
width = w;
|
|
44
|
+
resize();
|
|
45
|
+
});
|
|
46
|
+
ro?.observe(node);
|
|
35
47
|
return {
|
|
36
48
|
update() {
|
|
37
49
|
// re-measure when the bound value changes from outside (e.g. cleared)
|
|
@@ -39,6 +51,7 @@ export function autoresize(node, _value) {
|
|
|
39
51
|
},
|
|
40
52
|
destroy() {
|
|
41
53
|
node.removeEventListener('input', resize);
|
|
54
|
+
ro?.disconnect();
|
|
42
55
|
},
|
|
43
56
|
};
|
|
44
57
|
}
|
package/dist/clipboard.js
CHANGED
|
@@ -13,15 +13,27 @@ export async function copyToClipboard(value) {
|
|
|
13
13
|
/* fall through to legacy path */
|
|
14
14
|
}
|
|
15
15
|
// Fallback for insecure contexts / older browsers.
|
|
16
|
+
const prev = document.activeElement;
|
|
17
|
+
const sel = document.getSelection();
|
|
18
|
+
const ranges = sel
|
|
19
|
+
? Array.from({ length: sel.rangeCount }, (_, i) => sel.getRangeAt(i).cloneRange())
|
|
20
|
+
: [];
|
|
16
21
|
try {
|
|
17
22
|
const ta = document.createElement('textarea');
|
|
18
23
|
ta.value = value;
|
|
24
|
+
ta.setAttribute('readonly', '');
|
|
19
25
|
ta.style.position = 'fixed';
|
|
20
26
|
ta.style.opacity = '0';
|
|
21
27
|
document.body.appendChild(ta);
|
|
22
28
|
ta.select();
|
|
23
29
|
const ok = document.execCommand('copy');
|
|
24
30
|
document.body.removeChild(ta);
|
|
31
|
+
prev?.focus?.({ preventScroll: true });
|
|
32
|
+
if (sel && ranges.length) {
|
|
33
|
+
sel.removeAllRanges();
|
|
34
|
+
for (const r of ranges)
|
|
35
|
+
sel.addRange(r);
|
|
36
|
+
}
|
|
25
37
|
return ok;
|
|
26
38
|
}
|
|
27
39
|
catch {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
const key = $derived(seed ?? alt);
|
|
62
62
|
const showImage = $derived(Boolean(src) && !failed);
|
|
63
|
+
const tile = $derived(showImage ? undefined : alt.trim() ? 'named' : 'decorative');
|
|
63
64
|
const gradient = $derived(showImage ? undefined : artworkGradient(key));
|
|
64
65
|
const text = $derived(fallback === 'initials' ? initialsOf(key) : '');
|
|
65
66
|
|
|
@@ -76,8 +77,9 @@
|
|
|
76
77
|
style:aspect-ratio={aspect}
|
|
77
78
|
style:width={size}
|
|
78
79
|
style:background={gradient}
|
|
79
|
-
role={
|
|
80
|
-
aria-label={
|
|
80
|
+
role={tile === 'named' ? 'img' : undefined}
|
|
81
|
+
aria-label={tile === 'named' ? alt : undefined}
|
|
82
|
+
aria-hidden={tile === 'decorative' ? 'true' : undefined}
|
|
81
83
|
{...rest}
|
|
82
84
|
>
|
|
83
85
|
{#if showImage}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
const text = $derived(glyph?.trim() || avatarInitial(name) || '?');
|
|
40
40
|
const hashed = $derived(tone === undefined);
|
|
41
|
+
const hidden = $derived(decorative || !name.trim());
|
|
41
42
|
const resolvedHue = $derived(hashed ? (hue ?? avatarHue(seed ?? name)) : undefined);
|
|
42
43
|
</script>
|
|
43
44
|
|
|
@@ -55,9 +56,9 @@
|
|
|
55
56
|
class:size-lg={size === 'lg'}
|
|
56
57
|
style:--avatar-size={typeof size === 'number' ? `${size}px` : undefined}
|
|
57
58
|
style:--avatar-hue={resolvedHue}
|
|
58
|
-
role={
|
|
59
|
-
aria-label={
|
|
60
|
-
aria-hidden={
|
|
59
|
+
role={hidden ? undefined : 'img'}
|
|
60
|
+
aria-label={hidden ? undefined : name}
|
|
61
|
+
aria-hidden={hidden ? 'true' : undefined}
|
|
61
62
|
{...rest}
|
|
62
63
|
>
|
|
63
64
|
<span class="text" aria-hidden="true">{text}</span>
|
|
@@ -140,6 +140,7 @@
|
|
|
140
140
|
class:interactive={as === 'button' || !!href}
|
|
141
141
|
style:--badge-tone={toneColor}
|
|
142
142
|
style:--badge-max-width={maxWidth}
|
|
143
|
+
type={!href && as === 'button' ? 'button' : undefined}
|
|
143
144
|
{...rest}
|
|
144
145
|
>
|
|
145
146
|
{#if dot}
|
|
@@ -295,4 +296,16 @@
|
|
|
295
296
|
.action:hover {
|
|
296
297
|
opacity: 1;
|
|
297
298
|
}
|
|
299
|
+
@media (pointer: coarse) {
|
|
300
|
+
.action {
|
|
301
|
+
position: relative;
|
|
302
|
+
}
|
|
303
|
+
.action::before {
|
|
304
|
+
content: '';
|
|
305
|
+
position: absolute;
|
|
306
|
+
inset-block: var(--touch-slab);
|
|
307
|
+
inset-inline-start: var(--touch-slab);
|
|
308
|
+
inset-inline-end: 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
298
311
|
</style>
|
|
@@ -114,14 +114,23 @@
|
|
|
114
114
|
const t = $derived(canonicalTone(tone));
|
|
115
115
|
|
|
116
116
|
// A link-flavoured Button is a real <a>; native `disabled`/`type` don't apply,
|
|
117
|
-
// so
|
|
117
|
+
// so an inactive link drops `href` (no focus, no keyboard activation).
|
|
118
118
|
const tag = $derived(href !== undefined || as === 'a' ? 'a' : 'button');
|
|
119
119
|
const inactive = $derived(disabled || loading);
|
|
120
120
|
const elementAttrs = $derived(
|
|
121
121
|
tag === 'a'
|
|
122
|
-
?
|
|
122
|
+
? inactive
|
|
123
|
+
? { href: undefined, role: 'link', 'aria-disabled': true }
|
|
124
|
+
: { href }
|
|
123
125
|
: { type, disabled: inactive }
|
|
124
126
|
);
|
|
127
|
+
const handleClick = (e: Parameters<NonNullable<typeof onclick>>[0]) => {
|
|
128
|
+
if (inactive) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
onclick?.(e);
|
|
133
|
+
};
|
|
125
134
|
|
|
126
135
|
const counted = $derived(hasCount(count));
|
|
127
136
|
const countText = $derived(hasCount(count) ? formatCount(count, countMax) : '');
|
|
@@ -137,12 +146,19 @@
|
|
|
137
146
|
const ariaLabel = $derived(rest['aria-label'] ? withCount(rest['aria-label']) : undefined);
|
|
138
147
|
$effect(() => {
|
|
139
148
|
if (!el || collapseLabel === 'never' || explicitName) return;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
const node = el;
|
|
150
|
+
const sync = () => {
|
|
151
|
+
const labels = Array.from(node.querySelectorAll('[data-label]'), (n) => n.textContent?.trim())
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join(' ');
|
|
154
|
+
const text = labels && withCount(labels);
|
|
155
|
+
if (text) node.setAttribute('aria-label', text);
|
|
156
|
+
else node.removeAttribute('aria-label');
|
|
157
|
+
};
|
|
158
|
+
sync();
|
|
159
|
+
const observer = new MutationObserver(sync);
|
|
160
|
+
observer.observe(node, { childList: true, characterData: true, subtree: true });
|
|
161
|
+
return () => observer.disconnect();
|
|
146
162
|
});
|
|
147
163
|
</script>
|
|
148
164
|
|
|
@@ -188,7 +204,7 @@
|
|
|
188
204
|
class:btn-counted={counted}
|
|
189
205
|
class:btn-stacked={stacked}
|
|
190
206
|
class:btn-hinted={keyHint !== undefined}
|
|
191
|
-
onclick={
|
|
207
|
+
onclick={handleClick}
|
|
192
208
|
>
|
|
193
209
|
{#if loading}<span class="btn-spinner" aria-hidden="true"></span>{/if}
|
|
194
210
|
{#if glyph !== undefined}
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
tap = false,
|
|
66
66
|
interactive = false,
|
|
67
67
|
as = 'div',
|
|
68
|
+
type = 'button',
|
|
68
69
|
padding = 'md',
|
|
69
70
|
surface = 'base',
|
|
70
71
|
border = 'solid',
|
|
@@ -147,6 +148,7 @@
|
|
|
147
148
|
style={`${stackStyle}${style}`}
|
|
148
149
|
role={interactive && !native ? 'button' : undefined}
|
|
149
150
|
tabindex={interactive && !native ? 0 : undefined}
|
|
151
|
+
type={as === 'button' ? type : undefined}
|
|
150
152
|
onclick={handleClick}
|
|
151
153
|
onkeydown={handleKeydown}
|
|
152
154
|
{...rest}
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
</span>
|
|
54
54
|
{:else}
|
|
55
55
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
56
|
-
<span class="dot {klass}" class:glow class:ring style="--dot-color:{resolved}" data-tsu="Dot" role={interactive ? 'img' : undefined} tabindex={interactive ? 0 : undefined} {...rest}></span>
|
|
56
|
+
<span class="dot {klass}" class:glow class:ring style="--dot-color:{resolved}" data-tsu="Dot" role={interactive ? 'img' : undefined} tabindex={interactive ? 0 : undefined} aria-label={interactive && !color ? status : undefined} {...rest}></span>
|
|
57
57
|
{/if}
|
|
58
58
|
|
|
59
59
|
<style>
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
music: '<path d="M9 18V5l12-2v13" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" />',
|
|
51
51
|
tv: '<path d="m17 2-5 5-5-5" /><rect width="20" height="15" x="2" y="7" rx="2" />',
|
|
52
52
|
film: '<rect width="18" height="18" x="3" y="3" rx="2" /><path d="M7 3v18" /><path d="M3 7.5h4" /><path d="M3 12h18" /><path d="M3 16.5h4" /><path d="M17 3v18" /><path d="M17 7.5h4" /><path d="M17 16.5h4" />',
|
|
53
|
+
video: '<path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" /><rect x="2" y="6" width="14" height="12" rx="2" />',
|
|
53
54
|
disc: '<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="2" />',
|
|
54
55
|
|
|
55
56
|
// — files / content —
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
unlink: '<path d="m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71" /><path d="m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71" /><line x1="8" x2="8" y1="2" y2="5" /><line x1="2" x2="5" y1="8" y2="8" /><line x1="16" x2="16" y1="19" y2="22" /><line x1="19" x2="22" y1="16" y2="16" />',
|
|
69
70
|
'text-cursor': '<path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1" /><path d="M7 22h1a4 4 0 0 0 4-4" /><path d="M7 2h1a4 4 0 0 1 4 4" />',
|
|
70
71
|
tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" /><circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />',
|
|
72
|
+
map: '<path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z" /><path d="M15 5.764v15" /><path d="M9 3.236v15" />',
|
|
71
73
|
pin: '<path d="M12 17v5" /><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" />',
|
|
72
74
|
'pin-off': '<path d="M12 17v5" /><path d="M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89" /><path d="m2 2 20 20" /><path d="M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11" />',
|
|
73
75
|
'life-buoy': '<circle cx="12" cy="12" r="10" /><path d="m4.93 4.93 4.24 4.24" /><path d="m14.83 9.17 4.24-4.24" /><path d="m14.83 14.83 4.24 4.24" /><path d="m9.17 14.83-4.24 4.24" /><circle cx="12" cy="12" r="4" />',
|
|
@@ -37,6 +37,7 @@ declare const ICONS: {
|
|
|
37
37
|
readonly music: "<path d=\"M9 18V5l12-2v13\" /><circle cx=\"6\" cy=\"18\" r=\"3\" /><circle cx=\"18\" cy=\"16\" r=\"3\" />";
|
|
38
38
|
readonly tv: "<path d=\"m17 2-5 5-5-5\" /><rect width=\"20\" height=\"15\" x=\"2\" y=\"7\" rx=\"2\" />";
|
|
39
39
|
readonly film: "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" /><path d=\"M7 3v18\" /><path d=\"M3 7.5h4\" /><path d=\"M3 12h18\" /><path d=\"M3 16.5h4\" /><path d=\"M17 3v18\" /><path d=\"M17 7.5h4\" /><path d=\"M17 16.5h4\" />";
|
|
40
|
+
readonly video: "<path d=\"m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5\" /><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\" />";
|
|
40
41
|
readonly disc: "<circle cx=\"12\" cy=\"12\" r=\"10\" /><circle cx=\"12\" cy=\"12\" r=\"2\" />";
|
|
41
42
|
readonly file: "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" />";
|
|
42
43
|
readonly 'file-text': "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" /><path d=\"M10 9H8\" /><path d=\"M16 13H8\" /><path d=\"M16 17H8\" />";
|
|
@@ -51,6 +52,7 @@ declare const ICONS: {
|
|
|
51
52
|
readonly unlink: "<path d=\"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71\" /><path d=\"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71\" /><line x1=\"8\" x2=\"8\" y1=\"2\" y2=\"5\" /><line x1=\"2\" x2=\"5\" y1=\"8\" y2=\"8\" /><line x1=\"16\" x2=\"16\" y1=\"19\" y2=\"22\" /><line x1=\"19\" x2=\"22\" y1=\"16\" y2=\"16\" />";
|
|
52
53
|
readonly 'text-cursor': "<path d=\"M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1\" /><path d=\"M7 22h1a4 4 0 0 0 4-4\" /><path d=\"M7 2h1a4 4 0 0 1 4 4\" />";
|
|
53
54
|
readonly tag: "<path d=\"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z\" /><circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\" />";
|
|
55
|
+
readonly map: "<path d=\"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z\" /><path d=\"M15 5.764v15\" /><path d=\"M9 3.236v15\" />";
|
|
54
56
|
readonly pin: "<path d=\"M12 17v5\" /><path d=\"M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z\" />";
|
|
55
57
|
readonly 'pin-off': "<path d=\"M12 17v5\" /><path d=\"M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89\" /><path d=\"m2 2 20 20\" /><path d=\"M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11\" />";
|
|
56
58
|
readonly 'life-buoy': "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m4.93 4.93 4.24 4.24\" /><path d=\"m14.83 9.17 4.24-4.24\" /><path d=\"m14.83 14.83 4.24 4.24\" /><path d=\"m9.17 14.83-4.24 4.24\" /><circle cx=\"12\" cy=\"12\" r=\"4\" />";
|
|
@@ -18,43 +18,46 @@
|
|
|
18
18
|
typeof navigator !== 'undefined' &&
|
|
19
19
|
/mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '');
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
type Key = { glyph: string; name: string };
|
|
22
|
+
const key = (glyph: string, name = glyph): Key => ({ glyph, name });
|
|
23
|
+
|
|
24
|
+
function label(token: string): Key {
|
|
22
25
|
const t = token.trim().toLowerCase();
|
|
23
26
|
switch (t) {
|
|
24
27
|
case 'mod':
|
|
25
|
-
return isMac ? '⌘' : 'Ctrl';
|
|
28
|
+
return isMac ? key('⌘', 'Command') : key('Ctrl', 'Control');
|
|
26
29
|
case 'ctrl':
|
|
27
30
|
case 'control':
|
|
28
|
-
return 'Ctrl';
|
|
31
|
+
return key('Ctrl', 'Control');
|
|
29
32
|
case 'meta':
|
|
30
33
|
case 'cmd':
|
|
31
34
|
case 'command':
|
|
32
|
-
return '⌘';
|
|
35
|
+
return key('⌘', 'Command');
|
|
33
36
|
case 'alt':
|
|
34
37
|
case 'option':
|
|
35
|
-
return isMac ? '⌥' : 'Alt';
|
|
38
|
+
return isMac ? key('⌥', 'Option') : key('Alt');
|
|
36
39
|
case 'shift':
|
|
37
|
-
return '⇧';
|
|
40
|
+
return key('⇧', 'Shift');
|
|
38
41
|
case 'enter':
|
|
39
42
|
case 'return':
|
|
40
|
-
return '↵';
|
|
43
|
+
return key('↵', 'Enter');
|
|
41
44
|
case 'esc':
|
|
42
45
|
case 'escape':
|
|
43
|
-
return 'Esc';
|
|
46
|
+
return key('Esc', 'Escape');
|
|
44
47
|
case 'tab':
|
|
45
|
-
return 'Tab';
|
|
48
|
+
return key('Tab');
|
|
46
49
|
case 'space':
|
|
47
|
-
return 'Space';
|
|
50
|
+
return key('Space');
|
|
48
51
|
case 'up':
|
|
49
|
-
return '↑';
|
|
52
|
+
return key('↑', 'Up arrow');
|
|
50
53
|
case 'down':
|
|
51
|
-
return '↓';
|
|
54
|
+
return key('↓', 'Down arrow');
|
|
52
55
|
case 'left':
|
|
53
|
-
return '←';
|
|
56
|
+
return key('←', 'Left arrow');
|
|
54
57
|
case 'right':
|
|
55
|
-
return '→';
|
|
58
|
+
return key('→', 'Right arrow');
|
|
56
59
|
default:
|
|
57
|
-
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
60
|
+
return key(t.charAt(0).toUpperCase() + t.slice(1));
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
>
|
|
74
77
|
{#each tokens as token, i (i)}
|
|
75
78
|
{#if i > 0}<span class="kbd-sep" aria-hidden="true">+</span>{/if}
|
|
76
|
-
<kbd class="kbd">{token}</kbd>
|
|
79
|
+
<kbd class="kbd">{#if token.name === token.glyph}{token.glyph}{:else}<span aria-hidden="true">{token.glyph}</span><span class="kbd-name">{token.name}</span>{/if}</kbd>
|
|
77
80
|
{/each}
|
|
78
81
|
</span>
|
|
79
82
|
|
|
@@ -111,6 +114,15 @@
|
|
|
111
114
|
background: var(--bg-elevated-2);
|
|
112
115
|
color: inherit;
|
|
113
116
|
}
|
|
117
|
+
.kbd-name {
|
|
118
|
+
position: absolute;
|
|
119
|
+
width: 1px;
|
|
120
|
+
height: 1px;
|
|
121
|
+
margin: -1px;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
clip: rect(0, 0, 0, 0);
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
114
126
|
.kbd-sep {
|
|
115
127
|
font-size: 0.85em;
|
|
116
128
|
color: var(--text-faint);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
-
// Inline text link —
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Inline text link — accent text with no button chrome. Renders an <a> when
|
|
4
|
+
// `href` is given, otherwise a <button> (for in-page actions that read as
|
|
5
|
+
// links, e.g. "Open ChatGPT again").
|
|
6
|
+
// Underline: the default `hover` is for navigation, lists and entity titles;
|
|
7
|
+
// use `always` for links inside running prose.
|
|
6
8
|
import type { Snippet } from 'svelte';
|
|
7
9
|
|
|
8
10
|
type Own = {
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
let {
|
|
21
23
|
href,
|
|
22
24
|
tone = 'accent',
|
|
23
|
-
underline = '
|
|
25
|
+
underline = 'hover',
|
|
24
26
|
align = 'start',
|
|
25
27
|
class: klass = '',
|
|
26
28
|
children,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
const open: object[] = [];
|
|
3
|
+
</script>
|
|
4
|
+
|
|
1
5
|
<script lang="ts">
|
|
2
6
|
import { browser } from '../../env';
|
|
3
7
|
|
|
@@ -38,15 +42,27 @@
|
|
|
38
42
|
return () => query.removeEventListener('change', sync);
|
|
39
43
|
});
|
|
40
44
|
|
|
45
|
+
let el = $state<HTMLButtonElement | null>(null);
|
|
46
|
+
|
|
47
|
+
// Only the topmost scrim reacts, and never to an Escape aimed at a dialog
|
|
48
|
+
// or popover layered above it.
|
|
41
49
|
$effect(() => {
|
|
42
50
|
if (!browser) return;
|
|
51
|
+
const self = {};
|
|
52
|
+
open.push(self);
|
|
43
53
|
const onKeydown = (event: KeyboardEvent) => {
|
|
44
54
|
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
55
|
+
if (open.at(-1) !== self) return;
|
|
56
|
+
const layer = (event.target as Element | null)?.closest?.('dialog[open], [popover]');
|
|
57
|
+
if (layer && !(el && layer.contains(el))) return;
|
|
45
58
|
event.preventDefault();
|
|
46
59
|
onclose?.();
|
|
47
60
|
};
|
|
48
61
|
document.addEventListener('keydown', onKeydown);
|
|
49
|
-
return () =>
|
|
62
|
+
return () => {
|
|
63
|
+
document.removeEventListener('keydown', onKeydown);
|
|
64
|
+
open.splice(open.indexOf(self), 1);
|
|
65
|
+
};
|
|
50
66
|
});
|
|
51
67
|
</script>
|
|
52
68
|
|
|
@@ -54,6 +70,7 @@
|
|
|
54
70
|
<button
|
|
55
71
|
type="button"
|
|
56
72
|
class="scrim {klass}"
|
|
73
|
+
bind:this={el}
|
|
57
74
|
style={styleProp}
|
|
58
75
|
style:z-index={z}
|
|
59
76
|
aria-label={label}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
step = 1,
|
|
16
16
|
label,
|
|
17
17
|
showValue = false,
|
|
18
|
-
format
|
|
18
|
+
format: formatProp,
|
|
19
19
|
id: idProp,
|
|
20
20
|
'aria-describedby': ariaDescribedby,
|
|
21
21
|
'aria-invalid': ariaInvalid,
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
const fallbackId = $props.id();
|
|
54
54
|
const id = $derived(idProp ?? field?.id ?? fallbackId);
|
|
55
55
|
const isInvalid = $derived(invalid || !!field?.invalid);
|
|
56
|
+
const format = $derived(formatProp ?? ((v: number) => String(v)));
|
|
57
|
+
const valueText = $derived(
|
|
58
|
+
marks.find((m) => m.value === Number(value))?.label ?? formatProp?.(Number(value))
|
|
59
|
+
);
|
|
56
60
|
|
|
57
61
|
$effect(() => warnUnlabelled(el, 'Slider'));
|
|
58
62
|
|
|
@@ -78,6 +82,7 @@
|
|
|
78
82
|
{step}
|
|
79
83
|
bind:value
|
|
80
84
|
aria-label={label}
|
|
85
|
+
aria-valuetext={valueText}
|
|
81
86
|
{...rest}
|
|
82
87
|
aria-describedby={ariaDescribedby ?? field?.describedBy}
|
|
83
88
|
aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
|
|
@@ -212,6 +217,11 @@
|
|
|
212
217
|
outline: var(--focus-ring);
|
|
213
218
|
outline-offset: 2px;
|
|
214
219
|
}
|
|
220
|
+
@media (pointer: coarse) {
|
|
221
|
+
input[type='range'] {
|
|
222
|
+
min-height: var(--touch-target-compact);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
215
225
|
input[type='range']:disabled {
|
|
216
226
|
opacity: 0.45;
|
|
217
227
|
cursor: not-allowed;
|
|
@@ -111,4 +111,16 @@
|
|
|
111
111
|
outline: 2px solid var(--accent);
|
|
112
112
|
outline-offset: 3px;
|
|
113
113
|
}
|
|
114
|
+
@media (pointer: coarse) {
|
|
115
|
+
.interactive {
|
|
116
|
+
position: relative;
|
|
117
|
+
}
|
|
118
|
+
.interactive::before {
|
|
119
|
+
content: '';
|
|
120
|
+
position: absolute;
|
|
121
|
+
inset-block: var(--touch-slab);
|
|
122
|
+
inset-inline-start: var(--touch-slab);
|
|
123
|
+
inset-inline-end: 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
114
126
|
</style>
|