@mkatogui/uds-svelte 0.2.1 → 0.5.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/README.md +2 -2
- package/package.json +2 -2
- package/src/components/Box.svelte +26 -0
- package/src/components/Carousel.svelte +120 -0
- package/src/components/ChipInput.svelte +93 -0
- package/src/components/ColorPicker.svelte +51 -0
- package/src/components/Container.svelte +16 -0
- package/src/components/Divider.svelte +28 -0
- package/src/components/Form.svelte +16 -0
- package/src/components/Grid.svelte +19 -0
- package/src/components/Link.svelte +27 -0
- package/src/components/NumberInput.svelte +83 -0
- package/src/components/OTPInput.svelte +70 -0
- package/src/components/Popover.svelte +80 -0
- package/src/components/Rating.svelte +38 -0
- package/src/components/SegmentedControl.svelte +91 -0
- package/src/components/Slider.svelte +46 -0
- package/src/components/Stack.svelte +38 -0
- package/src/components/Stepper.svelte +73 -0
- package/src/components/TimePicker.svelte +31 -0
- package/src/components/Toolbar.svelte +31 -0
- package/src/components/TreeView.svelte +90 -0
- package/src/components/Typography.svelte +31 -0
- package/src/index.js +21 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mkatogui/uds-svelte
|
|
2
2
|
|
|
3
|
-
Svelte 5 components for the Universal Design System.
|
|
3
|
+
Svelte 5 components for the Universal Design System. 43 accessible, themeable components built with Svelte 5 runes, BEM naming, and full WCAG 2.2 AA compliance.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -75,7 +75,7 @@ Available palettes: `minimal-saas`, `ai-futuristic`, `gradient-startup`, `corpor
|
|
|
75
75
|
|
|
76
76
|
## Accessibility
|
|
77
77
|
|
|
78
|
-
All components follow WCAG 2.
|
|
78
|
+
All components follow WCAG 2.2 AA guidelines:
|
|
79
79
|
|
|
80
80
|
- Proper ARIA roles and attributes (dialog, alert, switch, tablist, etc.)
|
|
81
81
|
- Keyboard navigation support (arrow keys, Enter, Space, Escape)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mkatogui/uds-svelte",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Svelte 5 components for Universal Design System —
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Svelte 5 components for Universal Design System — 43 accessible, themeable components",
|
|
5
5
|
"svelte": "./src/index.js",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
padding?: string | number;
|
|
4
|
+
margin?: string | number;
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: import('svelte').Snippet;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { padding, margin, class: className = '', children, ...rest }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let classes = $derived(['uds-box', className].filter(Boolean).join(' '));
|
|
13
|
+
|
|
14
|
+
let styleObj = $derived.by(() => {
|
|
15
|
+
const s: Record<string, string> = {};
|
|
16
|
+
if (padding != null)
|
|
17
|
+
s.padding = typeof padding === 'number' ? `${padding}px` : `var(--space-${padding}, ${padding})`;
|
|
18
|
+
if (margin != null)
|
|
19
|
+
s.margin = typeof margin === 'number' ? `${margin}px` : `var(--space-${margin}, ${margin})`;
|
|
20
|
+
return s;
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class={classes} style={Object.keys(styleObj).length ? styleObj : undefined} {...rest}>
|
|
25
|
+
{@render children?.()}
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
items: unknown[];
|
|
4
|
+
autoPlay?: boolean;
|
|
5
|
+
interval?: number;
|
|
6
|
+
showDots?: boolean;
|
|
7
|
+
showArrows?: boolean;
|
|
8
|
+
ariaLabel?: string;
|
|
9
|
+
onSlideChange?: (index: number) => void;
|
|
10
|
+
children?: import('svelte').Snippet<[{ item: unknown; index: number }]>;
|
|
11
|
+
class?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
items,
|
|
17
|
+
autoPlay = false,
|
|
18
|
+
interval = 5000,
|
|
19
|
+
showDots = true,
|
|
20
|
+
showArrows = true,
|
|
21
|
+
ariaLabel = 'Content carousel',
|
|
22
|
+
onSlideChange,
|
|
23
|
+
children,
|
|
24
|
+
class: className = '',
|
|
25
|
+
...rest
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let current = $state(0);
|
|
29
|
+
let isPaused = $state(false);
|
|
30
|
+
|
|
31
|
+
function goTo(index: number) {
|
|
32
|
+
const next = Math.max(0, Math.min(index, items.length - 1));
|
|
33
|
+
current = next;
|
|
34
|
+
onSlideChange?.(next);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function next() {
|
|
38
|
+
goTo(current + 1);
|
|
39
|
+
}
|
|
40
|
+
function prev() {
|
|
41
|
+
goTo(current - 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (intervalId) clearInterval(intervalId);
|
|
47
|
+
if (autoPlay && !isPaused && items.length > 1) {
|
|
48
|
+
intervalId = setInterval(() => {
|
|
49
|
+
current = (current + 1) % items.length;
|
|
50
|
+
onSlideChange?.(current);
|
|
51
|
+
}, interval);
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
if (intervalId) clearInterval(intervalId);
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let classes = $derived(['uds-carousel', className].filter(Boolean).join(' '));
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
{#if items.length}
|
|
62
|
+
<section
|
|
63
|
+
class={classes}
|
|
64
|
+
role="region"
|
|
65
|
+
aria-roledescription="carousel"
|
|
66
|
+
aria-label={ariaLabel}
|
|
67
|
+
onfocus={() => (isPaused = true)}
|
|
68
|
+
onblur={() => (isPaused = false)}
|
|
69
|
+
onmouseenter={() => (isPaused = true)}
|
|
70
|
+
onmouseleave={() => (isPaused = false)}
|
|
71
|
+
{...rest}
|
|
72
|
+
>
|
|
73
|
+
<div class="uds-carousel__track" style="transform: translateX(-{current * 100}%)">
|
|
74
|
+
{#each items as item, i}
|
|
75
|
+
<div
|
|
76
|
+
class="uds-carousel__slide"
|
|
77
|
+
role="group"
|
|
78
|
+
aria-roledescription="slide"
|
|
79
|
+
aria-label="Slide {i + 1} of {items.length}"
|
|
80
|
+
>
|
|
81
|
+
{@render children?.({ item, index: i })}
|
|
82
|
+
</div>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
{#if showArrows && items.length > 1}
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="uds-carousel__arrow uds-carousel__arrow--prev"
|
|
89
|
+
aria-label="Previous slide"
|
|
90
|
+
disabled={current === 0}
|
|
91
|
+
onclick={() => prev()}
|
|
92
|
+
>
|
|
93
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg>
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class="uds-carousel__arrow uds-carousel__arrow--next"
|
|
98
|
+
aria-label="Next slide"
|
|
99
|
+
disabled={current === items.length - 1}
|
|
100
|
+
onclick={() => next()}
|
|
101
|
+
>
|
|
102
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 18l6-6-6-6" /></svg>
|
|
103
|
+
</button>
|
|
104
|
+
{/if}
|
|
105
|
+
{#if showDots && items.length > 1}
|
|
106
|
+
<div class="uds-carousel__dots" role="tablist" aria-label="Slide indicators">
|
|
107
|
+
{#each items as _, i}
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
role="tab"
|
|
111
|
+
aria-selected={i === current}
|
|
112
|
+
aria-label="Slide {i + 1}"
|
|
113
|
+
class="uds-carousel__dot {i === current ? 'uds-carousel__dot--active' : ''}"
|
|
114
|
+
onclick={() => goTo(i)}
|
|
115
|
+
/>
|
|
116
|
+
{/each}
|
|
117
|
+
</div>
|
|
118
|
+
{/if}
|
|
119
|
+
</section>
|
|
120
|
+
{/if}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string[];
|
|
4
|
+
defaultValue?: string[];
|
|
5
|
+
onChange?: (chips: string[]) => void;
|
|
6
|
+
onAdd?: (chip: string) => void;
|
|
7
|
+
onRemove?: (index: number) => void;
|
|
8
|
+
maxChips?: number;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
value: controlledValue,
|
|
17
|
+
defaultValue = [],
|
|
18
|
+
onChange,
|
|
19
|
+
onAdd,
|
|
20
|
+
onRemove,
|
|
21
|
+
maxChips,
|
|
22
|
+
placeholder = 'Add...',
|
|
23
|
+
disabled = false,
|
|
24
|
+
class: className = '',
|
|
25
|
+
...rest
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let internalValue = $state<string[]>([...defaultValue]);
|
|
29
|
+
let inputValue = $state('');
|
|
30
|
+
|
|
31
|
+
let chips = $derived(controlledValue ?? internalValue);
|
|
32
|
+
|
|
33
|
+
let classes = $derived(
|
|
34
|
+
['uds-chip-input', disabled && 'uds-chip-input--disabled', className].filter(Boolean).join(' ')
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function updateChips(next: string[]) {
|
|
38
|
+
if (controlledValue === undefined) internalValue = next;
|
|
39
|
+
onChange?.(next);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleAdd() {
|
|
43
|
+
const trimmed = inputValue.trim();
|
|
44
|
+
if (!trimmed || (maxChips != null && chips.length >= maxChips)) return;
|
|
45
|
+
if (chips.includes(trimmed)) return;
|
|
46
|
+
updateChips([...chips, trimmed]);
|
|
47
|
+
onAdd?.(trimmed);
|
|
48
|
+
inputValue = '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleRemove(index: number) {
|
|
52
|
+
updateChips(chips.filter((_, i) => i !== index));
|
|
53
|
+
onRemove?.(index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
57
|
+
if (e.key === 'Enter') {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
handleAdd();
|
|
60
|
+
}
|
|
61
|
+
if (e.key === 'Backspace' && inputValue === '' && chips.length > 0) {
|
|
62
|
+
handleRemove(chips.length - 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<div class={classes} role="listbox" aria-label="Chips" aria-disabled={disabled} {...rest}>
|
|
68
|
+
{#each chips as chip, index}
|
|
69
|
+
<span class="uds-chip-input__chip" role="option">
|
|
70
|
+
<span class="uds-chip-input__chip-label">{chip}</span>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
class="uds-chip-input__chip-remove"
|
|
74
|
+
aria-label="Remove {chip}"
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
onclick={() => handleRemove(index)}
|
|
77
|
+
>
|
|
78
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg>
|
|
79
|
+
</button>
|
|
80
|
+
</span>
|
|
81
|
+
{/each}
|
|
82
|
+
{#if maxChips == null || chips.length < maxChips}
|
|
83
|
+
<input
|
|
84
|
+
type="text"
|
|
85
|
+
class="uds-chip-input__input"
|
|
86
|
+
placeholder={placeholder}
|
|
87
|
+
aria-label="Add chip"
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
bind:value={inputValue}
|
|
90
|
+
onkeydown={handleKeydown}
|
|
91
|
+
/>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
showHexInput?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onChange?: (v: string) => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
value = '#000000',
|
|
14
|
+
label,
|
|
15
|
+
showHexInput = true,
|
|
16
|
+
disabled = false,
|
|
17
|
+
onChange,
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
let id = $state('uds-cp-' + Math.random().toString(36).slice(2, 9));
|
|
22
|
+
let classes = $derived(['uds-color-picker', className].filter(Boolean).join(' '));
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div class={classes}>
|
|
26
|
+
{#if label}
|
|
27
|
+
<label for={id} class="uds-color-picker__label">{label}</label>
|
|
28
|
+
{/if}
|
|
29
|
+
<div class="uds-color-picker__row">
|
|
30
|
+
<input
|
|
31
|
+
id={id}
|
|
32
|
+
type="color"
|
|
33
|
+
value={value}
|
|
34
|
+
{disabled}
|
|
35
|
+
class="uds-color-picker__swatch"
|
|
36
|
+
aria-describedby={showHexInput ? `${id}-hex` : undefined}
|
|
37
|
+
oninput={(e) => onChange?.((e.currentTarget as HTMLInputElement).value)}
|
|
38
|
+
/>
|
|
39
|
+
{#if showHexInput}
|
|
40
|
+
<input
|
|
41
|
+
id="{id}-hex"
|
|
42
|
+
type="text"
|
|
43
|
+
value={value}
|
|
44
|
+
{disabled}
|
|
45
|
+
class="uds-color-picker__hex"
|
|
46
|
+
aria-label="Hex color"
|
|
47
|
+
oninput={(e) => onChange?.((e.currentTarget as HTMLInputElement).value)}
|
|
48
|
+
/>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: 'sm' | 'md' | 'lg' | 'full';
|
|
4
|
+
class?: string;
|
|
5
|
+
children?: import('svelte').Snippet;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { size = 'lg', class: className = '', children, ...rest }: Props = $props();
|
|
10
|
+
|
|
11
|
+
let classes = $derived(['uds-container', `uds-container--${size}`, className].filter(Boolean).join(' '));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div class={classes} {...rest}>
|
|
15
|
+
{@render children?.()}
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
orientation?: 'horizontal' | 'vertical';
|
|
4
|
+
variant?: 'line' | 'dashed';
|
|
5
|
+
class?: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
orientation = 'horizontal',
|
|
11
|
+
variant = 'line',
|
|
12
|
+
class: className = '',
|
|
13
|
+
...rest
|
|
14
|
+
}: Props = $props();
|
|
15
|
+
|
|
16
|
+
let classes = $derived(
|
|
17
|
+
[
|
|
18
|
+
'uds-divider',
|
|
19
|
+
`uds-divider--${orientation}`,
|
|
20
|
+
variant === 'dashed' && 'uds-divider--dashed',
|
|
21
|
+
className,
|
|
22
|
+
]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(' ')
|
|
25
|
+
);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<hr class={classes} role="separator" {...rest} />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
children?: import('svelte').Snippet;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { id, class: className = '', children, ...rest }: Props = $props();
|
|
10
|
+
|
|
11
|
+
let classes = $derived(['uds-form', className].filter(Boolean).join(' '));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<form class={classes} {id} {...rest}>
|
|
15
|
+
{@render children?.()}
|
|
16
|
+
</form>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
columns?: 1 | 2 | 3 | 4 | 12;
|
|
4
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: import('svelte').Snippet;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { columns = 1, gap = 'md', class: className = '', children, ...rest }: Props = $props();
|
|
11
|
+
|
|
12
|
+
let classes = $derived(
|
|
13
|
+
['uds-grid', `uds-grid--cols-${columns}`, `uds-grid--gap-${gap}`, className].filter(Boolean).join(' ')
|
|
14
|
+
);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class={classes} {...rest}>
|
|
18
|
+
{@render children?.()}
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
href: string;
|
|
4
|
+
variant?: 'default' | 'muted' | 'primary';
|
|
5
|
+
external?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
children?: import('svelte').Snippet;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
href,
|
|
13
|
+
variant = 'primary',
|
|
14
|
+
external = false,
|
|
15
|
+
class: className = '',
|
|
16
|
+
children,
|
|
17
|
+
...rest
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let classes = $derived(['uds-link', `uds-link--${variant}`, className].filter(Boolean).join(' '));
|
|
21
|
+
let rel = $derived(external ? 'noopener noreferrer' : undefined);
|
|
22
|
+
let target = $derived(external ? '_blank' : undefined);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<a class={classes} {href} {rel} {target} {...rest}>
|
|
26
|
+
{@render children?.()}
|
|
27
|
+
</a>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: number | string;
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
step?: number;
|
|
7
|
+
showStepper?: boolean;
|
|
8
|
+
size?: 'sm' | 'md' | 'lg';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onChange?: (v: number | string) => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
value = '',
|
|
17
|
+
min,
|
|
18
|
+
max,
|
|
19
|
+
step = 1,
|
|
20
|
+
showStepper = false,
|
|
21
|
+
size = 'md',
|
|
22
|
+
disabled = false,
|
|
23
|
+
onChange,
|
|
24
|
+
class: className = '',
|
|
25
|
+
...rest
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let classes = $derived(
|
|
29
|
+
[
|
|
30
|
+
'uds-number-input',
|
|
31
|
+
`uds-number-input--${size}`,
|
|
32
|
+
showStepper && 'uds-number-input--stepper',
|
|
33
|
+
className,
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(' ')
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function handleStep(delta: number) {
|
|
40
|
+
const next = (Number(value) || 0) + delta;
|
|
41
|
+
const clamped = min != null && next < min ? min : max != null && next > max ? max : next;
|
|
42
|
+
onChange?.(clamped);
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class={classes}>
|
|
47
|
+
{#if showStepper}
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="uds-number-input__stepper uds-number-input__stepper--minus"
|
|
51
|
+
aria-label="Decrease"
|
|
52
|
+
disabled={disabled || (min != null && Number(value) <= min)}
|
|
53
|
+
onclick={() => handleStep(-step)}
|
|
54
|
+
>
|
|
55
|
+
−
|
|
56
|
+
</button>
|
|
57
|
+
{/if}
|
|
58
|
+
<input
|
|
59
|
+
type="number"
|
|
60
|
+
value={value}
|
|
61
|
+
min={min}
|
|
62
|
+
max={max}
|
|
63
|
+
step={step}
|
|
64
|
+
{disabled}
|
|
65
|
+
class="uds-number-input__input"
|
|
66
|
+
aria-valuenow={value !== '' && value !== undefined ? Number(value) : undefined}
|
|
67
|
+
aria-valuemin={min}
|
|
68
|
+
aria-valuemax={max}
|
|
69
|
+
oninput={(e) => onChange?.((e.currentTarget as HTMLInputElement).value)}
|
|
70
|
+
{...rest}
|
|
71
|
+
/>
|
|
72
|
+
{#if showStepper}
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
class="uds-number-input__stepper uds-number-input__stepper--plus"
|
|
76
|
+
aria-label="Increase"
|
|
77
|
+
disabled={disabled || (max != null && Number(value) >= max)}
|
|
78
|
+
onclick={() => handleStep(step)}
|
|
79
|
+
>
|
|
80
|
+
+
|
|
81
|
+
</button>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
length?: 4 | 6;
|
|
4
|
+
value?: string;
|
|
5
|
+
defaultValue?: string;
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
autoFocus?: boolean;
|
|
8
|
+
inputMode?: 'numeric' | 'text';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
length = 4,
|
|
16
|
+
value: controlledValue,
|
|
17
|
+
defaultValue = '',
|
|
18
|
+
onChange,
|
|
19
|
+
autoFocus = false,
|
|
20
|
+
inputMode = 'numeric',
|
|
21
|
+
disabled = false,
|
|
22
|
+
class: className = '',
|
|
23
|
+
...rest
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
let internalValue = $state(defaultValue.slice(0, length));
|
|
27
|
+
let value = $derived((controlledValue ?? internalValue).slice(0, length).padEnd(length, ''));
|
|
28
|
+
|
|
29
|
+
let classes = $derived(
|
|
30
|
+
['uds-otp-input', disabled && 'uds-otp-input--disabled', className].filter(Boolean).join(' ')
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
function setValue(next: string) {
|
|
34
|
+
const s = next.slice(0, length);
|
|
35
|
+
if (controlledValue === undefined) internalValue = s;
|
|
36
|
+
onChange?.(s);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleChange(index: number, digit: string) {
|
|
40
|
+
const char = inputMode === 'numeric' ? digit.replace(/\D/g, '').slice(-1) : digit.slice(-1);
|
|
41
|
+
const arr = value.split('');
|
|
42
|
+
arr[index] = char;
|
|
43
|
+
setValue(arr.join(''));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleKeydown(e: KeyboardEvent, index: number) {
|
|
47
|
+
if (e.key === 'Backspace' && value[index] === '' && index > 0) {
|
|
48
|
+
const inputs = document.querySelectorAll<HTMLInputElement>('.uds-otp-input__digit');
|
|
49
|
+
inputs[index - 1]?.focus();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class={classes} role="group" aria-label="One-time code" {...rest}>
|
|
55
|
+
{#each Array(length) as _, i}
|
|
56
|
+
<input
|
|
57
|
+
type="text"
|
|
58
|
+
inputmode={inputMode}
|
|
59
|
+
maxlength="1"
|
|
60
|
+
autocomplete="one-time-code"
|
|
61
|
+
class="uds-otp-input__digit"
|
|
62
|
+
value={value[i] ?? ''}
|
|
63
|
+
aria-label="Digit {i + 1}"
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
autofocus={autoFocus && i === 0}
|
|
66
|
+
oninput={(e) => handleChange(i, (e.currentTarget as HTMLInputElement).value)}
|
|
67
|
+
onkeydown={(e) => handleKeydown(e, i)}
|
|
68
|
+
/>
|
|
69
|
+
{/each}
|
|
70
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
onOpenChange?: (open: boolean) => void;
|
|
5
|
+
placement?: 'top' | 'bottom' | 'left' | 'right' | 'auto';
|
|
6
|
+
size?: 'sm' | 'md';
|
|
7
|
+
class?: string;
|
|
8
|
+
trigger?: import('svelte').Snippet<[{ open: boolean }]>;
|
|
9
|
+
children?: import('svelte').Snippet;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
open: controlledOpen,
|
|
15
|
+
onOpenChange,
|
|
16
|
+
placement = 'bottom',
|
|
17
|
+
size = 'md',
|
|
18
|
+
class: className = '',
|
|
19
|
+
trigger,
|
|
20
|
+
children,
|
|
21
|
+
...rest
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let internalOpen = $state(false);
|
|
25
|
+
let open = $derived(controlledOpen ?? internalOpen);
|
|
26
|
+
|
|
27
|
+
let contentRef: HTMLDivElement;
|
|
28
|
+
let triggerRef: HTMLDivElement;
|
|
29
|
+
let contentStyle = $state<Record<string, string>>({ position: 'fixed', left: '0', top: '0', zIndex: '9999' });
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (open && contentRef && triggerRef) {
|
|
33
|
+
const tr = triggerRef.getBoundingClientRect();
|
|
34
|
+
const cr = contentRef.getBoundingClientRect();
|
|
35
|
+
let top = tr.bottom + 8;
|
|
36
|
+
let left = tr.left + (tr.width - cr.width) / 2;
|
|
37
|
+
if (placement === 'top') top = tr.top - cr.height - 8;
|
|
38
|
+
left = Math.max(8, Math.min(window.innerWidth - cr.width - 8, left));
|
|
39
|
+
top = Math.max(8, Math.min(window.innerHeight - cr.height - 8, top));
|
|
40
|
+
contentStyle = { position: 'fixed', left: `${left}px`, top: `${top}px`, zIndex: '9999' };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let classes = $derived(
|
|
45
|
+
['uds-popover', `uds-popover--${size}`, `uds-popover--${placement}`, className].filter(Boolean).join(' ')
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function setOpen(v: boolean) {
|
|
49
|
+
if (controlledOpen === undefined) internalOpen = v;
|
|
50
|
+
onOpenChange?.(v);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleEscape(e: KeyboardEvent) {
|
|
54
|
+
if (e.key === 'Escape') setOpen(false);
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<svelte:window onkeydown={handleEscape} />
|
|
59
|
+
|
|
60
|
+
<div class="uds-popover__wrapper" {...rest}>
|
|
61
|
+
<div bind:this={triggerRef} onclick={() => setOpen(!open)}>
|
|
62
|
+
{@render trigger?.({ open })}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{#if open}
|
|
67
|
+
<svelte:portal target="body">
|
|
68
|
+
<div
|
|
69
|
+
bind:this={contentRef}
|
|
70
|
+
class={classes}
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-modal="false"
|
|
73
|
+
style={contentStyle}
|
|
74
|
+
>
|
|
75
|
+
<div class="uds-popover__content">
|
|
76
|
+
{@render children?.()}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</svelte:portal>
|
|
80
|
+
{/if}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
size?: 'sm' | 'md' | 'lg';
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
onChange?: (v: number) => void;
|
|
8
|
+
class?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
value = 0,
|
|
14
|
+
max = 5,
|
|
15
|
+
size = 'md',
|
|
16
|
+
disabled = false,
|
|
17
|
+
onChange,
|
|
18
|
+
class: className = '',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
let classes = $derived(['uds-rating', `uds-rating--${size}`, className].filter(Boolean).join(' '));
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class={classes} role="group" aria-label="Rating {value} of {max}">
|
|
25
|
+
{#each Array(max) as _, i}
|
|
26
|
+
{@const starValue = i + 1}
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
class="uds-rating__star"
|
|
30
|
+
class:uds-rating__star--filled={value >= starValue}
|
|
31
|
+
aria-label="{starValue} star{starValue > 1 ? 's' : ''}"
|
|
32
|
+
{disabled}
|
|
33
|
+
onclick={() => onChange?.(starValue)}
|
|
34
|
+
>
|
|
35
|
+
<span aria-hidden="true">{value >= starValue ? '★' : '☆'}</span>
|
|
36
|
+
</button>
|
|
37
|
+
{/each}
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface SegmentedControlOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: import('svelte').Snippet;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
options: SegmentedControlOption[];
|
|
10
|
+
value?: string;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
onChange?: (value: string) => void;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
iconOnly?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
class?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
options,
|
|
22
|
+
value: controlledValue,
|
|
23
|
+
defaultValue,
|
|
24
|
+
onChange,
|
|
25
|
+
size = 'md',
|
|
26
|
+
iconOnly = false,
|
|
27
|
+
disabled = false,
|
|
28
|
+
class: className = '',
|
|
29
|
+
...rest
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let internalValue = $state(defaultValue ?? options[0]?.value ?? '');
|
|
33
|
+
let value = $derived(controlledValue ?? internalValue);
|
|
34
|
+
|
|
35
|
+
let classes = $derived(
|
|
36
|
+
[
|
|
37
|
+
'uds-segmented-control',
|
|
38
|
+
`uds-segmented-control--${size}`,
|
|
39
|
+
iconOnly && 'uds-segmented-control--icon-only',
|
|
40
|
+
disabled && 'uds-segmented-control--disabled',
|
|
41
|
+
className,
|
|
42
|
+
]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(' ')
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function select(optionValue: string) {
|
|
48
|
+
if (disabled) return;
|
|
49
|
+
if (controlledValue === undefined) internalValue = optionValue;
|
|
50
|
+
onChange?.(optionValue);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleKeydown(e: KeyboardEvent, currentIndex: number) {
|
|
54
|
+
if (disabled) return;
|
|
55
|
+
let nextIndex = currentIndex;
|
|
56
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
nextIndex = Math.max(0, currentIndex - 1);
|
|
59
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
nextIndex = Math.min(options.length - 1, currentIndex + 1);
|
|
62
|
+
} else if (e.key === 'Home') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
nextIndex = 0;
|
|
65
|
+
} else if (e.key === 'End') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
nextIndex = options.length - 1;
|
|
68
|
+
} else return;
|
|
69
|
+
const nextValue = options[nextIndex]?.value;
|
|
70
|
+
if (nextValue != null) select(nextValue);
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<div class={classes} role="radiogroup" aria-label="Options" aria-disabled={disabled} {...rest}>
|
|
75
|
+
{#each options as option, index}
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
role="radio"
|
|
79
|
+
aria-checked={value === option.value}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
class="uds-segmented-control__option {value === option.value ? 'uds-segmented-control__option--selected' : ''}"
|
|
82
|
+
onclick={() => select(option.value)}
|
|
83
|
+
onkeydown={(e) => handleKeydown(e, index)}
|
|
84
|
+
>
|
|
85
|
+
{#if option.icon}
|
|
86
|
+
<span class="uds-segmented-control__icon" aria-hidden="true">{@render option.icon()}</span>
|
|
87
|
+
{/if}
|
|
88
|
+
{#if !iconOnly}<span class="uds-segmented-control__label">{option.label}</span>{/if}
|
|
89
|
+
</button>
|
|
90
|
+
{/each}
|
|
91
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: number;
|
|
4
|
+
min?: number;
|
|
5
|
+
max?: number;
|
|
6
|
+
step?: number;
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onChange?: (v: number) => void;
|
|
10
|
+
class?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
value,
|
|
16
|
+
min = 0,
|
|
17
|
+
max = 100,
|
|
18
|
+
step = 1,
|
|
19
|
+
size = 'md',
|
|
20
|
+
disabled = false,
|
|
21
|
+
onChange,
|
|
22
|
+
class: className = '',
|
|
23
|
+
...rest
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
let val = $derived(value ?? min);
|
|
27
|
+
let classes = $derived(['uds-slider', `uds-slider--${size}`, className].filter(Boolean).join(' '));
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class={classes}>
|
|
31
|
+
<input
|
|
32
|
+
type="range"
|
|
33
|
+
value={val}
|
|
34
|
+
{min}
|
|
35
|
+
{max}
|
|
36
|
+
{step}
|
|
37
|
+
{disabled}
|
|
38
|
+
role="slider"
|
|
39
|
+
aria-valuemin={min}
|
|
40
|
+
aria-valuemax={max}
|
|
41
|
+
aria-valuenow={val}
|
|
42
|
+
class="uds-slider__input"
|
|
43
|
+
oninput={(e) => onChange?.(Number((e.currentTarget as HTMLInputElement).value))}
|
|
44
|
+
{...rest}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
direction?: 'row' | 'column';
|
|
4
|
+
gap?: 'sm' | 'md' | 'lg';
|
|
5
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
6
|
+
wrap?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: import('svelte').Snippet;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
direction = 'column',
|
|
14
|
+
gap = 'md',
|
|
15
|
+
align = 'stretch',
|
|
16
|
+
wrap = false,
|
|
17
|
+
class: className = '',
|
|
18
|
+
children,
|
|
19
|
+
...rest
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
let classes = $derived(
|
|
23
|
+
[
|
|
24
|
+
'uds-stack',
|
|
25
|
+
`uds-stack--${direction}`,
|
|
26
|
+
`uds-stack--gap-${gap}`,
|
|
27
|
+
align !== 'stretch' && `uds-stack--align-${align}`,
|
|
28
|
+
wrap && 'uds-stack--wrap',
|
|
29
|
+
className,
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' ')
|
|
33
|
+
);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class={classes} {...rest}>
|
|
37
|
+
{@render children?.()}
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface StepperStep {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
optional?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
steps: StepperStep[];
|
|
10
|
+
activeStep?: number;
|
|
11
|
+
defaultActiveStep?: number;
|
|
12
|
+
orientation?: 'horizontal' | 'vertical';
|
|
13
|
+
linear?: boolean;
|
|
14
|
+
onStepClick?: (index: number) => void;
|
|
15
|
+
onChange?: (index: number) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
children?: import('svelte').Snippet;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
steps,
|
|
23
|
+
activeStep: controlledStep,
|
|
24
|
+
defaultActiveStep = 0,
|
|
25
|
+
orientation = 'horizontal',
|
|
26
|
+
linear = true,
|
|
27
|
+
onStepClick,
|
|
28
|
+
onChange,
|
|
29
|
+
class: className = '',
|
|
30
|
+
...rest
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
let internalStep = $state(defaultActiveStep);
|
|
34
|
+
let activeStep = $derived(controlledStep ?? internalStep);
|
|
35
|
+
|
|
36
|
+
let classes = $derived(
|
|
37
|
+
['uds-stepper', `uds-stepper--${orientation}`, className].filter(Boolean).join(' ')
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function handleStepClick(index: number) {
|
|
41
|
+
if (linear && index > activeStep) return;
|
|
42
|
+
if (controlledStep === undefined) internalStep = index;
|
|
43
|
+
onChange?.(index);
|
|
44
|
+
onStepClick?.(index);
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<nav class={classes} aria-label="Progress" {...rest}>
|
|
49
|
+
{#each steps as step, index}
|
|
50
|
+
<div
|
|
51
|
+
class="uds-stepper__step uds-stepper__step--{index < activeStep ? 'completed' : index === activeStep ? 'active' : 'pending'}"
|
|
52
|
+
aria-current={index === activeStep ? 'step' : undefined}
|
|
53
|
+
aria-disabled={index > activeStep && linear ? 'true' : undefined}
|
|
54
|
+
role="button"
|
|
55
|
+
tabindex={!linear || index <= activeStep ? 0 : -1}
|
|
56
|
+
onclick={() => (!linear || index <= activeStep) && handleStepClick(index)}
|
|
57
|
+
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && (!linear || index <= activeStep) && (e.preventDefault(), handleStepClick(index))}
|
|
58
|
+
>
|
|
59
|
+
<span class="uds-stepper__step-indicator">
|
|
60
|
+
{#if index < activeStep}
|
|
61
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M20 6L9 17l-5-5" /></svg>
|
|
62
|
+
{:else}
|
|
63
|
+
{index + 1}
|
|
64
|
+
{/if}
|
|
65
|
+
</span>
|
|
66
|
+
<span class="uds-stepper__step-label">{step.label}</span>
|
|
67
|
+
{#if step.optional}<span class="uds-stepper__step-optional">(optional)</span>{/if}
|
|
68
|
+
</div>
|
|
69
|
+
{#if index < steps.length - 1}
|
|
70
|
+
<span class="uds-stepper__connector uds-stepper__connector--{index < activeStep ? 'completed' : ''}" aria-hidden="true"></span>
|
|
71
|
+
{/if}
|
|
72
|
+
{/each}
|
|
73
|
+
</nav>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
size?: 'md' | 'lg';
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
onChange?: (v: string) => void;
|
|
7
|
+
class?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
value = '',
|
|
13
|
+
size = 'md',
|
|
14
|
+
disabled = false,
|
|
15
|
+
onChange,
|
|
16
|
+
class: className = '',
|
|
17
|
+
...rest
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let classes = $derived(['uds-time-picker', `uds-time-picker--${size}`, className].filter(Boolean).join(' '));
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<input
|
|
24
|
+
type="time"
|
|
25
|
+
value={value}
|
|
26
|
+
class={classes}
|
|
27
|
+
{disabled}
|
|
28
|
+
aria-label="Time"
|
|
29
|
+
oninput={(e) => onChange?.((e.currentTarget as HTMLInputElement).value)}
|
|
30
|
+
{...rest}
|
|
31
|
+
/>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
'aria-label': string;
|
|
4
|
+
orientation?: 'horizontal' | 'vertical';
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: import('svelte').Snippet;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
'aria-label': ariaLabel,
|
|
12
|
+
orientation = 'horizontal',
|
|
13
|
+
class: className = '',
|
|
14
|
+
children,
|
|
15
|
+
...rest
|
|
16
|
+
}: Props = $props();
|
|
17
|
+
|
|
18
|
+
let classes = $derived(
|
|
19
|
+
[
|
|
20
|
+
'uds-toolbar',
|
|
21
|
+
orientation === 'vertical' && 'uds-toolbar--vertical',
|
|
22
|
+
className,
|
|
23
|
+
]
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join(' ')
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class={classes} role="toolbar" aria-label={ariaLabel} {...rest}>
|
|
30
|
+
{@render children?.()}
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
children?: TreeNode[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
nodes: TreeNode[];
|
|
10
|
+
selectedIds?: string | string[];
|
|
11
|
+
onSelect?: (ids: string[]) => void;
|
|
12
|
+
onExpand?: (id: string, expanded: boolean) => void;
|
|
13
|
+
selectionMode?: 'single' | 'multi' | 'none';
|
|
14
|
+
defaultExpandedIds?: string[];
|
|
15
|
+
ariaLabel?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
nodes,
|
|
22
|
+
selectedIds,
|
|
23
|
+
onSelect,
|
|
24
|
+
onExpand,
|
|
25
|
+
selectionMode = 'single',
|
|
26
|
+
defaultExpandedIds = [],
|
|
27
|
+
ariaLabel = 'Tree',
|
|
28
|
+
class: className = '',
|
|
29
|
+
...rest
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
let expanded = $state<Set<string>>(new Set(defaultExpandedIds));
|
|
33
|
+
|
|
34
|
+
let selectedArray = $derived(
|
|
35
|
+
Array.isArray(selectedIds) ? selectedIds : selectedIds != null ? [selectedIds] : []
|
|
36
|
+
);
|
|
37
|
+
let selectedSet = $derived(new Set(selectedArray));
|
|
38
|
+
|
|
39
|
+
let classes = $derived(['uds-tree', className].filter(Boolean).join(' '));
|
|
40
|
+
|
|
41
|
+
function toggle(id: string, isExp: boolean) {
|
|
42
|
+
const next = new Set(expanded);
|
|
43
|
+
if (isExp) next.add(id);
|
|
44
|
+
else next.delete(id);
|
|
45
|
+
expanded = next;
|
|
46
|
+
onExpand?.(id, isExp);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleSelect(ids: string[]) {
|
|
50
|
+
onSelect?.(ids);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class={classes} role="tree" aria-label={ariaLabel} aria-multiselectable={selectionMode === 'multi'} {...rest}>
|
|
55
|
+
{#each nodes as node}
|
|
56
|
+
<div
|
|
57
|
+
class="uds-tree__item {node.children?.length ? 'uds-tree__item--branch' : ''} {selectedSet.has(node.id) ? 'uds-tree__item--selected' : ''}"
|
|
58
|
+
role="treeitem"
|
|
59
|
+
aria-expanded={node.children?.length ? expanded.has(node.id) : undefined}
|
|
60
|
+
aria-level="1"
|
|
61
|
+
aria-selected={selectionMode !== 'none' ? selectedSet.has(node.id) : undefined}
|
|
62
|
+
tabindex="0"
|
|
63
|
+
onclick={() => {
|
|
64
|
+
if (node.children?.length) toggle(node.id, !expanded.has(node.id));
|
|
65
|
+
if (selectionMode !== 'none') {
|
|
66
|
+
if (selectionMode === 'single') handleSelect([node.id]);
|
|
67
|
+
else handleSelect(selectedSet.has(node.id) ? selectedArray.filter((x) => x !== node.id) : [...selectedArray, node.id]);
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<span class="uds-tree__item-label">{node.label}</span>
|
|
72
|
+
{#if node.children?.length && expanded.has(node.id)}
|
|
73
|
+
<div role="group" class="uds-tree__group">
|
|
74
|
+
{#each node.children as child}
|
|
75
|
+
<div
|
|
76
|
+
class="uds-tree__item {selectedSet.has(child.id) ? 'uds-tree__item--selected' : ''}"
|
|
77
|
+
role="treeitem"
|
|
78
|
+
aria-level="2"
|
|
79
|
+
aria-selected={selectionMode !== 'none' ? selectedSet.has(child.id) : undefined}
|
|
80
|
+
tabindex="0"
|
|
81
|
+
onclick={() => selectionMode !== 'none' && handleSelect(selectionMode === 'single' ? [child.id] : [])}
|
|
82
|
+
>
|
|
83
|
+
<span class="uds-tree__item-label">{child.label}</span>
|
|
84
|
+
</div>
|
|
85
|
+
{/each}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Variant = 'h1' | 'h2' | 'h3' | 'body' | 'caption' | 'code';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: Variant;
|
|
6
|
+
as?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: import('svelte').Snippet;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { variant = 'body', as, class: className = '', children, ...rest }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const tagMap: Record<Variant, string> = {
|
|
15
|
+
h1: 'h1',
|
|
16
|
+
h2: 'h2',
|
|
17
|
+
h3: 'h3',
|
|
18
|
+
body: 'p',
|
|
19
|
+
caption: 'span',
|
|
20
|
+
code: 'code',
|
|
21
|
+
};
|
|
22
|
+
let tag = $derived(as ?? tagMap[variant]);
|
|
23
|
+
|
|
24
|
+
let classes = $derived(
|
|
25
|
+
['uds-typography', `uds-typography--${variant}`, className].filter(Boolean).join(' ')
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<svelte:element this={tag} class={classes} {...rest}>
|
|
30
|
+
{@render children?.()}
|
|
31
|
+
</svelte:element>
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,20 @@ export { default as Radio } from './components/Radio.svelte';
|
|
|
15
15
|
export { default as ToggleSwitch } from './components/ToggleSwitch.svelte';
|
|
16
16
|
export { default as Alert } from './components/Alert.svelte';
|
|
17
17
|
export { default as Badge } from './components/Badge.svelte';
|
|
18
|
+
export { default as Box } from './components/Box.svelte';
|
|
19
|
+
export { default as Container } from './components/Container.svelte';
|
|
20
|
+
export { default as Divider } from './components/Divider.svelte';
|
|
21
|
+
export { default as Grid } from './components/Grid.svelte';
|
|
22
|
+
export { default as Link } from './components/Link.svelte';
|
|
23
|
+
export { default as Stack } from './components/Stack.svelte';
|
|
24
|
+
export { default as Typography } from './components/Typography.svelte';
|
|
18
25
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
26
|
+
export { default as NumberInput } from './components/NumberInput.svelte';
|
|
27
|
+
export { default as Slider } from './components/Slider.svelte';
|
|
28
|
+
export { default as Form } from './components/Form.svelte';
|
|
29
|
+
export { default as TimePicker } from './components/TimePicker.svelte';
|
|
30
|
+
export { default as Rating } from './components/Rating.svelte';
|
|
31
|
+
export { default as ColorPicker } from './components/ColorPicker.svelte';
|
|
19
32
|
export { default as Accordion } from './components/Accordion.svelte';
|
|
20
33
|
export { default as Breadcrumb } from './components/Breadcrumb.svelte';
|
|
21
34
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
@@ -30,3 +43,11 @@ export { default as CommandPalette } from './components/CommandPalette.svelte';
|
|
|
30
43
|
export { default as ProgressIndicator } from './components/ProgressIndicator.svelte';
|
|
31
44
|
export { default as SideNavigation } from './components/SideNavigation.svelte';
|
|
32
45
|
export { default as FileUpload } from './components/FileUpload.svelte';
|
|
46
|
+
export { default as Toolbar } from './components/Toolbar.svelte';
|
|
47
|
+
export { default as Stepper } from './components/Stepper.svelte';
|
|
48
|
+
export { default as SegmentedControl } from './components/SegmentedControl.svelte';
|
|
49
|
+
export { default as OTPInput } from './components/OTPInput.svelte';
|
|
50
|
+
export { default as ChipInput } from './components/ChipInput.svelte';
|
|
51
|
+
export { default as Popover } from './components/Popover.svelte';
|
|
52
|
+
export { default as Carousel } from './components/Carousel.svelte';
|
|
53
|
+
export { default as TreeView } from './components/TreeView.svelte';
|