@orochibraru/ui 1.0.0 → 1.0.1
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/README.md +84 -3
- package/dist/components/blocks/accent-toggle.svelte +7 -3
- package/dist/components/blocks/accent-toggle.svelte.d.ts +1 -1
- package/dist/components/blocks/bottom-nav.svelte +1 -1
- package/dist/components/blocks/command-palette.svelte +268 -0
- package/dist/components/blocks/command-palette.svelte.d.ts +37 -0
- package/dist/components/blocks/error-page.svelte +23 -7
- package/dist/components/blocks/error-page.svelte.d.ts +4 -0
- package/dist/components/blocks/fab.svelte +1 -1
- package/dist/components/blocks/install-prompt.svelte +14 -8
- package/dist/components/blocks/install-prompt.svelte.d.ts +2 -1
- package/dist/components/blocks/music-player/labelled-slider.svelte +3 -12
- package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +1 -2
- package/dist/components/blocks/music-player.svelte +13 -12
- package/dist/components/blocks/otp-field.svelte +77 -33
- package/dist/components/blocks/otp-field.svelte.d.ts +7 -0
- package/dist/components/blocks/password-input.svelte +56 -16
- package/dist/components/blocks/password-input.svelte.d.ts +3 -0
- package/dist/components/blocks/stepper/stepper-phone-header.svelte +93 -0
- package/dist/components/blocks/stepper/stepper-phone-header.svelte.d.ts +13 -0
- package/dist/components/blocks/stepper.svelte +175 -0
- package/dist/components/blocks/stepper.svelte.d.ts +30 -0
- package/dist/components/blocks/time-range-picker.svelte +229 -0
- package/dist/components/blocks/time-range-picker.svelte.d.ts +33 -0
- package/dist/components/blocks/time-series-chart/data-table.svelte +43 -0
- package/dist/components/blocks/time-series-chart/data-table.svelte.d.ts +15 -0
- package/dist/components/blocks/time-series-chart/legend.svelte +23 -0
- package/dist/components/blocks/time-series-chart/legend.svelte.d.ts +10 -0
- package/dist/components/blocks/time-series-chart/marks.svelte +118 -0
- package/dist/components/blocks/time-series-chart/marks.svelte.d.ts +29 -0
- package/dist/components/blocks/time-series-chart/messages.d.ts +13 -0
- package/dist/components/blocks/time-series-chart/messages.js +12 -0
- package/dist/components/blocks/time-series-chart/readout.svelte +44 -0
- package/dist/components/blocks/time-series-chart/readout.svelte.d.ts +15 -0
- package/dist/components/blocks/time-series-chart/scale.d.ts +24 -0
- package/dist/components/blocks/time-series-chart/scale.js +136 -0
- package/dist/components/blocks/time-series-chart/time.d.ts +7 -0
- package/dist/components/blocks/time-series-chart/time.js +96 -0
- package/dist/components/blocks/time-series-chart.svelte +492 -0
- package/dist/components/blocks/time-series-chart.svelte.d.ts +18 -0
- package/dist/components/blocks/top-loading-bar.svelte +1 -1
- package/dist/components/blocks/video-player/center-controls.svelte +7 -7
- package/dist/components/blocks/video-player/control-bar.svelte +41 -35
- package/dist/components/blocks/video-player/control-button.svelte +1 -1
- package/dist/components/blocks/video-player/option-menu.svelte +1 -1
- package/dist/components/blocks/video-player/scrub-bar.svelte +39 -12
- package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +2 -0
- package/dist/components/blocks/video-player.svelte +2 -2
- package/dist/components/kit/accordion.svelte +70 -0
- package/dist/components/kit/accordion.svelte.d.ts +37 -0
- package/dist/components/kit/avatar-group.svelte +84 -0
- package/dist/components/kit/avatar-group.svelte.d.ts +14 -0
- package/dist/components/kit/bar-list.svelte +104 -0
- package/dist/components/kit/bar-list.svelte.d.ts +22 -0
- package/dist/components/kit/breadcrumbs.svelte +102 -0
- package/dist/components/kit/breadcrumbs.svelte.d.ts +14 -0
- package/dist/components/kit/checkbox-list.svelte +98 -0
- package/dist/components/kit/checkbox-list.svelte.d.ts +15 -0
- package/dist/components/kit/checkbox.svelte +84 -0
- package/dist/components/kit/checkbox.svelte.d.ts +8 -0
- package/dist/components/kit/confirm-dialog.svelte +127 -0
- package/dist/components/kit/confirm-dialog.svelte.d.ts +19 -0
- package/dist/components/kit/data-table.svelte +280 -0
- package/dist/components/kit/data-table.svelte.d.ts +46 -0
- package/dist/components/kit/date-field.svelte +210 -0
- package/dist/components/kit/date-field.svelte.d.ts +14 -0
- package/dist/components/kit/field-frame.svelte +71 -0
- package/dist/components/kit/field-frame.svelte.d.ts +26 -0
- package/dist/components/kit/fieldset-frame.svelte +69 -0
- package/dist/components/kit/fieldset-frame.svelte.d.ts +14 -0
- package/dist/components/kit/file-dropzone/files.d.ts +14 -0
- package/dist/components/kit/file-dropzone/files.js +69 -0
- package/dist/components/kit/file-dropzone.svelte +253 -0
- package/dist/components/kit/file-dropzone.svelte.d.ts +25 -0
- package/dist/components/kit/form.svelte +335 -0
- package/dist/components/kit/form.svelte.d.ts +61 -0
- package/dist/components/kit/input.svelte +73 -0
- package/dist/components/kit/input.svelte.d.ts +9 -0
- package/dist/components/kit/menu.svelte +163 -0
- package/dist/components/kit/menu.svelte.d.ts +28 -0
- package/dist/components/kit/pagination.svelte +134 -0
- package/dist/components/kit/pagination.svelte.d.ts +17 -0
- package/dist/components/kit/radio-list.svelte +131 -0
- package/dist/components/kit/radio-list.svelte.d.ts +15 -0
- package/dist/components/kit/segmented-control/segment.svelte +47 -0
- package/dist/components/kit/segmented-control/segment.svelte.d.ts +11 -0
- package/dist/components/kit/segmented-control.svelte +70 -0
- package/dist/components/kit/segmented-control.svelte.d.ts +16 -0
- package/dist/components/kit/select.svelte +244 -0
- package/dist/components/kit/select.svelte.d.ts +26 -0
- package/dist/components/kit/status-badge.svelte +54 -0
- package/dist/components/kit/status-badge.svelte.d.ts +31 -0
- package/dist/components/kit/switch.svelte +84 -0
- package/dist/components/kit/switch.svelte.d.ts +8 -0
- package/dist/components/kit/tabs.svelte +86 -0
- package/dist/components/kit/tabs.svelte.d.ts +20 -0
- package/dist/components/kit/textarea.svelte +92 -0
- package/dist/components/kit/textarea.svelte.d.ts +11 -0
- package/dist/components/ui/drawer/drawer-content.svelte +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/sonner/sonner.svelte +2 -4
- package/dist/mcp-content.json +1 -1
- package/dist/mcp.js +11 -11
- package/dist/server/hooks.d.ts +9 -0
- package/dist/server/hooks.js +41 -0
- package/dist/server/logger.d.ts +39 -0
- package/dist/server/logger.js +144 -0
- package/dist/server/telemetry.d.ts +23 -0
- package/dist/server/telemetry.js +82 -0
- package/dist/theme.css +18 -6
- package/package.json +40 -1
|
@@ -7,15 +7,13 @@
|
|
|
7
7
|
step: number;
|
|
8
8
|
label: string;
|
|
9
9
|
valueText: string;
|
|
10
|
-
onchange
|
|
11
|
-
oncommit?: (value: number) => void;
|
|
10
|
+
onchange: (value: number) => void;
|
|
12
11
|
orientation?: "horizontal" | "vertical";
|
|
13
12
|
disabled?: boolean;
|
|
14
13
|
class?: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
const { value, label, valueText, onchange,
|
|
18
|
-
$props();
|
|
16
|
+
const { value, label, valueText, onchange, ...rest }: Props = $props();
|
|
19
17
|
|
|
20
18
|
let ref: HTMLElement | null = $state(null);
|
|
21
19
|
|
|
@@ -26,11 +24,4 @@
|
|
|
26
24
|
});
|
|
27
25
|
</script>
|
|
28
26
|
|
|
29
|
-
<Slider
|
|
30
|
-
bind:ref
|
|
31
|
-
type="single"
|
|
32
|
-
{value}
|
|
33
|
-
onValueChange={onchange}
|
|
34
|
-
onValueCommit={oncommit}
|
|
35
|
-
{...rest}
|
|
36
|
-
/>
|
|
27
|
+
<Slider bind:ref type="single" {value} onValueChange={onchange} {...rest} />
|
|
@@ -4,8 +4,7 @@ interface Props {
|
|
|
4
4
|
step: number;
|
|
5
5
|
label: string;
|
|
6
6
|
valueText: string;
|
|
7
|
-
onchange
|
|
8
|
-
oncommit?: (value: number) => void;
|
|
7
|
+
onchange: (value: number) => void;
|
|
9
8
|
orientation?: "horizontal" | "vertical";
|
|
10
9
|
disabled?: boolean;
|
|
11
10
|
class?: string;
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
import { haptic } from "../../haptics.js";
|
|
22
22
|
|
|
23
23
|
import { formatTime } from "./music-player/format-time.js";
|
|
24
|
-
import LabelledSlider from "./music-player/labelled-slider.svelte";
|
|
25
24
|
import VolumeControl from "./music-player/volume-control.svelte";
|
|
25
|
+
import ScrubBar from "./video-player/scrub-bar.svelte";
|
|
26
26
|
|
|
27
27
|
const defaultMessages = {
|
|
28
28
|
player: "Music player",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
{#if track}
|
|
177
177
|
<section
|
|
178
178
|
aria-label={text.player}
|
|
179
|
-
class="glass grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2 rounded-3xl p-
|
|
179
|
+
class="glass grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2 rounded-3xl p-4 [grid-template-areas:'meta_volume'_'seek_seek'_'controls_controls'] md:grid-cols-[minmax(0,16rem)_minmax(0,1fr)_auto] md:gap-x-6 md:px-6 md:py-5 md:[grid-template-areas:'meta_controls_volume'_'meta_seek_volume']"
|
|
180
180
|
>
|
|
181
181
|
<div class="flex min-w-0 items-center gap-3 [grid-area:meta]">
|
|
182
182
|
{#if track.artwork}
|
|
@@ -206,16 +206,17 @@
|
|
|
206
206
|
<span class="text-muted-foreground tabular min-w-10 text-xs">
|
|
207
207
|
{formatTime(currentTime)}
|
|
208
208
|
</span>
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
<div class="min-w-0 flex-1">
|
|
210
|
+
<ScrubBar
|
|
211
|
+
variant="surface"
|
|
212
|
+
{currentTime}
|
|
213
|
+
duration={seekable ? duration : 0}
|
|
214
|
+
bufferedEnd={0}
|
|
215
|
+
chapters={[]}
|
|
216
|
+
label={text.seek}
|
|
217
|
+
onseek={(time) => (currentTime = time)}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
219
220
|
<span class="text-muted-foreground tabular min-w-10 text-right text-xs">
|
|
220
221
|
{formatTime(duration)}
|
|
221
222
|
</span>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import FieldFrame, {
|
|
3
|
+
errorList,
|
|
4
|
+
} from "../kit/field-frame.svelte";
|
|
2
5
|
import * as InputOTP from "../ui/input-otp/index.js";
|
|
6
|
+
import { cn } from "../../utils.js";
|
|
3
7
|
|
|
4
8
|
interface Props {
|
|
5
9
|
value?: string;
|
|
@@ -9,6 +13,13 @@
|
|
|
9
13
|
disabled?: boolean;
|
|
10
14
|
invalid?: boolean;
|
|
11
15
|
oncomplete?: (code: string) => void;
|
|
16
|
+
label?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
error?: string | string[];
|
|
19
|
+
required?: boolean;
|
|
20
|
+
/** `center` centres the label and the boxes, as a code screen usually does. */
|
|
21
|
+
align?: "start" | "center";
|
|
22
|
+
class?: string;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
let {
|
|
@@ -19,41 +30,74 @@
|
|
|
19
30
|
disabled = false,
|
|
20
31
|
invalid = false,
|
|
21
32
|
oncomplete,
|
|
33
|
+
label,
|
|
34
|
+
description,
|
|
35
|
+
error,
|
|
36
|
+
required = false,
|
|
37
|
+
align = "start",
|
|
38
|
+
class: className,
|
|
22
39
|
}: Props = $props();
|
|
23
40
|
|
|
24
41
|
const half = $derived(Math.ceil(length / 2));
|
|
42
|
+
const generatedId = $props.id();
|
|
43
|
+
const inputId = $derived(id ?? generatedId);
|
|
44
|
+
const framed = $derived(Boolean(label || description || error));
|
|
45
|
+
const showInvalid = $derived(invalid || errorList(error).length > 0);
|
|
46
|
+
const centered = $derived(align === "center");
|
|
25
47
|
</script>
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{#
|
|
41
|
-
<InputOTP.
|
|
42
|
-
{cell}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<InputOTP.
|
|
52
|
-
{cell}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
</InputOTP.
|
|
49
|
+
{#snippet boxes()}
|
|
50
|
+
<div class={cn("flex", centered && "justify-center", !framed && className)}>
|
|
51
|
+
<InputOTP.Root
|
|
52
|
+
maxlength={length}
|
|
53
|
+
pattern="^\d+$"
|
|
54
|
+
inputmode="numeric"
|
|
55
|
+
autocomplete="one-time-code"
|
|
56
|
+
{name}
|
|
57
|
+
{inputId}
|
|
58
|
+
{disabled}
|
|
59
|
+
bind:value
|
|
60
|
+
onComplete={() => oncomplete?.(value)}
|
|
61
|
+
>
|
|
62
|
+
{#snippet children({ cells })}
|
|
63
|
+
<InputOTP.Group>
|
|
64
|
+
{#each cells.slice(0, half) as cell (cell)}
|
|
65
|
+
<InputOTP.Slot
|
|
66
|
+
{cell}
|
|
67
|
+
aria-invalid={showInvalid}
|
|
68
|
+
class="h-13 w-11 text-xl font-semibold tabular md:h-12 md:w-10"
|
|
69
|
+
/>
|
|
70
|
+
{/each}
|
|
71
|
+
</InputOTP.Group>
|
|
72
|
+
<InputOTP.Separator />
|
|
73
|
+
<InputOTP.Group>
|
|
74
|
+
{#each cells.slice(half) as cell (cell)}
|
|
75
|
+
<InputOTP.Slot
|
|
76
|
+
{cell}
|
|
77
|
+
aria-invalid={showInvalid}
|
|
78
|
+
class="h-13 w-11 text-xl font-semibold tabular md:h-12 md:w-10"
|
|
79
|
+
/>
|
|
80
|
+
{/each}
|
|
81
|
+
</InputOTP.Group>
|
|
82
|
+
{/snippet}
|
|
83
|
+
</InputOTP.Root>
|
|
84
|
+
</div>
|
|
85
|
+
{/snippet}
|
|
86
|
+
|
|
87
|
+
{#if framed}
|
|
88
|
+
<FieldFrame
|
|
89
|
+
id={inputId}
|
|
90
|
+
{label}
|
|
91
|
+
{description}
|
|
92
|
+
{error}
|
|
93
|
+
{required}
|
|
94
|
+
class={cn(
|
|
95
|
+
centered && "items-center text-center *:data-[slot=field-label]:w-fit",
|
|
96
|
+
className,
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
{@render boxes()}
|
|
100
|
+
</FieldFrame>
|
|
101
|
+
{:else}
|
|
102
|
+
{@render boxes()}
|
|
103
|
+
{/if}
|
|
@@ -6,6 +6,13 @@ interface Props {
|
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
invalid?: boolean;
|
|
8
8
|
oncomplete?: (code: string) => void;
|
|
9
|
+
label?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
error?: string | string[];
|
|
12
|
+
required?: boolean;
|
|
13
|
+
/** `center` centres the label and the boxes, as a code screen usually does. */
|
|
14
|
+
align?: "start" | "center";
|
|
15
|
+
class?: string;
|
|
9
16
|
}
|
|
10
17
|
declare const OtpField: import("svelte").Component<Props, {}, "value">;
|
|
11
18
|
type OtpField = ReturnType<typeof OtpField>;
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
5
5
|
|
|
6
6
|
import IconButton from "./icon-button.svelte";
|
|
7
|
+
import FieldFrame, {
|
|
8
|
+
errorList,
|
|
9
|
+
} from "../kit/field-frame.svelte";
|
|
7
10
|
import { Input } from "../ui/input/index.js";
|
|
8
11
|
import { cn } from "../../utils.js";
|
|
9
12
|
|
|
@@ -15,6 +18,9 @@
|
|
|
15
18
|
type Props = Omit<HTMLInputAttributes, "type" | "value" | "files"> & {
|
|
16
19
|
value?: string;
|
|
17
20
|
visible?: boolean;
|
|
21
|
+
label?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
error?: string | string[];
|
|
18
24
|
messages?: Partial<typeof defaultMessages>;
|
|
19
25
|
};
|
|
20
26
|
|
|
@@ -23,29 +29,63 @@
|
|
|
23
29
|
visible = $bindable(false),
|
|
24
30
|
name = "password",
|
|
25
31
|
autocomplete = "current-password",
|
|
32
|
+
id,
|
|
33
|
+
label,
|
|
34
|
+
description,
|
|
35
|
+
error,
|
|
26
36
|
messages = {},
|
|
27
37
|
class: className,
|
|
28
38
|
...rest
|
|
29
39
|
}: Props = $props();
|
|
30
40
|
|
|
31
41
|
const text = $derived({ ...defaultMessages, ...messages });
|
|
42
|
+
const generatedId = $props.id();
|
|
43
|
+
const inputId = $derived(id ?? generatedId);
|
|
44
|
+
const framed = $derived(Boolean(label || description || error));
|
|
45
|
+
const invalid = $derived(errorList(error).length > 0);
|
|
46
|
+
const describedBy = $derived(
|
|
47
|
+
invalid
|
|
48
|
+
? `${inputId}-error`
|
|
49
|
+
: description
|
|
50
|
+
? `${inputId}-description`
|
|
51
|
+
: undefined,
|
|
52
|
+
);
|
|
32
53
|
</script>
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
icon={visible ? EyeOffIcon : EyeIcon}
|
|
47
|
-
onclick={() => (visible = !visible)}
|
|
48
|
-
class="size-11 md:size-11"
|
|
55
|
+
{#snippet control()}
|
|
56
|
+
<div class="relative">
|
|
57
|
+
<Input
|
|
58
|
+
aria-invalid={invalid || undefined}
|
|
59
|
+
aria-describedby={describedBy}
|
|
60
|
+
{...rest}
|
|
61
|
+
id={inputId}
|
|
62
|
+
{name}
|
|
63
|
+
{autocomplete}
|
|
64
|
+
type={visible ? "text" : "password"}
|
|
65
|
+
bind:value
|
|
66
|
+
class={cn("h-11 pr-12", className)}
|
|
49
67
|
/>
|
|
68
|
+
<div class="absolute inset-y-0 right-0 flex items-center">
|
|
69
|
+
<IconButton
|
|
70
|
+
label={visible ? text.hide : text.show}
|
|
71
|
+
icon={visible ? EyeOffIcon : EyeIcon}
|
|
72
|
+
onclick={() => (visible = !visible)}
|
|
73
|
+
class="size-11 md:size-11"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
50
76
|
</div>
|
|
51
|
-
|
|
77
|
+
{/snippet}
|
|
78
|
+
|
|
79
|
+
{#if framed}
|
|
80
|
+
<FieldFrame
|
|
81
|
+
id={inputId}
|
|
82
|
+
{label}
|
|
83
|
+
{description}
|
|
84
|
+
{error}
|
|
85
|
+
required={Boolean(rest.required)}
|
|
86
|
+
>
|
|
87
|
+
{@render control()}
|
|
88
|
+
</FieldFrame>
|
|
89
|
+
{:else}
|
|
90
|
+
{@render control()}
|
|
91
|
+
{/if}
|
|
@@ -2,6 +2,9 @@ import type { HTMLInputAttributes } from "svelte/elements";
|
|
|
2
2
|
declare const PasswordInput: import("svelte").Component<Omit<HTMLInputAttributes, "type" | "value" | "files"> & {
|
|
3
3
|
value?: string;
|
|
4
4
|
visible?: boolean;
|
|
5
|
+
label?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
error?: string | string[];
|
|
5
8
|
messages?: Partial<{
|
|
6
9
|
show: string;
|
|
7
10
|
hide: string;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
|
|
3
|
+
|
|
4
|
+
import type { StepperStep } from "../stepper.svelte";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
steps: StepperStep[];
|
|
8
|
+
active: number;
|
|
9
|
+
reached: number;
|
|
10
|
+
progress: string;
|
|
11
|
+
upNext: string;
|
|
12
|
+
stepLabel: (step: number, label: string) => string;
|
|
13
|
+
onselect: (step: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
steps,
|
|
18
|
+
active,
|
|
19
|
+
reached,
|
|
20
|
+
progress,
|
|
21
|
+
upNext,
|
|
22
|
+
stepLabel,
|
|
23
|
+
onselect,
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const current = $derived(steps[active]);
|
|
27
|
+
const following = $derived(steps[active + 1]);
|
|
28
|
+
|
|
29
|
+
function segmentClass(step: number): string {
|
|
30
|
+
if (step <= active) {
|
|
31
|
+
return "bg-primary";
|
|
32
|
+
}
|
|
33
|
+
if (step <= reached) {
|
|
34
|
+
return "bg-primary/30";
|
|
35
|
+
}
|
|
36
|
+
return "bg-muted";
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="sm:hidden">
|
|
41
|
+
<ol class="-my-3 flex gap-1.5">
|
|
42
|
+
{#each steps as step, index (step.label)}
|
|
43
|
+
<li class="flex-1">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
class="focus-visible:ring-ring/50 flex h-11 w-full items-center rounded-full outline-none focus-visible:ring-3 disabled:cursor-not-allowed"
|
|
47
|
+
aria-current={index === active ? "step" : undefined}
|
|
48
|
+
aria-label={stepLabel(index + 1, step.label)}
|
|
49
|
+
disabled={index > reached}
|
|
50
|
+
onclick={() => onselect(index)}
|
|
51
|
+
>
|
|
52
|
+
<span
|
|
53
|
+
class="h-1.5 w-full rounded-full transition-colors duration-300 {segmentClass(
|
|
54
|
+
index,
|
|
55
|
+
)}"
|
|
56
|
+
></span>
|
|
57
|
+
</button>
|
|
58
|
+
</li>
|
|
59
|
+
{/each}
|
|
60
|
+
</ol>
|
|
61
|
+
{#if current}
|
|
62
|
+
<div class="mt-4 flex items-center gap-3">
|
|
63
|
+
<span
|
|
64
|
+
class="bg-accent text-accent-foreground flex size-11 shrink-0 items-center justify-center rounded-xl"
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
>
|
|
67
|
+
{#if current.icon}
|
|
68
|
+
<current.icon class="size-5" />
|
|
69
|
+
{:else}
|
|
70
|
+
<span class="text-sm font-semibold tabular">{active + 1}</span>
|
|
71
|
+
{/if}
|
|
72
|
+
</span>
|
|
73
|
+
<div class="min-w-0" aria-live="polite" aria-atomic="true">
|
|
74
|
+
<p
|
|
75
|
+
class="text-muted-foreground text-[0.6875rem] font-medium tracking-wide uppercase"
|
|
76
|
+
>
|
|
77
|
+
{progress}
|
|
78
|
+
</p>
|
|
79
|
+
<p class="text-base leading-tight font-semibold">{current.label}</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
{#if current.description}
|
|
83
|
+
<p class="text-muted-foreground mt-2 text-sm">{current.description}</p>
|
|
84
|
+
{/if}
|
|
85
|
+
{#if following}
|
|
86
|
+
<p class="text-muted-foreground mt-1.5 flex items-center gap-1 text-xs">
|
|
87
|
+
{upNext}
|
|
88
|
+
{following.label}
|
|
89
|
+
<ArrowRightIcon class="size-3" aria-hidden="true" />
|
|
90
|
+
</p>
|
|
91
|
+
{/if}
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StepperStep } from "../stepper.svelte";
|
|
2
|
+
interface Props {
|
|
3
|
+
steps: StepperStep[];
|
|
4
|
+
active: number;
|
|
5
|
+
reached: number;
|
|
6
|
+
progress: string;
|
|
7
|
+
upNext: string;
|
|
8
|
+
stepLabel: (step: number, label: string) => string;
|
|
9
|
+
onselect: (step: number) => void;
|
|
10
|
+
}
|
|
11
|
+
declare const StepperPhoneHeader: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type StepperPhoneHeader = ReturnType<typeof StepperPhoneHeader>;
|
|
13
|
+
export default StepperPhoneHeader;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Component } from "svelte";
|
|
3
|
+
|
|
4
|
+
export interface StepperStep {
|
|
5
|
+
label: string;
|
|
6
|
+
/** One line under the step's name on phones, saying what the step asks for. */
|
|
7
|
+
description?: string;
|
|
8
|
+
icon?: Component<{ class?: string }>;
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
|
|
14
|
+
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
|
|
15
|
+
import CheckIcon from "@lucide/svelte/icons/check";
|
|
16
|
+
import type { Snippet } from "svelte";
|
|
17
|
+
|
|
18
|
+
import StepperPhoneHeader from "./stepper/stepper-phone-header.svelte";
|
|
19
|
+
import { Button } from "../ui/button/index.js";
|
|
20
|
+
import { haptic } from "../../haptics.js";
|
|
21
|
+
|
|
22
|
+
const defaultMessages = {
|
|
23
|
+
back: "Back",
|
|
24
|
+
next: "Next",
|
|
25
|
+
upNext: "Next:",
|
|
26
|
+
progress: (step: number, total: number) => `Step ${step} of ${total}`,
|
|
27
|
+
stepLabel: (step: number, label: string) => `Step ${step}: ${label}`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
steps: StepperStep[];
|
|
32
|
+
/** The shown step; the page hides every other panel with it so field state survives. */
|
|
33
|
+
active?: number;
|
|
34
|
+
/** Validates the step being left; false keeps the user on it. */
|
|
35
|
+
onnext?: (step: number) => boolean;
|
|
36
|
+
/** Every step's panel, the inactive ones hidden by the page. */
|
|
37
|
+
children: Snippet;
|
|
38
|
+
/** Shown instead of Next on the last step: the page's own submit button. */
|
|
39
|
+
finish: Snippet;
|
|
40
|
+
messages?: Partial<typeof defaultMessages>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
steps,
|
|
45
|
+
active = $bindable(0),
|
|
46
|
+
onnext = () => true,
|
|
47
|
+
children,
|
|
48
|
+
finish,
|
|
49
|
+
messages = {},
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
const text = $derived({ ...defaultMessages, ...messages });
|
|
53
|
+
|
|
54
|
+
let furthest = $state(0);
|
|
55
|
+
const reached = $derived(Math.max(furthest, active));
|
|
56
|
+
const last = $derived(active >= steps.length - 1);
|
|
57
|
+
|
|
58
|
+
function goTo(step: number) {
|
|
59
|
+
if (step <= reached) {
|
|
60
|
+
active = step;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function next() {
|
|
65
|
+
haptic("tap");
|
|
66
|
+
if (!onnext(active)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
furthest = Math.max(reached, active + 1);
|
|
70
|
+
active += 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function back() {
|
|
74
|
+
haptic("tap");
|
|
75
|
+
active = Math.max(0, active - 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function markerClass(step: number): string {
|
|
79
|
+
if (step === active) {
|
|
80
|
+
return "border-primary bg-primary text-primary-foreground glow";
|
|
81
|
+
}
|
|
82
|
+
if (step < active) {
|
|
83
|
+
return "border-primary/40 bg-accent text-accent-foreground";
|
|
84
|
+
}
|
|
85
|
+
return "border-border bg-muted text-muted-foreground";
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div class="grid w-full gap-7">
|
|
90
|
+
<ol class="hidden items-start sm:flex">
|
|
91
|
+
{#each steps as step, index (step.label)}
|
|
92
|
+
<li class="flex flex-1 items-start last:flex-none">
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class="group focus-visible:ring-ring/50 flex min-w-11 shrink-0 flex-col items-center gap-2 rounded-xl px-2 outline-none focus-visible:ring-3 disabled:cursor-not-allowed"
|
|
96
|
+
aria-current={index === active ? "step" : undefined}
|
|
97
|
+
aria-label={text.stepLabel(index + 1, step.label)}
|
|
98
|
+
disabled={index > reached}
|
|
99
|
+
onclick={() => goTo(index)}
|
|
100
|
+
>
|
|
101
|
+
<span
|
|
102
|
+
class="flex size-9 items-center justify-center rounded-full border transition-all duration-300 {markerClass(
|
|
103
|
+
index,
|
|
104
|
+
)}"
|
|
105
|
+
>
|
|
106
|
+
{#if index < active}
|
|
107
|
+
<CheckIcon class="size-4" />
|
|
108
|
+
{:else if step.icon}
|
|
109
|
+
<step.icon class="size-4" />
|
|
110
|
+
{:else}
|
|
111
|
+
<span class="text-xs font-medium tabular">{index + 1}</span>
|
|
112
|
+
{/if}
|
|
113
|
+
</span>
|
|
114
|
+
<span
|
|
115
|
+
class="text-xs font-medium tracking-wide transition-colors {index ===
|
|
116
|
+
active
|
|
117
|
+
? 'text-foreground'
|
|
118
|
+
: 'text-muted-foreground'} {index <= reached
|
|
119
|
+
? 'group-hover:text-foreground'
|
|
120
|
+
: 'opacity-60'}"
|
|
121
|
+
>
|
|
122
|
+
{step.label}
|
|
123
|
+
</span>
|
|
124
|
+
</button>
|
|
125
|
+
{#if index < steps.length - 1}
|
|
126
|
+
<span
|
|
127
|
+
class="mt-4.25 h-0.5 flex-1 rounded-full transition-colors duration-300 {index <
|
|
128
|
+
active
|
|
129
|
+
? 'bg-primary/50'
|
|
130
|
+
: 'bg-border'}"
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
></span>
|
|
133
|
+
{/if}
|
|
134
|
+
</li>
|
|
135
|
+
{/each}
|
|
136
|
+
</ol>
|
|
137
|
+
|
|
138
|
+
<StepperPhoneHeader
|
|
139
|
+
{steps}
|
|
140
|
+
{active}
|
|
141
|
+
{reached}
|
|
142
|
+
progress={text.progress(active + 1, steps.length)}
|
|
143
|
+
upNext={text.upNext}
|
|
144
|
+
stepLabel={text.stepLabel}
|
|
145
|
+
onselect={goTo}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
{@render children()}
|
|
149
|
+
|
|
150
|
+
<div class="flex gap-3">
|
|
151
|
+
{#if active > 0}
|
|
152
|
+
<Button
|
|
153
|
+
variant="outline"
|
|
154
|
+
class="h-11 flex-1 rounded-full px-5 sm:flex-none"
|
|
155
|
+
onclick={back}
|
|
156
|
+
>
|
|
157
|
+
<ArrowLeftIcon class="size-4" />
|
|
158
|
+
{text.back}
|
|
159
|
+
</Button>
|
|
160
|
+
{/if}
|
|
161
|
+
{#if last}
|
|
162
|
+
<div class="flex flex-1 *:flex-1 sm:ml-auto sm:flex-none sm:*:flex-none">
|
|
163
|
+
{@render finish()}
|
|
164
|
+
</div>
|
|
165
|
+
{:else}
|
|
166
|
+
<Button
|
|
167
|
+
class="glow h-11 flex-1 rounded-full px-5 sm:ml-auto sm:flex-none"
|
|
168
|
+
onclick={next}
|
|
169
|
+
>
|
|
170
|
+
{text.next}
|
|
171
|
+
<ArrowRightIcon class="size-4" />
|
|
172
|
+
</Button>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
export interface StepperStep {
|
|
3
|
+
label: string;
|
|
4
|
+
/** One line under the step's name on phones, saying what the step asks for. */
|
|
5
|
+
description?: string;
|
|
6
|
+
icon?: Component<{
|
|
7
|
+
class?: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
import type { Snippet } from "svelte";
|
|
11
|
+
declare const Stepper: Component<{
|
|
12
|
+
steps: StepperStep[];
|
|
13
|
+
/** The shown step; the page hides every other panel with it so field state survives. */
|
|
14
|
+
active?: number;
|
|
15
|
+
/** Validates the step being left; false keeps the user on it. */
|
|
16
|
+
onnext?: (step: number) => boolean;
|
|
17
|
+
/** Every step's panel, the inactive ones hidden by the page. */
|
|
18
|
+
children: Snippet;
|
|
19
|
+
/** Shown instead of Next on the last step: the page's own submit button. */
|
|
20
|
+
finish: Snippet;
|
|
21
|
+
messages?: Partial<{
|
|
22
|
+
back: string;
|
|
23
|
+
next: string;
|
|
24
|
+
upNext: string;
|
|
25
|
+
progress: (step: number, total: number) => string;
|
|
26
|
+
stepLabel: (step: number, label: string) => string;
|
|
27
|
+
}>;
|
|
28
|
+
}, {}, "active">;
|
|
29
|
+
type Stepper = ReturnType<typeof Stepper>;
|
|
30
|
+
export default Stepper;
|