@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,698 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/** A single tab descriptor in the `tabs` array. */
|
|
5
|
+
export interface TabItem {
|
|
6
|
+
/** The label text shown in the tab button. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** An optional badge (count or short string) shown after the label. */
|
|
9
|
+
badge?: string | number;
|
|
10
|
+
/** Whether this individual tab is disabled. */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** The panel content for this tab. When omitted, the component's
|
|
13
|
+
children are used as the panel instead (gate them yourself with the
|
|
14
|
+
bound `tab` index). */
|
|
15
|
+
content?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** How the panel content animates when the active tab changes. */
|
|
19
|
+
export type TabsTransition = 'none' | 'fade' | 'slide';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import { untrack } from 'svelte';
|
|
24
|
+
import { ripple } from '@delightstack/utilities';
|
|
25
|
+
|
|
26
|
+
const propId = $props.id();
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
/** The index of the active tab (bindable). */
|
|
30
|
+
tab = $bindable(0),
|
|
31
|
+
|
|
32
|
+
/** The tabs to render, in order. The array index is the tab's value. */
|
|
33
|
+
tabs = [] as TabItem[],
|
|
34
|
+
|
|
35
|
+
/** Use pill-shaped tab buttons. */
|
|
36
|
+
pills = false,
|
|
37
|
+
|
|
38
|
+
/** Use a boxed (segmented-control) tab style. */
|
|
39
|
+
boxed = false,
|
|
40
|
+
|
|
41
|
+
/** The orientation of the tab list. */
|
|
42
|
+
orientation = 'horizontal' as 'horizontal' | 'vertical',
|
|
43
|
+
|
|
44
|
+
/** The size of the tabs. 0=small, 1=default, 2=medium, 3=large. */
|
|
45
|
+
size = '1' as '0' | '1' | '2' | '3',
|
|
46
|
+
|
|
47
|
+
/** Stretch tab buttons to fill the available width. */
|
|
48
|
+
full_width = false,
|
|
49
|
+
|
|
50
|
+
/** Disable every tab. */
|
|
51
|
+
disabled = false,
|
|
52
|
+
|
|
53
|
+
/** How the panel content animates between tabs. */
|
|
54
|
+
transition = 'none' as TabsTransition,
|
|
55
|
+
|
|
56
|
+
/** Show a skeleton loading state in place of the tab list. */
|
|
57
|
+
skeleton = false,
|
|
58
|
+
|
|
59
|
+
/** Number of skeleton tab placeholders. */
|
|
60
|
+
skeleton_count = 3,
|
|
61
|
+
|
|
62
|
+
/** Called when the active tab changes. */
|
|
63
|
+
onchange = undefined as ((detail: { tab: number }) => void) | undefined,
|
|
64
|
+
|
|
65
|
+
/** The ID of the element. */
|
|
66
|
+
id = propId,
|
|
67
|
+
|
|
68
|
+
/** Additional class name(s). */
|
|
69
|
+
class: class_name = '',
|
|
70
|
+
|
|
71
|
+
/** Panel content used when a tab has no `content` snippet. Receives the
|
|
72
|
+
active tab index and a `select` helper. */
|
|
73
|
+
children = undefined as
|
|
74
|
+
| undefined
|
|
75
|
+
| Snippet<[{ tab: number; select: (i: number) => void }]>,
|
|
76
|
+
} = $props();
|
|
77
|
+
|
|
78
|
+
/* ------------------------------------------------------------------ */
|
|
79
|
+
/* Active tab + selection */
|
|
80
|
+
/* ------------------------------------------------------------------ */
|
|
81
|
+
function select(i: number) {
|
|
82
|
+
const item = tabs[i];
|
|
83
|
+
if (disabled || !item || item.disabled || i === tab) return;
|
|
84
|
+
tab = i;
|
|
85
|
+
onchange?.({ tab: i });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Track navigation direction (for the slide transition). */
|
|
89
|
+
let prevTab = tab;
|
|
90
|
+
let direction = $state(1);
|
|
91
|
+
$effect(() => {
|
|
92
|
+
const next = tab;
|
|
93
|
+
untrack(() => {
|
|
94
|
+
if (next !== prevTab) {
|
|
95
|
+
direction = next > prevTab ? 1 : -1;
|
|
96
|
+
prevTab = next;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const activeContent = $derived(tabs[tab]?.content);
|
|
102
|
+
|
|
103
|
+
/* ------------------------------------------------------------------ */
|
|
104
|
+
/* Sliding indicator */
|
|
105
|
+
/* ------------------------------------------------------------------ */
|
|
106
|
+
let listEl = $state<HTMLElement | undefined>(undefined);
|
|
107
|
+
let tabEls = $state<HTMLElement[]>([]);
|
|
108
|
+
let indicatorStyle = $state('opacity: 0;');
|
|
109
|
+
|
|
110
|
+
function measure() {
|
|
111
|
+
const el = tabEls[tab];
|
|
112
|
+
if (!listEl || !el) {
|
|
113
|
+
indicatorStyle = 'opacity: 0;';
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (boxed) {
|
|
117
|
+
// The thumb sits exactly on the active tab's box, so the list's padding
|
|
118
|
+
// shows as an equal gutter on all four sides (no top/left mismatch).
|
|
119
|
+
indicatorStyle =
|
|
120
|
+
`transform: translate(${el.offsetLeft}px, ${el.offsetTop}px);` +
|
|
121
|
+
` width: ${el.offsetWidth}px; height: ${el.offsetHeight}px; opacity: 1;`;
|
|
122
|
+
} else if (orientation === 'vertical') {
|
|
123
|
+
indicatorStyle = `transform: translateY(${el.offsetTop}px); height: ${el.offsetHeight}px; opacity: 1;`;
|
|
124
|
+
} else {
|
|
125
|
+
indicatorStyle = `transform: translateX(${el.offsetLeft}px); width: ${el.offsetWidth}px; opacity: 1;`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Re-measure whenever anything that affects geometry changes. Effects run
|
|
130
|
+
// after the DOM updates, so offsets are already settled — no rAF needed.
|
|
131
|
+
$effect(() => {
|
|
132
|
+
// Touch every geometry input so the effect re-runs when any of them change
|
|
133
|
+
// (measure() reads tab/orientation/variant, but not these layout inputs).
|
|
134
|
+
const _deps = [tabs.length, pills, full_width, size, skeleton];
|
|
135
|
+
void _deps;
|
|
136
|
+
if (!skeleton) measure();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Late layout shifts (font load, container resize, full-width reflow) don't
|
|
140
|
+
// touch any of the tracked state above, so observe the list directly.
|
|
141
|
+
$effect(() => {
|
|
142
|
+
if (!listEl || skeleton) return;
|
|
143
|
+
const ro = new ResizeObserver(() => measure());
|
|
144
|
+
ro.observe(listEl);
|
|
145
|
+
for (const el of tabEls) if (el) ro.observe(el);
|
|
146
|
+
return () => ro.disconnect();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/* ------------------------------------------------------------------ */
|
|
150
|
+
/* Keyboard navigation (roving focus, auto-activation) */
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
function enabledStep(from: number, step: number): number {
|
|
153
|
+
const n = tabs.length;
|
|
154
|
+
for (let i = 1; i <= n; i++) {
|
|
155
|
+
const idx = (from + step * i + n * i) % n;
|
|
156
|
+
if (!tabs[idx]?.disabled) return idx;
|
|
157
|
+
}
|
|
158
|
+
return from;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function firstEnabled(): number {
|
|
162
|
+
const i = tabs.findIndex((t) => !t?.disabled);
|
|
163
|
+
return i === -1 ? 0 : i;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function lastEnabled(): number {
|
|
167
|
+
for (let i = tabs.length - 1; i >= 0; i--) if (!tabs[i]?.disabled) return i;
|
|
168
|
+
return tabs.length - 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function focusIndex(i: number) {
|
|
172
|
+
const el = tabEls[i];
|
|
173
|
+
if (el) el.focus();
|
|
174
|
+
select(i);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function onKeyDown(e: KeyboardEvent, i: number) {
|
|
178
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
select(i);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const vertical = orientation === 'vertical';
|
|
184
|
+
const next = vertical ? 'ArrowDown' : 'ArrowRight';
|
|
185
|
+
const prev = vertical ? 'ArrowUp' : 'ArrowLeft';
|
|
186
|
+
if (e.key === next) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
focusIndex(enabledStep(i, 1));
|
|
189
|
+
} else if (e.key === prev) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
focusIndex(enabledStep(i, -1));
|
|
192
|
+
} else if (e.key === 'Home') {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
focusIndex(firstEnabled());
|
|
195
|
+
} else if (e.key === 'End') {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
focusIndex(lastEnabled());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ------------------------------------------------------------------ */
|
|
202
|
+
/* Transitions */
|
|
203
|
+
/* ------------------------------------------------------------------ */
|
|
204
|
+
let reduce_motion = $state(false);
|
|
205
|
+
$effect(() => {
|
|
206
|
+
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
207
|
+
reduce_motion = mq.matches;
|
|
208
|
+
const on = () => (reduce_motion = mq.matches);
|
|
209
|
+
mq.addEventListener('change', on);
|
|
210
|
+
return () => mq.removeEventListener('change', on);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const effTransition = $derived<TabsTransition>(reduce_motion ? 'none' : transition);
|
|
214
|
+
|
|
215
|
+
const DURATION = 300;
|
|
216
|
+
const EASE = (t: number) => 1 - Math.pow(1 - t, 3); // cubic-out
|
|
217
|
+
/** Slide travel distance (rem). Bold enough to read as a real slide. */
|
|
218
|
+
const SLIDE = 4.5;
|
|
219
|
+
|
|
220
|
+
function panelIn(_node: HTMLElement) {
|
|
221
|
+
if (effTransition === 'fade') {
|
|
222
|
+
return { duration: DURATION, easing: EASE, css: (t: number) => `opacity: ${t}` };
|
|
223
|
+
}
|
|
224
|
+
// slide: incoming panel flies in from the direction of travel
|
|
225
|
+
const d = direction;
|
|
226
|
+
return {
|
|
227
|
+
duration: DURATION,
|
|
228
|
+
easing: EASE,
|
|
229
|
+
css: (t: number) =>
|
|
230
|
+
`opacity: ${t}; transform: translateX(${(1 - t) * d * SLIDE}rem)`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function panelOut(_node: HTMLElement) {
|
|
235
|
+
if (effTransition === 'fade') {
|
|
236
|
+
return { duration: DURATION, easing: EASE, css: (t: number) => `opacity: ${t}` };
|
|
237
|
+
}
|
|
238
|
+
// slide: outgoing panel exits opposite the direction of travel
|
|
239
|
+
const d = direction;
|
|
240
|
+
return {
|
|
241
|
+
duration: DURATION,
|
|
242
|
+
easing: EASE,
|
|
243
|
+
css: (t: number) =>
|
|
244
|
+
`opacity: ${t}; transform: translateX(${(1 - t) * -d * SLIDE}rem)`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ------------------------------------------------------------------ */
|
|
249
|
+
const sizeMap: Record<string, string> = {
|
|
250
|
+
'0': 'var(--text-sm, 0.815rem)',
|
|
251
|
+
'1': 'var(--text-base, 1rem)',
|
|
252
|
+
'2': 'var(--text-lg, 1.1rem)',
|
|
253
|
+
'3': 'var(--text-xl, 1.25rem)',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Pseudo-random skeleton tab widths (em) — real labels vary, so should these.
|
|
257
|
+
const skeletonWidths = [5, 6.5, 4.25, 5.75];
|
|
258
|
+
|
|
259
|
+
const hasPanel = $derived(!!activeContent || !!children);
|
|
260
|
+
const panelId = `tabpanel-${id}`;
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
<div
|
|
264
|
+
{id}
|
|
265
|
+
class={['tabs', class_name].filter(Boolean).join(' ')}
|
|
266
|
+
class:pills
|
|
267
|
+
class:boxed
|
|
268
|
+
class:vertical={orientation === 'vertical'}
|
|
269
|
+
class:full-width={full_width}
|
|
270
|
+
class:disabled
|
|
271
|
+
style:font-size={sizeMap[size] ?? sizeMap['1']}>
|
|
272
|
+
{#if skeleton}
|
|
273
|
+
<div class="list skeleton" role="tablist" aria-hidden="true">
|
|
274
|
+
{#each { length: skeleton_count } as _, i}
|
|
275
|
+
<div
|
|
276
|
+
class="skeleton-tab"
|
|
277
|
+
style:width="{skeletonWidths[i % skeletonWidths.length]}em"
|
|
278
|
+
style:--shimmer-delay="{i * 120}ms">
|
|
279
|
+
</div>
|
|
280
|
+
{/each}
|
|
281
|
+
</div>
|
|
282
|
+
{:else}
|
|
283
|
+
<div class="list" role="tablist" aria-orientation={orientation} bind:this={listEl}>
|
|
284
|
+
<div class="indicator" style={indicatorStyle}></div>
|
|
285
|
+
{#each tabs as t, i (i)}
|
|
286
|
+
{@const isDisabled = disabled || !!t.disabled}
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
role="tab"
|
|
290
|
+
class="tab"
|
|
291
|
+
class:active={tab === i}
|
|
292
|
+
class:disabled={isDisabled}
|
|
293
|
+
aria-selected={tab === i}
|
|
294
|
+
aria-disabled={isDisabled || undefined}
|
|
295
|
+
aria-controls={hasPanel ? panelId : undefined}
|
|
296
|
+
id="tab-{id}-{i}"
|
|
297
|
+
tabindex={tab === i ? 0 : -1}
|
|
298
|
+
bind:this={tabEls[i]}
|
|
299
|
+
onclick={() => select(i)}
|
|
300
|
+
onkeydown={(e) => onKeyDown(e, i)}
|
|
301
|
+
{@attach ripple({ enabled: !isDisabled, zIndex: 0 })}>
|
|
302
|
+
<span class="label">{t.label}</span>
|
|
303
|
+
{#if t.badge !== undefined}
|
|
304
|
+
<span class="badge">{t.badge}</span>
|
|
305
|
+
{/if}
|
|
306
|
+
</button>
|
|
307
|
+
{/each}
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{#if hasPanel}
|
|
311
|
+
<div class="panels" class:animated={effTransition !== 'none'}>
|
|
312
|
+
{#if effTransition === 'none'}
|
|
313
|
+
<div
|
|
314
|
+
class="panel"
|
|
315
|
+
role="tabpanel"
|
|
316
|
+
id={panelId}
|
|
317
|
+
aria-labelledby="tab-{id}-{tab}"
|
|
318
|
+
tabindex="0">
|
|
319
|
+
{#if activeContent}
|
|
320
|
+
{@render activeContent()}
|
|
321
|
+
{:else if children}
|
|
322
|
+
{@render children({ tab, select })}
|
|
323
|
+
{/if}
|
|
324
|
+
</div>
|
|
325
|
+
{:else}
|
|
326
|
+
{#key tab}
|
|
327
|
+
<div
|
|
328
|
+
class="panel"
|
|
329
|
+
role="tabpanel"
|
|
330
|
+
id={panelId}
|
|
331
|
+
aria-labelledby="tab-{id}-{tab}"
|
|
332
|
+
tabindex="0"
|
|
333
|
+
in:panelIn
|
|
334
|
+
out:panelOut>
|
|
335
|
+
{#if activeContent}
|
|
336
|
+
{@render activeContent()}
|
|
337
|
+
{:else if children}
|
|
338
|
+
{@render children({ tab, select })}
|
|
339
|
+
{/if}
|
|
340
|
+
</div>
|
|
341
|
+
{/key}
|
|
342
|
+
{/if}
|
|
343
|
+
</div>
|
|
344
|
+
{/if}
|
|
345
|
+
{/if}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<style>
|
|
349
|
+
/* ========== Container ========== */
|
|
350
|
+
.tabs {
|
|
351
|
+
display: flex;
|
|
352
|
+
flex-direction: column;
|
|
353
|
+
width: 100%;
|
|
354
|
+
min-width: 0;
|
|
355
|
+
|
|
356
|
+
&.vertical {
|
|
357
|
+
flex-direction: row;
|
|
358
|
+
align-items: flex-start;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
&.disabled {
|
|
362
|
+
opacity: 0.6;
|
|
363
|
+
pointer-events: none;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ========== Tab list ========== */
|
|
368
|
+
.list {
|
|
369
|
+
display: flex;
|
|
370
|
+
position: relative;
|
|
371
|
+
gap: 0;
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
border-bottom: 1px solid var(--color-border, #e0e0e0);
|
|
374
|
+
|
|
375
|
+
.tabs.vertical & {
|
|
376
|
+
flex-direction: column;
|
|
377
|
+
border-bottom: none;
|
|
378
|
+
border-right: 1px solid var(--color-border, #e0e0e0);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.tabs.pills & {
|
|
382
|
+
border-bottom: none;
|
|
383
|
+
gap: 0.3rem;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.tabs.pills.vertical & {
|
|
387
|
+
border-right: none;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.tabs.boxed & {
|
|
391
|
+
background: var(--color-bg-muted, #f1f1f1);
|
|
392
|
+
border: 1px solid var(--color-border, #e0e0e0);
|
|
393
|
+
border-radius: var(--radius-lg, 10px);
|
|
394
|
+
padding: 0.3rem;
|
|
395
|
+
gap: 0;
|
|
396
|
+
@supports (corner-shape: squircle) {
|
|
397
|
+
corner-shape: squircle;
|
|
398
|
+
border-radius: calc(var(--radius-lg, 10px) * var(--squircle-ratio, 2));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.tabs.full-width & {
|
|
403
|
+
width: 100%;
|
|
404
|
+
}
|
|
405
|
+
.tabs.full-width & > .tab {
|
|
406
|
+
flex: 1;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ========== Sliding indicator ========== */
|
|
411
|
+
.indicator {
|
|
412
|
+
position: absolute;
|
|
413
|
+
bottom: -1px;
|
|
414
|
+
left: 0;
|
|
415
|
+
height: 2px;
|
|
416
|
+
background: var(--color-action, #1976d2);
|
|
417
|
+
border-radius: var(--radius-full, 1e5px);
|
|
418
|
+
pointer-events: none;
|
|
419
|
+
z-index: 1;
|
|
420
|
+
opacity: 0;
|
|
421
|
+
transition:
|
|
422
|
+
transform 260ms var(--ease-spring, cubic-bezier(0.34, 1.4, 0.64, 1)),
|
|
423
|
+
width 260ms var(--ease-spring, cubic-bezier(0.34, 1.4, 0.64, 1)),
|
|
424
|
+
height 260ms var(--ease-spring, cubic-bezier(0.34, 1.4, 0.64, 1)),
|
|
425
|
+
opacity 150ms ease;
|
|
426
|
+
|
|
427
|
+
.tabs.vertical & {
|
|
428
|
+
bottom: auto;
|
|
429
|
+
left: auto;
|
|
430
|
+
right: -1px;
|
|
431
|
+
top: 0;
|
|
432
|
+
width: 2px;
|
|
433
|
+
height: auto;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.tabs.pills & {
|
|
437
|
+
display: none;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Boxed: the indicator becomes the active "thumb" — an elevated surface
|
|
441
|
+
that glides between options. measure() gives it the active tab's exact
|
|
442
|
+
box (translate x/y + width/height), so the list padding reads as an
|
|
443
|
+
equal gutter on every side. */
|
|
444
|
+
.tabs.boxed & {
|
|
445
|
+
top: 0;
|
|
446
|
+
bottom: auto;
|
|
447
|
+
left: 0;
|
|
448
|
+
height: auto;
|
|
449
|
+
background: var(--color-surface, #fff);
|
|
450
|
+
border-radius: calc(var(--radius-lg, 10px) - 0.2rem);
|
|
451
|
+
box-shadow:
|
|
452
|
+
0 1px 2px rgb(0 0 0 / 0.06),
|
|
453
|
+
0 2px 6px rgb(0 0 0 / 0.08);
|
|
454
|
+
@supports (corner-shape: squircle) {
|
|
455
|
+
corner-shape: squircle;
|
|
456
|
+
border-radius: calc((var(--radius-lg, 10px) - 0.2rem) * var(--squircle-ratio, 2));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* ========== Tab button ========== */
|
|
462
|
+
.tab {
|
|
463
|
+
display: inline-flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
justify-content: center;
|
|
466
|
+
gap: 0.5em;
|
|
467
|
+
position: relative;
|
|
468
|
+
z-index: 2;
|
|
469
|
+
flex-shrink: 0;
|
|
470
|
+
background: transparent;
|
|
471
|
+
border: none;
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
padding: 0.7em 1.05em;
|
|
474
|
+
margin: 0;
|
|
475
|
+
font-size: inherit;
|
|
476
|
+
font-family: inherit;
|
|
477
|
+
font-weight: 500;
|
|
478
|
+
line-height: 1.2;
|
|
479
|
+
color: var(--color-text-muted, #666);
|
|
480
|
+
white-space: nowrap;
|
|
481
|
+
outline: none;
|
|
482
|
+
border-radius: var(--radius-md, 5px);
|
|
483
|
+
-webkit-tap-highlight-color: transparent;
|
|
484
|
+
@supports (corner-shape: squircle) {
|
|
485
|
+
corner-shape: squircle;
|
|
486
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
487
|
+
}
|
|
488
|
+
/* OUT transition: colours/background snap in on hover, ease back out;
|
|
489
|
+
the press scale always eases (both directions) so it feels physical. */
|
|
490
|
+
transition:
|
|
491
|
+
color 220ms ease,
|
|
492
|
+
background-color 220ms ease,
|
|
493
|
+
scale 160ms ease;
|
|
494
|
+
|
|
495
|
+
&:hover:not(.disabled):not(.active) {
|
|
496
|
+
color: var(--color-text, #222);
|
|
497
|
+
background: rgb(from var(--color-text, #333) r g b / 0.06);
|
|
498
|
+
/* snap the colour/background in; keep the scale easing */
|
|
499
|
+
transition: scale 160ms ease;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
&:active:not(.disabled) {
|
|
503
|
+
scale: 0.9;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
&:focus-visible {
|
|
507
|
+
box-shadow: inset 0 0 0 2px var(--color-action, #1976d2);
|
|
508
|
+
border-radius: var(--radius-md, 5px);
|
|
509
|
+
@supports (corner-shape: squircle) {
|
|
510
|
+
corner-shape: squircle;
|
|
511
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
&.active {
|
|
516
|
+
color: var(--color-action, #1976d2);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
&.disabled {
|
|
520
|
+
opacity: 0.45;
|
|
521
|
+
cursor: not-allowed;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* ---- Pills (variant class lives on the container) ---- */
|
|
525
|
+
.tabs.pills & {
|
|
526
|
+
border-radius: var(--radius-full, 1e5px);
|
|
527
|
+
padding: 0.5em 1.1em;
|
|
528
|
+
|
|
529
|
+
&.active {
|
|
530
|
+
background: var(--color-action, #1976d2);
|
|
531
|
+
color: var(--color-action-text, #fff);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* ---- Boxed (text rides above the gliding thumb) ---- */
|
|
536
|
+
.tabs.boxed & {
|
|
537
|
+
border-radius: calc(var(--radius-lg, 10px) - 0.2rem);
|
|
538
|
+
padding: 0.5em 1.1em;
|
|
539
|
+
@supports (corner-shape: squircle) {
|
|
540
|
+
corner-shape: squircle;
|
|
541
|
+
border-radius: calc((var(--radius-lg, 10px) - 0.2rem) * var(--squircle-ratio, 2));
|
|
542
|
+
}
|
|
543
|
+
&.active {
|
|
544
|
+
color: var(--color-text-active, var(--color-text, #222));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.label {
|
|
550
|
+
position: relative;
|
|
551
|
+
z-index: 1;
|
|
552
|
+
pointer-events: none;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* ========== Badge ========== */
|
|
556
|
+
.badge {
|
|
557
|
+
display: inline-flex;
|
|
558
|
+
align-items: center;
|
|
559
|
+
justify-content: center;
|
|
560
|
+
background: var(--color-action, #1976d2);
|
|
561
|
+
color: var(--color-action-text, #fff);
|
|
562
|
+
border-radius: var(--radius-full, 1e5px);
|
|
563
|
+
font-size: 0.72em;
|
|
564
|
+
font-weight: 600;
|
|
565
|
+
line-height: 1;
|
|
566
|
+
padding: 0.2em 0.45em;
|
|
567
|
+
min-width: 1.5em;
|
|
568
|
+
min-height: 1.35em;
|
|
569
|
+
position: relative;
|
|
570
|
+
z-index: 1;
|
|
571
|
+
pointer-events: none;
|
|
572
|
+
transition:
|
|
573
|
+
background-color 220ms ease,
|
|
574
|
+
color 220ms ease;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* Inactive tabs get a quieter, tinted badge; the active one inverts inside
|
|
578
|
+
pills so it stays legible on the filled background. */
|
|
579
|
+
.tab:not(.active) .badge {
|
|
580
|
+
background: rgb(from var(--color-text, #333) r g b / 0.1);
|
|
581
|
+
color: var(--color-text-muted, #666);
|
|
582
|
+
}
|
|
583
|
+
.tabs.pills .tab.active .badge {
|
|
584
|
+
background: var(--color-action-text, #fff);
|
|
585
|
+
color: var(--color-action, #1976d2);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* ========== Panels ========== */
|
|
589
|
+
.panels {
|
|
590
|
+
min-width: 0;
|
|
591
|
+
|
|
592
|
+
.tabs.vertical & {
|
|
593
|
+
flex: 1;
|
|
594
|
+
padding-left: 1.25em;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* When animated, stack incoming/outgoing panels in one grid cell so they
|
|
599
|
+
crossfade/slide over each other without a layout jump. */
|
|
600
|
+
.panels.animated {
|
|
601
|
+
display: grid;
|
|
602
|
+
overflow: hidden;
|
|
603
|
+
}
|
|
604
|
+
.panels.animated > .panel {
|
|
605
|
+
grid-area: 1 / 1;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.panel {
|
|
609
|
+
padding: 1.1em 0;
|
|
610
|
+
outline: none;
|
|
611
|
+
|
|
612
|
+
.tabs.vertical & {
|
|
613
|
+
padding-top: 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
&:focus-visible {
|
|
617
|
+
outline: 2px solid var(--color-action, #1976d2);
|
|
618
|
+
outline-offset: 2px;
|
|
619
|
+
border-radius: var(--radius-md, 5px);
|
|
620
|
+
@supports (corner-shape: squircle) {
|
|
621
|
+
corner-shape: squircle;
|
|
622
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/* ========== Skeleton ========== */
|
|
628
|
+
.list.skeleton {
|
|
629
|
+
pointer-events: none;
|
|
630
|
+
gap: 0.4em;
|
|
631
|
+
border-bottom-color: var(--color-border, #e0e0e0);
|
|
632
|
+
|
|
633
|
+
.tabs.full-width & > .skeleton-tab {
|
|
634
|
+
flex: 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.skeleton-tab {
|
|
639
|
+
/* Match a real tab's box exactly so toggling skeleton ↔ loaded never
|
|
640
|
+
shifts the row: line-height 1.2 (same as .tab) makes 1lh resolve to the
|
|
641
|
+
real line box instead of inheriting the page's larger line-height, and
|
|
642
|
+
1.4em is the default tab's block padding (0.7em × 2). */
|
|
643
|
+
line-height: 1.2;
|
|
644
|
+
height: calc(1lh + 1.4em);
|
|
645
|
+
flex-shrink: 0;
|
|
646
|
+
border-radius: var(--radius-md, 5px);
|
|
647
|
+
position: relative;
|
|
648
|
+
overflow: hidden;
|
|
649
|
+
background: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
|
|
650
|
+
@supports (corner-shape: squircle) {
|
|
651
|
+
corner-shape: squircle;
|
|
652
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
&::after {
|
|
656
|
+
content: '';
|
|
657
|
+
position: absolute;
|
|
658
|
+
inset: 0;
|
|
659
|
+
transform: translateX(-100%);
|
|
660
|
+
background-image: linear-gradient(
|
|
661
|
+
105deg,
|
|
662
|
+
transparent 25%,
|
|
663
|
+
var(--skeleton-sheen, rgb(from var(--color-text, #888) r g b / 0.12)) 50%,
|
|
664
|
+
transparent 75%
|
|
665
|
+
);
|
|
666
|
+
animation: delight-skeleton-shimmer var(--skeleton-duration, 2.4s) ease-in-out
|
|
667
|
+
infinite;
|
|
668
|
+
animation-delay: var(--shimmer-delay, 0s);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.tabs.pills &,
|
|
672
|
+
.tabs.boxed & {
|
|
673
|
+
height: calc(1lh + 1em);
|
|
674
|
+
}
|
|
675
|
+
.tabs.pills & {
|
|
676
|
+
border-radius: var(--radius-full, 1e5px);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
@keyframes -global-delight-skeleton-shimmer {
|
|
681
|
+
0% {
|
|
682
|
+
transform: translateX(-100%);
|
|
683
|
+
}
|
|
684
|
+
55%,
|
|
685
|
+
100% {
|
|
686
|
+
transform: translateX(100%);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
@media (prefers-reduced-motion: reduce) {
|
|
691
|
+
.skeleton-tab::after {
|
|
692
|
+
animation: none;
|
|
693
|
+
}
|
|
694
|
+
.indicator {
|
|
695
|
+
transition: opacity 150ms ease;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
</style>
|