@mkatogui/uds-vue 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 +1 -1
- package/package.json +2 -2
- package/src/components/UdsBox.vue +28 -0
- package/src/components/UdsCarousel.vue +113 -0
- package/src/components/UdsChipInput.vue +105 -0
- package/src/components/UdsColorPicker.vue +47 -0
- package/src/components/UdsContainer.vue +22 -0
- package/src/components/UdsDivider.vue +29 -0
- package/src/components/UdsForm.vue +14 -0
- package/src/components/UdsGrid.vue +24 -0
- package/src/components/UdsLink.vue +28 -0
- package/src/components/UdsNumberInput.vue +76 -0
- package/src/components/UdsOTPInput.vue +73 -0
- package/src/components/UdsPopover.vue +85 -0
- package/src/components/UdsRating.vue +36 -0
- package/src/components/UdsSegmentedControl.vue +92 -0
- package/src/components/UdsSlider.vue +43 -0
- package/src/components/UdsStack.vue +37 -0
- package/src/components/UdsStepper.vue +85 -0
- package/src/components/UdsTimePicker.vue +29 -0
- package/src/components/UdsToolbar.vue +33 -0
- package/src/components/UdsTreeView.vue +82 -0
- package/src/components/UdsTypography.vue +36 -0
- package/src/index.ts +21 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mkatogui/uds-vue
|
|
2
2
|
|
|
3
|
-
Vue 3 Composition API components for the Universal Design System.
|
|
3
|
+
Vue 3 Composition API components for the Universal Design System. 43 accessible, themeable components built with `<script setup lang="ts">` and `defineProps`.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mkatogui/uds-vue",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Vue 3 components for Universal Design System —
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Vue 3 components for Universal Design System — 43 accessible, themeable components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
padding?: string | number
|
|
6
|
+
margin?: string | number
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = defineProps<Props>()
|
|
11
|
+
|
|
12
|
+
const classes = computed(() => ['uds-box', props.class].filter(Boolean).join(' '))
|
|
13
|
+
|
|
14
|
+
const style = computed(() => {
|
|
15
|
+
const s: Record<string, string> = {}
|
|
16
|
+
if (props.padding != null)
|
|
17
|
+
s.padding = typeof props.padding === 'number' ? `${props.padding}px` : `var(--space-${props.padding}, ${props.padding})`
|
|
18
|
+
if (props.margin != null)
|
|
19
|
+
s.margin = typeof props.margin === 'number' ? `${props.margin}px` : `var(--space-${props.margin}, ${props.margin})`
|
|
20
|
+
return s
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div :class="classes" :style="style">
|
|
26
|
+
<slot />
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onUnmounted } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
items: unknown[]
|
|
6
|
+
autoPlay?: boolean
|
|
7
|
+
interval?: number
|
|
8
|
+
showDots?: boolean
|
|
9
|
+
showArrows?: boolean
|
|
10
|
+
ariaLabel?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
autoPlay: false,
|
|
15
|
+
interval: 5000,
|
|
16
|
+
showDots: true,
|
|
17
|
+
showArrows: true,
|
|
18
|
+
ariaLabel: 'Content carousel',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
slideChange: [index: number]
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const current = ref(0)
|
|
26
|
+
const isPaused = ref(false)
|
|
27
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
28
|
+
|
|
29
|
+
function goTo(index: number) {
|
|
30
|
+
const next = Math.max(0, Math.min(index, props.items.length - 1))
|
|
31
|
+
current.value = next
|
|
32
|
+
emit('slideChange', next)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function next() {
|
|
36
|
+
goTo(current.value + 1)
|
|
37
|
+
}
|
|
38
|
+
function prev() {
|
|
39
|
+
goTo(current.value - 1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
watch([() => props.autoPlay, isPaused], ([autoPlay, paused]) => {
|
|
43
|
+
if (timer) clearInterval(timer)
|
|
44
|
+
if (autoPlay && !paused && props.items.length > 1) {
|
|
45
|
+
timer = setInterval(() => {
|
|
46
|
+
current.value = (current.value + 1) % props.items.length
|
|
47
|
+
emit('slideChange', current.value)
|
|
48
|
+
}, props.interval)
|
|
49
|
+
}
|
|
50
|
+
return () => { if (timer) clearInterval(timer) }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
onUnmounted(() => { if (timer) clearInterval(timer) })
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<section
|
|
58
|
+
v-if="items.length"
|
|
59
|
+
class="uds-carousel"
|
|
60
|
+
role="region"
|
|
61
|
+
aria-roledescription="carousel"
|
|
62
|
+
:aria-label="ariaLabel"
|
|
63
|
+
@focus="isPaused = true"
|
|
64
|
+
@blur="isPaused = false"
|
|
65
|
+
@mouseenter="isPaused = true"
|
|
66
|
+
@mouseleave="isPaused = false"
|
|
67
|
+
>
|
|
68
|
+
<div class="uds-carousel__track" :style="{ transform: `translateX(-${current * 100}%)` }">
|
|
69
|
+
<div
|
|
70
|
+
v-for="(item, i) in items"
|
|
71
|
+
:key="i"
|
|
72
|
+
class="uds-carousel__slide"
|
|
73
|
+
role="group"
|
|
74
|
+
aria-roledescription="slide"
|
|
75
|
+
:aria-label="`Slide ${i + 1} of ${items.length}`"
|
|
76
|
+
>
|
|
77
|
+
<slot name="item" :item="item" :index="i" />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<template v-if="showArrows && items.length > 1">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="uds-carousel__arrow uds-carousel__arrow--prev"
|
|
84
|
+
aria-label="Previous slide"
|
|
85
|
+
:disabled="current === 0"
|
|
86
|
+
@click="prev"
|
|
87
|
+
>
|
|
88
|
+
<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>
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
class="uds-carousel__arrow uds-carousel__arrow--next"
|
|
93
|
+
aria-label="Next slide"
|
|
94
|
+
:disabled="current === items.length - 1"
|
|
95
|
+
@click="next"
|
|
96
|
+
>
|
|
97
|
+
<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>
|
|
98
|
+
</button>
|
|
99
|
+
</template>
|
|
100
|
+
<div v-if="showDots && items.length > 1" class="uds-carousel__dots" role="tablist" aria-label="Slide indicators">
|
|
101
|
+
<button
|
|
102
|
+
v-for="(_, i) in items"
|
|
103
|
+
:key="i"
|
|
104
|
+
type="button"
|
|
105
|
+
role="tab"
|
|
106
|
+
:aria-selected="i === current"
|
|
107
|
+
:aria-label="`Slide ${i + 1}`"
|
|
108
|
+
:class="['uds-carousel__dot', i === current && 'uds-carousel__dot--active'].filter(Boolean).join(' ')"
|
|
109
|
+
@click="goTo(i)"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
</template>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
value?: string[]
|
|
6
|
+
defaultValue?: string[]
|
|
7
|
+
maxChips?: number
|
|
8
|
+
placeholder?: string
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
defaultValue: () => [],
|
|
14
|
+
placeholder: 'Add...',
|
|
15
|
+
disabled: false,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
change: [chips: string[]]
|
|
20
|
+
add: [chip: string]
|
|
21
|
+
remove: [index: number]
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const internalValue = ref<string[]>([...(props.defaultValue ?? [])])
|
|
25
|
+
const inputValue = ref('')
|
|
26
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
27
|
+
|
|
28
|
+
const chips = computed(() => props.value ?? internalValue.value)
|
|
29
|
+
|
|
30
|
+
watch(() => props.value, (v) => { if (v !== undefined) internalValue.value = [...v] })
|
|
31
|
+
|
|
32
|
+
const classes = computed(() =>
|
|
33
|
+
['uds-chip-input', props.disabled && 'uds-chip-input--disabled'].filter(Boolean).join(' ')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
function updateChips(next: string[]) {
|
|
37
|
+
if (props.value === undefined) internalValue.value = next
|
|
38
|
+
emit('change', next)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleAdd() {
|
|
42
|
+
const trimmed = inputValue.value.trim()
|
|
43
|
+
if (!trimmed || (props.maxChips != null && chips.value.length >= props.maxChips)) return
|
|
44
|
+
if (chips.value.includes(trimmed)) return
|
|
45
|
+
updateChips([...chips.value, trimmed])
|
|
46
|
+
emit('add', trimmed)
|
|
47
|
+
inputValue.value = ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleRemove(index: number) {
|
|
51
|
+
updateChips(chips.value.filter((_, i) => i !== index))
|
|
52
|
+
emit('remove', index)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
56
|
+
if (e.key === 'Enter') {
|
|
57
|
+
e.preventDefault()
|
|
58
|
+
handleAdd()
|
|
59
|
+
}
|
|
60
|
+
if (e.key === 'Backspace' && inputValue.value === '' && chips.value.length > 0) {
|
|
61
|
+
handleRemove(chips.value.length - 1)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div
|
|
68
|
+
:class="classes"
|
|
69
|
+
role="listbox"
|
|
70
|
+
aria-label="Chips"
|
|
71
|
+
:aria-disabled="disabled"
|
|
72
|
+
@click="inputRef?.focus()"
|
|
73
|
+
>
|
|
74
|
+
<span
|
|
75
|
+
v-for="(chip, index) in chips"
|
|
76
|
+
:key="`${chip}-${index}`"
|
|
77
|
+
class="uds-chip-input__chip"
|
|
78
|
+
role="option"
|
|
79
|
+
>
|
|
80
|
+
<span class="uds-chip-input__chip-label">{{ chip }}</span>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
class="uds-chip-input__chip-remove"
|
|
84
|
+
:aria-label="`Remove ${chip}`"
|
|
85
|
+
:disabled="disabled"
|
|
86
|
+
@click.stop="handleRemove(index)"
|
|
87
|
+
>
|
|
88
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
89
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
</span>
|
|
93
|
+
<input
|
|
94
|
+
v-if="maxChips == null || chips.length < maxChips"
|
|
95
|
+
ref="inputRef"
|
|
96
|
+
v-model="inputValue"
|
|
97
|
+
type="text"
|
|
98
|
+
class="uds-chip-input__input"
|
|
99
|
+
:placeholder="placeholder"
|
|
100
|
+
aria-label="Add chip"
|
|
101
|
+
:disabled="disabled"
|
|
102
|
+
@keydown="handleKeydown"
|
|
103
|
+
>
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
modelValue?: string
|
|
6
|
+
label?: string
|
|
7
|
+
showHexInput?: boolean
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
12
|
+
modelValue: '#000000',
|
|
13
|
+
showHexInput: true,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{ 'update:modelValue': [v: string] }>()
|
|
17
|
+
|
|
18
|
+
const id = 'uds-color-picker-' + Math.random().toString(36).slice(2, 9)
|
|
19
|
+
const classes = computed(() => ['uds-color-picker'].filter(Boolean).join(' '))
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div :class="classes">
|
|
24
|
+
<label v-if="label" :for="id" class="uds-color-picker__label">{{ label }}</label>
|
|
25
|
+
<div class="uds-color-picker__row">
|
|
26
|
+
<input
|
|
27
|
+
:id="id"
|
|
28
|
+
type="color"
|
|
29
|
+
:value="modelValue"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
class="uds-color-picker__swatch"
|
|
32
|
+
:aria-describedby="showHexInput ? `${id}-hex` : undefined"
|
|
33
|
+
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
34
|
+
>
|
|
35
|
+
<input
|
|
36
|
+
v-if="showHexInput"
|
|
37
|
+
:id="`${id}-hex`"
|
|
38
|
+
type="text"
|
|
39
|
+
:value="modelValue"
|
|
40
|
+
:disabled="disabled"
|
|
41
|
+
class="uds-color-picker__hex"
|
|
42
|
+
aria-label="Hex color"
|
|
43
|
+
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
44
|
+
>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
size?: 'sm' | 'md' | 'lg' | 'full'
|
|
6
|
+
class?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
10
|
+
size: 'lg',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const classes = computed(() =>
|
|
14
|
+
['uds-container', `uds-container--${props.size}`, props.class].filter(Boolean).join(' ')
|
|
15
|
+
)
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div :class="classes">
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
orientation?: 'horizontal' | 'vertical'
|
|
6
|
+
variant?: 'line' | 'dashed'
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
11
|
+
orientation: 'horizontal',
|
|
12
|
+
variant: 'line',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const classes = computed(() =>
|
|
16
|
+
[
|
|
17
|
+
'uds-divider',
|
|
18
|
+
`uds-divider--${props.orientation}`,
|
|
19
|
+
props.variant === 'dashed' && 'uds-divider--dashed',
|
|
20
|
+
props.class,
|
|
21
|
+
]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(' ')
|
|
24
|
+
)
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<hr :class="classes" role="separator" />
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
columns?: 1 | 2 | 3 | 4 | 12
|
|
6
|
+
gap?: 'sm' | 'md' | 'lg'
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
11
|
+
columns: 1,
|
|
12
|
+
gap: 'md',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const classes = computed(() =>
|
|
16
|
+
['uds-grid', `uds-grid--cols-${props.columns}`, `uds-grid--gap-${props.gap}`, props.class].filter(Boolean).join(' ')
|
|
17
|
+
)
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div :class="classes">
|
|
22
|
+
<slot />
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
href: string
|
|
6
|
+
variant?: 'default' | 'muted' | 'primary'
|
|
7
|
+
external?: boolean
|
|
8
|
+
class?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
12
|
+
variant: 'primary',
|
|
13
|
+
external: false,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const classes = computed(() =>
|
|
17
|
+
['uds-link', `uds-link--${props.variant}`, props.class].filter(Boolean).join(' ')
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const rel = computed(() => (props.external ? 'noopener noreferrer' : undefined))
|
|
21
|
+
const target = computed(() => (props.external ? '_blank' : undefined))
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<a :class="classes" :href="href" :rel="rel" :target="target">
|
|
26
|
+
<slot />
|
|
27
|
+
</a>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
modelValue?: number | string
|
|
6
|
+
min?: number
|
|
7
|
+
max?: number
|
|
8
|
+
step?: number
|
|
9
|
+
showStepper?: boolean
|
|
10
|
+
size?: 'sm' | 'md' | 'lg'
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
step: 1,
|
|
16
|
+
showStepper: false,
|
|
17
|
+
size: 'md',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{ 'update:modelValue': [v: number | string] }>()
|
|
21
|
+
|
|
22
|
+
const classes = computed(() =>
|
|
23
|
+
[
|
|
24
|
+
'uds-number-input',
|
|
25
|
+
`uds-number-input--${props.size}`,
|
|
26
|
+
props.showStepper && 'uds-number-input--stepper',
|
|
27
|
+
]
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join(' ')
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
function handleStep(delta: number) {
|
|
33
|
+
const next = (Number(props.modelValue) || 0) + delta
|
|
34
|
+
const clamped =
|
|
35
|
+
props.min != null && next < props.min ? props.min : props.max != null && next > props.max ? props.max : next
|
|
36
|
+
emit('update:modelValue', clamped)
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div :class="classes">
|
|
42
|
+
<button
|
|
43
|
+
v-if="showStepper"
|
|
44
|
+
type="button"
|
|
45
|
+
class="uds-number-input__stepper uds-number-input__stepper--minus"
|
|
46
|
+
aria-label="Decrease"
|
|
47
|
+
:disabled="disabled || (min != null && Number(modelValue) <= min)"
|
|
48
|
+
@click="handleStep(-step)"
|
|
49
|
+
>
|
|
50
|
+
−
|
|
51
|
+
</button>
|
|
52
|
+
<input
|
|
53
|
+
type="number"
|
|
54
|
+
:value="modelValue"
|
|
55
|
+
:min="min"
|
|
56
|
+
:max="max"
|
|
57
|
+
:step="step"
|
|
58
|
+
:disabled="disabled"
|
|
59
|
+
class="uds-number-input__input"
|
|
60
|
+
:aria-valuenow="modelValue != null ? Number(modelValue) : undefined"
|
|
61
|
+
:aria-valuemin="min"
|
|
62
|
+
:aria-valuemax="max"
|
|
63
|
+
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
64
|
+
>
|
|
65
|
+
<button
|
|
66
|
+
v-if="showStepper"
|
|
67
|
+
type="button"
|
|
68
|
+
class="uds-number-input__stepper uds-number-input__stepper--plus"
|
|
69
|
+
aria-label="Increase"
|
|
70
|
+
:disabled="disabled || (max != null && Number(modelValue) >= max)"
|
|
71
|
+
@click="handleStep(step)"
|
|
72
|
+
>
|
|
73
|
+
+
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
length?: 4 | 6
|
|
6
|
+
value?: string
|
|
7
|
+
defaultValue?: string
|
|
8
|
+
autoFocus?: boolean
|
|
9
|
+
inputMode?: 'numeric' | 'text'
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
length: 4,
|
|
15
|
+
defaultValue: '',
|
|
16
|
+
autoFocus: false,
|
|
17
|
+
inputMode: 'numeric',
|
|
18
|
+
disabled: false,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
change: [value: string]
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const internalValue = ref(props.defaultValue.slice(0, props.length))
|
|
26
|
+
const value = computed(() => (props.value ?? internalValue.value).slice(0, props.length).padEnd(props.length, ''))
|
|
27
|
+
|
|
28
|
+
watch(() => props.value, (v) => { if (v !== undefined) internalValue.value = v.slice(0, props.length) })
|
|
29
|
+
|
|
30
|
+
const classes = computed(() =>
|
|
31
|
+
['uds-otp-input', props.disabled && 'uds-otp-input--disabled'].filter(Boolean).join(' ')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
function setValue(next: string) {
|
|
35
|
+
const s = next.slice(0, props.length)
|
|
36
|
+
if (props.value === undefined) internalValue.value = s
|
|
37
|
+
emit('change', s)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handleChange(index: number, digit: string) {
|
|
41
|
+
const char = props.inputMode === 'numeric' ? digit.replace(/\D/g, '').slice(-1) : digit.slice(-1)
|
|
42
|
+
const arr = value.value.split('')
|
|
43
|
+
arr[index] = char
|
|
44
|
+
setValue(arr.join(''))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleKeydown(e: KeyboardEvent, index: number) {
|
|
48
|
+
if (e.key === 'Backspace' && value.value[index] === '' && index > 0) {
|
|
49
|
+
const inputs = document.querySelectorAll<HTMLInputElement>('.uds-otp-input__digit')
|
|
50
|
+
inputs[index - 1]?.focus()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div :class="classes" role="group" aria-label="One-time code">
|
|
57
|
+
<input
|
|
58
|
+
v-for="i in length"
|
|
59
|
+
:key="i"
|
|
60
|
+
type="text"
|
|
61
|
+
:inputmode="inputMode"
|
|
62
|
+
maxlength="1"
|
|
63
|
+
autocomplete="one-time-code"
|
|
64
|
+
:class="['uds-otp-input__digit']"
|
|
65
|
+
:value="value[i - 1] ?? ''"
|
|
66
|
+
:aria-label="`Digit ${i}`"
|
|
67
|
+
:disabled="disabled"
|
|
68
|
+
:autofocus="autoFocus && i === 1"
|
|
69
|
+
@input="(e: Event) => handleChange(i - 1, (e.target as HTMLInputElement).value)"
|
|
70
|
+
@keydown="(e: KeyboardEvent) => handleKeydown(e, i - 1)"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
open?: boolean
|
|
6
|
+
placement?: 'top' | 'bottom' | 'left' | 'right' | 'auto'
|
|
7
|
+
size?: 'sm' | 'md'
|
|
8
|
+
class?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
12
|
+
placement: 'bottom',
|
|
13
|
+
size: 'md',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
'open-change': [open: boolean]
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const internalOpen = ref(false)
|
|
21
|
+
const open = computed(() => props.open ?? internalOpen.value)
|
|
22
|
+
|
|
23
|
+
const contentRef = ref<HTMLElement | null>(null)
|
|
24
|
+
const triggerRef = ref<HTMLElement | null>(null)
|
|
25
|
+
const contentStyle = ref<Record<string, string>>({ position: 'fixed', left: '0', top: '0', zIndex: '9999' })
|
|
26
|
+
|
|
27
|
+
watch(open, (isOpen) => {
|
|
28
|
+
if (isOpen && triggerRef.value && contentRef.value) {
|
|
29
|
+
requestAnimationFrame(() => {
|
|
30
|
+
if (!triggerRef.value || !contentRef.value) return
|
|
31
|
+
const tr = triggerRef.value.getBoundingClientRect()
|
|
32
|
+
const cr = contentRef.value.getBoundingClientRect()
|
|
33
|
+
let top = tr.bottom + 8
|
|
34
|
+
let left = tr.left + (tr.width - cr.width) / 2
|
|
35
|
+
if (props.placement === 'top') {
|
|
36
|
+
top = tr.top - cr.height - 8
|
|
37
|
+
}
|
|
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.value = { position: 'fixed', left: `${left}px`, top: `${top}px`, zIndex: '9999' }
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const classes = computed(() =>
|
|
46
|
+
['uds-popover', `uds-popover--${props.size}`, `uds-popover--${props.placement}`, props.class].filter(Boolean).join(' ')
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
function setOpen(v: boolean) {
|
|
50
|
+
if (props.open === undefined) internalOpen.value = v
|
|
51
|
+
emit('open-change', v)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleEscape(e: KeyboardEvent) {
|
|
55
|
+
if (e.key === 'Escape') setOpen(false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
watch(open, (isOpen) => {
|
|
59
|
+
if (isOpen) document.addEventListener('keydown', handleEscape)
|
|
60
|
+
else document.removeEventListener('keydown', handleEscape)
|
|
61
|
+
return () => document.removeEventListener('keydown', handleEscape)
|
|
62
|
+
})
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<div class="uds-popover__wrapper">
|
|
67
|
+
<div ref="triggerRef" @click="setOpen(!open)">
|
|
68
|
+
<slot name="trigger" :open="open" />
|
|
69
|
+
</div>
|
|
70
|
+
<Teleport to="body">
|
|
71
|
+
<div
|
|
72
|
+
v-if="open"
|
|
73
|
+
ref="contentRef"
|
|
74
|
+
:class="classes"
|
|
75
|
+
role="dialog"
|
|
76
|
+
aria-modal="false"
|
|
77
|
+
:style="contentStyle"
|
|
78
|
+
>
|
|
79
|
+
<div class="uds-popover__content">
|
|
80
|
+
<slot />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</Teleport>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
modelValue?: number
|
|
6
|
+
max?: number
|
|
7
|
+
size?: 'sm' | 'md' | 'lg'
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
12
|
+
modelValue: 0,
|
|
13
|
+
max: 5,
|
|
14
|
+
size: 'md',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{ 'update:modelValue': [v: number] }>()
|
|
18
|
+
|
|
19
|
+
const classes = computed(() => ['uds-rating', `uds-rating--${props.size}`].filter(Boolean).join(' '))
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div :class="classes" role="group" :aria-label="`Rating ${modelValue} of ${max}`">
|
|
24
|
+
<button
|
|
25
|
+
v-for="i in max"
|
|
26
|
+
:key="i"
|
|
27
|
+
type="button"
|
|
28
|
+
:class="['uds-rating__star', modelValue >= i && 'uds-rating__star--filled']"
|
|
29
|
+
:aria-label="`${i} star${i > 1 ? 's' : ''}`"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
@click="emit('update:modelValue', i)"
|
|
32
|
+
>
|
|
33
|
+
<span aria-hidden="true">{{ modelValue >= i ? '★' : '☆' }}</span>
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface SegmentedControlOption {
|
|
5
|
+
value: string
|
|
6
|
+
label: string
|
|
7
|
+
icon?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
options: SegmentedControlOption[]
|
|
12
|
+
value?: string
|
|
13
|
+
defaultValue?: string
|
|
14
|
+
size?: 'sm' | 'md' | 'lg'
|
|
15
|
+
iconOnly?: boolean
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
20
|
+
size: 'md',
|
|
21
|
+
iconOnly: false,
|
|
22
|
+
disabled: false,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
change: [value: string]
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const internalValue = ref(props.defaultValue ?? props.options[0]?.value ?? '')
|
|
30
|
+
const value = computed(() => props.value ?? internalValue.value)
|
|
31
|
+
|
|
32
|
+
const classes = computed(() =>
|
|
33
|
+
[
|
|
34
|
+
'uds-segmented-control',
|
|
35
|
+
`uds-segmented-control--${props.size}`,
|
|
36
|
+
props.iconOnly && 'uds-segmented-control--icon-only',
|
|
37
|
+
props.disabled && 'uds-segmented-control--disabled',
|
|
38
|
+
]
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join(' ')
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
function select(optionValue: string) {
|
|
44
|
+
if (props.disabled) return
|
|
45
|
+
if (props.value === undefined) internalValue.value = optionValue
|
|
46
|
+
emit('change', optionValue)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleKeydown(e: KeyboardEvent, currentIndex: number) {
|
|
50
|
+
if (props.disabled) return
|
|
51
|
+
let nextIndex = currentIndex
|
|
52
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
53
|
+
e.preventDefault()
|
|
54
|
+
nextIndex = Math.max(0, currentIndex - 1)
|
|
55
|
+
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
56
|
+
e.preventDefault()
|
|
57
|
+
nextIndex = Math.min(props.options.length - 1, currentIndex + 1)
|
|
58
|
+
} else if (e.key === 'Home') {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
nextIndex = 0
|
|
61
|
+
} else if (e.key === 'End') {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
nextIndex = props.options.length - 1
|
|
64
|
+
} else return
|
|
65
|
+
const nextValue = props.options[nextIndex]?.value
|
|
66
|
+
if (nextValue != null) select(nextValue)
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div
|
|
72
|
+
:class="classes"
|
|
73
|
+
role="radiogroup"
|
|
74
|
+
aria-label="Options"
|
|
75
|
+
:aria-disabled="disabled"
|
|
76
|
+
>
|
|
77
|
+
<button
|
|
78
|
+
v-for="(option, index) in options"
|
|
79
|
+
:key="option.value"
|
|
80
|
+
type="button"
|
|
81
|
+
role="radio"
|
|
82
|
+
:aria-checked="value === option.value"
|
|
83
|
+
:disabled="disabled"
|
|
84
|
+
:class="['uds-segmented-control__option', value === option.value && 'uds-segmented-control__option--selected'].filter(Boolean).join(' ')"
|
|
85
|
+
@click="select(option.value)"
|
|
86
|
+
@keydown="(e: KeyboardEvent) => handleKeydown(e, index)"
|
|
87
|
+
>
|
|
88
|
+
<span v-if="option.icon" class="uds-segmented-control__icon" aria-hidden="true"><slot name="icon" :option="option" /></span>
|
|
89
|
+
<span v-if="!iconOnly" class="uds-segmented-control__label">{{ option.label }}</span>
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
modelValue?: number
|
|
6
|
+
min?: number
|
|
7
|
+
max?: number
|
|
8
|
+
step?: number
|
|
9
|
+
size?: 'sm' | 'md' | 'lg'
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
min: 0,
|
|
15
|
+
max: 100,
|
|
16
|
+
step: 1,
|
|
17
|
+
size: 'md',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{ 'update:modelValue': [v: number] }>()
|
|
21
|
+
|
|
22
|
+
const value = computed(() => props.modelValue ?? props.min)
|
|
23
|
+
const classes = computed(() => ['uds-slider', `uds-slider--${props.size}`].filter(Boolean).join(' '))
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div :class="classes">
|
|
28
|
+
<input
|
|
29
|
+
type="range"
|
|
30
|
+
:value="value"
|
|
31
|
+
:min="min"
|
|
32
|
+
:max="max"
|
|
33
|
+
:step="step"
|
|
34
|
+
:disabled="disabled"
|
|
35
|
+
role="slider"
|
|
36
|
+
:aria-valuemin="min"
|
|
37
|
+
:aria-valuemax="max"
|
|
38
|
+
:aria-valuenow="value"
|
|
39
|
+
class="uds-slider__input"
|
|
40
|
+
@input="emit('update:modelValue', Number(($event.target as HTMLInputElement).value))"
|
|
41
|
+
>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
direction?: 'row' | 'column'
|
|
6
|
+
gap?: 'sm' | 'md' | 'lg'
|
|
7
|
+
align?: 'start' | 'center' | 'end' | 'stretch'
|
|
8
|
+
wrap?: boolean
|
|
9
|
+
class?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
direction: 'column',
|
|
14
|
+
gap: 'md',
|
|
15
|
+
align: 'stretch',
|
|
16
|
+
wrap: false,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const classes = computed(() =>
|
|
20
|
+
[
|
|
21
|
+
'uds-stack',
|
|
22
|
+
`uds-stack--${props.direction}`,
|
|
23
|
+
`uds-stack--gap-${props.gap}`,
|
|
24
|
+
props.align !== 'stretch' && `uds-stack--align-${props.align}`,
|
|
25
|
+
props.wrap && 'uds-stack--wrap',
|
|
26
|
+
props.class,
|
|
27
|
+
]
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join(' ')
|
|
30
|
+
)
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div :class="classes">
|
|
35
|
+
<slot />
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface StepperStep {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
optional?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
steps: StepperStep[]
|
|
12
|
+
activeStep?: number
|
|
13
|
+
defaultActiveStep?: number
|
|
14
|
+
orientation?: 'horizontal' | 'vertical'
|
|
15
|
+
linear?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
19
|
+
defaultActiveStep: 0,
|
|
20
|
+
orientation: 'horizontal',
|
|
21
|
+
linear: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
change: [index: number]
|
|
26
|
+
stepClick: [index: number]
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const internalStep = ref(props.defaultActiveStep)
|
|
30
|
+
const activeStep = computed(() => props.activeStep ?? internalStep.value)
|
|
31
|
+
|
|
32
|
+
watch(
|
|
33
|
+
() => props.activeStep,
|
|
34
|
+
(v) => { if (v !== undefined) internalStep.value = v },
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const classes = computed(() =>
|
|
38
|
+
['uds-stepper', `uds-stepper--${props.orientation}`].filter(Boolean).join(' ')
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
function handleStepClick(index: number) {
|
|
42
|
+
if (props.linear && index > activeStep.value) return
|
|
43
|
+
if (props.activeStep === undefined) internalStep.value = index
|
|
44
|
+
emit('change', index)
|
|
45
|
+
emit('stepClick', index)
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<nav :class="classes" aria-label="Progress">
|
|
51
|
+
<template v-for="(step, index) in steps" :key="step.id">
|
|
52
|
+
<div
|
|
53
|
+
:class="[
|
|
54
|
+
'uds-stepper__step',
|
|
55
|
+
index < activeStep && 'uds-stepper__step--completed',
|
|
56
|
+
index === activeStep && 'uds-stepper__step--active',
|
|
57
|
+
index > activeStep && 'uds-stepper__step--pending',
|
|
58
|
+
].filter(Boolean).join(' ')"
|
|
59
|
+
:aria-current="index === activeStep ? 'step' : undefined"
|
|
60
|
+
:aria-disabled="index > activeStep && linear ? 'true' : undefined"
|
|
61
|
+
role="button"
|
|
62
|
+
:tabindex="!linear || index <= activeStep ? 0 : -1"
|
|
63
|
+
@click="(!linear || index <= activeStep) && handleStepClick(index)"
|
|
64
|
+
@keydown.enter.prevent="(!linear || index <= activeStep) && handleStepClick(index)"
|
|
65
|
+
@keydown.space.prevent="(!linear || index <= activeStep) && handleStepClick(index)"
|
|
66
|
+
>
|
|
67
|
+
<span class="uds-stepper__step-indicator">
|
|
68
|
+
<template v-if="index < activeStep">
|
|
69
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
70
|
+
<path d="M20 6L9 17l-5-5" />
|
|
71
|
+
</svg>
|
|
72
|
+
</template>
|
|
73
|
+
<template v-else>{{ index + 1 }}</template>
|
|
74
|
+
</span>
|
|
75
|
+
<span class="uds-stepper__step-label">{{ step.label }}</span>
|
|
76
|
+
<span v-if="step.optional" class="uds-stepper__step-optional">(optional)</span>
|
|
77
|
+
</div>
|
|
78
|
+
<span
|
|
79
|
+
v-if="index < steps.length - 1"
|
|
80
|
+
:class="['uds-stepper__connector', index < activeStep && 'uds-stepper__connector--completed'].filter(Boolean).join(' ')"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
/>
|
|
83
|
+
</template>
|
|
84
|
+
</nav>
|
|
85
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
modelValue?: string
|
|
6
|
+
size?: 'md' | 'lg'
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
11
|
+
modelValue: '',
|
|
12
|
+
size: 'md',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{ 'update:modelValue': [v: string] }>()
|
|
16
|
+
|
|
17
|
+
const classes = computed(() => ['uds-time-picker', `uds-time-picker--${props.size}`].filter(Boolean).join(' '))
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<input
|
|
22
|
+
type="time"
|
|
23
|
+
:value="modelValue"
|
|
24
|
+
:class="classes"
|
|
25
|
+
:disabled="disabled"
|
|
26
|
+
aria-label="Time"
|
|
27
|
+
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
28
|
+
>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ariaLabel: string
|
|
6
|
+
orientation?: 'horizontal' | 'vertical'
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
11
|
+
orientation: 'horizontal',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const classes = computed(() =>
|
|
15
|
+
[
|
|
16
|
+
'uds-toolbar',
|
|
17
|
+
props.orientation === 'vertical' && 'uds-toolbar--vertical',
|
|
18
|
+
props.class,
|
|
19
|
+
]
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join(' ')
|
|
22
|
+
)
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div
|
|
27
|
+
:class="classes"
|
|
28
|
+
role="toolbar"
|
|
29
|
+
:aria-label="ariaLabel"
|
|
30
|
+
>
|
|
31
|
+
<slot />
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface TreeNode {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
children?: TreeNode[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
nodes: TreeNode[]
|
|
12
|
+
selectedIds?: string | string[]
|
|
13
|
+
selectionMode?: 'single' | 'multi' | 'none'
|
|
14
|
+
defaultExpandedIds?: string[]
|
|
15
|
+
ariaLabel?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
19
|
+
selectionMode: 'single',
|
|
20
|
+
ariaLabel: 'Tree',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
select: [ids: string[]]
|
|
25
|
+
expand: [id: string, expanded: boolean]
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const expanded = ref<Set<string>>(new Set(props.defaultExpandedIds ?? []))
|
|
29
|
+
|
|
30
|
+
const selectedArray = computed(() =>
|
|
31
|
+
Array.isArray(props.selectedIds) ? props.selectedIds : props.selectedIds != null ? [props.selectedIds] : []
|
|
32
|
+
)
|
|
33
|
+
const selectedSet = computed(() => new Set(selectedArray.value))
|
|
34
|
+
|
|
35
|
+
function toggle(id: string, isExp: boolean) {
|
|
36
|
+
const next = new Set(expanded.value)
|
|
37
|
+
if (isExp) next.add(id)
|
|
38
|
+
else next.delete(id)
|
|
39
|
+
expanded.value = next
|
|
40
|
+
emit('expand', id, isExp)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleSelect(ids: string[]) {
|
|
44
|
+
emit('select', ids)
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="uds-tree" role="tree" :aria-label="ariaLabel" :aria-multiselectable="selectionMode === 'multi'">
|
|
50
|
+
<template v-for="node in nodes" :key="node.id">
|
|
51
|
+
<div
|
|
52
|
+
:class="[
|
|
53
|
+
'uds-tree__item',
|
|
54
|
+
node.children?.length && 'uds-tree__item--branch',
|
|
55
|
+
selectedSet.has(node.id) && 'uds-tree__item--selected',
|
|
56
|
+
].filter(Boolean).join(' ')"
|
|
57
|
+
role="treeitem"
|
|
58
|
+
:aria-expanded="node.children?.length ? expanded.has(node.id) : undefined"
|
|
59
|
+
aria-level="1"
|
|
60
|
+
:aria-selected="selectionMode !== 'none' ? selectedSet.has(node.id) : undefined"
|
|
61
|
+
tabindex="0"
|
|
62
|
+
@click="node.children?.length && toggle(node.id, !expanded.has(node.id)); selectionMode !== 'none' && handleSelect(selectionMode === 'single' ? [node.id] : selectedSet.has(node.id) ? [...selectedArray].filter((x) => x !== node.id) : [...selectedArray, node.id])"
|
|
63
|
+
>
|
|
64
|
+
<span class="uds-tree__item-label">{{ node.label }}</span>
|
|
65
|
+
<div v-if="node.children?.length && expanded.has(node.id)" role="group" class="uds-tree__group">
|
|
66
|
+
<div
|
|
67
|
+
v-for="child in node.children"
|
|
68
|
+
:key="child.id"
|
|
69
|
+
:class="['uds-tree__item', selectedSet.has(child.id) && 'uds-tree__item--selected'].filter(Boolean).join(' ')"
|
|
70
|
+
role="treeitem"
|
|
71
|
+
aria-level="2"
|
|
72
|
+
:aria-selected="selectionMode !== 'none' ? selectedSet.has(child.id) : undefined"
|
|
73
|
+
tabindex="0"
|
|
74
|
+
@click="selectionMode !== 'none' && handleSelect(selectionMode === 'single' ? [child.id] : [])"
|
|
75
|
+
>
|
|
76
|
+
<span class="uds-tree__item-label">{{ child.label }}</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
type Variant = 'h1' | 'h2' | 'h3' | 'body' | 'caption' | 'code'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
variant?: Variant
|
|
8
|
+
class?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
12
|
+
variant: 'body',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const tag = computed(() => {
|
|
16
|
+
const map: Record<Variant, string> = {
|
|
17
|
+
h1: 'h1',
|
|
18
|
+
h2: 'h2',
|
|
19
|
+
h3: 'h3',
|
|
20
|
+
body: 'p',
|
|
21
|
+
caption: 'span',
|
|
22
|
+
code: 'code',
|
|
23
|
+
}
|
|
24
|
+
return map[props.variant]
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const classes = computed(() =>
|
|
28
|
+
['uds-typography', `uds-typography--${props.variant}`, props.class].filter(Boolean).join(' ')
|
|
29
|
+
)
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<component :is="tag" :class="classes">
|
|
34
|
+
<slot />
|
|
35
|
+
</component>
|
|
36
|
+
</template>
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,13 @@ export { default as UdsRadio } from './components/UdsRadio.vue'
|
|
|
15
15
|
export { default as UdsToggle } from './components/UdsToggle.vue'
|
|
16
16
|
export { default as UdsAlert } from './components/UdsAlert.vue'
|
|
17
17
|
export { default as UdsBadge } from './components/UdsBadge.vue'
|
|
18
|
+
export { default as UdsBox } from './components/UdsBox.vue'
|
|
19
|
+
export { default as UdsContainer } from './components/UdsContainer.vue'
|
|
20
|
+
export { default as UdsDivider } from './components/UdsDivider.vue'
|
|
21
|
+
export { default as UdsGrid } from './components/UdsGrid.vue'
|
|
22
|
+
export { default as UdsLink } from './components/UdsLink.vue'
|
|
23
|
+
export { default as UdsStack } from './components/UdsStack.vue'
|
|
24
|
+
export { default as UdsTypography } from './components/UdsTypography.vue'
|
|
18
25
|
export { default as UdsTabs } from './components/UdsTabs.vue'
|
|
19
26
|
export { default as UdsAccordion } from './components/UdsAccordion.vue'
|
|
20
27
|
export { default as UdsBreadcrumb } from './components/UdsBreadcrumb.vue'
|
|
@@ -30,3 +37,17 @@ export { default as UdsCommandPalette } from './components/UdsCommandPalette.vue
|
|
|
30
37
|
export { default as UdsProgress } from './components/UdsProgress.vue'
|
|
31
38
|
export { default as UdsSideNav } from './components/UdsSideNav.vue'
|
|
32
39
|
export { default as UdsFileUpload } from './components/UdsFileUpload.vue'
|
|
40
|
+
export { default as UdsToolbar } from './components/UdsToolbar.vue'
|
|
41
|
+
export { default as UdsStepper } from './components/UdsStepper.vue'
|
|
42
|
+
export { default as UdsSegmentedControl } from './components/UdsSegmentedControl.vue'
|
|
43
|
+
export { default as UdsOTPInput } from './components/UdsOTPInput.vue'
|
|
44
|
+
export { default as UdsChipInput } from './components/UdsChipInput.vue'
|
|
45
|
+
export { default as UdsColorPicker } from './components/UdsColorPicker.vue'
|
|
46
|
+
export { default as UdsForm } from './components/UdsForm.vue'
|
|
47
|
+
export { default as UdsNumberInput } from './components/UdsNumberInput.vue'
|
|
48
|
+
export { default as UdsPopover } from './components/UdsPopover.vue'
|
|
49
|
+
export { default as UdsRating } from './components/UdsRating.vue'
|
|
50
|
+
export { default as UdsSlider } from './components/UdsSlider.vue'
|
|
51
|
+
export { default as UdsTimePicker } from './components/UdsTimePicker.vue'
|
|
52
|
+
export { default as UdsCarousel } from './components/UdsCarousel.vue'
|
|
53
|
+
export { default as UdsTreeView } from './components/UdsTreeView.vue'
|