@dorsk/tsumikit 0.73.0 → 0.73.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/components/atoms/Button.svelte +12 -1
- package/dist/components/atoms/Checkbox.svelte +12 -0
- package/dist/components/atoms/Progress.svelte +1 -1
- package/dist/components/atoms/Select.svelte +1 -1
- package/dist/components/atoms/Slider.svelte +2 -2
- package/dist/components/layouts/TabBar.svelte +3 -1
- package/dist/components/molecules/EmojiPicker.svelte +1 -1
- package/dist/components/molecules/FilterInput.svelte +1 -1
- package/dist/components/molecules/Menu.svelte +3 -1
- package/dist/components/molecules/Menu.svelte.d.ts +2 -0
- package/dist/components/molecules/RadialMenu.svelte +1 -1
- package/dist/components/molecules/SegmentedControl.svelte +44 -0
- package/dist/components/molecules/SelectButton.svelte +1 -1
- package/dist/components/molecules/SwipeRow.svelte +6 -2
- package/dist/components/molecules/SwipeRow.svelte.d.ts +4 -0
- package/dist/components/organisms/DataTable.svelte +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,7 +286,8 @@ its icon leading and shows its check trailing, or uses the leading check slot wh
|
|
|
286
286
|
no icon; selecting closes the menu unless `closeOnSelect={false}` or the item sets `keepOpen`;
|
|
287
287
|
rest props land on the trigger, `count`/`countMax` paint its corner badge and `panelStyle`
|
|
288
288
|
sizes the panel; an item with a `control` snippet renders as a non-button row hosting a native
|
|
289
|
-
`<select>` or nested Popover trigger, keeping the icon column, row height and ↑/↓ navigation
|
|
289
|
+
`<select>` or nested Popover trigger, keeping the icon column, row height and ↑/↓ navigation;
|
|
290
|
+
labels may repeat, and an optional `id` keeps a row's identity when the list reorders),
|
|
290
291
|
Tabs (omit `panel` for a strip-only tablist of document tabs: `closable` + `onclose(id)`
|
|
291
292
|
close through the ✕, Delete and middle-click, per-tab `leading` snippet, `title`,
|
|
292
293
|
trailing `actions` snippet; `--tab-max-width` truncates long labels),
|
|
@@ -1033,6 +1034,10 @@ half the gap sideways, which stays inside the group's padding, so neighbours
|
|
|
1033
1034
|
never overlap. `Input size="sm"` cannot host a pseudo-element and grows to 40px
|
|
1034
1035
|
instead, as should any control that clips its own overflow:
|
|
1035
1036
|
`min-height: var(--touch-target-compact)`.
|
|
1037
|
+
`Checkbox`, `Button variant="link"` and the `DataTable` sort button take the same
|
|
1038
|
+
slab; inside a DataTable it is clipped at the table's top edge. A `SegmentedControl`
|
|
1039
|
+
wider than its container switches to the `scroll` layout until it fits again
|
|
1040
|
+
(`wrap` wraps instead), and `block` segments ellipsise their labels.
|
|
1036
1041
|
|
|
1037
1042
|
`--control-height-*` and `--box-*` are **required tokens**: a consumer that
|
|
1038
1043
|
forks `variables.css` must keep them (Button `control`, Select `sm`, every `box`)
|
|
@@ -561,7 +561,8 @@
|
|
|
561
561
|
overflow counts towards the scrollable overflow of every scroll ancestor, and
|
|
562
562
|
in a row that half lies under the next sibling anyway. Growing further towards
|
|
563
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
|
|
564
|
+
buttons at size sm and link buttons take the same shape sized by
|
|
565
|
+
--touch-slab. */
|
|
565
566
|
@media (pointer: coarse) {
|
|
566
567
|
.btn-icon:not(.hit-compact),
|
|
567
568
|
.btn-icon-inline:not(.hit-compact),
|
|
@@ -593,6 +594,16 @@
|
|
|
593
594
|
inset-inline-start: var(--touch-slab);
|
|
594
595
|
inset-inline-end: 0;
|
|
595
596
|
}
|
|
597
|
+
.btn-link:not(.hit-compact) {
|
|
598
|
+
position: relative;
|
|
599
|
+
}
|
|
600
|
+
.btn-link:not(.hit-compact)::before {
|
|
601
|
+
content: '';
|
|
602
|
+
position: absolute;
|
|
603
|
+
inset-block: var(--touch-slab);
|
|
604
|
+
inset-inline-start: var(--touch-slab);
|
|
605
|
+
inset-inline-end: 0;
|
|
606
|
+
}
|
|
596
607
|
}
|
|
597
608
|
|
|
598
609
|
/* Two-state (toggle) buttons — e.g. an IconButton with `pressed`. Reacts to
|
|
@@ -141,6 +141,18 @@
|
|
|
141
141
|
.checkbox:has(input:disabled) {
|
|
142
142
|
cursor: not-allowed;
|
|
143
143
|
}
|
|
144
|
+
@media (pointer: coarse) {
|
|
145
|
+
.checkbox {
|
|
146
|
+
position: relative;
|
|
147
|
+
}
|
|
148
|
+
.checkbox::before {
|
|
149
|
+
content: '';
|
|
150
|
+
position: absolute;
|
|
151
|
+
inset-block: var(--touch-slab);
|
|
152
|
+
inset-inline-start: var(--touch-slab);
|
|
153
|
+
inset-inline-end: 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
144
156
|
.sr-only {
|
|
145
157
|
position: absolute;
|
|
146
158
|
width: 1px;
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
aria-valuenow={indeterminate ? undefined : value}
|
|
97
97
|
>
|
|
98
98
|
<div class="bar" style={indeterminate ? undefined : `width: ${pct}%`}></div>
|
|
99
|
-
{#each markers as m (m.at)}
|
|
99
|
+
{#each markers as m, i (`${i}:${m.at}`)}
|
|
100
100
|
<span
|
|
101
101
|
class="marker tone-{m.tone ? canonicalTone(m.tone) : 'text'}"
|
|
102
102
|
style="left: {markerPct(m.at)}%"
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
|
|
113
113
|
{#snippet optionList()}
|
|
114
114
|
{#if options}
|
|
115
|
-
{#each sections as section, i (section.group ??
|
|
115
|
+
{#each sections as section, i (`${i}:${section.group ?? ''}`)}
|
|
116
116
|
{#if section.group === undefined}
|
|
117
117
|
{#each section.options as o (o.value)}
|
|
118
118
|
<option value={o.value} disabled={o.disabled}>{optionText(o)}</option>
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
/>
|
|
90
90
|
{#if tickValues.length > 0}
|
|
91
91
|
<span class="ticks" aria-hidden="true">
|
|
92
|
-
{#each tickValues as t (t)}
|
|
92
|
+
{#each tickValues as t, i (`${i}:${t}`)}
|
|
93
93
|
<i class="tick" class:reached={t <= Number(value)} style="--f: {frac(t)}"></i>
|
|
94
94
|
{/each}
|
|
95
95
|
</span>
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
{/if}
|
|
101
101
|
{#if marks.length > 0}
|
|
102
102
|
<span class="marks" aria-hidden="true">
|
|
103
|
-
{#each marks as m (m.value)}
|
|
103
|
+
{#each marks as m, i (`${i}:${m.value}`)}
|
|
104
104
|
<button type="button" class="mark" class:on={Number(value) === m.value} style="--f: {frac(m.value)}" tabindex="-1" onclick={() => (value = m.value)}>{m.label}</button>
|
|
105
105
|
{/each}
|
|
106
106
|
</span>
|
|
@@ -207,7 +207,9 @@
|
|
|
207
207
|
.tabbar :global(.tabbar-badge) {
|
|
208
208
|
position: absolute;
|
|
209
209
|
top: -0.4rem;
|
|
210
|
-
left: calc(100% -
|
|
210
|
+
left: calc(100% - 2px);
|
|
211
|
+
max-width: none;
|
|
212
|
+
padding-inline: 0.3rem;
|
|
211
213
|
}
|
|
212
214
|
@media (prefers-reduced-motion: reduce) {
|
|
213
215
|
.tabbar-item {
|
|
@@ -429,7 +429,7 @@
|
|
|
429
429
|
{#if open && menu}
|
|
430
430
|
<div bind:this={menuEl} id={listboxId} class="fi__menu" role="listbox" aria-label={kindLabel[menu.kind]}>
|
|
431
431
|
<div class="fi__menu-head">{kindLabel[menu.kind]}</div>
|
|
432
|
-
{#each menu.items as item, i (item.insert
|
|
432
|
+
{#each menu.items as item, i (`${i}:${item.insert}`)}
|
|
433
433
|
<button
|
|
434
434
|
type="button"
|
|
435
435
|
id="{uid}-opt-{i}"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
export interface MenuItem {
|
|
3
3
|
label: string;
|
|
4
|
+
/** Stable row identity; labels may repeat. */
|
|
5
|
+
id?: string;
|
|
4
6
|
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
5
7
|
onselect?: () => void;
|
|
6
8
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
@@ -202,7 +204,7 @@
|
|
|
202
204
|
>
|
|
203
205
|
{#snippet children({ close }: { close: () => void })}
|
|
204
206
|
<div bind:this={listEl} role="none" class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
|
|
205
|
-
{#each items as item (item.label)}
|
|
207
|
+
{#each items as item, i (item.id ?? `${i}:${item.label}`)}
|
|
206
208
|
{#if item.control}
|
|
207
209
|
<div {...item.attrs as HTMLAttributes<HTMLDivElement>} role="none" class="menu-item menu-row" class:danger={item.danger} inert={item.disabled}>
|
|
208
210
|
{#if item.icon}<Icon name={item.icon} />{/if}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface MenuItem {
|
|
2
2
|
label: string;
|
|
3
|
+
/** Stable row identity; labels may repeat. */
|
|
4
|
+
id?: string;
|
|
3
5
|
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
4
6
|
onselect?: () => void;
|
|
5
7
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
@@ -390,7 +390,7 @@
|
|
|
390
390
|
{/if}
|
|
391
391
|
<div id={menuId} role="menu" aria-label={label} class="radial-menu">
|
|
392
392
|
{#if open}
|
|
393
|
-
{#each petals as item, i (item.label)}
|
|
393
|
+
{#each petals as item, i (item.id ?? `${i}:${item.label}`)}
|
|
394
394
|
{@const p = points[i]}
|
|
395
395
|
{#if p}
|
|
396
396
|
{@const glyphs = menuItemGlyphs(item)}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// browser-free keyboard model matches Tabs: ←/→ and Home/End move selection,
|
|
25
25
|
// Space/Enter (or click) activate. `value` is bindable.
|
|
26
26
|
import type { Snippet } from 'svelte';
|
|
27
|
+
import type { Attachment } from 'svelte/attachments';
|
|
27
28
|
import Icon from '../atoms/Icon.svelte';
|
|
28
29
|
import { formatSegmentCount } from './segmented-control-count.js';
|
|
29
30
|
import { nextEnabledSegment } from './segmented-control-keyboard.js';
|
|
@@ -95,6 +96,26 @@
|
|
|
95
96
|
);
|
|
96
97
|
const segId = (val: string) => `${baseId}-${val}`;
|
|
97
98
|
|
|
99
|
+
// A pill wider than its container falls back to the `scroll` layout; the
|
|
100
|
+
// class is dropped again once the row fits so the touch slab stays unclipped.
|
|
101
|
+
let overflowing = $state(false);
|
|
102
|
+
const watchOverflow: Attachment<HTMLElement> = (node) => {
|
|
103
|
+
if (wrap || typeof ResizeObserver === 'undefined') {
|
|
104
|
+
overflowing = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const check = () => {
|
|
108
|
+
overflowing = node.scrollWidth - node.clientWidth > 1;
|
|
109
|
+
};
|
|
110
|
+
const ro = new ResizeObserver(check);
|
|
111
|
+
ro.observe(node);
|
|
112
|
+
check();
|
|
113
|
+
return () => {
|
|
114
|
+
ro.disconnect();
|
|
115
|
+
overflowing = false;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
98
119
|
function select(val: string, focus = false) {
|
|
99
120
|
if (options.find((o) => o.value === val)?.disabled) return;
|
|
100
121
|
value = val;
|
|
@@ -127,8 +148,10 @@
|
|
|
127
148
|
class:collapse-container={collapseLabels === 'container'}
|
|
128
149
|
class:seg-scroll={scroll}
|
|
129
150
|
class:seg-block={block}
|
|
151
|
+
class:seg-overflow={overflowing}
|
|
130
152
|
data-tsu="SegmentedControl"
|
|
131
153
|
{onkeydown}
|
|
154
|
+
{@attach watchOverflow}
|
|
132
155
|
>
|
|
133
156
|
{#each options as o (o.value)}
|
|
134
157
|
{@const selected = value === o.value}
|
|
@@ -165,6 +188,8 @@
|
|
|
165
188
|
background: var(--bg-elevated-2);
|
|
166
189
|
border: 1px solid var(--border);
|
|
167
190
|
border-radius: var(--r-pill);
|
|
191
|
+
max-width: 100%;
|
|
192
|
+
min-width: 0;
|
|
168
193
|
}
|
|
169
194
|
.seg-icon {
|
|
170
195
|
border-radius: var(--r-md);
|
|
@@ -193,6 +218,19 @@
|
|
|
193
218
|
.seg-scroll .seg-item {
|
|
194
219
|
flex: none;
|
|
195
220
|
}
|
|
221
|
+
.seg-overflow {
|
|
222
|
+
flex-wrap: nowrap;
|
|
223
|
+
overflow-x: auto;
|
|
224
|
+
overflow-y: hidden;
|
|
225
|
+
scrollbar-width: none;
|
|
226
|
+
-webkit-overflow-scrolling: touch;
|
|
227
|
+
}
|
|
228
|
+
.seg-overflow::-webkit-scrollbar {
|
|
229
|
+
display: none;
|
|
230
|
+
}
|
|
231
|
+
.seg-overflow .seg-item {
|
|
232
|
+
flex: none;
|
|
233
|
+
}
|
|
196
234
|
.seg-block {
|
|
197
235
|
display: flex;
|
|
198
236
|
width: 100%;
|
|
@@ -200,6 +238,12 @@
|
|
|
200
238
|
.seg-block .seg-item {
|
|
201
239
|
flex: 1 1 0;
|
|
202
240
|
justify-content: center;
|
|
241
|
+
min-width: 0;
|
|
242
|
+
}
|
|
243
|
+
.seg-block .seg-label {
|
|
244
|
+
min-width: 0;
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
text-overflow: ellipsis;
|
|
203
247
|
}
|
|
204
248
|
|
|
205
249
|
.seg-item {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
onchange={(e) => onchange((e.currentTarget as HTMLSelectElement).value)}
|
|
93
93
|
>
|
|
94
94
|
{#if groups}
|
|
95
|
-
{#each groups as g (g.label)}
|
|
95
|
+
{#each groups as g, i (`${i}:${g.label}`)}
|
|
96
96
|
<optgroup label={g.label}>
|
|
97
97
|
{#each g.options as o (o.value)}
|
|
98
98
|
<option value={o.value}>{o.label}</option>
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* a drag that is more vertical than horizontal is left to the scroller.
|
|
19
19
|
* The same actions are reachable as visually hidden buttons for keyboard
|
|
20
20
|
* and screen-reader users.
|
|
21
|
+
*
|
|
22
|
+
* On touch the browser implicitly captures the pointer on the element under
|
|
23
|
+
* the finger; taking capture on the row makes that child fire a bubbling
|
|
24
|
+
* `lostpointercapture`, which must not be read as a cancel.
|
|
21
25
|
*/
|
|
22
26
|
import type { Snippet } from 'svelte';
|
|
23
27
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
@@ -162,8 +166,8 @@
|
|
|
162
166
|
e.preventDefault();
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
function onlostpointercapture() {
|
|
166
|
-
if (pointerId === null) return;
|
|
169
|
+
function onlostpointercapture(e: PointerEvent) {
|
|
170
|
+
if (pointerId === null || e.pointerId !== pointerId || e.target !== e.currentTarget) return;
|
|
167
171
|
pointerId = null;
|
|
168
172
|
locked = null;
|
|
169
173
|
dragging = false;
|
|
@@ -13,6 +13,10 @@ export interface SwipeAction {
|
|
|
13
13
|
* a drag that is more vertical than horizontal is left to the scroller.
|
|
14
14
|
* The same actions are reachable as visually hidden buttons for keyboard
|
|
15
15
|
* and screen-reader users.
|
|
16
|
+
*
|
|
17
|
+
* On touch the browser implicitly captures the pointer on the element under
|
|
18
|
+
* the finger; taking capture on the row makes that child fire a bubbling
|
|
19
|
+
* `lostpointercapture`, which must not be read as a cancel.
|
|
16
20
|
*/
|
|
17
21
|
import type { Snippet } from 'svelte';
|
|
18
22
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
@@ -503,6 +503,18 @@
|
|
|
503
503
|
.dt-actions {
|
|
504
504
|
opacity: 1;
|
|
505
505
|
}
|
|
506
|
+
/* Sort slab: inline-end stays at 0 so it never reaches the next header
|
|
507
|
+
cell, and the inline-start reach stays inside the cell's own padding. */
|
|
508
|
+
.dt-sort {
|
|
509
|
+
position: relative;
|
|
510
|
+
}
|
|
511
|
+
.dt-sort::before {
|
|
512
|
+
content: '';
|
|
513
|
+
position: absolute;
|
|
514
|
+
inset-block: var(--touch-slab);
|
|
515
|
+
inset-inline-start: var(--touch-slab);
|
|
516
|
+
inset-inline-end: 0;
|
|
517
|
+
}
|
|
506
518
|
}
|
|
507
519
|
@container (max-width: 30rem) {
|
|
508
520
|
[data-hide-below='sm'] {
|
package/package.json
CHANGED