@dorsk/tsumikit 0.66.1 → 0.67.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
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
|
|
@@ -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
|
};
|
package/package.json
CHANGED