@dorsk/tsumikit 0.70.0 → 0.71.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 +16 -3
- package/dist/components/atoms/Artwork.svelte +6 -1
- package/dist/components/atoms/Artwork.svelte.d.ts +2 -0
- package/dist/components/atoms/Badge.svelte +10 -8
- package/dist/components/atoms/Badge.svelte.d.ts +2 -1
- package/dist/components/atoms/Button.svelte +14 -2
- package/dist/components/atoms/Input.svelte +3 -0
- package/dist/components/atoms/Link.svelte +12 -0
- package/dist/components/atoms/Select.svelte +1 -0
- package/dist/components/atoms/Switch.svelte +12 -0
- package/dist/components/molecules/Breadcrumb.svelte +44 -2
- package/dist/components/molecules/Breadcrumb.svelte.d.ts +6 -0
- package/dist/components/molecules/SegmentedControl.svelte +44 -1
- package/dist/components/molecules/Toggle.svelte +12 -0
- package/dist/components/organisms/DataTable.svelte +50 -7
- package/dist/components/organisms/DataTable.svelte.d.ts +3 -0
- package/dist/styles/tokens.css +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
@@ -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`,
|
|
@@ -349,7 +351,9 @@ hooks on head/row/cell; `responsive="stack"` turns rows into cards below
|
|
|
349
351
|
`title | detail | meta | actions | hidden` — detail cells get a `data-label`
|
|
350
352
|
prefix, `Column.stackLabel: false` drops it so metas join on one `·` line
|
|
351
353
|
and details share lines, `--dt-meta-font` restyles meta values (e.g. `var(--font-sans)`; a
|
|
352
|
-
literal `inherit` falls back to mono),
|
|
354
|
+
literal `inherit` falls back to mono), stacked tables use `table-layout: fixed`,
|
|
355
|
+
`stackActions="end"` closes every card with its actions, cells that render
|
|
356
|
+
nothing get `data-empty` and are hidden and skipped by the `·` separators, the `<table>`
|
|
353
357
|
stays a table for assistive tech), FilterSearchBar /
|
|
354
358
|
FilterInput (`size="sm"` compact bar on `--control-height-compact`, text
|
|
355
359
|
floored at 16px under `pointer: coarse` so iOS does not zoom on focus,
|
|
@@ -972,6 +976,15 @@ from the hidden text unless `aria-label` is set:
|
|
|
972
976
|
<Cluster stackAt="md"><Button grow>Cancel</Button><Button grow variant="primary">Save</Button></Cluster>
|
|
973
977
|
```
|
|
974
978
|
|
|
979
|
+
Compact text controls (`Button size="sm"`, `Toggle`, `Switch`, `Link`) take
|
|
980
|
+
the same shape, sized by `--touch-slab`: at least 8px past the block edges and
|
|
981
|
+
the inline start, and at least `--touch-target-compact` (40px) tall, with no
|
|
982
|
+
inline-end overflow. `SegmentedControl` items grow on the block axis and by
|
|
983
|
+
half the gap sideways, which stays inside the group's padding, so neighbours
|
|
984
|
+
never overlap. `Input size="sm"` cannot host a pseudo-element and grows to 40px
|
|
985
|
+
instead, as should any control that clips its own overflow:
|
|
986
|
+
`min-height: var(--touch-target-compact)`.
|
|
987
|
+
|
|
975
988
|
`--control-height-*` and `--box-*` are **required tokens**: a consumer that
|
|
976
989
|
forks `variables.css` must keep them (Button `control`, Select `sm`, every `box`)
|
|
977
990
|
or those controls render with unresolved sizes.
|
|
@@ -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,6 +54,7 @@
|
|
|
50
54
|
let failed = $state(false);
|
|
51
55
|
$effect(() => {
|
|
52
56
|
src;
|
|
57
|
+
srcset;
|
|
53
58
|
failed = false;
|
|
54
59
|
});
|
|
55
60
|
|
|
@@ -76,7 +81,7 @@
|
|
|
76
81
|
{...rest}
|
|
77
82
|
>
|
|
78
83
|
{#if showImage}
|
|
79
|
-
<img {src} {alt} class="img fit-{fit}" loading="lazy" decoding="async" onerror={fail} />
|
|
84
|
+
<img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async" onerror={fail} />
|
|
80
85
|
{:else if fallback === 'initials' && text}
|
|
81
86
|
<span class="initials" aria-hidden="true">{text}</span>
|
|
82
87
|
{:else if fallback === 'icon'}
|
|
@@ -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'}
|
|
@@ -193,7 +195,7 @@
|
|
|
193
195
|
}
|
|
194
196
|
.badge-sm,
|
|
195
197
|
.badge-xs {
|
|
196
|
-
font-size: var(--badge-fs,
|
|
198
|
+
font-size: var(--badge-fs, var(--fs-xs));
|
|
197
199
|
gap: 0.15rem;
|
|
198
200
|
}
|
|
199
201
|
.badge-sm {
|
|
@@ -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`). */
|
|
@@ -544,7 +544,8 @@
|
|
|
544
544
|
The slab is the centred target with its inline-end half cut off: inline-end
|
|
545
545
|
overflow counts towards the scrollable overflow of every scroll ancestor, and
|
|
546
546
|
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.
|
|
547
|
+
the start would let each slab cover the previous sibling's centre. Text
|
|
548
|
+
buttons at size sm take the same shape sized by --touch-slab. */
|
|
548
549
|
@media (pointer: coarse) {
|
|
549
550
|
.btn-icon:not(.hit-compact),
|
|
550
551
|
.btn-icon-inline:not(.hit-compact),
|
|
@@ -566,6 +567,16 @@
|
|
|
566
567
|
inset-inline-start: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
567
568
|
inset-inline-end: 0;
|
|
568
569
|
}
|
|
570
|
+
.btn-sm:not(.btn-icon, .btn-icon-inline, .btn-box, .btn-square, .btn-chip, .btn-stacked, .hit-compact) {
|
|
571
|
+
position: relative;
|
|
572
|
+
}
|
|
573
|
+
.btn-sm:not(.btn-icon, .btn-icon-inline, .btn-box, .btn-square, .btn-chip, .btn-stacked, .hit-compact)::before {
|
|
574
|
+
content: '';
|
|
575
|
+
position: absolute;
|
|
576
|
+
inset-block: var(--touch-slab);
|
|
577
|
+
inset-inline-start: var(--touch-slab);
|
|
578
|
+
inset-inline-end: 0;
|
|
579
|
+
}
|
|
569
580
|
}
|
|
570
581
|
|
|
571
582
|
/* Two-state (toggle) buttons — e.g. an IconButton with `pressed`. Reacts to
|
|
@@ -703,7 +714,8 @@
|
|
|
703
714
|
.btn-link .btn-count,
|
|
704
715
|
.btn-icon-inline .btn-count {
|
|
705
716
|
--btn-count-size: 1rem;
|
|
706
|
-
|
|
717
|
+
padding: 0 0.2rem;
|
|
718
|
+
font-size: var(--fs-xs);
|
|
707
719
|
}
|
|
708
720
|
/* The accent fill would swallow an accent pill: invert on primary. */
|
|
709
721
|
.btn-primary .btn-count {
|
|
@@ -74,4 +74,16 @@
|
|
|
74
74
|
.align-center {
|
|
75
75
|
text-align: center;
|
|
76
76
|
}
|
|
77
|
+
@media (pointer: coarse) {
|
|
78
|
+
.link {
|
|
79
|
+
position: relative;
|
|
80
|
+
}
|
|
81
|
+
.link::before {
|
|
82
|
+
content: '';
|
|
83
|
+
position: absolute;
|
|
84
|
+
inset-block: var(--touch-slab);
|
|
85
|
+
inset-inline-start: var(--touch-slab);
|
|
86
|
+
inset-inline-end: 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
</style>
|
|
@@ -241,6 +241,7 @@
|
|
|
241
241
|
with Button size="sm", Popover size="sm" and SegmentedControl size="sm". */
|
|
242
242
|
.select.select-sm {
|
|
243
243
|
height: var(--select-size, var(--control-height-compact));
|
|
244
|
+
min-height: var(--select-size, var(--control-height-compact));
|
|
244
245
|
}
|
|
245
246
|
.select.w-auto {
|
|
246
247
|
width: auto;
|
|
@@ -124,6 +124,18 @@
|
|
|
124
124
|
.switch-sm.on .knob {
|
|
125
125
|
transform: translateX(0.8rem);
|
|
126
126
|
}
|
|
127
|
+
@media (pointer: coarse) {
|
|
128
|
+
.switch {
|
|
129
|
+
position: relative;
|
|
130
|
+
}
|
|
131
|
+
.switch::before {
|
|
132
|
+
content: '';
|
|
133
|
+
position: absolute;
|
|
134
|
+
inset-block: var(--touch-slab);
|
|
135
|
+
inset-inline-start: var(--touch-slab);
|
|
136
|
+
inset-inline-end: 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
127
139
|
.switch-row {
|
|
128
140
|
display: inline-flex;
|
|
129
141
|
align-items: center;
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
char,
|
|
20
20
|
separator,
|
|
21
21
|
maxItems = 0,
|
|
22
|
+
size = 'sm',
|
|
23
|
+
wrap = true,
|
|
24
|
+
collapseCurrent = false,
|
|
22
25
|
class: klass = '',
|
|
23
26
|
style: styleProp = '',
|
|
24
27
|
}: {
|
|
@@ -29,6 +32,12 @@
|
|
|
29
32
|
/** Collapse the middle to an ellipsis when the trail is longer than this
|
|
30
33
|
* (0 = never collapse). Keeps the first item and the last `maxItems - 1`. */
|
|
31
34
|
maxItems?: number;
|
|
35
|
+
size?: 'sm' | 'md';
|
|
36
|
+
/** `false` keeps the trail on one line: ancestors keep their width and the
|
|
37
|
+
* current crumb shrinks with an ellipsis. */
|
|
38
|
+
wrap?: boolean;
|
|
39
|
+
/** Hide the current crumb and its separator, keeping only the ancestors. */
|
|
40
|
+
collapseCurrent?: boolean;
|
|
32
41
|
class?: string;
|
|
33
42
|
style?: string;
|
|
34
43
|
} = $props();
|
|
@@ -43,7 +52,14 @@
|
|
|
43
52
|
</script>
|
|
44
53
|
|
|
45
54
|
<nav aria-label="breadcrumb" data-tsu="Breadcrumb" class={klass} style={styleProp}>
|
|
46
|
-
<ol
|
|
55
|
+
<ol
|
|
56
|
+
class={[
|
|
57
|
+
'crumbs',
|
|
58
|
+
size === 'md' && 'crumbs--md',
|
|
59
|
+
!wrap && 'crumbs--nowrap',
|
|
60
|
+
collapseCurrent && 'crumbs--collapse',
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
47
63
|
{#each shown as item, i (i)}
|
|
48
64
|
<li class="crumb">
|
|
49
65
|
{#if 'ellipsis' in item}
|
|
@@ -57,7 +73,7 @@
|
|
|
57
73
|
<span class="sep" aria-hidden="true">
|
|
58
74
|
{#if separator}{@render separator()}{:else if char}{char}{:else}<Icon
|
|
59
75
|
name="chevron-right"
|
|
60
|
-
size={14}
|
|
76
|
+
size={size === 'md' ? 16 : 14}
|
|
61
77
|
/>{/if}
|
|
62
78
|
</span>
|
|
63
79
|
{/if}
|
|
@@ -77,6 +93,26 @@
|
|
|
77
93
|
list-style: none;
|
|
78
94
|
font-size: var(--fs-sm);
|
|
79
95
|
}
|
|
96
|
+
.crumbs--md {
|
|
97
|
+
font-size: var(--fs-md);
|
|
98
|
+
}
|
|
99
|
+
.crumbs--nowrap {
|
|
100
|
+
flex-wrap: nowrap;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
}
|
|
103
|
+
.crumbs--nowrap .crumb {
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
}
|
|
106
|
+
.crumbs--nowrap .crumb:last-child {
|
|
107
|
+
flex-shrink: 1;
|
|
108
|
+
}
|
|
109
|
+
.crumbs--nowrap .current {
|
|
110
|
+
max-width: none;
|
|
111
|
+
}
|
|
112
|
+
.crumbs--collapse .crumb:last-child:not(:first-child),
|
|
113
|
+
.crumbs--collapse .crumb:nth-last-child(2) .sep {
|
|
114
|
+
display: none;
|
|
115
|
+
}
|
|
80
116
|
.crumb {
|
|
81
117
|
display: inline-flex;
|
|
82
118
|
align-items: center;
|
|
@@ -104,6 +140,12 @@
|
|
|
104
140
|
text-overflow: ellipsis;
|
|
105
141
|
max-width: 24ch;
|
|
106
142
|
}
|
|
143
|
+
@media (hover: none) and (pointer: coarse) {
|
|
144
|
+
.link {
|
|
145
|
+
min-height: 40px;
|
|
146
|
+
align-content: center;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
107
149
|
.ellipsis {
|
|
108
150
|
color: var(--text-muted);
|
|
109
151
|
}
|
|
@@ -11,6 +11,12 @@ type $$ComponentProps = {
|
|
|
11
11
|
/** Collapse the middle to an ellipsis when the trail is longer than this
|
|
12
12
|
* (0 = never collapse). Keeps the first item and the last `maxItems - 1`. */
|
|
13
13
|
maxItems?: number;
|
|
14
|
+
size?: 'sm' | 'md';
|
|
15
|
+
/** `false` keeps the trail on one line: ancestors keep their width and the
|
|
16
|
+
* current crumb shrinks with an ellipsis. */
|
|
17
|
+
wrap?: boolean;
|
|
18
|
+
/** Hide the current crumb and its separator, keeping only the ancestors. */
|
|
19
|
+
collapseCurrent?: boolean;
|
|
14
20
|
class?: string;
|
|
15
21
|
style?: string;
|
|
16
22
|
};
|
|
@@ -179,6 +179,7 @@
|
|
|
179
179
|
flex-wrap: nowrap;
|
|
180
180
|
max-width: 100%;
|
|
181
181
|
overflow-x: auto;
|
|
182
|
+
overflow-y: hidden;
|
|
182
183
|
scrollbar-width: none;
|
|
183
184
|
-webkit-overflow-scrolling: touch;
|
|
184
185
|
}
|
|
@@ -244,6 +245,37 @@
|
|
|
244
245
|
.seg-box .seg-item {
|
|
245
246
|
height: 100%;
|
|
246
247
|
}
|
|
248
|
+
/* Wrapped rows size to their content; each item keeps the one-row height. */
|
|
249
|
+
.seg-wrap.seg-sm,
|
|
250
|
+
.seg-wrap.seg-control,
|
|
251
|
+
.seg-wrap.seg-box {
|
|
252
|
+
height: auto;
|
|
253
|
+
}
|
|
254
|
+
.seg-wrap.seg-sm .seg-item {
|
|
255
|
+
height: auto;
|
|
256
|
+
min-height: calc(var(--control-height-compact) - 8px);
|
|
257
|
+
}
|
|
258
|
+
.seg-wrap.seg-control .seg-item {
|
|
259
|
+
height: auto;
|
|
260
|
+
min-height: calc(var(--control-height) - 8px);
|
|
261
|
+
}
|
|
262
|
+
.seg-wrap.seg-box .seg-item {
|
|
263
|
+
height: auto;
|
|
264
|
+
min-height: calc(var(--control-height-default) - 8px);
|
|
265
|
+
}
|
|
266
|
+
/* Capped at half a row: one row stays a pill, extra rows square the corners. */
|
|
267
|
+
.seg-wrap:not(.seg-icon) {
|
|
268
|
+
border-radius: calc((0.6rem + 1.3 * var(--fs-sm)) / 2 + 4px);
|
|
269
|
+
}
|
|
270
|
+
.seg-wrap.seg-sm:not(.seg-icon) {
|
|
271
|
+
border-radius: calc(var(--control-height-compact) / 2);
|
|
272
|
+
}
|
|
273
|
+
.seg-wrap.seg-control:not(.seg-icon) {
|
|
274
|
+
border-radius: calc(var(--control-height) / 2);
|
|
275
|
+
}
|
|
276
|
+
.seg-wrap.seg-box:not(.seg-icon) {
|
|
277
|
+
border-radius: calc(var(--control-height-default) / 2);
|
|
278
|
+
}
|
|
247
279
|
.seg-icon .seg-item {
|
|
248
280
|
gap: 0;
|
|
249
281
|
padding: 0.35rem;
|
|
@@ -320,13 +352,24 @@
|
|
|
320
352
|
border-radius: var(--r-pill);
|
|
321
353
|
background: var(--bg-elevated-2);
|
|
322
354
|
color: var(--text-faint);
|
|
323
|
-
font-size:
|
|
355
|
+
font-size: var(--fs-xs);
|
|
324
356
|
font-weight: var(--fw-semibold);
|
|
325
357
|
font-variant-numeric: tabular-nums;
|
|
326
358
|
transition:
|
|
327
359
|
background 0.12s var(--ease),
|
|
328
360
|
color 0.12s var(--ease);
|
|
329
361
|
}
|
|
362
|
+
@media (pointer: coarse) {
|
|
363
|
+
.seg-item {
|
|
364
|
+
position: relative;
|
|
365
|
+
}
|
|
366
|
+
.seg-item::before {
|
|
367
|
+
content: '';
|
|
368
|
+
position: absolute;
|
|
369
|
+
inset-block: var(--touch-slab);
|
|
370
|
+
inset-inline: calc(var(--sp-1) / -2);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
330
373
|
.seg-item.selected .seg-count {
|
|
331
374
|
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
|
332
375
|
color: var(--accent);
|
|
@@ -108,4 +108,16 @@
|
|
|
108
108
|
.toggle.struck {
|
|
109
109
|
text-decoration: line-through;
|
|
110
110
|
}
|
|
111
|
+
@media (pointer: coarse) {
|
|
112
|
+
.toggle {
|
|
113
|
+
position: relative;
|
|
114
|
+
}
|
|
115
|
+
.toggle::before {
|
|
116
|
+
content: '';
|
|
117
|
+
position: absolute;
|
|
118
|
+
inset-block: var(--touch-slab);
|
|
119
|
+
inset-inline-start: var(--touch-slab);
|
|
120
|
+
inset-inline-end: 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
111
123
|
</style>
|
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
* detail cells share lines (`responsive="stack"` only, default true). */
|
|
26
26
|
stackLabel?: boolean;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
let rootFontPx: number | undefined;
|
|
30
|
+
|
|
31
|
+
function rootFontSize(): number {
|
|
32
|
+
if (rootFontPx === undefined) {
|
|
33
|
+
rootFontPx = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
34
|
+
window.addEventListener('resize', () => (rootFontPx = undefined), { once: true });
|
|
35
|
+
}
|
|
36
|
+
return rootFontPx;
|
|
37
|
+
}
|
|
28
38
|
</script>
|
|
29
39
|
|
|
30
40
|
<script lang="ts" generics="T">
|
|
@@ -60,6 +70,7 @@
|
|
|
60
70
|
loadMoreLabel = 'Load more',
|
|
61
71
|
responsive = 'scroll',
|
|
62
72
|
stackBelow = '48rem',
|
|
73
|
+
stackActions = 'start',
|
|
63
74
|
class: klass = '',
|
|
64
75
|
style: styleProp = '',
|
|
65
76
|
tableClass = '',
|
|
@@ -101,6 +112,9 @@
|
|
|
101
112
|
responsive?: 'scroll' | 'stack';
|
|
102
113
|
/** Width (px/em/rem) under which `stack` mode kicks in. */
|
|
103
114
|
stackBelow?: string;
|
|
115
|
+
/** Where actions sit in a stacked card: `start` beside the title (wraps
|
|
116
|
+
* under a long one), `end` always closing the card. */
|
|
117
|
+
stackActions?: 'start' | 'end';
|
|
104
118
|
class?: string;
|
|
105
119
|
style?: string;
|
|
106
120
|
/** Class on the inner <table>. */
|
|
@@ -121,22 +135,31 @@
|
|
|
121
135
|
return { destroy: () => observer.disconnect() };
|
|
122
136
|
}
|
|
123
137
|
|
|
138
|
+
function markEmpty(node: HTMLElement) {
|
|
139
|
+
const update = () => node.toggleAttribute('data-empty', !node.firstElementChild && !node.textContent?.trim());
|
|
140
|
+
update();
|
|
141
|
+
const observer = new MutationObserver(update);
|
|
142
|
+
observer.observe(node, { childList: true, subtree: true, characterData: true });
|
|
143
|
+
return () => observer.disconnect();
|
|
144
|
+
}
|
|
145
|
+
|
|
124
146
|
function toPx(length: string, el: HTMLElement): number {
|
|
125
147
|
const n = Number.parseFloat(length);
|
|
126
|
-
|
|
127
|
-
if (length.endsWith('
|
|
128
|
-
if (length.endsWith('em')) return n * fontSize(el);
|
|
148
|
+
if (length.endsWith('rem')) return n * rootFontSize();
|
|
149
|
+
if (length.endsWith('em')) return n * Number.parseFloat(getComputedStyle(el).fontSize);
|
|
129
150
|
return n;
|
|
130
151
|
}
|
|
131
152
|
|
|
153
|
+
// Font sizes are read in the observer callback, after layout, so mounting never forces a style recalc.
|
|
132
154
|
$effect(() => {
|
|
133
155
|
if (responsive !== 'stack' || !wrapEl) {
|
|
134
156
|
stacked = false;
|
|
135
157
|
return;
|
|
136
158
|
}
|
|
137
159
|
const el = wrapEl;
|
|
138
|
-
const
|
|
160
|
+
const below = stackBelow;
|
|
139
161
|
const observer = new ResizeObserver(([entry]) => {
|
|
162
|
+
const limit = toPx(below, el);
|
|
140
163
|
stacked = entry.contentRect.width < limit;
|
|
141
164
|
});
|
|
142
165
|
observer.observe(el);
|
|
@@ -188,6 +211,7 @@
|
|
|
188
211
|
data-tsu="DataTable"
|
|
189
212
|
data-size={size}
|
|
190
213
|
data-stacked={stacked || undefined}
|
|
214
|
+
data-stack-actions={responsive === 'stack' && stackActions === 'end' ? 'end' : undefined}
|
|
191
215
|
aria-busy={loading || undefined}
|
|
192
216
|
>
|
|
193
217
|
<table
|
|
@@ -267,20 +291,25 @@
|
|
|
267
291
|
: undefined}
|
|
268
292
|
>
|
|
269
293
|
{#each columns as col (col.key)}
|
|
294
|
+
{@const value = cellSnippets[col.key] ? undefined : display(col, row)}
|
|
270
295
|
<td
|
|
271
296
|
data-part="cell"
|
|
272
297
|
data-hide-below={col.hideBelow}
|
|
273
298
|
data-role={responsive === 'stack' ? (col.role ?? 'detail') : undefined}
|
|
274
299
|
data-label={responsive === 'stack' ? col.label : undefined}
|
|
275
300
|
data-stack-label={responsive === 'stack' && col.stackLabel === false ? 'false' : undefined}
|
|
301
|
+
data-empty={responsive === 'stack' && !cellSnippets[col.key] && (value == null || value === '')
|
|
302
|
+
? ''
|
|
303
|
+
: undefined}
|
|
276
304
|
class:truncate={col.truncate}
|
|
277
305
|
class:nowrap={col.nowrap}
|
|
278
306
|
style:text-align={stacked ? undefined : (col.align ?? 'left')}
|
|
307
|
+
{@attach responsive === 'stack' && cellSnippets[col.key] ? markEmpty : undefined}
|
|
279
308
|
>
|
|
280
309
|
{#if cellSnippets[col.key]}
|
|
281
310
|
{@render cellSnippets[col.key](row)}
|
|
282
311
|
{:else}
|
|
283
|
-
{
|
|
312
|
+
{value}
|
|
284
313
|
{/if}
|
|
285
314
|
</td>
|
|
286
315
|
{/each}
|
|
@@ -479,6 +508,9 @@
|
|
|
479
508
|
[data-stacked] {
|
|
480
509
|
overflow-x: visible;
|
|
481
510
|
}
|
|
511
|
+
[data-stacked] .dt {
|
|
512
|
+
table-layout: fixed;
|
|
513
|
+
}
|
|
482
514
|
[data-stacked] thead {
|
|
483
515
|
position: absolute;
|
|
484
516
|
width: 1px;
|
|
@@ -552,10 +584,17 @@
|
|
|
552
584
|
[data-stacked] td[data-stack-label='false']::before {
|
|
553
585
|
content: none;
|
|
554
586
|
}
|
|
555
|
-
[data-stacked] td[data-
|
|
587
|
+
[data-stacked] td[data-part='cell'][data-empty] {
|
|
588
|
+
display: none;
|
|
589
|
+
}
|
|
590
|
+
[data-stacked]
|
|
591
|
+
td[data-role='meta']:not([data-empty])
|
|
592
|
+
~ td[data-role='meta'][data-stack-label='false']:not([data-empty]) {
|
|
556
593
|
margin-inline-start: calc(var(--sp-1) - var(--sp-3));
|
|
557
594
|
}
|
|
558
|
-
[data-stacked]
|
|
595
|
+
[data-stacked]
|
|
596
|
+
td[data-role='meta']:not([data-empty])
|
|
597
|
+
~ td[data-role='meta'][data-stack-label='false']:not([data-empty])::before {
|
|
559
598
|
content: '·';
|
|
560
599
|
margin-inline-end: var(--sp-1);
|
|
561
600
|
color: var(--text-faint);
|
|
@@ -568,6 +607,10 @@
|
|
|
568
607
|
width: auto;
|
|
569
608
|
opacity: 1;
|
|
570
609
|
}
|
|
610
|
+
[data-stacked][data-stack-actions='end'] td[data-role='actions'],
|
|
611
|
+
[data-stacked][data-stack-actions='end'] td.dt-actions {
|
|
612
|
+
order: 2;
|
|
613
|
+
}
|
|
571
614
|
[data-stacked] td[data-role='hidden'] {
|
|
572
615
|
display: none;
|
|
573
616
|
}
|
|
@@ -63,6 +63,9 @@ declare function $$render<T>(): {
|
|
|
63
63
|
responsive?: "scroll" | "stack";
|
|
64
64
|
/** Width (px/em/rem) under which `stack` mode kicks in. */
|
|
65
65
|
stackBelow?: string;
|
|
66
|
+
/** Where actions sit in a stacked card: `start` beside the title (wraps
|
|
67
|
+
* under a long one), `end` always closing the card. */
|
|
68
|
+
stackActions?: "start" | "end";
|
|
66
69
|
class?: string;
|
|
67
70
|
style?: string;
|
|
68
71
|
/** Class on the inner <table>. */
|
package/dist/styles/tokens.css
CHANGED
|
@@ -184,6 +184,12 @@
|
|
|
184
184
|
/* Minimum hit area on coarse pointers (WCAG 2.5.8); px so it never shrinks
|
|
185
185
|
under a small root font-size. */
|
|
186
186
|
--touch-target: 44px;
|
|
187
|
+
/* Compact controls on coarse pointers: an absolute ::before with inset-block
|
|
188
|
+
and inset-inline-start at var(--touch-slab) and inset-inline-end: 0 (no
|
|
189
|
+
scrollable overflow sideways). Controls that clip their overflow grow
|
|
190
|
+
min-height to --touch-target-compact instead. */
|
|
191
|
+
--touch-target-compact: 40px;
|
|
192
|
+
--touch-slab: min(-8px, calc((100% - var(--touch-target-compact)) / 2));
|
|
187
193
|
|
|
188
194
|
/* ---- Layout ---- */
|
|
189
195
|
/* Fixed (px, not rem): the header is chrome that must NOT scale with the
|
package/package.json
CHANGED