@revojs/ui 0.2.7 → 0.2.8
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 +1 -1
- package/dist/module/index.js +34 -34
- package/dist/vue/components/button.vue +8 -8
- package/dist/vue/components/card.vue +4 -1
- package/dist/vue/components/checkbox.vue +12 -1
- package/dist/vue/components/navigate.vue +1 -1
- package/dist/vue/components/progress.vue +1 -1
- package/dist/vue/components/radio.vue +9 -1
- package/dist/vue/components/range.vue +97 -14
- package/dist/vue/components/rating.vue +2 -2
- package/dist/vue/components/select.vue +75 -45
- package/dist/vue/components/sheet.vue +13 -6
- package/dist/vue/components/switch.vue +4 -7
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ export type ButtonSize = "default" | "icon";
|
|
|
3
3
|
export type ButtonFont = "normal" | "medium" | "semibold";
|
|
4
4
|
export type ButtonLayout = "left" | "center" | "right";
|
|
5
5
|
export type ButtonText = "sm" | "md";
|
|
6
|
-
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "destructive"
|
|
6
|
+
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "destructive";
|
|
7
7
|
export interface ButtonProps {
|
|
8
8
|
iconPrefix?: string;
|
|
9
9
|
iconSuffix?: string;
|
package/dist/module/index.js
CHANGED
|
@@ -31,45 +31,45 @@ var module_default = defineModule({
|
|
|
31
31
|
radius: "rounded-md",
|
|
32
32
|
light: {
|
|
33
33
|
background: "white",
|
|
34
|
-
foreground: "
|
|
34
|
+
foreground: "zinc-950",
|
|
35
35
|
card: "white",
|
|
36
|
-
cardForeground: "
|
|
36
|
+
cardForeground: "zinc-950",
|
|
37
37
|
popover: "white",
|
|
38
|
-
popoverForeground: "
|
|
39
|
-
primary: "
|
|
40
|
-
primaryForeground: "
|
|
41
|
-
secondary: "
|
|
42
|
-
secondaryForeground: "
|
|
43
|
-
muted: "
|
|
44
|
-
mutedForeground: "
|
|
45
|
-
accent: "
|
|
46
|
-
accentForeground: "
|
|
38
|
+
popoverForeground: "zinc-950",
|
|
39
|
+
primary: "green-600",
|
|
40
|
+
primaryForeground: "zinc-50",
|
|
41
|
+
secondary: "zinc-100",
|
|
42
|
+
secondaryForeground: "zinc-900",
|
|
43
|
+
muted: "zinc-100",
|
|
44
|
+
mutedForeground: "zinc-500",
|
|
45
|
+
accent: "zinc-100",
|
|
46
|
+
accentForeground: "zinc-900",
|
|
47
47
|
destructive: "red-600",
|
|
48
|
-
destructiveForeground: "
|
|
49
|
-
border: "
|
|
50
|
-
input: "
|
|
51
|
-
ring: "
|
|
48
|
+
destructiveForeground: "zinc-50",
|
|
49
|
+
border: "zinc-200",
|
|
50
|
+
input: "zinc-200",
|
|
51
|
+
ring: "zinc-400"
|
|
52
52
|
},
|
|
53
53
|
dark: {
|
|
54
|
-
background: "
|
|
55
|
-
foreground: "
|
|
56
|
-
card: "
|
|
57
|
-
cardForeground: "
|
|
58
|
-
popover: "
|
|
59
|
-
popoverForeground: "
|
|
60
|
-
primary: "
|
|
61
|
-
primaryForeground: "
|
|
62
|
-
secondary: "
|
|
63
|
-
secondaryForeground: "
|
|
64
|
-
muted: "
|
|
65
|
-
mutedForeground: "
|
|
66
|
-
accent: "
|
|
67
|
-
accentForeground: "
|
|
68
|
-
destructive: "red-
|
|
69
|
-
destructiveForeground: "
|
|
70
|
-
border: "
|
|
71
|
-
input: "
|
|
72
|
-
ring: "
|
|
54
|
+
background: "zinc-950",
|
|
55
|
+
foreground: "zinc-50",
|
|
56
|
+
card: "zinc-900",
|
|
57
|
+
cardForeground: "zinc-50",
|
|
58
|
+
popover: "zinc-900",
|
|
59
|
+
popoverForeground: "zinc-50",
|
|
60
|
+
primary: "green-500",
|
|
61
|
+
primaryForeground: "zinc-950",
|
|
62
|
+
secondary: "zinc-900",
|
|
63
|
+
secondaryForeground: "zinc-50",
|
|
64
|
+
muted: "zinc-800",
|
|
65
|
+
mutedForeground: "zinc-400",
|
|
66
|
+
accent: "zinc-900",
|
|
67
|
+
accentForeground: "zinc-50",
|
|
68
|
+
destructive: "red-600",
|
|
69
|
+
destructiveForeground: "zinc-50",
|
|
70
|
+
border: "zinc-700",
|
|
71
|
+
input: "zinc-700",
|
|
72
|
+
ring: "zinc-500"
|
|
73
73
|
}
|
|
74
74
|
}),
|
|
75
75
|
setup(config) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="is"
|
|
4
|
-
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-ui border
|
|
4
|
+
class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-ui border font-[inherit] font-medium leading-normal no-underline outline-ring transition-[background-color,border-color,color,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
|
|
5
5
|
:class="[fonts[font], layouts[layout], sizes[size], texts[text], variants[variant]]"
|
|
6
6
|
:type="attributes.type ?? 'button'"
|
|
7
7
|
:disabled="disabled"
|
|
@@ -76,12 +76,12 @@ const texts: Record<ButtonText, string> = {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const variants: Record<ButtonVariant, string> = {
|
|
79
|
-
primary: "bg-primary text-primary-foreground",
|
|
80
|
-
secondary: "bg-secondary text-secondary-foreground",
|
|
81
|
-
outline:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
primary: "border-transparent bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80",
|
|
80
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/90 active:bg-secondary/80",
|
|
81
|
+
outline:
|
|
82
|
+
"border-transparent bg-secondary text-secondary-foreground ring-1 ring-border ring-inset hover:bg-secondary/90 active:bg-secondary/80",
|
|
83
|
+
ghost: "border-transparent bg-transparent text-foreground hover:bg-accent active:bg-accent/80",
|
|
84
|
+
destructive:
|
|
85
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80",
|
|
86
86
|
};
|
|
87
87
|
</script>
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
>
|
|
9
9
|
<slot name="header">
|
|
10
10
|
<div class="flex flex-col gap-1.5">
|
|
11
|
-
<
|
|
11
|
+
<p v-if="label" class="text-xs font-semibold uppercase tracking-widest opacity-50">{{ label }}</p>
|
|
12
|
+
|
|
13
|
+
<h3 v-if="title" class="text-lg font-semibold leading-none">{{ title }}</h3>
|
|
14
|
+
|
|
12
15
|
<p v-if="description" class="text-sm opacity-70">{{ description }}</p>
|
|
13
16
|
</div>
|
|
14
17
|
</slot>
|
|
@@ -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="checkbox"
|
|
6
6
|
:checked="modelValue"
|
|
7
7
|
:name="name"
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
@change="$emit('update:modelValue', ($event.currentTarget as HTMLInputElement).checked)"
|
|
11
11
|
/>
|
|
12
12
|
|
|
13
|
+
<span
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
class="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-md border transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring"
|
|
16
|
+
:class="modelValue ? 'border-transparent bg-primary' : 'border-input bg-background'"
|
|
17
|
+
>
|
|
18
|
+
<span
|
|
19
|
+
v-if="modelValue"
|
|
20
|
+
class="mb-px h-2.5 w-1.5 rotate-45 border-b-2 border-r-2 border-primary-foreground"
|
|
21
|
+
/>
|
|
22
|
+
</span>
|
|
23
|
+
|
|
13
24
|
<span v-if="label || description || $slots.default" class="flex flex-col gap-0.5">
|
|
14
25
|
<span class="text-sm font-medium"
|
|
15
26
|
><slot>{{ label }}</slot></span
|
|
@@ -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"
|
|
@@ -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>
|
|
@@ -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,21 +8,35 @@
|
|
|
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";
|
|
27
41
|
|
|
28
42
|
const {
|
|
@@ -41,7 +55,7 @@ const {
|
|
|
41
55
|
disabled?: boolean;
|
|
42
56
|
}>();
|
|
43
57
|
|
|
44
|
-
defineEmits<{
|
|
58
|
+
const emit = defineEmits<{
|
|
45
59
|
"update:modelValue": [value: number];
|
|
46
60
|
}>();
|
|
47
61
|
|
|
@@ -51,4 +65,73 @@ const interactionVariants: Record<ControlInteraction, string> = {
|
|
|
51
65
|
disabled: "cursor-not-allowed opacity-50",
|
|
52
66
|
enabled: "",
|
|
53
67
|
};
|
|
68
|
+
|
|
69
|
+
const track = useTemplateRef("track");
|
|
70
|
+
|
|
71
|
+
const percent = computed(() => (max > min ? Math.min(100, Math.max(0, ((modelValue - min) / (max - min)) * 100)) : 0));
|
|
72
|
+
|
|
73
|
+
let dragging = false;
|
|
74
|
+
|
|
75
|
+
function commit(raw: number) {
|
|
76
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
77
|
+
const snapped = min + Math.round((raw - min) / step) * step;
|
|
78
|
+
const rounded = Number(snapped.toFixed(decimals));
|
|
79
|
+
const value = Math.min(max, Math.max(min, rounded));
|
|
80
|
+
|
|
81
|
+
if (value !== modelValue) {
|
|
82
|
+
emit("update:modelValue", value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setValueFromClientX(clientX: number) {
|
|
87
|
+
if (!track.value) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rect = track.value.getBoundingClientRect();
|
|
92
|
+
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
93
|
+
|
|
94
|
+
commit(min + ratio * (max - min));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onPointerDown(event: PointerEvent) {
|
|
98
|
+
if (disabled || !track.value) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dragging = true;
|
|
103
|
+
track.value.setPointerCapture(event.pointerId);
|
|
104
|
+
track.value.focus({ preventScroll: true });
|
|
105
|
+
setValueFromClientX(event.clientX);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onPointerMove(event: PointerEvent) {
|
|
109
|
+
if (dragging) {
|
|
110
|
+
setValueFromClientX(event.clientX);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onPointerUp() {
|
|
115
|
+
dragging = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
119
|
+
if (disabled) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (event.key === "ArrowRight" || event.key === "ArrowUp") {
|
|
124
|
+
commit(modelValue + step);
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
} else if (event.key === "ArrowLeft" || event.key === "ArrowDown") {
|
|
127
|
+
commit(modelValue - step);
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
} else if (event.key === "Home") {
|
|
130
|
+
commit(min);
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
} else if (event.key === "End") {
|
|
133
|
+
commit(max);
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
54
137
|
</script>
|
|
@@ -50,7 +50,7 @@ function state(value: number): RatingState {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const variants: Record<RatingState, string> = {
|
|
53
|
-
active: "text-
|
|
54
|
-
inactive: "text-
|
|
53
|
+
active: "text-primary",
|
|
54
|
+
inactive: "text-input",
|
|
55
55
|
};
|
|
56
56
|
</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"
|
|
@@ -41,46 +41,47 @@
|
|
|
41
41
|
<Icon name="arrow-down-01" />
|
|
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" name="tick-02" />
|
|
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 { computed, nextTick, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
|
|
84
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, useId, useTemplateRef, watch } from "vue";
|
|
84
85
|
import type { SelectOption, SelectValue } from "../../components/select.ts";
|
|
85
86
|
import Button from "./button.vue";
|
|
86
87
|
import Icon from "./icon.vue";
|
|
@@ -121,6 +122,8 @@ const dropdown = useTemplateRef("dropdown");
|
|
|
121
122
|
|
|
122
123
|
const listboxId = `select-${useId()}`;
|
|
123
124
|
|
|
125
|
+
const popoverSupported = ref(true);
|
|
126
|
+
|
|
124
127
|
const selected = computed(() => options.find((option) => option.value === modelValue));
|
|
125
128
|
|
|
126
129
|
const query = ref(selected.value?.label ?? "");
|
|
@@ -133,6 +136,14 @@ const filtered = computed(() => {
|
|
|
133
136
|
return term === "" ? options : options.filter((option) => option.label.toLowerCase().includes(term));
|
|
134
137
|
});
|
|
135
138
|
|
|
139
|
+
const fallbackClass = computed(() => {
|
|
140
|
+
if (popoverSupported.value) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return open.value ? "absolute inset-x-0 top-full z-50 mt-2" : "hidden";
|
|
145
|
+
});
|
|
146
|
+
|
|
136
147
|
function handlePointerDown(event: PointerEvent) {
|
|
137
148
|
const target = event.target as Node;
|
|
138
149
|
|
|
@@ -156,12 +167,13 @@ function updatePanel() {
|
|
|
156
167
|
return;
|
|
157
168
|
}
|
|
158
169
|
|
|
170
|
+
const height = window.visualViewport?.height ?? window.innerHeight;
|
|
171
|
+
const width = window.visualViewport?.width ?? window.innerWidth;
|
|
159
172
|
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);
|
|
173
|
+
const listHeight = list.offsetHeight;
|
|
174
|
+
const listWidth = bounds.width;
|
|
175
|
+
const left = Math.max(8, Math.min(Math.max(8, bounds.left), width - listWidth - 8));
|
|
176
|
+
const top = bounds.bottom + listHeight + 8 <= height ? bounds.bottom + 8 : Math.max(8, bounds.top - listHeight - 8);
|
|
165
177
|
|
|
166
178
|
panel.value = {
|
|
167
179
|
bottom: "auto",
|
|
@@ -169,7 +181,7 @@ function updatePanel() {
|
|
|
169
181
|
position: "fixed",
|
|
170
182
|
right: "auto",
|
|
171
183
|
top: `${top}px`,
|
|
172
|
-
width: `${
|
|
184
|
+
width: `${listWidth}px`,
|
|
173
185
|
};
|
|
174
186
|
}
|
|
175
187
|
|
|
@@ -186,7 +198,7 @@ watch(open, async (value) => {
|
|
|
186
198
|
return;
|
|
187
199
|
}
|
|
188
200
|
|
|
189
|
-
if (value
|
|
201
|
+
if (value) {
|
|
190
202
|
query.value = "";
|
|
191
203
|
|
|
192
204
|
document.addEventListener("pointerdown", handlePointerDown);
|
|
@@ -195,31 +207,49 @@ watch(open, async (value) => {
|
|
|
195
207
|
|
|
196
208
|
window.addEventListener("scroll", updatePanel, true);
|
|
197
209
|
|
|
198
|
-
|
|
210
|
+
window.visualViewport?.addEventListener("resize", updatePanel);
|
|
211
|
+
|
|
212
|
+
if (popoverSupported.value && !list.matches(":popover-open")) {
|
|
213
|
+
list.showPopover();
|
|
214
|
+
}
|
|
199
215
|
|
|
200
216
|
await nextTick();
|
|
201
217
|
|
|
202
218
|
updatePanel();
|
|
203
219
|
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
requestAnimationFrame(updatePanel);
|
|
221
|
+
|
|
222
|
+
if (document.activeElement !== search.value) {
|
|
223
|
+
search.value?.focus();
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
206
226
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
207
227
|
|
|
208
228
|
window.removeEventListener("resize", updatePanel);
|
|
209
229
|
|
|
210
230
|
window.removeEventListener("scroll", updatePanel, true);
|
|
211
231
|
|
|
212
|
-
|
|
232
|
+
window.visualViewport?.removeEventListener("resize", updatePanel);
|
|
233
|
+
|
|
234
|
+
if (popoverSupported.value && list.matches(":popover-open")) {
|
|
235
|
+
list.hidePopover();
|
|
236
|
+
}
|
|
213
237
|
|
|
214
238
|
query.value = selected.value?.label ?? "";
|
|
215
239
|
}
|
|
216
240
|
});
|
|
217
241
|
|
|
242
|
+
onMounted(() => {
|
|
243
|
+
popoverSupported.value = typeof dropdown.value?.showPopover === "function";
|
|
244
|
+
});
|
|
245
|
+
|
|
218
246
|
onUnmounted(() => {
|
|
219
247
|
document.removeEventListener("pointerdown", handlePointerDown);
|
|
220
248
|
|
|
221
249
|
window.removeEventListener("resize", updatePanel);
|
|
222
250
|
|
|
223
251
|
window.removeEventListener("scroll", updatePanel, true);
|
|
252
|
+
|
|
253
|
+
window.visualViewport?.removeEventListener("resize", updatePanel);
|
|
224
254
|
});
|
|
225
255
|
</script>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<dialog
|
|
3
3
|
ref="sheet"
|
|
4
|
-
class="m-0
|
|
4
|
+
class="m-0 max-w-none border-0 bg-transparent p-0 backdrop:bg-black/50"
|
|
5
5
|
:class="positions[side]"
|
|
6
6
|
@close="$emit('update:modelValue', false)"
|
|
7
7
|
@click="$event.target === $event.currentTarget && ($event.currentTarget as HTMLDialogElement).close()"
|
|
8
8
|
>
|
|
9
|
-
<Card :title="title" :description="description" class="
|
|
9
|
+
<Card :title="title" :description="description" :class="cardClasses[side]">
|
|
10
10
|
<template #actions>
|
|
11
11
|
<Button
|
|
12
12
|
size="icon"
|
|
@@ -55,10 +55,17 @@ defineEmits<{
|
|
|
55
55
|
const sheet = useTemplateRef("sheet");
|
|
56
56
|
|
|
57
57
|
const positions: Record<SheetSide, string> = {
|
|
58
|
-
top: "
|
|
59
|
-
right: "
|
|
60
|
-
bottom: "
|
|
61
|
-
left: "
|
|
58
|
+
top: "bottom-auto left-0 right-0 top-0 h-auto w-full",
|
|
59
|
+
right: "bottom-0 left-auto right-0 top-0 h-full max-h-full w-[min(28rem,90vw)]",
|
|
60
|
+
bottom: "bottom-0 left-0 right-0 top-auto h-auto w-full",
|
|
61
|
+
left: "bottom-0 left-0 right-auto top-0 h-full max-h-full w-[min(28rem,90vw)]",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const cardClasses: Record<SheetSide, string> = {
|
|
65
|
+
top: "max-h-[85vh] w-full shadow-2xl",
|
|
66
|
+
right: "h-full w-full shadow-2xl",
|
|
67
|
+
bottom: "max-h-[85vh] w-full shadow-2xl",
|
|
68
|
+
left: "h-full w-full shadow-2xl",
|
|
62
69
|
};
|
|
63
70
|
|
|
64
71
|
watchEffect(() => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<input v-if="name" type="hidden" :name="name" :value="modelValue ? 'on' : 'off'" />
|
|
4
4
|
|
|
5
5
|
<button
|
|
6
|
-
class="relative mt-0.5 h-6 w-11 shrink-0 rounded-full outline-ring transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed"
|
|
6
|
+
class="relative mt-0.5 flex h-6 w-11 shrink-0 items-center rounded-full outline-ring transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed"
|
|
7
7
|
:class="variants[state]"
|
|
8
8
|
type="button"
|
|
9
9
|
role="switch"
|
|
@@ -11,10 +11,7 @@
|
|
|
11
11
|
:disabled="disabled"
|
|
12
12
|
@click="$emit('update:modelValue', !modelValue)"
|
|
13
13
|
>
|
|
14
|
-
<span
|
|
15
|
-
class="block size-5 translate-x-0.5 rounded-full bg-white shadow transition-transform"
|
|
16
|
-
:class="thumbVariants[state]"
|
|
17
|
-
/>
|
|
14
|
+
<span class="block size-5 rounded-full shadow transition-transform" :class="thumbVariants[state]" />
|
|
18
15
|
</button>
|
|
19
16
|
|
|
20
17
|
<span v-if="label || description || $slots.default" class="flex flex-col gap-0.5">
|
|
@@ -60,8 +57,8 @@ const interactionVariants: Record<ControlInteraction, string> = {
|
|
|
60
57
|
};
|
|
61
58
|
|
|
62
59
|
const thumbVariants: Record<SwitchState, string> = {
|
|
63
|
-
checked: "translate-x-5",
|
|
64
|
-
unchecked: "",
|
|
60
|
+
checked: "translate-x-5 bg-primary-foreground",
|
|
61
|
+
unchecked: "translate-x-0.5 bg-white",
|
|
65
62
|
};
|
|
66
63
|
|
|
67
64
|
const variants: Record<SwitchState, string> = {
|