sv5ui 2.2.0 → 2.3.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.
Files changed (58) hide show
  1. package/dist/components/Error/Error.svelte +89 -0
  2. package/dist/components/Error/Error.svelte.d.ts +5 -0
  3. package/dist/components/Error/error.types.d.ts +87 -0
  4. package/dist/components/Error/error.types.js +1 -0
  5. package/dist/components/Error/error.variants.d.ts +66 -0
  6. package/dist/components/Error/error.variants.js +16 -0
  7. package/dist/components/Error/index.d.ts +2 -0
  8. package/dist/components/Error/index.js +1 -0
  9. package/dist/components/Footer/Footer.svelte +71 -0
  10. package/dist/components/Footer/Footer.svelte.d.ts +5 -0
  11. package/dist/components/Footer/FooterColumns.svelte +110 -0
  12. package/dist/components/Footer/FooterColumns.svelte.d.ts +5 -0
  13. package/dist/components/Footer/footer-columns.types.d.ts +73 -0
  14. package/dist/components/Footer/footer-columns.types.js +1 -0
  15. package/dist/components/Footer/footer-columns.variants.d.ts +108 -0
  16. package/dist/components/Footer/footer-columns.variants.js +23 -0
  17. package/dist/components/Footer/footer.types.d.ts +46 -0
  18. package/dist/components/Footer/footer.types.js +1 -0
  19. package/dist/components/Footer/footer.variants.d.ts +66 -0
  20. package/dist/components/Footer/footer.variants.js +16 -0
  21. package/dist/components/Footer/index.d.ts +4 -0
  22. package/dist/components/Footer/index.js +2 -0
  23. package/dist/components/Header/Header.svelte +177 -0
  24. package/dist/components/Header/Header.svelte.d.ts +5 -0
  25. package/dist/components/Header/header.types.d.ts +124 -0
  26. package/dist/components/Header/header.types.js +1 -0
  27. package/dist/components/Header/header.variants.d.ts +108 -0
  28. package/dist/components/Header/header.variants.js +32 -0
  29. package/dist/components/Header/index.d.ts +2 -0
  30. package/dist/components/Header/index.js +1 -0
  31. package/dist/components/Main/Main.svelte +20 -0
  32. package/dist/components/Main/Main.svelte.d.ts +5 -0
  33. package/dist/components/Main/index.d.ts +2 -0
  34. package/dist/components/Main/index.js +1 -0
  35. package/dist/components/Main/main.types.d.ts +27 -0
  36. package/dist/components/Main/main.types.js +1 -0
  37. package/dist/components/Main/main.variants.d.ts +30 -0
  38. package/dist/components/Main/main.variants.js +10 -0
  39. package/dist/components/Tour/Tour.svelte +459 -0
  40. package/dist/components/Tour/Tour.svelte.d.ts +5 -0
  41. package/dist/components/Tour/index.d.ts +2 -0
  42. package/dist/components/Tour/index.js +1 -0
  43. package/dist/components/Tour/tour.types.d.ts +457 -0
  44. package/dist/components/Tour/tour.types.js +1 -0
  45. package/dist/components/Tour/tour.variants.d.ts +208 -0
  46. package/dist/components/Tour/tour.variants.js +56 -0
  47. package/dist/config.d.ts +2 -0
  48. package/dist/config.js +2 -0
  49. package/dist/hooks/index.d.ts +1 -0
  50. package/dist/hooks/index.js +1 -0
  51. package/dist/hooks/useTour/index.d.ts +1 -0
  52. package/dist/hooks/useTour/index.js +1 -0
  53. package/dist/hooks/useTour/useTour.svelte.d.ts +29 -0
  54. package/dist/hooks/useTour/useTour.svelte.js +194 -0
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.js +5 -0
  57. package/dist/theme.css +3 -0
  58. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ export declare const mainVariants: import("tailwind-variants").TVReturnType<{
2
+ [key: string]: {
3
+ [key: string]: import("tailwind-merge").ClassNameValue | {
4
+ root?: import("tailwind-merge").ClassNameValue;
5
+ };
6
+ };
7
+ } | {
8
+ [x: string]: {
9
+ [x: string]: import("tailwind-merge").ClassNameValue | {
10
+ root?: import("tailwind-merge").ClassNameValue;
11
+ };
12
+ };
13
+ } | {}, {
14
+ root: string;
15
+ }, undefined, {
16
+ [key: string]: {
17
+ [key: string]: import("tailwind-merge").ClassNameValue | {
18
+ root?: import("tailwind-merge").ClassNameValue;
19
+ };
20
+ };
21
+ } | {}, {
22
+ root: string;
23
+ }, import("tailwind-variants").TVReturnType<unknown, {
24
+ root: string;
25
+ }, undefined, unknown, unknown, undefined>>;
26
+ export type MainSlots = keyof ReturnType<typeof mainVariants>;
27
+ export declare const mainDefaults: {
28
+ defaultVariants: Record<string, never>;
29
+ slots: Partial<Record<MainSlots, string>>;
30
+ };
@@ -0,0 +1,10 @@
1
+ import { tv } from 'tailwind-variants';
2
+ export const mainVariants = tv({
3
+ slots: {
4
+ root: 'min-h-[calc(100svh-var(--ui-header-height,0px))]'
5
+ }
6
+ });
7
+ export const mainDefaults = {
8
+ defaultVariants: {},
9
+ slots: {}
10
+ };
@@ -0,0 +1,459 @@
1
+ <script lang="ts" module>let uidCounter = 0;
2
+ </script>
3
+
4
+ <script lang="ts">import { untrack } from "svelte";
5
+ import { Portal } from "bits-ui";
6
+ import { computePosition, autoUpdate, offset, flip, shift, arrow } from "@floating-ui/dom";
7
+ import { tourVariants, tourDefaults } from "./tour.variants.js";
8
+ import { getComponentConfig, iconsDefaults } from "../../config.js";
9
+ import { useTour } from "../../hooks/useTour/index.js";
10
+ import { useFocusTrap } from "../../hooks/useFocusTrap/index.js";
11
+ import { useEventListener } from "../../hooks/useEventListener/index.js";
12
+ import { useMediaQuery } from "../../hooks/useMediaQuery/index.js";
13
+ import Button from "../Button/Button.svelte";
14
+ const config = getComponentConfig("tour", tourDefaults);
15
+ const icons = getComponentConfig("icons", iconsDefaults);
16
+ let {
17
+ steps,
18
+ controller,
19
+ api = $bindable(),
20
+ defaultStep = 0,
21
+ onOpenChange,
22
+ onStepChange,
23
+ onStart,
24
+ onComplete,
25
+ onSkip,
26
+ placement = "bottom",
27
+ sideOffset = 12,
28
+ spotlight = true,
29
+ spotlightPadding = 8,
30
+ spotlightRadius = 8,
31
+ spotlightInteractable = false,
32
+ scrollIntoView = true,
33
+ waitForTarget = 2e3,
34
+ dismissible = false,
35
+ arrow: showArrow = true,
36
+ prevLabel = "Back",
37
+ nextLabel = "Next",
38
+ doneLabel = "Done",
39
+ skipLabel = "Skip",
40
+ showProgress = true,
41
+ showSkip = true,
42
+ size = "md",
43
+ transition = config.defaultVariants.transition ?? true,
44
+ portal = true,
45
+ class: className,
46
+ ui,
47
+ header: headerSlot,
48
+ footer: footerSlot,
49
+ content: contentSlot,
50
+ ...restProps
51
+ } = $props();
52
+ const ctrl = untrack(() => controller) ?? useTour({
53
+ get steps() {
54
+ return steps;
55
+ },
56
+ get defaultStep() {
57
+ return defaultStep;
58
+ },
59
+ get onStart() {
60
+ return onStart;
61
+ },
62
+ get onComplete() {
63
+ return onComplete;
64
+ },
65
+ get onSkip() {
66
+ return onSkip;
67
+ },
68
+ get onStepChange() {
69
+ return onStepChange;
70
+ }
71
+ });
72
+ $effect(() => {
73
+ api = ctrl;
74
+ });
75
+ const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
76
+ const animate = $derived(transition && !reduceMotion.matches);
77
+ const active = $derived(ctrl.isActive);
78
+ const index = $derived(ctrl.currentIndex);
79
+ const step = $derived(index >= 0 ? steps[index] : void 0);
80
+ const uid = `sv5ui-tour-${++uidCounter}`;
81
+ const titleId = `${uid}-title`;
82
+ const descId = `${uid}-desc`;
83
+ let panelEl = $state(null);
84
+ let arrowEl = $state(null);
85
+ let targetEl = $state(null);
86
+ let rect = $state(null);
87
+ let panelPos = $state({ x: 0, y: 0 });
88
+ let arrowPos = $state({});
89
+ let resolvedPlacement = $state(untrack(() => placement));
90
+ let ready = $state(false);
91
+ const side = $derived(resolvedPlacement.split("-")[0]);
92
+ const stepPadding = $derived(step?.spotlightPadding ?? spotlightPadding);
93
+ const stepRadius = $derived(step?.spotlightRadius ?? spotlightRadius);
94
+ const stepInteractable = $derived(step?.spotlightInteractable ?? spotlightInteractable);
95
+ const showSpotlight = $derived(spotlight && !step?.disableSpotlight && !!targetEl && !!rect);
96
+ const isDialog = $derived(!step?.target || !targetEl);
97
+ const variantSlots = $derived(tourVariants({ size, transition: animate }));
98
+ const classes = $derived({
99
+ overlay: variantSlots.overlay({ class: [config.slots.overlay, ui?.overlay] }),
100
+ spotlight: variantSlots.spotlight({ class: [config.slots.spotlight, ui?.spotlight] }),
101
+ panel: variantSlots.panel({
102
+ class: [config.slots.panel, ui?.panel, className, step?.class]
103
+ }),
104
+ arrow: variantSlots.arrow({ class: [config.slots.arrow, ui?.arrow] }),
105
+ header: variantSlots.header({ class: [config.slots.header, ui?.header] }),
106
+ title: variantSlots.title({ class: [config.slots.title, ui?.title] }),
107
+ description: variantSlots.description({
108
+ class: [config.slots.description, ui?.description]
109
+ }),
110
+ body: variantSlots.body({ class: [config.slots.body, ui?.body] }),
111
+ footer: variantSlots.footer({ class: [config.slots.footer, ui?.footer] }),
112
+ progress: variantSlots.progress({ class: [config.slots.progress, ui?.progress] }),
113
+ nav: variantSlots.nav({ class: [config.slots.nav, ui?.nav] }),
114
+ skipButton: variantSlots.skipButton({ class: [config.slots.skipButton, ui?.skipButton] }),
115
+ closeButton: variantSlots.closeButton({
116
+ class: [config.slots.closeButton, ui?.closeButton]
117
+ })
118
+ });
119
+ function normalizeWait(value) {
120
+ if (value === void 0)
121
+ return typeof waitForTarget === "number" ? waitForTarget : waitForTarget ? 2e3 : 0;
122
+ if (value === true) return 2e3;
123
+ if (value === false) return 0;
124
+ return value;
125
+ }
126
+ async function resolveTarget(target, waitMs) {
127
+ if (target === null || target === void 0) return null;
128
+ if (target instanceof HTMLElement) return target;
129
+ const get = typeof target === "function" ? target : () => document.querySelector(target);
130
+ const first = get();
131
+ if (first || waitMs <= 0) return first;
132
+ return new Promise((resolve) => {
133
+ const start = performance.now();
134
+ const tick = () => {
135
+ const el = get();
136
+ if (el) return resolve(el);
137
+ if (performance.now() - start >= waitMs) return resolve(null);
138
+ requestAnimationFrame(tick);
139
+ };
140
+ requestAnimationFrame(tick);
141
+ });
142
+ }
143
+ const effectivePlacement = $derived(step?.placement ?? placement);
144
+ const effectiveOffset = $derived((step?.sideOffset ?? sideOffset) + (showArrow ? 6 : 0));
145
+ async function position() {
146
+ if (!targetEl || !panelEl) return;
147
+ const middleware = [
148
+ offset(effectiveOffset),
149
+ flip({ padding: 8, fallbackAxisSideDirection: "start" }),
150
+ shift({ padding: 8 })
151
+ ];
152
+ if (showArrow && arrowEl) middleware.push(arrow({ element: arrowEl, padding: 8 }));
153
+ const result = await computePosition(targetEl, panelEl, {
154
+ strategy: "fixed",
155
+ placement: effectivePlacement,
156
+ middleware
157
+ });
158
+ panelPos = { x: result.x, y: result.y };
159
+ resolvedPlacement = result.placement;
160
+ arrowPos = result.middlewareData.arrow ?? {};
161
+ const r = targetEl.getBoundingClientRect();
162
+ rect = { x: r.left, y: r.top, width: r.width, height: r.height };
163
+ ready = true;
164
+ }
165
+ $effect(() => {
166
+ if (!active || !step) {
167
+ ready = false;
168
+ targetEl = null;
169
+ rect = null;
170
+ return;
171
+ }
172
+ const current = step;
173
+ void index;
174
+ let cancelled = false;
175
+ let stopAuto;
176
+ ready = false;
177
+ untrack(() => {
178
+ ;
179
+ (async () => {
180
+ await current.onEnter?.();
181
+ if (cancelled) return;
182
+ const el = await resolveTarget(current.target, normalizeWait(current.waitForTarget));
183
+ if (cancelled) return;
184
+ targetEl = el;
185
+ const scroll = current.scrollIntoView ?? scrollIntoView;
186
+ if (el && scroll) {
187
+ const opts = typeof scroll === "object" ? scroll : { behavior: "smooth", block: "center" };
188
+ el.scrollIntoView(opts);
189
+ }
190
+ if (el && panelEl) {
191
+ stopAuto = autoUpdate(el, panelEl, position);
192
+ } else {
193
+ rect = null;
194
+ ready = true;
195
+ }
196
+ })();
197
+ });
198
+ return () => {
199
+ cancelled = true;
200
+ stopAuto?.();
201
+ };
202
+ });
203
+ useFocusTrap(() => panelEl, { active: () => active && ready });
204
+ let busy = $state(false);
205
+ async function runGuard(guard) {
206
+ if (!guard) return true;
207
+ busy = true;
208
+ try {
209
+ return await guard();
210
+ } finally {
211
+ busy = false;
212
+ }
213
+ }
214
+ async function handleNext() {
215
+ if (busy) return;
216
+ if (!await runGuard(step?.onBeforeNext)) return;
217
+ ctrl.next();
218
+ }
219
+ async function handlePrev() {
220
+ if (busy) return;
221
+ if (!await runGuard(step?.onBeforePrev)) return;
222
+ ctrl.prev();
223
+ }
224
+ function handleStop() {
225
+ ctrl.stop();
226
+ }
227
+ function isInteractiveTarget(el) {
228
+ const tag = el?.tagName;
229
+ return tag === "BUTTON" || tag === "A" || tag === "INPUT" || tag === "TEXTAREA";
230
+ }
231
+ useEventListener(
232
+ () => active ? document : null,
233
+ "keydown",
234
+ (event) => {
235
+ const advance = event.key === "ArrowRight" || event.key === "Enter" && !isInteractiveTarget(document.activeElement);
236
+ if (advance) {
237
+ event.preventDefault();
238
+ handleNext();
239
+ } else if (event.key === "ArrowLeft") {
240
+ event.preventDefault();
241
+ handlePrev();
242
+ } else if (event.key === "Escape" && dismissible) {
243
+ handleStop();
244
+ }
245
+ }
246
+ );
247
+ let prevActive = false;
248
+ $effect(() => {
249
+ if (active !== prevActive) {
250
+ prevActive = active;
251
+ onOpenChange?.(active);
252
+ }
253
+ });
254
+ const slotProps = $derived({
255
+ step,
256
+ index,
257
+ number: index + 1,
258
+ total: steps.length,
259
+ isFirst: ctrl.isFirst,
260
+ isLast: ctrl.isLast,
261
+ controller: ctrl,
262
+ next: handleNext,
263
+ prev: handlePrev,
264
+ stop: handleStop
265
+ });
266
+ const showPrevButton = $derived(step?.showPrev ?? !ctrl.isFirst);
267
+ const showSkipButton = $derived((step?.showSkip ?? showSkip) && !ctrl.isLast);
268
+ const nextButtonLabel = $derived(step?.nextLabel ?? (ctrl.isLast ? doneLabel : nextLabel));
269
+ const prevButtonLabel = $derived(step?.prevLabel ?? prevLabel);
270
+ const staticSide = $derived(
271
+ { top: "bottom", right: "left", bottom: "top", left: "right" }[side]
272
+ );
273
+ const arrowBorder = $derived(
274
+ {
275
+ top: "border-b border-r",
276
+ bottom: "border-t border-l",
277
+ left: "border-t border-r",
278
+ right: "border-b border-l"
279
+ }[side]
280
+ );
281
+ const arrowStyle = $derived.by(() => {
282
+ const parts = [`${staticSide}: -6px`];
283
+ if (arrowPos.x !== void 0) parts.push(`left: ${arrowPos.x}px`);
284
+ if (arrowPos.y !== void 0) parts.push(`top: ${arrowPos.y}px`);
285
+ return parts.join("; ");
286
+ });
287
+ const dotIndices = $derived(steps.map((_, i) => i));
288
+ </script>
289
+
290
+ {#snippet defaultHeader(p: TourStepSlotProps)}
291
+ <div class={classes.header}>
292
+ <div class="min-w-0">
293
+ {#if p.step.title}
294
+ <p id={titleId} class={classes.title}>{p.step.title}</p>
295
+ {/if}
296
+ </div>
297
+ {#if dismissible}
298
+ <Button
299
+ class={classes.closeButton}
300
+ size="xs"
301
+ variant="ghost"
302
+ color="surface"
303
+ square
304
+ icon={icons.close}
305
+ aria-label="Close tour"
306
+ onclick={handleStop}
307
+ />
308
+ {/if}
309
+ </div>
310
+ {/snippet}
311
+
312
+ {#snippet defaultBody(p: TourStepSlotProps)}
313
+ <div class={classes.body}>
314
+ {#if p.step.content}
315
+ {@render p.step.content(p)}
316
+ {:else if p.step.description}
317
+ <p id={descId}>{p.step.description}</p>
318
+ {/if}
319
+ </div>
320
+ {/snippet}
321
+
322
+ {#snippet defaultFooter()}
323
+ <div class={classes.footer}>
324
+ {#if showProgress}
325
+ <div class={classes.progress}>
326
+ {#each dotIndices as i (i)}
327
+ <span
328
+ class={variantSlots.progressDot({
329
+ active: i === index,
330
+ class: [config.slots.progressDot, ui?.progressDot]
331
+ })}
332
+ ></span>
333
+ {/each}
334
+ </div>
335
+ {:else}
336
+ <span></span>
337
+ {/if}
338
+
339
+ <div class={classes.nav}>
340
+ {#if showSkipButton}
341
+ <Button
342
+ class={classes.skipButton}
343
+ size="sm"
344
+ variant="ghost"
345
+ color="surface"
346
+ label={skipLabel}
347
+ onclick={handleStop}
348
+ />
349
+ {/if}
350
+ {#if showPrevButton}
351
+ <Button
352
+ class={variantSlots.prevButton({
353
+ class: [config.slots.prevButton, ui?.prevButton]
354
+ })}
355
+ size="sm"
356
+ variant="soft"
357
+ color="surface"
358
+ label={prevButtonLabel}
359
+ disabled={busy}
360
+ onclick={handlePrev}
361
+ />
362
+ {/if}
363
+ <Button
364
+ class={variantSlots.nextButton({
365
+ class: [config.slots.nextButton, ui?.nextButton]
366
+ })}
367
+ size="sm"
368
+ variant="solid"
369
+ color="primary"
370
+ label={nextButtonLabel}
371
+ loading={busy}
372
+ onclick={handleNext}
373
+ />
374
+ </div>
375
+ </div>
376
+ {/snippet}
377
+
378
+ {#snippet tourPanel()}
379
+ {#if active && step}
380
+ {#if showSpotlight}
381
+ {#if !stepInteractable}
382
+ <div
383
+ class={classes.overlay}
384
+ onclick={dismissible ? handleStop : undefined}
385
+ role="presentation"
386
+ ></div>
387
+ {/if}
388
+ <div
389
+ class={classes.spotlight}
390
+ style:width="{(rect?.width ?? 0) + stepPadding * 2}px"
391
+ style:height="{(rect?.height ?? 0) + stepPadding * 2}px"
392
+ style:border-radius="{stepRadius}px"
393
+ style:transform="translate({(rect?.x ?? 0) - stepPadding}px, {(rect?.y ?? 0) -
394
+ stepPadding}px)"
395
+ ></div>
396
+ {:else}
397
+ <div
398
+ class={classes.overlay}
399
+ style:background-color="rgba(0,0,0,0.5)"
400
+ onclick={dismissible ? handleStop : undefined}
401
+ role="presentation"
402
+ ></div>
403
+ {/if}
404
+
405
+ <div
406
+ bind:this={panelEl}
407
+ class={classes.panel}
408
+ role="dialog"
409
+ aria-modal="false"
410
+ aria-labelledby={step.title ? titleId : undefined}
411
+ aria-describedby={step.description ? descId : undefined}
412
+ data-state={ready ? 'open' : 'closed'}
413
+ style:position="fixed"
414
+ style:left={isDialog ? '50%' : `${panelPos.x}px`}
415
+ style:top={isDialog ? '50%' : `${panelPos.y}px`}
416
+ style:transform={isDialog ? 'translate(-50%, -50%)' : undefined}
417
+ style:opacity={ready ? undefined : '0'}
418
+ {...restProps}
419
+ >
420
+ {#if showArrow && !isDialog}
421
+ <div
422
+ bind:this={arrowEl}
423
+ class="absolute size-3 rotate-45 {arrowBorder} {classes.arrow}"
424
+ style={arrowStyle}
425
+ ></div>
426
+ {/if}
427
+
428
+ <div class="sr-only" aria-live="polite">
429
+ Step {index + 1} of {steps.length}
430
+ </div>
431
+
432
+ {#if contentSlot}
433
+ {@render contentSlot(slotProps)}
434
+ {:else}
435
+ {#if headerSlot}
436
+ {@render headerSlot(slotProps)}
437
+ {:else}
438
+ {@render defaultHeader(slotProps)}
439
+ {/if}
440
+
441
+ {@render defaultBody(slotProps)}
442
+
443
+ {#if footerSlot}
444
+ {@render footerSlot(slotProps)}
445
+ {:else}
446
+ {@render defaultFooter()}
447
+ {/if}
448
+ {/if}
449
+ </div>
450
+ {/if}
451
+ {/snippet}
452
+
453
+ {#if portal}
454
+ <Portal>
455
+ {@render tourPanel()}
456
+ </Portal>
457
+ {:else}
458
+ {@render tourPanel()}
459
+ {/if}
@@ -0,0 +1,5 @@
1
+ import type { TourProps } from './tour.types.js';
2
+ export type Props = TourProps;
3
+ declare const Tour: import("svelte").Component<TourProps, {}, "api">;
4
+ type Tour = ReturnType<typeof Tour>;
5
+ export default Tour;
@@ -0,0 +1,2 @@
1
+ export { default as Tour } from './Tour.svelte';
2
+ export type { TourProps, TourStep, TourController, TourStepSlotProps } from './tour.types.js';
@@ -0,0 +1 @@
1
+ export { default as Tour } from './Tour.svelte';