@dorsk/tsumikit 0.64.0 → 0.65.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 +7 -2
- package/dist/components/atoms/Button.svelte +8 -2
- package/dist/components/layouts/ResizablePanel.svelte +2 -1
- package/dist/components/molecules/FileButton.svelte +3 -1
- package/dist/components/molecules/Menu.svelte +40 -11
- package/dist/components/molecules/Menu.svelte.d.ts +11 -1
- package/dist/components/molecules/Popover.svelte +6 -2
- package/dist/styles/reset.css +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -276,7 +276,8 @@ and an `attrs` object for `data-*`/test ids on the row; a checkable `pressed` it
|
|
|
276
276
|
its icon leading and shows its check trailing, or uses the leading check slot when it has
|
|
277
277
|
no icon; selecting closes the menu unless `closeOnSelect={false}` or the item sets `keepOpen`;
|
|
278
278
|
rest props land on the trigger, `count`/`countMax` paint its corner badge and `panelStyle`
|
|
279
|
-
sizes the panel
|
|
279
|
+
sizes the panel; an item with a `control` snippet renders as a non-button row hosting a native
|
|
280
|
+
`<select>` or nested Popover trigger, keeping the icon column, row height and ↑/↓ navigation),
|
|
280
281
|
Tabs (omit `panel` for a strip-only tablist of document tabs: `closable` + `onclose(id)`
|
|
281
282
|
close through the ✕, Delete and middle-click, per-tab `leading` snippet, `title`,
|
|
282
283
|
trailing `actions` snippet; `--tab-max-width` truncates long labels),
|
|
@@ -837,7 +838,11 @@ glyphs (`size` stays the SVG px, emoji ×1.35).
|
|
|
837
838
|
On `(pointer: coarse)` every icon-only/square control (and `Popover`'s default
|
|
838
839
|
trigger) carries an invisible `::after` slab that extends its hit area to
|
|
839
840
|
`--touch-target` (44px, WCAG 2.5.8) without moving layout; fine pointers are
|
|
840
|
-
untouched.
|
|
841
|
+
untouched. The slab grows on both block sides but only towards the inline
|
|
842
|
+
*start*: inline-end overflow would count towards the scrollable overflow of
|
|
843
|
+
every scroll ancestor, so a button near the right edge of an `overflow-y: auto`
|
|
844
|
+
container would make it scroll sideways by a pixel or two.
|
|
845
|
+
`hitArea="compact"` opts a dense-table row out. `Button
|
|
841
846
|
collapseLabel="mobile" | "container"` hides `<span data-label>` children below
|
|
842
847
|
40rem viewport / 30rem container, drops to icon padding, and names the button
|
|
843
848
|
from the hidden text unless `aria-label` is set:
|
|
@@ -508,7 +508,11 @@
|
|
|
508
508
|
|
|
509
509
|
/* Coarse pointers: icon-only and square buttons carry an invisible slab that
|
|
510
510
|
extends the hit area to --touch-target without changing layout. Buttons
|
|
511
|
-
already at or above the target keep their own box (inset never positive).
|
|
511
|
+
already at or above the target keep their own box (inset never positive).
|
|
512
|
+
The slab grows towards the inline start only: inline-end overflow counts
|
|
513
|
+
towards the scrollable overflow of every scroll ancestor, so a button near
|
|
514
|
+
the right edge of an `overflow-y: auto` container would make it scroll
|
|
515
|
+
sideways. Start-side overflow is unscrollable, so the full target survives. */
|
|
512
516
|
@media (pointer: coarse) {
|
|
513
517
|
.btn-icon:not(.hit-compact),
|
|
514
518
|
.btn-icon-inline:not(.hit-compact),
|
|
@@ -524,7 +528,9 @@
|
|
|
524
528
|
.btn-chip:not(.hit-compact)::after {
|
|
525
529
|
content: '';
|
|
526
530
|
position: absolute;
|
|
527
|
-
inset: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
531
|
+
inset-block: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
532
|
+
inset-inline-start: min(0px, calc(100% - var(--touch-target)));
|
|
533
|
+
inset-inline-end: 0;
|
|
528
534
|
}
|
|
529
535
|
}
|
|
530
536
|
|
|
@@ -422,7 +422,8 @@
|
|
|
422
422
|
.panel-content {
|
|
423
423
|
flex: 1;
|
|
424
424
|
min-height: 0;
|
|
425
|
-
overflow: auto;
|
|
425
|
+
overflow-y: auto;
|
|
426
|
+
overflow-x: hidden;
|
|
426
427
|
}
|
|
427
428
|
/* Subtle chevron handle anchored to the panel's inner edge — never a filled
|
|
428
429
|
button, never overlapping the neighbouring main content. It stays visible
|
|
@@ -157,7 +157,9 @@
|
|
|
157
157
|
.file-btn.icon-only:not(.hit-compact)::after {
|
|
158
158
|
content: '';
|
|
159
159
|
position: absolute;
|
|
160
|
-
inset: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
160
|
+
inset-block: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
161
|
+
inset-inline-start: min(0px, calc(100% - var(--touch-target)));
|
|
162
|
+
inset-inline-end: 0;
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
/* Emoji glyph is bumped above the text size — at 1em a paperclip is hard to
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
export interface MenuItem {
|
|
3
3
|
label: string;
|
|
4
|
-
|
|
4
|
+
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
5
|
+
onselect?: () => void;
|
|
5
6
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
6
7
|
danger?: boolean;
|
|
7
8
|
disabled?: boolean;
|
|
@@ -18,6 +19,13 @@
|
|
|
18
19
|
keepOpen?: boolean;
|
|
19
20
|
/** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
|
|
20
21
|
content?: import('svelte').Snippet<[MenuItem]>;
|
|
22
|
+
/**
|
|
23
|
+
* A row that is itself a control (a native `<select>`, a nested Popover trigger):
|
|
24
|
+
* rendered as a non-button row with the menu's icon column and row height, its
|
|
25
|
+
* first focusable element joining ↑/↓ navigation. Replaces label/tag; `onselect`,
|
|
26
|
+
* `pressed` and `keepOpen` do not apply.
|
|
27
|
+
*/
|
|
28
|
+
control?: import('svelte').Snippet<[MenuItem, { close: () => void }]>;
|
|
21
29
|
/**
|
|
22
30
|
* Extra attributes for the rendered row — `data-*`, `title`, `aria-describedby`…
|
|
23
31
|
* The row's own semantics (`role`, `aria-checked`, `disabled`, `class`, `onclick`)
|
|
@@ -117,18 +125,27 @@
|
|
|
117
125
|
|
|
118
126
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
const ROW_TARGET = ':is(select, input, textarea, button):not(:disabled), a[href], [tabindex]:not([tabindex="-1"])';
|
|
129
|
+
|
|
130
|
+
function targets(): HTMLElement[] {
|
|
131
|
+
if (!listEl) return [];
|
|
132
|
+
const rows = listEl.querySelectorAll<HTMLElement>(
|
|
133
|
+
':scope > [role="menuitem"]:not(:disabled), :scope > [role="menuitemcheckbox"]:not(:disabled), :scope > .menu-row:not([inert])'
|
|
134
|
+
);
|
|
135
|
+
return Array.from(rows).flatMap((row) => {
|
|
136
|
+
if (!row.classList.contains('menu-row')) return [row];
|
|
137
|
+
const control = row.querySelector<HTMLElement>(ROW_TARGET);
|
|
138
|
+
return control ? [control] : [];
|
|
139
|
+
});
|
|
124
140
|
}
|
|
125
141
|
function focusAt(i: number) {
|
|
126
|
-
const b =
|
|
142
|
+
const b = targets();
|
|
127
143
|
if (b.length) b[(i + b.length) % b.length].focus();
|
|
128
144
|
}
|
|
129
145
|
function onkeydown(e: KeyboardEvent) {
|
|
130
|
-
|
|
131
|
-
const
|
|
146
|
+
if ((e.target as Element).closest('[popover]') !== listEl?.closest('[popover]')) return;
|
|
147
|
+
const b = targets();
|
|
148
|
+
const i = b.indexOf(document.activeElement as HTMLElement);
|
|
132
149
|
if (e.key === 'ArrowDown') {
|
|
133
150
|
e.preventDefault();
|
|
134
151
|
focusAt(i + 1);
|
|
@@ -146,7 +163,7 @@
|
|
|
146
163
|
function select(item: MenuItem, close: () => void) {
|
|
147
164
|
if (item.disabled) return;
|
|
148
165
|
if (menuItemCloses(item, closeOnSelect)) close();
|
|
149
|
-
item.onselect();
|
|
166
|
+
item.onselect?.();
|
|
150
167
|
}
|
|
151
168
|
</script>
|
|
152
169
|
|
|
@@ -186,6 +203,12 @@
|
|
|
186
203
|
{#snippet children({ close }: { close: () => void })}
|
|
187
204
|
<div bind:this={listEl} role="none" class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
|
|
188
205
|
{#each items as item (item.label)}
|
|
206
|
+
{#if item.control}
|
|
207
|
+
<div {...item.attrs as HTMLAttributes<HTMLDivElement>} role="none" class="menu-item menu-row" class:danger={item.danger} inert={item.disabled}>
|
|
208
|
+
{#if item.icon}<Icon name={item.icon} />{/if}
|
|
209
|
+
{@render item.control(item, { close })}
|
|
210
|
+
</div>
|
|
211
|
+
{:else}
|
|
189
212
|
<button
|
|
190
213
|
type="button"
|
|
191
214
|
{...item.attrs}
|
|
@@ -216,6 +239,7 @@
|
|
|
216
239
|
{#if glyphs.trailingCheck}<span class="menu-check menu-check-trailing"><Icon name="check" /></span>{/if}
|
|
217
240
|
{/if}
|
|
218
241
|
</button>
|
|
242
|
+
{/if}
|
|
219
243
|
{/each}
|
|
220
244
|
</div>
|
|
221
245
|
{/snippet}
|
|
@@ -241,8 +265,12 @@
|
|
|
241
265
|
font-size: var(--fs-sm);
|
|
242
266
|
white-space: nowrap;
|
|
243
267
|
}
|
|
268
|
+
.menu-row {
|
|
269
|
+
position: relative;
|
|
270
|
+
}
|
|
244
271
|
.menu-item:hover:not(:disabled),
|
|
245
|
-
.menu-item:focus-visible
|
|
272
|
+
.menu-item:focus-visible,
|
|
273
|
+
.menu-row:focus-within {
|
|
246
274
|
background: var(--bg-elevated-2);
|
|
247
275
|
outline: none;
|
|
248
276
|
}
|
|
@@ -273,7 +301,8 @@
|
|
|
273
301
|
margin-inline-start: 0;
|
|
274
302
|
padding-inline-start: 0;
|
|
275
303
|
}
|
|
276
|
-
.menu-item:disabled
|
|
304
|
+
.menu-item:disabled,
|
|
305
|
+
.menu-row[inert] {
|
|
277
306
|
opacity: 0.45;
|
|
278
307
|
cursor: not-allowed;
|
|
279
308
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface MenuItem {
|
|
2
2
|
label: string;
|
|
3
|
-
|
|
3
|
+
/** Required for button rows; a `control` row's hosted control acts on its own. */
|
|
4
|
+
onselect?: () => void;
|
|
4
5
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
5
6
|
danger?: boolean;
|
|
6
7
|
disabled?: boolean;
|
|
@@ -17,6 +18,15 @@ export interface MenuItem {
|
|
|
17
18
|
keepOpen?: boolean;
|
|
18
19
|
/** Custom row content (a rename Input, a slider…) replacing icon/label/tag. */
|
|
19
20
|
content?: import('svelte').Snippet<[MenuItem]>;
|
|
21
|
+
/**
|
|
22
|
+
* A row that is itself a control (a native `<select>`, a nested Popover trigger):
|
|
23
|
+
* rendered as a non-button row with the menu's icon column and row height, its
|
|
24
|
+
* first focusable element joining ↑/↓ navigation. Replaces label/tag; `onselect`,
|
|
25
|
+
* `pressed` and `keepOpen` do not apply.
|
|
26
|
+
*/
|
|
27
|
+
control?: import('svelte').Snippet<[MenuItem, {
|
|
28
|
+
close: () => void;
|
|
29
|
+
}]>;
|
|
20
30
|
/**
|
|
21
31
|
* Extra attributes for the rendered row — `data-*`, `title`, `aria-describedby`…
|
|
22
32
|
* The row's own semantics (`role`, `aria-checked`, `disabled`, `class`, `onclick`)
|
|
@@ -450,7 +450,9 @@
|
|
|
450
450
|
flex: none;
|
|
451
451
|
}
|
|
452
452
|
/* Coarse pointers: icon-only triggers extend their hit area to --touch-target
|
|
453
|
-
via an invisible slab; layout does not move.
|
|
453
|
+
via an invisible slab; layout does not move. The slab grows towards the
|
|
454
|
+
inline start only, so it never adds scrollable overflow to a scroll ancestor
|
|
455
|
+
(see Button.svelte). */
|
|
454
456
|
@media (pointer: coarse) {
|
|
455
457
|
.pop-trigger:not(.canonical, .hit-compact) {
|
|
456
458
|
position: relative;
|
|
@@ -458,7 +460,9 @@
|
|
|
458
460
|
.pop-trigger:not(.canonical, .hit-compact)::after {
|
|
459
461
|
content: '';
|
|
460
462
|
position: absolute;
|
|
461
|
-
inset: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
463
|
+
inset-block: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
464
|
+
inset-inline-start: min(0px, calc(100% - var(--touch-target)));
|
|
465
|
+
inset-inline-end: 0;
|
|
462
466
|
}
|
|
463
467
|
}
|
|
464
468
|
.pop-trigger.trigger-tone-accent {
|
package/dist/styles/reset.css
CHANGED
|
@@ -88,12 +88,16 @@ pre {
|
|
|
88
88
|
border-radius: var(--r-sm);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
/* Slim scrollbars for mice only. On touch, a custom scrollbar replaces the
|
|
92
|
+
native overlay bars with classic space-taking ones. */
|
|
93
|
+
@media (hover: hover) and (pointer: fine) {
|
|
94
|
+
::-webkit-scrollbar {
|
|
95
|
+
width: 8px;
|
|
96
|
+
height: 8px;
|
|
97
|
+
}
|
|
98
|
+
::-webkit-scrollbar-thumb {
|
|
99
|
+
background: var(--border-strong);
|
|
100
|
+
border-radius: var(--r-pill);
|
|
101
|
+
}
|
|
98
102
|
}
|
|
99
103
|
|
package/package.json
CHANGED