@mkatogui/uds-svelte 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +100 -0
  2. package/package.json +27 -0
  3. package/src/components/Accordion.svelte +99 -0
  4. package/src/components/Alert.svelte +67 -0
  5. package/src/components/Avatar.svelte +62 -0
  6. package/src/components/Badge.svelte +50 -0
  7. package/src/components/Breadcrumb.svelte +60 -0
  8. package/src/components/Button.svelte +53 -0
  9. package/src/components/Checkbox.svelte +62 -0
  10. package/src/components/CodeBlock.svelte +90 -0
  11. package/src/components/CommandPalette.svelte +133 -0
  12. package/src/components/DataTable.svelte +140 -0
  13. package/src/components/DatePicker.svelte +73 -0
  14. package/src/components/DropdownMenu.svelte +113 -0
  15. package/src/components/FeatureCard.svelte +63 -0
  16. package/src/components/FileUpload.svelte +120 -0
  17. package/src/components/Footer.svelte +72 -0
  18. package/src/components/FormInput.svelte +102 -0
  19. package/src/components/HeroSection.svelte +65 -0
  20. package/src/components/Modal.svelte +67 -0
  21. package/src/components/NavigationBar.svelte +56 -0
  22. package/src/components/Pagination.svelte +115 -0
  23. package/src/components/PricingTable.svelte +89 -0
  24. package/src/components/ProgressIndicator.svelte +86 -0
  25. package/src/components/Radio.svelte +64 -0
  26. package/src/components/Select.svelte +85 -0
  27. package/src/components/SideNavigation.svelte +130 -0
  28. package/src/components/Skeleton.svelte +53 -0
  29. package/src/components/SocialProofBar.svelte +90 -0
  30. package/src/components/Tabs.svelte +100 -0
  31. package/src/components/TestimonialCard.svelte +71 -0
  32. package/src/components/Toast.svelte +83 -0
  33. package/src/components/ToggleSwitch.svelte +61 -0
  34. package/src/components/Tooltip.svelte +67 -0
  35. package/src/index.d.ts +553 -0
  36. package/src/index.js +32 -0
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @mkatogui/uds-svelte
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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mkatogui/uds-svelte
9
+ ```
10
+
11
+ Requires Svelte 5 or later as a peer dependency.
12
+
13
+ ## Usage
14
+
15
+ ```svelte
16
+ <script>
17
+ import { Button, Alert, Modal } from '@mkatogui/uds-svelte';
18
+ </script>
19
+
20
+ <Button variant="primary" size="md" onclick={() => console.log('clicked')}>
21
+ Get Started
22
+ </Button>
23
+
24
+ <Alert variant="success" title="Done" message="Operation completed successfully." dismissible />
25
+ ```
26
+
27
+ ## Theming
28
+
29
+ Components use UDS CSS custom properties (`--color-*`, `--space-*`, `--font-size-*`). Apply a palette with the `data-theme` attribute on a parent element:
30
+
31
+ ```html
32
+ <div data-theme="minimal-saas">
33
+ <!-- All UDS components inside inherit the palette -->
34
+ </div>
35
+ ```
36
+
37
+ Available palettes: `minimal-saas`, `ai-futuristic`, `gradient-startup`, `corporate`, `apple-minimal`, `illustration`, `dashboard`, `bold-lifestyle`, `minimal-corporate`.
38
+
39
+ ## Components
40
+
41
+ | Component | CSS Class | Variants |
42
+ |---|---|---|
43
+ | Button | `uds-btn` | primary, secondary, ghost, gradient, destructive, icon-only |
44
+ | NavigationBar | `uds-navbar` | standard, minimal, dark, transparent |
45
+ | HeroSection | `uds-hero` | centered, product-screenshot, video-bg, gradient-mesh, search-forward, split |
46
+ | FeatureCard | `uds-card` | icon-top, image-top, horizontal, stat-card, dashboard-preview |
47
+ | PricingTable | `uds-pricing` | 2-column, 3-column, 4-column, toggle |
48
+ | SocialProofBar | `uds-social-proof` | logo-strip, stats-counter, testimonial-mini, combined |
49
+ | TestimonialCard | `uds-testimonial` | quote-card, video, metric, carousel |
50
+ | Footer | `uds-footer` | simple, multi-column, newsletter, mega-footer |
51
+ | CodeBlock | `uds-code-block` | syntax-highlighted, terminal, multi-tab |
52
+ | Modal | `uds-modal` | confirmation, task, alert |
53
+ | FormInput | `uds-input` | text, email, password, number, search, textarea |
54
+ | Select | `uds-select` | native, custom |
55
+ | Checkbox | `uds-checkbox` | standard, indeterminate |
56
+ | Radio | `uds-radio` | standard, card |
57
+ | ToggleSwitch | `uds-toggle` | standard, with-label |
58
+ | Alert | `uds-alert` | success, warning, error, info |
59
+ | Badge | `uds-badge` | status, count, tag |
60
+ | Tabs | `uds-tabs` | line, pill, segmented |
61
+ | Accordion | `uds-accordion` | single, multi, flush |
62
+ | Breadcrumb | `uds-breadcrumb` | standard, truncated |
63
+ | Tooltip | `uds-tooltip` | simple, rich |
64
+ | DropdownMenu | `uds-dropdown` | action, context, nav-sub |
65
+ | Avatar | `uds-avatar` | image, initials, icon, group |
66
+ | Skeleton | `uds-skeleton` | text, card, avatar, table |
67
+ | Toast | `uds-toast` | success, error, warning, info, neutral |
68
+ | Pagination | `uds-pagination` | numbered, simple, load-more, infinite-scroll |
69
+ | DataTable | `uds-data-table` | basic, sortable, selectable, expandable |
70
+ | DatePicker | `uds-date-picker` | single, range, with-time |
71
+ | CommandPalette | `uds-command-palette` | standard |
72
+ | ProgressIndicator | `uds-progress` | bar, circular, stepper |
73
+ | SideNavigation | `uds-side-nav` | default, collapsed, with-sections |
74
+ | FileUpload | `uds-file-upload` | dropzone, button, avatar-upload |
75
+
76
+ ## Accessibility
77
+
78
+ All components follow WCAG 2.1 AA guidelines:
79
+
80
+ - Proper ARIA roles and attributes (dialog, alert, switch, tablist, etc.)
81
+ - Keyboard navigation support (arrow keys, Enter, Space, Escape)
82
+ - Focus management and visible focus indicators
83
+ - Label associations (`for`/`id` pairing on form controls)
84
+ - Semantic HTML elements (nav, blockquote, fieldset, etc.)
85
+ - Live regions for dynamic content (role=alert, role=status, aria-live)
86
+
87
+ ## Svelte 5 Patterns
88
+
89
+ Components use Svelte 5 features:
90
+
91
+ - `$props()` rune for prop declarations
92
+ - `$derived()` rune for computed values
93
+ - `$state()` rune for internal state
94
+ - `$effect()` rune for side effects
95
+ - `$bindable()` for two-way binding on form values
96
+ - `{@render children?.()}` for snippet-based slot content
97
+
98
+ ## License
99
+
100
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@mkatogui/uds-svelte",
3
+ "version": "0.2.1",
4
+ "description": "Svelte 5 components for Universal Design System — 31 accessible, themeable components",
5
+ "svelte": "./src/index.js",
6
+ "main": "./src/index.js",
7
+ "module": "./src/index.js",
8
+ "types": "./src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "svelte": "./src/index.js",
12
+ "types": "./src/index.d.ts"
13
+ }
14
+ },
15
+ "files": ["src/"],
16
+ "peerDependencies": {
17
+ "svelte": ">=5.0.0"
18
+ },
19
+ "keywords": ["svelte", "design-system", "components", "accessibility", "wcag"],
20
+ "author": "Marcelo Katogui",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/mkatogui/universal-design-system.git",
25
+ "directory": "packages/svelte"
26
+ }
27
+ }
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ interface AccordionItem {
3
+ id: string;
4
+ title: string;
5
+ content: string;
6
+ }
7
+
8
+ interface Props {
9
+ variant?: 'single' | 'multi' | 'flush';
10
+ items?: AccordionItem[];
11
+ defaultExpanded?: string[];
12
+ allowMultiple?: boolean;
13
+ class?: string;
14
+ children?: import('svelte').Snippet;
15
+ [key: string]: any;
16
+ }
17
+
18
+ let {
19
+ variant = 'single',
20
+ items = [],
21
+ defaultExpanded = [],
22
+ allowMultiple = false,
23
+ class: className = '',
24
+ children,
25
+ ...rest
26
+ }: Props = $props();
27
+
28
+ let expandedItems = $state<Set<string>>(new Set(defaultExpanded));
29
+
30
+ let classes = $derived(
31
+ [
32
+ 'uds-accordion',
33
+ `uds-accordion--${variant}`,
34
+ className,
35
+ ]
36
+ .filter(Boolean)
37
+ .join(' ')
38
+ );
39
+
40
+ function toggle(id: string) {
41
+ if (expandedItems.has(id)) {
42
+ expandedItems.delete(id);
43
+ expandedItems = new Set(expandedItems);
44
+ } else {
45
+ if (allowMultiple || variant === 'multi') {
46
+ expandedItems.add(id);
47
+ expandedItems = new Set(expandedItems);
48
+ } else {
49
+ expandedItems = new Set([id]);
50
+ }
51
+ }
52
+ }
53
+
54
+ function handleKeydown(event: KeyboardEvent, index: number) {
55
+ let nextIndex: number | undefined;
56
+ if (event.key === 'ArrowDown') {
57
+ nextIndex = (index + 1) % items.length;
58
+ } else if (event.key === 'ArrowUp') {
59
+ nextIndex = (index - 1 + items.length) % items.length;
60
+ }
61
+ if (nextIndex !== undefined) {
62
+ event.preventDefault();
63
+ const buttons = (event.currentTarget as HTMLElement)
64
+ .closest('.uds-accordion')?.querySelectorAll('.uds-accordion__trigger') as NodeListOf<HTMLElement>;
65
+ buttons?.[nextIndex]?.focus();
66
+ }
67
+ }
68
+ </script>
69
+
70
+ <div class={classes} {...rest}>
71
+ {#each items as item, i}
72
+ {@const isExpanded = expandedItems.has(item.id)}
73
+ <div class="uds-accordion__item" class:uds-accordion__item--expanded={isExpanded}>
74
+ <h3 class="uds-accordion__header">
75
+ <button
76
+ class="uds-accordion__trigger"
77
+ aria-expanded={isExpanded}
78
+ aria-controls="accordion-panel-{item.id}"
79
+ id="accordion-header-{item.id}"
80
+ onclick={() => toggle(item.id)}
81
+ onkeydown={(e) => handleKeydown(e, i)}
82
+ >
83
+ {item.title}
84
+ <span class="uds-accordion__icon" aria-hidden="true"></span>
85
+ </button>
86
+ </h3>
87
+ <div
88
+ class="uds-accordion__panel"
89
+ id="accordion-panel-{item.id}"
90
+ role="region"
91
+ aria-labelledby="accordion-header-{item.id}"
92
+ hidden={!isExpanded}
93
+ >
94
+ <div class="uds-accordion__content">{item.content}</div>
95
+ </div>
96
+ </div>
97
+ {/each}
98
+ {@render children?.()}
99
+ </div>
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ variant?: 'success' | 'warning' | 'error' | 'info';
4
+ size?: 'sm' | 'md' | 'lg';
5
+ title?: string;
6
+ message?: string;
7
+ dismissible?: boolean;
8
+ onDismiss?: () => void;
9
+ class?: string;
10
+ children?: import('svelte').Snippet;
11
+ [key: string]: any;
12
+ }
13
+
14
+ let {
15
+ variant = 'info',
16
+ size = 'md',
17
+ title = '',
18
+ message = '',
19
+ dismissible = false,
20
+ onDismiss,
21
+ class: className = '',
22
+ children,
23
+ ...rest
24
+ }: Props = $props();
25
+
26
+ let dismissed = $state(false);
27
+
28
+ let alertRole = $derived(
29
+ variant === 'error' || variant === 'warning' ? 'alert' : 'status'
30
+ );
31
+
32
+ let classes = $derived(
33
+ [
34
+ 'uds-alert',
35
+ `uds-alert--${variant}`,
36
+ `uds-alert--${size}`,
37
+ dismissed && 'uds-alert--dismissing',
38
+ className,
39
+ ]
40
+ .filter(Boolean)
41
+ .join(' ')
42
+ );
43
+
44
+ function handleDismiss() {
45
+ dismissed = true;
46
+ onDismiss?.();
47
+ }
48
+ </script>
49
+
50
+ {#if !dismissed}
51
+ <div class={classes} role={alertRole} {...rest}>
52
+ <div class="uds-alert__content">
53
+ {#if title}
54
+ <p class="uds-alert__title">{title}</p>
55
+ {/if}
56
+ {#if message}
57
+ <p class="uds-alert__message">{message}</p>
58
+ {/if}
59
+ {@render children?.()}
60
+ </div>
61
+ {#if dismissible}
62
+ <button class="uds-alert__dismiss" onclick={handleDismiss} aria-label="Dismiss alert">
63
+ <span aria-hidden="true">&times;</span>
64
+ </button>
65
+ {/if}
66
+ </div>
67
+ {/if}
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ variant?: 'image' | 'initials' | 'icon' | 'group';
4
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
5
+ src?: string;
6
+ alt?: string;
7
+ initials?: string;
8
+ status?: 'online' | 'offline' | 'busy';
9
+ fallback?: string;
10
+ class?: string;
11
+ icon?: import('svelte').Snippet;
12
+ children?: import('svelte').Snippet;
13
+ [key: string]: any;
14
+ }
15
+
16
+ let {
17
+ variant = 'image',
18
+ size = 'md',
19
+ src,
20
+ alt = '',
21
+ initials = '',
22
+ status,
23
+ fallback,
24
+ class: className = '',
25
+ icon,
26
+ children,
27
+ ...rest
28
+ }: Props = $props();
29
+
30
+ let imgError = $state(false);
31
+
32
+ let classes = $derived(
33
+ [
34
+ 'uds-avatar',
35
+ `uds-avatar--${variant}`,
36
+ `uds-avatar--${size}`,
37
+ status && `uds-avatar--${status}`,
38
+ className,
39
+ ]
40
+ .filter(Boolean)
41
+ .join(' ')
42
+ );
43
+ </script>
44
+
45
+ {#if variant === 'group'}
46
+ <div class={classes} role="group" aria-label="User avatars" {...rest}>
47
+ {@render children?.()}
48
+ </div>
49
+ {:else}
50
+ <div class={classes} {...rest}>
51
+ {#if variant === 'image' && src && !imgError}
52
+ <img class="uds-avatar__image" src={src} alt={alt} onerror={() => (imgError = true)} />
53
+ {:else if variant === 'icon' && icon}
54
+ <span class="uds-avatar__icon" aria-hidden="true">{@render icon()}</span>
55
+ {:else}
56
+ <span class="uds-avatar__initials" aria-label={alt || initials}>{initials || fallback || '?'}</span>
57
+ {/if}
58
+ {#if status}
59
+ <span class="uds-avatar__status" aria-label={status}></span>
60
+ {/if}
61
+ </div>
62
+ {/if}
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ variant?: 'status' | 'count' | 'tag';
4
+ size?: 'sm' | 'md';
5
+ label?: string;
6
+ color?: string;
7
+ removable?: boolean;
8
+ onRemove?: () => void;
9
+ class?: string;
10
+ children?: import('svelte').Snippet;
11
+ [key: string]: any;
12
+ }
13
+
14
+ let {
15
+ variant = 'status',
16
+ size = 'md',
17
+ label = '',
18
+ color,
19
+ removable = false,
20
+ onRemove,
21
+ class: className = '',
22
+ children,
23
+ ...rest
24
+ }: Props = $props();
25
+
26
+ let classes = $derived(
27
+ [
28
+ 'uds-badge',
29
+ `uds-badge--${variant}`,
30
+ `uds-badge--${size}`,
31
+ color && `uds-badge--${color}`,
32
+ removable && 'uds-badge--removable',
33
+ className,
34
+ ]
35
+ .filter(Boolean)
36
+ .join(' ')
37
+ );
38
+ </script>
39
+
40
+ <span class={classes} aria-label={label || undefined} {...rest}>
41
+ {#if label}
42
+ {label}
43
+ {/if}
44
+ {@render children?.()}
45
+ {#if removable}
46
+ <button class="uds-badge__remove" onclick={onRemove} aria-label="Remove {label}">
47
+ <span aria-hidden="true">&times;</span>
48
+ </button>
49
+ {/if}
50
+ </span>
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ interface BreadcrumbItem {
3
+ label: string;
4
+ href?: string;
5
+ }
6
+
7
+ interface Props {
8
+ variant?: 'standard' | 'truncated';
9
+ items?: BreadcrumbItem[];
10
+ separator?: string;
11
+ maxItems?: number;
12
+ class?: string;
13
+ [key: string]: any;
14
+ }
15
+
16
+ let {
17
+ variant = 'standard',
18
+ items = [],
19
+ separator = '/',
20
+ maxItems,
21
+ class: className = '',
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ let classes = $derived(
26
+ [
27
+ 'uds-breadcrumb',
28
+ `uds-breadcrumb--${variant}`,
29
+ className,
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' ')
33
+ );
34
+
35
+ let displayItems = $derived(() => {
36
+ if (variant === 'truncated' && maxItems && items.length > maxItems) {
37
+ const first = items.slice(0, 1);
38
+ const last = items.slice(-(maxItems - 1));
39
+ return [...first, { label: '...', href: undefined }, ...last];
40
+ }
41
+ return items;
42
+ });
43
+ </script>
44
+
45
+ <nav class={classes} aria-label="Breadcrumb" {...rest}>
46
+ <ol class="uds-breadcrumb__list">
47
+ {#each displayItems() as item, i}
48
+ <li class="uds-breadcrumb__item">
49
+ {#if i > 0}
50
+ <span class="uds-breadcrumb__separator" aria-hidden="true">{separator}</span>
51
+ {/if}
52
+ {#if item.href && i < displayItems().length - 1}
53
+ <a class="uds-breadcrumb__link" href={item.href}>{item.label}</a>
54
+ {:else}
55
+ <span class="uds-breadcrumb__current" aria-current={i === displayItems().length - 1 ? 'page' : undefined}>{item.label}</span>
56
+ {/if}
57
+ </li>
58
+ {/each}
59
+ </ol>
60
+ </nav>
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ variant?: 'primary' | 'secondary' | 'ghost' | 'gradient' | 'destructive' | 'icon-only';
4
+ size?: 'sm' | 'md' | 'lg' | 'xl';
5
+ loading?: boolean;
6
+ fullWidth?: boolean;
7
+ disabled?: boolean;
8
+ class?: string;
9
+ iconLeft?: import('svelte').Snippet;
10
+ iconRight?: import('svelte').Snippet;
11
+ children?: import('svelte').Snippet;
12
+ [key: string]: any;
13
+ }
14
+
15
+ let {
16
+ variant = 'primary',
17
+ size = 'md',
18
+ loading = false,
19
+ fullWidth = false,
20
+ disabled = false,
21
+ class: className = '',
22
+ iconLeft,
23
+ iconRight,
24
+ children,
25
+ ...rest
26
+ }: Props = $props();
27
+
28
+ let classes = $derived(
29
+ [
30
+ 'uds-btn',
31
+ `uds-btn--${variant}`,
32
+ `uds-btn--${size}`,
33
+ fullWidth && 'uds-btn--full-width',
34
+ loading && 'uds-btn--loading',
35
+ className,
36
+ ]
37
+ .filter(Boolean)
38
+ .join(' ')
39
+ );
40
+ </script>
41
+
42
+ <button class={classes} disabled={disabled || loading} aria-busy={loading || undefined} aria-disabled={disabled || undefined} {...rest}>
43
+ {#if loading}
44
+ <span class="uds-btn__spinner" aria-hidden="true"></span>
45
+ {/if}
46
+ {#if iconLeft}
47
+ <span class="uds-btn__icon-left">{@render iconLeft()}</span>
48
+ {/if}
49
+ {@render children?.()}
50
+ {#if iconRight}
51
+ <span class="uds-btn__icon-right">{@render iconRight()}</span>
52
+ {/if}
53
+ </button>
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ checked?: boolean;
4
+ indeterminate?: boolean;
5
+ disabled?: boolean;
6
+ label?: string;
7
+ name?: string;
8
+ value?: string;
9
+ id?: string;
10
+ class?: string;
11
+ [key: string]: any;
12
+ }
13
+
14
+ let {
15
+ checked = $bindable(false),
16
+ indeterminate = false,
17
+ disabled = false,
18
+ label = '',
19
+ name,
20
+ value,
21
+ id,
22
+ class: className = '',
23
+ ...rest
24
+ }: Props = $props();
25
+
26
+ let checkboxId = $derived(id || `uds-checkbox-${Math.random().toString(36).slice(2, 9)}`);
27
+
28
+ let classes = $derived(
29
+ [
30
+ 'uds-checkbox',
31
+ indeterminate && 'uds-checkbox--indeterminate',
32
+ disabled && 'uds-checkbox--disabled',
33
+ className,
34
+ ]
35
+ .filter(Boolean)
36
+ .join(' ')
37
+ );
38
+
39
+ function bindIndeterminate(node: HTMLInputElement) {
40
+ $effect(() => {
41
+ node.indeterminate = indeterminate;
42
+ });
43
+ }
44
+ </script>
45
+
46
+ <div class={classes}>
47
+ <input
48
+ class="uds-checkbox__input"
49
+ type="checkbox"
50
+ id={checkboxId}
51
+ bind:checked
52
+ {disabled}
53
+ {name}
54
+ {value}
55
+ aria-checked={indeterminate ? 'mixed' : checked}
56
+ use:bindIndeterminate
57
+ {...rest}
58
+ />
59
+ {#if label}
60
+ <label class="uds-checkbox__label" for={checkboxId}>{label}</label>
61
+ {/if}
62
+ </div>
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ interface Tab {
3
+ label: string;
4
+ language: string;
5
+ code: string;
6
+ }
7
+
8
+ interface Props {
9
+ variant?: 'syntax-highlighted' | 'terminal' | 'multi-tab';
10
+ size?: 'sm' | 'md' | 'lg';
11
+ language?: string;
12
+ code?: string;
13
+ showLineNumbers?: boolean;
14
+ showCopy?: boolean;
15
+ tabs?: Tab[];
16
+ class?: string;
17
+ [key: string]: any;
18
+ }
19
+
20
+ let {
21
+ variant = 'syntax-highlighted',
22
+ size = 'md',
23
+ language = '',
24
+ code = '',
25
+ showLineNumbers = false,
26
+ showCopy = true,
27
+ tabs = [],
28
+ class: className = '',
29
+ ...rest
30
+ }: Props = $props();
31
+
32
+ let copied = $state(false);
33
+ let activeTab = $state(0);
34
+
35
+ let classes = $derived(
36
+ [
37
+ 'uds-code-block',
38
+ `uds-code-block--${variant}`,
39
+ `uds-code-block--${size}`,
40
+ showLineNumbers && 'uds-code-block--line-numbers',
41
+ className,
42
+ ]
43
+ .filter(Boolean)
44
+ .join(' ')
45
+ );
46
+
47
+ let displayCode = $derived(
48
+ variant === 'multi-tab' && tabs.length > 0 ? tabs[activeTab].code : code
49
+ );
50
+
51
+ let displayLanguage = $derived(
52
+ variant === 'multi-tab' && tabs.length > 0 ? tabs[activeTab].language : language
53
+ );
54
+
55
+ async function copyToClipboard() {
56
+ try {
57
+ await navigator.clipboard.writeText(displayCode);
58
+ copied = true;
59
+ setTimeout(() => (copied = false), 2000);
60
+ } catch {
61
+ // Clipboard API not available
62
+ }
63
+ }
64
+ </script>
65
+
66
+ <div class={classes} {...rest}>
67
+ {#if variant === 'multi-tab' && tabs.length > 0}
68
+ <div class="uds-code-block__tabs" role="tablist">
69
+ {#each tabs as tab, i}
70
+ <button
71
+ class="uds-code-block__tab"
72
+ class:uds-code-block__tab--active={i === activeTab}
73
+ role="tab"
74
+ aria-selected={i === activeTab}
75
+ onclick={() => (activeTab = i)}
76
+ >
77
+ {tab.label}
78
+ </button>
79
+ {/each}
80
+ </div>
81
+ {/if}
82
+ <div class="uds-code-block__container">
83
+ {#if showCopy}
84
+ <button class="uds-code-block__copy" onclick={copyToClipboard} aria-label="Copy code">
85
+ {copied ? 'Copied' : 'Copy'}
86
+ </button>
87
+ {/if}
88
+ <pre class="uds-code-block__pre"><code class="uds-code-block__code language-{displayLanguage}">{displayCode}</code></pre>
89
+ </div>
90
+ </div>