@orochibraru/ui 1.0.0 → 1.0.1-canary.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.
Files changed (112) hide show
  1. package/README.md +84 -3
  2. package/dist/components/blocks/accent-toggle.svelte +7 -3
  3. package/dist/components/blocks/accent-toggle.svelte.d.ts +1 -1
  4. package/dist/components/blocks/bottom-nav.svelte +1 -1
  5. package/dist/components/blocks/command-palette.svelte +268 -0
  6. package/dist/components/blocks/command-palette.svelte.d.ts +37 -0
  7. package/dist/components/blocks/error-page.svelte +23 -7
  8. package/dist/components/blocks/error-page.svelte.d.ts +4 -0
  9. package/dist/components/blocks/fab.svelte +1 -1
  10. package/dist/components/blocks/install-prompt.svelte +14 -8
  11. package/dist/components/blocks/install-prompt.svelte.d.ts +2 -1
  12. package/dist/components/blocks/music-player/labelled-slider.svelte +3 -12
  13. package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +1 -2
  14. package/dist/components/blocks/music-player.svelte +13 -12
  15. package/dist/components/blocks/otp-field.svelte +77 -33
  16. package/dist/components/blocks/otp-field.svelte.d.ts +7 -0
  17. package/dist/components/blocks/password-input.svelte +56 -16
  18. package/dist/components/blocks/password-input.svelte.d.ts +3 -0
  19. package/dist/components/blocks/stepper/stepper-phone-header.svelte +93 -0
  20. package/dist/components/blocks/stepper/stepper-phone-header.svelte.d.ts +13 -0
  21. package/dist/components/blocks/stepper.svelte +175 -0
  22. package/dist/components/blocks/stepper.svelte.d.ts +30 -0
  23. package/dist/components/blocks/time-range-picker.svelte +229 -0
  24. package/dist/components/blocks/time-range-picker.svelte.d.ts +33 -0
  25. package/dist/components/blocks/time-series-chart/data-table.svelte +43 -0
  26. package/dist/components/blocks/time-series-chart/data-table.svelte.d.ts +15 -0
  27. package/dist/components/blocks/time-series-chart/legend.svelte +23 -0
  28. package/dist/components/blocks/time-series-chart/legend.svelte.d.ts +10 -0
  29. package/dist/components/blocks/time-series-chart/marks.svelte +118 -0
  30. package/dist/components/blocks/time-series-chart/marks.svelte.d.ts +29 -0
  31. package/dist/components/blocks/time-series-chart/messages.d.ts +13 -0
  32. package/dist/components/blocks/time-series-chart/messages.js +12 -0
  33. package/dist/components/blocks/time-series-chart/readout.svelte +44 -0
  34. package/dist/components/blocks/time-series-chart/readout.svelte.d.ts +15 -0
  35. package/dist/components/blocks/time-series-chart/scale.d.ts +24 -0
  36. package/dist/components/blocks/time-series-chart/scale.js +136 -0
  37. package/dist/components/blocks/time-series-chart/time.d.ts +7 -0
  38. package/dist/components/blocks/time-series-chart/time.js +96 -0
  39. package/dist/components/blocks/time-series-chart.svelte +492 -0
  40. package/dist/components/blocks/time-series-chart.svelte.d.ts +18 -0
  41. package/dist/components/blocks/top-loading-bar.svelte +1 -1
  42. package/dist/components/blocks/video-player/center-controls.svelte +7 -7
  43. package/dist/components/blocks/video-player/control-bar.svelte +41 -35
  44. package/dist/components/blocks/video-player/control-button.svelte +1 -1
  45. package/dist/components/blocks/video-player/option-menu.svelte +1 -1
  46. package/dist/components/blocks/video-player/scrub-bar.svelte +39 -12
  47. package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +2 -0
  48. package/dist/components/blocks/video-player.svelte +2 -2
  49. package/dist/components/kit/accordion.svelte +70 -0
  50. package/dist/components/kit/accordion.svelte.d.ts +37 -0
  51. package/dist/components/kit/avatar-group.svelte +84 -0
  52. package/dist/components/kit/avatar-group.svelte.d.ts +14 -0
  53. package/dist/components/kit/bar-list.svelte +104 -0
  54. package/dist/components/kit/bar-list.svelte.d.ts +22 -0
  55. package/dist/components/kit/breadcrumbs.svelte +102 -0
  56. package/dist/components/kit/breadcrumbs.svelte.d.ts +14 -0
  57. package/dist/components/kit/checkbox-list.svelte +98 -0
  58. package/dist/components/kit/checkbox-list.svelte.d.ts +15 -0
  59. package/dist/components/kit/checkbox.svelte +84 -0
  60. package/dist/components/kit/checkbox.svelte.d.ts +8 -0
  61. package/dist/components/kit/confirm-dialog.svelte +127 -0
  62. package/dist/components/kit/confirm-dialog.svelte.d.ts +19 -0
  63. package/dist/components/kit/data-table.svelte +280 -0
  64. package/dist/components/kit/data-table.svelte.d.ts +46 -0
  65. package/dist/components/kit/date-field.svelte +210 -0
  66. package/dist/components/kit/date-field.svelte.d.ts +14 -0
  67. package/dist/components/kit/field-frame.svelte +71 -0
  68. package/dist/components/kit/field-frame.svelte.d.ts +26 -0
  69. package/dist/components/kit/fieldset-frame.svelte +69 -0
  70. package/dist/components/kit/fieldset-frame.svelte.d.ts +14 -0
  71. package/dist/components/kit/file-dropzone/files.d.ts +14 -0
  72. package/dist/components/kit/file-dropzone/files.js +69 -0
  73. package/dist/components/kit/file-dropzone.svelte +253 -0
  74. package/dist/components/kit/file-dropzone.svelte.d.ts +25 -0
  75. package/dist/components/kit/form.svelte +335 -0
  76. package/dist/components/kit/form.svelte.d.ts +61 -0
  77. package/dist/components/kit/input.svelte +73 -0
  78. package/dist/components/kit/input.svelte.d.ts +9 -0
  79. package/dist/components/kit/menu.svelte +163 -0
  80. package/dist/components/kit/menu.svelte.d.ts +28 -0
  81. package/dist/components/kit/pagination.svelte +134 -0
  82. package/dist/components/kit/pagination.svelte.d.ts +17 -0
  83. package/dist/components/kit/radio-list.svelte +131 -0
  84. package/dist/components/kit/radio-list.svelte.d.ts +15 -0
  85. package/dist/components/kit/segmented-control/segment.svelte +47 -0
  86. package/dist/components/kit/segmented-control/segment.svelte.d.ts +11 -0
  87. package/dist/components/kit/segmented-control.svelte +70 -0
  88. package/dist/components/kit/segmented-control.svelte.d.ts +16 -0
  89. package/dist/components/kit/select.svelte +244 -0
  90. package/dist/components/kit/select.svelte.d.ts +26 -0
  91. package/dist/components/kit/status-badge.svelte +54 -0
  92. package/dist/components/kit/status-badge.svelte.d.ts +31 -0
  93. package/dist/components/kit/switch.svelte +84 -0
  94. package/dist/components/kit/switch.svelte.d.ts +8 -0
  95. package/dist/components/kit/tabs.svelte +86 -0
  96. package/dist/components/kit/tabs.svelte.d.ts +20 -0
  97. package/dist/components/kit/textarea.svelte +92 -0
  98. package/dist/components/kit/textarea.svelte.d.ts +11 -0
  99. package/dist/components/ui/drawer/drawer-content.svelte +1 -1
  100. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  101. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  102. package/dist/components/ui/sonner/sonner.svelte +2 -4
  103. package/dist/mcp-content.json +1 -1
  104. package/dist/mcp.js +11 -11
  105. package/dist/server/hooks.d.ts +9 -0
  106. package/dist/server/hooks.js +41 -0
  107. package/dist/server/logger.d.ts +39 -0
  108. package/dist/server/logger.js +144 -0
  109. package/dist/server/telemetry.d.ts +23 -0
  110. package/dist/server/telemetry.js +82 -0
  111. package/dist/theme.css +18 -6
  112. package/package.json +40 -1
@@ -7,15 +7,13 @@
7
7
  step: number;
8
8
  label: string;
9
9
  valueText: string;
10
- onchange?: (value: number) => void;
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, oncommit, ...rest }: Props =
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?: (value: number) => void;
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-3 [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-4 md:[grid-template-areas:'meta_controls_volume'_'meta_seek_volume']"
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
- <LabelledSlider
210
- class="flex-1"
211
- value={seekable ? currentTime : 0}
212
- max={seekable ? duration : 1}
213
- step={1}
214
- disabled={!seekable}
215
- label={text.seek}
216
- valueText={`${formatTime(currentTime)} / ${formatTime(duration)}`}
217
- oncommit={(value) => (currentTime = value)}
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
- <InputOTP.Root
28
- maxlength={length}
29
- pattern="^\d+$"
30
- inputmode="numeric"
31
- autocomplete="one-time-code"
32
- {name}
33
- inputId={id}
34
- {disabled}
35
- bind:value
36
- onComplete={() => oncomplete?.(value)}
37
- >
38
- {#snippet children({ cells })}
39
- <InputOTP.Group>
40
- {#each cells.slice(0, half) as cell (cell)}
41
- <InputOTP.Slot
42
- {cell}
43
- aria-invalid={invalid}
44
- class="h-13 w-11 text-xl font-semibold tabular md:h-12 md:w-10"
45
- />
46
- {/each}
47
- </InputOTP.Group>
48
- <InputOTP.Separator />
49
- <InputOTP.Group>
50
- {#each cells.slice(half) as cell (cell)}
51
- <InputOTP.Slot
52
- {cell}
53
- aria-invalid={invalid}
54
- class="h-13 w-11 text-xl font-semibold tabular md:h-12 md:w-10"
55
- />
56
- {/each}
57
- </InputOTP.Group>
58
- {/snippet}
59
- </InputOTP.Root>
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
- <div class="relative">
35
- <Input
36
- {...rest}
37
- {name}
38
- {autocomplete}
39
- type={visible ? "text" : "password"}
40
- bind:value
41
- class={cn("h-11 pr-12", className)}
42
- />
43
- <div class="absolute inset-y-0 right-0 flex items-center">
44
- <IconButton
45
- label={visible ? text.hide : text.show}
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
- </div>
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;