@dorsk/tsumikit 0.2.3 → 0.2.5

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,40 +1,91 @@
1
1
  <script lang="ts" module>
2
- // Open icon system. A curated, dependency-free set ships as named glyphs, but
3
- // the atom is NOT closed: pass a `children` snippet of raw <path>/<circle>/…
4
- // (24×24 viewBox) to render any icon without adding it to the registry, or
5
- // register project icons by extending PATHS in your own wrapper. Sized at 1em
6
- // so it tracks the surrounding text (and the --fs-scale font control).
7
- export type IconName =
8
- | 'archive'
9
- | 'back'
10
- | 'check'
11
- | 'chevron-down'
12
- | 'copy'
13
- | 'download'
14
- | 'edit'
15
- | 'external'
16
- | 'filter'
17
- | 'folder'
18
- | 'fork'
19
- | 'image'
20
- | 'info'
21
- | 'link'
22
- | 'live'
23
- | 'markdown'
24
- | 'menu'
25
- | 'more'
26
- | 'plus'
27
- | 'retry'
28
- | 'search'
29
- | 'send'
30
- | 'settings'
31
- | 'star'
32
- | 'stop'
33
- | 'tag'
34
- | 'trash'
35
- | 'upload'
36
- | 'warning'
37
- | 'x';
2
+ // Open icon system. A curated, dependency-free set ships as named glyphs
3
+ // (lucide path data, 24×24 viewBox, 2px stroke), but the atom is NOT closed:
4
+ // pass a `children` snippet of raw <path>/<circle>/… to render any icon
5
+ // e.g. a lucide-svelte component's contents without adding it to the
6
+ // registry. Sized at 1em so it tracks the surrounding text (and the
7
+ // --fs-scale font control).
8
+ const ICONS = {
9
+ // — navigation / chevrons —
10
+ back: '<path d="m12 19-7-7 7-7" /><path d="M19 12H5" />',
11
+ 'arrow-right': '<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />',
12
+ 'arrow-up': '<path d="m5 12 7-7 7 7" /><path d="M12 19V5" />',
13
+ 'arrow-down': '<path d="M12 5v14" /><path d="m19 12-7 7-7-7" />',
14
+ 'chevron-up': '<path d="m18 15-6-6-6 6" />',
15
+ 'chevron-down': '<path d="m6 9 6 6 6-6" />',
16
+ 'chevron-left': '<path d="m15 18-6-6 6-6" />',
17
+ 'chevron-right': '<path d="m9 18 6-6-6-6" />',
18
+ menu: '<path d="M4 5h16" /><path d="M4 12h16" /><path d="M4 19h16" />',
19
+ more: '<circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" />',
20
+ 'external': '<path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />',
21
+ 'log-out': '<path d="m16 17 5-5-5-5" /><path d="M21 12H9" /><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />',
22
+
23
+ // — actions —
24
+ plus: '<path d="M5 12h14" /><path d="M12 5v14" />',
25
+ minus: '<path d="M5 12h14" />',
26
+ check: '<path d="M20 6 9 17l-5-5" />',
27
+ x: '<path d="M18 6 6 18" /><path d="m6 6 12 12" />',
28
+ search: '<path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" />',
29
+ filter: '<path d="M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z" />',
30
+ copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />',
31
+ edit: '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /><path d="m15 5 4 4" />',
32
+ trash: '<path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />',
33
+ save: '<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" /><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" /><path d="M7 3v4a1 1 0 0 0 1 1h7" />',
34
+ download: '<path d="M12 15V3" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m7 10 5 5 5-5" />',
35
+ upload: '<path d="M12 3v12" /><path d="m17 8-5-5-5 5" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />',
36
+ send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" /><path d="m21.854 2.147-10.94 10.939" />',
37
+ share: '<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" />',
38
+ retry: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" />',
39
+ settings: '<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" />',
40
+
41
+ // — media playback —
42
+ play: '<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" />',
43
+ pause: '<rect x="14" y="3" width="5" height="18" rx="1" /><rect x="5" y="3" width="5" height="18" rx="1" />',
44
+ stop: '<rect width="18" height="18" x="3" y="3" rx="2" />',
45
+
46
+ // — files / content —
47
+ file: '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" />',
48
+ 'file-text': '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" />',
49
+ folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />',
50
+ archive: '<rect width="20" height="5" x="2" y="3" rx="1" /><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" /><path d="M10 12h4" />',
51
+ image: '<rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />',
52
+ markdown: '<rect x="3" y="5" width="18" height="14" rx="2" /><path d="M7 15V9l3 3 3-3v6" /><path d="m15 11 2 2 2-2" />',
53
+ list: '<path d="M3 5h.01" /><path d="M3 12h.01" /><path d="M3 19h.01" /><path d="M8 5h13" /><path d="M8 12h13" /><path d="M8 19h13" />',
54
+ grid: '<rect width="7" height="7" x="3" y="3" rx="1" /><rect width="7" height="7" x="14" y="3" rx="1" /><rect width="7" height="7" x="14" y="14" rx="1" /><rect width="7" height="7" x="3" y="14" rx="1" />',
55
+
56
+ // — objects / status —
57
+ link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />',
58
+ tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" /><circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />',
59
+ bookmark: '<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />',
60
+ star: '<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />',
61
+ heart: '<path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" />',
62
+ fork: '<circle cx="12" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><circle cx="18" cy="6" r="3" /><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9" /><path d="M12 12v3" />',
63
+ live: '<circle cx="12" cy="12" r="5" />',
64
+ eye: '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" /><circle cx="12" cy="12" r="3" />',
65
+ 'eye-off': '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" /><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" /><path d="m2 2 20 20" />',
66
+ lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />',
67
+ unlock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 9.9-1" />',
68
+ bell: '<path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />',
69
+ mail: '<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" /><rect x="2" y="4" width="20" height="16" rx="2" />',
70
+ calendar: '<path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" />',
71
+ clock: '<circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />',
72
+ home: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" /><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />',
73
+ user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />',
74
+ users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" />',
75
+ sun: '<circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />',
76
+ moon: '<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />',
77
+ loader: '<path d="M21 12a9 9 0 1 1-6.219-8.56" />',
78
+
79
+ // — circled status —
80
+ info: '<circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" />',
81
+ warning: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /><path d="M12 9v4" /><path d="M12 17h.01" />',
82
+ help: '<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" />',
83
+ 'check-circle': '<circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" />',
84
+ 'x-circle': '<circle cx="12" cy="12" r="10" /><path d="m15 9-6 6" /><path d="m9 9 6 6" />',
85
+ 'alert-circle': '<circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" />',
86
+ } as const;
87
+
88
+ export type IconName = keyof typeof ICONS;
38
89
 
39
90
  // Glyphs that read better filled than stroked.
40
91
  const FILLED = new Set<IconName>(['stop', 'star', 'live']);
@@ -58,7 +109,8 @@
58
109
  /** When set, the icon is exposed to AT with this label; otherwise it is
59
110
  * decorative (aria-hidden) and the parent control carries the label. */
60
111
  label?: string;
61
- /** Raw SVG markup (24×24 viewBox) — overrides `name`. */
112
+ /** Raw SVG markup (24×24 viewBox) — overrides `name`. Pass a lucide-svelte
113
+ * component's contents here to render any icon not in the registry. */
62
114
  children?: Snippet;
63
115
  [key: string]: unknown;
64
116
  } = $props();
@@ -82,66 +134,9 @@
82
134
  >
83
135
  {#if children}
84
136
  {@render children()}
85
- {:else if name === 'search'}
86
- <circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" />
87
- {:else if name === 'back'}
88
- <path d="m15 18-6-6 6-6" />
89
- {:else if name === 'check'}
90
- <path d="M20 6 9 17l-5-5" />
91
- {:else if name === 'x'}
92
- <path d="M18 6 6 18" /><path d="m6 6 12 12" />
93
- {:else if name === 'plus'}
94
- <path d="M12 5v14" /><path d="M5 12h14" />
95
- {:else if name === 'chevron-down'}
96
- <path d="m6 9 6 6 6-6" />
97
- {:else if name === 'menu'}
98
- <path d="M4 6h16" /><path d="M4 12h16" /><path d="M4 18h16" />
99
- {:else if name === 'archive'}
100
- <rect x="3" y="4" width="18" height="4" rx="1" /><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" /><path d="M9 12h6" />
101
- {:else if name === 'trash'}
102
- <path d="M3 6h18" /><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" /><path d="M6 6v14a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6" /><path d="M10 11v6" /><path d="M14 11v6" />
103
- {:else if name === 'download'}
104
- <path d="M12 3v12" /><path d="m7 10 5 5 5-5" /><path d="M4 19h16" />
105
- {:else if name === 'upload'}
106
- <path d="M12 15V3" /><path d="m7 8 5-5 5 5" /><path d="M4 19h16" />
107
- {:else if name === 'copy'}
108
- <rect x="9" y="9" width="12" height="12" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
109
- {:else if name === 'edit'}
110
- <path d="M12 20h9" /><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
111
- {:else if name === 'folder'}
112
- <path d="M3 7a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
113
- {:else if name === 'link'}
114
- <path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
115
- {:else if name === 'external'}
116
- <path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" />
117
- {:else if name === 'markdown'}
118
- <rect x="3" y="5" width="18" height="14" rx="2" /><path d="M7 15V9l3 3 3-3v6" /><path d="m15 11 2 2 2-2" />
119
- {:else if name === 'image'}
120
- <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.8" /><path d="m21 15-4.5-4.5L7 21" />
121
- {:else if name === 'fork'}
122
- <circle cx="6" cy="6" r="2.5" /><circle cx="18" cy="6" r="2.5" /><circle cx="12" cy="19" r="2.5" /><path d="M6 8.5v2a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3v-2" /><path d="M12 13.5v3" />
123
- {:else if name === 'more'}
124
- <circle cx="5" cy="12" r="1.6" /><circle cx="12" cy="12" r="1.6" /><circle cx="19" cy="12" r="1.6" />
125
- {:else if name === 'settings'}
126
- <circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" />
127
- {:else if name === 'retry'}
128
- <path d="M21 12a9 9 0 1 1-2.64-6.36" /><path d="M21 3v6h-6" />
129
- {:else if name === 'stop'}
130
- <rect x="6" y="6" width="12" height="12" rx="1.5" />
131
- {:else if name === 'tag'}
132
- <path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" /><path d="M7.5 7.5h.01" />
133
- {:else if name === 'star'}
134
- <path d="M12 2.5l2.9 5.9 6.5.95-4.7 4.58 1.1 6.47L12 17.9l-5.8 3.05 1.1-6.47-4.7-4.58 6.5-.95z" />
135
- {:else if name === 'live'}
136
- <circle cx="12" cy="12" r="5" />
137
- {:else if name === 'send'}
138
- <path d="M22 2 11 13" /><path d="M22 2 15 22l-4-9-9-4z" />
139
- {:else if name === 'filter'}
140
- <path d="M3 4h18l-7 8v6l-4 2v-10z" />
141
- {:else if name === 'info'}
142
- <circle cx="12" cy="12" r="9" /><path d="M12 11v5" /><path d="M12 8h.01" />
143
- {:else if name === 'warning'}
144
- <path d="M10.3 3.6 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.6a2 2 0 0 0-3.4 0Z" /><path d="M12 9v4" /><path d="M12 17h.01" />
137
+ {:else if name}
138
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- trusted, static path data -->
139
+ {@html ICONS[name]}
145
140
  {/if}
146
141
  </svg>
147
142
 
@@ -1,4 +1,72 @@
1
- export type IconName = 'archive' | 'back' | 'check' | 'chevron-down' | 'copy' | 'download' | 'edit' | 'external' | 'filter' | 'folder' | 'fork' | 'image' | 'info' | 'link' | 'live' | 'markdown' | 'menu' | 'more' | 'plus' | 'retry' | 'search' | 'send' | 'settings' | 'star' | 'stop' | 'tag' | 'trash' | 'upload' | 'warning' | 'x';
1
+ declare const ICONS: {
2
+ readonly back: "<path d=\"m12 19-7-7 7-7\" /><path d=\"M19 12H5\" />";
3
+ readonly 'arrow-right': "<path d=\"M5 12h14\" /><path d=\"m12 5 7 7-7 7\" />";
4
+ readonly 'arrow-up': "<path d=\"m5 12 7-7 7 7\" /><path d=\"M12 19V5\" />";
5
+ readonly 'arrow-down': "<path d=\"M12 5v14\" /><path d=\"m19 12-7 7-7-7\" />";
6
+ readonly 'chevron-up': "<path d=\"m18 15-6-6-6 6\" />";
7
+ readonly 'chevron-down': "<path d=\"m6 9 6 6 6-6\" />";
8
+ readonly 'chevron-left': "<path d=\"m15 18-6-6 6-6\" />";
9
+ readonly 'chevron-right': "<path d=\"m9 18 6-6-6-6\" />";
10
+ readonly menu: "<path d=\"M4 5h16\" /><path d=\"M4 12h16\" /><path d=\"M4 19h16\" />";
11
+ readonly more: "<circle cx=\"12\" cy=\"12\" r=\"1\" /><circle cx=\"19\" cy=\"12\" r=\"1\" /><circle cx=\"5\" cy=\"12\" r=\"1\" />";
12
+ readonly external: "<path d=\"M15 3h6v6\" /><path d=\"M10 14 21 3\" /><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />";
13
+ readonly 'log-out': "<path d=\"m16 17 5-5-5-5\" /><path d=\"M21 12H9\" /><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />";
14
+ readonly plus: "<path d=\"M5 12h14\" /><path d=\"M12 5v14\" />";
15
+ readonly minus: "<path d=\"M5 12h14\" />";
16
+ readonly check: "<path d=\"M20 6 9 17l-5-5\" />";
17
+ readonly x: "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />";
18
+ readonly search: "<path d=\"m21 21-4.34-4.34\" /><circle cx=\"11\" cy=\"11\" r=\"8\" />";
19
+ readonly filter: "<path d=\"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z\" />";
20
+ readonly copy: "<rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" /><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />";
21
+ readonly edit: "<path d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\" /><path d=\"m15 5 4 4\" />";
22
+ readonly trash: "<path d=\"M10 11v6\" /><path d=\"M14 11v6\" /><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" /><path d=\"M3 6h18\" /><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />";
23
+ readonly save: "<path d=\"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z\" /><path d=\"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7\" /><path d=\"M7 3v4a1 1 0 0 0 1 1h7\" />";
24
+ readonly download: "<path d=\"M12 15V3\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /><path d=\"m7 10 5 5 5-5\" />";
25
+ readonly upload: "<path d=\"M12 3v12\" /><path d=\"m17 8-5-5-5 5\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />";
26
+ readonly send: "<path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\" /><path d=\"m21.854 2.147-10.94 10.939\" />";
27
+ readonly share: "<circle cx=\"18\" cy=\"5\" r=\"3\" /><circle cx=\"6\" cy=\"12\" r=\"3\" /><circle cx=\"18\" cy=\"19\" r=\"3\" /><line x1=\"8.59\" x2=\"15.42\" y1=\"13.51\" y2=\"17.49\" /><line x1=\"15.41\" x2=\"8.59\" y1=\"6.51\" y2=\"10.49\" />";
28
+ readonly retry: "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" /><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" /><path d=\"M8 16H3v5\" />";
29
+ readonly settings: "<path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />";
30
+ readonly play: "<path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\" />";
31
+ readonly pause: "<rect x=\"14\" y=\"3\" width=\"5\" height=\"18\" rx=\"1\" /><rect x=\"5\" y=\"3\" width=\"5\" height=\"18\" rx=\"1\" />";
32
+ readonly stop: "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />";
33
+ readonly file: "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" />";
34
+ readonly 'file-text': "<path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\" /><path d=\"M14 2v5a1 1 0 0 0 1 1h5\" /><path d=\"M10 9H8\" /><path d=\"M16 13H8\" /><path d=\"M16 17H8\" />";
35
+ readonly folder: "<path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\" />";
36
+ readonly archive: "<rect width=\"20\" height=\"5\" x=\"2\" y=\"3\" rx=\"1\" /><path d=\"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8\" /><path d=\"M10 12h4\" />";
37
+ readonly image: "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\" /><circle cx=\"9\" cy=\"9\" r=\"2\" /><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />";
38
+ readonly markdown: "<rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" /><path d=\"M7 15V9l3 3 3-3v6\" /><path d=\"m15 11 2 2 2-2\" />";
39
+ readonly list: "<path d=\"M3 5h.01\" /><path d=\"M3 12h.01\" /><path d=\"M3 19h.01\" /><path d=\"M8 5h13\" /><path d=\"M8 12h13\" /><path d=\"M8 19h13\" />";
40
+ readonly grid: "<rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\" /><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\" />";
41
+ readonly link: "<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\" /><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\" />";
42
+ readonly tag: "<path d=\"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z\" /><circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\" />";
43
+ readonly bookmark: "<path d=\"M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z\" />";
44
+ readonly star: "<path d=\"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\" />";
45
+ readonly heart: "<path d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\" />";
46
+ readonly fork: "<circle cx=\"12\" cy=\"18\" r=\"3\" /><circle cx=\"6\" cy=\"6\" r=\"3\" /><circle cx=\"18\" cy=\"6\" r=\"3\" /><path d=\"M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9\" /><path d=\"M12 12v3\" />";
47
+ readonly live: "<circle cx=\"12\" cy=\"12\" r=\"5\" />";
48
+ readonly eye: "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />";
49
+ readonly 'eye-off': "<path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\" /><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\" /><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\" /><path d=\"m2 2 20 20\" />";
50
+ readonly lock: "<rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\" /><path d=\"M7 11V7a5 5 0 0 1 10 0v4\" />";
51
+ readonly unlock: "<rect width=\"18\" height=\"11\" x=\"3\" y=\"11\" rx=\"2\" ry=\"2\" /><path d=\"M7 11V7a5 5 0 0 1 9.9-1\" />";
52
+ readonly bell: "<path d=\"M10.268 21a2 2 0 0 0 3.464 0\" /><path d=\"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326\" />";
53
+ readonly mail: "<path d=\"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7\" /><rect x=\"2\" y=\"4\" width=\"20\" height=\"16\" rx=\"2\" />";
54
+ readonly calendar: "<path d=\"M8 2v4\" /><path d=\"M16 2v4\" /><rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" /><path d=\"M3 10h18\" />";
55
+ readonly clock: "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 6v6l4 2\" />";
56
+ readonly home: "<path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\" /><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />";
57
+ readonly user: "<path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\" /><circle cx=\"12\" cy=\"7\" r=\"4\" />";
58
+ readonly users: "<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /><path d=\"M16 3.128a4 4 0 0 1 0 7.744\" /><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\" /><circle cx=\"9\" cy=\"7\" r=\"4\" />";
59
+ readonly sun: "<circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2v2\" /><path d=\"M12 20v2\" /><path d=\"m4.93 4.93 1.41 1.41\" /><path d=\"m17.66 17.66 1.41 1.41\" /><path d=\"M2 12h2\" /><path d=\"M20 12h2\" /><path d=\"m6.34 17.66-1.41 1.41\" /><path d=\"m19.07 4.93-1.41 1.41\" />";
60
+ readonly moon: "<path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\" />";
61
+ readonly loader: "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />";
62
+ readonly info: "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 16v-4\" /><path d=\"M12 8h.01\" />";
63
+ readonly warning: "<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" /><path d=\"M12 9v4\" /><path d=\"M12 17h.01\" />";
64
+ readonly help: "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\" /><path d=\"M12 17h.01\" />";
65
+ readonly 'check-circle': "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m9 12 2 2 4-4\" />";
66
+ readonly 'x-circle': "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m15 9-6 6\" /><path d=\"m9 9 6 6\" />";
67
+ readonly 'alert-circle': "<circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" /><line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />";
68
+ };
69
+ export type IconName = keyof typeof ICONS;
2
70
  import type { Snippet } from 'svelte';
3
71
  type $$ComponentProps = {
4
72
  /** Named glyph from the registry. Omit when supplying `children`. */
@@ -9,7 +77,8 @@ type $$ComponentProps = {
9
77
  /** When set, the icon is exposed to AT with this label; otherwise it is
10
78
  * decorative (aria-hidden) and the parent control carries the label. */
11
79
  label?: string;
12
- /** Raw SVG markup (24×24 viewBox) — overrides `name`. */
80
+ /** Raw SVG markup (24×24 viewBox) — overrides `name`. Pass a lucide-svelte
81
+ * component's contents here to render any icon not in the registry. */
13
82
  children?: Snippet;
14
83
  [key: string]: unknown;
15
84
  };
@@ -1,11 +1,16 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
3
4
  import Button from '../atoms/Button.svelte';
4
5
  import Icon from '../atoms/Icon.svelte';
5
6
  import type { IconName } from '../atoms/Icon.svelte';
6
7
 
7
8
  type IconButtonProps = HTMLButtonAttributes & {
8
- icon: IconName;
9
+ /** Named glyph from the registry. Omit when supplying `children`. */
10
+ icon?: IconName;
11
+ /** Raw SVG markup (24×24 viewBox) — overrides `icon`. Pass a
12
+ * lucide-svelte component's contents to render any off-registry icon. */
13
+ children?: Snippet;
9
14
  label: string;
10
15
  variant?: 'default' | 'primary' | 'ghost' | 'danger';
11
16
  size?: number;
@@ -23,6 +28,7 @@
23
28
 
24
29
  let {
25
30
  icon,
31
+ children,
26
32
  label,
27
33
  title = label,
28
34
  variant = 'ghost',
@@ -52,5 +58,9 @@
52
58
  class={klass}
53
59
  aria-label={label}
54
60
  >
55
- <Icon name={icon} {size} />
61
+ {#if children}
62
+ <Icon {size}>{@render children()}</Icon>
63
+ {:else}
64
+ <Icon name={icon} {size} />
65
+ {/if}
56
66
  </Button>
@@ -1,7 +1,12 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLButtonAttributes } from 'svelte/elements';
2
3
  import type { IconName } from '../atoms/Icon.svelte';
3
4
  type IconButtonProps = HTMLButtonAttributes & {
4
- icon: IconName;
5
+ /** Named glyph from the registry. Omit when supplying `children`. */
6
+ icon?: IconName;
7
+ /** Raw SVG markup (24×24 viewBox) — overrides `icon`. Pass a
8
+ * lucide-svelte component's contents to render any off-registry icon. */
9
+ children?: Snippet;
5
10
  label: string;
6
11
  variant?: 'default' | 'primary' | 'ghost' | 'danger';
7
12
  size?: number;
@@ -0,0 +1,226 @@
1
+ <script lang="ts">
2
+ // A timestamp you can read four ways and inspect in one place. Inline it
3
+ // renders in the chosen `mode` (ISO / local / clock / relative); by default
4
+ // clicking it opens a popover that lays out the same instant as ISO-UTC,
5
+ // local, relative, the IANA zone and the unix epoch — so the one value
6
+ // answers "when, exactly?" without leaving the row. The popover is read-only
7
+ // unless you opt into `selectable`, which adds buttons to switch the inline
8
+ // display mode (handy in a demo / settings surface, rarely in a data row).
9
+ //
10
+ // All formatting is delegated to the pure helpers in `$lib/timestamp`; this
11
+ // component owns only the mode state, the live tick for relative mode, and
12
+ // the popover chrome. The trigger is a real <time datetime> element for
13
+ // machine-readability and a11y.
14
+ import Popover from './Popover.svelte';
15
+ import {
16
+ formatTimestamp,
17
+ localTimeZone,
18
+ relativeTime,
19
+ toDate,
20
+ toEpochSeconds,
21
+ toISO,
22
+ toLocal,
23
+ type TimeInput,
24
+ type TimestampMode
25
+ } from '../../timestamp';
26
+
27
+ type Tone = 'inherit' | 'default' | 'muted' | 'faint' | 'danger' | 'accent';
28
+
29
+ let {
30
+ value,
31
+ mode = 'iso',
32
+ details = true,
33
+ selectable = false,
34
+ mono = false,
35
+ tone = 'muted',
36
+ tickMs = 30_000
37
+ }: {
38
+ /** The instant: a Date, epoch milliseconds, or an ISO/parseable string. */
39
+ value: TimeInput;
40
+ /** Inline display mode. Default 'iso'. */
41
+ mode?: TimestampMode;
42
+ /** Click to open the details popover (UTC / local / relative / zone /
43
+ * epoch). Default true. When false, renders as a bare inline <time>. */
44
+ details?: boolean;
45
+ /** Add mode-switch buttons inside the popover so the viewer can change the
46
+ * inline display mode. Default false. Implies `details`. */
47
+ selectable?: boolean;
48
+ /** Render in the monospace font (tabular figures stay on regardless). */
49
+ mono?: boolean;
50
+ /** Text colour, mirroring <Text> tones. Default 'muted' — timestamps read
51
+ * as subdued metadata; pass 'inherit' to blend with surrounding copy. */
52
+ tone?: Tone;
53
+ /** How often relative mode re-renders so "3m ago" stays fresh. */
54
+ tickMs?: number;
55
+ } = $props();
56
+
57
+ // User's in-popover choice overrides the `mode` prop; until they pick, we
58
+ // follow the prop (so a parent can still drive it reactively). Deriving —
59
+ // rather than seeding $state from a prop — keeps both behaviours and avoids
60
+ // capturing only the prop's initial value.
61
+ let override = $state<TimestampMode | null>(null);
62
+ const current = $derived(override ?? mode);
63
+ // Live clock for relative mode only — a single timer that exists solely while
64
+ // relative is on screen, so static modes cost nothing.
65
+ let now = $state(Date.now());
66
+ $effect(() => {
67
+ if (current !== 'relative') return;
68
+ const t = setInterval(() => (now = Date.now()), tickMs);
69
+ return () => clearInterval(t);
70
+ });
71
+
72
+ const date = $derived(toDate(value));
73
+ const label = $derived(formatTimestamp(value, current, now));
74
+ // datetime= wants a valid ISO string; omit it entirely on bad input.
75
+ const machine = $derived(date ? toISO(date) : undefined);
76
+ const showPopover = $derived(details || selectable);
77
+ const cls = $derived(`ts tone-${tone}${mono ? ' mono' : ''}`);
78
+
79
+ const MODES: { id: TimestampMode; name: string }[] = [
80
+ { id: 'iso', name: 'ISO' },
81
+ { id: 'local', name: 'Local' },
82
+ { id: 'time', name: 'Time' },
83
+ { id: 'relative', name: 'Relative' }
84
+ ];
85
+
86
+ const rows = $derived(
87
+ date
88
+ ? [
89
+ { k: 'UTC', v: toISO(date) },
90
+ { k: 'Local', v: toLocal(date) },
91
+ { k: 'Relative', v: relativeTime(date, now) },
92
+ { k: 'Time zone', v: localTimeZone() },
93
+ { k: 'Unix', v: String(toEpochSeconds(date)) }
94
+ ]
95
+ : []
96
+ );
97
+ </script>
98
+
99
+ {#if !date}
100
+ <!-- Unparseable input: degrade to an inert dash rather than empty text. -->
101
+ <time class="{cls} ts-invalid">—</time>
102
+ {:else if showPopover}
103
+ <Popover label="Timestamp details" placement="bottom-start" bare>
104
+ {#snippet trigger()}
105
+ <time class="{cls} ts-trigger" datetime={machine}>{label}</time>
106
+ {/snippet}
107
+ <div class="ts-panel">
108
+ {#if selectable}
109
+ <div class="ts-modes" role="group" aria-label="Display mode">
110
+ {#each MODES as m (m.id)}
111
+ <button
112
+ type="button"
113
+ class="ts-mode"
114
+ class:active={current === m.id}
115
+ aria-pressed={current === m.id}
116
+ onclick={() => (override = m.id)}
117
+ >
118
+ {m.name}
119
+ </button>
120
+ {/each}
121
+ </div>
122
+ {/if}
123
+ <dl class="ts-details">
124
+ {#each rows as row (row.k)}
125
+ <dt>{row.k}</dt>
126
+ <dd>{row.v}</dd>
127
+ {/each}
128
+ </dl>
129
+ </div>
130
+ </Popover>
131
+ {:else}
132
+ <time class={cls} datetime={machine}>{label}</time>
133
+ {/if}
134
+
135
+ <style>
136
+ .ts {
137
+ font-variant-numeric: tabular-nums;
138
+ }
139
+ .mono {
140
+ font-family: var(--font-mono);
141
+ }
142
+ /* Tones mirror <Text>; 'inherit' deliberately sets no colour so the timestamp
143
+ blends into surrounding copy. */
144
+ .tone-default {
145
+ color: var(--text);
146
+ }
147
+ .tone-muted {
148
+ color: var(--text-muted);
149
+ }
150
+ .tone-faint {
151
+ color: var(--text-faint);
152
+ }
153
+ .tone-danger {
154
+ color: var(--danger);
155
+ }
156
+ .tone-accent {
157
+ color: var(--accent);
158
+ }
159
+ .ts-invalid {
160
+ color: var(--text-muted);
161
+ }
162
+ /* The popover trigger is a plain inline run of text that hints it's clickable
163
+ — no button chrome (we pass `bare`), just an underline on hover/focus. */
164
+ .ts-trigger {
165
+ cursor: pointer;
166
+ border-radius: var(--r-sm);
167
+ text-decoration-line: underline;
168
+ text-decoration-style: dotted;
169
+ text-underline-offset: 2px;
170
+ text-decoration-color: var(--border-strong);
171
+ }
172
+ .ts-trigger:hover,
173
+ .ts-trigger:focus-visible {
174
+ text-decoration-color: currentColor;
175
+ }
176
+
177
+ .ts-panel {
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: var(--sp-2);
181
+ padding: var(--sp-1);
182
+ }
183
+ .ts-modes {
184
+ display: flex;
185
+ gap: var(--sp-1);
186
+ }
187
+ .ts-mode {
188
+ flex: 1;
189
+ padding: var(--sp-1) var(--sp-2);
190
+ border: 1px solid var(--border-strong);
191
+ border-radius: var(--r-sm);
192
+ background: transparent;
193
+ color: var(--text-muted);
194
+ font-size: var(--fs-xs);
195
+ cursor: pointer;
196
+ transition:
197
+ background 0.12s var(--ease),
198
+ color 0.12s var(--ease);
199
+ }
200
+ .ts-mode:hover {
201
+ background: var(--bg-elevated-2);
202
+ color: var(--text);
203
+ }
204
+ .ts-mode.active {
205
+ background: var(--bg-elevated-2);
206
+ color: var(--text);
207
+ border-color: var(--text);
208
+ }
209
+ .ts-details {
210
+ display: grid;
211
+ grid-template-columns: auto 1fr;
212
+ gap: var(--sp-1) var(--sp-2);
213
+ margin: 0;
214
+ font-size: var(--fs-xs);
215
+ }
216
+ .ts-details dt {
217
+ color: var(--text-muted);
218
+ white-space: nowrap;
219
+ }
220
+ .ts-details dd {
221
+ margin: 0;
222
+ color: var(--text);
223
+ font-variant-numeric: tabular-nums;
224
+ overflow-wrap: anywhere;
225
+ }
226
+ </style>
@@ -0,0 +1,24 @@
1
+ import { type TimeInput, type TimestampMode } from '../../timestamp';
2
+ type Tone = 'inherit' | 'default' | 'muted' | 'faint' | 'danger' | 'accent';
3
+ type $$ComponentProps = {
4
+ /** The instant: a Date, epoch milliseconds, or an ISO/parseable string. */
5
+ value: TimeInput;
6
+ /** Inline display mode. Default 'iso'. */
7
+ mode?: TimestampMode;
8
+ /** Click to open the details popover (UTC / local / relative / zone /
9
+ * epoch). Default true. When false, renders as a bare inline <time>. */
10
+ details?: boolean;
11
+ /** Add mode-switch buttons inside the popover so the viewer can change the
12
+ * inline display mode. Default false. Implies `details`. */
13
+ selectable?: boolean;
14
+ /** Render in the monospace font (tabular figures stay on regardless). */
15
+ mono?: boolean;
16
+ /** Text colour, mirroring <Text> tones. Default 'muted' — timestamps read
17
+ * as subdued metadata; pass 'inherit' to blend with surrounding copy. */
18
+ tone?: Tone;
19
+ /** How often relative mode re-renders so "3m ago" stays fresh. */
20
+ tickMs?: number;
21
+ };
22
+ declare const Timestamp: import("svelte").Component<$$ComponentProps, {}, "">;
23
+ type Timestamp = ReturnType<typeof Timestamp>;
24
+ export default Timestamp;
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export { default as RadioGroup, type RadioOption, } from './components/molecules
38
38
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
39
39
  export { default as Tabs, type TabItem } from './components/molecules/Tabs.svelte';
40
40
  export { default as ThemePicker } from './components/molecules/ThemePicker.svelte';
41
+ export { default as Timestamp } from './components/molecules/Timestamp.svelte';
41
42
  export { default as Toaster } from './components/molecules/Toaster.svelte';
42
43
  export { default as Toggle } from './components/molecules/Toggle.svelte';
43
44
  export { default as Tooltip } from './components/molecules/Tooltip.svelte';
@@ -46,4 +47,5 @@ export { type Column, default as DataTable } from './components/organisms/DataTa
46
47
  export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
47
48
  export { type Mode, THEMES, theme } from './stores/theme.svelte';
48
49
  export { type Toast, type ToastTone, toasts } from './stores/toast.svelte';
50
+ export { formatTimestamp, localTimeZone, relativeTime, type TimeInput, type TimestampMode, } from './timestamp';
49
51
  export { type TruncateMode, type TruncateOptions, truncate } from './truncate';
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ export { default as RadioGroup, } from './components/molecules/RadioGroup.svelte
46
46
  export { default as SelectButton } from './components/molecules/SelectButton.svelte';
47
47
  export { default as Tabs } from './components/molecules/Tabs.svelte';
48
48
  export { default as ThemePicker } from './components/molecules/ThemePicker.svelte';
49
+ export { default as Timestamp } from './components/molecules/Timestamp.svelte';
49
50
  export { default as Toaster } from './components/molecules/Toaster.svelte';
50
51
  export { default as Toggle } from './components/molecules/Toggle.svelte';
51
52
  export { default as Tooltip } from './components/molecules/Tooltip.svelte';
@@ -56,4 +57,5 @@ export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
56
57
  // ---- stores / actions ----
57
58
  export { THEMES, theme } from './stores/theme.svelte';
58
59
  export { toasts } from './stores/toast.svelte';
60
+ export { formatTimestamp, localTimeZone, relativeTime, } from './timestamp';
59
61
  export { truncate } from './truncate';
@@ -0,0 +1,23 @@
1
+ export type TimeInput = Date | number | string;
2
+ /** How the timestamp text renders inline. */
3
+ export type TimestampMode = 'iso' | 'local' | 'time' | 'relative';
4
+ /** Coerce a loose input to a Date, or null when it can't be parsed. */
5
+ export declare function toDate(value: TimeInput): Date | null;
6
+ /** Full ISO 8601, UTC — `2026-06-14T07:30:00.000Z`. */
7
+ export declare function toISO(d: Date): string;
8
+ /** Locale date+time in the viewer's zone — `6/14/2026, 4:30:00 PM`. */
9
+ export declare function toLocal(d: Date): string;
10
+ /** Clock only, zero-padded `HH:MM:SS`, in the viewer's local zone. */
11
+ export declare function toClock(d: Date): string;
12
+ /** Unix epoch in whole seconds. */
13
+ export declare function toEpochSeconds(d: Date): number;
14
+ /** The viewer's IANA time zone — `Asia/Tokyo`, `UTC`, … */
15
+ export declare function localTimeZone(): string;
16
+ /**
17
+ * "3m ago", "2h ago", "5d ago" — past-relative, coarsening as it ages and
18
+ * falling back to a locale date once past ~30 days. Future instants read
19
+ * "in 3m" etc. `now` is injectable so callers (and tests) control the clock.
20
+ */
21
+ export declare function relativeTime(value: TimeInput, now?: number): string;
22
+ /** Render a date in the chosen inline mode. Returns '' for unparseable input. */
23
+ export declare function formatTimestamp(value: TimeInput, mode: TimestampMode, now?: number): string;
@@ -0,0 +1,74 @@
1
+ // Pure date-formatting helpers behind <Timestamp> — the logic lives here (and
2
+ // stays unit-testable / framework-free) while the component owns only the
3
+ // display-mode switching and the popover. Everything accepts the same loose
4
+ // `TimeInput` (Date | epoch ms | ISO string) and tolerates bad input by
5
+ // returning '' rather than throwing, so a single malformed value never takes
6
+ // down a table row.
7
+ /** Coerce a loose input to a Date, or null when it can't be parsed. */
8
+ export function toDate(value) {
9
+ const d = value instanceof Date ? value : new Date(value);
10
+ return Number.isNaN(d.getTime()) ? null : d;
11
+ }
12
+ /** Full ISO 8601, UTC — `2026-06-14T07:30:00.000Z`. */
13
+ export function toISO(d) {
14
+ return d.toISOString();
15
+ }
16
+ /** Locale date+time in the viewer's zone — `6/14/2026, 4:30:00 PM`. */
17
+ export function toLocal(d) {
18
+ return d.toLocaleString();
19
+ }
20
+ /** Clock only, zero-padded `HH:MM:SS`, in the viewer's local zone. */
21
+ export function toClock(d) {
22
+ const p = (n) => String(n).padStart(2, '0');
23
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
24
+ }
25
+ /** Unix epoch in whole seconds. */
26
+ export function toEpochSeconds(d) {
27
+ return Math.floor(d.getTime() / 1000);
28
+ }
29
+ /** The viewer's IANA time zone — `Asia/Tokyo`, `UTC`, … */
30
+ export function localTimeZone() {
31
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
32
+ }
33
+ /**
34
+ * "3m ago", "2h ago", "5d ago" — past-relative, coarsening as it ages and
35
+ * falling back to a locale date once past ~30 days. Future instants read
36
+ * "in 3m" etc. `now` is injectable so callers (and tests) control the clock.
37
+ */
38
+ export function relativeTime(value, now = Date.now()) {
39
+ const d = toDate(value);
40
+ if (!d)
41
+ return '';
42
+ const deltaMs = now - d.getTime();
43
+ const future = deltaMs < 0;
44
+ const secs = Math.floor(Math.abs(deltaMs) / 1000);
45
+ const suffix = (s) => (future ? `in ${s}` : `${s} ago`);
46
+ if (secs < 60)
47
+ return suffix(`${secs}s`);
48
+ const mins = Math.floor(secs / 60);
49
+ if (mins < 60)
50
+ return suffix(`${mins}m`);
51
+ const hrs = Math.floor(mins / 60);
52
+ if (hrs < 24)
53
+ return suffix(`${hrs}h`);
54
+ const days = Math.floor(hrs / 24);
55
+ if (days < 30)
56
+ return suffix(`${days}d`);
57
+ return d.toLocaleDateString();
58
+ }
59
+ /** Render a date in the chosen inline mode. Returns '' for unparseable input. */
60
+ export function formatTimestamp(value, mode, now) {
61
+ const d = toDate(value);
62
+ if (!d)
63
+ return '';
64
+ switch (mode) {
65
+ case 'iso':
66
+ return toISO(d);
67
+ case 'local':
68
+ return toLocal(d);
69
+ case 'time':
70
+ return toClock(d);
71
+ case 'relative':
72
+ return relativeTime(d, now);
73
+ }
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",