@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mkatogui/uds-svelte
2
2
 
3
- Svelte 5 components for the Universal Design System. 32 accessible, themeable components built with Svelte 5 runes, BEM naming, and full WCAG 2.1 AA compliance.
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.1 AA guidelines:
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.2.1",
4
- "description": "Svelte 5 components for Universal Design System — 31 accessible, themeable components",
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';