@orochibraru/ui 0.0.0 → 1.0.0-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 (90) hide show
  1. package/README.md +97 -21
  2. package/dist/components/blocks/accent-toggle.svelte +85 -0
  3. package/dist/components/blocks/accent-toggle.svelte.d.ts +18 -0
  4. package/dist/components/blocks/bottom-nav.svelte +67 -0
  5. package/dist/components/blocks/bottom-nav.svelte.d.ts +18 -0
  6. package/dist/components/blocks/music-player/format-time.d.ts +2 -0
  7. package/dist/components/blocks/music-player/format-time.js +13 -0
  8. package/dist/components/blocks/music-player/labelled-slider.svelte +36 -0
  9. package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +15 -0
  10. package/dist/components/blocks/music-player/volume-control.svelte +48 -0
  11. package/dist/components/blocks/music-player/volume-control.svelte.d.ts +7 -0
  12. package/dist/components/blocks/music-player.svelte +263 -0
  13. package/dist/components/blocks/music-player.svelte.d.ts +26 -0
  14. package/dist/components/blocks/nav-cards.svelte +42 -0
  15. package/dist/components/blocks/nav-cards.svelte.d.ts +15 -0
  16. package/dist/components/blocks/new-password-fields.svelte +127 -0
  17. package/dist/components/blocks/new-password-fields.svelte.d.ts +19 -0
  18. package/dist/components/blocks/option-group.svelte +71 -0
  19. package/dist/components/blocks/option-group.svelte.d.ts +20 -0
  20. package/dist/components/blocks/otp-field.svelte +59 -0
  21. package/dist/components/blocks/otp-field.svelte.d.ts +12 -0
  22. package/dist/components/blocks/password-input.svelte +51 -0
  23. package/dist/components/blocks/password-input.svelte.d.ts +11 -0
  24. package/dist/components/blocks/stat-card.svelte +1 -1
  25. package/dist/components/blocks/tab-nav.svelte +38 -0
  26. package/dist/components/blocks/tab-nav.svelte.d.ts +14 -0
  27. package/dist/components/blocks/tab-section.svelte +48 -0
  28. package/dist/components/blocks/tab-section.svelte.d.ts +12 -0
  29. package/dist/components/blocks/theme-picker.svelte +69 -0
  30. package/dist/components/blocks/theme-picker.svelte.d.ts +16 -0
  31. package/dist/components/blocks/theme-toggle.svelte +68 -0
  32. package/dist/components/blocks/theme-toggle.svelte.d.ts +12 -0
  33. package/dist/components/blocks/video-player/center-controls.svelte +54 -0
  34. package/dist/components/blocks/video-player/center-controls.svelte.d.ts +10 -0
  35. package/dist/components/blocks/video-player/control-bar.svelte +184 -0
  36. package/dist/components/blocks/video-player/control-bar.svelte.d.ts +18 -0
  37. package/dist/components/blocks/video-player/control-button.svelte +54 -0
  38. package/dist/components/blocks/video-player/control-button.svelte.d.ts +13 -0
  39. package/dist/components/blocks/video-player/format.d.ts +9 -0
  40. package/dist/components/blocks/video-player/format.js +24 -0
  41. package/dist/components/blocks/video-player/gesture-feedback.svelte +32 -0
  42. package/dist/components/blocks/video-player/gesture-feedback.svelte.d.ts +7 -0
  43. package/dist/components/blocks/video-player/gestures.d.ts +13 -0
  44. package/dist/components/blocks/video-player/gestures.js +25 -0
  45. package/dist/components/blocks/video-player/keymap.d.ts +14 -0
  46. package/dist/components/blocks/video-player/keymap.js +55 -0
  47. package/dist/components/blocks/video-player/messages.d.ts +22 -0
  48. package/dist/components/blocks/video-player/messages.js +21 -0
  49. package/dist/components/blocks/video-player/option-menu.svelte +85 -0
  50. package/dist/components/blocks/video-player/option-menu.svelte.d.ts +18 -0
  51. package/dist/components/blocks/video-player/player.svelte.d.ts +37 -0
  52. package/dist/components/blocks/video-player/player.svelte.js +115 -0
  53. package/dist/components/blocks/video-player/scrub-bar.svelte +95 -0
  54. package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +12 -0
  55. package/dist/components/blocks/video-player/tap-gestures.svelte.d.ts +19 -0
  56. package/dist/components/blocks/video-player/tap-gestures.svelte.js +56 -0
  57. package/dist/components/blocks/video-player.svelte +288 -0
  58. package/dist/components/blocks/video-player.svelte.d.ts +29 -0
  59. package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
  60. package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
  61. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  62. package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
  63. package/dist/components/ui/command/command.svelte.d.ts +1 -1
  64. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  65. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  66. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  67. package/dist/components/ui/input/input.svelte +2 -2
  68. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  69. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +2 -2
  70. package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
  71. package/dist/components/ui/input-group/input-group.svelte +1 -1
  72. package/dist/components/ui/input-otp/input-otp-slot.svelte +1 -1
  73. package/dist/components/ui/input-otp/input-otp.svelte.d.ts +1 -1
  74. package/dist/components/ui/menubar/menubar-radio-group.svelte.d.ts +1 -1
  75. package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
  76. package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
  77. package/dist/components/ui/select/select-trigger.svelte +1 -1
  78. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +2 -2
  79. package/dist/components/ui/slider/slider.svelte.d.ts +1 -1
  80. package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
  81. package/dist/components/ui/textarea/textarea.svelte +1 -1
  82. package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
  83. package/dist/components/ui/toggle/toggle.svelte.d.ts +1 -1
  84. package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
  85. package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
  86. package/dist/mcp-content.json +1 -0
  87. package/dist/mcp.js +83 -0
  88. package/dist/password.d.ts +11 -0
  89. package/dist/password.js +38 -0
  90. package/package.json +15 -3
@@ -0,0 +1,263 @@
1
+ <script lang="ts" module>
2
+ export interface MusicTrack {
3
+ src: string;
4
+ title: string;
5
+ artist?: string;
6
+ artwork?: string;
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import Disc3Icon from "@lucide/svelte/icons/disc-3";
12
+ import FastForwardIcon from "@lucide/svelte/icons/fast-forward";
13
+ import PauseIcon from "@lucide/svelte/icons/pause";
14
+ import PlayIcon from "@lucide/svelte/icons/play";
15
+ import RewindIcon from "@lucide/svelte/icons/rewind";
16
+ import SkipBackIcon from "@lucide/svelte/icons/skip-back";
17
+ import SkipForwardIcon from "@lucide/svelte/icons/skip-forward";
18
+
19
+ import IconButton from "./icon-button.svelte";
20
+ import { Spinner } from "../ui/spinner/index.js";
21
+ import { haptic } from "../../haptics.js";
22
+
23
+ import { formatTime } from "./music-player/format-time.js";
24
+ import LabelledSlider from "./music-player/labelled-slider.svelte";
25
+ import VolumeControl from "./music-player/volume-control.svelte";
26
+
27
+ const defaultMessages = {
28
+ player: "Music player",
29
+ play: "Play",
30
+ pause: "Pause",
31
+ previous: "Previous",
32
+ next: "Next",
33
+ back: "Back 5 seconds",
34
+ forward: "Forward 5 seconds",
35
+ seek: "Seek",
36
+ volume: "Volume",
37
+ };
38
+
39
+ interface Props {
40
+ tracks: MusicTrack[];
41
+ index?: number;
42
+ paused?: boolean;
43
+ onchange?: (track: MusicTrack, index: number) => void;
44
+ onended?: (track: MusicTrack, index: number) => void;
45
+ messages?: Partial<typeof defaultMessages>;
46
+ }
47
+
48
+ let {
49
+ tracks,
50
+ index = $bindable(0),
51
+ paused = $bindable(true),
52
+ onchange,
53
+ onended,
54
+ messages = {},
55
+ }: Props = $props();
56
+
57
+ const SKIP_SECONDS = 5;
58
+ const RESTART_AFTER_SECONDS = 3;
59
+
60
+ const text = $derived({ ...defaultMessages, ...messages });
61
+ const track = $derived(tracks[index]);
62
+ const hasNext = $derived(index < tracks.length - 1);
63
+
64
+ let audio: HTMLAudioElement | undefined = $state();
65
+ let currentTime = $state(0);
66
+ let duration = $state(0);
67
+ let volume = $state(1);
68
+ let waiting = $state(false);
69
+ let loadedSource: string | undefined;
70
+
71
+ const seekable = $derived(Number.isFinite(duration) && duration > 0);
72
+ const ToggleIcon = $derived(
73
+ paused ? PlayIcon : waiting ? Spinner : PauseIcon,
74
+ );
75
+
76
+ $effect(() => {
77
+ const source = track?.src;
78
+ const shouldPlay = !paused;
79
+ if (!audio) {
80
+ return;
81
+ }
82
+ const reload = source !== loadedSource;
83
+ if (reload) {
84
+ loadedSource = source;
85
+ currentTime = 0;
86
+ waiting = false;
87
+ if (source) {
88
+ audio.src = source;
89
+ } else {
90
+ audio.removeAttribute("src");
91
+ }
92
+ audio.load();
93
+ }
94
+ if (shouldPlay && source && (reload || audio.paused)) {
95
+ void start(audio);
96
+ } else if (!shouldPlay && !audio.paused) {
97
+ audio.pause();
98
+ }
99
+ });
100
+
101
+ async function start(element: HTMLAudioElement) {
102
+ try {
103
+ await element.play();
104
+ } catch (error) {
105
+ if (!(error instanceof DOMException && error.name === "AbortError")) {
106
+ paused = true;
107
+ }
108
+ }
109
+ }
110
+
111
+ function go(to: number) {
112
+ const target = tracks[to];
113
+ if (!target) {
114
+ return;
115
+ }
116
+ index = to;
117
+ onchange?.(target, to);
118
+ }
119
+
120
+ function toggle() {
121
+ haptic("tap");
122
+ paused = !paused;
123
+ }
124
+
125
+ function previous() {
126
+ haptic("tap");
127
+ if (currentTime > RESTART_AFTER_SECONDS || index === 0) {
128
+ currentTime = 0;
129
+ return;
130
+ }
131
+ go(index - 1);
132
+ }
133
+
134
+ function next() {
135
+ haptic("tap");
136
+ go(index + 1);
137
+ }
138
+
139
+ function skip(by: number) {
140
+ if (seekable) {
141
+ currentTime = Math.min(Math.max(currentTime + by, 0), duration);
142
+ }
143
+ }
144
+
145
+ function ended() {
146
+ if (track) {
147
+ onended?.(track, index);
148
+ }
149
+ if (hasNext) {
150
+ go(index + 1);
151
+ paused = false;
152
+ }
153
+ }
154
+
155
+ function syncPaused() {
156
+ if (audio) {
157
+ paused = audio.paused;
158
+ }
159
+ }
160
+ </script>
161
+
162
+ <audio
163
+ bind:this={audio}
164
+ bind:currentTime
165
+ bind:duration
166
+ bind:volume
167
+ preload="metadata"
168
+ onplay={syncPaused}
169
+ onpause={syncPaused}
170
+ onwaiting={() => (waiting = true)}
171
+ onplaying={() => (waiting = false)}
172
+ oncanplay={() => (waiting = false)}
173
+ onended={ended}
174
+ ></audio>
175
+
176
+ {#if track}
177
+ <section
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']"
180
+ >
181
+ <div class="flex min-w-0 items-center gap-3 [grid-area:meta]">
182
+ {#if track.artwork}
183
+ <img
184
+ src={track.artwork}
185
+ alt=""
186
+ class="size-12 shrink-0 rounded-xl object-cover"
187
+ />
188
+ {:else}
189
+ <div
190
+ class="bg-primary/12 text-primary flex size-12 shrink-0 items-center justify-center rounded-xl"
191
+ >
192
+ <Disc3Icon class="size-6" />
193
+ </div>
194
+ {/if}
195
+ <div class="min-w-0">
196
+ <p class="truncate font-medium">{track.title}</p>
197
+ {#if track.artist}
198
+ <p class="text-muted-foreground truncate text-sm">
199
+ {track.artist}
200
+ </p>
201
+ {/if}
202
+ </div>
203
+ </div>
204
+
205
+ <div class="flex items-center gap-3 [grid-area:seek]">
206
+ <span class="text-muted-foreground tabular min-w-10 text-xs">
207
+ {formatTime(currentTime)}
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
+ />
219
+ <span class="text-muted-foreground tabular min-w-10 text-right text-xs">
220
+ {formatTime(duration)}
221
+ </span>
222
+ </div>
223
+
224
+ <div
225
+ class="flex items-center justify-center gap-1 [grid-area:controls] md:gap-2"
226
+ >
227
+ <IconButton
228
+ label={text.back}
229
+ icon={RewindIcon}
230
+ disabled={!seekable}
231
+ onclick={() => skip(-SKIP_SECONDS)}
232
+ />
233
+ <IconButton
234
+ label={text.previous}
235
+ icon={SkipBackIcon}
236
+ onclick={previous}
237
+ />
238
+ <IconButton
239
+ label={paused ? text.play : text.pause}
240
+ icon={ToggleIcon}
241
+ variant="default"
242
+ class="glow size-14 md:size-11"
243
+ onclick={toggle}
244
+ />
245
+ <IconButton
246
+ label={text.next}
247
+ icon={SkipForwardIcon}
248
+ disabled={!hasNext}
249
+ onclick={next}
250
+ />
251
+ <IconButton
252
+ label={text.forward}
253
+ icon={FastForwardIcon}
254
+ disabled={!seekable}
255
+ onclick={() => skip(SKIP_SECONDS)}
256
+ />
257
+ </div>
258
+
259
+ <div class="[grid-area:volume]">
260
+ <VolumeControl bind:volume label={text.volume} />
261
+ </div>
262
+ </section>
263
+ {/if}
@@ -0,0 +1,26 @@
1
+ export interface MusicTrack {
2
+ src: string;
3
+ title: string;
4
+ artist?: string;
5
+ artwork?: string;
6
+ }
7
+ declare const MusicPlayer: import("svelte").Component<{
8
+ tracks: MusicTrack[];
9
+ index?: number;
10
+ paused?: boolean;
11
+ onchange?: (track: MusicTrack, index: number) => void;
12
+ onended?: (track: MusicTrack, index: number) => void;
13
+ messages?: Partial<{
14
+ player: string;
15
+ play: string;
16
+ pause: string;
17
+ previous: string;
18
+ next: string;
19
+ back: string;
20
+ forward: string;
21
+ seek: string;
22
+ volume: string;
23
+ }>;
24
+ }, {}, "index" | "paused">;
25
+ type MusicPlayer = ReturnType<typeof MusicPlayer>;
26
+ export default MusicPlayer;
@@ -0,0 +1,42 @@
1
+ <script lang="ts" module>
2
+ import type { Component } from "svelte";
3
+
4
+ export interface SectionTab {
5
+ href: string;
6
+ label: string;
7
+ description: string;
8
+ icon: Component<{ class?: string }>;
9
+ }
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
14
+
15
+ const { tabs }: { tabs: SectionTab[] } = $props();
16
+ </script>
17
+
18
+ <nav aria-label="Sections" class="md:hidden">
19
+ <ul class="glass divide-border divide-y overflow-hidden rounded-2xl">
20
+ {#each tabs as tab (tab.href)}
21
+ <li>
22
+ <a
23
+ href={tab.href}
24
+ class="active:bg-muted flex min-h-18 items-center gap-4 px-4 py-3 transition-colors"
25
+ >
26
+ <span
27
+ class="bg-accent text-accent-foreground flex size-11 shrink-0 items-center justify-center rounded-xl"
28
+ >
29
+ <tab.icon class="size-5" />
30
+ </span>
31
+ <span class="min-w-0 flex-1">
32
+ <span class="block font-semibold">{tab.label}</span>
33
+ <span class="text-muted-foreground block text-sm"
34
+ >{tab.description}</span
35
+ >
36
+ </span>
37
+ <ChevronRightIcon class="text-muted-foreground/60 size-5 shrink-0" />
38
+ </a>
39
+ </li>
40
+ {/each}
41
+ </ul>
42
+ </nav>
@@ -0,0 +1,15 @@
1
+ import type { Component } from "svelte";
2
+ export interface SectionTab {
3
+ href: string;
4
+ label: string;
5
+ description: string;
6
+ icon: Component<{
7
+ class?: string;
8
+ }>;
9
+ }
10
+ type $$ComponentProps = {
11
+ tabs: SectionTab[];
12
+ };
13
+ declare const NavCards: Component<$$ComponentProps, {}, "">;
14
+ type NavCards = ReturnType<typeof NavCards>;
15
+ export default NavCards;
@@ -0,0 +1,127 @@
1
+ <script lang="ts">
2
+ import CheckIcon from "@lucide/svelte/icons/check";
3
+ import XIcon from "@lucide/svelte/icons/x";
4
+
5
+ import PasswordInput from "./password-input.svelte";
6
+ import * as Field from "../ui/field/index.js";
7
+ import { Input } from "../ui/input/index.js";
8
+ import {
9
+ passwordMaxLength,
10
+ passwordRules,
11
+ passwordScore,
12
+ } from "../../password.js";
13
+
14
+ const defaultMessages = {
15
+ password: "Password",
16
+ confirm: "Type it again",
17
+ mismatch: "The passwords don't match.",
18
+ weak: "Weak",
19
+ fair: "Fair",
20
+ good: "Good",
21
+ strong: "Strong",
22
+ show: "Show password",
23
+ hide: "Hide password",
24
+ };
25
+
26
+ interface Props {
27
+ value?: string;
28
+ email?: string;
29
+ errors?: Record<string, string[] | undefined>;
30
+ ruleLabels?: Record<string, string>;
31
+ messages?: Partial<typeof defaultMessages>;
32
+ }
33
+
34
+ let {
35
+ value = $bindable(""),
36
+ email = "",
37
+ errors = {},
38
+ ruleLabels = {},
39
+ messages = {},
40
+ }: Props = $props();
41
+
42
+ let confirmation = $state("");
43
+ let visible = $state(false);
44
+
45
+ const text = $derived({ ...defaultMessages, ...messages });
46
+ const rules = $derived(
47
+ passwordRules(value, email).filter((rule) => rule.id !== "max"),
48
+ );
49
+ const score = $derived(passwordScore(value, email));
50
+ const strengthLabels = $derived([
51
+ "",
52
+ text.weak,
53
+ text.fair,
54
+ text.good,
55
+ text.strong,
56
+ ]);
57
+ const strengthColors = [
58
+ "bg-muted",
59
+ "bg-destructive",
60
+ "bg-orange-500",
61
+ "bg-primary/70",
62
+ "bg-primary",
63
+ ];
64
+ const mismatch = $derived(confirmation.length > 0 && confirmation !== value);
65
+ </script>
66
+
67
+ <Field.Field>
68
+ <Field.Label for="new-password">{text.password}</Field.Label>
69
+ <PasswordInput
70
+ id="new-password"
71
+ autocomplete="new-password"
72
+ maxlength={passwordMaxLength}
73
+ required
74
+ bind:value
75
+ bind:visible
76
+ messages={{ show: text.show, hide: text.hide }}
77
+ />
78
+ <div class="mt-1 flex items-center gap-2" aria-live="polite">
79
+ <div class="flex flex-1 gap-1">
80
+ {#each [1, 2, 3, 4] as step (step)}
81
+ <span
82
+ class="h-1.5 flex-1 rounded-full transition-colors {score >= step
83
+ ? strengthColors[score]
84
+ : 'bg-muted'}"
85
+ ></span>
86
+ {/each}
87
+ </div>
88
+ <span class="text-muted-foreground w-12 text-right text-xs"
89
+ >{strengthLabels[score]}</span
90
+ >
91
+ </div>
92
+ <ul class="mt-1 flex flex-col gap-1 text-sm">
93
+ {#each rules as rule (rule.id)}
94
+ <li
95
+ class="flex items-center gap-2 {rule.passed
96
+ ? 'text-positive'
97
+ : 'text-muted-foreground'}"
98
+ >
99
+ {#if rule.passed}<CheckIcon class="size-4" />{:else}<XIcon
100
+ class="size-4"
101
+ />{/if}
102
+ {ruleLabels[rule.id] ?? rule.label}
103
+ </li>
104
+ {/each}
105
+ </ul>
106
+ <Field.Error errors={errors.password?.map((message) => ({ message }))} />
107
+ </Field.Field>
108
+ <Field.Field>
109
+ <Field.Label for="confirm-password">{text.confirm}</Field.Label>
110
+ <Input
111
+ id="confirm-password"
112
+ name="confirmPassword"
113
+ type={visible ? "text" : "password"}
114
+ autocomplete="new-password"
115
+ maxlength={passwordMaxLength}
116
+ required
117
+ bind:value={confirmation}
118
+ aria-invalid={mismatch}
119
+ class="h-11"
120
+ />
121
+ {#if mismatch}
122
+ <p class="text-destructive text-sm">{text.mismatch}</p>
123
+ {/if}
124
+ <Field.Error
125
+ errors={errors.confirmPassword?.map((message) => ({ message }))}
126
+ />
127
+ </Field.Field>
@@ -0,0 +1,19 @@
1
+ declare const NewPasswordFields: import("svelte").Component<{
2
+ value?: string;
3
+ email?: string;
4
+ errors?: Record<string, string[] | undefined>;
5
+ ruleLabels?: Record<string, string>;
6
+ messages?: Partial<{
7
+ password: string;
8
+ confirm: string;
9
+ mismatch: string;
10
+ weak: string;
11
+ fair: string;
12
+ good: string;
13
+ strong: string;
14
+ show: string;
15
+ hide: string;
16
+ }>;
17
+ }, {}, "value">;
18
+ type NewPasswordFields = ReturnType<typeof NewPasswordFields>;
19
+ export default NewPasswordFields;
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import CheckIcon from "@lucide/svelte/icons/check";
3
+ import type { Component, Snippet } from "svelte";
4
+
5
+ interface Option {
6
+ value: string;
7
+ label: string;
8
+ description?: string;
9
+ icon?: Component<{ class?: string }>;
10
+ }
11
+
12
+ interface Props {
13
+ name: string;
14
+ legend: string;
15
+ options: readonly Option[];
16
+ value: string;
17
+ onchange: () => void;
18
+ swatch?: Snippet<[Option]>;
19
+ }
20
+
21
+ let {
22
+ name,
23
+ legend,
24
+ options,
25
+ value = $bindable(),
26
+ onchange,
27
+ swatch,
28
+ }: Props = $props();
29
+ </script>
30
+
31
+ <fieldset>
32
+ <legend class="mb-2 px-1 text-sm font-medium">{legend}</legend>
33
+ <div class="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
34
+ {#each options as option (option.value)}
35
+ <label
36
+ class="group border-border bg-card has-checked:border-primary has-checked:ring-primary/30 has-focus-visible:ring-ring/50 flex min-h-14 cursor-pointer items-center gap-3 rounded-2xl border p-3 transition-all has-checked:ring-2 has-focus-visible:ring-3"
37
+ >
38
+ <input
39
+ type="radio"
40
+ {name}
41
+ value={option.value}
42
+ bind:group={value}
43
+ {onchange}
44
+ class="sr-only"
45
+ />
46
+ <span
47
+ class="bg-accent text-accent-foreground flex size-10 shrink-0 items-center justify-center rounded-xl"
48
+ aria-hidden="true"
49
+ >
50
+ {#if swatch}
51
+ {@render swatch(option)}
52
+ {:else if option.icon}
53
+ <option.icon class="size-5" />
54
+ {/if}
55
+ </span>
56
+ <span class="min-w-0 flex-1">
57
+ <span class="block text-sm font-medium">{option.label}</span>
58
+ {#if option.description}
59
+ <span class="text-muted-foreground block text-sm"
60
+ >{option.description}</span
61
+ >
62
+ {/if}
63
+ </span>
64
+ <CheckIcon
65
+ class="text-primary hidden size-5 shrink-0 group-has-checked:block"
66
+ aria-hidden="true"
67
+ />
68
+ </label>
69
+ {/each}
70
+ </div>
71
+ </fieldset>
@@ -0,0 +1,20 @@
1
+ import type { Component, Snippet } from "svelte";
2
+ interface Option {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ icon?: Component<{
7
+ class?: string;
8
+ }>;
9
+ }
10
+ interface Props {
11
+ name: string;
12
+ legend: string;
13
+ options: readonly Option[];
14
+ value: string;
15
+ onchange: () => void;
16
+ swatch?: Snippet<[Option]>;
17
+ }
18
+ declare const OptionGroup: Component<Props, {}, "value">;
19
+ type OptionGroup = ReturnType<typeof OptionGroup>;
20
+ export default OptionGroup;
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import * as InputOTP from "../ui/input-otp/index.js";
3
+
4
+ interface Props {
5
+ value?: string;
6
+ length?: number;
7
+ name?: string;
8
+ id?: string;
9
+ disabled?: boolean;
10
+ invalid?: boolean;
11
+ oncomplete?: (code: string) => void;
12
+ }
13
+
14
+ let {
15
+ value = $bindable(""),
16
+ length = 6,
17
+ name = "code",
18
+ id,
19
+ disabled = false,
20
+ invalid = false,
21
+ oncomplete,
22
+ }: Props = $props();
23
+
24
+ const half = $derived(Math.ceil(length / 2));
25
+ </script>
26
+
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>
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ value?: string;
3
+ length?: number;
4
+ name?: string;
5
+ id?: string;
6
+ disabled?: boolean;
7
+ invalid?: boolean;
8
+ oncomplete?: (code: string) => void;
9
+ }
10
+ declare const OtpField: import("svelte").Component<Props, {}, "value">;
11
+ type OtpField = ReturnType<typeof OtpField>;
12
+ export default OtpField;