@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
- {f.name}{#if f.size !== undefined}<span class="size">{fmt(f.size)}</span>{/if}
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
- {#if src}
95
- <img class="thumb" {src} alt="" />
96
- {:else}
97
- <Icon name={isImage(f) ? 'image' : 'file'} size={18} />
98
- {/if}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.66.1",
3
+ "version": "0.67.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",