@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.
- package/README.md +97 -21
- package/dist/components/blocks/accent-toggle.svelte +85 -0
- package/dist/components/blocks/accent-toggle.svelte.d.ts +18 -0
- package/dist/components/blocks/bottom-nav.svelte +67 -0
- package/dist/components/blocks/bottom-nav.svelte.d.ts +18 -0
- package/dist/components/blocks/music-player/format-time.d.ts +2 -0
- package/dist/components/blocks/music-player/format-time.js +13 -0
- package/dist/components/blocks/music-player/labelled-slider.svelte +36 -0
- package/dist/components/blocks/music-player/labelled-slider.svelte.d.ts +15 -0
- package/dist/components/blocks/music-player/volume-control.svelte +48 -0
- package/dist/components/blocks/music-player/volume-control.svelte.d.ts +7 -0
- package/dist/components/blocks/music-player.svelte +263 -0
- package/dist/components/blocks/music-player.svelte.d.ts +26 -0
- package/dist/components/blocks/nav-cards.svelte +42 -0
- package/dist/components/blocks/nav-cards.svelte.d.ts +15 -0
- package/dist/components/blocks/new-password-fields.svelte +127 -0
- package/dist/components/blocks/new-password-fields.svelte.d.ts +19 -0
- package/dist/components/blocks/option-group.svelte +71 -0
- package/dist/components/blocks/option-group.svelte.d.ts +20 -0
- package/dist/components/blocks/otp-field.svelte +59 -0
- package/dist/components/blocks/otp-field.svelte.d.ts +12 -0
- package/dist/components/blocks/password-input.svelte +51 -0
- package/dist/components/blocks/password-input.svelte.d.ts +11 -0
- package/dist/components/blocks/stat-card.svelte +1 -1
- package/dist/components/blocks/tab-nav.svelte +38 -0
- package/dist/components/blocks/tab-nav.svelte.d.ts +14 -0
- package/dist/components/blocks/tab-section.svelte +48 -0
- package/dist/components/blocks/tab-section.svelte.d.ts +12 -0
- package/dist/components/blocks/theme-picker.svelte +69 -0
- package/dist/components/blocks/theme-picker.svelte.d.ts +16 -0
- package/dist/components/blocks/theme-toggle.svelte +68 -0
- package/dist/components/blocks/theme-toggle.svelte.d.ts +12 -0
- package/dist/components/blocks/video-player/center-controls.svelte +54 -0
- package/dist/components/blocks/video-player/center-controls.svelte.d.ts +10 -0
- package/dist/components/blocks/video-player/control-bar.svelte +184 -0
- package/dist/components/blocks/video-player/control-bar.svelte.d.ts +18 -0
- package/dist/components/blocks/video-player/control-button.svelte +54 -0
- package/dist/components/blocks/video-player/control-button.svelte.d.ts +13 -0
- package/dist/components/blocks/video-player/format.d.ts +9 -0
- package/dist/components/blocks/video-player/format.js +24 -0
- package/dist/components/blocks/video-player/gesture-feedback.svelte +32 -0
- package/dist/components/blocks/video-player/gesture-feedback.svelte.d.ts +7 -0
- package/dist/components/blocks/video-player/gestures.d.ts +13 -0
- package/dist/components/blocks/video-player/gestures.js +25 -0
- package/dist/components/blocks/video-player/keymap.d.ts +14 -0
- package/dist/components/blocks/video-player/keymap.js +55 -0
- package/dist/components/blocks/video-player/messages.d.ts +22 -0
- package/dist/components/blocks/video-player/messages.js +21 -0
- package/dist/components/blocks/video-player/option-menu.svelte +85 -0
- package/dist/components/blocks/video-player/option-menu.svelte.d.ts +18 -0
- package/dist/components/blocks/video-player/player.svelte.d.ts +37 -0
- package/dist/components/blocks/video-player/player.svelte.js +115 -0
- package/dist/components/blocks/video-player/scrub-bar.svelte +95 -0
- package/dist/components/blocks/video-player/scrub-bar.svelte.d.ts +12 -0
- package/dist/components/blocks/video-player/tap-gestures.svelte.d.ts +19 -0
- package/dist/components/blocks/video-player/tap-gestures.svelte.js +56 -0
- package/dist/components/blocks/video-player.svelte +288 -0
- package/dist/components/blocks/video-player.svelte.d.ts +29 -0
- package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
- package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
- package/dist/components/ui/command/command.svelte.d.ts +1 -1
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/input/input.svelte +2 -2
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +2 -2
- package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group.svelte +1 -1
- package/dist/components/ui/input-otp/input-otp-slot.svelte +1 -1
- package/dist/components/ui/input-otp/input-otp.svelte.d.ts +1 -1
- package/dist/components/ui/menubar/menubar-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/components/ui/select/select-trigger.svelte +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +2 -2
- package/dist/components/ui/slider/slider.svelte.d.ts +1 -1
- package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
- package/dist/components/ui/textarea/textarea.svelte +1 -1
- package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
- package/dist/components/ui/toggle/toggle.svelte.d.ts +1 -1
- package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
- package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
- package/dist/mcp-content.json +1 -0
- package/dist/mcp.js +83 -0
- package/dist/password.d.ts +11 -0
- package/dist/password.js +38 -0
- 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;
|