@svadmin/ui 0.75.0 → 0.76.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 +14 -0
- package/dist/app.theme.css +14 -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/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/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/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/package.json +64 -4
|
@@ -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;
|