This repository has no description
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3
4 export const welcomeCard = tv({
5 slots: {
6 root: "overflow-hidden rounded-sm border border-border-default bg-background-default shadow-xs",
7 header: "overflow-x-auto border-b border-border-default px-6 py-5",
8 // grid so the outgoing and incoming panes share one cell and cross-fade in
9 // place; overflow-hidden is what keeps the swipe inside the card
10 body: "grid overflow-hidden p-6",
11 pane: "col-start-1 row-start-1 min-w-0",
12 footer: "flex items-center justify-between gap-4 border-t border-border-default px-6 py-4"
13 }
14 });
15
16 export type WelcomeCardVariants = VariantProps<typeof welcomeCard>;
17</script>
18
19<script lang="ts">
20 import type { Snippet } from "svelte";
21 import { untrack } from "svelte";
22 import { cubicIn, cubicOut } from "svelte/easing";
23 import { prefersReducedMotion } from "svelte/motion";
24 import { fly } from "svelte/transition";
25 import ArrowLeft from "$icon/arrow-left";
26 import ArrowRight from "$icon/arrow-right";
27 import Check from "$icon/check";
28 import Button from "$lib/components/ui/Button.svelte";
29 import Steps from "./Steps.svelte";
30 import { WELCOME_STEPS, LAST_STEP } from "./steps";
31
32 interface Props {
33 step: number;
34 /** +1 when advancing, -1 when going back — decides which way the pane slides */
35 direction: number;
36 /** the appview greens Next once the step has something worth saving */
37 nextReady?: boolean;
38 onBack: () => void;
39 onNext: () => void;
40 /** rendered per step index, frozen at pane creation so the outgoing pane keeps its own content */
41 children: Snippet<[number]>;
42 }
43
44 let { step, direction, nextReady = false, onBack, onNext, children }: Props = $props();
45
46 const slot = welcomeCard();
47
48 const DISTANCE = 40;
49 const enter = $derived(
50 prefersReducedMotion.current
51 ? { duration: 0 }
52 : { x: direction * DISTANCE, duration: 240, delay: 90, easing: cubicOut }
53 );
54 const leave = $derived(
55 prefersReducedMotion.current
56 ? { duration: 0 }
57 : { x: direction * -DISTANCE, duration: 150, easing: cubicIn }
58 );
59
60 const isLast = $derived(step === LAST_STEP);
61</script>
62
63<div class={slot.root()}>
64 <div class={slot.header()}>
65 <Steps items={WELCOME_STEPS} current={step} />
66 </div>
67
68 <div class={slot.body()}>
69 {#key step}
70 <!-- untrack freezes the index at creation: `children` is a live snippet, so
71 without this the leaving pane would re-render with the new step's content
72 halfway through its own exit transition -->
73 {@const frozen = untrack(() => step)}
74 <div class={slot.pane()} in:fly={enter} out:fly={leave}>
75 {@render children(frozen)}
76 </div>
77 {/key}
78 </div>
79
80 <div class={slot.footer()}>
81 {#if step > 0}
82 <Button variant="ghost" icon={ArrowLeft} onclick={onBack}>Back</Button>
83 {:else}
84 <span></span>
85 {/if}
86
87 <Button
88 variant={nextReady ? "primary" : "default"}
89 icon={isLast ? Check : ArrowRight}
90 iconSide={isLast ? "left" : "right"}
91 onclick={onNext}
92 >
93 {isLast ? "Finish" : "Next"}
94 </Button>
95 </div>
96</div>