@revojs/ui 0.2.7 → 0.2.9
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/dist/components/button.d.ts +4 -3
- package/dist/components/icon.d.ts +2 -1
- package/dist/components/input.d.ts +3 -2
- package/dist/components/navigate.d.ts +3 -2
- package/dist/module/icons.d.ts +2 -0
- package/dist/module/icons.js +12 -0
- package/dist/module/index.d.ts +1 -1
- package/dist/module/index.js +37 -47
- package/dist/types/index.d.ts +0 -7
- package/dist/vue/components/breadcrumb.vue +2 -4
- package/dist/vue/components/button.vue +26 -26
- package/dist/vue/components/card.vue +7 -10
- package/dist/vue/components/checkbox.vue +17 -12
- package/dist/vue/components/collection.vue +2 -10
- package/dist/vue/components/container.vue +2 -4
- package/dist/vue/components/counter.vue +7 -11
- package/dist/vue/components/dialog.vue +4 -18
- package/dist/vue/components/field.vue +3 -13
- package/dist/vue/components/heading.vue +7 -8
- package/dist/vue/components/icon.vue +3 -41
- package/dist/vue/components/input.vue +9 -19
- package/dist/vue/components/link.vue +8 -9
- package/dist/vue/components/navigate.vue +3 -14
- package/dist/vue/components/popover.vue +3 -11
- package/dist/vue/components/progress.vue +3 -10
- package/dist/vue/components/radio.vue +12 -20
- package/dist/vue/components/range.vue +99 -29
- package/dist/vue/components/rating.vue +6 -17
- package/dist/vue/components/select.vue +84 -61
- package/dist/vue/components/separator.vue +2 -10
- package/dist/vue/components/sheet.vue +17 -21
- package/dist/vue/components/switch.vue +6 -21
- package/dist/vue/components/upload.vue +2 -18
- package/package.json +5 -4
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
<slot name="leading" />
|
|
7
7
|
|
|
8
|
-
<Icon v-if="iconPrefix" :
|
|
8
|
+
<Icon v-if="iconPrefix" :is="iconPrefix" />
|
|
9
9
|
|
|
10
10
|
<input
|
|
11
11
|
class="w-full min-w-0 appearance-none bg-transparent py-2 outline-none placeholder:opacity-50"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
@input="input"
|
|
21
21
|
/>
|
|
22
22
|
|
|
23
|
-
<Icon v-if="iconSuffix" :
|
|
23
|
+
<Icon v-if="iconSuffix" :is="iconSuffix" />
|
|
24
24
|
|
|
25
25
|
<slot name="trailing" />
|
|
26
26
|
</span>
|
|
@@ -29,31 +29,21 @@
|
|
|
29
29
|
<script setup lang="ts">
|
|
30
30
|
import { computed } from "vue";
|
|
31
31
|
import type { ControlInteraction } from "../../components/control.ts";
|
|
32
|
+
import type { InputProps } from "../../components/input";
|
|
32
33
|
import Icon from "./icon.vue";
|
|
33
34
|
|
|
34
35
|
const {
|
|
35
|
-
modelValue
|
|
36
|
+
modelValue,
|
|
36
37
|
type = "text",
|
|
37
|
-
iconPrefix
|
|
38
|
-
iconSuffix
|
|
39
|
-
placeholder
|
|
40
|
-
name
|
|
38
|
+
iconPrefix,
|
|
39
|
+
iconSuffix,
|
|
40
|
+
placeholder,
|
|
41
|
+
name,
|
|
41
42
|
spellcheck = undefined,
|
|
42
43
|
required = false,
|
|
43
44
|
readonly = false,
|
|
44
45
|
disabled = false,
|
|
45
|
-
} = defineProps<
|
|
46
|
-
modelValue?: string | number;
|
|
47
|
-
type?: string;
|
|
48
|
-
iconPrefix?: string;
|
|
49
|
-
iconSuffix?: string;
|
|
50
|
-
placeholder?: string;
|
|
51
|
-
name?: string;
|
|
52
|
-
spellcheck?: boolean;
|
|
53
|
-
required?: boolean;
|
|
54
|
-
readonly?: boolean;
|
|
55
|
-
disabled?: boolean;
|
|
56
|
-
}>();
|
|
46
|
+
} = defineProps<InputProps>();
|
|
57
47
|
|
|
58
48
|
const emit = defineEmits<{
|
|
59
49
|
"update:modelValue": [value: string | number];
|
|
@@ -11,19 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
<script setup lang="ts">
|
|
13
13
|
import type { Component } from "vue";
|
|
14
|
-
import type { LinkVariant } from "../../components/link
|
|
14
|
+
import type { LinkProps, LinkVariant } from "../../components/link";
|
|
15
15
|
|
|
16
16
|
const {
|
|
17
17
|
is = "a",
|
|
18
|
-
href
|
|
19
|
-
label
|
|
18
|
+
href,
|
|
19
|
+
label,
|
|
20
20
|
variant = "primary",
|
|
21
|
-
} = defineProps<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}>();
|
|
21
|
+
} = defineProps<
|
|
22
|
+
LinkProps & {
|
|
23
|
+
is?: string | Component;
|
|
24
|
+
}
|
|
25
|
+
>();
|
|
27
26
|
|
|
28
27
|
const variants: Record<LinkVariant, string> = {
|
|
29
28
|
primary: "text-foreground",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
:href="href"
|
|
5
5
|
font="normal"
|
|
6
6
|
layout="left"
|
|
7
|
-
:variant="active ? 'secondary' : '
|
|
7
|
+
:variant="active ? 'secondary' : 'ghost'"
|
|
8
8
|
:icon-prefix="iconPrefix"
|
|
9
9
|
:icon-suffix="iconSuffix"
|
|
10
10
|
:label="label"
|
|
@@ -12,19 +12,8 @@
|
|
|
12
12
|
</template>
|
|
13
13
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
|
+
import type { NavigateProps } from "../../components/navigate";
|
|
15
16
|
import Button from "./button.vue";
|
|
16
17
|
|
|
17
|
-
const {
|
|
18
|
-
href,
|
|
19
|
-
label = undefined,
|
|
20
|
-
iconPrefix = undefined,
|
|
21
|
-
iconSuffix = undefined,
|
|
22
|
-
active = false,
|
|
23
|
-
} = defineProps<{
|
|
24
|
-
href: string;
|
|
25
|
-
label?: string;
|
|
26
|
-
iconPrefix?: string;
|
|
27
|
-
iconSuffix?: string;
|
|
28
|
-
active?: boolean;
|
|
29
|
-
}>();
|
|
18
|
+
const { href, label, iconPrefix, iconSuffix, active = false } = defineProps<NavigateProps>();
|
|
30
19
|
</script>
|
|
@@ -26,26 +26,18 @@
|
|
|
26
26
|
|
|
27
27
|
<script setup lang="ts">
|
|
28
28
|
import { ref, useId, useTemplateRef, watchEffect } from "vue";
|
|
29
|
-
import type {
|
|
29
|
+
import type { PopoverProps } from "../../components/popover";
|
|
30
30
|
import Button from "./button.vue";
|
|
31
31
|
|
|
32
32
|
const {
|
|
33
33
|
modelValue = false,
|
|
34
34
|
triggerLabel = "Open",
|
|
35
|
-
triggerClass
|
|
35
|
+
triggerClass,
|
|
36
36
|
triggerLayout = "center",
|
|
37
37
|
contentClass = "p-4",
|
|
38
38
|
matchTriggerWidth = false,
|
|
39
39
|
disabled = false,
|
|
40
|
-
} = defineProps<
|
|
41
|
-
modelValue?: boolean;
|
|
42
|
-
triggerLabel?: string;
|
|
43
|
-
triggerClass?: string;
|
|
44
|
-
triggerLayout?: ButtonLayout;
|
|
45
|
-
contentClass?: string;
|
|
46
|
-
matchTriggerWidth?: boolean;
|
|
47
|
-
disabled?: boolean;
|
|
48
|
-
}>();
|
|
40
|
+
} = defineProps<PopoverProps>();
|
|
49
41
|
|
|
50
42
|
const emit = defineEmits<{
|
|
51
43
|
"update:modelValue": [value: boolean];
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<progress class="sr-only" :value="value" :max="max">{{ percent }}%</progress>
|
|
10
10
|
|
|
11
11
|
<div
|
|
12
|
-
class="h-2 w-full overflow-hidden rounded-full bg-
|
|
12
|
+
class="h-2 w-full overflow-hidden rounded-full bg-input"
|
|
13
13
|
role="progressbar"
|
|
14
14
|
:aria-label="label"
|
|
15
15
|
:aria-valuenow="value"
|
|
@@ -23,16 +23,9 @@
|
|
|
23
23
|
|
|
24
24
|
<script setup lang="ts">
|
|
25
25
|
import { computed } from "vue";
|
|
26
|
+
import type { ProgressProps } from "../../components/progress";
|
|
26
27
|
|
|
27
|
-
const {
|
|
28
|
-
value = 0,
|
|
29
|
-
max = 100,
|
|
30
|
-
label = undefined,
|
|
31
|
-
} = defineProps<{
|
|
32
|
-
value?: number;
|
|
33
|
-
max?: number;
|
|
34
|
-
label?: string;
|
|
35
|
-
}>();
|
|
28
|
+
const { value = 0, max = 100, label } = defineProps<ProgressProps>();
|
|
36
29
|
|
|
37
30
|
const percent = computed(() => (max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0));
|
|
38
31
|
</script>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<label class="inline-flex items-start gap-3" :class="interactionVariants[interaction]">
|
|
3
3
|
<input
|
|
4
|
-
class="
|
|
4
|
+
class="peer sr-only"
|
|
5
5
|
type="radio"
|
|
6
6
|
:checked="modelValue === value"
|
|
7
7
|
:value="value"
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
@change="$emit('update:modelValue', value)"
|
|
12
12
|
/>
|
|
13
13
|
|
|
14
|
+
<span
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
class="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring"
|
|
17
|
+
:class="modelValue === value ? 'border-primary bg-background' : 'border-input bg-background'"
|
|
18
|
+
>
|
|
19
|
+
<span v-if="modelValue === value" class="size-2 rounded-full bg-primary" />
|
|
20
|
+
</span>
|
|
21
|
+
|
|
14
22
|
<span class="flex flex-col gap-0.5">
|
|
15
23
|
<span class="text-sm font-medium">
|
|
16
24
|
<slot> {{ label }}</slot>
|
|
@@ -24,25 +32,9 @@
|
|
|
24
32
|
<script setup lang="ts">
|
|
25
33
|
import { computed } from "vue";
|
|
26
34
|
import type { ControlInteraction } from "../../components/control.ts";
|
|
27
|
-
import type { RadioValue } from "../../components/radio
|
|
28
|
-
|
|
29
|
-
const {
|
|
30
|
-
modelValue = undefined,
|
|
31
|
-
value,
|
|
32
|
-
label = undefined,
|
|
33
|
-
description = undefined,
|
|
34
|
-
name = undefined,
|
|
35
|
-
required = false,
|
|
36
|
-
disabled = false,
|
|
37
|
-
} = defineProps<{
|
|
38
|
-
modelValue?: RadioValue;
|
|
39
|
-
value: RadioValue;
|
|
40
|
-
label?: string;
|
|
41
|
-
description?: string;
|
|
42
|
-
name?: string;
|
|
43
|
-
required?: boolean;
|
|
44
|
-
disabled?: boolean;
|
|
45
|
-
}>();
|
|
35
|
+
import type { RadioProps, RadioValue } from "../../components/radio";
|
|
36
|
+
|
|
37
|
+
const { modelValue, value, label, description, name, required = false, disabled = false } = defineProps<RadioProps>();
|
|
46
38
|
|
|
47
39
|
defineEmits<{
|
|
48
40
|
"update:modelValue": [value: RadioValue];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<div class="flex flex-col gap-2" :class="interactionVariants[interaction]">
|
|
3
3
|
<span v-if="label" class="flex items-center justify-between gap-4 text-sm">
|
|
4
4
|
<span class="font-medium"> {{ label }} </span>
|
|
5
5
|
|
|
@@ -8,40 +8,41 @@
|
|
|
8
8
|
</output>
|
|
9
9
|
</span>
|
|
10
10
|
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
:
|
|
15
|
-
|
|
16
|
-
:
|
|
17
|
-
:
|
|
18
|
-
:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
<div
|
|
12
|
+
ref="track"
|
|
13
|
+
class="relative h-2 w-full touch-none rounded-full bg-input outline-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
14
|
+
:class="disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'"
|
|
15
|
+
role="slider"
|
|
16
|
+
:tabindex="disabled ? -1 : 0"
|
|
17
|
+
:aria-valuemin="min"
|
|
18
|
+
:aria-valuemax="max"
|
|
19
|
+
:aria-valuenow="modelValue"
|
|
20
|
+
:aria-label="label"
|
|
21
|
+
:aria-disabled="disabled"
|
|
22
|
+
@pointerdown="onPointerDown"
|
|
23
|
+
@pointermove="onPointerMove"
|
|
24
|
+
@pointerup="onPointerUp"
|
|
25
|
+
@pointercancel="onPointerUp"
|
|
26
|
+
@keydown="onKeyDown"
|
|
27
|
+
>
|
|
28
|
+
<div class="absolute inset-y-0 left-0 rounded-full bg-primary" :style="{ width: `${percent}%` }" />
|
|
29
|
+
|
|
30
|
+
<span
|
|
31
|
+
class="absolute top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-primary bg-background shadow"
|
|
32
|
+
:style="{ left: `${percent}%` }"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
22
36
|
</template>
|
|
23
37
|
|
|
24
38
|
<script setup lang="ts">
|
|
25
|
-
import { computed } from "vue";
|
|
39
|
+
import { computed, useTemplateRef } from "vue";
|
|
26
40
|
import type { ControlInteraction } from "../../components/control.ts";
|
|
41
|
+
import type { RangeProps } from "../../components/range";
|
|
27
42
|
|
|
28
|
-
const {
|
|
29
|
-
modelValue = 0,
|
|
30
|
-
label = undefined,
|
|
31
|
-
min = 0,
|
|
32
|
-
max = 100,
|
|
33
|
-
step = 1,
|
|
34
|
-
disabled = false,
|
|
35
|
-
} = defineProps<{
|
|
36
|
-
modelValue?: number;
|
|
37
|
-
label?: string;
|
|
38
|
-
min?: number;
|
|
39
|
-
max?: number;
|
|
40
|
-
step?: number;
|
|
41
|
-
disabled?: boolean;
|
|
42
|
-
}>();
|
|
43
|
+
const { modelValue = 0, label, min = 0, max = 100, step = 1, disabled = false } = defineProps<RangeProps>();
|
|
43
44
|
|
|
44
|
-
defineEmits<{
|
|
45
|
+
const emit = defineEmits<{
|
|
45
46
|
"update:modelValue": [value: number];
|
|
46
47
|
}>();
|
|
47
48
|
|
|
@@ -51,4 +52,73 @@ const interactionVariants: Record<ControlInteraction, string> = {
|
|
|
51
52
|
disabled: "cursor-not-allowed opacity-50",
|
|
52
53
|
enabled: "",
|
|
53
54
|
};
|
|
55
|
+
|
|
56
|
+
const track = useTemplateRef("track");
|
|
57
|
+
|
|
58
|
+
const percent = computed(() => (max > min ? Math.min(100, Math.max(0, ((modelValue - min) / (max - min)) * 100)) : 0));
|
|
59
|
+
|
|
60
|
+
let dragging = false;
|
|
61
|
+
|
|
62
|
+
function commit(raw: number) {
|
|
63
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
64
|
+
const snapped = min + Math.round((raw - min) / step) * step;
|
|
65
|
+
const rounded = Number(snapped.toFixed(decimals));
|
|
66
|
+
const value = Math.min(max, Math.max(min, rounded));
|
|
67
|
+
|
|
68
|
+
if (value !== modelValue) {
|
|
69
|
+
emit("update:modelValue", value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setValueFromClientX(clientX: number) {
|
|
74
|
+
if (!track.value) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rect = track.value.getBoundingClientRect();
|
|
79
|
+
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
80
|
+
|
|
81
|
+
commit(min + ratio * (max - min));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onPointerDown(event: PointerEvent) {
|
|
85
|
+
if (disabled || !track.value) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
dragging = true;
|
|
90
|
+
track.value.setPointerCapture(event.pointerId);
|
|
91
|
+
track.value.focus({ preventScroll: true });
|
|
92
|
+
setValueFromClientX(event.clientX);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onPointerMove(event: PointerEvent) {
|
|
96
|
+
if (dragging) {
|
|
97
|
+
setValueFromClientX(event.clientX);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function onPointerUp() {
|
|
102
|
+
dragging = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
106
|
+
if (disabled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (event.key === "ArrowRight" || event.key === "ArrowUp") {
|
|
111
|
+
commit(modelValue + step);
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
|
|
114
|
+
commit(modelValue - step);
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
} else if (event.key === "Home") {
|
|
117
|
+
commit(min);
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
} else if (event.key === "End") {
|
|
120
|
+
commit(max);
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
54
124
|
</script>
|
|
@@ -16,30 +16,19 @@
|
|
|
16
16
|
:disabled="readonly || disabled"
|
|
17
17
|
@click="$emit('update:modelValue', value)"
|
|
18
18
|
>
|
|
19
|
-
<Icon
|
|
19
|
+
<Icon :is="StarIcon" />
|
|
20
20
|
</Button>
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
</template>
|
|
24
24
|
|
|
25
25
|
<script setup lang="ts">
|
|
26
|
+
import StarIcon from "~icons/hugeicons/star";
|
|
26
27
|
import Button from "./button.vue";
|
|
27
28
|
import Icon from "./icon.vue";
|
|
28
|
-
import type { RatingState } from "../../components/rating
|
|
29
|
+
import type { RatingProps, RatingState } from "../../components/rating";
|
|
29
30
|
|
|
30
|
-
const {
|
|
31
|
-
modelValue = 0,
|
|
32
|
-
max = 5,
|
|
33
|
-
label = undefined,
|
|
34
|
-
readonly = false,
|
|
35
|
-
disabled = false,
|
|
36
|
-
} = defineProps<{
|
|
37
|
-
modelValue?: number;
|
|
38
|
-
max?: number;
|
|
39
|
-
label?: string;
|
|
40
|
-
readonly?: boolean;
|
|
41
|
-
disabled?: boolean;
|
|
42
|
-
}>();
|
|
31
|
+
const { modelValue = 0, max = 5, label, readonly = false, disabled = false } = defineProps<RatingProps>();
|
|
43
32
|
|
|
44
33
|
defineEmits<{
|
|
45
34
|
"update:modelValue": [value: number];
|
|
@@ -50,7 +39,7 @@ function state(value: number): RatingState {
|
|
|
50
39
|
}
|
|
51
40
|
|
|
52
41
|
const variants: Record<RatingState, string> = {
|
|
53
|
-
active: "text-
|
|
54
|
-
inactive: "text-
|
|
42
|
+
active: "text-primary",
|
|
43
|
+
inactive: "text-input",
|
|
55
44
|
};
|
|
56
45
|
</script>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<input v-if="name" type="hidden" :name="name" :value="modelValue" :required="required" />
|
|
8
8
|
|
|
9
|
-
<div ref="container">
|
|
9
|
+
<div ref="container" class="relative">
|
|
10
10
|
<div
|
|
11
11
|
ref="trigger"
|
|
12
12
|
class="flex min-h-10 w-full cursor-text items-center gap-2 rounded-ui border border-input px-3 text-sm ring-ring focus-within:ring-2"
|
|
@@ -38,72 +38,66 @@
|
|
|
38
38
|
:disabled="disabled"
|
|
39
39
|
@click.stop="open = !open"
|
|
40
40
|
>
|
|
41
|
-
<Icon
|
|
41
|
+
<Icon :is="ArrowDownIcon" />
|
|
42
42
|
</button>
|
|
43
43
|
</div>
|
|
44
|
-
</div>
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<Button
|
|
56
|
-
v-for="option in filtered"
|
|
57
|
-
:key="option.value"
|
|
58
|
-
class="w-full"
|
|
59
|
-
variant="ghost"
|
|
60
|
-
layout="left"
|
|
61
|
-
:disabled="option.disabled"
|
|
62
|
-
role="option"
|
|
63
|
-
:aria-selected="option.value === modelValue"
|
|
64
|
-
@click="select(option)"
|
|
45
|
+
<div
|
|
46
|
+
ref="dropdown"
|
|
47
|
+
:popover="popoverSupported ? 'manual' : undefined"
|
|
48
|
+
:id="listboxId"
|
|
49
|
+
role="listbox"
|
|
50
|
+
:aria-label="label"
|
|
51
|
+
class="m-0 max-h-[min(16rem,50vh)] overflow-y-auto rounded-ui border border-border bg-popover p-1 text-popover-foreground shadow-xl"
|
|
52
|
+
:class="fallbackClass"
|
|
53
|
+
:style="popoverSupported ? panel : undefined"
|
|
65
54
|
>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
55
|
+
<Button
|
|
56
|
+
v-for="option in filtered"
|
|
57
|
+
:key="option.value"
|
|
58
|
+
class="w-full"
|
|
59
|
+
variant="ghost"
|
|
60
|
+
layout="left"
|
|
61
|
+
:disabled="option.disabled"
|
|
62
|
+
role="option"
|
|
63
|
+
:aria-selected="option.value === modelValue"
|
|
64
|
+
@click="select(option)"
|
|
65
|
+
>
|
|
66
|
+
<span class="flex w-full items-center justify-between gap-4 text-left">
|
|
67
|
+
<slot name="option" :option="option" :selected="option.value === modelValue">
|
|
68
|
+
{{ option.label }}
|
|
69
|
+
</slot>
|
|
70
|
+
|
|
71
|
+
<Icon v-if="option.value === modelValue" :is="TickIcon" />
|
|
72
|
+
</span>
|
|
73
|
+
</Button>
|
|
74
|
+
|
|
75
|
+
<p v-if="filtered.length === 0" class="px-2 py-6 text-center text-sm opacity-60">
|
|
76
|
+
{{ emptyLabel }}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
78
79
|
</div>
|
|
79
80
|
</div>
|
|
80
81
|
</template>
|
|
81
82
|
|
|
82
83
|
<script setup lang="ts">
|
|
83
|
-
import
|
|
84
|
-
import
|
|
84
|
+
import ArrowDownIcon from "~icons/hugeicons/arrow-down-01";
|
|
85
|
+
import TickIcon from "~icons/hugeicons/tick-02";
|
|
86
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
|
|
87
|
+
import type { SelectOption, SelectProps, SelectValue } from "../../components/select";
|
|
85
88
|
import Button from "./button.vue";
|
|
86
89
|
import Icon from "./icon.vue";
|
|
87
90
|
|
|
88
91
|
const {
|
|
89
|
-
modelValue
|
|
92
|
+
modelValue,
|
|
90
93
|
options,
|
|
91
|
-
label
|
|
92
|
-
placeholder
|
|
94
|
+
label,
|
|
95
|
+
placeholder,
|
|
93
96
|
emptyLabel = "No results found",
|
|
94
|
-
name
|
|
97
|
+
name,
|
|
95
98
|
required = false,
|
|
96
99
|
disabled = false,
|
|
97
|
-
} = defineProps<
|
|
98
|
-
modelValue?: SelectValue;
|
|
99
|
-
options: Array<SelectOption>;
|
|
100
|
-
label?: string;
|
|
101
|
-
placeholder?: string;
|
|
102
|
-
emptyLabel?: string;
|
|
103
|
-
name?: string;
|
|
104
|
-
required?: boolean;
|
|
105
|
-
disabled?: boolean;
|
|
106
|
-
}>();
|
|
100
|
+
} = defineProps<SelectProps>();
|
|
107
101
|
|
|
108
102
|
const emit = defineEmits<{
|
|
109
103
|
"update:modelValue": [value: SelectValue];
|
|
@@ -121,6 +115,8 @@ const dropdown = useTemplateRef("dropdown");
|
|
|
121
115
|
|
|
122
116
|
const listboxId = `select-${useId()}`;
|
|
123
117
|
|
|
118
|
+
const popoverSupported = ref(true);
|
|
119
|
+
|
|
124
120
|
const selected = computed(() => options.find((option) => option.value === modelValue));
|
|
125
121
|
|
|
126
122
|
const query = ref(selected.value?.label ?? "");
|
|
@@ -133,6 +129,14 @@ const filtered = computed(() => {
|
|
|
133
129
|
return term === "" ? options : options.filter((option) => option.label.toLowerCase().includes(term));
|
|
134
130
|
});
|
|
135
131
|
|
|
132
|
+
const fallbackClass = computed(() => {
|
|
133
|
+
if (popoverSupported.value) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return open.value ? "absolute inset-x-0 top-full z-50 mt-2" : "hidden";
|
|
138
|
+
});
|
|
139
|
+
|
|
136
140
|
function handlePointerDown(event: PointerEvent) {
|
|
137
141
|
const target = event.target as Node;
|
|
138
142
|
|
|
@@ -156,12 +160,13 @@ function updatePanel() {
|
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
const height = window.visualViewport?.height ?? window.innerHeight;
|
|
164
|
+
const width = window.visualViewport?.width ?? window.innerWidth;
|
|
159
165
|
const bounds = anchor.getBoundingClientRect();
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const left = Math.min(Math.max(8, bounds.left),
|
|
163
|
-
const top =
|
|
164
|
-
bounds.bottom + height + 8 <= window.innerHeight ? bounds.bottom + 8 : Math.max(8, bounds.top - height - 8);
|
|
166
|
+
const listHeight = list.offsetHeight;
|
|
167
|
+
const listWidth = bounds.width;
|
|
168
|
+
const left = Math.max(8, Math.min(Math.max(8, bounds.left), width - listWidth - 8));
|
|
169
|
+
const top = bounds.bottom + listHeight + 8 <= height ? bounds.bottom + 8 : Math.max(8, bounds.top - listHeight - 8);
|
|
165
170
|
|
|
166
171
|
panel.value = {
|
|
167
172
|
bottom: "auto",
|
|
@@ -169,7 +174,7 @@ function updatePanel() {
|
|
|
169
174
|
position: "fixed",
|
|
170
175
|
right: "auto",
|
|
171
176
|
top: `${top}px`,
|
|
172
|
-
width: `${
|
|
177
|
+
width: `${listWidth}px`,
|
|
173
178
|
};
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -186,7 +191,7 @@ watch(open, async (value) => {
|
|
|
186
191
|
return;
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
if (value
|
|
194
|
+
if (value) {
|
|
190
195
|
query.value = "";
|
|
191
196
|
|
|
192
197
|
document.addEventListener("pointerdown", handlePointerDown);
|
|
@@ -195,31 +200,49 @@ watch(open, async (value) => {
|
|
|
195
200
|
|
|
196
201
|
window.addEventListener("scroll", updatePanel, true);
|
|
197
202
|
|
|
198
|
-
|
|
203
|
+
window.visualViewport?.addEventListener("resize", updatePanel);
|
|
204
|
+
|
|
205
|
+
if (popoverSupported.value && !list.matches(":popover-open")) {
|
|
206
|
+
list.showPopover();
|
|
207
|
+
}
|
|
199
208
|
|
|
200
209
|
await nextTick();
|
|
201
210
|
|
|
202
211
|
updatePanel();
|
|
203
212
|
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
requestAnimationFrame(updatePanel);
|
|
214
|
+
|
|
215
|
+
if (document.activeElement !== search.value) {
|
|
216
|
+
search.value?.focus();
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
206
219
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
207
220
|
|
|
208
221
|
window.removeEventListener("resize", updatePanel);
|
|
209
222
|
|
|
210
223
|
window.removeEventListener("scroll", updatePanel, true);
|
|
211
224
|
|
|
212
|
-
|
|
225
|
+
window.visualViewport?.removeEventListener("resize", updatePanel);
|
|
226
|
+
|
|
227
|
+
if (popoverSupported.value && list.matches(":popover-open")) {
|
|
228
|
+
list.hidePopover();
|
|
229
|
+
}
|
|
213
230
|
|
|
214
231
|
query.value = selected.value?.label ?? "";
|
|
215
232
|
}
|
|
216
233
|
});
|
|
217
234
|
|
|
235
|
+
onMounted(() => {
|
|
236
|
+
popoverSupported.value = typeof dropdown.value?.showPopover === "function";
|
|
237
|
+
});
|
|
238
|
+
|
|
218
239
|
onUnmounted(() => {
|
|
219
240
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
220
241
|
|
|
221
242
|
window.removeEventListener("resize", updatePanel);
|
|
222
243
|
|
|
223
244
|
window.removeEventListener("scroll", updatePanel, true);
|
|
245
|
+
|
|
246
|
+
window.visualViewport?.removeEventListener("resize", updatePanel);
|
|
224
247
|
});
|
|
225
248
|
</script>
|
|
@@ -9,17 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
<script setup lang="ts">
|
|
11
11
|
import { computed } from "vue";
|
|
12
|
-
import type {
|
|
12
|
+
import type { SeparatorOrientation, SeparatorProps } from "../../components/separator";
|
|
13
13
|
|
|
14
|
-
const {
|
|
15
|
-
orientation = "horizontal",
|
|
16
|
-
variant = undefined,
|
|
17
|
-
decorative = false,
|
|
18
|
-
} = defineProps<{
|
|
19
|
-
orientation?: SeparatorOrientation;
|
|
20
|
-
variant?: DividerVariant;
|
|
21
|
-
decorative?: boolean;
|
|
22
|
-
}>();
|
|
14
|
+
const { orientation = "horizontal", variant, decorative = false } = defineProps<SeparatorProps>();
|
|
23
15
|
|
|
24
16
|
const selected = computed(() => variant ?? orientation);
|
|
25
17
|
|