@dorsk/tsumikit 0.80.2 → 0.81.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.
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- // Cover / thumbnail tile: a lazy <img> that swaps to a seeded gradient with
4
- // initials (or an icon) when there is no source or it fails to load. Width
5
- // comes from `size`, height from `aspect`; `status` overlays a Dot/Badge in
6
- // the bottom-right corner.
3
+ // Cover / thumbnail tile: a seeded gradient with initials (or an icon) that a
4
+ // lazy <img> fades in over once it loads; the gradient stays when there is no
5
+ // source or it fails. Width comes from `size`, height from `aspect`; `status`
6
+ // overlays a Dot/Badge in the bottom-right corner.
7
7
  import { untrack, type Snippet } from 'svelte';
8
8
  import { artworkGradient, initials as initialsOf } from '../../artwork';
9
9
  import Icon from './Icon.svelte';
@@ -53,11 +53,13 @@
53
53
 
54
54
  const source = $derived(`${src ?? ''} ${srcset ?? ''}`);
55
55
  let failedSource = $state<string | null>(null);
56
+ let loadedSource = $state<string | null>(null);
56
57
 
57
58
  const key = $derived(seed ?? alt);
58
59
  const showImage = $derived(Boolean(src) && failedSource !== source);
60
+ const loaded = $derived(showImage && loadedSource === source);
59
61
  const tile = $derived(showImage ? undefined : alt.trim() ? 'named' : 'decorative');
60
- const gradient = $derived(showImage ? undefined : artworkGradient(key));
62
+ const gradient = $derived(artworkGradient(key));
61
63
  const text = $derived(fallback === 'initials' ? initialsOf(key) : '');
62
64
 
63
65
  function fail() {
@@ -66,11 +68,20 @@
66
68
  }
67
69
 
68
70
  function watchLoad(img: HTMLImageElement) {
71
+ const load = () => {
72
+ loadedSource = source;
73
+ };
69
74
  img.addEventListener('error', fail);
75
+ img.addEventListener('load', load);
70
76
  untrack(() => {
71
- if (img.complete && img.naturalWidth === 0) fail();
77
+ if (!img.complete) return;
78
+ if (img.naturalWidth === 0) fail();
79
+ else load();
72
80
  });
73
- return () => img.removeEventListener('error', fail);
81
+ return () => {
82
+ img.removeEventListener('error', fail);
83
+ img.removeEventListener('load', load);
84
+ };
74
85
  }
75
86
  </script>
76
87
 
@@ -86,13 +97,24 @@
86
97
  aria-hidden={tile === 'decorative' ? 'true' : undefined}
87
98
  {...rest}
88
99
  >
89
- {#if showImage}
90
- <img {src} {srcset} {sizes} {alt} class="img fit-{fit}" loading="lazy" decoding="async" {@attach watchLoad} />
91
- {:else if fallback === 'initials' && text}
100
+ {#if fallback === 'initials' && text}
92
101
  <span class="initials" aria-hidden="true">{text}</span>
93
102
  {:else if fallback === 'icon'}
94
103
  <span class="glyph" aria-hidden="true"><Icon name={icon} /></span>
95
104
  {/if}
105
+ {#if showImage}
106
+ <img
107
+ {src}
108
+ {srcset}
109
+ {sizes}
110
+ {alt}
111
+ class="img fit-{fit}"
112
+ class:loaded
113
+ loading="lazy"
114
+ decoding="async"
115
+ {@attach watchLoad}
116
+ />
117
+ {/if}
96
118
  {#if status}
97
119
  <span class="status">{@render status()}</span>
98
120
  {/if}
@@ -145,6 +167,11 @@
145
167
  width: 100%;
146
168
  height: 100%;
147
169
  display: block;
170
+ opacity: 0;
171
+ transition: opacity var(--dur-release) var(--ease);
172
+ }
173
+ .img.loaded {
174
+ opacity: 1;
148
175
  }
149
176
  .fit-cover {
150
177
  object-fit: cover;
@@ -230,7 +230,7 @@
230
230
  border-color: transparent;
231
231
  }
232
232
  .mono {
233
- font-family: var(--font-mono);
233
+ font-family: var(--font-numeric);
234
234
  font-weight: var(--badge-fw, var(--fw-normal));
235
235
  }
236
236
  .uppercase {
@@ -687,7 +687,7 @@
687
687
  position: absolute;
688
688
  top: var(--sp-0-5);
689
689
  left: var(--sp-1);
690
- font-family: var(--font-mono);
690
+ font-family: var(--font-label);
691
691
  font-size: calc(var(--fs-xs) * 0.75);
692
692
  font-weight: var(--fw-medium);
693
693
  color: var(--text-faint);
@@ -259,7 +259,7 @@
259
259
  opacity: 1;
260
260
  }
261
261
  .mono {
262
- font-family: var(--font-mono);
262
+ font-family: var(--font-numeric);
263
263
  }
264
264
 
265
265
  .input-wrap {
@@ -53,7 +53,7 @@
53
53
  padding: 0 var(--sp-3);
54
54
  margin-bottom: var(--sp-1);
55
55
  color: var(--text-faint);
56
- font-family: var(--font-mono);
56
+ font-family: var(--font-label);
57
57
  font-size: var(--fs-xs);
58
58
  font-weight: var(--fw-semibold);
59
59
  text-transform: uppercase;
@@ -239,13 +239,18 @@
239
239
  const pos = el.selectionStart ?? value.length;
240
240
  const id = ++reqId;
241
241
  const next = key ? await suggestSingle(schema, key, value) : await suggest(schema, value, pos);
242
- if (id !== reqId) return; // a newer keystroke won
242
+ if (id !== reqId || document.activeElement !== el) return;
243
243
  menu = next;
244
244
  active = 0;
245
245
  keyboardNavigation = false;
246
246
  open = !!next && next.items.length > 0;
247
247
  }
248
248
 
249
+ function close() {
250
+ ++reqId;
251
+ open = false;
252
+ }
253
+
249
254
  function accept(i: number) {
250
255
  if (!menu || !el) return;
251
256
  const s = menu.items[i];
@@ -269,7 +274,7 @@
269
274
  el.focus();
270
275
  el.setSelectionRange(caret, caret);
271
276
  if (s.advance) refresh();
272
- else open = false;
277
+ else close();
273
278
  });
274
279
  }
275
280
 
@@ -282,7 +287,7 @@
282
287
  const exit = closingQuoteExit(value, pos);
283
288
  if (exit !== null) {
284
289
  e.preventDefault();
285
- open = false;
290
+ close();
286
291
  setCaret(exit);
287
292
  return;
288
293
  }
@@ -299,6 +304,7 @@
299
304
  }
300
305
  if (!open || !menu) {
301
306
  if (e.key === 'Enter') submit();
307
+ else if (e.key === 'Escape') close();
302
308
  return;
303
309
  }
304
310
  switch (e.key) {
@@ -319,13 +325,13 @@
319
325
  break;
320
326
  case 'Escape':
321
327
  e.preventDefault();
322
- open = false;
328
+ close();
323
329
  break;
324
330
  }
325
331
  }
326
332
 
327
333
  function submit() {
328
- open = false;
334
+ close();
329
335
  onsubmit?.(value);
330
336
  }
331
337
 
@@ -399,7 +405,9 @@
399
405
  }}
400
406
  onblur={() => {
401
407
  focused = false;
402
- setTimeout(() => (open = false), 120);
408
+ setTimeout(() => {
409
+ if (document.activeElement !== el) close();
410
+ }, 120);
403
411
  }}
404
412
  {onkeydown}
405
413
  />
@@ -488,7 +496,7 @@
488
496
  font-size: var(--control-fs-sm);
489
497
  }
490
498
  .fi--pill .fi__bar {
491
- border-radius: var(--r-pill);
499
+ border-radius: var(--r-control);
492
500
  padding-inline: var(--sp-4);
493
501
  }
494
502
  .surface-raised .fi__bar {
@@ -540,7 +548,7 @@
540
548
  border: 0;
541
549
  background: transparent;
542
550
  color: var(--text);
543
- font-family: var(--font-mono);
551
+ font-family: var(--font-numeric);
544
552
  font-size: var(--control-fs-md);
545
553
  outline: none;
546
554
  min-width: 0;
@@ -656,7 +664,7 @@
656
664
  }
657
665
  .fi__opt-code {
658
666
  margin-left: auto;
659
- font-family: var(--font-mono);
667
+ font-family: var(--font-numeric);
660
668
  font-size: var(--fs-xs);
661
669
  color: var(--text-muted);
662
670
  background: var(--bg-elevated-2);
@@ -187,7 +187,7 @@
187
187
  text-transform: uppercase;
188
188
  letter-spacing: 0.06em;
189
189
  color: var(--text-muted);
190
- font-family: var(--font-mono);
190
+ font-family: var(--font-label);
191
191
  line-height: var(--lh-snug);
192
192
  }
193
193
  .metric-chip {
@@ -207,7 +207,7 @@
207
207
  display: flex;
208
208
  align-items: baseline;
209
209
  gap: var(--sp-1);
210
- font-family: var(--font-mono);
210
+ font-family: var(--font-numeric);
211
211
  line-height: var(--lh-none);
212
212
  }
213
213
  .metric-num {
@@ -274,7 +274,7 @@
274
274
  flex-wrap: wrap;
275
275
  align-items: baseline;
276
276
  gap: var(--sp-1) var(--sp-3);
277
- font-family: var(--font-mono);
277
+ font-family: var(--font-numeric);
278
278
  font-size: var(--fs-xs);
279
279
  color: var(--text-muted);
280
280
  }
@@ -249,7 +249,7 @@
249
249
  padding: var(--sp-0-5);
250
250
  background: var(--track-fill);
251
251
  border: var(--edge-width) solid var(--track-edge);
252
- border-radius: var(--r-pill);
252
+ border-radius: var(--r-control);
253
253
  max-width: 100%;
254
254
  min-width: 0;
255
255
  }
@@ -325,7 +325,7 @@
325
325
  left: 0;
326
326
  width: 0;
327
327
  height: 0;
328
- border-radius: var(--r-pill);
328
+ border-radius: var(--r-control);
329
329
  background: var(--thumb-fill);
330
330
  box-shadow: var(--thumb-shadow);
331
331
  transform-origin: top left;
@@ -346,7 +346,7 @@
346
346
  height: 100%;
347
347
  padding: 0 var(--control-px-md);
348
348
  border: 0;
349
- border-radius: var(--r-pill);
349
+ border-radius: var(--r-control);
350
350
  background: transparent;
351
351
  color: var(--text-muted);
352
352
  font-size: var(--control-fs-md);
@@ -370,7 +370,7 @@
370
370
  }
371
371
  /* Capped at half a row: one row stays a pill, extra rows square the corners. */
372
372
  .seg-wrap:not(.seg-icon) {
373
- border-radius: calc(var(--seg-h) / 2);
373
+ border-radius: min(var(--r-control), calc(var(--seg-h) / 2));
374
374
  }
375
375
  /* Icon-only segments are squares; corners stay concentric with the track. */
376
376
  .seg-icon .seg-item,
@@ -113,6 +113,10 @@
113
113
  --font-mono:
114
114
  ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, Consolas,
115
115
  monospace;
116
+ /* Non-code UI text: counts and stat values (--font-numeric), eyebrow labels
117
+ and shortcut hints (--font-label). Code-ish slots keep --font-mono. */
118
+ --font-numeric: var(--font-mono);
119
+ --font-label: var(--font-mono);
116
120
 
117
121
  /* Text-scale factor (CCT-305). The UI font control multiplies ONLY the
118
122
  font-size tokens by this — not the root font-size — so bumping the size up
@@ -171,6 +175,8 @@
171
175
  --r-md: 10px;
172
176
  --r-lg: 14px;
173
177
  --r-pill: 999px;
178
+ /* Segmented tracks and pill-shaped filter bars; squarish apps set --r-md. */
179
+ --r-control: var(--r-pill);
174
180
 
175
181
  /* ---- Elevation ---- */
176
182
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.80.2",
3
+ "version": "0.81.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",