@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,1258 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export { default as TimelineItem } from './Timeline.svelte';
|
|
3
|
+
|
|
4
|
+
export interface TimelineContext {
|
|
5
|
+
/** Whether the timeline flows horizontally instead of vertically */
|
|
6
|
+
horizontal: boolean;
|
|
7
|
+
/** Whether items alternate sides of the timeline axis */
|
|
8
|
+
alternate: boolean;
|
|
9
|
+
/** Whether the timeline uses dense (compact) spacing */
|
|
10
|
+
dense: boolean;
|
|
11
|
+
/** Whether the timeline uses comfortable (roomy) spacing */
|
|
12
|
+
comfortable: boolean;
|
|
13
|
+
/** Whether continuous motion (the active pulse) is allowed */
|
|
14
|
+
animate: boolean;
|
|
15
|
+
/** Whether items should play their entrance reveal as they scroll in */
|
|
16
|
+
reveal: boolean;
|
|
17
|
+
/** Registers a new item with the timeline and returns its index */
|
|
18
|
+
register: () => number;
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import { intersectionObserver, ripple } from '@delightstack/utilities';
|
|
24
|
+
import { scrollbar } from '../actions/scrollbar';
|
|
25
|
+
import { getContext, setContext, type Component, type Snippet } from 'svelte';
|
|
26
|
+
import { fade, type TransitionConfig } from 'svelte/transition';
|
|
27
|
+
import { backOut } from 'svelte/easing';
|
|
28
|
+
import Button from './../actions/Button.svelte';
|
|
29
|
+
import Progress from '../feedback/Progress.svelte';
|
|
30
|
+
|
|
31
|
+
const propId = $props.id();
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
/* --- TimelineItem props --- */
|
|
35
|
+
/** Timestamp for this event */
|
|
36
|
+
date = undefined as Date | string | undefined,
|
|
37
|
+
|
|
38
|
+
/** Event title */
|
|
39
|
+
title = '',
|
|
40
|
+
|
|
41
|
+
/** Marker icon component */
|
|
42
|
+
icon = undefined as Component | undefined,
|
|
43
|
+
|
|
44
|
+
/** Marker color override */
|
|
45
|
+
color = '' as string,
|
|
46
|
+
|
|
47
|
+
/** Event status */
|
|
48
|
+
status = undefined as 'complete' | 'active' | 'pending' | undefined,
|
|
49
|
+
|
|
50
|
+
/** Makes the item clickable — turns it into a link. Mirrors `<Button>`. */
|
|
51
|
+
href = undefined as string | undefined,
|
|
52
|
+
|
|
53
|
+
/** Link target (only used with `href`). */
|
|
54
|
+
target = undefined as '_self' | '_blank' | '_parent' | '_top' | undefined,
|
|
55
|
+
|
|
56
|
+
/** Called when the item is clicked. Makes the item interactive (like
|
|
57
|
+
`href`). Return a promise to drive a loading spinner in the marker:
|
|
58
|
+
a spinner appears if the work outlasts ~100ms, then a brief success
|
|
59
|
+
check confirms a resolve (mirrors `<Button>`). */
|
|
60
|
+
onclick = undefined as
|
|
61
|
+
| undefined
|
|
62
|
+
| ((event: MouseEvent | KeyboardEvent) => void)
|
|
63
|
+
| ((event: MouseEvent | KeyboardEvent) => Promise<void>),
|
|
64
|
+
|
|
65
|
+
/* --- Timeline container props --- */
|
|
66
|
+
/** Horizontal layout */
|
|
67
|
+
horizontal = false,
|
|
68
|
+
|
|
69
|
+
/** Alternate sides */
|
|
70
|
+
alternate = false,
|
|
71
|
+
|
|
72
|
+
/** Show pending indicator at end */
|
|
73
|
+
pending = false,
|
|
74
|
+
|
|
75
|
+
/** Compact spacing */
|
|
76
|
+
dense = false,
|
|
77
|
+
|
|
78
|
+
/** Relaxed spacing */
|
|
79
|
+
comfortable = false,
|
|
80
|
+
|
|
81
|
+
/** Play the entrance + pulse animations. On by default. */
|
|
82
|
+
animate = true,
|
|
83
|
+
|
|
84
|
+
/** Loading skeleton */
|
|
85
|
+
skeleton = false,
|
|
86
|
+
|
|
87
|
+
/** Skeleton items count */
|
|
88
|
+
skeleton_count = 3,
|
|
89
|
+
|
|
90
|
+
/** Element ID */
|
|
91
|
+
id = propId,
|
|
92
|
+
|
|
93
|
+
/** Additional CSS classes */
|
|
94
|
+
class: class_name = '',
|
|
95
|
+
|
|
96
|
+
/** Child content snippet */
|
|
97
|
+
children = undefined as undefined | Snippet,
|
|
98
|
+
|
|
99
|
+
/** On-demand loading */
|
|
100
|
+
onloadmore = undefined as (() => void | Promise<void>) | undefined,
|
|
101
|
+
} = $props();
|
|
102
|
+
|
|
103
|
+
/* ------------------------------------------------------------------ */
|
|
104
|
+
/* Determine whether this instance is a container or an item */
|
|
105
|
+
/* ------------------------------------------------------------------ */
|
|
106
|
+
const parentContext = getContext<TimelineContext | undefined>('timeline');
|
|
107
|
+
const isItem = !!parentContext;
|
|
108
|
+
|
|
109
|
+
/* ------------------------------------------------------------------ */
|
|
110
|
+
/* Timeline container behaviour */
|
|
111
|
+
/* ------------------------------------------------------------------ */
|
|
112
|
+
let item_counter = 0;
|
|
113
|
+
|
|
114
|
+
if (!isItem) {
|
|
115
|
+
// Once a skeleton has been shown, the real content takes its place — the
|
|
116
|
+
// space was already occupied, so re-animating it in reads as a jump. Latch
|
|
117
|
+
// this and suppress the entrance reveal for everything that loads after.
|
|
118
|
+
let had_skeleton = $state(skeleton);
|
|
119
|
+
|
|
120
|
+
const ctx = $state<TimelineContext>({
|
|
121
|
+
horizontal,
|
|
122
|
+
alternate,
|
|
123
|
+
dense,
|
|
124
|
+
comfortable,
|
|
125
|
+
animate,
|
|
126
|
+
reveal: animate && !skeleton,
|
|
127
|
+
register() {
|
|
128
|
+
return item_counter++;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
setContext<TimelineContext>('timeline', ctx);
|
|
132
|
+
|
|
133
|
+
$effect(() => {
|
|
134
|
+
if (skeleton) had_skeleton = true;
|
|
135
|
+
ctx.horizontal = horizontal;
|
|
136
|
+
ctx.alternate = alternate;
|
|
137
|
+
ctx.dense = dense;
|
|
138
|
+
ctx.comfortable = comfortable;
|
|
139
|
+
ctx.animate = animate;
|
|
140
|
+
ctx.reveal = animate && !had_skeleton;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ------------------------------------------------------------------ */
|
|
145
|
+
/* TimelineItem behaviour */
|
|
146
|
+
/* ------------------------------------------------------------------ */
|
|
147
|
+
const item_index = isItem ? parentContext.register() : -1;
|
|
148
|
+
|
|
149
|
+
const is_horizontal = $derived(isItem ? parentContext.horizontal : horizontal);
|
|
150
|
+
const is_alternate = $derived(isItem ? parentContext.alternate : alternate);
|
|
151
|
+
const is_dense = $derived(isItem ? parentContext.dense : dense);
|
|
152
|
+
const is_comfortable = $derived(isItem ? parentContext.comfortable : comfortable);
|
|
153
|
+
const is_even = $derived(item_index % 2 === 0);
|
|
154
|
+
const do_reveal = $derived(isItem ? parentContext.reveal : false);
|
|
155
|
+
const do_motion = $derived(isItem ? parentContext.animate : false);
|
|
156
|
+
|
|
157
|
+
/** An item is interactive when it has somewhere to go or something to do. */
|
|
158
|
+
const interactive = $derived(isItem && (!!onclick || !!href));
|
|
159
|
+
|
|
160
|
+
/* ------------------------------------------------------------------ */
|
|
161
|
+
/* Promise-aware onclick (mirrors <Button>) */
|
|
162
|
+
/* A returned promise drives a spinner in the marker: it appears only */
|
|
163
|
+
/* if the work outlasts SHOW_DELAY (sub-100ms work reads as instant), */
|
|
164
|
+
/* stays at least MIN_VISIBLE so it can't blink, then a success check */
|
|
165
|
+
/* confirms a resolve. */
|
|
166
|
+
/* ------------------------------------------------------------------ */
|
|
167
|
+
const SHOW_DELAY = 100;
|
|
168
|
+
const MIN_VISIBLE = 1000;
|
|
169
|
+
const CHECK_HOLD = 1000;
|
|
170
|
+
|
|
171
|
+
let in_flight = $state(false); // a returned promise is running
|
|
172
|
+
let spinner_visible = $state(false); // the spinner is actually rendered
|
|
173
|
+
let check_visible = $state(false); // the success checkmark is rendered
|
|
174
|
+
let show_timer: ReturnType<typeof setTimeout> | undefined;
|
|
175
|
+
let hide_timer: ReturnType<typeof setTimeout> | undefined;
|
|
176
|
+
let check_timer: ReturnType<typeof setTimeout> | undefined;
|
|
177
|
+
let spinner_shown_at = 0;
|
|
178
|
+
|
|
179
|
+
function clearTimers() {
|
|
180
|
+
clearTimeout(show_timer);
|
|
181
|
+
clearTimeout(hide_timer);
|
|
182
|
+
clearTimeout(check_timer);
|
|
183
|
+
show_timer = hide_timer = check_timer = undefined;
|
|
184
|
+
}
|
|
185
|
+
$effect(() => clearTimers); // tear down pending timers on destroy
|
|
186
|
+
|
|
187
|
+
function flashCheck() {
|
|
188
|
+
clearTimeout(check_timer);
|
|
189
|
+
check_visible = true;
|
|
190
|
+
check_timer = setTimeout(() => (check_visible = false), CHECK_HOLD);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function settle(success: boolean) {
|
|
194
|
+
// Settled before the spinner appeared → treat as instant, no spinner.
|
|
195
|
+
if (show_timer) {
|
|
196
|
+
clearTimeout(show_timer);
|
|
197
|
+
show_timer = undefined;
|
|
198
|
+
in_flight = false;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Keep the spinner for the rest of its minimum-visible window so it
|
|
202
|
+
// doesn't blink away the instant the promise resolves.
|
|
203
|
+
const remaining = Math.max(0, MIN_VISIBLE - (performance.now() - spinner_shown_at));
|
|
204
|
+
clearTimeout(hide_timer);
|
|
205
|
+
hide_timer = setTimeout(() => {
|
|
206
|
+
spinner_visible = false;
|
|
207
|
+
in_flight = false;
|
|
208
|
+
if (success) flashCheck();
|
|
209
|
+
}, remaining);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handleActivate(event: MouseEvent | KeyboardEvent) {
|
|
213
|
+
if (in_flight) return;
|
|
214
|
+
const result = onclick?.(event);
|
|
215
|
+
if (!(result instanceof Promise)) return;
|
|
216
|
+
|
|
217
|
+
clearTimers();
|
|
218
|
+
check_visible = false;
|
|
219
|
+
in_flight = true;
|
|
220
|
+
// Hold off on the spinner — work that settles within SHOW_DELAY was
|
|
221
|
+
// effectively instant and never needs one.
|
|
222
|
+
show_timer = setTimeout(() => {
|
|
223
|
+
show_timer = undefined;
|
|
224
|
+
spinner_visible = true;
|
|
225
|
+
spinner_shown_at = performance.now();
|
|
226
|
+
}, SHOW_DELAY);
|
|
227
|
+
|
|
228
|
+
result.then(
|
|
229
|
+
() => settle(true),
|
|
230
|
+
() => settle(false),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleKey(event: KeyboardEvent) {
|
|
235
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
handleActivate(event);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The success check draws its stroke on as it spring-pops in.
|
|
242
|
+
function checkIn(_node: Element): TransitionConfig {
|
|
243
|
+
const reduce =
|
|
244
|
+
typeof matchMedia !== 'undefined' &&
|
|
245
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
246
|
+
return {
|
|
247
|
+
duration: reduce ? 0 : 440,
|
|
248
|
+
easing: backOut,
|
|
249
|
+
css: (t: number) =>
|
|
250
|
+
`transform: scale(${0.3 + 0.7 * t}); opacity: ${Math.min(1, t * 2)}; --check-draw: ${24 * (1 - t)};`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ------------------------------------------------------------------ */
|
|
255
|
+
/* Scroll-reveal for items */
|
|
256
|
+
/* ------------------------------------------------------------------ */
|
|
257
|
+
let visible = $state(false);
|
|
258
|
+
|
|
259
|
+
/* ------------------------------------------------------------------ */
|
|
260
|
+
/* Date formatting */
|
|
261
|
+
/* ------------------------------------------------------------------ */
|
|
262
|
+
const formatted_date = $derived.by(() => {
|
|
263
|
+
if (!date) return '';
|
|
264
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
265
|
+
if (isNaN(d.getTime())) return typeof date === 'string' ? date : '';
|
|
266
|
+
return d.toLocaleDateString(undefined, {
|
|
267
|
+
year: 'numeric',
|
|
268
|
+
month: 'short',
|
|
269
|
+
day: 'numeric',
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const iso_date = $derived.by(() => {
|
|
274
|
+
if (!date) return '';
|
|
275
|
+
const d = typeof date === 'string' ? new Date(date) : date;
|
|
276
|
+
if (isNaN(d.getTime())) return '';
|
|
277
|
+
return d.toISOString();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/* ------------------------------------------------------------------ */
|
|
281
|
+
/* Load-more sentinel */
|
|
282
|
+
/* ------------------------------------------------------------------ */
|
|
283
|
+
function handleLoadMore() {
|
|
284
|
+
onloadmore?.();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ------------------------------------------------------------------ */
|
|
288
|
+
/* Horizontal scroll: chevron next/prev buttons */
|
|
289
|
+
/* ------------------------------------------------------------------ */
|
|
290
|
+
let scroll_el = $state<HTMLElement | undefined>(undefined);
|
|
291
|
+
let can_scroll_prev = $state(false);
|
|
292
|
+
let can_scroll_next = $state(false);
|
|
293
|
+
|
|
294
|
+
function updateScrollState() {
|
|
295
|
+
if (!scroll_el) return;
|
|
296
|
+
can_scroll_prev = scroll_el.scrollLeft > 4;
|
|
297
|
+
can_scroll_next =
|
|
298
|
+
scroll_el.scrollLeft + scroll_el.clientWidth < scroll_el.scrollWidth - 4;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function scrollNext() {
|
|
302
|
+
if (!scroll_el) return;
|
|
303
|
+
scroll_el.scrollBy({ left: scroll_el.clientWidth * 0.8, behavior: 'smooth' });
|
|
304
|
+
}
|
|
305
|
+
function scrollPrev() {
|
|
306
|
+
if (!scroll_el) return;
|
|
307
|
+
scroll_el.scrollBy({ left: -scroll_el.clientWidth * 0.8, behavior: 'smooth' });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
$effect(() => {
|
|
311
|
+
if (!horizontal || !scroll_el) return;
|
|
312
|
+
updateScrollState();
|
|
313
|
+
const el = scroll_el;
|
|
314
|
+
const onScroll = () => updateScrollState();
|
|
315
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
316
|
+
const ro = new ResizeObserver(updateScrollState);
|
|
317
|
+
ro.observe(el);
|
|
318
|
+
return () => {
|
|
319
|
+
el.removeEventListener('scroll', onScroll);
|
|
320
|
+
ro.disconnect();
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
</script>
|
|
324
|
+
|
|
325
|
+
{#if isItem}
|
|
326
|
+
<!-- TimelineItem -->
|
|
327
|
+
<li
|
|
328
|
+
class={['item', class_name].filter(Boolean).join(' ')}
|
|
329
|
+
class:horizontal={is_horizontal}
|
|
330
|
+
class:vertical={!is_horizontal}
|
|
331
|
+
class:alternate={is_alternate}
|
|
332
|
+
class:even={is_alternate && !is_even}
|
|
333
|
+
class:odd={is_alternate && is_even}
|
|
334
|
+
class:dense={is_dense}
|
|
335
|
+
class:comfortable={is_comfortable}
|
|
336
|
+
class:reveal={do_reveal}
|
|
337
|
+
class:motion={do_motion}
|
|
338
|
+
class:interactive
|
|
339
|
+
class:visible
|
|
340
|
+
class:complete={status === 'complete'}
|
|
341
|
+
class:active={status === 'active'}
|
|
342
|
+
class:pending={status === 'pending'}
|
|
343
|
+
{id}
|
|
344
|
+
style:--marker-color={color || undefined}
|
|
345
|
+
{@attach intersectionObserver({ onintersectonce: () => (visible = true) })}>
|
|
346
|
+
<!-- The marker + content together form one clickable surface (`.lead`),
|
|
347
|
+
so the step circle is part of the touch target — not just the text.
|
|
348
|
+
The connector lives outside it (it's the rail, not the button). -->
|
|
349
|
+
<svelte:element
|
|
350
|
+
this={href ? 'a' : 'div'}
|
|
351
|
+
class="lead"
|
|
352
|
+
class:interactive
|
|
353
|
+
href={href || undefined}
|
|
354
|
+
target={href ? target : undefined}
|
|
355
|
+
rel={href && target === '_blank' ? 'noreferrer' : undefined}
|
|
356
|
+
role={interactive && !href ? 'button' : undefined}
|
|
357
|
+
tabindex={interactive && !href ? 0 : undefined}
|
|
358
|
+
aria-busy={in_flight ? 'true' : undefined}
|
|
359
|
+
onclick={interactive ? handleActivate : undefined}
|
|
360
|
+
onkeydown={interactive && !href ? handleKey : undefined}>
|
|
361
|
+
<div class="marker">
|
|
362
|
+
<span class="node" class:busy={spinner_visible || check_visible}>
|
|
363
|
+
{#if spinner_visible || check_visible}
|
|
364
|
+
<!-- Promise-aware feedback: a spinner while the work runs, then a
|
|
365
|
+
brief success check, both sitting in the step's circle. -->
|
|
366
|
+
<span class="feedback" transition:fade={{ duration: 150 }}>
|
|
367
|
+
{#if spinner_visible}
|
|
368
|
+
<span class="layer" out:fade={{ duration: 120 }}>
|
|
369
|
+
<Progress size="00" color="currentColor" />
|
|
370
|
+
</span>
|
|
371
|
+
{:else}
|
|
372
|
+
<span class="layer" in:checkIn>
|
|
373
|
+
<svg
|
|
374
|
+
class="check"
|
|
375
|
+
viewBox="0 0 24 24"
|
|
376
|
+
fill="none"
|
|
377
|
+
stroke="currentColor"
|
|
378
|
+
stroke-width="3.5"
|
|
379
|
+
stroke-linecap="round"
|
|
380
|
+
stroke-linejoin="round"
|
|
381
|
+
aria-hidden="true">
|
|
382
|
+
<polyline points="20 6 9 17 4 12" />
|
|
383
|
+
</svg>
|
|
384
|
+
</span>
|
|
385
|
+
{/if}
|
|
386
|
+
</span>
|
|
387
|
+
{/if}
|
|
388
|
+
{#if icon}
|
|
389
|
+
{@const Icon = icon}
|
|
390
|
+
<span class="glyph"><Icon /></span>
|
|
391
|
+
{:else if status === 'complete'}
|
|
392
|
+
<span class="glyph">
|
|
393
|
+
<svg
|
|
394
|
+
class="check"
|
|
395
|
+
viewBox="0 0 24 24"
|
|
396
|
+
fill="none"
|
|
397
|
+
stroke="currentColor"
|
|
398
|
+
stroke-width="3.5"
|
|
399
|
+
stroke-linecap="round"
|
|
400
|
+
stroke-linejoin="round"
|
|
401
|
+
aria-hidden="true">
|
|
402
|
+
<polyline points="20 6 9 17 4 12" />
|
|
403
|
+
</svg>
|
|
404
|
+
</span>
|
|
405
|
+
{/if}
|
|
406
|
+
</span>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="content">
|
|
409
|
+
<!-- The ripple + hover tint share one rounded panel that hugs the text,
|
|
410
|
+
so the press reads as intentional (not the whole arbitrary row).
|
|
411
|
+
The panel sits over the text but the click still bubbles to the
|
|
412
|
+
`.lead`, so the marker stays part of the touch target. -->
|
|
413
|
+
{#if interactive}
|
|
414
|
+
<span class="surface" aria-hidden="true" {@attach ripple({ zIndex: 0 })}></span>
|
|
415
|
+
{/if}
|
|
416
|
+
{#if date}
|
|
417
|
+
<time datetime={iso_date}>{formatted_date}</time>
|
|
418
|
+
{/if}
|
|
419
|
+
{#if title}
|
|
420
|
+
<div class="title">{title}</div>
|
|
421
|
+
{/if}
|
|
422
|
+
{#if children}
|
|
423
|
+
<div class="body">
|
|
424
|
+
{@render children()}
|
|
425
|
+
</div>
|
|
426
|
+
{/if}
|
|
427
|
+
</div>
|
|
428
|
+
</svelte:element>
|
|
429
|
+
<div class="connector"></div>
|
|
430
|
+
</li>
|
|
431
|
+
{:else if skeleton}
|
|
432
|
+
<!-- Skeleton -->
|
|
433
|
+
<ol
|
|
434
|
+
class={['timeline skeleton', horizontal ? 'horizontal' : 'vertical', class_name]
|
|
435
|
+
.filter(Boolean)
|
|
436
|
+
.join(' ')}
|
|
437
|
+
class:dense
|
|
438
|
+
class:comfortable
|
|
439
|
+
{id}
|
|
440
|
+
aria-hidden="true">
|
|
441
|
+
{#each { length: skeleton_count } as _, i}
|
|
442
|
+
<li
|
|
443
|
+
class="item skeleton-item"
|
|
444
|
+
class:horizontal
|
|
445
|
+
class:vertical={!horizontal}
|
|
446
|
+
class:dense
|
|
447
|
+
class:comfortable>
|
|
448
|
+
<div class="lead">
|
|
449
|
+
<div class="marker">
|
|
450
|
+
<span class="skeleton-circle" style:--shimmer-delay="{i * 120}ms"></span>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="content">
|
|
453
|
+
<div
|
|
454
|
+
class="skeleton-bar skeleton-date"
|
|
455
|
+
style:--shimmer-delay="{i * 120 + 60}ms">
|
|
456
|
+
</div>
|
|
457
|
+
<div
|
|
458
|
+
class="skeleton-bar skeleton-title-bar"
|
|
459
|
+
style:--shimmer-delay="{i * 120 + 120}ms">
|
|
460
|
+
</div>
|
|
461
|
+
<div
|
|
462
|
+
class="skeleton-bar skeleton-body-bar"
|
|
463
|
+
style:--shimmer-delay="{i * 120 + 180}ms">
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
<div class="connector"></div>
|
|
468
|
+
</li>
|
|
469
|
+
{/each}
|
|
470
|
+
</ol>
|
|
471
|
+
{:else if horizontal}
|
|
472
|
+
<!-- Horizontal timeline container with chevron next/prev controls -->
|
|
473
|
+
<div class={['wrap', class_name].filter(Boolean).join(' ')} {id}>
|
|
474
|
+
{#if can_scroll_prev}
|
|
475
|
+
<Button
|
|
476
|
+
icon
|
|
477
|
+
size="00"
|
|
478
|
+
class="timeline-nav timeline-nav-prev"
|
|
479
|
+
aria-label="Scroll back"
|
|
480
|
+
onclick={scrollPrev}>
|
|
481
|
+
<svg
|
|
482
|
+
viewBox="0 0 24 24"
|
|
483
|
+
fill="none"
|
|
484
|
+
stroke="currentColor"
|
|
485
|
+
stroke-width="2"
|
|
486
|
+
stroke-linecap="round"
|
|
487
|
+
stroke-linejoin="round"
|
|
488
|
+
aria-hidden="true">
|
|
489
|
+
<polyline points="15 18 9 12 15 6" />
|
|
490
|
+
</svg>
|
|
491
|
+
</Button>
|
|
492
|
+
{/if}
|
|
493
|
+
<ol
|
|
494
|
+
bind:this={scroll_el}
|
|
495
|
+
class="timeline horizontal"
|
|
496
|
+
class:alternate
|
|
497
|
+
class:dense
|
|
498
|
+
class:comfortable
|
|
499
|
+
role="list"
|
|
500
|
+
{@attach scrollbar()}>
|
|
501
|
+
{@render children?.()}
|
|
502
|
+
{#if pending}
|
|
503
|
+
<li class="item pending-item horizontal" class:motion={animate}>
|
|
504
|
+
<div class="lead">
|
|
505
|
+
<div class="marker">
|
|
506
|
+
<span class="node pending-node"></span>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</li>
|
|
510
|
+
{/if}
|
|
511
|
+
{#if onloadmore}
|
|
512
|
+
<li
|
|
513
|
+
class="sentinel"
|
|
514
|
+
aria-hidden="true"
|
|
515
|
+
{@attach intersectionObserver({ onintersectonce: () => handleLoadMore() })}>
|
|
516
|
+
</li>
|
|
517
|
+
{/if}
|
|
518
|
+
</ol>
|
|
519
|
+
{#if can_scroll_next}
|
|
520
|
+
<Button
|
|
521
|
+
icon
|
|
522
|
+
size="00"
|
|
523
|
+
class="timeline-nav timeline-nav-next"
|
|
524
|
+
aria-label="Scroll forward"
|
|
525
|
+
onclick={scrollNext}>
|
|
526
|
+
<svg
|
|
527
|
+
viewBox="0 0 24 24"
|
|
528
|
+
fill="none"
|
|
529
|
+
stroke="currentColor"
|
|
530
|
+
stroke-width="2"
|
|
531
|
+
stroke-linecap="round"
|
|
532
|
+
stroke-linejoin="round"
|
|
533
|
+
aria-hidden="true">
|
|
534
|
+
<polyline points="9 18 15 12 9 6" />
|
|
535
|
+
</svg>
|
|
536
|
+
</Button>
|
|
537
|
+
{/if}
|
|
538
|
+
</div>
|
|
539
|
+
{:else}
|
|
540
|
+
<!-- Timeline container -->
|
|
541
|
+
<ol
|
|
542
|
+
class={['timeline vertical', class_name].filter(Boolean).join(' ')}
|
|
543
|
+
class:alternate
|
|
544
|
+
class:dense
|
|
545
|
+
class:comfortable
|
|
546
|
+
{id}
|
|
547
|
+
role="list">
|
|
548
|
+
{@render children?.()}
|
|
549
|
+
{#if pending}
|
|
550
|
+
<li class="item pending-item vertical" class:motion={animate}>
|
|
551
|
+
<div class="lead">
|
|
552
|
+
<div class="marker">
|
|
553
|
+
<span class="node pending-node"></span>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</li>
|
|
557
|
+
{/if}
|
|
558
|
+
{#if onloadmore}
|
|
559
|
+
<li
|
|
560
|
+
class="sentinel"
|
|
561
|
+
aria-hidden="true"
|
|
562
|
+
{@attach intersectionObserver({ onintersectonce: () => handleLoadMore() })}>
|
|
563
|
+
</li>
|
|
564
|
+
{/if}
|
|
565
|
+
</ol>
|
|
566
|
+
{/if}
|
|
567
|
+
|
|
568
|
+
<style>
|
|
569
|
+
/* ============================================================
|
|
570
|
+
* Timeline
|
|
571
|
+
*
|
|
572
|
+
* Geometry is driven by a small set of custom properties set on
|
|
573
|
+
* each .item (and swapped by the dense/comfortable density flags),
|
|
574
|
+
* so the marker, rail and content stay in lockstep without any
|
|
575
|
+
* per-shape offset hacks:
|
|
576
|
+
*
|
|
577
|
+
* --node marker diameter --gap marker → content gap
|
|
578
|
+
* --rail rail thickness --run space below an item (= rail length)
|
|
579
|
+
* --node-gap breathing room between the node and the rail
|
|
580
|
+
*
|
|
581
|
+
* The rail is a real progress line: each item's connector is the
|
|
582
|
+
* segment that descends to the NEXT node, coloured by THIS item's
|
|
583
|
+
* status — completed segments read solid, the active segment fades
|
|
584
|
+
* into the muted "not yet" stretch ahead.
|
|
585
|
+
* ============================================================ */
|
|
586
|
+
|
|
587
|
+
/* ========== Container ========== */
|
|
588
|
+
.timeline {
|
|
589
|
+
list-style: none;
|
|
590
|
+
padding: 0;
|
|
591
|
+
margin: 0;
|
|
592
|
+
position: relative;
|
|
593
|
+
width: 100%;
|
|
594
|
+
|
|
595
|
+
&.vertical {
|
|
596
|
+
display: flex;
|
|
597
|
+
flex-direction: column;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
&.horizontal {
|
|
601
|
+
display: flex;
|
|
602
|
+
flex-direction: row;
|
|
603
|
+
overflow-x: auto;
|
|
604
|
+
scroll-snap-type: x proximity;
|
|
605
|
+
-webkit-overflow-scrolling: touch;
|
|
606
|
+
gap: 0;
|
|
607
|
+
/* overflow-x clips the y-axis too, so leave room for the active node's
|
|
608
|
+
glow + pulse ring (which reach beyond the marker). */
|
|
609
|
+
padding-block: 1.4rem;
|
|
610
|
+
scrollbar-width: none;
|
|
611
|
+
}
|
|
612
|
+
&.horizontal::-webkit-scrollbar {
|
|
613
|
+
display: none;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ========== Horizontal Navigation ========== */
|
|
618
|
+
.wrap {
|
|
619
|
+
position: relative;
|
|
620
|
+
width: 100%;
|
|
621
|
+
|
|
622
|
+
/* The nav controls are <Button icon> instances (rendered by the Button
|
|
623
|
+
* component), so target their forwarded class names with :global, scoped
|
|
624
|
+
* inside the wrap. We only position + float them; Button owns appearance. */
|
|
625
|
+
:global(.timeline-nav) {
|
|
626
|
+
position: absolute;
|
|
627
|
+
top: 50%;
|
|
628
|
+
transform: translateY(-50%);
|
|
629
|
+
z-index: 3;
|
|
630
|
+
background: var(--color-surface, #fff);
|
|
631
|
+
box-shadow: var(--shadow-md, 0 3px 10px rgb(0 0 0 / 0.12));
|
|
632
|
+
opacity: 0;
|
|
633
|
+
animation: timeline-nav-fade 200ms var(--ease-out, ease) forwards;
|
|
634
|
+
}
|
|
635
|
+
:global(.timeline-nav-prev) {
|
|
636
|
+
left: -0.75rem;
|
|
637
|
+
}
|
|
638
|
+
:global(.timeline-nav-next) {
|
|
639
|
+
right: -0.75rem;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
@keyframes timeline-nav-fade {
|
|
643
|
+
to {
|
|
644
|
+
opacity: 1;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* ========== Item: geometry ========== */
|
|
649
|
+
.item {
|
|
650
|
+
/* geometry */
|
|
651
|
+
--node: 18px;
|
|
652
|
+
--rail: 2px;
|
|
653
|
+
--gap: 1rem;
|
|
654
|
+
--run: 1.9rem;
|
|
655
|
+
--node-gap: 4px;
|
|
656
|
+
--fs-date: 0.72rem;
|
|
657
|
+
--fs-title: 0.9rem;
|
|
658
|
+
--fs-body: 0.83rem;
|
|
659
|
+
|
|
660
|
+
/* the marker / rail accent — `color` prop wins, else the status hue */
|
|
661
|
+
--accent: var(--marker-color, var(--color-action, #2563eb));
|
|
662
|
+
--node-fg: var(--color-action-text, #fff);
|
|
663
|
+
/* rail segment colour (overridden per status below) */
|
|
664
|
+
--rail-color: var(--color-border, #e5e7eb);
|
|
665
|
+
|
|
666
|
+
position: relative;
|
|
667
|
+
display: block;
|
|
668
|
+
|
|
669
|
+
&.dense {
|
|
670
|
+
--node: 14px;
|
|
671
|
+
--gap: 0.7rem;
|
|
672
|
+
--run: 1.1rem;
|
|
673
|
+
--node-gap: 3px;
|
|
674
|
+
--fs-date: 0.68rem;
|
|
675
|
+
--fs-title: 0.83rem;
|
|
676
|
+
--fs-body: 0.78rem;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
&.comfortable {
|
|
680
|
+
--node: 24px;
|
|
681
|
+
--rail: 2.5px;
|
|
682
|
+
--gap: 1.25rem;
|
|
683
|
+
--run: 2.6rem;
|
|
684
|
+
--node-gap: 5px;
|
|
685
|
+
--fs-date: 0.78rem;
|
|
686
|
+
--fs-title: 1rem;
|
|
687
|
+
--fs-body: 0.9rem;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* Vertical layout — the rail trails below each item. */
|
|
691
|
+
&.vertical {
|
|
692
|
+
padding-bottom: var(--run);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Horizontal layout. Equal-width columns (min-width drives the spacing)
|
|
696
|
+
with the node centred, so adjacent nodes sit one item-width apart and
|
|
697
|
+
the rail can span cleanly from one to the next. */
|
|
698
|
+
&.horizontal {
|
|
699
|
+
min-width: 9rem;
|
|
700
|
+
scroll-snap-align: start;
|
|
701
|
+
|
|
702
|
+
&.dense {
|
|
703
|
+
min-width: 6.5rem;
|
|
704
|
+
}
|
|
705
|
+
&.comfortable {
|
|
706
|
+
min-width: 12.5rem;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* ========== Lead: the marker + content row (one clickable surface) ========== */
|
|
712
|
+
.lead {
|
|
713
|
+
display: flex;
|
|
714
|
+
width: 100%;
|
|
715
|
+
box-sizing: border-box;
|
|
716
|
+
color: inherit;
|
|
717
|
+
text-decoration: none;
|
|
718
|
+
|
|
719
|
+
.item.vertical & {
|
|
720
|
+
flex-direction: row;
|
|
721
|
+
align-items: flex-start;
|
|
722
|
+
gap: var(--gap);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.item.horizontal & {
|
|
726
|
+
flex-direction: column;
|
|
727
|
+
align-items: center;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* status → accent hue + rail-segment colour */
|
|
732
|
+
.item.complete {
|
|
733
|
+
--accent: var(--marker-color, var(--color-success, #16a34a));
|
|
734
|
+
--node-fg: var(--color-success-text, #fff);
|
|
735
|
+
--rail-color: var(--marker-color, var(--color-success, #16a34a));
|
|
736
|
+
}
|
|
737
|
+
.item.active {
|
|
738
|
+
--accent: var(--marker-color, var(--color-action, #2563eb));
|
|
739
|
+
}
|
|
740
|
+
.item.pending {
|
|
741
|
+
--accent: var(--marker-color, var(--color-text-muted, #9ca3af));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* ========== Entrance reveal (only with the `reveal` class) ==========
|
|
745
|
+
Each node springs in while its rail draws toward the next one. Gated on
|
|
746
|
+
`.reveal` so a timeline with `animate={false}` — or one revealed after a
|
|
747
|
+
skeleton — simply appears, fully formed. */
|
|
748
|
+
.item.reveal {
|
|
749
|
+
opacity: 0;
|
|
750
|
+
transform: translateY(14px);
|
|
751
|
+
transition:
|
|
752
|
+
opacity 520ms var(--ease-out, ease),
|
|
753
|
+
transform 520ms var(--ease-out, ease);
|
|
754
|
+
}
|
|
755
|
+
.item.reveal.visible {
|
|
756
|
+
opacity: 1;
|
|
757
|
+
transform: none;
|
|
758
|
+
}
|
|
759
|
+
.item.reveal.horizontal {
|
|
760
|
+
transform: translateX(18px);
|
|
761
|
+
}
|
|
762
|
+
.item.reveal.horizontal.visible {
|
|
763
|
+
transform: none;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/* ========== Alternate Mode (vertical) ==========
|
|
767
|
+
Each item is a half-width column whose node lands exactly on the central
|
|
768
|
+
axis, so the rail stays a single straight line with events fanning out
|
|
769
|
+
left/right. */
|
|
770
|
+
.item.vertical.alternate {
|
|
771
|
+
width: calc(50% + var(--node) / 2);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.item.vertical.alternate.odd {
|
|
775
|
+
margin-left: calc(50% - var(--node) / 2);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.item.vertical.alternate.even {
|
|
779
|
+
text-align: right;
|
|
780
|
+
}
|
|
781
|
+
.item.vertical.alternate.even .lead {
|
|
782
|
+
flex-direction: row-reverse;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* ========== Marker / node ========== */
|
|
786
|
+
.marker {
|
|
787
|
+
position: relative;
|
|
788
|
+
z-index: 1;
|
|
789
|
+
flex-shrink: 0;
|
|
790
|
+
display: flex;
|
|
791
|
+
align-items: flex-start;
|
|
792
|
+
justify-content: center;
|
|
793
|
+
}
|
|
794
|
+
.item.horizontal .marker {
|
|
795
|
+
align-items: center;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.node {
|
|
799
|
+
position: relative;
|
|
800
|
+
width: var(--node);
|
|
801
|
+
height: var(--node);
|
|
802
|
+
border-radius: var(--radius-full, 1e5px);
|
|
803
|
+
display: grid;
|
|
804
|
+
place-items: center;
|
|
805
|
+
color: var(--node-fg);
|
|
806
|
+
background: var(--accent);
|
|
807
|
+
/* soft halo so a filled marker reads as lit, not flat */
|
|
808
|
+
box-shadow: 0 0 0 4px rgb(from var(--accent) r g b / 0.12);
|
|
809
|
+
/* reveal: spring-pop; hover: gentle grow. Colour/shadow snap in (see below). */
|
|
810
|
+
scale: var(--node-scale, 1);
|
|
811
|
+
transition:
|
|
812
|
+
scale 360ms var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1)),
|
|
813
|
+
background-color 240ms ease,
|
|
814
|
+
box-shadow 240ms ease;
|
|
815
|
+
}
|
|
816
|
+
/* start collapsed only while a reveal is pending */
|
|
817
|
+
.item.reveal .node {
|
|
818
|
+
--node-scale: 0;
|
|
819
|
+
}
|
|
820
|
+
.item.reveal.visible .node {
|
|
821
|
+
--node-scale: 1;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* Resting glyph (a custom icon, or the complete-status check). The wrapper is
|
|
825
|
+
sized to the node and its svg fills it — so it works whatever the icon
|
|
826
|
+
component renders, and stays separate from the `.feedback` layers. */
|
|
827
|
+
.node > .glyph {
|
|
828
|
+
display: grid;
|
|
829
|
+
place-items: center;
|
|
830
|
+
width: 63%;
|
|
831
|
+
height: 63%;
|
|
832
|
+
}
|
|
833
|
+
.node > .glyph :global(svg) {
|
|
834
|
+
width: 100%;
|
|
835
|
+
height: 100%;
|
|
836
|
+
}
|
|
837
|
+
/* The resting glyph fades away while the promise feedback occupies the node. */
|
|
838
|
+
.node.busy > .glyph {
|
|
839
|
+
opacity: 0;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* ========== Promise-aware feedback (spinner → success check) ========== */
|
|
843
|
+
.feedback {
|
|
844
|
+
position: absolute;
|
|
845
|
+
inset: 0;
|
|
846
|
+
display: grid;
|
|
847
|
+
place-items: center;
|
|
848
|
+
z-index: 1;
|
|
849
|
+
}
|
|
850
|
+
.feedback .layer {
|
|
851
|
+
grid-area: 1 / 1; /* stack the spinner and the check in one cell */
|
|
852
|
+
display: grid;
|
|
853
|
+
place-items: center;
|
|
854
|
+
}
|
|
855
|
+
/* Fit the (fixed 16px) spinner to the node and give it a faint same-colour
|
|
856
|
+
track so it reads as one ring inside the marker. */
|
|
857
|
+
.feedback :global(.progress) {
|
|
858
|
+
scale: calc(var(--node) / 20);
|
|
859
|
+
}
|
|
860
|
+
.feedback :global(circle.track) {
|
|
861
|
+
stroke: rgb(from currentColor r g b / 0.25);
|
|
862
|
+
}
|
|
863
|
+
.feedback :global(circle.arc) {
|
|
864
|
+
stroke: currentColor;
|
|
865
|
+
}
|
|
866
|
+
.feedback .check {
|
|
867
|
+
width: 64%;
|
|
868
|
+
height: 64%;
|
|
869
|
+
}
|
|
870
|
+
.feedback .check polyline {
|
|
871
|
+
stroke-dasharray: 24;
|
|
872
|
+
stroke-dashoffset: var(--check-draw, 0);
|
|
873
|
+
}
|
|
874
|
+
/* Hold the active ping while the step is working — the spinner is the focus. */
|
|
875
|
+
.item.active.motion .node.busy::before {
|
|
876
|
+
animation: none;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/* Active — the "you are here" node: a steady glow plus an expanding ping. */
|
|
880
|
+
.item.active .node {
|
|
881
|
+
box-shadow:
|
|
882
|
+
0 0 0 4px rgb(from var(--accent) r g b / 0.18),
|
|
883
|
+
0 0 14px 1px rgb(from var(--accent) r g b / 0.45);
|
|
884
|
+
}
|
|
885
|
+
.item.active.motion .node::before {
|
|
886
|
+
content: '';
|
|
887
|
+
position: absolute;
|
|
888
|
+
inset: 0;
|
|
889
|
+
border-radius: inherit;
|
|
890
|
+
background: var(--accent);
|
|
891
|
+
z-index: -1;
|
|
892
|
+
animation: timeline-ping 2.4s var(--ease-out, ease-out) infinite;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Pending — a clean hollow ring with a faint fill. */
|
|
896
|
+
.item.pending .node,
|
|
897
|
+
.pending-node {
|
|
898
|
+
background: rgb(from var(--accent) r g b / 0.1);
|
|
899
|
+
box-shadow: inset 0 0 0 var(--rail) var(--accent);
|
|
900
|
+
color: var(--accent);
|
|
901
|
+
}
|
|
902
|
+
.pending-node {
|
|
903
|
+
--accent: var(--marker-color, var(--color-text-muted, #9ca3af));
|
|
904
|
+
width: var(--node, 18px);
|
|
905
|
+
height: var(--node, 18px);
|
|
906
|
+
border-radius: var(--radius-full, 1e5px);
|
|
907
|
+
}
|
|
908
|
+
.pending-item.motion .pending-node {
|
|
909
|
+
animation: timeline-breathe 2.4s ease-in-out infinite;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* ========== Connector (the progress rail) ========== */
|
|
913
|
+
.connector {
|
|
914
|
+
position: absolute;
|
|
915
|
+
border-radius: var(--radius-full, 1e5px);
|
|
916
|
+
background: var(--rail-color);
|
|
917
|
+
z-index: 0;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.item.vertical > .connector {
|
|
921
|
+
left: calc(var(--node) / 2 - var(--rail) / 2);
|
|
922
|
+
top: calc(var(--node) + var(--node-gap));
|
|
923
|
+
bottom: var(--node-gap);
|
|
924
|
+
width: var(--rail);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/* The node is centred (50%); the next item's node sits one full item-width
|
|
928
|
+
away, so the segment runs from this node's right edge to the next node's
|
|
929
|
+
left edge (minus the breathing gap at each end). */
|
|
930
|
+
.item.horizontal > .connector {
|
|
931
|
+
top: calc(var(--node) / 2 - var(--rail) / 2);
|
|
932
|
+
left: calc(50% + var(--node) / 2 + var(--node-gap));
|
|
933
|
+
width: calc(100% - var(--node) - 2 * var(--node-gap));
|
|
934
|
+
height: var(--rail);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* draw-in: the rail grows toward the next node after this one pops */
|
|
938
|
+
.item.reveal.vertical > .connector {
|
|
939
|
+
transform: scaleY(0);
|
|
940
|
+
transform-origin: top center;
|
|
941
|
+
transition: transform 560ms var(--ease-out, ease) 120ms;
|
|
942
|
+
}
|
|
943
|
+
.item.reveal.vertical.visible > .connector {
|
|
944
|
+
transform: scaleY(1);
|
|
945
|
+
}
|
|
946
|
+
.item.reveal.horizontal > .connector {
|
|
947
|
+
transform: scaleX(0);
|
|
948
|
+
transform-origin: left center;
|
|
949
|
+
transition: transform 560ms var(--ease-out, ease) 120ms;
|
|
950
|
+
}
|
|
951
|
+
.item.reveal.horizontal.visible > .connector {
|
|
952
|
+
transform: scaleX(1);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* The active segment fades from "done" into the muted road ahead. */
|
|
956
|
+
.item.active.vertical > .connector {
|
|
957
|
+
background: linear-gradient(to bottom, var(--accent), var(--color-border, #e5e7eb));
|
|
958
|
+
}
|
|
959
|
+
.item.active.horizontal > .connector {
|
|
960
|
+
background: linear-gradient(to right, var(--accent), var(--color-border, #e5e7eb));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/* Hide the trailing connector — the last node has nowhere to go. The
|
|
964
|
+
pending-item carries the `.item` class, so an item followed by the
|
|
965
|
+
pending node still draws its rail; only the genuine last node drops it. */
|
|
966
|
+
.item:not(:has(~ .item)) > .connector {
|
|
967
|
+
display: none;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/* Alternate mode: the rail hugs the central axis on both sides. */
|
|
971
|
+
.item.vertical.alternate.even > .connector {
|
|
972
|
+
left: auto;
|
|
973
|
+
right: calc(var(--node) / 2 - var(--rail) / 2);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/* ========== Interactive lead (clickable step) ==========
|
|
977
|
+
The whole marker + content surface is the touch target (mirrors Button:
|
|
978
|
+
pointer cursor, ripple, press scale, snap-in / ease-out hover). The hover
|
|
979
|
+
tint is painted only behind the text, by a pseudo-element, so an item
|
|
980
|
+
gaining an onclick/href never changes its layout. */
|
|
981
|
+
.lead.interactive {
|
|
982
|
+
position: relative;
|
|
983
|
+
cursor: pointer;
|
|
984
|
+
outline: none;
|
|
985
|
+
-webkit-tap-highlight-color: transparent;
|
|
986
|
+
transition: scale 180ms ease;
|
|
987
|
+
}
|
|
988
|
+
.lead.interactive:active {
|
|
989
|
+
scale: 0.985;
|
|
990
|
+
}
|
|
991
|
+
.lead.interactive:focus-visible {
|
|
992
|
+
outline: 2px solid var(--color-action, #2563eb);
|
|
993
|
+
outline-offset: 3px;
|
|
994
|
+
border-radius: var(--radius-md, 5px);
|
|
995
|
+
@supports (corner-shape: squircle) {
|
|
996
|
+
corner-shape: squircle;
|
|
997
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/* Hovering the step leans its marker in and deepens its title. */
|
|
1002
|
+
.lead.interactive:hover .node {
|
|
1003
|
+
--node-scale: 1.1;
|
|
1004
|
+
box-shadow: 0 0 0 6px rgb(from var(--accent) r g b / 0.18);
|
|
1005
|
+
transition: scale 320ms var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
1006
|
+
}
|
|
1007
|
+
.item.active .lead.interactive:hover .node {
|
|
1008
|
+
box-shadow:
|
|
1009
|
+
0 0 0 6px rgb(from var(--accent) r g b / 0.22),
|
|
1010
|
+
0 0 16px 1px rgb(from var(--accent) r g b / 0.5);
|
|
1011
|
+
}
|
|
1012
|
+
.lead.interactive:hover .title {
|
|
1013
|
+
color: var(--color-text-active, var(--color-text, #1a1a1a));
|
|
1014
|
+
transition: none;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/* ========== Content ========== */
|
|
1018
|
+
.content {
|
|
1019
|
+
flex: 1;
|
|
1020
|
+
min-width: 0;
|
|
1021
|
+
position: relative;
|
|
1022
|
+
/* own stacking context so the tint (::before, z-index -1) tucks behind the
|
|
1023
|
+
text without escaping behind the whole item */
|
|
1024
|
+
isolation: isolate;
|
|
1025
|
+
/* nudge the first text line so it sits centred against the node */
|
|
1026
|
+
padding-top: calc(var(--node) / 2 - 0.5em);
|
|
1027
|
+
|
|
1028
|
+
.item.horizontal & {
|
|
1029
|
+
margin-top: 0.85rem;
|
|
1030
|
+
padding-top: 0;
|
|
1031
|
+
text-align: center;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
/* The hover tint — a rounded panel behind the text only. Absolutely
|
|
1035
|
+
positioned, so it adds no layout (interactive and plain items match). */
|
|
1036
|
+
.content::before {
|
|
1037
|
+
content: '';
|
|
1038
|
+
position: absolute;
|
|
1039
|
+
inset: -0.3rem -0.6rem;
|
|
1040
|
+
z-index: -1;
|
|
1041
|
+
border-radius: var(--radius-md, 5px);
|
|
1042
|
+
background: transparent;
|
|
1043
|
+
transition: background-color 240ms ease;
|
|
1044
|
+
@supports (corner-shape: squircle) {
|
|
1045
|
+
corner-shape: squircle;
|
|
1046
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
.item.horizontal .content::before {
|
|
1050
|
+
inset: -0.3rem -0.7rem;
|
|
1051
|
+
}
|
|
1052
|
+
.lead.interactive:hover .content::before {
|
|
1053
|
+
background: rgb(from var(--color-text, #333) r g b / 0.06);
|
|
1054
|
+
transition: none;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/* The ripple panel — the same rounded footprint as the tint, sitting on top
|
|
1058
|
+
so it catches the press and clips the ripple to a clean, intentional shape
|
|
1059
|
+
(not the whole arbitrary marker+content row). Transparent, so the tint
|
|
1060
|
+
behind the text stays the resting look. Clicks still bubble to `.lead`, so
|
|
1061
|
+
the marker remains part of the touch target. */
|
|
1062
|
+
.surface {
|
|
1063
|
+
position: absolute;
|
|
1064
|
+
inset: -0.3rem -0.6rem;
|
|
1065
|
+
z-index: 2;
|
|
1066
|
+
border-radius: var(--radius-md, 5px);
|
|
1067
|
+
cursor: pointer;
|
|
1068
|
+
-webkit-tap-highlight-color: transparent;
|
|
1069
|
+
@supports (corner-shape: squircle) {
|
|
1070
|
+
corner-shape: squircle;
|
|
1071
|
+
border-radius: calc(var(--radius-md, 5px) * var(--squircle-ratio, 2));
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
.item.horizontal .surface {
|
|
1075
|
+
inset: -0.3rem -0.7rem;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
time {
|
|
1079
|
+
display: block;
|
|
1080
|
+
font-size: var(--fs-date);
|
|
1081
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1082
|
+
letter-spacing: 0.04em;
|
|
1083
|
+
text-transform: uppercase;
|
|
1084
|
+
color: var(--color-text-muted, #6b7280);
|
|
1085
|
+
margin-bottom: 0.3em;
|
|
1086
|
+
line-height: 1.2;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.title {
|
|
1090
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
1091
|
+
font-size: var(--fs-title);
|
|
1092
|
+
color: var(--color-text, #1a1a1a);
|
|
1093
|
+
line-height: 1.35;
|
|
1094
|
+
transition: color 240ms ease;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.body {
|
|
1098
|
+
margin-top: 0.25em;
|
|
1099
|
+
font-size: var(--fs-body);
|
|
1100
|
+
color: var(--color-text-muted, #6b7280);
|
|
1101
|
+
line-height: 1.55;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/* ========== Pending Indicator (trailing) ========== */
|
|
1105
|
+
.pending-item {
|
|
1106
|
+
padding-bottom: 0;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/* ========== Load-more Sentinel ========== */
|
|
1110
|
+
.sentinel {
|
|
1111
|
+
height: 1px;
|
|
1112
|
+
width: 1px;
|
|
1113
|
+
overflow: hidden;
|
|
1114
|
+
position: absolute;
|
|
1115
|
+
bottom: 0;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/* ========== Skeleton ========== */
|
|
1119
|
+
.timeline.skeleton {
|
|
1120
|
+
pointer-events: none;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.skeleton-item > .connector {
|
|
1124
|
+
background: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.skeleton-circle,
|
|
1128
|
+
.skeleton-bar {
|
|
1129
|
+
position: relative;
|
|
1130
|
+
overflow: hidden;
|
|
1131
|
+
background: var(--skeleton-bg, rgb(from var(--color-text, #888) r g b / 0.1));
|
|
1132
|
+
|
|
1133
|
+
&::after {
|
|
1134
|
+
content: '';
|
|
1135
|
+
position: absolute;
|
|
1136
|
+
inset: 0;
|
|
1137
|
+
transform: translateX(-100%);
|
|
1138
|
+
background-image: linear-gradient(
|
|
1139
|
+
105deg,
|
|
1140
|
+
transparent 25%,
|
|
1141
|
+
var(--skeleton-sheen, rgb(from var(--color-text, #888) r g b / 0.12)) 50%,
|
|
1142
|
+
transparent 75%
|
|
1143
|
+
);
|
|
1144
|
+
animation: delight-skeleton-shimmer var(--skeleton-duration, 2.4s) ease-in-out
|
|
1145
|
+
infinite;
|
|
1146
|
+
animation-delay: var(--shimmer-delay, 0s);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/* Same footprint as the real .node. */
|
|
1151
|
+
.skeleton-circle {
|
|
1152
|
+
width: var(--node, 18px);
|
|
1153
|
+
height: var(--node, 18px);
|
|
1154
|
+
border-radius: var(--radius-full, 1e5px);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/* Flex column so the bars' line-padding margins don't collapse — each bar's
|
|
1158
|
+
margins pad it out to its real text line's 1lh, keeping skeleton items
|
|
1159
|
+
exactly as tall as loaded ones. */
|
|
1160
|
+
.skeleton-item .content {
|
|
1161
|
+
display: flex;
|
|
1162
|
+
flex-direction: column;
|
|
1163
|
+
align-items: flex-start;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.skeleton-item.horizontal .content {
|
|
1167
|
+
align-items: center;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.skeleton-bar {
|
|
1171
|
+
height: 0.7em;
|
|
1172
|
+
border-radius: var(--radius-full, 1e5px);
|
|
1173
|
+
max-width: 100%;
|
|
1174
|
+
|
|
1175
|
+
&.skeleton-date {
|
|
1176
|
+
width: 5rem;
|
|
1177
|
+
font-size: var(--fs-date);
|
|
1178
|
+
line-height: 1.2;
|
|
1179
|
+
margin-block: calc((1lh - 0.7em) / 2) calc((1lh - 0.7em) / 2 + 0.3em);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
&.skeleton-title-bar {
|
|
1183
|
+
width: 8rem;
|
|
1184
|
+
font-size: var(--fs-title);
|
|
1185
|
+
line-height: 1.35;
|
|
1186
|
+
margin-block: calc((1lh - 0.7em) / 2);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
&.skeleton-body-bar {
|
|
1190
|
+
width: 12rem;
|
|
1191
|
+
font-size: var(--fs-body);
|
|
1192
|
+
line-height: 1.55;
|
|
1193
|
+
margin-block: calc((1lh - 0.7em) / 2 + 0.25em) calc((1lh - 0.7em) / 2);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* ========== Animations ========== */
|
|
1198
|
+
@keyframes timeline-ping {
|
|
1199
|
+
0% {
|
|
1200
|
+
transform: scale(1);
|
|
1201
|
+
opacity: 0.55;
|
|
1202
|
+
}
|
|
1203
|
+
70%,
|
|
1204
|
+
100% {
|
|
1205
|
+
transform: scale(2.6);
|
|
1206
|
+
opacity: 0;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
@keyframes timeline-breathe {
|
|
1211
|
+
0%,
|
|
1212
|
+
100% {
|
|
1213
|
+
opacity: 0.55;
|
|
1214
|
+
}
|
|
1215
|
+
50% {
|
|
1216
|
+
opacity: 1;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
@keyframes -global-delight-skeleton-shimmer {
|
|
1221
|
+
0% {
|
|
1222
|
+
transform: translateX(-100%);
|
|
1223
|
+
}
|
|
1224
|
+
55%,
|
|
1225
|
+
100% {
|
|
1226
|
+
transform: translateX(100%);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/* ========== Reduced Motion ========== */
|
|
1231
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1232
|
+
.item.reveal {
|
|
1233
|
+
opacity: 1 !important;
|
|
1234
|
+
transform: none !important;
|
|
1235
|
+
transition: none !important;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.node {
|
|
1239
|
+
scale: 1 !important;
|
|
1240
|
+
transition: none !important;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.item.active .node::before,
|
|
1244
|
+
.pending-node {
|
|
1245
|
+
animation: none !important;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.item.reveal > .connector {
|
|
1249
|
+
transform: none !important;
|
|
1250
|
+
transition: none !important;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
.skeleton-circle::after,
|
|
1254
|
+
.skeleton-bar::after {
|
|
1255
|
+
animation: none;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
</style>
|