@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,965 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export { default as Step } from './Steps.svelte';
|
|
3
|
+
|
|
4
|
+
export interface StepsContext {
|
|
5
|
+
/** Index of the currently active step */
|
|
6
|
+
current: number;
|
|
7
|
+
/** The layout direction of the steps */
|
|
8
|
+
orientation: 'horizontal' | 'vertical';
|
|
9
|
+
/** Whether steps can be clicked to navigate */
|
|
10
|
+
clickable: boolean;
|
|
11
|
+
/** Whether steps must be completed in order (only visited/adjacent steps are clickable) */
|
|
12
|
+
linear: boolean;
|
|
13
|
+
/** The size applied to all steps */
|
|
14
|
+
size: string;
|
|
15
|
+
/** Total number of registered steps */
|
|
16
|
+
totalSteps: number;
|
|
17
|
+
/** Registers a new step and returns its index */
|
|
18
|
+
register: () => number;
|
|
19
|
+
/** Navigates to the step at the given index */
|
|
20
|
+
navigate: (index: number) => void;
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import { ripple } from '@delightstack/utilities';
|
|
26
|
+
import { getContext, setContext, type Snippet } from 'svelte';
|
|
27
|
+
import Button from '../actions/Button.svelte';
|
|
28
|
+
import Expand from '../display/Expand.svelte';
|
|
29
|
+
|
|
30
|
+
const propId = $props.id();
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
/* --- Step item props --- */
|
|
34
|
+
/** The title text for this step */
|
|
35
|
+
title = '',
|
|
36
|
+
|
|
37
|
+
/** Optional description text below the title */
|
|
38
|
+
description = '',
|
|
39
|
+
|
|
40
|
+
/** Whether this step is optional */
|
|
41
|
+
optional = false,
|
|
42
|
+
|
|
43
|
+
/** Whether this step is in an error state */
|
|
44
|
+
error = false,
|
|
45
|
+
|
|
46
|
+
/* --- Steps container props --- */
|
|
47
|
+
/** The current active step index */
|
|
48
|
+
current = $bindable(0),
|
|
49
|
+
|
|
50
|
+
/** Layout orientation */
|
|
51
|
+
orientation = 'horizontal' as 'horizontal' | 'vertical',
|
|
52
|
+
|
|
53
|
+
/** Whether completed steps are clickable for navigation */
|
|
54
|
+
clickable = false,
|
|
55
|
+
|
|
56
|
+
/** Whether navigation is restricted to sequential order */
|
|
57
|
+
linear = true,
|
|
58
|
+
|
|
59
|
+
/** Size variant */
|
|
60
|
+
size = '1' as '0' | '1' | '2' | '3',
|
|
61
|
+
|
|
62
|
+
/** Show skeleton shimmer placeholders */
|
|
63
|
+
skeleton = false,
|
|
64
|
+
|
|
65
|
+
/** Number of skeleton placeholder steps */
|
|
66
|
+
skeleton_count = 4,
|
|
67
|
+
|
|
68
|
+
/** Element ID */
|
|
69
|
+
id = propId,
|
|
70
|
+
|
|
71
|
+
/** Additional CSS classes */
|
|
72
|
+
class: class_name = '',
|
|
73
|
+
|
|
74
|
+
/** Child content snippet */
|
|
75
|
+
children = undefined as undefined | Snippet,
|
|
76
|
+
|
|
77
|
+
/** Called when the active step changes */
|
|
78
|
+
onchange = undefined as ((detail: { step: number }) => void) | undefined,
|
|
79
|
+
|
|
80
|
+
/** Called when all steps are completed */
|
|
81
|
+
oncomplete = undefined as (() => void) | undefined,
|
|
82
|
+
} = $props();
|
|
83
|
+
|
|
84
|
+
/* ------------------------------------------------------------------ */
|
|
85
|
+
/* Determine whether this instance is a container or an item */
|
|
86
|
+
/* ------------------------------------------------------------------ */
|
|
87
|
+
const parentContext = getContext<StepsContext | undefined>('steps');
|
|
88
|
+
const isItem = !!parentContext;
|
|
89
|
+
|
|
90
|
+
/* ------------------------------------------------------------------ */
|
|
91
|
+
/* Steps container behaviour */
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
let stepCounter = 0;
|
|
94
|
+
|
|
95
|
+
if (!isItem) {
|
|
96
|
+
const ctx = $state<StepsContext>({
|
|
97
|
+
current,
|
|
98
|
+
orientation,
|
|
99
|
+
clickable,
|
|
100
|
+
linear,
|
|
101
|
+
size,
|
|
102
|
+
totalSteps: 0,
|
|
103
|
+
register() {
|
|
104
|
+
const index = stepCounter++;
|
|
105
|
+
ctx.totalSteps = stepCounter;
|
|
106
|
+
return index;
|
|
107
|
+
},
|
|
108
|
+
navigate(index: number) {
|
|
109
|
+
if (!ctx.clickable) return;
|
|
110
|
+
if (ctx.linear && index > ctx.current) return;
|
|
111
|
+
if (index === ctx.current) return;
|
|
112
|
+
current = index;
|
|
113
|
+
onchange?.({ step: index });
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
setContext<StepsContext>('steps', ctx);
|
|
117
|
+
|
|
118
|
+
$effect(() => {
|
|
119
|
+
ctx.current = current;
|
|
120
|
+
ctx.orientation = orientation;
|
|
121
|
+
ctx.clickable = clickable;
|
|
122
|
+
ctx.linear = linear;
|
|
123
|
+
ctx.size = size;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
$effect(() => {
|
|
127
|
+
if (current >= ctx.totalSteps && ctx.totalSteps > 0) {
|
|
128
|
+
oncomplete?.();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
/* Step item behaviour */
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
const stepIndex = isItem ? parentContext.register() : -1;
|
|
137
|
+
|
|
138
|
+
const isComplete = $derived(isItem && !error && stepIndex < parentContext.current);
|
|
139
|
+
const isCurrent = $derived(isItem && stepIndex === parentContext.current);
|
|
140
|
+
const isUpcoming = $derived(isItem && stepIndex > parentContext.current);
|
|
141
|
+
const isError = $derived(isItem && error);
|
|
142
|
+
const isLast = $derived(isItem && stepIndex === parentContext.totalSteps - 1);
|
|
143
|
+
const connectorFilled = $derived(isItem && stepIndex < parentContext.current);
|
|
144
|
+
|
|
145
|
+
const canClick = $derived(
|
|
146
|
+
isItem &&
|
|
147
|
+
parentContext.clickable &&
|
|
148
|
+
(isComplete || (!parentContext.linear && !isCurrent)),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const stepState = $derived(
|
|
152
|
+
isError ? 'error' : isComplete ? 'complete' : isCurrent ? 'current' : 'upcoming',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const ariaLabel = $derived(
|
|
156
|
+
isItem
|
|
157
|
+
? `Step ${stepIndex + 1}: ${title}${isComplete ? ', completed' : isCurrent ? ', current' : isError ? ', error' : ''}${optional ? ', optional' : ''}`
|
|
158
|
+
: undefined,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
function handleClick() {
|
|
162
|
+
if (!canClick || !isItem) return;
|
|
163
|
+
parentContext.navigate(stepIndex);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
167
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
handleClick();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ------------------------------------------------------------------ */
|
|
174
|
+
/* Size mapping */
|
|
175
|
+
/* ------------------------------------------------------------------ */
|
|
176
|
+
const CIRCLE_SIZES: Record<string, number> = {
|
|
177
|
+
'0': 24,
|
|
178
|
+
'1': 32,
|
|
179
|
+
'2': 40,
|
|
180
|
+
'3': 48,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const FONT_SIZES: Record<string, string> = {
|
|
184
|
+
'0': '0.625rem',
|
|
185
|
+
'1': '0.75rem',
|
|
186
|
+
'2': '0.875rem',
|
|
187
|
+
'3': '1rem',
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const resolvedCircleSize = $derived(
|
|
191
|
+
isItem ? (CIRCLE_SIZES[parentContext.size] ?? 32) : (CIRCLE_SIZES[size] ?? 32),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const resolvedFontSize = $derived(
|
|
195
|
+
isItem
|
|
196
|
+
? (FONT_SIZES[parentContext.size] ?? '0.75rem')
|
|
197
|
+
: (FONT_SIZES[size] ?? '0.75rem'),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const resolvedOrientation = $derived(isItem ? parentContext.orientation : orientation);
|
|
201
|
+
|
|
202
|
+
/* ------------------------------------------------------------------ */
|
|
203
|
+
/* Horizontal overflow (container only) */
|
|
204
|
+
/* */
|
|
205
|
+
/* When the row can't fit, it becomes a snap-scrollable strip with */
|
|
206
|
+
/* edge fade masks. Chevron pagers render only while overflowing and */
|
|
207
|
+
/* are shown only for fine pointers (CSS); touch users swipe. */
|
|
208
|
+
/* ------------------------------------------------------------------ */
|
|
209
|
+
let scroll_el = $state<HTMLElement | undefined>(undefined);
|
|
210
|
+
let overflowing = $state(false);
|
|
211
|
+
let canScrollPrev = $state(false);
|
|
212
|
+
let canScrollNext = $state(false);
|
|
213
|
+
|
|
214
|
+
function updateScrollState() {
|
|
215
|
+
if (!scroll_el) return;
|
|
216
|
+
// Small thresholds absorb sub-pixel scroll positions (fractional
|
|
217
|
+
// scrollLeft / device-pixel rounding) so the end states latch cleanly.
|
|
218
|
+
const { scrollLeft, clientWidth, scrollWidth } = scroll_el;
|
|
219
|
+
overflowing = scrollWidth > clientWidth + 1;
|
|
220
|
+
canScrollPrev = scrollLeft > 2;
|
|
221
|
+
canScrollNext = scrollLeft + clientWidth < scrollWidth - 2;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function scrollNext() {
|
|
225
|
+
scroll_el?.scrollBy({ left: scroll_el.clientWidth * 0.8, behavior: 'smooth' });
|
|
226
|
+
}
|
|
227
|
+
function scrollPrev() {
|
|
228
|
+
scroll_el?.scrollBy({ left: -scroll_el.clientWidth * 0.8, behavior: 'smooth' });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
$effect(() => {
|
|
232
|
+
if (isItem || orientation === 'vertical' || !scroll_el) return;
|
|
233
|
+
const el = scroll_el;
|
|
234
|
+
updateScrollState();
|
|
235
|
+
const onScroll = () => updateScrollState();
|
|
236
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
237
|
+
const ro = new ResizeObserver(updateScrollState);
|
|
238
|
+
ro.observe(el);
|
|
239
|
+
// Content width changes (steps mounting, labels reflowing) don't resize
|
|
240
|
+
// the scroller's own box, so watch the steps too.
|
|
241
|
+
for (const child of Array.from(el.children)) ro.observe(child);
|
|
242
|
+
return () => {
|
|
243
|
+
el.removeEventListener('scroll', onScroll);
|
|
244
|
+
ro.disconnect();
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Keep the active step in view: when `current` changes while overflowing,
|
|
249
|
+
// center it in the strip (instantly on first sync so the initial paint
|
|
250
|
+
// doesn't animate).
|
|
251
|
+
let scrollSynced = false;
|
|
252
|
+
$effect(() => {
|
|
253
|
+
if (isItem || !scroll_el) return;
|
|
254
|
+
void current;
|
|
255
|
+
if (!overflowing) return;
|
|
256
|
+
const target = scroll_el.querySelector<HTMLElement>('[aria-current="step"]');
|
|
257
|
+
if (!target) return;
|
|
258
|
+
const left = target.offsetLeft + target.offsetWidth / 2 - scroll_el.clientWidth / 2;
|
|
259
|
+
scroll_el.scrollTo({ left, behavior: scrollSynced ? 'smooth' : 'instant' });
|
|
260
|
+
scrollSynced = true;
|
|
261
|
+
});
|
|
262
|
+
</script>
|
|
263
|
+
|
|
264
|
+
{#if isItem}
|
|
265
|
+
<!-- Step item -->
|
|
266
|
+
<div
|
|
267
|
+
class={['step', stepState, class_name].filter(Boolean).join(' ')}
|
|
268
|
+
class:vertical={resolvedOrientation === 'vertical'}
|
|
269
|
+
class:horizontal={resolvedOrientation !== 'vertical'}
|
|
270
|
+
{id}
|
|
271
|
+
style:--circle-size="{resolvedCircleSize}px"
|
|
272
|
+
style:--step-font-size={resolvedFontSize}
|
|
273
|
+
aria-current={isCurrent ? 'step' : undefined}>
|
|
274
|
+
<div class="main">
|
|
275
|
+
{#if canClick}
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
class="circle"
|
|
279
|
+
aria-label={ariaLabel}
|
|
280
|
+
{@attach ripple({ zIndex: 1 })}
|
|
281
|
+
onclick={handleClick}
|
|
282
|
+
onkeydown={handleKeyDown}>
|
|
283
|
+
{#if isError}
|
|
284
|
+
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
|
|
285
|
+
<path
|
|
286
|
+
d="M6 18L18 6M6 6l12 12"
|
|
287
|
+
stroke="currentColor"
|
|
288
|
+
stroke-width="2.5"
|
|
289
|
+
stroke-linecap="round"
|
|
290
|
+
fill="none" />
|
|
291
|
+
</svg>
|
|
292
|
+
{:else if isComplete}
|
|
293
|
+
<svg
|
|
294
|
+
class="checkmark"
|
|
295
|
+
width="16"
|
|
296
|
+
height="16"
|
|
297
|
+
viewBox="0 0 24 24"
|
|
298
|
+
aria-hidden="true">
|
|
299
|
+
<path
|
|
300
|
+
d="M5 13l4 4L19 7"
|
|
301
|
+
stroke="currentColor"
|
|
302
|
+
stroke-width="2.5"
|
|
303
|
+
stroke-linecap="round"
|
|
304
|
+
stroke-linejoin="round"
|
|
305
|
+
fill="none" />
|
|
306
|
+
</svg>
|
|
307
|
+
{:else}
|
|
308
|
+
<span class="number">{stepIndex + 1}</span>
|
|
309
|
+
{/if}
|
|
310
|
+
</button>
|
|
311
|
+
{:else}
|
|
312
|
+
<span class="circle" role="img" aria-label={ariaLabel}>
|
|
313
|
+
{#if isError}
|
|
314
|
+
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
|
|
315
|
+
<path
|
|
316
|
+
d="M6 18L18 6M6 6l12 12"
|
|
317
|
+
stroke="currentColor"
|
|
318
|
+
stroke-width="2.5"
|
|
319
|
+
stroke-linecap="round"
|
|
320
|
+
fill="none" />
|
|
321
|
+
</svg>
|
|
322
|
+
{:else if isComplete}
|
|
323
|
+
<svg
|
|
324
|
+
class="checkmark"
|
|
325
|
+
width="16"
|
|
326
|
+
height="16"
|
|
327
|
+
viewBox="0 0 24 24"
|
|
328
|
+
aria-hidden="true">
|
|
329
|
+
<path
|
|
330
|
+
d="M5 13l4 4L19 7"
|
|
331
|
+
stroke="currentColor"
|
|
332
|
+
stroke-width="2.5"
|
|
333
|
+
stroke-linecap="round"
|
|
334
|
+
stroke-linejoin="round"
|
|
335
|
+
fill="none" />
|
|
336
|
+
</svg>
|
|
337
|
+
{:else}
|
|
338
|
+
<span class="number">{stepIndex + 1}</span>
|
|
339
|
+
{/if}
|
|
340
|
+
</span>
|
|
341
|
+
{/if}
|
|
342
|
+
|
|
343
|
+
<div class="label">
|
|
344
|
+
<span class="title">{title}</span>
|
|
345
|
+
{#if description}
|
|
346
|
+
<span class="description">{description}</span>
|
|
347
|
+
{/if}
|
|
348
|
+
{#if optional}
|
|
349
|
+
<span class="optional">Optional</span>
|
|
350
|
+
{/if}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{#if !isLast}
|
|
355
|
+
<div class="connector">
|
|
356
|
+
<div class="fill" class:filled={connectorFilled}></div>
|
|
357
|
+
</div>
|
|
358
|
+
{/if}
|
|
359
|
+
|
|
360
|
+
{#if children}
|
|
361
|
+
<!-- Expand animates the content open/closed so the surrounding steps
|
|
362
|
+
reflow smoothly when the active step changes. Hidden content stays
|
|
363
|
+
mounted but inert (Expand handles that). -->
|
|
364
|
+
<Expand show={isCurrent} style="width: 100%">
|
|
365
|
+
<div class="content">
|
|
366
|
+
{@render children()}
|
|
367
|
+
</div>
|
|
368
|
+
</Expand>
|
|
369
|
+
{/if}
|
|
370
|
+
</div>
|
|
371
|
+
{:else if skeleton}
|
|
372
|
+
<!-- Skeleton -->
|
|
373
|
+
<div
|
|
374
|
+
class={['steps skeleton', class_name].filter(Boolean).join(' ')}
|
|
375
|
+
class:vertical={orientation === 'vertical'}
|
|
376
|
+
class:horizontal={orientation !== 'vertical'}
|
|
377
|
+
{id}
|
|
378
|
+
style:--circle-size="{CIRCLE_SIZES[size] ?? 32}px"
|
|
379
|
+
style:--step-font-size={FONT_SIZES[size] ?? '0.75rem'}
|
|
380
|
+
aria-hidden="true">
|
|
381
|
+
{#each { length: skeleton_count } as _, i}
|
|
382
|
+
<!-- Reuses the real .step/.main/.label/.connector layout
|
|
383
|
+
classes so every placeholder sits exactly where the real step will. -->
|
|
384
|
+
<div
|
|
385
|
+
class="step"
|
|
386
|
+
class:vertical={orientation === 'vertical'}
|
|
387
|
+
class:horizontal={orientation !== 'vertical'}
|
|
388
|
+
style:--shimmer-delay="{i * 120}ms">
|
|
389
|
+
<div class="main">
|
|
390
|
+
<div class="skeleton-circle"></div>
|
|
391
|
+
<div class="label">
|
|
392
|
+
<div class="skeleton-bar skeleton-title"></div>
|
|
393
|
+
<div class="skeleton-bar skeleton-desc"></div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
{#if i < skeleton_count - 1}
|
|
397
|
+
<div class="connector"></div>
|
|
398
|
+
{/if}
|
|
399
|
+
</div>
|
|
400
|
+
{/each}
|
|
401
|
+
</div>
|
|
402
|
+
{:else}
|
|
403
|
+
<!-- Steps container -->
|
|
404
|
+
<div
|
|
405
|
+
class="wrap"
|
|
406
|
+
class:vertical={orientation === 'vertical'}
|
|
407
|
+
class:horizontal={orientation !== 'vertical'}
|
|
408
|
+
style:--circle-size="{CIRCLE_SIZES[size] ?? 32}px">
|
|
409
|
+
{#if orientation !== 'vertical' && overflowing}
|
|
410
|
+
<Button
|
|
411
|
+
icon
|
|
412
|
+
translucent
|
|
413
|
+
size="00"
|
|
414
|
+
class="steps-nav steps-nav-prev"
|
|
415
|
+
aria-label="Scroll to previous steps"
|
|
416
|
+
disabled={!canScrollPrev}
|
|
417
|
+
disable_ripple={!canScrollPrev}
|
|
418
|
+
onclick={scrollPrev}>
|
|
419
|
+
<svg
|
|
420
|
+
viewBox="0 0 24 24"
|
|
421
|
+
fill="none"
|
|
422
|
+
stroke="currentColor"
|
|
423
|
+
stroke-width="2"
|
|
424
|
+
stroke-linecap="round"
|
|
425
|
+
stroke-linejoin="round"
|
|
426
|
+
aria-hidden="true">
|
|
427
|
+
<polyline points="15 18 9 12 15 6" />
|
|
428
|
+
</svg>
|
|
429
|
+
</Button>
|
|
430
|
+
{/if}
|
|
431
|
+
<div
|
|
432
|
+
bind:this={scroll_el}
|
|
433
|
+
class={['steps', class_name].filter(Boolean).join(' ')}
|
|
434
|
+
class:vertical={orientation === 'vertical'}
|
|
435
|
+
class:horizontal={orientation !== 'vertical'}
|
|
436
|
+
{id}
|
|
437
|
+
role="group"
|
|
438
|
+
aria-label="Progress"
|
|
439
|
+
style:--fade-l={canScrollPrev ? '3rem' : '0px'}
|
|
440
|
+
style:--fade-r={canScrollNext ? '3rem' : '0px'}>
|
|
441
|
+
{@render children?.()}
|
|
442
|
+
</div>
|
|
443
|
+
{#if orientation !== 'vertical' && overflowing}
|
|
444
|
+
<Button
|
|
445
|
+
icon
|
|
446
|
+
translucent
|
|
447
|
+
size="00"
|
|
448
|
+
class="steps-nav steps-nav-next"
|
|
449
|
+
aria-label="Scroll to next steps"
|
|
450
|
+
disabled={!canScrollNext}
|
|
451
|
+
disable_ripple={!canScrollNext}
|
|
452
|
+
onclick={scrollNext}>
|
|
453
|
+
<svg
|
|
454
|
+
viewBox="0 0 24 24"
|
|
455
|
+
fill="none"
|
|
456
|
+
stroke="currentColor"
|
|
457
|
+
stroke-width="2"
|
|
458
|
+
stroke-linecap="round"
|
|
459
|
+
stroke-linejoin="round"
|
|
460
|
+
aria-hidden="true">
|
|
461
|
+
<polyline points="9 18 15 12 9 6" />
|
|
462
|
+
</svg>
|
|
463
|
+
</Button>
|
|
464
|
+
{/if}
|
|
465
|
+
</div>
|
|
466
|
+
{/if}
|
|
467
|
+
|
|
468
|
+
<style>
|
|
469
|
+
/* ========== Steps Container ========== */
|
|
470
|
+
.wrap {
|
|
471
|
+
position: relative;
|
|
472
|
+
width: 100%;
|
|
473
|
+
|
|
474
|
+
/* Vertical (and SSR'd) layouts need no overflow chrome — the wrapper
|
|
475
|
+
disappears from layout entirely. */
|
|
476
|
+
&.vertical {
|
|
477
|
+
display: contents;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* Chevron pagers: <Button icon> instances positioned over the strip's
|
|
481
|
+
faded edges, vertically centered on the STEP CIRCLE (not the whole
|
|
482
|
+
step, which is taller because of the label below). The scroll strip
|
|
483
|
+
adds 0.5rem of top padding (halo breathing room), so the circle's
|
|
484
|
+
center sits at 0.5rem + half the circle — match that exactly.
|
|
485
|
+
Touch devices swipe instead, so they only display for fine pointers. */
|
|
486
|
+
:global(.steps-nav) {
|
|
487
|
+
display: none;
|
|
488
|
+
position: absolute;
|
|
489
|
+
top: calc(0.5rem + var(--circle-size, 32px) / 2);
|
|
490
|
+
translate: 0 -50%;
|
|
491
|
+
z-index: 2;
|
|
492
|
+
/* Fade out when there's nothing more to scroll in that direction —
|
|
493
|
+
a clear "you're at the end" cue, not just a dead button. */
|
|
494
|
+
transition:
|
|
495
|
+
opacity 200ms ease,
|
|
496
|
+
visibility 200ms ease;
|
|
497
|
+
box-shadow:
|
|
498
|
+
0 2px 6px rgba(0, 0, 0, 0.12),
|
|
499
|
+
0 1px 2px rgba(0, 0, 0, 0.08);
|
|
500
|
+
|
|
501
|
+
/* Button puts `disabled` on its inner <button>, so reach it via :has().
|
|
502
|
+
Muting opacity + dropping the shadow reads as "nothing more this
|
|
503
|
+
way" — distinct from an active, liftable pager. */
|
|
504
|
+
&:has(button:disabled) {
|
|
505
|
+
opacity: 0.32;
|
|
506
|
+
box-shadow: none;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
:global(.steps-nav-prev) {
|
|
510
|
+
left: -0.75rem;
|
|
511
|
+
}
|
|
512
|
+
:global(.steps-nav-next) {
|
|
513
|
+
right: -0.75rem;
|
|
514
|
+
}
|
|
515
|
+
@media (hover: hover) and (pointer: fine) {
|
|
516
|
+
:global(.steps-nav) {
|
|
517
|
+
display: inline-flex;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.steps {
|
|
523
|
+
display: flex;
|
|
524
|
+
align-items: flex-start;
|
|
525
|
+
width: 100%;
|
|
526
|
+
|
|
527
|
+
&.vertical {
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
&.horizontal {
|
|
532
|
+
position: relative;
|
|
533
|
+
overflow-x: auto;
|
|
534
|
+
overscroll-behavior-x: contain;
|
|
535
|
+
scroll-snap-type: x proximity;
|
|
536
|
+
-webkit-overflow-scrolling: touch;
|
|
537
|
+
/* Hide the native scrollbar — overflow is paged via the chevrons on
|
|
538
|
+
desktop and swiped on touch (same pattern as Timeline). */
|
|
539
|
+
scrollbar-width: none;
|
|
540
|
+
/* Breathing room for the current-step halo / focus ring that the
|
|
541
|
+
scroll clip would otherwise crop; the negative margin cancels it
|
|
542
|
+
so nothing shifts when the steps fit. */
|
|
543
|
+
padding-block: 0.5rem;
|
|
544
|
+
margin-block: -0.5rem;
|
|
545
|
+
/* Edge fades hint at clipped content; 0px fades are a no-op mask. */
|
|
546
|
+
mask-image: linear-gradient(
|
|
547
|
+
to right,
|
|
548
|
+
transparent 0,
|
|
549
|
+
black var(--fade-l, 0px),
|
|
550
|
+
black calc(100% - var(--fade-r, 0px)),
|
|
551
|
+
transparent 100%
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
&.horizontal::-webkit-scrollbar {
|
|
555
|
+
display: none;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* ========== Step Item ========== */
|
|
560
|
+
.step {
|
|
561
|
+
display: flex;
|
|
562
|
+
position: relative;
|
|
563
|
+
|
|
564
|
+
&.horizontal {
|
|
565
|
+
flex-direction: column;
|
|
566
|
+
align-items: center;
|
|
567
|
+
flex: 1;
|
|
568
|
+
/* The readability floor: steps refuse to crush below this, which is
|
|
569
|
+
what tips a crowded row into the scrollable overflow strip. */
|
|
570
|
+
min-width: var(--step-min-width, 7rem);
|
|
571
|
+
scroll-snap-align: center;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
&.vertical {
|
|
575
|
+
flex-direction: column;
|
|
576
|
+
align-items: flex-start;
|
|
577
|
+
flex: none;
|
|
578
|
+
|
|
579
|
+
/* Inter-step spacing (the connector is absolutely positioned so it
|
|
580
|
+
can run continuously past expanded step content). */
|
|
581
|
+
&:not(:last-child) {
|
|
582
|
+
padding-bottom: 2rem;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.main {
|
|
588
|
+
display: flex;
|
|
589
|
+
gap: 0.625rem;
|
|
590
|
+
/* Press feedback for clickable steps: the indicator + label wrapper
|
|
591
|
+
squeezes toward its OWN center (not the row's), like Button's press. */
|
|
592
|
+
transform-origin: center;
|
|
593
|
+
transition: scale 150ms ease;
|
|
594
|
+
|
|
595
|
+
&:has(button.circle:active) {
|
|
596
|
+
scale: 0.95;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.step.horizontal & {
|
|
600
|
+
flex-direction: column;
|
|
601
|
+
align-items: center;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.step.vertical & {
|
|
605
|
+
flex-direction: row;
|
|
606
|
+
align-items: flex-start;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* ========== Circle ========== */
|
|
611
|
+
.circle {
|
|
612
|
+
width: var(--circle-size);
|
|
613
|
+
height: var(--circle-size);
|
|
614
|
+
min-width: var(--circle-size);
|
|
615
|
+
min-height: var(--circle-size);
|
|
616
|
+
border-radius: 9999px;
|
|
617
|
+
display: flex;
|
|
618
|
+
align-items: center;
|
|
619
|
+
justify-content: center;
|
|
620
|
+
font-weight: 500;
|
|
621
|
+
font-size: var(--step-font-size);
|
|
622
|
+
border: 2px solid
|
|
623
|
+
light-dark(var(--color-border, #d1d5db), var(--color-border, #4b5563));
|
|
624
|
+
background: transparent;
|
|
625
|
+
color: light-dark(
|
|
626
|
+
var(--color-text-disabled, #9ca3af),
|
|
627
|
+
var(--color-text-disabled, #6b7280)
|
|
628
|
+
);
|
|
629
|
+
position: relative;
|
|
630
|
+
padding: 0;
|
|
631
|
+
cursor: default;
|
|
632
|
+
outline: none;
|
|
633
|
+
transition:
|
|
634
|
+
background-color 300ms ease,
|
|
635
|
+
border-color 300ms ease,
|
|
636
|
+
color 300ms ease,
|
|
637
|
+
box-shadow 300ms ease;
|
|
638
|
+
|
|
639
|
+
svg {
|
|
640
|
+
width: 55%;
|
|
641
|
+
height: 55%;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.step.complete & {
|
|
645
|
+
background: var(--color-success, #16a34a);
|
|
646
|
+
border-color: var(--color-success, #16a34a);
|
|
647
|
+
color: white;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* "You are here" is neutral on purpose: a surface-filled circle with a
|
|
651
|
+
strong text-colored ring and a soft breathing halo. It reads clearly
|
|
652
|
+
without adding a hue that clashes with the success/error states. */
|
|
653
|
+
.step.current & {
|
|
654
|
+
background: var(--color-surface, light-dark(#ffffff, #111111));
|
|
655
|
+
border-color: var(--color-text, light-dark(#1a1a1a, #f5f5f5));
|
|
656
|
+
color: var(--color-text, light-dark(#1a1a1a, #f5f5f5));
|
|
657
|
+
font-weight: 600;
|
|
658
|
+
animation: steps-pulse 2.4s ease-in-out infinite;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.step.error & {
|
|
662
|
+
background: var(--color-error, #dc2626);
|
|
663
|
+
border-color: var(--color-error, #dc2626);
|
|
664
|
+
color: white;
|
|
665
|
+
animation: steps-shake 400ms ease;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
button.circle {
|
|
670
|
+
cursor: pointer;
|
|
671
|
+
|
|
672
|
+
/* Hover colors snap IN instantly (the :hover rule owns the in-transition
|
|
673
|
+
and omits color properties) and ease OUT via the base .circle
|
|
674
|
+
transition — the library's "instant in, slow out" pattern. */
|
|
675
|
+
&:hover {
|
|
676
|
+
transition: none;
|
|
677
|
+
}
|
|
678
|
+
.step.complete &:hover {
|
|
679
|
+
background: var(--color-success-active, #128b7e);
|
|
680
|
+
border-color: var(--color-success-active, #128b7e);
|
|
681
|
+
}
|
|
682
|
+
.step.error &:hover {
|
|
683
|
+
background: var(--color-error-active, light-dark(#ca3030, #f55d5d));
|
|
684
|
+
border-color: var(--color-error-active, light-dark(#ca3030, #f55d5d));
|
|
685
|
+
}
|
|
686
|
+
.step.upcoming &:hover,
|
|
687
|
+
.step.current &:hover {
|
|
688
|
+
background: rgb(from var(--color-text, #888888) r g b / 0.08);
|
|
689
|
+
border-color: var(--color-text, light-dark(#1a1a1a, #f5f5f5));
|
|
690
|
+
color: var(--color-text, light-dark(#1a1a1a, #f5f5f5));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
&:focus-visible {
|
|
694
|
+
outline: 2px solid var(--color-action, #2563eb);
|
|
695
|
+
outline-offset: 2px;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/* ========== Checkmark draw-in ========== */
|
|
700
|
+
.step.complete .checkmark path {
|
|
701
|
+
stroke-dasharray: 30;
|
|
702
|
+
stroke-dashoffset: 30;
|
|
703
|
+
animation: steps-check-draw 400ms ease forwards;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* ========== Step Number ========== */
|
|
707
|
+
.number {
|
|
708
|
+
line-height: 1;
|
|
709
|
+
user-select: none;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/* ========== Labels ========== */
|
|
713
|
+
.label {
|
|
714
|
+
display: flex;
|
|
715
|
+
flex-direction: column;
|
|
716
|
+
gap: 0.125rem;
|
|
717
|
+
|
|
718
|
+
.step.horizontal & {
|
|
719
|
+
align-items: center;
|
|
720
|
+
text-align: center;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.step.vertical & {
|
|
724
|
+
align-items: flex-start;
|
|
725
|
+
text-align: left;
|
|
726
|
+
padding-top: 0.25rem;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.title {
|
|
731
|
+
font-weight: 500;
|
|
732
|
+
font-size: var(--step-font-size);
|
|
733
|
+
color: light-dark(var(--color-text, #1a1a1a), var(--color-text, #f5f5f5));
|
|
734
|
+
line-height: 1.3;
|
|
735
|
+
|
|
736
|
+
.step.upcoming & {
|
|
737
|
+
color: light-dark(
|
|
738
|
+
var(--color-text-disabled, #9ca3af),
|
|
739
|
+
var(--color-text-disabled, #6b7280)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/* Bolder label reinforces the neutral "current" indicator. */
|
|
744
|
+
.step.current & {
|
|
745
|
+
font-weight: 600;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.description {
|
|
750
|
+
font-size: calc(var(--step-font-size) * 0.85);
|
|
751
|
+
color: light-dark(
|
|
752
|
+
var(--color-text-disabled, #9ca3af),
|
|
753
|
+
var(--color-text-disabled, #6b7280)
|
|
754
|
+
);
|
|
755
|
+
line-height: 1.3;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.optional {
|
|
759
|
+
font-size: calc(var(--step-font-size) * 0.8);
|
|
760
|
+
font-style: italic;
|
|
761
|
+
color: light-dark(
|
|
762
|
+
var(--color-text-disabled, #9ca3af),
|
|
763
|
+
var(--color-text-disabled, #6b7280)
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* ========== Connector ========== */
|
|
768
|
+
.connector {
|
|
769
|
+
position: relative;
|
|
770
|
+
overflow: hidden;
|
|
771
|
+
|
|
772
|
+
.step.horizontal & {
|
|
773
|
+
width: 100%;
|
|
774
|
+
height: 2px;
|
|
775
|
+
position: absolute;
|
|
776
|
+
top: calc(var(--circle-size) / 2);
|
|
777
|
+
left: calc(50% + var(--circle-size) / 2 + 4px);
|
|
778
|
+
right: calc(-50% + var(--circle-size) / 2 + 4px);
|
|
779
|
+
width: calc(100% - var(--circle-size) - 8px);
|
|
780
|
+
background: light-dark(var(--color-border, #d1d5db), var(--color-border, #4b5563));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Absolutely positioned so the line spans the step's FULL height —
|
|
784
|
+
including expanded wizard content — keeping it visually continuous
|
|
785
|
+
from this circle down to the next one. (The step's padding-bottom
|
|
786
|
+
provides the minimum run between collapsed steps.) */
|
|
787
|
+
.step.vertical & {
|
|
788
|
+
position: absolute;
|
|
789
|
+
top: calc(var(--circle-size) + 0.25rem);
|
|
790
|
+
bottom: 0.25rem;
|
|
791
|
+
left: calc(var(--circle-size) / 2 - 1px);
|
|
792
|
+
width: 2px;
|
|
793
|
+
background: light-dark(var(--color-border, #d1d5db), var(--color-border, #4b5563));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.fill {
|
|
798
|
+
position: absolute;
|
|
799
|
+
inset: 0;
|
|
800
|
+
background: var(--color-success, #16a34a);
|
|
801
|
+
transform-origin: left;
|
|
802
|
+
transform: scaleX(0);
|
|
803
|
+
transition: transform 300ms ease;
|
|
804
|
+
|
|
805
|
+
.step.vertical & {
|
|
806
|
+
transform-origin: top;
|
|
807
|
+
transform: scaleY(0);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
&.filled {
|
|
811
|
+
transform: scaleX(1);
|
|
812
|
+
|
|
813
|
+
.step.vertical & {
|
|
814
|
+
transform: scaleY(1);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* ========== Step Content (wizard mode) ========== */
|
|
820
|
+
.content {
|
|
821
|
+
padding: 0.75rem 0;
|
|
822
|
+
width: 100%;
|
|
823
|
+
|
|
824
|
+
.step.vertical & {
|
|
825
|
+
padding-left: calc(var(--circle-size) + 0.625rem);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/* ========== Skeleton ========== */
|
|
830
|
+
/* Layout comes from the shared .step/.main/.label/.connector
|
|
831
|
+
rules (the placeholder markup reuses those classes), so only the shimmer
|
|
832
|
+
shapes are defined here. The connector renders bare: its border-color
|
|
833
|
+
background already matches an upcoming step's unfilled track. */
|
|
834
|
+
.steps.skeleton {
|
|
835
|
+
pointer-events: none;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.skeleton-circle,
|
|
839
|
+
.skeleton-bar {
|
|
840
|
+
position: relative;
|
|
841
|
+
overflow: hidden;
|
|
842
|
+
background: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
|
|
843
|
+
|
|
844
|
+
&::after {
|
|
845
|
+
content: '';
|
|
846
|
+
position: absolute;
|
|
847
|
+
inset: 0;
|
|
848
|
+
transform: translateX(-100%);
|
|
849
|
+
background-image: linear-gradient(
|
|
850
|
+
105deg,
|
|
851
|
+
transparent 25%,
|
|
852
|
+
var(--skeleton-sheen, rgb(from var(--color-text, #888) r g b / 0.12)) 50%,
|
|
853
|
+
transparent 75%
|
|
854
|
+
);
|
|
855
|
+
animation: delight-skeleton-shimmer var(--skeleton-duration, 2.4s) ease-in-out
|
|
856
|
+
infinite;
|
|
857
|
+
animation-delay: var(--shimmer-delay, 0s);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/* Matches .circle's outer box exactly (border-box, so the real 2px
|
|
862
|
+
border is already inside --circle-size). */
|
|
863
|
+
.skeleton-circle {
|
|
864
|
+
width: var(--circle-size);
|
|
865
|
+
height: var(--circle-size);
|
|
866
|
+
min-width: var(--circle-size);
|
|
867
|
+
min-height: var(--circle-size);
|
|
868
|
+
border-radius: 9999px;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/* Text pills in the label's own font scale; block margins pad each 0.7em
|
|
872
|
+
bar out to the real 1.3 line box so the label column height matches. */
|
|
873
|
+
.skeleton-bar {
|
|
874
|
+
height: 0.7em;
|
|
875
|
+
border-radius: var(--radius-full, 1e5px);
|
|
876
|
+
|
|
877
|
+
&.skeleton-title {
|
|
878
|
+
font-size: var(--step-font-size);
|
|
879
|
+
width: 4.5em;
|
|
880
|
+
margin-block: 0.3em;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
&.skeleton-desc {
|
|
884
|
+
font-size: calc(var(--step-font-size) * 0.85);
|
|
885
|
+
width: 4em;
|
|
886
|
+
margin-block: 0.3em;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/* ========== Animations ========== */
|
|
891
|
+
/* Neutral breathing halo for the current step — derived from the text
|
|
892
|
+
color so it stays hue-free in both light and dark mode. */
|
|
893
|
+
@keyframes steps-pulse {
|
|
894
|
+
0%,
|
|
895
|
+
100% {
|
|
896
|
+
box-shadow: 0 0 0 3px rgb(from var(--color-text, #888888) r g b / 0.18);
|
|
897
|
+
}
|
|
898
|
+
50% {
|
|
899
|
+
box-shadow: 0 0 0 7px rgb(from var(--color-text, #888888) r g b / 0.05);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
@keyframes steps-shake {
|
|
904
|
+
0%,
|
|
905
|
+
100% {
|
|
906
|
+
transform: translateX(0);
|
|
907
|
+
}
|
|
908
|
+
20% {
|
|
909
|
+
transform: translateX(-3px);
|
|
910
|
+
}
|
|
911
|
+
40% {
|
|
912
|
+
transform: translateX(3px);
|
|
913
|
+
}
|
|
914
|
+
60% {
|
|
915
|
+
transform: translateX(-2px);
|
|
916
|
+
}
|
|
917
|
+
80% {
|
|
918
|
+
transform: translateX(2px);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
@keyframes steps-check-draw {
|
|
923
|
+
to {
|
|
924
|
+
stroke-dashoffset: 0;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
@keyframes -global-delight-skeleton-shimmer {
|
|
929
|
+
0% {
|
|
930
|
+
transform: translateX(-100%);
|
|
931
|
+
}
|
|
932
|
+
55%,
|
|
933
|
+
100% {
|
|
934
|
+
transform: translateX(100%);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
@media (prefers-reduced-motion: reduce) {
|
|
939
|
+
.step.current .circle {
|
|
940
|
+
animation: none;
|
|
941
|
+
/* Keep a static halo so "current" stays identifiable without motion. */
|
|
942
|
+
box-shadow: 0 0 0 3px rgb(from var(--color-text, #888888) r g b / 0.18);
|
|
943
|
+
}
|
|
944
|
+
.main {
|
|
945
|
+
transition: none;
|
|
946
|
+
}
|
|
947
|
+
.main:has(button.circle:active) {
|
|
948
|
+
scale: none;
|
|
949
|
+
}
|
|
950
|
+
.step.error .circle {
|
|
951
|
+
animation: none;
|
|
952
|
+
}
|
|
953
|
+
.step.complete .checkmark path {
|
|
954
|
+
animation: none;
|
|
955
|
+
stroke-dashoffset: 0;
|
|
956
|
+
}
|
|
957
|
+
.skeleton-circle::after,
|
|
958
|
+
.skeleton-bar::after {
|
|
959
|
+
animation: none;
|
|
960
|
+
}
|
|
961
|
+
.fill {
|
|
962
|
+
transition: none;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
</style>
|