@urbicon-ui/blocks 6.8.1 → 6.9.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/dist/i18n/index.d.ts +20 -0
- package/dist/primitives/JourneyTimeline/JourneyTimeline.svelte +372 -0
- package/dist/primitives/JourneyTimeline/JourneyTimeline.svelte.d.ts +4 -0
- package/dist/primitives/JourneyTimeline/index.d.ts +114 -0
- package/dist/primitives/JourneyTimeline/index.js +2 -0
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +250 -0
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +175 -0
- package/dist/primitives/Stepper/index.d.ts +1 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/translations/de.d.ts +10 -0
- package/dist/translations/de.js +10 -0
- package/dist/translations/en.d.ts +10 -0
- package/dist/translations/en.js +10 -0
- package/package.json +3 -3
package/dist/i18n/index.d.ts
CHANGED
|
@@ -131,6 +131,16 @@ declare const blocksTranslations: {
|
|
|
131
131
|
readonly filterPlaceholder: "Filter topics…";
|
|
132
132
|
readonly noResults: "No matching topics";
|
|
133
133
|
};
|
|
134
|
+
readonly journeyTimeline: {
|
|
135
|
+
readonly label: "Journey";
|
|
136
|
+
readonly status: {
|
|
137
|
+
readonly complete: "Completed";
|
|
138
|
+
readonly active: "In progress";
|
|
139
|
+
readonly pending: "Pending";
|
|
140
|
+
readonly blocked: "Blocked";
|
|
141
|
+
readonly skipped: "Skipped";
|
|
142
|
+
};
|
|
143
|
+
};
|
|
134
144
|
readonly datepicker: {
|
|
135
145
|
readonly placeholder: "Select a date...";
|
|
136
146
|
readonly rangePlaceholder: "Select a date range...";
|
|
@@ -302,6 +312,16 @@ declare const blocksTranslations: {
|
|
|
302
312
|
readonly filterPlaceholder: "Themen filtern…";
|
|
303
313
|
readonly noResults: "Keine passenden Themen";
|
|
304
314
|
};
|
|
315
|
+
readonly journeyTimeline: {
|
|
316
|
+
readonly label: "Verlauf";
|
|
317
|
+
readonly status: {
|
|
318
|
+
readonly complete: "Abgeschlossen";
|
|
319
|
+
readonly active: "In Bearbeitung";
|
|
320
|
+
readonly pending: "Ausstehend";
|
|
321
|
+
readonly blocked: "Blockiert";
|
|
322
|
+
readonly skipped: "Übersprungen";
|
|
323
|
+
};
|
|
324
|
+
};
|
|
305
325
|
readonly datepicker: {
|
|
306
326
|
readonly placeholder: "Datum wählen...";
|
|
307
327
|
readonly rangePlaceholder: "Zeitraum wählen...";
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useBlocksI18n } from '../..';
|
|
3
|
+
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
4
|
+
import { resolveIcon } from '../../icons';
|
|
5
|
+
import CheckIconDefault from '../../icons/CheckIcon.svelte';
|
|
6
|
+
import CircleDotIconDefault from '../../icons/CircleDotIcon.svelte';
|
|
7
|
+
import BanIconDefault from '../../icons/BanIcon.svelte';
|
|
8
|
+
import MinusIconDefault from '../../icons/MinusIcon.svelte';
|
|
9
|
+
import {
|
|
10
|
+
journeyTimelineVariants,
|
|
11
|
+
type JourneyTimelineSlots,
|
|
12
|
+
type JourneyTimelineVariants
|
|
13
|
+
} from './journey-timeline.variants';
|
|
14
|
+
import type { JourneyNode, JourneyStatus, JourneyTimelineProps } from './index';
|
|
15
|
+
|
|
16
|
+
const bt = useBlocksI18n();
|
|
17
|
+
|
|
18
|
+
const CheckIcon = resolveIcon('check', CheckIconDefault);
|
|
19
|
+
const CircleDotIcon = resolveIcon('circleDot', CircleDotIconDefault);
|
|
20
|
+
const BanIcon = resolveIcon('ban', BanIconDefault);
|
|
21
|
+
const MinusIcon = resolveIcon('minus', MinusIconDefault);
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
items,
|
|
25
|
+
orientation = 'vertical',
|
|
26
|
+
size = 'md',
|
|
27
|
+
focusId = $bindable(),
|
|
28
|
+
defaultFocusId,
|
|
29
|
+
scrollSpy = false,
|
|
30
|
+
onFocusChange,
|
|
31
|
+
node,
|
|
32
|
+
class: className = '',
|
|
33
|
+
unstyled: unstyledProp = false,
|
|
34
|
+
slotClasses: slotClassesProp = {},
|
|
35
|
+
preset,
|
|
36
|
+
...restProps
|
|
37
|
+
}: JourneyTimelineProps = $props();
|
|
38
|
+
|
|
39
|
+
const blocksConfig = getBlocksConfig();
|
|
40
|
+
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
41
|
+
|
|
42
|
+
// --- focus state (controlled via bind:focusId, else internal) -------------
|
|
43
|
+
const focusableItems = $derived(items.filter((it) => it.focusable !== false));
|
|
44
|
+
const isFocusable = (id: string | undefined) =>
|
|
45
|
+
id !== undefined && focusableItems.some((it) => it.id === id);
|
|
46
|
+
|
|
47
|
+
// Default focus: explicit → first `active` node → first focusable node.
|
|
48
|
+
const defaultFocus = $derived(
|
|
49
|
+
(isFocusable(defaultFocusId) ? defaultFocusId : undefined) ??
|
|
50
|
+
items.find((it) => it.status === 'active' && it.focusable !== false)?.id ??
|
|
51
|
+
focusableItems[0]?.id
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let internalFocusId = $state<string | undefined>(undefined);
|
|
55
|
+
// A stale internal focus (the expanded node was removed) falls back to the
|
|
56
|
+
// default so a node is always open. A controlled `focusId` is honoured as-is
|
|
57
|
+
// (write-strict) and only surfaced via the dev warning below.
|
|
58
|
+
const activeFocusId = $derived(
|
|
59
|
+
focusId !== undefined ? focusId : isFocusable(internalFocusId) ? internalFocusId : defaultFocus
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// The roving-tabindex target: the last keyboard/click-touched node, else the
|
|
63
|
+
// expanded node. Kept distinct from `activeFocusId` so arrow keys can move DOM
|
|
64
|
+
// focus without expanding (expansion happens on Enter/Space/click). Guarded
|
|
65
|
+
// against a removed node so the tab stop never lands on nothing.
|
|
66
|
+
let rovingId = $state<string | undefined>(undefined);
|
|
67
|
+
// Every candidate is validated against the live focusable set (not just the
|
|
68
|
+
// first non-undefined one), so a stale roving/active id — or a bad controlled
|
|
69
|
+
// focusId — can never leave every button with tabindex=-1.
|
|
70
|
+
const tabbableId = $derived(
|
|
71
|
+
[rovingId, activeFocusId, focusableItems[0]?.id].find((id) => isFocusable(id))
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Dev-only signals for caller mistakes that otherwise fail silently. Mirrors
|
|
75
|
+
// the `Select`/`Guide` precedent (`import.meta.env?.DEV && console.warn`).
|
|
76
|
+
$effect(() => {
|
|
77
|
+
if (!import.meta.env?.DEV || items.length === 0) return;
|
|
78
|
+
if (focusId !== undefined && !isFocusable(focusId)) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[JourneyTimeline] focusId "${focusId}" matches no focusable node — every node stays collapsed.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (focusId === undefined && defaultFocusId !== undefined && !isFocusable(defaultFocusId)) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`[JourneyTimeline] defaultFocusId "${defaultFocusId}" matches no focusable node — falling back to the first active/focusable node.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!node && focusableItems.length > 0) {
|
|
89
|
+
console.warn(
|
|
90
|
+
'[JourneyTimeline] focusable nodes render as expandable buttons but no `node` snippet was provided. Pass a `node` snippet or set focusable:false on nodes that carry no detail.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function setFocus(id: string) {
|
|
96
|
+
const target = items.find((it) => it.id === id);
|
|
97
|
+
if (!target || target.focusable === false || id === activeFocusId) return;
|
|
98
|
+
if (focusId !== undefined) focusId = id;
|
|
99
|
+
else internalFocusId = id;
|
|
100
|
+
onFocusChange?.(id);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function activate(item: JourneyNode) {
|
|
104
|
+
rovingId = item.id;
|
|
105
|
+
setFocus(item.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- keyboard roving navigation -------------------------------------------
|
|
109
|
+
let rootRef = $state<HTMLDivElement>();
|
|
110
|
+
|
|
111
|
+
function triggerEls(): HTMLElement[] {
|
|
112
|
+
return rootRef
|
|
113
|
+
? Array.from(rootRef.querySelectorAll<HTMLElement>('[data-journey-trigger]'))
|
|
114
|
+
: [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function moveFocus(delta: number) {
|
|
118
|
+
const els = triggerEls();
|
|
119
|
+
if (els.length === 0) return;
|
|
120
|
+
const currentId = rovingId ?? activeFocusId;
|
|
121
|
+
const currentIdx = els.findIndex((el) => el.dataset.nodeId === currentId);
|
|
122
|
+
const startIdx = currentIdx === -1 ? (delta > 0 ? -1 : 0) : currentIdx;
|
|
123
|
+
const next = els[(startIdx + delta + els.length) % els.length];
|
|
124
|
+
if (!next) return;
|
|
125
|
+
rovingId = next.dataset.nodeId;
|
|
126
|
+
next.focus();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function focusEdge(edge: 'first' | 'last') {
|
|
130
|
+
const els = triggerEls();
|
|
131
|
+
const el = edge === 'first' ? els[0] : els[els.length - 1];
|
|
132
|
+
if (!el) return;
|
|
133
|
+
rovingId = el.dataset.nodeId;
|
|
134
|
+
el.focus();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
138
|
+
// Only navigate when a node header is focused — never hijack arrow keys
|
|
139
|
+
// typed inside an expanded node's detail content.
|
|
140
|
+
if (!(e.target as HTMLElement)?.hasAttribute?.('data-journey-trigger')) return;
|
|
141
|
+
const forward = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
142
|
+
const backward = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
143
|
+
switch (e.key) {
|
|
144
|
+
case forward:
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
moveFocus(1);
|
|
147
|
+
break;
|
|
148
|
+
case backward:
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
moveFocus(-1);
|
|
151
|
+
break;
|
|
152
|
+
case 'Home':
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
focusEdge('first');
|
|
155
|
+
break;
|
|
156
|
+
case 'End':
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
focusEdge('last');
|
|
159
|
+
break;
|
|
160
|
+
// Enter / Space activate via the native <button> click → activate().
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- scroll-spy (optional) -------------------------------------------------
|
|
165
|
+
$effect(() => {
|
|
166
|
+
if (!scrollSpy || !rootRef) return;
|
|
167
|
+
// Re-subscribe when the node set changes (reading `items` makes it a dep).
|
|
168
|
+
const currentItems = items;
|
|
169
|
+
const nodeEls = Array.from(rootRef.querySelectorAll<HTMLElement>('[data-journey-node]'));
|
|
170
|
+
if (nodeEls.length === 0) return;
|
|
171
|
+
|
|
172
|
+
const visible = new Set<string>();
|
|
173
|
+
// `observe()` delivers a synthetic initial callback for every target before
|
|
174
|
+
// any scrolling. Seed `visible` from it but do NOT act — otherwise mounting
|
|
175
|
+
// would clobber the resolved default focus (or a controlled focusId) and fire
|
|
176
|
+
// a spurious onFocusChange. Scroll-spy only *follows* real scrolling.
|
|
177
|
+
let seeded = false;
|
|
178
|
+
const observer = new IntersectionObserver(
|
|
179
|
+
(entries) => {
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const id = (entry.target as HTMLElement).dataset.nodeId;
|
|
182
|
+
if (!id) continue;
|
|
183
|
+
if (entry.isIntersecting) visible.add(id);
|
|
184
|
+
else visible.delete(id);
|
|
185
|
+
}
|
|
186
|
+
if (!seeded) {
|
|
187
|
+
seeded = true;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const topmost = currentItems.find((it) => it.focusable !== false && visible.has(it.id));
|
|
191
|
+
if (topmost) setFocus(topmost.id);
|
|
192
|
+
},
|
|
193
|
+
// A node counts as "current" while it sits in the top 40% of the viewport.
|
|
194
|
+
{ rootMargin: '0px 0px -60% 0px', threshold: 0 }
|
|
195
|
+
);
|
|
196
|
+
for (const el of nodeEls) observer.observe(el);
|
|
197
|
+
return () => observer.disconnect();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// --- styling ---------------------------------------------------------------
|
|
201
|
+
type Styles = ReturnType<typeof journeyTimelineVariants>;
|
|
202
|
+
|
|
203
|
+
const containerStyles = $derived(
|
|
204
|
+
unstyled ? null : journeyTimelineVariants({ orientation, size })
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
function nodeStyles(item: JourneyNode, focused: boolean): Styles | null {
|
|
208
|
+
return unstyled
|
|
209
|
+
? null
|
|
210
|
+
: journeyTimelineVariants({
|
|
211
|
+
orientation,
|
|
212
|
+
size,
|
|
213
|
+
status: item.status,
|
|
214
|
+
focused,
|
|
215
|
+
interactive: item.focusable !== false,
|
|
216
|
+
// The connector leaving a completed node reads as "travelled".
|
|
217
|
+
connectorComplete: item.status === 'complete'
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const slotClasses = $derived(
|
|
222
|
+
resolveSlotClasses(
|
|
223
|
+
blocksConfig,
|
|
224
|
+
'JourneyTimeline',
|
|
225
|
+
preset,
|
|
226
|
+
{ orientation, size } satisfies JourneyTimelineVariants,
|
|
227
|
+
slotClassesProp
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
function sc(styles: Styles | null, key: JourneyTimelineSlots, extra?: string | false) {
|
|
232
|
+
const override = [slotClasses?.[key], extra].filter(Boolean).join(' ') || undefined;
|
|
233
|
+
if (unstyled || !styles) return override;
|
|
234
|
+
return styles[key]({ class: override });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- misc derived ----------------------------------------------------------
|
|
238
|
+
const iconSize = $derived(size === 'sm' ? 14 : size === 'lg' ? 20 : 16);
|
|
239
|
+
const focusedItem = $derived(
|
|
240
|
+
items.find((it) => it.focusable !== false && it.id === activeFocusId)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const propsId = $props.id();
|
|
244
|
+
const uid = `journey-${propsId}`;
|
|
245
|
+
const detailDomId = (index: number) => `${uid}-detail-${index}`;
|
|
246
|
+
const panelId = `${uid}-panel`;
|
|
247
|
+
|
|
248
|
+
function statusLabel(status: JourneyStatus) {
|
|
249
|
+
return bt(`journeyTimeline.status.${status}`);
|
|
250
|
+
}
|
|
251
|
+
</script>
|
|
252
|
+
|
|
253
|
+
<!-- Marker glyph — decorative; the status is conveyed by the sr-only label. -->
|
|
254
|
+
{#snippet markerGlyph(item: JourneyNode)}
|
|
255
|
+
{#if item.status === 'complete'}
|
|
256
|
+
<CheckIcon size={iconSize} />
|
|
257
|
+
{:else if item.status === 'active'}
|
|
258
|
+
<CircleDotIcon size={iconSize} />
|
|
259
|
+
{:else if item.status === 'blocked'}
|
|
260
|
+
<BanIcon size={iconSize} />
|
|
261
|
+
{:else if item.status === 'skipped'}
|
|
262
|
+
<MinusIcon size={iconSize} />
|
|
263
|
+
{/if}
|
|
264
|
+
{/snippet}
|
|
265
|
+
|
|
266
|
+
{#snippet header(item: JourneyNode, styles: Styles | null)}
|
|
267
|
+
<span class={sc(styles, 'marker')}>{@render markerGlyph(item)}</span>
|
|
268
|
+
<span class={sc(styles, 'labelGroup')}>
|
|
269
|
+
<span class={sc(styles, 'title')}>{item.title}</span>
|
|
270
|
+
{#if item.subtitle}
|
|
271
|
+
<span class={sc(styles, 'subtitle')}>{item.subtitle}</span>
|
|
272
|
+
{/if}
|
|
273
|
+
</span>
|
|
274
|
+
<span class="sr-only">{statusLabel(item.status)}</span>
|
|
275
|
+
{/snippet}
|
|
276
|
+
|
|
277
|
+
{#snippet trigger(item: JourneyNode, index: number, focused: boolean, styles: Styles | null)}
|
|
278
|
+
{@const interactive = item.focusable !== false}
|
|
279
|
+
{#if interactive}
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
class={sc(styles, 'trigger')}
|
|
283
|
+
data-journey-trigger=""
|
|
284
|
+
data-node-id={item.id}
|
|
285
|
+
tabindex={item.id === tabbableId ? 0 : -1}
|
|
286
|
+
aria-expanded={node ? focused : undefined}
|
|
287
|
+
aria-controls={node
|
|
288
|
+
? orientation === 'horizontal'
|
|
289
|
+
? panelId
|
|
290
|
+
: detailDomId(index)
|
|
291
|
+
: undefined}
|
|
292
|
+
onclick={() => activate(item)}
|
|
293
|
+
>
|
|
294
|
+
{@render header(item, styles)}
|
|
295
|
+
</button>
|
|
296
|
+
{:else}
|
|
297
|
+
<div class={sc(styles, 'trigger')} data-node-id={item.id}>
|
|
298
|
+
{@render header(item, styles)}
|
|
299
|
+
</div>
|
|
300
|
+
{/if}
|
|
301
|
+
{/snippet}
|
|
302
|
+
|
|
303
|
+
<div
|
|
304
|
+
bind:this={rootRef}
|
|
305
|
+
class={sc(containerStyles, 'base', className)}
|
|
306
|
+
data-orientation={orientation}
|
|
307
|
+
{...restProps}
|
|
308
|
+
>
|
|
309
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
310
|
+
<ol
|
|
311
|
+
class={sc(containerStyles, 'rail')}
|
|
312
|
+
aria-label={bt('journeyTimeline.label')}
|
|
313
|
+
onkeydown={handleKeydown}
|
|
314
|
+
>
|
|
315
|
+
{#each items as item, index (item.id)}
|
|
316
|
+
{@const focused = item.focusable !== false && item.id === activeFocusId}
|
|
317
|
+
{@const styles = nodeStyles(item, focused)}
|
|
318
|
+
{#if orientation === 'horizontal'}
|
|
319
|
+
<li
|
|
320
|
+
class={sc(styles, 'node')}
|
|
321
|
+
data-journey-node=""
|
|
322
|
+
data-node-id={item.id}
|
|
323
|
+
aria-current={item.status === 'active' ? 'step' : undefined}
|
|
324
|
+
>
|
|
325
|
+
{@render trigger(item, index, focused, styles)}
|
|
326
|
+
<span data-journey-connector class={sc(styles, 'connector')} aria-hidden="true"></span>
|
|
327
|
+
</li>
|
|
328
|
+
{:else}
|
|
329
|
+
<li
|
|
330
|
+
class={sc(styles, 'node')}
|
|
331
|
+
data-journey-node=""
|
|
332
|
+
data-node-id={item.id}
|
|
333
|
+
aria-current={item.status === 'active' ? 'step' : undefined}
|
|
334
|
+
>
|
|
335
|
+
{@render trigger(item, index, focused, styles)}
|
|
336
|
+
<div class={sc(styles, 'body')}>
|
|
337
|
+
<div class={sc(styles, 'connectorColumn')}>
|
|
338
|
+
<span data-journey-connector class={sc(styles, 'connector')} aria-hidden="true"
|
|
339
|
+
></span>
|
|
340
|
+
</div>
|
|
341
|
+
{#if node && item.focusable !== false}
|
|
342
|
+
<div
|
|
343
|
+
id={detailDomId(index)}
|
|
344
|
+
role="region"
|
|
345
|
+
aria-label={item.title}
|
|
346
|
+
class={sc(styles, 'detail')}
|
|
347
|
+
style="grid-template-rows: {focused ? '1fr' : '0fr'}"
|
|
348
|
+
>
|
|
349
|
+
<div class={sc(styles, 'detailInner')}>
|
|
350
|
+
<div class={sc(styles, 'detailContent')}>
|
|
351
|
+
{#if focused}{@render node(item)}{/if}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
{/if}
|
|
356
|
+
</div>
|
|
357
|
+
</li>
|
|
358
|
+
{/if}
|
|
359
|
+
{/each}
|
|
360
|
+
</ol>
|
|
361
|
+
|
|
362
|
+
{#if orientation === 'horizontal' && node && focusedItem}
|
|
363
|
+
<div
|
|
364
|
+
id={panelId}
|
|
365
|
+
role="region"
|
|
366
|
+
aria-label={focusedItem.title}
|
|
367
|
+
class={sc(containerStyles, 'panel')}
|
|
368
|
+
>
|
|
369
|
+
{@render node(focusedItem)}
|
|
370
|
+
</div>
|
|
371
|
+
{/if}
|
|
372
|
+
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { JourneyTimelineSlots, JourneyTimelineVariants } from './journey-timeline.variants.js';
|
|
4
|
+
/** Lifecycle status of a journey node — drives marker colour and glyph. */
|
|
5
|
+
export type JourneyStatus = 'complete' | 'active' | 'pending' | 'blocked' | 'skipped';
|
|
6
|
+
/** A single node (waypoint) on a {@link JourneyTimelineProps | JourneyTimeline}. */
|
|
7
|
+
export interface JourneyNode {
|
|
8
|
+
/** Stable unique identifier — used as the focus key and for `{#each}` keying. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Node title, always visible next to the marker. */
|
|
11
|
+
title: string;
|
|
12
|
+
/**
|
|
13
|
+
* Lifecycle status. Maps to a semantic marker colour + glyph:
|
|
14
|
+
* `complete` (success ✓), `active` (primary ◉), `pending` (empty circle),
|
|
15
|
+
* `blocked` (danger ⊘), `skipped` (muted −).
|
|
16
|
+
*/
|
|
17
|
+
status: JourneyStatus;
|
|
18
|
+
/** Short status line shown below the title while collapsed. */
|
|
19
|
+
subtitle?: string;
|
|
20
|
+
/**
|
|
21
|
+
* When `false`, the node is a pure waypoint: it renders a marker + labels but
|
|
22
|
+
* cannot receive focus, does not expand, and is skipped by keyboard navigation.
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
focusable?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @description Connected timeline whose markers *are* the progress indicator and
|
|
29
|
+
* where exactly one focusable node expands to reveal rich per-step detail (the
|
|
30
|
+
* "journey" / travel-log pattern). Data-driven via `items`; the focused node's
|
|
31
|
+
* body is rendered through the `node` snippet. For a compact progress indicator
|
|
32
|
+
* without a detail container use `Stepper`; for peer views without sequence use
|
|
33
|
+
* `Tab`.
|
|
34
|
+
*
|
|
35
|
+
* @tag navigation
|
|
36
|
+
* @tag display
|
|
37
|
+
* @related Stepper
|
|
38
|
+
* @related Tab
|
|
39
|
+
* @related Accordion
|
|
40
|
+
* @stability beta
|
|
41
|
+
*
|
|
42
|
+
* @example Vertical journey with inline detail
|
|
43
|
+
* ```svelte
|
|
44
|
+
* <script lang="ts">
|
|
45
|
+
* import { JourneyTimeline, type JourneyNode } from '@urbicon-ui/blocks';
|
|
46
|
+
* const stages: JourneyNode[] = [
|
|
47
|
+
* { id: 'draft', title: 'Draft', status: 'complete', subtitle: 'Sent 3 Jun' },
|
|
48
|
+
* { id: 'review', title: 'Review', status: 'active', subtitle: 'In progress' },
|
|
49
|
+
* { id: 'approve', title: 'Approval', status: 'pending' }
|
|
50
|
+
* ];
|
|
51
|
+
* let focusId = $state('review');
|
|
52
|
+
* </script>
|
|
53
|
+
*
|
|
54
|
+
* <JourneyTimeline items={stages} bind:focusId>
|
|
55
|
+
* {#snippet node(item)}
|
|
56
|
+
* <p>Details for {item.title}…</p>
|
|
57
|
+
* {/snippet}
|
|
58
|
+
* </JourneyTimeline>
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example Horizontal lifecycle with a shared detail panel + scroll-spy off
|
|
62
|
+
* ```svelte
|
|
63
|
+
* <JourneyTimeline items={phases} orientation="horizontal" onFocusChange={(id) => track(id)}>
|
|
64
|
+
* {#snippet node(item)}
|
|
65
|
+
* <PhaseSummary phase={item.id} />
|
|
66
|
+
* {/snippet}
|
|
67
|
+
* </JourneyTimeline>
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export interface JourneyTimelineProps extends Pick<JourneyTimelineVariants, 'orientation' | 'size'>, Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
71
|
+
/** The ordered journey nodes. */
|
|
72
|
+
items: JourneyNode[];
|
|
73
|
+
/** Layout direction. Vertical expands detail inline; horizontal into a panel below the rail. @default 'vertical' */
|
|
74
|
+
orientation?: 'vertical' | 'horizontal';
|
|
75
|
+
/** Marker + label scale. @default 'md' */
|
|
76
|
+
size?: 'sm' | 'md' | 'lg';
|
|
77
|
+
/**
|
|
78
|
+
* The expanded (focused) node id. Supports `bind:focusId`. When omitted the
|
|
79
|
+
* component is uncontrolled and falls back to `defaultFocusId`, then the first
|
|
80
|
+
* `active` node, then the first focusable node.
|
|
81
|
+
*/
|
|
82
|
+
focusId?: string;
|
|
83
|
+
/** Initial focused node id in uncontrolled mode. Ignored once `focusId` is bound. */
|
|
84
|
+
defaultFocusId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* When `true`, the focus follows the node scrolled to the top of the viewport
|
|
87
|
+
* (the travel-log feel), driven by an IntersectionObserver. Reduced-motion safe.
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
scrollSpy?: boolean;
|
|
91
|
+
/** Fires when the focused node changes (click, keyboard, or scroll-spy). */
|
|
92
|
+
onFocusChange?: (id: string) => void;
|
|
93
|
+
/** Renders the body of the focused node. Receives the focused `JourneyNode`. */
|
|
94
|
+
node?: Snippet<[JourneyNode]>;
|
|
95
|
+
/** Extra classes merged onto the root element. */
|
|
96
|
+
class?: string;
|
|
97
|
+
/** Remove all default tv() classes. */
|
|
98
|
+
unstyled?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Per-slot class overrides. Slots: base | rail | node | trigger | marker |
|
|
101
|
+
* connectorColumn | connector | labelGroup | title | subtitle | body | detail |
|
|
102
|
+
* detailInner | detailContent | panel
|
|
103
|
+
*/
|
|
104
|
+
slotClasses?: Partial<Record<JourneyTimelineSlots, string>>;
|
|
105
|
+
/**
|
|
106
|
+
* Apply a named preset registered via `<BlocksProvider presets={{ JourneyTimeline: {...} }}>`.
|
|
107
|
+
* Prefer this over `class` overrides when the requested look falls outside the
|
|
108
|
+
* semantic intent palette — presets keep hover/active/dark-mode logic coherent
|
|
109
|
+
* and make the custom look reusable across the project.
|
|
110
|
+
*/
|
|
111
|
+
preset?: string;
|
|
112
|
+
}
|
|
113
|
+
export { default as JourneyTimeline } from './JourneyTimeline.svelte';
|
|
114
|
+
export { type JourneyTimelineVariants, journeyTimelineVariants } from './journey-timeline.variants.js';
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { type SlotNames, type VariantProps } from '../../utils/variants.js';
|
|
2
|
+
export declare const journeyTimelineVariants: (props?: import("../../utils/variants.js").TVProps<{
|
|
3
|
+
orientation: {
|
|
4
|
+
vertical: {
|
|
5
|
+
rail: string;
|
|
6
|
+
};
|
|
7
|
+
horizontal: {
|
|
8
|
+
base: string;
|
|
9
|
+
rail: string;
|
|
10
|
+
node: string;
|
|
11
|
+
trigger: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
size: {
|
|
15
|
+
sm: {
|
|
16
|
+
marker: string;
|
|
17
|
+
connectorColumn: string;
|
|
18
|
+
trigger: string;
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle: string;
|
|
21
|
+
detailContent: string;
|
|
22
|
+
};
|
|
23
|
+
md: {
|
|
24
|
+
marker: string;
|
|
25
|
+
connectorColumn: string;
|
|
26
|
+
trigger: string;
|
|
27
|
+
title: string;
|
|
28
|
+
subtitle: string;
|
|
29
|
+
detailContent: string;
|
|
30
|
+
};
|
|
31
|
+
lg: {
|
|
32
|
+
marker: string;
|
|
33
|
+
connectorColumn: string;
|
|
34
|
+
trigger: string;
|
|
35
|
+
title: string;
|
|
36
|
+
subtitle: string;
|
|
37
|
+
detailContent: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
status: {
|
|
41
|
+
complete: {
|
|
42
|
+
marker: string;
|
|
43
|
+
};
|
|
44
|
+
active: {
|
|
45
|
+
marker: string;
|
|
46
|
+
title: string;
|
|
47
|
+
};
|
|
48
|
+
pending: {
|
|
49
|
+
marker: string;
|
|
50
|
+
};
|
|
51
|
+
blocked: {
|
|
52
|
+
marker: string;
|
|
53
|
+
title: string;
|
|
54
|
+
};
|
|
55
|
+
skipped: {
|
|
56
|
+
marker: string;
|
|
57
|
+
title: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
focused: {
|
|
61
|
+
true: {
|
|
62
|
+
trigger: string;
|
|
63
|
+
title: string;
|
|
64
|
+
};
|
|
65
|
+
false: {};
|
|
66
|
+
};
|
|
67
|
+
interactive: {
|
|
68
|
+
true: {
|
|
69
|
+
trigger: string;
|
|
70
|
+
};
|
|
71
|
+
false: {
|
|
72
|
+
trigger: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
connectorComplete: {
|
|
76
|
+
true: {
|
|
77
|
+
connector: string;
|
|
78
|
+
};
|
|
79
|
+
false: {};
|
|
80
|
+
};
|
|
81
|
+
}> | undefined) => {
|
|
82
|
+
base: (props?: ({
|
|
83
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
84
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
85
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
86
|
+
focused?: boolean | undefined;
|
|
87
|
+
interactive?: boolean | undefined;
|
|
88
|
+
connectorComplete?: boolean | undefined;
|
|
89
|
+
} & {
|
|
90
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
91
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
92
|
+
}) | undefined) => string;
|
|
93
|
+
rail: (props?: ({
|
|
94
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
95
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
96
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
97
|
+
focused?: boolean | undefined;
|
|
98
|
+
interactive?: boolean | undefined;
|
|
99
|
+
connectorComplete?: boolean | undefined;
|
|
100
|
+
} & {
|
|
101
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
102
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
103
|
+
}) | undefined) => string;
|
|
104
|
+
node: (props?: ({
|
|
105
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
106
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
107
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
108
|
+
focused?: boolean | undefined;
|
|
109
|
+
interactive?: boolean | undefined;
|
|
110
|
+
connectorComplete?: boolean | undefined;
|
|
111
|
+
} & {
|
|
112
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
113
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
114
|
+
}) | undefined) => string;
|
|
115
|
+
trigger: (props?: ({
|
|
116
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
117
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
118
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
119
|
+
focused?: boolean | undefined;
|
|
120
|
+
interactive?: boolean | undefined;
|
|
121
|
+
connectorComplete?: boolean | undefined;
|
|
122
|
+
} & {
|
|
123
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
124
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
125
|
+
}) | undefined) => string;
|
|
126
|
+
marker: (props?: ({
|
|
127
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
128
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
129
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
130
|
+
focused?: boolean | undefined;
|
|
131
|
+
interactive?: boolean | undefined;
|
|
132
|
+
connectorComplete?: boolean | undefined;
|
|
133
|
+
} & {
|
|
134
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
135
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
136
|
+
}) | undefined) => string;
|
|
137
|
+
connectorColumn: (props?: ({
|
|
138
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
139
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
140
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
141
|
+
focused?: boolean | undefined;
|
|
142
|
+
interactive?: boolean | undefined;
|
|
143
|
+
connectorComplete?: boolean | undefined;
|
|
144
|
+
} & {
|
|
145
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
146
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
147
|
+
}) | undefined) => string;
|
|
148
|
+
connector: (props?: ({
|
|
149
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
150
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
151
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
152
|
+
focused?: boolean | undefined;
|
|
153
|
+
interactive?: boolean | undefined;
|
|
154
|
+
connectorComplete?: boolean | undefined;
|
|
155
|
+
} & {
|
|
156
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
157
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
158
|
+
}) | undefined) => string;
|
|
159
|
+
labelGroup: (props?: ({
|
|
160
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
161
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
162
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
163
|
+
focused?: boolean | undefined;
|
|
164
|
+
interactive?: boolean | undefined;
|
|
165
|
+
connectorComplete?: boolean | undefined;
|
|
166
|
+
} & {
|
|
167
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
168
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
169
|
+
}) | undefined) => string;
|
|
170
|
+
title: (props?: ({
|
|
171
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
172
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
173
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
174
|
+
focused?: boolean | undefined;
|
|
175
|
+
interactive?: boolean | undefined;
|
|
176
|
+
connectorComplete?: boolean | undefined;
|
|
177
|
+
} & {
|
|
178
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
179
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
180
|
+
}) | undefined) => string;
|
|
181
|
+
subtitle: (props?: ({
|
|
182
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
183
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
184
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
185
|
+
focused?: boolean | undefined;
|
|
186
|
+
interactive?: boolean | undefined;
|
|
187
|
+
connectorComplete?: boolean | undefined;
|
|
188
|
+
} & {
|
|
189
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
190
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
191
|
+
}) | undefined) => string;
|
|
192
|
+
body: (props?: ({
|
|
193
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
194
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
195
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
196
|
+
focused?: boolean | undefined;
|
|
197
|
+
interactive?: boolean | undefined;
|
|
198
|
+
connectorComplete?: boolean | undefined;
|
|
199
|
+
} & {
|
|
200
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
201
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
202
|
+
}) | undefined) => string;
|
|
203
|
+
detail: (props?: ({
|
|
204
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
205
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
206
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
207
|
+
focused?: boolean | undefined;
|
|
208
|
+
interactive?: boolean | undefined;
|
|
209
|
+
connectorComplete?: boolean | undefined;
|
|
210
|
+
} & {
|
|
211
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
212
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
213
|
+
}) | undefined) => string;
|
|
214
|
+
detailInner: (props?: ({
|
|
215
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
216
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
217
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
218
|
+
focused?: boolean | undefined;
|
|
219
|
+
interactive?: boolean | undefined;
|
|
220
|
+
connectorComplete?: boolean | undefined;
|
|
221
|
+
} & {
|
|
222
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
223
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
224
|
+
}) | undefined) => string;
|
|
225
|
+
detailContent: (props?: ({
|
|
226
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
227
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
228
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
229
|
+
focused?: boolean | undefined;
|
|
230
|
+
interactive?: boolean | undefined;
|
|
231
|
+
connectorComplete?: boolean | undefined;
|
|
232
|
+
} & {
|
|
233
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
234
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
235
|
+
}) | undefined) => string;
|
|
236
|
+
panel: (props?: ({
|
|
237
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
238
|
+
size?: "sm" | "md" | "lg" | undefined;
|
|
239
|
+
status?: "active" | "blocked" | "complete" | "pending" | "skipped" | undefined;
|
|
240
|
+
focused?: boolean | undefined;
|
|
241
|
+
interactive?: boolean | undefined;
|
|
242
|
+
connectorComplete?: boolean | undefined;
|
|
243
|
+
} & {
|
|
244
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
245
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
246
|
+
}) | undefined) => string;
|
|
247
|
+
};
|
|
248
|
+
export type JourneyTimelineVariants = VariantProps<typeof journeyTimelineVariants>;
|
|
249
|
+
/** Slot names derived from the tv() config — single source of truth for slotClasses. */
|
|
250
|
+
export type JourneyTimelineSlots = SlotNames<typeof journeyTimelineVariants>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { tv } from '../../utils/variants.js';
|
|
2
|
+
export const journeyTimelineVariants = tv({
|
|
3
|
+
slots: {
|
|
4
|
+
// Root wrapper. Always a <div> so the same element hosts the marker rail
|
|
5
|
+
// AND (horizontal only) the shared detail panel beneath it. The last node's
|
|
6
|
+
// connector is hidden in both orientations — nothing to connect onward to.
|
|
7
|
+
base: 'w-full [&_[data-journey-node]:last-child_[data-journey-connector]]:hidden',
|
|
8
|
+
// The ordered list of nodes. `flex-col` vertical, `flex-row` horizontal.
|
|
9
|
+
rail: 'flex list-none m-0 p-0',
|
|
10
|
+
// A single node <li>.
|
|
11
|
+
node: 'group/node',
|
|
12
|
+
// The interactive header — marker + title/subtitle. A <button> for focusable
|
|
13
|
+
// nodes, a plain <div> for pure waypoints (focusable === false).
|
|
14
|
+
trigger: [
|
|
15
|
+
'flex items-center text-left w-full bg-transparent border-0 appearance-none p-1 -m-1',
|
|
16
|
+
'rounded-contain',
|
|
17
|
+
'transition-[color,background-color] duration-[var(--blocks-duration-fast)]',
|
|
18
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-base'
|
|
19
|
+
],
|
|
20
|
+
// The status-coloured circle.
|
|
21
|
+
marker: [
|
|
22
|
+
'flex items-center justify-center shrink-0 font-semibold rounded-commit border-2 box-border',
|
|
23
|
+
'transition-[color,background-color,border-color,box-shadow] duration-[var(--blocks-duration-fast)]'
|
|
24
|
+
],
|
|
25
|
+
// Column (marker width) that carries the vertical connector below a marker.
|
|
26
|
+
// `flex-col` is essential: it makes the connector's `flex-1` grow *down* the
|
|
27
|
+
// column (a thin vertical line) instead of *across* it (a fat block).
|
|
28
|
+
connectorColumn: 'flex flex-col shrink-0 items-center',
|
|
29
|
+
// The connecting line between markers.
|
|
30
|
+
connector: [
|
|
31
|
+
'bg-border-subtle',
|
|
32
|
+
'transition-[background-color] duration-[var(--blocks-duration-normal)]'
|
|
33
|
+
],
|
|
34
|
+
// Title + subtitle wrapper.
|
|
35
|
+
labelGroup: 'flex flex-col min-w-0',
|
|
36
|
+
title: [
|
|
37
|
+
'font-medium leading-tight truncate',
|
|
38
|
+
'transition-colors duration-[var(--blocks-duration-fast)]'
|
|
39
|
+
],
|
|
40
|
+
subtitle: 'text-text-tertiary leading-tight mt-0.5 truncate',
|
|
41
|
+
// Lower row (vertical only): connector column + inline detail region.
|
|
42
|
+
body: 'flex',
|
|
43
|
+
// Grid-rows collapse wrapper (0fr → 1fr). Reduced-motion safe: the duration
|
|
44
|
+
// token collapses to 1ms under prefers-reduced-motion (interaction.css).
|
|
45
|
+
detail: [
|
|
46
|
+
'grid',
|
|
47
|
+
'transition-[grid-template-rows] duration-[var(--blocks-duration-normal)] ease-[var(--blocks-ease-smooth)]'
|
|
48
|
+
],
|
|
49
|
+
detailInner: 'overflow-hidden min-h-0',
|
|
50
|
+
detailContent: 'text-text-secondary',
|
|
51
|
+
// Shared detail panel (horizontal only) rendered beneath the rail.
|
|
52
|
+
panel: 'text-text-secondary border border-border-subtle rounded-contain bg-surface-quiet'
|
|
53
|
+
},
|
|
54
|
+
variants: {
|
|
55
|
+
orientation: {
|
|
56
|
+
vertical: {
|
|
57
|
+
rail: 'flex-col'
|
|
58
|
+
},
|
|
59
|
+
horizontal: {
|
|
60
|
+
// Column layout so the shared detail panel sits below the rail.
|
|
61
|
+
base: 'flex flex-col',
|
|
62
|
+
rail: 'flex-row items-start',
|
|
63
|
+
// Each node grows to spread markers evenly; the last one hugs its content.
|
|
64
|
+
node: 'flex items-center [&:not(:last-child)]:flex-1',
|
|
65
|
+
// Compact header (marker + label side by side) so the connector can grow.
|
|
66
|
+
trigger: 'w-auto shrink-0'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
size: {
|
|
70
|
+
sm: {
|
|
71
|
+
marker: 'size-7 text-xs',
|
|
72
|
+
connectorColumn: 'w-7',
|
|
73
|
+
trigger: 'gap-2',
|
|
74
|
+
title: 'text-xs',
|
|
75
|
+
subtitle: 'text-[11px]',
|
|
76
|
+
detailContent: 'text-sm pl-2'
|
|
77
|
+
},
|
|
78
|
+
md: {
|
|
79
|
+
marker: 'size-9 text-sm',
|
|
80
|
+
connectorColumn: 'w-9',
|
|
81
|
+
trigger: 'gap-2.5',
|
|
82
|
+
title: 'text-sm',
|
|
83
|
+
subtitle: 'text-xs',
|
|
84
|
+
detailContent: 'text-sm pl-2.5'
|
|
85
|
+
},
|
|
86
|
+
lg: {
|
|
87
|
+
marker: 'size-11 text-base',
|
|
88
|
+
connectorColumn: 'w-11',
|
|
89
|
+
trigger: 'gap-3',
|
|
90
|
+
title: 'text-base',
|
|
91
|
+
subtitle: 'text-sm',
|
|
92
|
+
detailContent: 'text-base pl-3'
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
// Marker colour + title tone per journey status. Mirrors the semantic intent
|
|
96
|
+
// palette (success / primary / danger / neutral) used across Badge, Alert,
|
|
97
|
+
// Progress and Stepper.
|
|
98
|
+
status: {
|
|
99
|
+
complete: {
|
|
100
|
+
marker: 'border-success bg-success text-text-on-primary'
|
|
101
|
+
},
|
|
102
|
+
active: {
|
|
103
|
+
marker: 'border-primary bg-primary text-text-on-primary shadow-[var(--blocks-shadow-sm)]',
|
|
104
|
+
title: 'text-text-primary'
|
|
105
|
+
},
|
|
106
|
+
pending: {
|
|
107
|
+
marker: 'border-border-default bg-surface-base text-text-tertiary'
|
|
108
|
+
},
|
|
109
|
+
blocked: {
|
|
110
|
+
marker: 'border-danger bg-danger text-text-on-primary',
|
|
111
|
+
title: 'text-danger'
|
|
112
|
+
},
|
|
113
|
+
skipped: {
|
|
114
|
+
marker: 'border-border-subtle bg-surface-subtle text-text-tertiary opacity-70',
|
|
115
|
+
title: 'text-text-tertiary'
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
// The currently expanded node. Distinct from DOM focus (focus-visible ring).
|
|
119
|
+
focused: {
|
|
120
|
+
true: {
|
|
121
|
+
trigger: 'bg-surface-subtle',
|
|
122
|
+
title: 'text-text-primary font-semibold'
|
|
123
|
+
},
|
|
124
|
+
false: {}
|
|
125
|
+
},
|
|
126
|
+
// Focusable nodes react to hover + show a pointer; pure waypoints do not.
|
|
127
|
+
interactive: {
|
|
128
|
+
true: {
|
|
129
|
+
trigger: 'cursor-pointer hover:bg-surface-hover'
|
|
130
|
+
},
|
|
131
|
+
false: {
|
|
132
|
+
trigger: 'cursor-default'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
// The connector segment leading out of a completed node reads as "travelled".
|
|
136
|
+
connectorComplete: {
|
|
137
|
+
true: {
|
|
138
|
+
connector: 'bg-success'
|
|
139
|
+
},
|
|
140
|
+
false: {}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
compoundVariants: [
|
|
144
|
+
// Vertical connector geometry: a thin line that grows down the marker column.
|
|
145
|
+
{
|
|
146
|
+
orientation: 'vertical',
|
|
147
|
+
class: {
|
|
148
|
+
connector: 'w-0.5 flex-1 my-1 min-h-4 rounded-commit'
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
// Horizontal connector geometry: a thin line filling the gap to the next marker.
|
|
152
|
+
{
|
|
153
|
+
orientation: 'horizontal',
|
|
154
|
+
class: {
|
|
155
|
+
connector: 'h-0.5 flex-1 mx-2 self-center rounded-commit'
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
// A focused pure waypoint keeps a bold title but gains no hover surface.
|
|
159
|
+
{
|
|
160
|
+
interactive: false,
|
|
161
|
+
focused: true,
|
|
162
|
+
class: {
|
|
163
|
+
trigger: 'bg-transparent'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
defaultVariants: {
|
|
168
|
+
orientation: 'vertical',
|
|
169
|
+
size: 'md',
|
|
170
|
+
status: 'pending',
|
|
171
|
+
focused: false,
|
|
172
|
+
interactive: true,
|
|
173
|
+
connectorComplete: false
|
|
174
|
+
}
|
|
175
|
+
});
|
|
@@ -30,6 +30,8 @@ export type { FormFieldProps, FormFieldSlotContext } from './FormField/index.js'
|
|
|
30
30
|
export * from './FormField/index.js';
|
|
31
31
|
export type { InputProps } from './Input/index.js';
|
|
32
32
|
export * from './Input/index.js';
|
|
33
|
+
export type { JourneyNode, JourneyStatus, JourneyTimelineProps } from './JourneyTimeline/index.js';
|
|
34
|
+
export * from './JourneyTimeline/index.js';
|
|
33
35
|
export type { MenuContext, MenuCustomSlots, MenuItemType, MenuObjectOption, MenuOption, MenuProps, MenuSectionHeader } from './Menu/index.js';
|
|
34
36
|
export * from './Menu/index.js';
|
|
35
37
|
export type { PaginationItemProps, PaginationProps } from './Pagination/index.js';
|
package/dist/primitives/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './Dialog/index.js';
|
|
|
16
16
|
export * from './Drawer/index.js';
|
|
17
17
|
export * from './FormField/index.js';
|
|
18
18
|
export * from './Input/index.js';
|
|
19
|
+
export * from './JourneyTimeline/index.js';
|
|
19
20
|
export * from './Menu/index.js';
|
|
20
21
|
export * from './Pagination/index.js';
|
|
21
22
|
export * from './Popover/index.js';
|
|
@@ -129,6 +129,16 @@ declare const _default: {
|
|
|
129
129
|
readonly filterPlaceholder: "Themen filtern…";
|
|
130
130
|
readonly noResults: "Keine passenden Themen";
|
|
131
131
|
};
|
|
132
|
+
readonly journeyTimeline: {
|
|
133
|
+
readonly label: "Verlauf";
|
|
134
|
+
readonly status: {
|
|
135
|
+
readonly complete: "Abgeschlossen";
|
|
136
|
+
readonly active: "In Bearbeitung";
|
|
137
|
+
readonly pending: "Ausstehend";
|
|
138
|
+
readonly blocked: "Blockiert";
|
|
139
|
+
readonly skipped: "Übersprungen";
|
|
140
|
+
};
|
|
141
|
+
};
|
|
132
142
|
readonly datepicker: {
|
|
133
143
|
readonly placeholder: "Datum wählen...";
|
|
134
144
|
readonly rangePlaceholder: "Zeitraum wählen...";
|
package/dist/translations/de.js
CHANGED
|
@@ -129,6 +129,16 @@ export default {
|
|
|
129
129
|
filterPlaceholder: 'Themen filtern…',
|
|
130
130
|
noResults: 'Keine passenden Themen'
|
|
131
131
|
},
|
|
132
|
+
journeyTimeline: {
|
|
133
|
+
label: 'Verlauf',
|
|
134
|
+
status: {
|
|
135
|
+
complete: 'Abgeschlossen',
|
|
136
|
+
active: 'In Bearbeitung',
|
|
137
|
+
pending: 'Ausstehend',
|
|
138
|
+
blocked: 'Blockiert',
|
|
139
|
+
skipped: 'Übersprungen'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
132
142
|
datepicker: {
|
|
133
143
|
placeholder: 'Datum wählen...',
|
|
134
144
|
rangePlaceholder: 'Zeitraum wählen...',
|
|
@@ -129,6 +129,16 @@ declare const _default: {
|
|
|
129
129
|
readonly filterPlaceholder: "Filter topics…";
|
|
130
130
|
readonly noResults: "No matching topics";
|
|
131
131
|
};
|
|
132
|
+
readonly journeyTimeline: {
|
|
133
|
+
readonly label: "Journey";
|
|
134
|
+
readonly status: {
|
|
135
|
+
readonly complete: "Completed";
|
|
136
|
+
readonly active: "In progress";
|
|
137
|
+
readonly pending: "Pending";
|
|
138
|
+
readonly blocked: "Blocked";
|
|
139
|
+
readonly skipped: "Skipped";
|
|
140
|
+
};
|
|
141
|
+
};
|
|
132
142
|
readonly datepicker: {
|
|
133
143
|
readonly placeholder: "Select a date...";
|
|
134
144
|
readonly rangePlaceholder: "Select a date range...";
|
package/dist/translations/en.js
CHANGED
|
@@ -129,6 +129,16 @@ export default {
|
|
|
129
129
|
filterPlaceholder: 'Filter topics…',
|
|
130
130
|
noResults: 'No matching topics'
|
|
131
131
|
},
|
|
132
|
+
journeyTimeline: {
|
|
133
|
+
label: 'Journey',
|
|
134
|
+
status: {
|
|
135
|
+
complete: 'Completed',
|
|
136
|
+
active: 'In progress',
|
|
137
|
+
pending: 'Pending',
|
|
138
|
+
blocked: 'Blocked',
|
|
139
|
+
skipped: 'Skipped'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
132
142
|
datepicker: {
|
|
133
143
|
placeholder: 'Select a date...',
|
|
134
144
|
rangePlaceholder: 'Select a date range...',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"@sveltejs/package": "^2.5.8",
|
|
92
92
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
93
93
|
"@tailwindcss/vite": "^4.3.1",
|
|
94
|
-
"@urbicon-ui/i18n": "6.
|
|
95
|
-
"@urbicon-ui/shared-types": "6.
|
|
94
|
+
"@urbicon-ui/i18n": "6.9.0",
|
|
95
|
+
"@urbicon-ui/shared-types": "6.9.0",
|
|
96
96
|
"prettier": "^3.8.4",
|
|
97
97
|
"prettier-plugin-svelte": "^4.1.1",
|
|
98
98
|
"prettier-plugin-tailwindcss": "^0.8.0",
|