@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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- 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"}
|