@svadmin/ui 0.75.0 → 0.77.0
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/dist/app.css +383 -22
- package/dist/app.theme.css +383 -22
- package/dist/component-registry.d.ts +300 -0
- package/dist/component-registry.js +2 -0
- package/dist/components/AppFooter.svelte +137 -0
- package/dist/components/AppFooter.svelte.d.ts +25 -0
- package/dist/components/CodeEditor.svelte +145 -0
- package/dist/components/CodeEditor.svelte.d.ts +26 -0
- package/dist/components/HighlightText.svelte +46 -0
- package/dist/components/HighlightText.svelte.d.ts +13 -0
- package/dist/components/JsonEditor.svelte +182 -0
- package/dist/components/JsonEditor.svelte.d.ts +22 -0
- package/dist/components/Layout.svelte +38 -5
- package/dist/components/Layout.svelte.d.ts +4 -3
- package/dist/components/Marquee.svelte +104 -0
- package/dist/components/Marquee.svelte.d.ts +20 -0
- package/dist/components/MediaPlayer.svelte +104 -0
- package/dist/components/MediaPlayer.svelte.d.ts +21 -0
- package/dist/components/PhoneInput.svelte +157 -0
- package/dist/components/PhoneInput.svelte.d.ts +26 -0
- package/dist/components/QRCode.svelte +88 -0
- package/dist/components/QRCode.svelte.d.ts +22 -0
- package/dist/components/ResizableGrid.svelte +252 -0
- package/dist/components/ResizableGrid.svelte.d.ts +34 -0
- package/dist/components/Sidebar.svelte +1 -1
- package/dist/components/TopProgressBar.svelte +90 -0
- package/dist/components/TopProgressBar.svelte.d.ts +15 -0
- package/dist/components/browser.svelte.d.ts +65 -0
- package/dist/components/browser.svelte.js +228 -0
- package/dist/components/charts/AreaChart.svelte +78 -0
- package/dist/components/charts/AreaChart.svelte.d.ts +17 -0
- package/dist/components/charts/ScatterChart.svelte +82 -0
- package/dist/components/charts/ScatterChart.svelte.d.ts +17 -0
- package/dist/components/charts/index.d.ts +2 -0
- package/dist/components/charts/index.js +2 -0
- package/dist/components/code-editor-presets.d.ts +14 -0
- package/dist/components/code-editor-presets.js +14 -0
- package/dist/components/content/ContentPageHeader.svelte +17 -3
- package/dist/components/content/ContentPageHeader.svelte.d.ts +1 -0
- package/dist/components/content/ContentPageShell.svelte +12 -3
- package/dist/components/content/ContentPageShell.svelte.d.ts +1 -0
- package/dist/components/content/MetricBlock.svelte +1 -1
- package/dist/components/highlight.d.ts +16 -0
- package/dist/components/highlight.js +68 -0
- package/dist/components/media.svelte.d.ts +27 -0
- package/dist/components/media.svelte.js +60 -0
- package/dist/components/phone-dial-codes.d.ts +19 -0
- package/dist/components/phone-dial-codes.js +112 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.js +14 -1
- package/dist/recipes.d.ts +77 -38
- package/dist/recipes.js +33 -8
- package/package.json +64 -4
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { Menu } from '@lucide/svelte';
|
|
28
28
|
import { getComponentRegistry } from '../component-registry.svelte.js';
|
|
29
29
|
import { Button } from './ui/button/index.js';
|
|
30
|
+
import { cn } from '../utils.js';
|
|
30
31
|
|
|
31
32
|
const i18n = useTranslation();
|
|
32
33
|
|
|
@@ -34,7 +35,25 @@
|
|
|
34
35
|
let shortcutsOpen = $state(false);
|
|
35
36
|
let mobileMenuOpen = $state(false);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
interface Props {
|
|
39
|
+
children: Snippet;
|
|
40
|
+
title?: string;
|
|
41
|
+
menu?: MenuItem[];
|
|
42
|
+
siteUrl?: string;
|
|
43
|
+
routeMode?: 'hash' | 'path' | 'auto';
|
|
44
|
+
aiAssistant?: Snippet<[LayoutAIAssistantProps]>;
|
|
45
|
+
class?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let {
|
|
49
|
+
children,
|
|
50
|
+
title = 'Admin',
|
|
51
|
+
menu,
|
|
52
|
+
siteUrl,
|
|
53
|
+
routeMode = 'auto',
|
|
54
|
+
aiAssistant,
|
|
55
|
+
class: className = '',
|
|
56
|
+
}: Props = $props();
|
|
38
57
|
const layoutId = $props.id();
|
|
39
58
|
const layoutScope = `svadmin-layout-${layoutId}`;
|
|
40
59
|
const mainContentId = `${layoutScope}-main`;
|
|
@@ -150,7 +169,14 @@
|
|
|
150
169
|
ontouchend={handleTouchEnd}
|
|
151
170
|
/>
|
|
152
171
|
{#if loading}
|
|
153
|
-
<div
|
|
172
|
+
<div
|
|
173
|
+
data-svadmin-layout-scope={layoutScope}
|
|
174
|
+
data-svadmin-layout
|
|
175
|
+
data-svadmin-layout-state="loading"
|
|
176
|
+
aria-busy="true"
|
|
177
|
+
class={cn('svadmin-u-60fbb7713999 svadmin-u-ef114b5f5ad1', className)}
|
|
178
|
+
in:fade={{ duration: 150 }}
|
|
179
|
+
>
|
|
154
180
|
<div class="svadmin-u-99d72c7fc3e2 svadmin-u-9d60be3a6d80 svadmin-u-e2fe2012a697 svadmin-u-ac402894e356 svadmin-u-8e63407b5ceb svadmin-u-3e7ce58d64fa">
|
|
155
181
|
<Skeleton class="svadmin-u-ed8a5df7b2fb svadmin-u-516b03df0b7c" />
|
|
156
182
|
<div class="svadmin-u-6f7e013d6499 svadmin-u-31f2553311b6">
|
|
@@ -172,7 +198,14 @@
|
|
|
172
198
|
<DevTools docked />
|
|
173
199
|
</div>
|
|
174
200
|
{:else}
|
|
175
|
-
<div
|
|
201
|
+
<div
|
|
202
|
+
data-svadmin-layout-scope={layoutScope}
|
|
203
|
+
data-svadmin-layout
|
|
204
|
+
data-svadmin-layout-state="ready"
|
|
205
|
+
aria-busy="false"
|
|
206
|
+
class={cn('svadmin-u-60fbb7713999 svadmin-u-ef114b5f5ad1 svadmin-u-e6f9e383a762', className)}
|
|
207
|
+
in:fade={{ duration: 200, delay: 50 }}
|
|
208
|
+
>
|
|
176
209
|
<button
|
|
177
210
|
type="button"
|
|
178
211
|
data-svadmin-skip-link={mainContentId}
|
|
@@ -241,7 +274,7 @@
|
|
|
241
274
|
|
|
242
275
|
<!-- Content area: responsive padding + centered max-width container
|
|
243
276
|
so wide screens don't stretch content indefinitely (avoids sparse layouts) -->
|
|
244
|
-
<main id={mainContentId} tabindex="-1" data-svadmin-main class="svadmin-u-36e579c0b41c svadmin-u-92bf82f493b1 svadmin-u-2859c861d7de svadmin-u-f0faeb26d656 svadmin-u-c9b99cd93450 svadmin-u-cc06a6575385 svadmin-u-daf5dc5fac2b svadmin-u-e86fadb84483">
|
|
277
|
+
<main id={mainContentId} tabindex="-1" data-svadmin-main data-svadmin-layout-content class="svadmin-u-36e579c0b41c svadmin-u-92bf82f493b1 svadmin-u-2859c861d7de svadmin-u-f0faeb26d656 svadmin-u-c9b99cd93450 svadmin-u-cc06a6575385 svadmin-u-daf5dc5fac2b svadmin-u-e86fadb84483">
|
|
245
278
|
<div class="svadmin-u-0e12dc7de920 svadmin-u-6da6a3c3f741 svadmin-u-9e3dc30c26a3">
|
|
246
279
|
{#key getPath()}
|
|
247
280
|
<!-- 路由切换立即释放旧页面,避免退出期间继续读取已失效的路由派生状态。 -->
|
|
@@ -252,7 +285,7 @@
|
|
|
252
285
|
</div>
|
|
253
286
|
</main>
|
|
254
287
|
|
|
255
|
-
<footer class="svadmin-u-60fbb7713999 svadmin-u-0cfe3fb5e434 svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-77c08e015d14 svadmin-u-77a2a20e90d4 svadmin-u-b950dda299d3 svadmin-u-05faf5c801ff svadmin-u-e6f9e383a762 svadmin-u-f0faeb26d656 svadmin-u-472f43d3a27b">
|
|
288
|
+
<footer data-svadmin-layout-footer class="svadmin-u-60fbb7713999 svadmin-u-0cfe3fb5e434 svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-77c08e015d14 svadmin-u-77a2a20e90d4 svadmin-u-b950dda299d3 svadmin-u-05faf5c801ff svadmin-u-e6f9e383a762 svadmin-u-f0faeb26d656 svadmin-u-472f43d3a27b">
|
|
256
289
|
<DevTools docked />
|
|
257
290
|
{@render aiAssistant?.({ docked: true, scope: chatScope, ownerScope: layoutScope })}
|
|
258
291
|
</footer>
|
|
@@ -5,14 +5,15 @@ export interface LayoutAIAssistantProps {
|
|
|
5
5
|
}
|
|
6
6
|
import type { Snippet } from 'svelte';
|
|
7
7
|
import type { MenuItem } from '@svadmin/core';
|
|
8
|
-
|
|
8
|
+
interface Props {
|
|
9
9
|
children: Snippet;
|
|
10
10
|
title?: string;
|
|
11
11
|
menu?: MenuItem[];
|
|
12
12
|
siteUrl?: string;
|
|
13
13
|
routeMode?: 'hash' | 'path' | 'auto';
|
|
14
14
|
aiAssistant?: Snippet<[LayoutAIAssistantProps]>;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const Layout: import("svelte").Component<Props, {}, "">;
|
|
17
18
|
type Layout = ReturnType<typeof Layout>;
|
|
18
19
|
export default Layout;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Plain text shortcut; ignored when `children` is provided. */
|
|
7
|
+
text?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
/** Seconds for one full loop. */
|
|
10
|
+
duration?: number;
|
|
11
|
+
direction?: 'left' | 'right';
|
|
12
|
+
vertical?: boolean;
|
|
13
|
+
/** Copies of the content used to build a seamless loop. */
|
|
14
|
+
repeat?: number;
|
|
15
|
+
pauseOnHover?: boolean;
|
|
16
|
+
/** Fades the leading and trailing edges. */
|
|
17
|
+
fade?: boolean;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
text,
|
|
24
|
+
children,
|
|
25
|
+
duration = 14,
|
|
26
|
+
direction = 'left',
|
|
27
|
+
vertical = false,
|
|
28
|
+
repeat = 2,
|
|
29
|
+
pauseOnHover = true,
|
|
30
|
+
fade = true,
|
|
31
|
+
ariaLabel,
|
|
32
|
+
class: className = '',
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const copies = $derived(Math.max(2, Math.round(repeat)));
|
|
36
|
+
const safeDuration = $derived(duration > 0 ? duration : 14);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
class={cn('svadmin-marquee', className)}
|
|
41
|
+
data-slot="marquee"
|
|
42
|
+
data-direction={direction}
|
|
43
|
+
data-vertical={vertical ? 'true' : 'false'}
|
|
44
|
+
data-pause={pauseOnHover ? 'true' : 'false'}
|
|
45
|
+
data-fade={fade ? 'true' : 'false'}
|
|
46
|
+
role="marquee"
|
|
47
|
+
aria-label={ariaLabel}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
class="svadmin-marquee__track"
|
|
51
|
+
style={`--svadmin-marquee-duration: ${safeDuration}s; --svadmin-marquee-repeat: ${copies};`}
|
|
52
|
+
>
|
|
53
|
+
{#each Array.from({ length: copies }) as _, index (index)}
|
|
54
|
+
<div class="svadmin-marquee__item" aria-hidden={index > 0 ? 'true' : undefined}>
|
|
55
|
+
{#if children}{@render children()}{:else}{text}{/if}
|
|
56
|
+
</div>
|
|
57
|
+
{/each}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.svadmin-marquee {
|
|
63
|
+
position: relative;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
color: inherit;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.svadmin-marquee[data-fade='true'] {
|
|
69
|
+
mask-image: linear-gradient(to right, transparent, currentColor 8%, currentColor 92%, transparent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.svadmin-marquee__track {
|
|
73
|
+
display: flex;
|
|
74
|
+
width: max-content;
|
|
75
|
+
animation: svadmin-marquee-scroll var(--svadmin-marquee-duration, 14s) linear infinite;
|
|
76
|
+
will-change: transform;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.svadmin-marquee[data-direction='right'] .svadmin-marquee__track { animation-direction: reverse; }
|
|
80
|
+
.svadmin-marquee[data-vertical='true'] .svadmin-marquee__track { flex-direction: column; }
|
|
81
|
+
.svadmin-marquee[data-pause='true']:hover .svadmin-marquee__track,
|
|
82
|
+
.svadmin-marquee[data-pause='true']:focus-within .svadmin-marquee__track { animation-play-state: paused; }
|
|
83
|
+
|
|
84
|
+
.svadmin-marquee__item {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex: none;
|
|
87
|
+
align-items: center;
|
|
88
|
+
padding-inline-end: 2rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.svadmin-marquee[data-vertical='true'] .svadmin-marquee__item {
|
|
92
|
+
padding-inline-end: 0;
|
|
93
|
+
padding-block-end: 1rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@keyframes svadmin-marquee-scroll {
|
|
97
|
+
from { transform: translate(0, 0); }
|
|
98
|
+
to { transform: translate(calc(-100% / var(--svadmin-marquee-repeat, 2)), 0); }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@media (prefers-reduced-motion: reduce) {
|
|
102
|
+
.svadmin-marquee__track { animation: none; }
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Plain text shortcut; ignored when `children` is provided. */
|
|
4
|
+
text?: string;
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
/** Seconds for one full loop. */
|
|
7
|
+
duration?: number;
|
|
8
|
+
direction?: 'left' | 'right';
|
|
9
|
+
vertical?: boolean;
|
|
10
|
+
/** Copies of the content used to build a seamless loop. */
|
|
11
|
+
repeat?: number;
|
|
12
|
+
pauseOnHover?: boolean;
|
|
13
|
+
/** Fades the leading and trailing edges. */
|
|
14
|
+
fade?: boolean;
|
|
15
|
+
ariaLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const Marquee: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type Marquee = ReturnType<typeof Marquee>;
|
|
20
|
+
export default Marquee;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
src: string;
|
|
6
|
+
media?: 'video' | 'audio';
|
|
7
|
+
poster?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
controls?: boolean;
|
|
11
|
+
autoplay?: boolean;
|
|
12
|
+
loop?: boolean;
|
|
13
|
+
muted?: boolean;
|
|
14
|
+
playsinline?: boolean;
|
|
15
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
16
|
+
/** CSS aspect ratio for video, e.g. `16 / 9`. */
|
|
17
|
+
aspectRatio?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
ontimeupdate?: (event: Event) => void;
|
|
20
|
+
onended?: (event: Event) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
src,
|
|
25
|
+
media = 'video',
|
|
26
|
+
poster,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
controls = true,
|
|
30
|
+
autoplay = false,
|
|
31
|
+
loop = false,
|
|
32
|
+
muted = false,
|
|
33
|
+
playsinline = true,
|
|
34
|
+
preload = 'metadata',
|
|
35
|
+
aspectRatio = '16 / 9',
|
|
36
|
+
class: className = '',
|
|
37
|
+
ontimeupdate,
|
|
38
|
+
onended,
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<figure class={cn('svadmin-media-player', className)} data-slot="media-player" data-media={media}>
|
|
43
|
+
{#if media === 'video'}
|
|
44
|
+
<div class="svadmin-media-player__stage" style={`aspect-ratio: ${aspectRatio};`}>
|
|
45
|
+
<video
|
|
46
|
+
class="svadmin-media-player__video"
|
|
47
|
+
{src}
|
|
48
|
+
{poster}
|
|
49
|
+
{controls}
|
|
50
|
+
autoplay={autoplay}
|
|
51
|
+
{loop}
|
|
52
|
+
{muted}
|
|
53
|
+
{playsinline}
|
|
54
|
+
{preload}
|
|
55
|
+
aria-label={title}
|
|
56
|
+
ontimeupdate={ontimeupdate}
|
|
57
|
+
onended={onended}
|
|
58
|
+
></video>
|
|
59
|
+
</div>
|
|
60
|
+
{:else}
|
|
61
|
+
<div class="svadmin-media-player__audio-stage">
|
|
62
|
+
<audio
|
|
63
|
+
class="svadmin-media-player__audio"
|
|
64
|
+
{src}
|
|
65
|
+
{controls}
|
|
66
|
+
autoplay={autoplay}
|
|
67
|
+
{loop}
|
|
68
|
+
{muted}
|
|
69
|
+
{preload}
|
|
70
|
+
aria-label={title}
|
|
71
|
+
ontimeupdate={ontimeupdate}
|
|
72
|
+
onended={onended}
|
|
73
|
+
></audio>
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
{#if title || description}
|
|
78
|
+
<figcaption class="svadmin-media-player__caption">
|
|
79
|
+
{#if title}<span class="svadmin-media-player__title">{title}</span>{/if}
|
|
80
|
+
{#if description}<span class="svadmin-media-player__description">{description}</span>{/if}
|
|
81
|
+
</figcaption>
|
|
82
|
+
{/if}
|
|
83
|
+
</figure>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.svadmin-media-player {
|
|
87
|
+
display: grid;
|
|
88
|
+
gap: 0.5rem;
|
|
89
|
+
margin: 0;
|
|
90
|
+
border: 1px solid var(--border, currentColor);
|
|
91
|
+
border-radius: min(var(--radius, 0.5rem), 0.75rem);
|
|
92
|
+
padding: 0.5rem;
|
|
93
|
+
background: var(--card, transparent);
|
|
94
|
+
color: var(--card-foreground, var(--foreground, currentColor));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.svadmin-media-player__stage { overflow: hidden; border-radius: min(var(--radius, 0.5rem), 0.5rem); background: var(--muted, transparent); }
|
|
98
|
+
.svadmin-media-player__video { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
99
|
+
.svadmin-media-player__audio-stage { padding: 0.25rem; }
|
|
100
|
+
.svadmin-media-player__audio { width: 100%; }
|
|
101
|
+
.svadmin-media-player__caption { display: grid; gap: 0.125rem; padding-inline: 0.25rem; }
|
|
102
|
+
.svadmin-media-player__title { font-size: 0.875rem; font-weight: 600; }
|
|
103
|
+
.svadmin-media-player__description { color: var(--muted-foreground, currentColor); font-size: 0.8125rem; }
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
src: string;
|
|
3
|
+
media?: 'video' | 'audio';
|
|
4
|
+
poster?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
controls?: boolean;
|
|
8
|
+
autoplay?: boolean;
|
|
9
|
+
loop?: boolean;
|
|
10
|
+
muted?: boolean;
|
|
11
|
+
playsinline?: boolean;
|
|
12
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
13
|
+
/** CSS aspect ratio for video, e.g. `16 / 9`. */
|
|
14
|
+
aspectRatio?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
ontimeupdate?: (event: Event) => void;
|
|
17
|
+
onended?: (event: Event) => void;
|
|
18
|
+
}
|
|
19
|
+
declare const MediaPlayer: import("svelte").Component<Props, {}, "">;
|
|
20
|
+
type MediaPlayer = ReturnType<typeof MediaPlayer>;
|
|
21
|
+
export default MediaPlayer;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
import {
|
|
4
|
+
findDialCodeByCountry,
|
|
5
|
+
PHONE_DIAL_CODES,
|
|
6
|
+
toE164,
|
|
7
|
+
type PhoneDialCode,
|
|
8
|
+
} from './phone-dial-codes.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** ISO 3166-1 alpha-2 country code. */
|
|
12
|
+
country?: string;
|
|
13
|
+
/** National number without the dialling prefix. */
|
|
14
|
+
nationalNumber?: string;
|
|
15
|
+
countries?: readonly PhoneDialCode[];
|
|
16
|
+
id?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
readonly?: boolean;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
invalid?: boolean;
|
|
23
|
+
describedby?: string;
|
|
24
|
+
ariaLabel?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
onchange?: (payload: { country: string; nationalNumber: string; e164: string }) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
country = $bindable('CN'),
|
|
31
|
+
nationalNumber = $bindable(''),
|
|
32
|
+
countries = PHONE_DIAL_CODES,
|
|
33
|
+
id,
|
|
34
|
+
name,
|
|
35
|
+
placeholder = 'Phone number',
|
|
36
|
+
disabled = false,
|
|
37
|
+
readonly = false,
|
|
38
|
+
required = false,
|
|
39
|
+
invalid = false,
|
|
40
|
+
describedby,
|
|
41
|
+
ariaLabel = 'Phone number',
|
|
42
|
+
class: className = '',
|
|
43
|
+
onchange,
|
|
44
|
+
}: Props = $props();
|
|
45
|
+
|
|
46
|
+
const entry = $derived(findDialCodeByCountry(country, countries));
|
|
47
|
+
const dial = $derived(entry?.dial ?? '');
|
|
48
|
+
|
|
49
|
+
function emit(): void {
|
|
50
|
+
onchange?.({
|
|
51
|
+
country,
|
|
52
|
+
nationalNumber,
|
|
53
|
+
e164: toE164(country, nationalNumber, countries),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div
|
|
59
|
+
class={cn('svadmin-phone-input', className)}
|
|
60
|
+
data-slot="phone-input"
|
|
61
|
+
data-invalid={invalid ? 'true' : undefined}
|
|
62
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
63
|
+
>
|
|
64
|
+
<select
|
|
65
|
+
class="svadmin-phone-input__country"
|
|
66
|
+
bind:value={country}
|
|
67
|
+
{disabled}
|
|
68
|
+
aria-label={`${ariaLabel}: country code`}
|
|
69
|
+
onchange={emit}
|
|
70
|
+
>
|
|
71
|
+
{#each countries as option (option.country)}
|
|
72
|
+
<option value={option.country}>{option.name} (+{option.dial})</option>
|
|
73
|
+
{/each}
|
|
74
|
+
</select>
|
|
75
|
+
|
|
76
|
+
<div class="svadmin-phone-input__control">
|
|
77
|
+
{#if dial}<span class="svadmin-phone-input__dial" aria-hidden="true">+{dial}</span>{/if}
|
|
78
|
+
<input
|
|
79
|
+
class="svadmin-phone-input__field"
|
|
80
|
+
type="tel"
|
|
81
|
+
inputmode="tel"
|
|
82
|
+
autocomplete="tel-national"
|
|
83
|
+
{id}
|
|
84
|
+
{name}
|
|
85
|
+
bind:value={nationalNumber}
|
|
86
|
+
{placeholder}
|
|
87
|
+
{disabled}
|
|
88
|
+
{readonly}
|
|
89
|
+
{required}
|
|
90
|
+
aria-label={ariaLabel}
|
|
91
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
92
|
+
aria-describedby={describedby}
|
|
93
|
+
oninput={emit}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
.svadmin-phone-input {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: stretch;
|
|
102
|
+
border: 1px solid var(--border, currentColor);
|
|
103
|
+
border-radius: min(var(--radius, 0.5rem), 0.5rem);
|
|
104
|
+
background: var(--background, transparent);
|
|
105
|
+
color: var(--foreground, currentColor);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.svadmin-phone-input:focus-within { outline: 2px solid var(--ring, currentColor); outline-offset: 1px; }
|
|
109
|
+
.svadmin-phone-input[data-invalid='true'] { border-color: var(--destructive, currentColor); }
|
|
110
|
+
.svadmin-phone-input[data-disabled='true'] { cursor: not-allowed; opacity: 0.6; }
|
|
111
|
+
|
|
112
|
+
.svadmin-phone-input__country {
|
|
113
|
+
flex: none;
|
|
114
|
+
max-width: 11rem;
|
|
115
|
+
border: 0;
|
|
116
|
+
border-inline-end: 1px solid var(--border, currentColor);
|
|
117
|
+
border-start-start-radius: inherit;
|
|
118
|
+
border-end-start-radius: inherit;
|
|
119
|
+
padding: 0.5rem 0.625rem;
|
|
120
|
+
background: var(--muted, transparent);
|
|
121
|
+
color: inherit;
|
|
122
|
+
font: inherit;
|
|
123
|
+
font-size: 0.8125rem;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.svadmin-phone-input__country:focus-visible { outline: none; }
|
|
128
|
+
|
|
129
|
+
.svadmin-phone-input__control {
|
|
130
|
+
display: flex;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
flex: 1;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 0.375rem;
|
|
135
|
+
padding-inline: 0.625rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.svadmin-phone-input__dial {
|
|
139
|
+
flex: none;
|
|
140
|
+
color: var(--muted-foreground, currentColor);
|
|
141
|
+
font-size: 0.8125rem;
|
|
142
|
+
font-variant-numeric: tabular-nums;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.svadmin-phone-input__field {
|
|
146
|
+
width: 100%;
|
|
147
|
+
min-width: 0;
|
|
148
|
+
border: 0;
|
|
149
|
+
padding-block: 0.5rem;
|
|
150
|
+
background: transparent;
|
|
151
|
+
color: inherit;
|
|
152
|
+
font: inherit;
|
|
153
|
+
font-size: 0.875rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.svadmin-phone-input__field:focus { outline: none; }
|
|
157
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type PhoneDialCode } from './phone-dial-codes.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** ISO 3166-1 alpha-2 country code. */
|
|
4
|
+
country?: string;
|
|
5
|
+
/** National number without the dialling prefix. */
|
|
6
|
+
nationalNumber?: string;
|
|
7
|
+
countries?: readonly PhoneDialCode[];
|
|
8
|
+
id?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
readonly?: boolean;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
invalid?: boolean;
|
|
15
|
+
describedby?: string;
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
onchange?: (payload: {
|
|
19
|
+
country: string;
|
|
20
|
+
nationalNumber: string;
|
|
21
|
+
e164: string;
|
|
22
|
+
}) => void;
|
|
23
|
+
}
|
|
24
|
+
declare const PhoneInput: import("svelte").Component<Props, {}, "country" | "nationalNumber">;
|
|
25
|
+
type PhoneInput = ReturnType<typeof PhoneInput>;
|
|
26
|
+
export default PhoneInput;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import QRCodeLib from 'qrcode';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders a QR code from the Svelte ecosystem `qrcode` package as a crisp
|
|
7
|
+
* SVG. No canvas is required, so it is safe under SSR and test DOMs.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Content to encode. Empty content renders a neutral placeholder. */
|
|
11
|
+
value: string;
|
|
12
|
+
/** Rendered size in pixels. */
|
|
13
|
+
size?: number;
|
|
14
|
+
/** Quiet-zone width in modules. Keep at least 4 for reliable scanning. */
|
|
15
|
+
margin?: number;
|
|
16
|
+
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
|
|
17
|
+
/** Module color; defaults to the semantic foreground token. */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Background color; defaults to the semantic background token. */
|
|
20
|
+
background?: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
value,
|
|
27
|
+
size = 160,
|
|
28
|
+
margin = 4,
|
|
29
|
+
errorCorrectionLevel = 'M',
|
|
30
|
+
color = 'var(--foreground, black)',
|
|
31
|
+
background = 'var(--background, white)',
|
|
32
|
+
ariaLabel = 'QR code',
|
|
33
|
+
class: className = '',
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const matrix = $derived.by(() => {
|
|
37
|
+
if (!value) return null;
|
|
38
|
+
try {
|
|
39
|
+
const qr = QRCodeLib.create(value, { errorCorrectionLevel });
|
|
40
|
+
const moduleCount = qr.modules.size;
|
|
41
|
+
const data = qr.modules.data;
|
|
42
|
+
const total = moduleCount + margin * 2;
|
|
43
|
+
let path = '';
|
|
44
|
+
for (let y = 0; y < moduleCount; y += 1) {
|
|
45
|
+
for (let x = 0; x < moduleCount; x += 1) {
|
|
46
|
+
if (data[y * moduleCount + x]) path += `M${x + margin} ${y + margin}h1v1h-1z`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { total, path };
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<figure class={cn('svadmin-qr-code', className)} data-slot="qr-code">
|
|
57
|
+
{#if matrix}
|
|
58
|
+
<svg
|
|
59
|
+
class="svadmin-qr-code__svg"
|
|
60
|
+
width={size}
|
|
61
|
+
height={size}
|
|
62
|
+
viewBox={`0 0 ${matrix.total} ${matrix.total}`}
|
|
63
|
+
role="img"
|
|
64
|
+
aria-label={ariaLabel}
|
|
65
|
+
shape-rendering="crispEdges"
|
|
66
|
+
>
|
|
67
|
+
<rect width={matrix.total} height={matrix.total} fill={background} />
|
|
68
|
+
<path d={matrix.path} fill={color} />
|
|
69
|
+
</svg>
|
|
70
|
+
{:else}
|
|
71
|
+
<p class="svadmin-qr-code__empty" role="status">No QR content.</p>
|
|
72
|
+
{/if}
|
|
73
|
+
</figure>
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.svadmin-qr-code {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
margin: 0;
|
|
79
|
+
border: 1px solid var(--border, currentColor);
|
|
80
|
+
border-radius: min(var(--radius, 0.5rem), 0.75rem);
|
|
81
|
+
padding: 0.5rem;
|
|
82
|
+
background: var(--card, var(--background, transparent));
|
|
83
|
+
color: var(--card-foreground, var(--foreground, currentColor));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.svadmin-qr-code__svg { display: block; border-radius: calc(min(var(--radius, 0.5rem), 0.75rem) - 0.25rem); }
|
|
87
|
+
.svadmin-qr-code__empty { margin: 0; padding: 1rem; color: var(--muted-foreground, currentColor); font-size: 0.8125rem; }
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a QR code from the Svelte ecosystem `qrcode` package as a crisp
|
|
3
|
+
* SVG. No canvas is required, so it is safe under SSR and test DOMs.
|
|
4
|
+
*/
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Content to encode. Empty content renders a neutral placeholder. */
|
|
7
|
+
value: string;
|
|
8
|
+
/** Rendered size in pixels. */
|
|
9
|
+
size?: number;
|
|
10
|
+
/** Quiet-zone width in modules. Keep at least 4 for reliable scanning. */
|
|
11
|
+
margin?: number;
|
|
12
|
+
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
|
|
13
|
+
/** Module color; defaults to the semantic foreground token. */
|
|
14
|
+
color?: string;
|
|
15
|
+
/** Background color; defaults to the semantic background token. */
|
|
16
|
+
background?: string;
|
|
17
|
+
ariaLabel?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const QRCode: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type QRCode = ReturnType<typeof QRCode>;
|
|
22
|
+
export default QRCode;
|