@mkatogui/uds-vue 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 (35) hide show
  1. package/README.md +102 -0
  2. package/package.json +27 -0
  3. package/src/components/UdsAccordion.vue +82 -0
  4. package/src/components/UdsAlert.vue +61 -0
  5. package/src/components/UdsAvatar.vue +59 -0
  6. package/src/components/UdsBadge.vue +48 -0
  7. package/src/components/UdsBreadcrumb.vue +73 -0
  8. package/src/components/UdsButton.vue +43 -0
  9. package/src/components/UdsCard.vue +49 -0
  10. package/src/components/UdsCheckbox.vue +56 -0
  11. package/src/components/UdsCodeBlock.vue +86 -0
  12. package/src/components/UdsCommandPalette.vue +144 -0
  13. package/src/components/UdsDataTable.vue +142 -0
  14. package/src/components/UdsDatePicker.vue +69 -0
  15. package/src/components/UdsDropdown.vue +132 -0
  16. package/src/components/UdsFileUpload.vue +148 -0
  17. package/src/components/UdsFooter.vue +39 -0
  18. package/src/components/UdsHero.vue +44 -0
  19. package/src/components/UdsInput.vue +95 -0
  20. package/src/components/UdsModal.vue +114 -0
  21. package/src/components/UdsNavbar.vue +57 -0
  22. package/src/components/UdsPagination.vue +96 -0
  23. package/src/components/UdsPricing.vue +58 -0
  24. package/src/components/UdsProgress.vue +92 -0
  25. package/src/components/UdsRadio.vue +56 -0
  26. package/src/components/UdsSelect.vue +84 -0
  27. package/src/components/UdsSideNav.vue +102 -0
  28. package/src/components/UdsSkeleton.vue +51 -0
  29. package/src/components/UdsSocialProof.vue +58 -0
  30. package/src/components/UdsTabs.vue +106 -0
  31. package/src/components/UdsTestimonial.vue +57 -0
  32. package/src/components/UdsToast.vue +70 -0
  33. package/src/components/UdsToggle.vue +60 -0
  34. package/src/components/UdsTooltip.vue +62 -0
  35. package/src/index.ts +32 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @mkatogui/uds-vue
2
+
3
+ Vue 3 Composition API components for the Universal Design System. 32 accessible, themeable components built with `<script setup lang="ts">` and `defineProps`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mkatogui/uds-vue
9
+ ```
10
+
11
+ Vue 3.3+ is required as a peer dependency.
12
+
13
+ ## Usage
14
+
15
+ Import individual components:
16
+
17
+ ```vue
18
+ <script setup>
19
+ import { UdsButton, UdsInput, UdsModal } from '@mkatogui/uds-vue'
20
+ </script>
21
+
22
+ <template>
23
+ <UdsButton variant="primary" size="md" @click="handleClick">
24
+ Get Started
25
+ </UdsButton>
26
+ </template>
27
+ ```
28
+
29
+ ## Theming
30
+
31
+ Components use UDS design tokens via CSS custom properties and BEM class naming (`uds-{component}--{variant}`). Apply a palette with the `data-theme` attribute on a parent element:
32
+
33
+ ```html
34
+ <div data-theme="ai-futuristic">
35
+ <UdsButton variant="gradient">Launch</UdsButton>
36
+ </div>
37
+ ```
38
+
39
+ Available palettes: `minimal-saas`, `ai-futuristic`, `gradient-startup`, `corporate`, `apple-minimal`, `illustration`, `dashboard`, `bold-lifestyle`, `minimal-corporate`.
40
+
41
+ ## Components
42
+
43
+ | Component | File | Variants |
44
+ |-----------|------|----------|
45
+ | Button | `UdsButton.vue` | primary, secondary, ghost, gradient, destructive, icon-only |
46
+ | Navigation Bar | `UdsNavbar.vue` | standard, minimal, dark, transparent |
47
+ | Hero Section | `UdsHero.vue` | centered, product-screenshot, video-bg, gradient-mesh, search-forward, split |
48
+ | Feature Card | `UdsCard.vue` | icon-top, image-top, horizontal, stat-card, dashboard-preview |
49
+ | Pricing Table | `UdsPricing.vue` | 2-column, 3-column, 4-column, toggle |
50
+ | Social Proof Bar | `UdsSocialProof.vue` | logo-strip, stats-counter, testimonial-mini, combined |
51
+ | Testimonial Card | `UdsTestimonial.vue` | quote-card, video, metric, carousel |
52
+ | Footer | `UdsFooter.vue` | simple, multi-column, newsletter, mega-footer |
53
+ | Code Block | `UdsCodeBlock.vue` | syntax-highlighted, terminal, multi-tab |
54
+ | Modal | `UdsModal.vue` | confirmation, task, alert |
55
+ | Form Input | `UdsInput.vue` | text, email, password, number, search, textarea |
56
+ | Select | `UdsSelect.vue` | native, custom |
57
+ | Checkbox | `UdsCheckbox.vue` | standard, indeterminate |
58
+ | Radio | `UdsRadio.vue` | standard, card |
59
+ | Toggle Switch | `UdsToggle.vue` | standard, with-label |
60
+ | Alert | `UdsAlert.vue` | success, warning, error, info |
61
+ | Badge | `UdsBadge.vue` | status, count, tag |
62
+ | Tabs | `UdsTabs.vue` | line, pill, segmented |
63
+ | Accordion | `UdsAccordion.vue` | single, multi, flush |
64
+ | Breadcrumb | `UdsBreadcrumb.vue` | standard, truncated |
65
+ | Tooltip | `UdsTooltip.vue` | simple, rich |
66
+ | Dropdown Menu | `UdsDropdown.vue` | action, context, nav-sub |
67
+ | Avatar | `UdsAvatar.vue` | image, initials, icon, group |
68
+ | Skeleton | `UdsSkeleton.vue` | text, card, avatar, table |
69
+ | Toast | `UdsToast.vue` | success, error, warning, info, neutral |
70
+ | Pagination | `UdsPagination.vue` | numbered, simple, load-more, infinite-scroll |
71
+ | Data Table | `UdsDataTable.vue` | basic, sortable, selectable, expandable |
72
+ | Date Picker | `UdsDatePicker.vue` | single, range, with-time |
73
+ | Command Palette | `UdsCommandPalette.vue` | standard |
74
+ | Progress Indicator | `UdsProgress.vue` | bar, circular, stepper |
75
+ | Side Navigation | `UdsSideNav.vue` | default, collapsed, with-sections |
76
+ | File Upload | `UdsFileUpload.vue` | dropzone, button, avatar-upload |
77
+
78
+ ## Accessibility
79
+
80
+ All components follow WCAG 2.1 AA guidelines:
81
+
82
+ - Proper ARIA roles and attributes (`role`, `aria-label`, `aria-expanded`, `aria-selected`, etc.)
83
+ - Keyboard navigation support (arrow keys, Enter, Space, Escape, Tab)
84
+ - Focus management (focus traps in Modal, focus restoration)
85
+ - Semantic HTML elements (`nav`, `button`, `figure`, `blockquote`, `table`)
86
+ - Screen reader announcements via `role="alert"` and `role="status"`
87
+ - Form label associations via `for`/`id` pairs
88
+
89
+ ## v-model Support
90
+
91
+ Form components support Vue's `v-model` directive:
92
+
93
+ ```vue
94
+ <UdsInput v-model="email" variant="email" label="Email" />
95
+ <UdsSelect v-model="country" :options="countries" label="Country" />
96
+ <UdsCheckbox v-model="agreed" label="I agree to the terms" />
97
+ <UdsToggle v-model="darkMode" label="Dark mode" />
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@mkatogui/uds-vue",
3
+ "version": "0.2.1",
4
+ "description": "Vue 3 components for Universal Design System — 31 accessible, themeable components",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": ["dist/", "src/"],
16
+ "peerDependencies": {
17
+ "vue": ">=3.3.0"
18
+ },
19
+ "keywords": ["vue", "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/vue"
26
+ }
27
+ }
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ interface AccordionItem {
5
+ title: string
6
+ content?: string
7
+ defaultExpanded?: boolean
8
+ }
9
+
10
+ interface Props {
11
+ variant?: 'single' | 'multi' | 'flush'
12
+ items?: AccordionItem[]
13
+ allowMultiple?: boolean
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ variant: 'single',
18
+ items: () => [],
19
+ })
20
+
21
+ const expandedItems = ref<Set<number>>(
22
+ new Set(props.items.map((item, i) => (item.defaultExpanded ? i : -1)).filter((i) => i >= 0))
23
+ )
24
+
25
+ const classes = computed(() =>
26
+ [
27
+ 'uds-accordion',
28
+ `uds-accordion--${props.variant}`,
29
+ ]
30
+ .filter(Boolean)
31
+ .join(' ')
32
+ )
33
+
34
+ function toggle(index: number) {
35
+ const next = new Set(expandedItems.value)
36
+ if (next.has(index)) {
37
+ next.delete(index)
38
+ } else {
39
+ if (!props.allowMultiple && props.variant === 'single') {
40
+ next.clear()
41
+ }
42
+ next.add(index)
43
+ }
44
+ expandedItems.value = next
45
+ }
46
+
47
+ function handleKeydown(e: KeyboardEvent, index: number) {
48
+ if (e.key === 'Enter' || e.key === ' ') {
49
+ e.preventDefault()
50
+ toggle(index)
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <div :class="classes">
57
+ <div v-for="(item, i) in items" :key="i" class="uds-accordion__item">
58
+ <button
59
+ class="uds-accordion__trigger"
60
+ :aria-expanded="expandedItems.has(i)"
61
+ :aria-controls="`accordion-panel-${i}`"
62
+ @click="toggle(i)"
63
+ @keydown="handleKeydown($event, i)"
64
+ >
65
+ <span class="uds-accordion__title">{{ item.title }}</span>
66
+ <span class="uds-accordion__icon" aria-hidden="true" />
67
+ </button>
68
+ <div
69
+ :id="`accordion-panel-${i}`"
70
+ class="uds-accordion__panel"
71
+ role="region"
72
+ :hidden="!expandedItems.has(i)"
73
+ >
74
+ <div class="uds-accordion__content">
75
+ <slot :name="`item-${i}`">
76
+ {{ item.content }}
77
+ </slot>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'success' | 'warning' | 'error' | 'info'
6
+ size?: 'sm' | 'md' | 'lg'
7
+ title?: string
8
+ message?: string
9
+ dismissible?: boolean
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ variant: 'info',
14
+ size: 'md',
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ dismiss: []
19
+ }>()
20
+
21
+ const dismissed = ref(false)
22
+
23
+ const classes = computed(() =>
24
+ [
25
+ 'uds-alert',
26
+ `uds-alert--${props.variant}`,
27
+ `uds-alert--${props.size}`,
28
+ ]
29
+ .filter(Boolean)
30
+ .join(' ')
31
+ )
32
+
33
+ const alertRole = computed(() =>
34
+ props.variant === 'error' || props.variant === 'warning' ? 'alert' : 'status'
35
+ )
36
+
37
+ function handleDismiss() {
38
+ dismissed.value = true
39
+ emit('dismiss')
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <div v-if="!dismissed" :class="classes" :role="alertRole">
45
+ <div class="uds-alert__content">
46
+ <strong v-if="title" class="uds-alert__title">{{ title }}</strong>
47
+ <p v-if="message" class="uds-alert__message">{{ message }}</p>
48
+ <slot />
49
+ </div>
50
+ <button
51
+ v-if="dismissible"
52
+ class="uds-alert__dismiss"
53
+ aria-label="Dismiss alert"
54
+ @click="handleDismiss"
55
+ >
56
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
57
+ <path d="M18 6L6 18M6 6l12 12" />
58
+ </svg>
59
+ </button>
60
+ </div>
61
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'image' | 'initials' | 'icon' | 'group'
6
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
7
+ src?: string
8
+ alt?: string
9
+ initials?: string
10
+ status?: 'online' | 'offline' | 'busy'
11
+ fallback?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'image',
16
+ size: 'md',
17
+ })
18
+
19
+ const imgError = ref(false)
20
+
21
+ const classes = computed(() =>
22
+ [
23
+ 'uds-avatar',
24
+ `uds-avatar--${props.variant}`,
25
+ `uds-avatar--${props.size}`,
26
+ props.status && `uds-avatar--${props.status}`,
27
+ ]
28
+ .filter(Boolean)
29
+ .join(' ')
30
+ )
31
+
32
+ function handleError() {
33
+ imgError.value = true
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <div :class="classes">
39
+ <img
40
+ v-if="variant === 'image' && src && !imgError"
41
+ :src="src"
42
+ :alt="alt || ''"
43
+ class="uds-avatar__image"
44
+ @error="handleError"
45
+ />
46
+ <span v-else-if="variant === 'initials' || imgError" class="uds-avatar__initials" :aria-label="alt">
47
+ {{ initials || fallback || '?' }}
48
+ </span>
49
+ <span v-else class="uds-avatar__icon" :aria-label="alt">
50
+ <slot />
51
+ </span>
52
+ <span
53
+ v-if="status"
54
+ class="uds-avatar__status"
55
+ :class="`uds-avatar__status--${status}`"
56
+ :aria-label="status"
57
+ />
58
+ </div>
59
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'status' | 'count' | 'tag'
6
+ size?: 'sm' | 'md'
7
+ label?: string
8
+ color?: string
9
+ removable?: boolean
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ variant: 'status',
14
+ size: 'md',
15
+ })
16
+
17
+ const emit = defineEmits<{
18
+ remove: []
19
+ }>()
20
+
21
+ const classes = computed(() =>
22
+ [
23
+ 'uds-badge',
24
+ `uds-badge--${props.variant}`,
25
+ `uds-badge--${props.size}`,
26
+ props.color && `uds-badge--${props.color}`,
27
+ props.removable && 'uds-badge--removable',
28
+ ]
29
+ .filter(Boolean)
30
+ .join(' ')
31
+ )
32
+ </script>
33
+
34
+ <template>
35
+ <span :class="classes" :aria-label="label">
36
+ <slot>{{ label }}</slot>
37
+ <button
38
+ v-if="removable"
39
+ class="uds-badge__remove"
40
+ :aria-label="`Remove ${label || ''}`"
41
+ @click="emit('remove')"
42
+ >
43
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
44
+ <path d="M18 6L6 18M6 6l12 12" />
45
+ </svg>
46
+ </button>
47
+ </span>
48
+ </template>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface BreadcrumbItem {
5
+ label: string
6
+ href?: string
7
+ }
8
+
9
+ interface Props {
10
+ variant?: 'standard' | 'truncated'
11
+ items?: BreadcrumbItem[]
12
+ separator?: string
13
+ maxItems?: number
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ variant: 'standard',
18
+ separator: '/',
19
+ items: () => [],
20
+ })
21
+
22
+ const classes = computed(() =>
23
+ [
24
+ 'uds-breadcrumb',
25
+ `uds-breadcrumb--${props.variant}`,
26
+ ]
27
+ .filter(Boolean)
28
+ .join(' ')
29
+ )
30
+
31
+ const displayItems = computed(() => {
32
+ if (props.variant === 'truncated' && props.maxItems && props.items.length > props.maxItems) {
33
+ const first = props.items.slice(0, 1)
34
+ const last = props.items.slice(-(props.maxItems - 1))
35
+ return [...first, { label: '...', href: undefined } as BreadcrumbItem, ...last]
36
+ }
37
+ return props.items
38
+ })
39
+ </script>
40
+
41
+ <template>
42
+ <nav :class="classes" aria-label="Breadcrumb">
43
+ <ol class="uds-breadcrumb__list">
44
+ <li
45
+ v-for="(item, i) in displayItems"
46
+ :key="i"
47
+ class="uds-breadcrumb__item"
48
+ >
49
+ <a
50
+ v-if="item.href && i < displayItems.length - 1"
51
+ :href="item.href"
52
+ class="uds-breadcrumb__link"
53
+ >
54
+ {{ item.label }}
55
+ </a>
56
+ <span
57
+ v-else
58
+ class="uds-breadcrumb__current"
59
+ :aria-current="i === displayItems.length - 1 ? 'page' : undefined"
60
+ >
61
+ {{ item.label }}
62
+ </span>
63
+ <span
64
+ v-if="i < displayItems.length - 1"
65
+ class="uds-breadcrumb__separator"
66
+ aria-hidden="true"
67
+ >
68
+ {{ separator }}
69
+ </span>
70
+ </li>
71
+ </ol>
72
+ </nav>
73
+ </template>
@@ -0,0 +1,43 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'primary' | 'secondary' | 'ghost' | 'gradient' | 'destructive' | 'icon-only'
6
+ size?: 'sm' | 'md' | 'lg' | 'xl'
7
+ loading?: boolean
8
+ fullWidth?: boolean
9
+ disabled?: boolean
10
+ iconLeft?: boolean
11
+ iconRight?: boolean
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'primary',
16
+ size: 'md',
17
+ })
18
+
19
+ const classes = computed(() =>
20
+ [
21
+ 'uds-btn',
22
+ `uds-btn--${props.variant}`,
23
+ `uds-btn--${props.size}`,
24
+ props.fullWidth && 'uds-btn--full-width',
25
+ props.loading && 'uds-btn--loading',
26
+ ]
27
+ .filter(Boolean)
28
+ .join(' ')
29
+ )
30
+ </script>
31
+
32
+ <template>
33
+ <button
34
+ :class="classes"
35
+ :disabled="disabled || loading"
36
+ :aria-busy="loading || undefined"
37
+ :aria-disabled="disabled || undefined"
38
+ role="button"
39
+ >
40
+ <span v-if="loading" class="uds-btn__spinner" aria-hidden="true" />
41
+ <slot />
42
+ </button>
43
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'icon-top' | 'image-top' | 'horizontal' | 'stat-card' | 'dashboard-preview'
6
+ size?: 'sm' | 'md' | 'lg'
7
+ title?: string
8
+ description?: string
9
+ link?: string
10
+ image?: string
11
+ imageAlt?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'icon-top',
16
+ size: 'md',
17
+ })
18
+
19
+ const classes = computed(() =>
20
+ [
21
+ 'uds-card',
22
+ `uds-card--${props.variant}`,
23
+ `uds-card--${props.size}`,
24
+ ]
25
+ .filter(Boolean)
26
+ .join(' ')
27
+ )
28
+ </script>
29
+
30
+ <template>
31
+ <div :class="classes">
32
+ <div v-if="image" class="uds-card__image">
33
+ <img :src="image" :alt="imageAlt || ''" />
34
+ </div>
35
+ <div class="uds-card__icon">
36
+ <slot name="icon" />
37
+ </div>
38
+ <div class="uds-card__body">
39
+ <h3 v-if="title" class="uds-card__title">{{ title }}</h3>
40
+ <p v-if="description" class="uds-card__description">{{ description }}</p>
41
+ <slot />
42
+ </div>
43
+ <div v-if="link" class="uds-card__link">
44
+ <a :href="link">
45
+ <slot name="link-text">Learn more</slot>
46
+ </a>
47
+ </div>
48
+ </div>
49
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { computed, useId } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'standard' | 'indeterminate'
6
+ checked?: boolean
7
+ indeterminate?: boolean
8
+ disabled?: boolean
9
+ label?: string
10
+ name?: string
11
+ modelValue?: boolean
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'standard',
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ 'update:modelValue': [value: boolean]
20
+ }>()
21
+
22
+ const checkboxId = useId()
23
+
24
+ const classes = computed(() =>
25
+ [
26
+ 'uds-checkbox',
27
+ `uds-checkbox--${props.variant}`,
28
+ props.disabled && 'uds-checkbox--disabled',
29
+ props.indeterminate && 'uds-checkbox--indeterminate',
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' ')
33
+ )
34
+
35
+ function handleChange(e: Event) {
36
+ const target = e.target as HTMLInputElement
37
+ emit('update:modelValue', target.checked)
38
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <div :class="classes">
43
+ <input
44
+ :id="checkboxId"
45
+ type="checkbox"
46
+ class="uds-checkbox__input"
47
+ :checked="modelValue ?? checked"
48
+ :indeterminate="indeterminate"
49
+ :disabled="disabled"
50
+ :name="name"
51
+ :aria-checked="indeterminate ? 'mixed' : undefined"
52
+ @change="handleChange"
53
+ />
54
+ <label v-if="label" :for="checkboxId" class="uds-checkbox__label">{{ label }}</label>
55
+ </div>
56
+ </template>
@@ -0,0 +1,86 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ interface Tab {
5
+ label: string
6
+ language: string
7
+ code: string
8
+ }
9
+
10
+ interface Props {
11
+ variant?: 'syntax-highlighted' | 'terminal' | 'multi-tab'
12
+ size?: 'sm' | 'md' | 'lg'
13
+ language?: string
14
+ code?: string
15
+ showLineNumbers?: boolean
16
+ showCopy?: boolean
17
+ tabs?: Tab[]
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ variant: 'syntax-highlighted',
22
+ size: 'md',
23
+ showCopy: true,
24
+ tabs: () => [],
25
+ })
26
+
27
+ const copied = ref(false)
28
+ const activeTab = ref(0)
29
+
30
+ const classes = computed(() =>
31
+ [
32
+ 'uds-code-block',
33
+ `uds-code-block--${props.variant}`,
34
+ `uds-code-block--${props.size}`,
35
+ props.showLineNumbers && 'uds-code-block--line-numbers',
36
+ copied.value && 'uds-code-block--copied',
37
+ ]
38
+ .filter(Boolean)
39
+ .join(' ')
40
+ )
41
+
42
+ const displayCode = computed(() => {
43
+ if (props.variant === 'multi-tab' && props.tabs.length) {
44
+ return props.tabs[activeTab.value]?.code || ''
45
+ }
46
+ return props.code || ''
47
+ })
48
+
49
+ async function copyToClipboard() {
50
+ try {
51
+ await navigator.clipboard.writeText(displayCode.value)
52
+ copied.value = true
53
+ setTimeout(() => { copied.value = false }, 2000)
54
+ } catch {
55
+ // Clipboard API not available
56
+ }
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div :class="classes">
62
+ <div v-if="variant === 'multi-tab' && tabs.length" class="uds-code-block__tabs" role="tablist">
63
+ <button
64
+ v-for="(tab, i) in tabs"
65
+ :key="i"
66
+ role="tab"
67
+ :aria-selected="activeTab === i"
68
+ class="uds-code-block__tab"
69
+ @click="activeTab = i"
70
+ >
71
+ {{ tab.label }}
72
+ </button>
73
+ </div>
74
+ <div class="uds-code-block__container">
75
+ <button
76
+ v-if="showCopy"
77
+ class="uds-code-block__copy"
78
+ aria-label="Copy code to clipboard"
79
+ @click="copyToClipboard"
80
+ >
81
+ {{ copied ? 'Copied' : 'Copy' }}
82
+ </button>
83
+ <pre><code :class="language ? `language-${language}` : undefined">{{ displayCode }}</code></pre>
84
+ </div>
85
+ </div>
86
+ </template>