@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.
- package/README.md +102 -0
- package/package.json +27 -0
- package/src/components/UdsAccordion.vue +82 -0
- package/src/components/UdsAlert.vue +61 -0
- package/src/components/UdsAvatar.vue +59 -0
- package/src/components/UdsBadge.vue +48 -0
- package/src/components/UdsBreadcrumb.vue +73 -0
- package/src/components/UdsButton.vue +43 -0
- package/src/components/UdsCard.vue +49 -0
- package/src/components/UdsCheckbox.vue +56 -0
- package/src/components/UdsCodeBlock.vue +86 -0
- package/src/components/UdsCommandPalette.vue +144 -0
- package/src/components/UdsDataTable.vue +142 -0
- package/src/components/UdsDatePicker.vue +69 -0
- package/src/components/UdsDropdown.vue +132 -0
- package/src/components/UdsFileUpload.vue +148 -0
- package/src/components/UdsFooter.vue +39 -0
- package/src/components/UdsHero.vue +44 -0
- package/src/components/UdsInput.vue +95 -0
- package/src/components/UdsModal.vue +114 -0
- package/src/components/UdsNavbar.vue +57 -0
- package/src/components/UdsPagination.vue +96 -0
- package/src/components/UdsPricing.vue +58 -0
- package/src/components/UdsProgress.vue +92 -0
- package/src/components/UdsRadio.vue +56 -0
- package/src/components/UdsSelect.vue +84 -0
- package/src/components/UdsSideNav.vue +102 -0
- package/src/components/UdsSkeleton.vue +51 -0
- package/src/components/UdsSocialProof.vue +58 -0
- package/src/components/UdsTabs.vue +106 -0
- package/src/components/UdsTestimonial.vue +57 -0
- package/src/components/UdsToast.vue +70 -0
- package/src/components/UdsToggle.vue +60 -0
- package/src/components/UdsTooltip.vue +62 -0
- 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>
|