@dorsk/tsumikit 0.70.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 +49 -8
- package/dist/autoresize.js +13 -0
- package/dist/clipboard.js +12 -0
- package/dist/components/atoms/Artwork.svelte +10 -3
- package/dist/components/atoms/Artwork.svelte.d.ts +2 -0
- package/dist/components/atoms/Avatar.svelte +4 -3
- package/dist/components/atoms/Badge.svelte +23 -8
- package/dist/components/atoms/Badge.svelte.d.ts +2 -1
- package/dist/components/atoms/Button.svelte +39 -11
- 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/Input.svelte +3 -0
- package/dist/components/atoms/Kbd.svelte +28 -16
- package/dist/components/atoms/Link.svelte +18 -4
- package/dist/components/atoms/Scrim.svelte +18 -1
- package/dist/components/atoms/Select.svelte +1 -0
- package/dist/components/atoms/Slider.svelte +11 -1
- package/dist/components/atoms/Swatch.svelte +12 -0
- package/dist/components/atoms/Switch.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/Breadcrumb.svelte +44 -2
- package/dist/components/molecules/Breadcrumb.svelte.d.ts +6 -0
- 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/SegmentedControl.svelte +44 -1
- package/dist/components/molecules/SwipeRow.svelte +349 -0
- package/dist/components/molecules/SwipeRow.svelte.d.ts +37 -0
- package/dist/components/molecules/Toggle.svelte +12 -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 +72 -12
- package/dist/components/organisms/DataTable.svelte.d.ts +7 -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/dist/styles/tokens.css +6 -0
- 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
|
|
|
@@ -261,9 +261,9 @@ the trigger overlays the selected option's glyph + muted hint on the native cont
|
|
|
261
261
|
sm/md/lg as on `Switch`, or `--cb-box` for the box alone),
|
|
262
262
|
Slider (`ticks` draws a dot per step for a segmented slider), Progress, Gauge (vertical consumption cell, `variant` continuous/segments,
|
|
263
263
|
threshold tones via `warnAt`/`dangerAt`, `corner` snippet), Artwork (lazy cover image with seeded gradient + initials
|
|
264
|
-
fallback, `aspect`, `status` overlay), Avatar (grapheme-safe initial or `glyph`, hue hashed from
|
|
264
|
+
fallback, `aspect`, `status` overlay, `srcset`/`sizes` passed to the `<img>`), Avatar (grapheme-safe initial or `glyph`, hue hashed from
|
|
265
265
|
`seed`/`name` or a fixed `tone`, `size` xs–lg or px, `shape` circle/square, `decorative`, corner `status`), Card (`tone` tints the surface for inline banners), Badge
|
|
266
|
-
(`tone` semantic palette or `color` for any CSS colour, `size` xs/sm/md, `dot`,
|
|
266
|
+
(`tone` semantic palette (the shared `Tone`, `success` = `ok`, plus `muted`/`violet`) or `color` for any CSS colour, `size` xs/sm/md (sm/xs on `--fs-xs`), `dot`,
|
|
267
267
|
`icon`, `numeric`, `truncate`, `variant="text"`, `removable`/`onremove` or any
|
|
268
268
|
trailing action via `actionIcon`/`actionLabel`/`onaction`, `removed` mutes and
|
|
269
269
|
strikes the pill for a restorable entry; all tints derive from
|
|
@@ -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
|
|
@@ -290,6 +290,8 @@ sizes the panel; an item with a `control` snippet renders as a non-button row ho
|
|
|
290
290
|
Tabs (omit `panel` for a strip-only tablist of document tabs: `closable` + `onclose(id)`
|
|
291
291
|
close through the ✕, Delete and middle-click, per-tab `leading` snippet, `title`,
|
|
292
292
|
trailing `actions` snippet; `--tab-max-width` truncates long labels),
|
|
293
|
+
Breadcrumb (`maxItems` collapses the middle, `size` sm/md, `wrap={false}` keeps one
|
|
294
|
+
line with the current crumb ellipsised, `collapseCurrent` keeps only the ancestors),
|
|
293
295
|
RadioGroup (per-option `description` in every variant, announced via
|
|
294
296
|
`aria-describedby`; `onfocuschange(value)` fires once per newly focused/hovered option;
|
|
295
297
|
`variant="rows"`: bordered rows, per-option `note`,
|
|
@@ -342,16 +344,19 @@ replaces a header cell (a select-all checkbox);
|
|
|
342
344
|
paints a left accent bar + `data-tone`, `rowClass(row)`, `rowActions` snippet
|
|
343
345
|
for a hover/focus-revealed trailing cell (always visible on touch; under
|
|
344
346
|
`layout="fixed"` its column is `--dt-actions-w`, default 3.5rem),
|
|
345
|
-
`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`,
|
|
346
349
|
`onloadmore` footer button, `empty` as string or snippet; `data-part`
|
|
347
350
|
hooks on head/row/cell; `responsive="stack"` turns rows into cards below
|
|
348
351
|
`stackBelow` (48rem, measured on the table's own box) using `Column.role`
|
|
349
352
|
`title | detail | meta | actions | hidden` — detail cells get a `data-label`
|
|
350
353
|
prefix, `Column.stackLabel: false` drops it so metas join on one `·` line
|
|
351
354
|
and details share lines, `--dt-meta-font` restyles meta values (e.g. `var(--font-sans)`; a
|
|
352
|
-
literal `inherit` falls back to mono),
|
|
355
|
+
literal `inherit` falls back to mono), stacked tables use `table-layout: fixed`,
|
|
356
|
+
`stackActions="end"` closes every card with its actions, cells that render
|
|
357
|
+
nothing get `data-empty` and are hidden and skipped by the `·` separators, the `<table>`
|
|
353
358
|
stays a table for assistive tech), FilterSearchBar /
|
|
354
|
-
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
|
|
355
360
|
floored at 16px under `pointer: coarse` so iOS does not zoom on focus,
|
|
356
361
|
`shape="pill"`, `surface`, `hotkey="/"` focuses the input from anywhere
|
|
357
362
|
outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`;
|
|
@@ -379,7 +384,8 @@ insets it as an elevated rounded card), NavItem
|
|
|
379
384
|
(collapses to an icon rail when the sidebar is narrow; `icon` from the
|
|
380
385
|
registry, `iconPath` / `iconChildren` for custom glyphs, `activeStyle="bar"`
|
|
381
386
|
for the inset-bar active look), MasterDetail (list + detail columns that
|
|
382
|
-
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
|
|
383
389
|
their own `breakpoint`, so `selected` can be driven from the URL; `gap` spaces
|
|
384
390
|
the columns and `divider="none"` drops the seam, both also settable as the
|
|
385
391
|
`--md-gap` / `--md-divider` custom properties), Container, Stack
|
|
@@ -861,6 +867,32 @@ proportionally with no code. That's the recommended path for magnification.
|
|
|
861
867
|
text tokens only) for reading-dense apps that want larger body text while
|
|
862
868
|
keeping chrome compact. It isn't wired into AppShell or any default.
|
|
863
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
|
+
|
|
864
896
|
### Toolbar control heights
|
|
865
897
|
|
|
866
898
|
`size="sm"` is the shared compact-toolbar contract for `Button`, `Popover`
|
|
@@ -972,6 +1004,15 @@ from the hidden text unless `aria-label` is set:
|
|
|
972
1004
|
<Cluster stackAt="md"><Button grow>Cancel</Button><Button grow variant="primary">Save</Button></Cluster>
|
|
973
1005
|
```
|
|
974
1006
|
|
|
1007
|
+
Compact text controls (`Button size="sm"`, `Toggle`, `Switch`, `Link`) take
|
|
1008
|
+
the same shape, sized by `--touch-slab`: at least 8px past the block edges and
|
|
1009
|
+
the inline start, and at least `--touch-target-compact` (40px) tall, with no
|
|
1010
|
+
inline-end overflow. `SegmentedControl` items grow on the block axis and by
|
|
1011
|
+
half the gap sideways, which stays inside the group's padding, so neighbours
|
|
1012
|
+
never overlap. `Input size="sm"` cannot host a pseudo-element and grows to 40px
|
|
1013
|
+
instead, as should any control that clips its own overflow:
|
|
1014
|
+
`min-height: var(--touch-target-compact)`.
|
|
1015
|
+
|
|
975
1016
|
`--control-height-*` and `--box-*` are **required tokens**: a consumer that
|
|
976
1017
|
forks `variables.css` must keep them (Button `control`, Select `sm`, every `box`)
|
|
977
1018
|
or those controls render with unresolved sizes.
|
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 {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
type Own = {
|
|
13
13
|
src?: string | null;
|
|
14
|
+
srcset?: string;
|
|
15
|
+
sizes?: string;
|
|
14
16
|
alt: string;
|
|
15
17
|
/** Drives the gradient hue and the initials. Defaults to `alt`. */
|
|
16
18
|
seed?: string;
|
|
@@ -32,6 +34,8 @@
|
|
|
32
34
|
};
|
|
33
35
|
let {
|
|
34
36
|
src,
|
|
37
|
+
srcset,
|
|
38
|
+
sizes,
|
|
35
39
|
alt,
|
|
36
40
|
seed,
|
|
37
41
|
aspect = '1/1',
|
|
@@ -50,11 +54,13 @@
|
|
|
50
54
|
let failed = $state(false);
|
|
51
55
|
$effect(() => {
|
|
52
56
|
src;
|
|
57
|
+
srcset;
|
|
53
58
|
failed = false;
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
const key = $derived(seed ?? alt);
|
|
57
62
|
const showImage = $derived(Boolean(src) && !failed);
|
|
63
|
+
const tile = $derived(showImage ? undefined : alt.trim() ? 'named' : 'decorative');
|
|
58
64
|
const gradient = $derived(showImage ? undefined : artworkGradient(key));
|
|
59
65
|
const text = $derived(fallback === 'initials' ? initialsOf(key) : '');
|
|
60
66
|
|
|
@@ -71,12 +77,13 @@
|
|
|
71
77
|
style:aspect-ratio={aspect}
|
|
72
78
|
style:width={size}
|
|
73
79
|
style:background={gradient}
|
|
74
|
-
role={
|
|
75
|
-
aria-label={
|
|
80
|
+
role={tile === 'named' ? 'img' : undefined}
|
|
81
|
+
aria-label={tile === 'named' ? alt : undefined}
|
|
82
|
+
aria-hidden={tile === 'decorative' ? 'true' : undefined}
|
|
76
83
|
{...rest}
|
|
77
84
|
>
|
|
78
85
|
{#if showImage}
|
|
79
|
-
<img {src} {alt} class="img fit-{fit}" loading="lazy" decoding="async" onerror={fail} />
|
|
86
|
+
<img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async" onerror={fail} />
|
|
80
87
|
{:else if fallback === 'initials' && text}
|
|
81
88
|
<span class="initials" aria-hidden="true">{text}</span>
|
|
82
89
|
{:else if fallback === 'icon'}
|
|
@@ -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>
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
import type { Snippet } from 'svelte';
|
|
14
14
|
import Dot from './Dot.svelte';
|
|
15
15
|
import Icon, { type IconName } from './Icon.svelte';
|
|
16
|
+
import { canonicalTone, type Tone as SharedTone } from '../../tone';
|
|
16
17
|
|
|
17
|
-
type Tone =
|
|
18
|
+
type Tone = SharedTone | 'muted' | 'violet';
|
|
18
19
|
|
|
19
|
-
const TONE_COLOR: Record<Exclude<Tone, 'neutral'>, string> = {
|
|
20
|
+
const TONE_COLOR: Record<Exclude<Tone, 'neutral' | 'success'>, string> = {
|
|
20
21
|
ok: 'var(--ok)',
|
|
21
22
|
warn: 'var(--warn)',
|
|
22
23
|
danger: 'var(--danger)',
|
|
@@ -106,7 +107,8 @@
|
|
|
106
107
|
...rest
|
|
107
108
|
}: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
|
|
108
109
|
|
|
109
|
-
const
|
|
110
|
+
const t = $derived(canonicalTone(tone));
|
|
111
|
+
const toneColor = $derived(color ?? (t === 'neutral' ? undefined : TONE_COLOR[t]));
|
|
110
112
|
const hasAction = $derived(removable || actionIcon !== undefined);
|
|
111
113
|
</script>
|
|
112
114
|
|
|
@@ -120,10 +122,10 @@
|
|
|
120
122
|
class:grow={grow}
|
|
121
123
|
class:no-shrink={!shrink}
|
|
122
124
|
class:block={block}
|
|
123
|
-
class:badge-ok={
|
|
124
|
-
class:badge-warn={
|
|
125
|
-
class:badge-danger={
|
|
126
|
-
class:badge-info={
|
|
125
|
+
class:badge-ok={t === 'ok'}
|
|
126
|
+
class:badge-warn={t === 'warn'}
|
|
127
|
+
class:badge-danger={t === 'danger'}
|
|
128
|
+
class:badge-info={t === 'info'}
|
|
127
129
|
class:toned={toneColor !== undefined}
|
|
128
130
|
class:badge-sm={size === 'sm'}
|
|
129
131
|
class:badge-xs={size === 'xs'}
|
|
@@ -138,6 +140,7 @@
|
|
|
138
140
|
class:interactive={as === 'button' || !!href}
|
|
139
141
|
style:--badge-tone={toneColor}
|
|
140
142
|
style:--badge-max-width={maxWidth}
|
|
143
|
+
type={!href && as === 'button' ? 'button' : undefined}
|
|
141
144
|
{...rest}
|
|
142
145
|
>
|
|
143
146
|
{#if dot}
|
|
@@ -193,7 +196,7 @@
|
|
|
193
196
|
}
|
|
194
197
|
.badge-sm,
|
|
195
198
|
.badge-xs {
|
|
196
|
-
font-size: var(--badge-fs,
|
|
199
|
+
font-size: var(--badge-fs, var(--fs-xs));
|
|
197
200
|
gap: 0.15rem;
|
|
198
201
|
}
|
|
199
202
|
.badge-sm {
|
|
@@ -293,4 +296,16 @@
|
|
|
293
296
|
.action:hover {
|
|
294
297
|
opacity: 1;
|
|
295
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
|
+
}
|
|
296
311
|
</style>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { type IconName } from './Icon.svelte';
|
|
4
|
-
type Tone
|
|
4
|
+
import { type Tone as SharedTone } from '../../tone';
|
|
5
|
+
type Tone = SharedTone | 'muted' | 'violet';
|
|
5
6
|
type Own = {
|
|
6
7
|
tone?: Tone;
|
|
7
8
|
/** Fill the free space of a flex row (`flex: 1 1 0`). */
|
|
@@ -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}
|
|
@@ -544,7 +560,8 @@
|
|
|
544
560
|
The slab is the centred target with its inline-end half cut off: inline-end
|
|
545
561
|
overflow counts towards the scrollable overflow of every scroll ancestor, and
|
|
546
562
|
in a row that half lies under the next sibling anyway. Growing further towards
|
|
547
|
-
the start would let each slab cover the previous sibling's centre.
|
|
563
|
+
the start would let each slab cover the previous sibling's centre. Text
|
|
564
|
+
buttons at size sm take the same shape sized by --touch-slab. */
|
|
548
565
|
@media (pointer: coarse) {
|
|
549
566
|
.btn-icon:not(.hit-compact),
|
|
550
567
|
.btn-icon-inline:not(.hit-compact),
|
|
@@ -566,6 +583,16 @@
|
|
|
566
583
|
inset-inline-start: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
567
584
|
inset-inline-end: 0;
|
|
568
585
|
}
|
|
586
|
+
.btn-sm:not(.btn-icon, .btn-icon-inline, .btn-box, .btn-square, .btn-chip, .btn-stacked, .hit-compact) {
|
|
587
|
+
position: relative;
|
|
588
|
+
}
|
|
589
|
+
.btn-sm:not(.btn-icon, .btn-icon-inline, .btn-box, .btn-square, .btn-chip, .btn-stacked, .hit-compact)::before {
|
|
590
|
+
content: '';
|
|
591
|
+
position: absolute;
|
|
592
|
+
inset-block: var(--touch-slab);
|
|
593
|
+
inset-inline-start: var(--touch-slab);
|
|
594
|
+
inset-inline-end: 0;
|
|
595
|
+
}
|
|
569
596
|
}
|
|
570
597
|
|
|
571
598
|
/* Two-state (toggle) buttons — e.g. an IconButton with `pressed`. Reacts to
|
|
@@ -703,7 +730,8 @@
|
|
|
703
730
|
.btn-link .btn-count,
|
|
704
731
|
.btn-icon-inline .btn-count {
|
|
705
732
|
--btn-count-size: 1rem;
|
|
706
|
-
|
|
733
|
+
padding: 0 0.2rem;
|
|
734
|
+
font-size: var(--fs-xs);
|
|
707
735
|
}
|
|
708
736
|
/* The accent fill would swallow an accent pill: invert on primary. */
|
|
709
737
|
.btn-primary .btn-count {
|
|
@@ -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,
|
|
@@ -74,4 +76,16 @@
|
|
|
74
76
|
.align-center {
|
|
75
77
|
text-align: center;
|
|
76
78
|
}
|
|
79
|
+
@media (pointer: coarse) {
|
|
80
|
+
.link {
|
|
81
|
+
position: relative;
|
|
82
|
+
}
|
|
83
|
+
.link::before {
|
|
84
|
+
content: '';
|
|
85
|
+
position: absolute;
|
|
86
|
+
inset-block: var(--touch-slab);
|
|
87
|
+
inset-inline-start: var(--touch-slab);
|
|
88
|
+
inset-inline-end: 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
77
91
|
</style>
|