@delightstack/components 0.1.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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,611 @@
1
+ <script lang="ts" module>
2
+ export interface BreadcrumbItem {
3
+ /** Display text for the breadcrumb */
4
+ label: string;
5
+ /** Link target — omit for the current (non-clickable) crumb */
6
+ href?: string;
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import Button from '../actions/Button.svelte';
13
+ import List from '../display/List.svelte';
14
+ import ListItem from '../display/ListItem.svelte';
15
+
16
+ const propId = $props.id();
17
+
18
+ let {
19
+ /** The breadcrumb items to display */
20
+ items = [] as BreadcrumbItem[],
21
+
22
+ /** Max visible items before collapsing middle items into an ellipsis dropdown.
23
+ * When undefined, the component auto-collapses to fit the container width using
24
+ * pure CSS container queries (no JS measurement, works during SSR). */
25
+ max_items = undefined as number | undefined,
26
+
27
+ /** Whether to show a home icon as the first breadcrumb */
28
+ show_home = true,
29
+
30
+ /** The href for the home breadcrumb */
31
+ home_href = '/',
32
+
33
+ /** The size of the breadcrumbs */
34
+ size = '1' as '0' | '1' | '2' | '3',
35
+
36
+ /** Condensed spacing: smaller gaps between items and separators */
37
+ dense = false,
38
+
39
+ /** Whether to display skeleton loading state. Only shown when `items` is empty —
40
+ * as soon as any real items are provided the skeleton is replaced by them. */
41
+ skeleton = false,
42
+
43
+ /** Number of skeleton placeholder items */
44
+ skeleton_count = 3,
45
+
46
+ /** The ID of the element */
47
+ id = propId,
48
+
49
+ /** Specifies a custom class name */
50
+ class: class_name = '',
51
+
52
+ /** Custom rendering snippet */
53
+ children = undefined as undefined | Snippet,
54
+
55
+ /** Custom separator snippet */
56
+ separator = undefined as undefined | Snippet,
57
+
58
+ /** Called when a breadcrumb item is clicked. May return a promise — while
59
+ * it is pending the clicked crumb shows a loading spinner (the trail
60
+ * re-flows smoothly), which clears automatically once the promise
61
+ * settles. Only fires for crumbs without an `href` (crumbs with an
62
+ * `href` navigate natively). */
63
+ onclick = undefined as
64
+ | ((detail: { item: BreadcrumbItem; index: number }) => void | Promise<void>)
65
+ | undefined,
66
+ } = $props();
67
+
68
+ const allItems = $derived<BreadcrumbItem[]>(
69
+ show_home ? [{ label: 'Home', href: home_href }, ...items] : items,
70
+ );
71
+
72
+ // Only show the skeleton when explicitly requested AND there is no real data
73
+ // yet. Once any item is provided, the real trail renders instead.
74
+ const showSkeleton = $derived(skeleton && items.length === 0);
75
+
76
+ // ──────────────────────────────────────────────────────────────────────
77
+ // Width estimation (em, relative to the breadcrumb font-size).
78
+ //
79
+ // We can't measure the DOM during SSR, so each item's rendered width is
80
+ // *estimated* from its label length. These estimates feed CSS custom
81
+ // properties (`--bc-reveal`) that, combined with container query units
82
+ // (`cqi`), let the browser collapse/expand items purely in CSS — so the
83
+ // collapse is correct on the very first server-rendered paint.
84
+ // ──────────────────────────────────────────────────────────────────────
85
+ const CHAR_EM = 0.52; // approx width of one character
86
+ const LABEL_MAX_EM = 11; // cap (mirrors the label's max-width truncation)
87
+ const BTN_PAD_EM = 1.1; // breadcrumb button horizontal padding (0.55em per side)
88
+ const CUR_PAD_EM = 1; // current (last) item uses lighter padding
89
+ const SEP_EM = 1.4; // separator glyph + its padding
90
+ const HOME_EM = 1; // home icon glyph
91
+ const ELLIPSIS_EM = SEP_EM + 1.5; // leading sep + "…" trigger
92
+
93
+ function estItem(
94
+ item: BreadcrumbItem,
95
+ index: number,
96
+ isHome: boolean,
97
+ isLast: boolean,
98
+ ): number {
99
+ const lead = index === 0 ? 0 : SEP_EM;
100
+ const pad = isLast ? CUR_PAD_EM : BTN_PAD_EM;
101
+ if (isHome) return lead + pad + HOME_EM;
102
+ const labelW = Math.min(item.label.length * CHAR_EM, LABEL_MAX_EM);
103
+ return lead + pad + labelW;
104
+ }
105
+
106
+ const n = $derived(allItems.length);
107
+
108
+ // head = always-visible first item; tail = always-visible last N items.
109
+ const tailCount = $derived(max_items !== undefined ? Math.max(1, max_items - 2) : 2);
110
+ const tailStart = $derived(n - tailCount);
111
+
112
+ const collapsible = $derived(
113
+ max_items !== undefined ? max_items >= 2 && n > max_items : n >= 4,
114
+ );
115
+
116
+ type MiddleEntry = { item: BreadcrumbItem; index: number; reveal: number };
117
+
118
+ // Per-item reveal thresholds + the threshold at which the ellipsis disappears.
119
+ const collapse = $derived.by(() => {
120
+ if (!collapsible) {
121
+ return { middle: [] as MiddleEntry[], ellipsisReveal: 0 };
122
+ }
123
+ const isAuto = max_items === undefined;
124
+ const ests = allItems.map((it, i) =>
125
+ estItem(it, i, show_home && i === 0, i === n - 1),
126
+ );
127
+ const middle: MiddleEntry[] = [];
128
+
129
+ if (isAuto) {
130
+ const fullTrail = ests.reduce((a, b) => a + b, 0);
131
+ const baseB =
132
+ ests[0] + ELLIPSIS_EM + ests.slice(tailStart).reduce((a, b) => a + b, 0);
133
+ // Reveal middle items from the tail side inward: the right-most middle
134
+ // item needs the least room, the left-most needs the most. Each
135
+ // intermediate threshold includes the ellipsis (it's still showing).
136
+ let suffix = 0;
137
+ const reveals: number[] = [];
138
+ for (let i = tailStart - 1; i >= 1; i--) {
139
+ suffix += ests[i];
140
+ reveals[i] = baseB + suffix;
141
+ }
142
+ // Revealing the left-most middle item shows the *entire* trail — at which
143
+ // point the ellipsis disappears, so drop its width from that threshold
144
+ // (otherwise a trail that fits would still collapse). Keep it monotonic.
145
+ reveals[1] = Math.max(fullTrail, reveals[2] ?? 0);
146
+ for (let i = 1; i < tailStart; i++) {
147
+ middle.push({ item: allItems[i], index: i, reveal: reveals[i] });
148
+ }
149
+ // Ellipsis hides once every middle item fits (the largest threshold).
150
+ const ellipsisReveal = reveals[1] ?? baseB;
151
+ return { middle, ellipsisReveal };
152
+ }
153
+
154
+ // Explicit max_items: collapse the whole middle block at once.
155
+ const ALWAYS = 99999;
156
+ for (let i = 1; i < tailStart; i++) {
157
+ middle.push({ item: allItems[i], index: i, reveal: ALWAYS });
158
+ }
159
+ return { middle, ellipsisReveal: ALWAYS };
160
+ });
161
+
162
+ const middleEntries = $derived(collapse.middle);
163
+ const ellipsisReveal = $derived(collapse.ellipsisReveal);
164
+ const tailEntries = $derived(
165
+ collapsible
166
+ ? allItems.slice(tailStart).map((item, i) => ({
167
+ item,
168
+ index: tailStart + i,
169
+ isLast: tailStart + i === n - 1,
170
+ }))
171
+ : [],
172
+ );
173
+
174
+ const skeletonWidths = [4.5, 6, 3.5, 5, 4];
175
+
176
+ function handleItemClick(item: BreadcrumbItem, index: number) {
177
+ // Return the handler's result so a returned promise propagates to the
178
+ // underlying Button, which drives the per-crumb loading spinner and the
179
+ // smooth width transition while it's pending.
180
+ return onclick?.({ item, index });
181
+ }
182
+
183
+ let navEl: HTMLElement | undefined = $state(undefined);
184
+
185
+ // The inline collapse is pure CSS (so it's correct in SSR). The ellipsis
186
+ // *menu* only matters once the user opens it (client-only), so its contents
187
+ // are derived from measuring which inline copies the CSS has collapsed —
188
+ // letting the menu reuse the real Button/Popover/List components.
189
+ let collapsedSet = $state<Set<number>>(new Set());
190
+ const collapsedEntries = $derived(
191
+ middleEntries.filter((m) => collapsedSet.has(m.index)),
192
+ );
193
+
194
+ function syncLayout() {
195
+ if (!navEl) return;
196
+ // CSS collapses items to zero size but leaves them in the DOM (and tab
197
+ // order). Mark the zero-size copies `inert` so only the visible copy is
198
+ // focusable / announced.
199
+ navEl.querySelectorAll<HTMLElement>('[data-bc-inert]').forEach((el) => {
200
+ const r = el.getBoundingClientRect();
201
+ const collapsed = r.width < 1 || r.height < 1;
202
+ if (el.inert !== collapsed) el.inert = collapsed;
203
+ });
204
+ // Track which middle items are currently collapsed → the menu lists exactly
205
+ // those (no duplication of the inline-visible ones).
206
+ const next = new Set<number>();
207
+ navEl.querySelectorAll<HTMLElement>('[data-bc-mid]').forEach((el) => {
208
+ if (el.getBoundingClientRect().width < 1) next.add(Number(el.dataset.bcMid));
209
+ });
210
+ let changed = next.size !== collapsedSet.size;
211
+ if (!changed) {
212
+ for (const i of next) {
213
+ if (!collapsedSet.has(i)) {
214
+ changed = true;
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ if (changed) collapsedSet = next;
220
+ }
221
+
222
+ $effect(() => {
223
+ if (!navEl) return;
224
+ const ro = new ResizeObserver(() => syncLayout());
225
+ ro.observe(navEl);
226
+ syncLayout();
227
+ return () => ro.disconnect();
228
+ });
229
+
230
+ const schemaJson = $derived(
231
+ JSON.stringify({
232
+ '@context': 'https://schema.org',
233
+ '@type': 'BreadcrumbList',
234
+ itemListElement: allItems.map((item, i) => ({
235
+ '@type': 'ListItem',
236
+ position: i + 1,
237
+ name: item.label,
238
+ ...(item.href ? { item: item.href } : {}),
239
+ })),
240
+ }),
241
+ );
242
+
243
+ const navClass = $derived(
244
+ [`size-${size}`, dense ? 'dense' : '', class_name].filter(Boolean).join(' '),
245
+ );
246
+ </script>
247
+
248
+ {#snippet sep()}
249
+ {#if separator}
250
+ {@render separator()}
251
+ {:else}
252
+ <svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true">
253
+ <path
254
+ d="M9 18l6-6-6-6"
255
+ stroke="currentColor"
256
+ stroke-width="2"
257
+ stroke-linecap="round"
258
+ stroke-linejoin="round"
259
+ fill="none" />
260
+ </svg>
261
+ {/if}
262
+ {/snippet}
263
+
264
+ {#snippet homeIcon()}
265
+ <svg class="home-icon" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
266
+ <path
267
+ d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
268
+ stroke="currentColor"
269
+ stroke-width="2"
270
+ stroke-linecap="round"
271
+ stroke-linejoin="round"
272
+ fill="none" />
273
+ </svg>
274
+ {/snippet}
275
+
276
+ {#snippet itemButton(item: BreadcrumbItem, index: number, isLast: boolean)}
277
+ {#if isLast}
278
+ <span class="label current">
279
+ {#if show_home && index === 0}
280
+ {@render homeIcon()}
281
+ <span class="sr-only">{item.label}</span>
282
+ {:else}
283
+ {item.label}
284
+ {/if}
285
+ </span>
286
+ {:else}
287
+ <Button
288
+ transparent
289
+ dense
290
+ href={item.href}
291
+ onclick={item.href ? undefined : () => handleItemClick(item, index)}>
292
+ {#if show_home && index === 0}
293
+ {@render homeIcon()}
294
+ <span class="sr-only">{item.label}</span>
295
+ {:else}
296
+ <span class="label">{item.label}</span>
297
+ {/if}
298
+ </Button>
299
+ {/if}
300
+ {/snippet}
301
+
302
+ {#if showSkeleton}
303
+ <nav class={navClass} aria-label="Breadcrumb" aria-hidden="true" {id}>
304
+ <ol>
305
+ {#if show_home}
306
+ <li>
307
+ <span class="skeleton-cell">{@render homeIcon()}</span>
308
+ </li>
309
+ {/if}
310
+ {#each { length: skeleton_count } as _, i}
311
+ {#if show_home || i > 0}
312
+ <li class="sep">{@render sep()}</li>
313
+ {/if}
314
+ <li>
315
+ <span class="skeleton-cell" style:--shimmer-delay="{i * 120}ms">
316
+ <span
317
+ class="skeleton-bar"
318
+ style:width="{skeletonWidths[i % skeletonWidths.length]}em">
319
+ </span>
320
+ </span>
321
+ </li>
322
+ {/each}
323
+ </ol>
324
+ </nav>
325
+ {:else if children}
326
+ <nav class={navClass} aria-label="Breadcrumb" {id}>
327
+ {@render children()}
328
+ </nav>
329
+ {#if allItems.length > 0}
330
+ {@html `<script type="application/ld+json">${schemaJson}</script>`}
331
+ {/if}
332
+ {:else}
333
+ <nav class={navClass} aria-label="Breadcrumb" bind:this={navEl} {id}>
334
+ <ol>
335
+ {#if !collapsible}
336
+ {#each allItems as item, i}
337
+ {@const isLast = i === n - 1}
338
+ {#if i > 0}<li class="sep">{@render sep()}</li>{/if}
339
+ <li class:current={isLast} aria-current={isLast ? 'page' : undefined}>
340
+ {@render itemButton(item, i, isLast)}
341
+ </li>
342
+ {/each}
343
+ {:else}
344
+ <!-- Head: always visible -->
345
+ <li>
346
+ {@render itemButton(allItems[0], 0, n === 1)}
347
+ </li>
348
+
349
+ <!-- Ellipsis: a real Button + Popover menu listing the collapsed items.
350
+ The separator + trigger collapse to zero (pure CSS) once every
351
+ middle item fits inline; the portaled Popover is unaffected. -->
352
+ <li class="sep collapse-inv" style:--bc-reveal="{ellipsisReveal}em">
353
+ {@render sep()}
354
+ </li>
355
+ <li class="collapse-inv" style:--bc-reveal="{ellipsisReveal}em" data-bc-inert>
356
+ <Button
357
+ transparent
358
+ dense
359
+ aria-label="Show hidden breadcrumbs"
360
+ popover_placement="bottom-start">
361
+ {#snippet children()}…{/snippet}
362
+ {#snippet menu({ close })}
363
+ <List>
364
+ {#each collapsedEntries as c (c.item.href ?? c.index)}
365
+ <ListItem
366
+ href={c.item.href}
367
+ onclick={() => {
368
+ handleItemClick(c.item, c.index);
369
+ close();
370
+ }}>
371
+ {c.item.label}
372
+ </ListItem>
373
+ {/each}
374
+ </List>
375
+ {/snippet}
376
+ </Button>
377
+ </li>
378
+
379
+ <!-- Middle items: collapse from the tail side inward as space shrinks -->
380
+ {#each middleEntries as m (m.item.href ?? m.index)}
381
+ <li class="sep collapse" style:--bc-reveal="{m.reveal}em">
382
+ {@render sep()}
383
+ </li>
384
+ <li
385
+ class="collapse"
386
+ style:--bc-reveal="{m.reveal}em"
387
+ data-bc-inert
388
+ data-bc-mid={m.index}>
389
+ {@render itemButton(m.item, m.index, false)}
390
+ </li>
391
+ {/each}
392
+
393
+ <!-- Tail: always visible -->
394
+ {#each tailEntries as t (t.item.href ?? t.index)}
395
+ <li class="sep">{@render sep()}</li>
396
+ <li class:current={t.isLast} aria-current={t.isLast ? 'page' : undefined}>
397
+ {@render itemButton(t.item, t.index, t.isLast)}
398
+ </li>
399
+ {/each}
400
+ {/if}
401
+ </ol>
402
+ </nav>
403
+
404
+ {#if allItems.length > 0}
405
+ {@html `<script type="application/ld+json">${schemaJson}</script>`}
406
+ {/if}
407
+ {/if}
408
+
409
+ <style>
410
+ nav {
411
+ display: block;
412
+ position: relative;
413
+ /* Establish a query container so descendants can collapse/expand using
414
+ * container query units (cqi) — the engine of the SSR-safe auto-collapse.
415
+ * `container-type: inline-size` disables intrinsic sizing, so the element
416
+ * must fill its parent's width (otherwise it collapses to 0 in any
417
+ * shrink-to-fit context like a flex/inline parent). */
418
+ container-type: inline-size;
419
+ box-sizing: border-box;
420
+ width: 100%;
421
+ max-width: 100%;
422
+ font-size: var(--text-base, 0.875rem);
423
+ &.size-0 {
424
+ font-size: var(--text-sm, 0.75rem);
425
+ }
426
+ &.size-1 {
427
+ font-size: var(--text-base, 0.875rem);
428
+ }
429
+ &.size-2 {
430
+ font-size: var(--text-lg, 1rem);
431
+ }
432
+ &.size-3 {
433
+ font-size: var(--text-xl, 1.125rem);
434
+ }
435
+
436
+ /* The Button component is generously padded for standalone use; tighten the
437
+ * horizontal padding for the dense breadcrumb trail. Scoped to the nav by
438
+ * Svelte; the inner :global() pierces the child Button without leaking. */
439
+ :global(.button.dense a),
440
+ :global(.button.dense button) {
441
+ padding-inline: 0.55em;
442
+ }
443
+
444
+ &.dense {
445
+ :global(.button.dense a),
446
+ :global(.button.dense button) {
447
+ padding-inline: 0.4em;
448
+ }
449
+ ol {
450
+ --bc-sep-pad: 0.0625rem;
451
+ }
452
+ .skeleton-cell {
453
+ padding-inline: 0.4em;
454
+ }
455
+ }
456
+ }
457
+
458
+ ol {
459
+ --bc-sep-pad: 0.25rem;
460
+ display: flex;
461
+ align-items: center;
462
+ flex-wrap: nowrap;
463
+ gap: 0;
464
+ list-style: none;
465
+ margin: 0;
466
+ padding: 0;
467
+ min-width: 0;
468
+ color: light-dark(var(--color-text-muted, #6b7280), var(--color-text-muted, #9ca3af));
469
+ }
470
+
471
+ li {
472
+ box-sizing: border-box;
473
+ display: flex;
474
+ align-items: center;
475
+ flex: 0 0 auto;
476
+ min-width: 0;
477
+
478
+ &.current {
479
+ color: light-dark(var(--color-text, #1a1a1a), var(--color-text, #f5f5f5));
480
+ font-weight: 500;
481
+ }
482
+ }
483
+
484
+ .label {
485
+ max-width: 150px;
486
+ overflow: hidden;
487
+ text-overflow: ellipsis;
488
+ white-space: nowrap;
489
+ display: inline-block;
490
+
491
+ &.current {
492
+ padding: 0 0.5em;
493
+ }
494
+ }
495
+
496
+ .sep {
497
+ /* Spacing lives in padding (not gap/margin). When a separator collapses,
498
+ * the padding must collapse too — border-box keeps padding at its set
499
+ * value even at max-width:0, which would leave a ghost gap — so the
500
+ * collapsible variants drive padding-inline with the same clamp. */
501
+ padding-inline: var(--bc-sep-pad);
502
+ /* Subtler than the full-contrast crumb labels, but still clearly visible.
503
+ * Must NOT use --color-text-disabled here: that token is a currentColor-
504
+ * relative dim meant to be applied to a full-contrast text color. The
505
+ * separator inherits the already-muted list color, so the disabled token
506
+ * compounds and washes the chevron into the background in both modes. */
507
+ color: light-dark(var(--color-text-muted, #6b7280), var(--color-text-muted, #9ca3af));
508
+ flex-shrink: 0;
509
+
510
+ &.collapse {
511
+ padding-inline: clamp(0px, (100cqi - var(--bc-reveal)) * 1000, var(--bc-sep-pad));
512
+ }
513
+ &.collapse-inv {
514
+ padding-inline: clamp(0px, (var(--bc-reveal) - 100cqi) * 1000, var(--bc-sep-pad));
515
+ }
516
+
517
+ svg {
518
+ display: block;
519
+ }
520
+ }
521
+
522
+ .home-icon {
523
+ display: block;
524
+ flex-shrink: 0;
525
+ }
526
+
527
+ .sr-only {
528
+ position: absolute;
529
+ width: 1px;
530
+ height: 1px;
531
+ padding: 0;
532
+ margin: -1px;
533
+ overflow: hidden;
534
+ clip: rect(0, 0, 0, 0);
535
+ white-space: nowrap;
536
+ border-width: 0;
537
+ }
538
+
539
+ /* ── CSS-only collapse primitives ───────────────────────────────────────
540
+ * `--bc-reveal` is an em length estimated from the label. An element with
541
+ * `.collapse` is visible when the container is at least that wide;
542
+ * `.collapse-inv` is the inverse (visible only while narrower). The
543
+ * `* 1000` turns the width difference into a near-instant 0 ↔ full switch. */
544
+ .collapse {
545
+ overflow: clip;
546
+ max-width: clamp(0px, (100cqi - var(--bc-reveal)) * 1000, 100cqi);
547
+ }
548
+ .collapse-inv {
549
+ overflow: clip;
550
+ max-width: clamp(0px, (var(--bc-reveal) - 100cqi) * 1000, 100cqi);
551
+ }
552
+
553
+ /* The ellipsis cell collapses to zero (via .collapse-inv) when no items are
554
+ * hidden; its Button's Popover is portaled, so it isn't affected by the clip. */
555
+
556
+ /* ── Skeleton ────────────────────────────────────────────────────────────
557
+ * Each cell mirrors a dense Button's box exactly — same fixed control font
558
+ * and the shared dense control-height formula (see Button's standalone
559
+ * height rule) — so toggling skeleton ↔ loaded never shifts the row
560
+ * height. The text-height pill bar is centered inside that slot. */
561
+ .skeleton-cell {
562
+ display: inline-flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ box-sizing: border-box;
566
+ font-size: var(--control-font-1, 1rem);
567
+ padding: 0 0.55em;
568
+ min-height: calc(1em * var(--control-height-ratio-dense, 2.5));
569
+ line-height: 1em;
570
+ }
571
+ .skeleton-bar {
572
+ display: block;
573
+ height: 0.7em;
574
+ border-radius: var(--radius-full, 1e5px);
575
+ position: relative;
576
+ overflow: hidden;
577
+ background: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
578
+
579
+ &::after {
580
+ content: '';
581
+ position: absolute;
582
+ inset: 0;
583
+ transform: translateX(-100%);
584
+ background-image: linear-gradient(
585
+ 105deg,
586
+ transparent 25%,
587
+ var(--skeleton-sheen, rgb(from var(--color-text, #888) r g b / 0.12)) 50%,
588
+ transparent 75%
589
+ );
590
+ animation: delight-skeleton-shimmer var(--skeleton-duration, 2.4s) ease-in-out
591
+ infinite;
592
+ animation-delay: var(--shimmer-delay, 0s);
593
+ }
594
+ }
595
+
596
+ @keyframes -global-delight-skeleton-shimmer {
597
+ 0% {
598
+ transform: translateX(-100%);
599
+ }
600
+ 55%,
601
+ 100% {
602
+ transform: translateX(100%);
603
+ }
604
+ }
605
+
606
+ @media (prefers-reduced-motion: reduce) {
607
+ .skeleton-bar::after {
608
+ animation: none;
609
+ }
610
+ }
611
+ </style>
@@ -0,0 +1,28 @@
1
+ export interface BreadcrumbItem {
2
+ /** Display text for the breadcrumb */
3
+ label: string;
4
+ /** Link target — omit for the current (non-clickable) crumb */
5
+ href?: string;
6
+ }
7
+ import type { Snippet } from 'svelte';
8
+ declare const Breadcrumbs: import("svelte").Component<{
9
+ items?: BreadcrumbItem[];
10
+ max_items?: number | undefined;
11
+ show_home?: boolean;
12
+ home_href?: string;
13
+ size?: "0" | "1" | "2" | "3";
14
+ dense?: boolean;
15
+ skeleton?: boolean;
16
+ skeleton_count?: number;
17
+ id?: string;
18
+ class?: string;
19
+ children?: undefined | Snippet;
20
+ separator?: undefined | Snippet;
21
+ onclick?: ((detail: {
22
+ item: BreadcrumbItem;
23
+ index: number;
24
+ }) => void | Promise<void>) | undefined;
25
+ }, {}, "">;
26
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
27
+ export default Breadcrumbs;
28
+ //# sourceMappingURL=Breadcrumbs.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Breadcrumbs.svelte.d.ts","sourceRoot":"","sources":["../../src/navigation/Breadcrumbs.svelte.ts"],"names":[],"mappings":"AAGC,MAAM,WAAW,cAAc;IAC9B,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAGF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AA6XtC,QAAA,MAAM,WAAW;YAlVgE,cAAc,EAAE;gBAAc,MAAM,GAAG,SAAS;gBAAc,OAAO;gBAAc,MAAM;WAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;YAAU,OAAO;eAAa,OAAO;qBAAmB,MAAM;;YAA8B,MAAM;eAAa,SAAS,GAAG,OAAO;gBAAc,SAAS,GAAG,OAAO;cAAc,CAAC,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAC/b,SAAS;UAiV2C,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}