@dorsk/tsumikit 0.66.1 → 0.67.1
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 +15 -4
- package/dist/components/atoms/Button.svelte +7 -7
- package/dist/components/molecules/AttachmentList.svelte +158 -7
- package/dist/components/molecules/AttachmentList.svelte.d.ts +5 -0
- package/dist/components/molecules/FileButton.svelte +2 -2
- package/dist/components/molecules/FileButton.svelte.d.ts +1 -1
- package/dist/components/molecules/IconButton.svelte +1 -1
- package/dist/components/molecules/Popover.svelte +5 -6
- package/dist/components/molecules/Popover.svelte.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -580,10 +580,17 @@ on a phone instead of four. `tiles` forces the tile mode (`false` forces
|
|
|
580
580
|
chips); `removeLabel` names the popover button. Both variants are in the DOM
|
|
581
581
|
and the container query displays one; the hidden one is `display: none`, so
|
|
582
582
|
it is neither focusable nor announced. `Composer` inherits the behaviour.
|
|
583
|
+
Chips show a small thumbnail before an image's name and clip it in the middle
|
|
584
|
+
(the last 12 characters stay, so look-alike screenshot names remain telling).
|
|
585
|
+
`numbered` shows each file's 1-based position on the chip and the tile,
|
|
586
|
+
matching `[#N]` markers written in a prompt. `onopen(index)` makes the chip name a button and adds a
|
|
587
|
+
larger preview (images) plus an `openLabel` ("Open") action to the tile
|
|
588
|
+
popover; Remove stays separate.
|
|
583
589
|
|
|
584
590
|
```svelte
|
|
585
591
|
<AttachmentList files={attachments} onremove={(i) => (attachments = attachments.toSpliced(i, 1))} />
|
|
586
592
|
<AttachmentList tiles files={[{ name: 'cover.png', size: 1024, type: 'image/png', url }]} />
|
|
593
|
+
<AttachmentList numbered files={attachments} onopen={(i) => preview(attachments[i])} />
|
|
587
594
|
```
|
|
588
595
|
|
|
589
596
|
### ConversationPanel, ConversationFeed, StatusBar
|
|
@@ -845,10 +852,14 @@ glyphs (`size` stays the SVG px, emoji ×1.35).
|
|
|
845
852
|
On `(pointer: coarse)` every icon-only/square control (and `Popover`'s default
|
|
846
853
|
trigger) carries an invisible `::after` slab that extends its hit area to
|
|
847
854
|
`--touch-target` (44px, WCAG 2.5.8) without moving layout; fine pointers are
|
|
848
|
-
untouched. The slab
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
855
|
+
untouched. The slab is the centred target with its inline-*end* half cut off:
|
|
856
|
+
44px tall, and the box plus half the deficit wide (34px for a 24px icon).
|
|
857
|
+
Inline-end overflow would count towards the scrollable overflow of every scroll
|
|
858
|
+
ancestor, so a button near the right edge of an `overflow-y: auto` container
|
|
859
|
+
would make it scroll sideways; in a row that half lies under the next sibling's
|
|
860
|
+
slab anyway. Growing the whole deficit towards the start instead would let each
|
|
861
|
+
slab cover the previous sibling's centre, so taps would land one icon to the
|
|
862
|
+
right.
|
|
852
863
|
`hitArea="compact"` opts a dense-table row out. `Button
|
|
853
864
|
collapseLabel="mobile" | "container"` hides `<span data-label>` children below
|
|
854
865
|
40rem viewport / 30rem container, drops to icon padding, and names the button
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
// container (`container`) so the button becomes icon-only; the hidden text
|
|
52
52
|
// becomes the aria-label unless one is set.
|
|
53
53
|
collapseLabel?: 'never' | 'mobile' | 'container';
|
|
54
|
-
// Icon-only/square buttons grow a
|
|
54
|
+
// Icon-only/square buttons grow a touch hit slab on coarse pointers without
|
|
55
55
|
// moving layout; `compact` opts out in dense tables.
|
|
56
56
|
hitArea?: 'auto' | 'compact';
|
|
57
57
|
// Async/busy state: shows a spinner, blocks clicks, sets aria-busy. Stays
|
|
@@ -507,12 +507,12 @@
|
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
/* Coarse pointers: icon-only and square buttons carry an invisible slab that
|
|
510
|
-
extends the hit area
|
|
510
|
+
extends the hit area towards --touch-target without changing layout. Buttons
|
|
511
511
|
already at or above the target keep their own box (inset never positive).
|
|
512
|
-
The slab
|
|
513
|
-
towards the scrollable overflow of every scroll ancestor,
|
|
514
|
-
|
|
515
|
-
|
|
512
|
+
The slab is the centred target with its inline-end half cut off: inline-end
|
|
513
|
+
overflow counts towards the scrollable overflow of every scroll ancestor, and
|
|
514
|
+
in a row that half lies under the next sibling anyway. Growing further towards
|
|
515
|
+
the start would let each slab cover the previous sibling's centre. */
|
|
516
516
|
@media (pointer: coarse) {
|
|
517
517
|
.btn-icon:not(.hit-compact),
|
|
518
518
|
.btn-icon-inline:not(.hit-compact),
|
|
@@ -529,7 +529,7 @@
|
|
|
529
529
|
content: '';
|
|
530
530
|
position: absolute;
|
|
531
531
|
inset-block: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
532
|
-
inset-inline-start: min(0px, calc(100% - var(--touch-target)));
|
|
532
|
+
inset-inline-start: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
533
533
|
inset-inline-end: 0;
|
|
534
534
|
}
|
|
535
535
|
}
|
|
@@ -15,16 +15,24 @@
|
|
|
15
15
|
let {
|
|
16
16
|
files,
|
|
17
17
|
onremove,
|
|
18
|
+
onopen,
|
|
18
19
|
tiles = 'auto',
|
|
20
|
+
numbered = false,
|
|
19
21
|
removeLabel = 'Remove',
|
|
22
|
+
openLabel = 'Open',
|
|
20
23
|
class: klass = '',
|
|
21
24
|
style: styleProp = ''
|
|
22
25
|
}: {
|
|
23
26
|
files: (File | Attachment)[];
|
|
24
27
|
onremove?: (index: number) => void;
|
|
28
|
+
/** Makes the chip name a button and adds a preview + Open action to the tile popover. */
|
|
29
|
+
onopen?: (index: number) => void;
|
|
25
30
|
/** `auto` switches to tiles when the list is at most 30rem wide. */
|
|
26
31
|
tiles?: boolean | 'auto';
|
|
32
|
+
/** Shows each file's 1-based position, matching `[#N]` markers in a prompt. */
|
|
33
|
+
numbered?: boolean;
|
|
27
34
|
removeLabel?: string;
|
|
35
|
+
openLabel?: string;
|
|
28
36
|
class?: string;
|
|
29
37
|
style?: string;
|
|
30
38
|
} = $props();
|
|
@@ -57,6 +65,16 @@
|
|
|
57
65
|
for (const url of untrack(() => objectUrls).values()) URL.revokeObjectURL(url);
|
|
58
66
|
});
|
|
59
67
|
|
|
68
|
+
const TAIL = 12;
|
|
69
|
+
|
|
70
|
+
/** Chips clip the head and keep the tail, where look-alike names (macOS
|
|
71
|
+
* screenshots) differ. */
|
|
72
|
+
function split(name: string): [string, string] {
|
|
73
|
+
const chars = [...name];
|
|
74
|
+
if (chars.length <= TAIL + 4) return [name, ''];
|
|
75
|
+
return [chars.slice(0, -TAIL).join(''), chars.slice(-TAIL).join('')];
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
function thumb(f: File | Attachment): string | undefined {
|
|
61
79
|
if (!isImage(f)) return undefined;
|
|
62
80
|
return f instanceof File ? objectUrls.get(f) : f.url;
|
|
@@ -72,6 +90,11 @@
|
|
|
72
90
|
style={styleProp}
|
|
73
91
|
>
|
|
74
92
|
{#each files as f, i (`${f.name}-${i}`)}
|
|
93
|
+
{#snippet label(f: File | Attachment)}
|
|
94
|
+
{@const [head, tail] = split(f.name)}
|
|
95
|
+
<span class="head">{head}</span>{#if tail}<span class="tail">{tail}</span>{/if}
|
|
96
|
+
{#if f.size !== undefined}<span class="size">{fmt(f.size)}</span>{/if}
|
|
97
|
+
{/snippet}
|
|
75
98
|
{#if tiles !== true}
|
|
76
99
|
<li class="chip">
|
|
77
100
|
<Badge
|
|
@@ -82,20 +105,32 @@
|
|
|
82
105
|
onremove={() => onremove?.(i)}
|
|
83
106
|
title={f.name}
|
|
84
107
|
>
|
|
85
|
-
{
|
|
108
|
+
{@const src = thumb(f)}
|
|
109
|
+
{#if numbered}<span class="num">{i + 1}</span>{/if}
|
|
110
|
+
{#if src}<img class="mini" {src} alt="" />{/if}
|
|
111
|
+
{#if onopen}
|
|
112
|
+
<button type="button" class="open" title={openLabel} onclick={() => onopen?.(i)}>
|
|
113
|
+
{@render label(f)}
|
|
114
|
+
</button>
|
|
115
|
+
{:else}
|
|
116
|
+
<span class="name">{@render label(f)}</span>
|
|
117
|
+
{/if}
|
|
86
118
|
</Badge>
|
|
87
119
|
</li>
|
|
88
120
|
{/if}
|
|
89
121
|
{#if tiles !== false}
|
|
90
122
|
<li class="tile">
|
|
91
|
-
<Popover label={f.name} box="lg" placement="top-start" style="--pop-trigger-pad: 0; --pop-trigger-border: var(--border-strong); --pop-trigger-bg: var(--bg)">
|
|
123
|
+
<Popover label={numbered ? `${i + 1}. ${f.name}` : f.name} box="lg" placement="top-start" style="--pop-trigger-pad: 0; --pop-trigger-border: var(--border-strong); --pop-trigger-bg: var(--bg)">
|
|
92
124
|
{#snippet trigger()}
|
|
93
125
|
{@const src = thumb(f)}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
<span class="face">
|
|
127
|
+
{#if src}
|
|
128
|
+
<img class="thumb" {src} alt="" />
|
|
129
|
+
{:else}
|
|
130
|
+
<Icon name={isImage(f) ? 'image' : 'file'} size={18} />
|
|
131
|
+
{/if}
|
|
132
|
+
{#if numbered}<span class="num tile-num">{i + 1}</span>{/if}
|
|
133
|
+
</span>
|
|
99
134
|
{/snippet}
|
|
100
135
|
{#snippet children({ close })}
|
|
101
136
|
<div class="detail">
|
|
@@ -118,6 +153,31 @@
|
|
|
118
153
|
{/if}
|
|
119
154
|
</div>
|
|
120
155
|
{#if f.size !== undefined}<Text size="xs" tone="faint">{fmt(f.size)}</Text>{/if}
|
|
156
|
+
{#if onopen}
|
|
157
|
+
{@const src = thumb(f)}
|
|
158
|
+
{#if src}
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class="preview"
|
|
162
|
+
title={openLabel}
|
|
163
|
+
onclick={() => {
|
|
164
|
+
close();
|
|
165
|
+
onopen?.(i);
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<img {src} alt="" />
|
|
169
|
+
</button>
|
|
170
|
+
{/if}
|
|
171
|
+
<Button
|
|
172
|
+
size="sm"
|
|
173
|
+
onclick={() => {
|
|
174
|
+
close();
|
|
175
|
+
onopen?.(i);
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{openLabel}
|
|
179
|
+
</Button>
|
|
180
|
+
{/if}
|
|
121
181
|
</div>
|
|
122
182
|
{/snippet}
|
|
123
183
|
</Popover>
|
|
@@ -188,4 +248,95 @@
|
|
|
188
248
|
.chip :global(.action) {
|
|
189
249
|
font-size: 1.4em;
|
|
190
250
|
}
|
|
251
|
+
.chip :global(.clip) {
|
|
252
|
+
display: inline-flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: var(--sp-1);
|
|
255
|
+
}
|
|
256
|
+
.name,
|
|
257
|
+
.open {
|
|
258
|
+
display: inline-flex;
|
|
259
|
+
align-items: baseline;
|
|
260
|
+
min-width: 0;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
}
|
|
263
|
+
.head {
|
|
264
|
+
min-width: 0;
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
text-overflow: ellipsis;
|
|
267
|
+
}
|
|
268
|
+
.tail,
|
|
269
|
+
.size {
|
|
270
|
+
flex: none;
|
|
271
|
+
}
|
|
272
|
+
.open {
|
|
273
|
+
padding: 0;
|
|
274
|
+
border: 0;
|
|
275
|
+
background: none;
|
|
276
|
+
font: inherit;
|
|
277
|
+
color: inherit;
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
}
|
|
280
|
+
.open:hover {
|
|
281
|
+
text-decoration: underline;
|
|
282
|
+
}
|
|
283
|
+
.open:focus-visible {
|
|
284
|
+
outline: var(--focus-ring);
|
|
285
|
+
outline-offset: var(--focus-ring-offset);
|
|
286
|
+
border-radius: var(--r-sm);
|
|
287
|
+
}
|
|
288
|
+
.mini {
|
|
289
|
+
flex: none;
|
|
290
|
+
width: 1.25em;
|
|
291
|
+
height: 1.25em;
|
|
292
|
+
object-fit: cover;
|
|
293
|
+
border-radius: var(--r-sm);
|
|
294
|
+
}
|
|
295
|
+
.num {
|
|
296
|
+
flex: none;
|
|
297
|
+
font-size: 0.8em;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
line-height: 1;
|
|
300
|
+
color: var(--text-muted);
|
|
301
|
+
}
|
|
302
|
+
.face {
|
|
303
|
+
position: relative;
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
justify-content: center;
|
|
307
|
+
width: 100%;
|
|
308
|
+
height: 100%;
|
|
309
|
+
}
|
|
310
|
+
.tile-num {
|
|
311
|
+
position: absolute;
|
|
312
|
+
top: -0.35em;
|
|
313
|
+
right: -0.35em;
|
|
314
|
+
min-width: 1.4em;
|
|
315
|
+
padding: 0.1em 0.3em;
|
|
316
|
+
border: 1px solid var(--border-strong);
|
|
317
|
+
border-radius: var(--r-pill);
|
|
318
|
+
background: var(--bg);
|
|
319
|
+
color: var(--text);
|
|
320
|
+
text-align: center;
|
|
321
|
+
}
|
|
322
|
+
.preview {
|
|
323
|
+
display: block;
|
|
324
|
+
padding: 0;
|
|
325
|
+
border: 1px solid var(--border);
|
|
326
|
+
border-radius: var(--r-md);
|
|
327
|
+
background: none;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
line-height: 0;
|
|
331
|
+
}
|
|
332
|
+
.preview:focus-visible {
|
|
333
|
+
outline: var(--focus-ring);
|
|
334
|
+
outline-offset: var(--focus-ring-offset);
|
|
335
|
+
}
|
|
336
|
+
.preview img {
|
|
337
|
+
display: block;
|
|
338
|
+
max-width: 16rem;
|
|
339
|
+
max-height: 16rem;
|
|
340
|
+
object-fit: contain;
|
|
341
|
+
}
|
|
191
342
|
</style>
|
|
@@ -7,9 +7,14 @@ export type Attachment = {
|
|
|
7
7
|
type $$ComponentProps = {
|
|
8
8
|
files: (File | Attachment)[];
|
|
9
9
|
onremove?: (index: number) => void;
|
|
10
|
+
/** Makes the chip name a button and adds a preview + Open action to the tile popover. */
|
|
11
|
+
onopen?: (index: number) => void;
|
|
10
12
|
/** `auto` switches to tiles when the list is at most 30rem wide. */
|
|
11
13
|
tiles?: boolean | 'auto';
|
|
14
|
+
/** Shows each file's 1-based position, matching `[#N]` markers in a prompt. */
|
|
15
|
+
numbered?: boolean;
|
|
12
16
|
removeLabel?: string;
|
|
17
|
+
openLabel?: string;
|
|
13
18
|
class?: string;
|
|
14
19
|
style?: string;
|
|
15
20
|
};
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
control?: boolean;
|
|
46
46
|
/** Shared square box scale (`--box-xs/sm/md/lg`); implies `iconOnly`. */
|
|
47
47
|
box?: 'xs' | 'sm' | 'md' | 'lg';
|
|
48
|
-
/** Icon-only buttons grow a
|
|
48
|
+
/** Icon-only buttons grow a touch hit slab on coarse pointers; `compact` opts out. */
|
|
49
49
|
hitArea?: 'auto' | 'compact';
|
|
50
50
|
class?: string;
|
|
51
51
|
} = $props();
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
content: '';
|
|
159
159
|
position: absolute;
|
|
160
160
|
inset-block: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
161
|
-
inset-inline-start: min(0px, calc(100% - var(--touch-target)));
|
|
161
|
+
inset-inline-start: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
162
162
|
inset-inline-end: 0;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
@@ -24,7 +24,7 @@ type $$ComponentProps = {
|
|
|
24
24
|
control?: boolean;
|
|
25
25
|
/** Shared square box scale (`--box-xs/sm/md/lg`); implies `iconOnly`. */
|
|
26
26
|
box?: 'xs' | 'sm' | 'md' | 'lg';
|
|
27
|
-
/** Icon-only buttons grow a
|
|
27
|
+
/** Icon-only buttons grow a touch hit slab on coarse pointers; `compact` opts out. */
|
|
28
28
|
hitArea?: 'auto' | 'compact';
|
|
29
29
|
class?: string;
|
|
30
30
|
};
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// Exact glyph size for SVG *and* text glyphs (px number or any CSS length),
|
|
35
35
|
// no emoji multiplier. Wins over `size`.
|
|
36
36
|
glyphSize?: number | string;
|
|
37
|
-
// Opt out of the
|
|
37
|
+
// Opt out of the coarse-pointer hit slab in dense rows.
|
|
38
38
|
hitArea?: 'auto' | 'compact';
|
|
39
39
|
// Borderless, compact icon affordance (chip-remove ✕, inline edit ✎) —
|
|
40
40
|
// no square box; just a muted glyph that brightens on hover. Pair with
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
/** Use the shared `--control-height` toolbar/composer contract. */
|
|
62
62
|
control?: boolean;
|
|
63
63
|
block?: boolean;
|
|
64
|
-
/** Icon-only triggers grow a
|
|
64
|
+
/** Icon-only triggers grow a touch hit slab on coarse pointers; `compact` opts out. */
|
|
65
65
|
hitArea?: 'auto' | 'compact';
|
|
66
66
|
/** Corner count indicator on the trigger, as on `Button`: hidden at
|
|
67
67
|
* 0/undefined, capped at `countMax+`, the exact number joins `label`. */
|
|
@@ -449,10 +449,9 @@
|
|
|
449
449
|
padding: var(--pop-trigger-pad, 0);
|
|
450
450
|
flex: none;
|
|
451
451
|
}
|
|
452
|
-
/* Coarse pointers: icon-only triggers extend their hit area
|
|
453
|
-
via an invisible slab; layout does not move.
|
|
454
|
-
inline
|
|
455
|
-
(see Button.svelte). */
|
|
452
|
+
/* Coarse pointers: icon-only triggers extend their hit area towards --touch-target
|
|
453
|
+
via an invisible slab; layout does not move. Same geometry as Button.svelte:
|
|
454
|
+
no inline-end growth, half the deficit towards the inline start. */
|
|
456
455
|
@media (pointer: coarse) {
|
|
457
456
|
.pop-trigger:not(.canonical, .hit-compact) {
|
|
458
457
|
position: relative;
|
|
@@ -461,7 +460,7 @@
|
|
|
461
460
|
content: '';
|
|
462
461
|
position: absolute;
|
|
463
462
|
inset-block: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
464
|
-
inset-inline-start: min(0px, calc(100% - var(--touch-target)));
|
|
463
|
+
inset-inline-start: min(0px, calc((100% - var(--touch-target)) / 2));
|
|
465
464
|
inset-inline-end: 0;
|
|
466
465
|
}
|
|
467
466
|
}
|
|
@@ -46,7 +46,7 @@ type Own = {
|
|
|
46
46
|
/** Use the shared `--control-height` toolbar/composer contract. */
|
|
47
47
|
control?: boolean;
|
|
48
48
|
block?: boolean;
|
|
49
|
-
/** Icon-only triggers grow a
|
|
49
|
+
/** Icon-only triggers grow a touch hit slab on coarse pointers; `compact` opts out. */
|
|
50
50
|
hitArea?: 'auto' | 'compact';
|
|
51
51
|
/** Corner count indicator on the trigger, as on `Button`: hidden at
|
|
52
52
|
* 0/undefined, capped at `countMax+`, the exact number joins `label`. */
|
package/package.json
CHANGED