@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
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'centered' | 'product-screenshot' | 'video-bg' | 'gradient-mesh' | 'search-forward' | 'split'
6
+ size?: 'full' | 'compact'
7
+ headline?: string
8
+ subheadline?: string
9
+ }
10
+
11
+ const props = withDefaults(defineProps<Props>(), {
12
+ variant: 'centered',
13
+ size: 'full',
14
+ })
15
+
16
+ const classes = computed(() =>
17
+ [
18
+ 'uds-hero',
19
+ `uds-hero--${props.variant}`,
20
+ `uds-hero--${props.size}`,
21
+ ]
22
+ .filter(Boolean)
23
+ .join(' ')
24
+ )
25
+ </script>
26
+
27
+ <template>
28
+ <section :class="classes">
29
+ <div class="uds-hero__content">
30
+ <h1 v-if="headline" class="uds-hero__headline">{{ headline }}</h1>
31
+ <p v-if="subheadline" class="uds-hero__subheadline">{{ subheadline }}</p>
32
+ <div class="uds-hero__cta">
33
+ <slot name="cta" />
34
+ </div>
35
+ <div class="uds-hero__social-proof">
36
+ <slot name="social-proof" />
37
+ </div>
38
+ </div>
39
+ <div class="uds-hero__visual">
40
+ <slot name="visual" />
41
+ </div>
42
+ <slot />
43
+ </section>
44
+ </template>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { computed, useId } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'text' | 'email' | 'password' | 'number' | 'search' | 'textarea'
6
+ size?: 'sm' | 'md' | 'lg'
7
+ state?: 'default' | 'focus' | 'error' | 'disabled' | 'readonly'
8
+ label?: string
9
+ helperText?: string
10
+ errorText?: string
11
+ required?: boolean
12
+ modelValue?: string | number
13
+ placeholder?: string
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ variant: 'text',
18
+ size: 'md',
19
+ state: 'default',
20
+ })
21
+
22
+ const emit = defineEmits<{
23
+ 'update:modelValue': [value: string | number]
24
+ }>()
25
+
26
+ const inputId = useId()
27
+ const helperId = useId()
28
+ const errorId = useId()
29
+
30
+ const classes = computed(() =>
31
+ [
32
+ 'uds-input',
33
+ `uds-input--${props.variant}`,
34
+ `uds-input--${props.size}`,
35
+ props.state === 'error' && 'uds-input--error',
36
+ props.state === 'disabled' && 'uds-input--disabled',
37
+ ]
38
+ .filter(Boolean)
39
+ .join(' ')
40
+ )
41
+
42
+ const describedBy = computed(() => {
43
+ const ids: string[] = []
44
+ if (props.errorText) ids.push(errorId)
45
+ if (props.helperText) ids.push(helperId)
46
+ return ids.length ? ids.join(' ') : undefined
47
+ })
48
+
49
+ function handleInput(e: Event) {
50
+ const target = e.target as HTMLInputElement | HTMLTextAreaElement
51
+ emit('update:modelValue', target.value)
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <div :class="classes">
57
+ <label v-if="label" :for="inputId" class="uds-input__label">
58
+ {{ label }}
59
+ <span v-if="required" class="uds-input__required" aria-hidden="true">*</span>
60
+ </label>
61
+ <textarea
62
+ v-if="variant === 'textarea'"
63
+ :id="inputId"
64
+ class="uds-input__field uds-input__textarea"
65
+ :value="modelValue"
66
+ :placeholder="placeholder"
67
+ :required="required"
68
+ :disabled="state === 'disabled'"
69
+ :readonly="state === 'readonly'"
70
+ :aria-invalid="state === 'error' || undefined"
71
+ :aria-describedby="describedBy"
72
+ @input="handleInput"
73
+ />
74
+ <input
75
+ v-else
76
+ :id="inputId"
77
+ class="uds-input__field"
78
+ :type="variant"
79
+ :value="modelValue"
80
+ :placeholder="placeholder"
81
+ :required="required"
82
+ :disabled="state === 'disabled'"
83
+ :readonly="state === 'readonly'"
84
+ :aria-invalid="state === 'error' || undefined"
85
+ :aria-describedby="describedBy"
86
+ @input="handleInput"
87
+ />
88
+ <p v-if="errorText && state === 'error'" :id="errorId" class="uds-input__error" role="alert">
89
+ {{ errorText }}
90
+ </p>
91
+ <p v-if="helperText" :id="helperId" class="uds-input__helper">
92
+ {{ helperText }}
93
+ </p>
94
+ </div>
95
+ </template>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, onUnmounted, nextTick } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'confirmation' | 'task' | 'alert'
6
+ size?: 'sm' | 'md' | 'lg'
7
+ open?: boolean
8
+ title?: string
9
+ }
10
+
11
+ const props = withDefaults(defineProps<Props>(), {
12
+ variant: 'confirmation',
13
+ size: 'md',
14
+ })
15
+
16
+ const emit = defineEmits<{
17
+ close: []
18
+ }>()
19
+
20
+ const modalRef = ref<HTMLElement | null>(null)
21
+ let previousFocus: HTMLElement | null = null
22
+
23
+ const classes = computed(() =>
24
+ [
25
+ 'uds-modal',
26
+ `uds-modal--${props.variant}`,
27
+ `uds-modal--${props.size}`,
28
+ ]
29
+ .filter(Boolean)
30
+ .join(' ')
31
+ )
32
+
33
+ function getFocusableElements(): HTMLElement[] {
34
+ if (!modalRef.value) return []
35
+ return Array.from(
36
+ modalRef.value.querySelectorAll<HTMLElement>(
37
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
38
+ )
39
+ )
40
+ }
41
+
42
+ function handleKeydown(e: KeyboardEvent) {
43
+ if (e.key === 'Escape') {
44
+ emit('close')
45
+ return
46
+ }
47
+ if (e.key === 'Tab') {
48
+ const focusable = getFocusableElements()
49
+ if (!focusable.length) return
50
+ const first = focusable[0]
51
+ const last = focusable[focusable.length - 1]
52
+ if (e.shiftKey && document.activeElement === first) {
53
+ e.preventDefault()
54
+ last.focus()
55
+ } else if (!e.shiftKey && document.activeElement === last) {
56
+ e.preventDefault()
57
+ first.focus()
58
+ }
59
+ }
60
+ }
61
+
62
+ watch(() => props.open, async (isOpen) => {
63
+ if (isOpen) {
64
+ previousFocus = document.activeElement as HTMLElement
65
+ document.body.style.overflow = 'hidden'
66
+ document.addEventListener('keydown', handleKeydown)
67
+ await nextTick()
68
+ const focusable = getFocusableElements()
69
+ if (focusable.length) focusable[0].focus()
70
+ } else {
71
+ document.body.style.overflow = ''
72
+ document.removeEventListener('keydown', handleKeydown)
73
+ previousFocus?.focus()
74
+ }
75
+ })
76
+
77
+ onUnmounted(() => {
78
+ document.body.style.overflow = ''
79
+ document.removeEventListener('keydown', handleKeydown)
80
+ })
81
+
82
+ function handleOverlayClick(e: MouseEvent) {
83
+ if (e.target === e.currentTarget) emit('close')
84
+ }
85
+ </script>
86
+
87
+ <template>
88
+ <Teleport to="body">
89
+ <div v-if="open" class="uds-modal-overlay" role="presentation" @click="handleOverlayClick">
90
+ <div
91
+ ref="modalRef"
92
+ :class="classes"
93
+ role="dialog"
94
+ aria-modal="true"
95
+ :aria-label="title"
96
+ >
97
+ <div v-if="title" class="uds-modal__header">
98
+ <h2 class="uds-modal__title">{{ title }}</h2>
99
+ <button class="uds-modal__close" aria-label="Close dialog" @click="emit('close')">
100
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
101
+ <path d="M18 6L6 18M6 6l12 12" />
102
+ </svg>
103
+ </button>
104
+ </div>
105
+ <div class="uds-modal__body">
106
+ <slot />
107
+ </div>
108
+ <div class="uds-modal__footer">
109
+ <slot name="actions" />
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </Teleport>
114
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'standard' | 'minimal' | 'dark' | 'transparent'
6
+ sticky?: boolean
7
+ blurOnScroll?: boolean
8
+ megaMenu?: boolean
9
+ darkModeToggle?: boolean
10
+ ctaButton?: boolean
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ variant: 'standard',
15
+ })
16
+
17
+ const mobileOpen = ref(false)
18
+
19
+ const classes = computed(() =>
20
+ [
21
+ 'uds-navbar',
22
+ `uds-navbar--${props.variant}`,
23
+ props.sticky && 'uds-navbar--sticky',
24
+ mobileOpen.value && 'uds-navbar--mobile-open',
25
+ ]
26
+ .filter(Boolean)
27
+ .join(' ')
28
+ )
29
+
30
+ function toggleMobile() {
31
+ mobileOpen.value = !mobileOpen.value
32
+ }
33
+ </script>
34
+
35
+ <template>
36
+ <nav :class="classes" aria-label="Main navigation">
37
+ <div class="uds-navbar__brand">
38
+ <slot name="brand" />
39
+ </div>
40
+ <button
41
+ class="uds-navbar__toggle"
42
+ :aria-expanded="mobileOpen"
43
+ aria-label="Toggle navigation menu"
44
+ @click="toggleMobile"
45
+ >
46
+ <slot name="toggle-icon">
47
+ <span class="uds-navbar__hamburger" aria-hidden="true" />
48
+ </slot>
49
+ </button>
50
+ <div class="uds-navbar__menu" :hidden="!mobileOpen && undefined">
51
+ <slot />
52
+ </div>
53
+ <div v-if="ctaButton" class="uds-navbar__cta">
54
+ <slot name="cta" />
55
+ </div>
56
+ </nav>
57
+ </template>
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'numbered' | 'simple' | 'load-more' | 'infinite-scroll'
6
+ size?: 'sm' | 'md'
7
+ currentPage?: number
8
+ totalPages?: number
9
+ }
10
+
11
+ const props = withDefaults(defineProps<Props>(), {
12
+ variant: 'numbered',
13
+ size: 'md',
14
+ currentPage: 1,
15
+ totalPages: 1,
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ pageChange: [page: number]
20
+ }>()
21
+
22
+ const classes = computed(() =>
23
+ [
24
+ 'uds-pagination',
25
+ `uds-pagination--${props.variant}`,
26
+ `uds-pagination--${props.size}`,
27
+ ]
28
+ .filter(Boolean)
29
+ .join(' ')
30
+ )
31
+
32
+ const pages = computed(() => {
33
+ const result: (number | string)[] = []
34
+ const total = props.totalPages
35
+ const current = props.currentPage
36
+
37
+ if (total <= 7) {
38
+ for (let i = 1; i <= total; i++) result.push(i)
39
+ } else {
40
+ result.push(1)
41
+ if (current > 3) result.push('...')
42
+ const start = Math.max(2, current - 1)
43
+ const end = Math.min(total - 1, current + 1)
44
+ for (let i = start; i <= end; i++) result.push(i)
45
+ if (current < total - 2) result.push('...')
46
+ result.push(total)
47
+ }
48
+ return result
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <nav :class="classes" aria-label="Pagination">
54
+ <template v-if="variant === 'numbered' || variant === 'simple'">
55
+ <button
56
+ class="uds-pagination__prev"
57
+ :disabled="currentPage <= 1"
58
+ aria-label="Previous page"
59
+ @click="emit('pageChange', currentPage - 1)"
60
+ >
61
+ Previous
62
+ </button>
63
+ <ol v-if="variant === 'numbered'" class="uds-pagination__list">
64
+ <li v-for="(page, i) in pages" :key="i">
65
+ <span v-if="page === '...'" class="uds-pagination__ellipsis">...</span>
66
+ <button
67
+ v-else
68
+ class="uds-pagination__page"
69
+ :aria-current="page === currentPage ? 'page' : undefined"
70
+ :aria-label="`Page ${page}`"
71
+ @click="emit('pageChange', page as number)"
72
+ >
73
+ {{ page }}
74
+ </button>
75
+ </li>
76
+ </ol>
77
+ <button
78
+ class="uds-pagination__next"
79
+ :disabled="currentPage >= totalPages"
80
+ aria-label="Next page"
81
+ @click="emit('pageChange', currentPage + 1)"
82
+ >
83
+ Next
84
+ </button>
85
+ </template>
86
+ <template v-if="variant === 'load-more'">
87
+ <button
88
+ class="uds-pagination__load-more"
89
+ :disabled="currentPage >= totalPages"
90
+ @click="emit('pageChange', currentPage + 1)"
91
+ >
92
+ <slot>Load more</slot>
93
+ </button>
94
+ </template>
95
+ </nav>
96
+ </template>
@@ -0,0 +1,58 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ interface Plan {
5
+ name: string
6
+ price: string
7
+ annualPrice?: string
8
+ features: string[]
9
+ highlighted?: boolean
10
+ cta?: string
11
+ }
12
+
13
+ interface Props {
14
+ variant?: '2-column' | '3-column' | '4-column' | 'toggle'
15
+ size?: 'standard' | 'compact'
16
+ plans?: Plan[]
17
+ highlightedPlan?: string
18
+ }
19
+
20
+ const props = withDefaults(defineProps<Props>(), {
21
+ variant: '3-column',
22
+ size: 'standard',
23
+ plans: () => [],
24
+ })
25
+
26
+ const isAnnual = ref(false)
27
+
28
+ const classes = computed(() =>
29
+ [
30
+ 'uds-pricing',
31
+ `uds-pricing--${props.variant}`,
32
+ `uds-pricing--${props.size}`,
33
+ ]
34
+ .filter(Boolean)
35
+ .join(' ')
36
+ )
37
+ </script>
38
+
39
+ <template>
40
+ <div :class="classes">
41
+ <div v-if="variant === 'toggle'" class="uds-pricing__toggle">
42
+ <span>Monthly</span>
43
+ <button
44
+ role="switch"
45
+ :aria-checked="isAnnual"
46
+ aria-label="Toggle annual pricing"
47
+ class="uds-pricing__switch"
48
+ @click="isAnnual = !isAnnual"
49
+ >
50
+ <span class="uds-pricing__switch-thumb" />
51
+ </button>
52
+ <span>Annual</span>
53
+ </div>
54
+ <div class="uds-pricing__plans">
55
+ <slot :is-annual="isAnnual" />
56
+ </div>
57
+ </div>
58
+ </template>
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'bar' | 'circular' | 'stepper'
6
+ size?: 'sm' | 'md' | 'lg'
7
+ value?: number
8
+ max?: number
9
+ label?: string
10
+ showValue?: boolean
11
+ indeterminate?: boolean
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'bar',
16
+ size: 'md',
17
+ value: 0,
18
+ max: 100,
19
+ })
20
+
21
+ const classes = computed(() =>
22
+ [
23
+ 'uds-progress',
24
+ `uds-progress--${props.variant}`,
25
+ `uds-progress--${props.size}`,
26
+ props.indeterminate && 'uds-progress--indeterminate',
27
+ ]
28
+ .filter(Boolean)
29
+ .join(' ')
30
+ )
31
+
32
+ const percentage = computed(() =>
33
+ props.max > 0 ? Math.round((props.value / props.max) * 100) : 0
34
+ )
35
+
36
+ const circumference = computed(() => 2 * Math.PI * 40)
37
+ const strokeDashoffset = computed(() =>
38
+ circumference.value - (percentage.value / 100) * circumference.value
39
+ )
40
+ </script>
41
+
42
+ <template>
43
+ <div :class="classes">
44
+ <div v-if="label" class="uds-progress__label">{{ label }}</div>
45
+
46
+ <template v-if="variant === 'bar'">
47
+ <div
48
+ class="uds-progress__track"
49
+ role="progressbar"
50
+ :aria-valuenow="indeterminate ? undefined : value"
51
+ :aria-valuemin="0"
52
+ :aria-valuemax="max"
53
+ :aria-label="label"
54
+ >
55
+ <div
56
+ class="uds-progress__fill"
57
+ :style="indeterminate ? {} : { width: `${percentage}%` }"
58
+ />
59
+ </div>
60
+ </template>
61
+
62
+ <template v-else-if="variant === 'circular'">
63
+ <svg
64
+ class="uds-progress__circle"
65
+ viewBox="0 0 100 100"
66
+ role="progressbar"
67
+ :aria-valuenow="indeterminate ? undefined : value"
68
+ :aria-valuemin="0"
69
+ :aria-valuemax="max"
70
+ :aria-label="label"
71
+ >
72
+ <circle class="uds-progress__circle-track" cx="50" cy="50" r="40" fill="none" stroke-width="8" />
73
+ <circle
74
+ class="uds-progress__circle-fill"
75
+ cx="50" cy="50" r="40"
76
+ fill="none" stroke-width="8"
77
+ :stroke-dasharray="circumference"
78
+ :stroke-dashoffset="indeterminate ? circumference * 0.75 : strokeDashoffset"
79
+ stroke-linecap="round"
80
+ />
81
+ </svg>
82
+ </template>
83
+
84
+ <template v-else-if="variant === 'stepper'">
85
+ <div class="uds-progress__steps" role="progressbar" :aria-valuenow="value" :aria-valuemin="0" :aria-valuemax="max" :aria-label="label">
86
+ <slot />
87
+ </div>
88
+ </template>
89
+
90
+ <span v-if="showValue && !indeterminate" class="uds-progress__value">{{ percentage }}%</span>
91
+ </div>
92
+ </template>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ import { computed, useId } from 'vue'
3
+
4
+ interface Props {
5
+ variant?: 'standard' | 'card'
6
+ checked?: boolean
7
+ disabled?: boolean
8
+ label?: string
9
+ name?: string
10
+ value?: string
11
+ modelValue?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ variant: 'standard',
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ 'update:modelValue': [value: string]
20
+ }>()
21
+
22
+ const radioId = useId()
23
+
24
+ const classes = computed(() =>
25
+ [
26
+ 'uds-radio',
27
+ `uds-radio--${props.variant}`,
28
+ props.disabled && 'uds-radio--disabled',
29
+ ]
30
+ .filter(Boolean)
31
+ .join(' ')
32
+ )
33
+
34
+ function handleChange() {
35
+ if (props.value !== undefined) {
36
+ emit('update:modelValue', props.value)
37
+ }
38
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <div :class="classes">
43
+ <input
44
+ :id="radioId"
45
+ type="radio"
46
+ class="uds-radio__input"
47
+ :checked="modelValue !== undefined ? modelValue === value : checked"
48
+ :disabled="disabled"
49
+ :name="name"
50
+ :value="value"
51
+ :aria-checked="modelValue === value || checked || undefined"
52
+ @change="handleChange"
53
+ />
54
+ <label v-if="label" :for="radioId" class="uds-radio__label">{{ label }}</label>
55
+ </div>
56
+ </template>
@@ -0,0 +1,84 @@
1
+ <script setup lang="ts">
2
+ import { computed, useId } from 'vue'
3
+
4
+ interface Option {
5
+ value: string
6
+ label: string
7
+ disabled?: boolean
8
+ }
9
+
10
+ interface Props {
11
+ variant?: 'native' | 'custom'
12
+ size?: 'sm' | 'md' | 'lg'
13
+ options?: Option[]
14
+ placeholder?: string
15
+ required?: boolean
16
+ disabled?: boolean
17
+ state?: 'default' | 'focus' | 'error' | 'disabled'
18
+ label?: string
19
+ helperText?: string
20
+ errorText?: string
21
+ modelValue?: string
22
+ }
23
+
24
+ const props = withDefaults(defineProps<Props>(), {
25
+ variant: 'native',
26
+ size: 'md',
27
+ state: 'default',
28
+ options: () => [],
29
+ })
30
+
31
+ const emit = defineEmits<{
32
+ 'update:modelValue': [value: string]
33
+ }>()
34
+
35
+ const selectId = useId()
36
+ const errorId = useId()
37
+
38
+ const classes = computed(() =>
39
+ [
40
+ 'uds-select',
41
+ `uds-select--${props.variant}`,
42
+ `uds-select--${props.size}`,
43
+ props.state === 'error' && 'uds-select--error',
44
+ props.disabled && 'uds-select--disabled',
45
+ ]
46
+ .filter(Boolean)
47
+ .join(' ')
48
+ )
49
+
50
+ function handleChange(e: Event) {
51
+ const target = e.target as HTMLSelectElement
52
+ emit('update:modelValue', target.value)
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div :class="classes">
58
+ <label v-if="label" :for="selectId" class="uds-select__label">{{ label }}</label>
59
+ <select
60
+ :id="selectId"
61
+ class="uds-select__field"
62
+ :value="modelValue"
63
+ :required="required"
64
+ :disabled="disabled || state === 'disabled'"
65
+ :aria-invalid="state === 'error' || undefined"
66
+ :aria-describedby="errorText ? errorId : undefined"
67
+ @change="handleChange"
68
+ >
69
+ <option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
70
+ <option
71
+ v-for="opt in options"
72
+ :key="opt.value"
73
+ :value="opt.value"
74
+ :disabled="opt.disabled"
75
+ >
76
+ {{ opt.label }}
77
+ </option>
78
+ </select>
79
+ <p v-if="errorText && state === 'error'" :id="errorId" class="uds-select__error" role="alert">
80
+ {{ errorText }}
81
+ </p>
82
+ <p v-if="helperText" class="uds-select__helper">{{ helperText }}</p>
83
+ </div>
84
+ </template>