@revojs/ui 0.1.58
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/breadcrumb.d.ts +4 -0
- package/dist/components/breadcrumb.js +1 -0
- package/dist/components/button.d.ts +18 -0
- package/dist/components/button.js +1 -0
- package/dist/components/card.d.ts +5 -0
- package/dist/components/card.js +1 -0
- package/dist/components/checkbox.d.ts +8 -0
- package/dist/components/checkbox.js +1 -0
- package/dist/components/collection.d.ts +6 -0
- package/dist/components/collection.js +1 -0
- package/dist/components/container.d.ts +4 -0
- package/dist/components/container.js +1 -0
- package/dist/components/control.d.ts +1 -0
- package/dist/components/control.js +1 -0
- package/dist/components/counter.d.ts +8 -0
- package/dist/components/counter.js +1 -0
- package/dist/components/dialog.d.ts +6 -0
- package/dist/components/dialog.js +1 -0
- package/dist/components/field.d.ts +7 -0
- package/dist/components/field.js +1 -0
- package/dist/components/heading.d.ts +7 -0
- package/dist/components/heading.js +1 -0
- package/dist/components/icon.d.ts +3 -0
- package/dist/components/icon.js +1 -0
- package/dist/components/input.d.ts +12 -0
- package/dist/components/input.js +1 -0
- package/dist/components/link.d.ts +6 -0
- package/dist/components/link.js +1 -0
- package/dist/components/navigate.d.ts +7 -0
- package/dist/components/navigate.js +1 -0
- package/dist/components/popover.d.ts +10 -0
- package/dist/components/popover.js +1 -0
- package/dist/components/progress.d.ts +5 -0
- package/dist/components/progress.js +1 -0
- package/dist/components/radio.d.ts +10 -0
- package/dist/components/radio.js +1 -0
- package/dist/components/range.d.ts +8 -0
- package/dist/components/range.js +1 -0
- package/dist/components/rating.d.ts +8 -0
- package/dist/components/rating.js +1 -0
- package/dist/components/select.d.ts +16 -0
- package/dist/components/select.js +1 -0
- package/dist/components/separator.d.ts +11 -0
- package/dist/components/separator.js +1 -0
- package/dist/components/sheet.d.ts +8 -0
- package/dist/components/sheet.js +1 -0
- package/dist/components/switch.d.ts +8 -0
- package/dist/components/switch.js +1 -0
- package/dist/components/upload.d.ts +10 -0
- package/dist/components/upload.js +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +1 -0
- package/dist/module/index.d.ts +35 -0
- package/dist/module/index.js +123 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/vue/components/breadcrumb.vue +20 -0
- package/dist/vue/components/button.vue +124 -0
- package/dist/vue/components/card.vue +53 -0
- package/dist/vue/components/checkbox.vue +54 -0
- package/dist/vue/components/collection.vue +51 -0
- package/dist/vue/components/container.vue +19 -0
- package/dist/vue/components/counter.vue +61 -0
- package/dist/vue/components/dialog.vue +76 -0
- package/dist/vue/components/field.vue +38 -0
- package/dist/vue/components/heading.vue +34 -0
- package/dist/vue/components/icon.vue +49 -0
- package/dist/vue/components/input.vue +82 -0
- package/dist/vue/components/link.vue +33 -0
- package/dist/vue/components/navigate.vue +30 -0
- package/dist/vue/components/popover.vue +123 -0
- package/dist/vue/components/progress.vue +44 -0
- package/dist/vue/components/radio.vue +59 -0
- package/dist/vue/components/range.vue +56 -0
- package/dist/vue/components/rating.vue +56 -0
- package/dist/vue/components/select.vue +99 -0
- package/dist/vue/components/separator.vue +31 -0
- package/dist/vue/components/sheet.vue +71 -0
- package/dist/vue/components/switch.vue +72 -0
- package/dist/vue/components/upload.vue +133 -0
- package/dist/vue/index.d.ts +25 -0
- package/dist/vue/index.js +25 -0
- package/package.json +46 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Button
|
|
3
|
+
is="a"
|
|
4
|
+
:href="href"
|
|
5
|
+
font="normal"
|
|
6
|
+
layout="left"
|
|
7
|
+
:variant="active ? 'secondary' : 'text'"
|
|
8
|
+
:icon-prefix="iconPrefix"
|
|
9
|
+
:icon-suffix="iconSuffix"
|
|
10
|
+
:label="label"
|
|
11
|
+
/>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import Button from "./button.vue";
|
|
16
|
+
|
|
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
|
+
}>();
|
|
30
|
+
</script>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="container" class="inline-flex">
|
|
3
|
+
<Button
|
|
4
|
+
variant="outline"
|
|
5
|
+
:class="triggerClass"
|
|
6
|
+
:layout="triggerLayout"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
:popovertarget="id"
|
|
9
|
+
>
|
|
10
|
+
<slot name="trigger" :open="open" :close="close" :toggle="toggle">{{ triggerLabel }}</slot>
|
|
11
|
+
</Button>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
:id="id"
|
|
15
|
+
ref="popover"
|
|
16
|
+
popover
|
|
17
|
+
class="m-0 mt-2 max-w-sm border shadow-xl"
|
|
18
|
+
:class="[
|
|
19
|
+
config.radius,
|
|
20
|
+
`border-${config.light.border}`,
|
|
21
|
+
`bg-${config.light.popover}`,
|
|
22
|
+
`text-${config.light.popoverForeground}`,
|
|
23
|
+
`dark:border-${config.dark.border}`,
|
|
24
|
+
`dark:bg-${config.dark.popover}`,
|
|
25
|
+
`dark:text-${config.dark.popoverForeground}`,
|
|
26
|
+
contentClass,
|
|
27
|
+
]"
|
|
28
|
+
:style="position"
|
|
29
|
+
@toggle="handleToggle"
|
|
30
|
+
>
|
|
31
|
+
<slot :open="open" :close="close" :toggle="toggle" />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import config from "#config/ui";
|
|
38
|
+
import { ref, useId, useTemplateRef, watchEffect } from "vue";
|
|
39
|
+
import type { ButtonLayout } from "../../components/button.ts";
|
|
40
|
+
import Button from "./button.vue";
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
modelValue = false,
|
|
44
|
+
triggerLabel = "Open",
|
|
45
|
+
triggerClass = undefined,
|
|
46
|
+
triggerLayout = "center",
|
|
47
|
+
contentClass = "p-4",
|
|
48
|
+
matchTriggerWidth = false,
|
|
49
|
+
disabled = false,
|
|
50
|
+
} = defineProps<{
|
|
51
|
+
modelValue?: boolean;
|
|
52
|
+
triggerLabel?: string;
|
|
53
|
+
triggerClass?: string;
|
|
54
|
+
triggerLayout?: ButtonLayout;
|
|
55
|
+
contentClass?: string;
|
|
56
|
+
matchTriggerWidth?: boolean;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
}>();
|
|
59
|
+
|
|
60
|
+
const emit = defineEmits<{
|
|
61
|
+
"update:modelValue": [value: boolean];
|
|
62
|
+
}>();
|
|
63
|
+
|
|
64
|
+
const id = `popover-${useId()}`;
|
|
65
|
+
|
|
66
|
+
const container = useTemplateRef("container");
|
|
67
|
+
|
|
68
|
+
const popover = useTemplateRef("popover");
|
|
69
|
+
|
|
70
|
+
const position = ref<Record<string, string>>({});
|
|
71
|
+
|
|
72
|
+
function open() {
|
|
73
|
+
popover.value?.showPopover();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function close() {
|
|
77
|
+
popover.value?.hidePopover();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toggle() {
|
|
81
|
+
popover.value?.togglePopover();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleToggle(event: Event) {
|
|
85
|
+
const element = event.currentTarget as HTMLElement;
|
|
86
|
+
const open = element.matches(":popover-open");
|
|
87
|
+
|
|
88
|
+
if (open) {
|
|
89
|
+
const trigger = container.value?.querySelector("button");
|
|
90
|
+
|
|
91
|
+
if (trigger) {
|
|
92
|
+
const bounds = trigger.getBoundingClientRect();
|
|
93
|
+
const width = matchTriggerWidth ? bounds.width : element.offsetWidth;
|
|
94
|
+
const left = Math.min(Math.max(8, bounds.left), window.innerWidth - width - 8);
|
|
95
|
+
const top =
|
|
96
|
+
bounds.bottom + element.offsetHeight + 8 <= window.innerHeight
|
|
97
|
+
? bounds.bottom + 8
|
|
98
|
+
: bounds.top - element.offsetHeight - 8;
|
|
99
|
+
|
|
100
|
+
position.value = {
|
|
101
|
+
bottom: "auto",
|
|
102
|
+
left: `${left}px`,
|
|
103
|
+
right: "auto",
|
|
104
|
+
top: `${Math.max(8, top)}px`,
|
|
105
|
+
position: "fixed",
|
|
106
|
+
width: matchTriggerWidth ? `${width}px` : "auto",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
emit("update:modelValue", open);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
watchEffect(() => {
|
|
115
|
+
if (modelValue && !popover.value?.matches(":popover-open")) {
|
|
116
|
+
popover.value?.showPopover();
|
|
117
|
+
} else if (!modelValue && popover.value?.matches(":popover-open")) {
|
|
118
|
+
popover.value.hidePopover();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
defineExpose({ open, close, toggle });
|
|
123
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col gap-2">
|
|
3
|
+
<div v-if="label" class="flex items-center justify-between gap-4 text-sm">
|
|
4
|
+
<span class="font-medium">{{ label }}</span>
|
|
5
|
+
|
|
6
|
+
<span class="tabular-nums opacity-60">{{ Math.round(percent) }}%</span>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<progress class="sr-only" :value="value" :max="max">{{ percent }}%</progress>
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class="h-2 w-full overflow-hidden rounded-full"
|
|
13
|
+
:class="[`bg-${config.light.secondary}`, `dark:bg-${config.dark.secondary}`]"
|
|
14
|
+
role="progressbar"
|
|
15
|
+
:aria-label="label"
|
|
16
|
+
:aria-valuenow="value"
|
|
17
|
+
aria-valuemin="0"
|
|
18
|
+
:aria-valuemax="max"
|
|
19
|
+
>
|
|
20
|
+
<div
|
|
21
|
+
class="h-full rounded-full transition-[width]"
|
|
22
|
+
:class="[`bg-${config.light.primary}`, `dark:bg-${config.dark.primary}`]"
|
|
23
|
+
:style="{ width: `${percent}%` }"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import config from "#config/ui";
|
|
31
|
+
import { computed } from "vue";
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
value = 0,
|
|
35
|
+
max = 100,
|
|
36
|
+
label = undefined,
|
|
37
|
+
} = defineProps<{
|
|
38
|
+
value?: number;
|
|
39
|
+
max?: number;
|
|
40
|
+
label?: string;
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const percent = computed(() => (max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0));
|
|
44
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="inline-flex items-start gap-3" :class="interactionVariants[interaction]">
|
|
3
|
+
<input
|
|
4
|
+
class="mt-0.5 size-5 shrink-0 cursor-inherit accent-current"
|
|
5
|
+
:class="[`text-${config.light.primary}`, `dark:text-${config.dark.primary}`]"
|
|
6
|
+
type="radio"
|
|
7
|
+
:checked="modelValue === value"
|
|
8
|
+
:value="value"
|
|
9
|
+
:name="name"
|
|
10
|
+
:required="required"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
@change="$emit('update:modelValue', value)"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<span class="flex flex-col gap-0.5">
|
|
16
|
+
<span class="text-sm font-medium">
|
|
17
|
+
<slot> {{ label }}</slot>
|
|
18
|
+
</span>
|
|
19
|
+
|
|
20
|
+
<span v-if="description" class="text-sm opacity-60">{{ description }}</span>
|
|
21
|
+
</span>
|
|
22
|
+
</label>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import config from "#config/ui";
|
|
27
|
+
import { computed } from "vue";
|
|
28
|
+
import type { ControlInteraction } from "../../components/control.ts";
|
|
29
|
+
import type { RadioValue } from "../../components/radio.ts";
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
modelValue = undefined,
|
|
33
|
+
value,
|
|
34
|
+
label = undefined,
|
|
35
|
+
description = undefined,
|
|
36
|
+
name = undefined,
|
|
37
|
+
required = false,
|
|
38
|
+
disabled = false,
|
|
39
|
+
} = defineProps<{
|
|
40
|
+
modelValue?: RadioValue;
|
|
41
|
+
value: RadioValue;
|
|
42
|
+
label?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
required?: boolean;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
defineEmits<{
|
|
50
|
+
"update:modelValue": [value: RadioValue];
|
|
51
|
+
}>();
|
|
52
|
+
|
|
53
|
+
const interaction = computed<ControlInteraction>(() => (disabled ? "disabled" : "enabled"));
|
|
54
|
+
|
|
55
|
+
const interactionVariants: Record<ControlInteraction, string> = {
|
|
56
|
+
disabled: "cursor-not-allowed opacity-50",
|
|
57
|
+
enabled: "cursor-pointer",
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="flex flex-col gap-2" :class="interactionVariants[interaction]">
|
|
3
|
+
<span v-if="label" class="flex items-center justify-between gap-4 text-sm">
|
|
4
|
+
<span class="font-medium"> {{ label }} </span>
|
|
5
|
+
|
|
6
|
+
<output class="tabular-nums opacity-60">
|
|
7
|
+
{{ modelValue }}
|
|
8
|
+
</output>
|
|
9
|
+
</span>
|
|
10
|
+
|
|
11
|
+
<input
|
|
12
|
+
class="h-2 w-full cursor-pointer accent-current disabled:cursor-not-allowed"
|
|
13
|
+
:class="[`text-${config.light.primary}`, `dark:text-${config.dark.primary}`]"
|
|
14
|
+
type="range"
|
|
15
|
+
:value="modelValue"
|
|
16
|
+
:min="min"
|
|
17
|
+
:max="max"
|
|
18
|
+
:step="step"
|
|
19
|
+
:disabled="disabled"
|
|
20
|
+
@input="$emit('update:modelValue', Number(($event.currentTarget as HTMLInputElement).value))"
|
|
21
|
+
/>
|
|
22
|
+
</label>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import config from "#config/ui";
|
|
27
|
+
import { computed } from "vue";
|
|
28
|
+
import type { ControlInteraction } from "../../components/control.ts";
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
modelValue = 0,
|
|
32
|
+
label = undefined,
|
|
33
|
+
min = 0,
|
|
34
|
+
max = 100,
|
|
35
|
+
step = 1,
|
|
36
|
+
disabled = false,
|
|
37
|
+
} = defineProps<{
|
|
38
|
+
modelValue?: number;
|
|
39
|
+
label?: string;
|
|
40
|
+
min?: number;
|
|
41
|
+
max?: number;
|
|
42
|
+
step?: number;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
defineEmits<{
|
|
47
|
+
"update:modelValue": [value: number];
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
const interaction = computed<ControlInteraction>(() => (disabled ? "disabled" : "enabled"));
|
|
51
|
+
|
|
52
|
+
const interactionVariants: Record<ControlInteraction, string> = {
|
|
53
|
+
disabled: "cursor-not-allowed opacity-50",
|
|
54
|
+
enabled: "",
|
|
55
|
+
};
|
|
56
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="inline-flex flex-col gap-2">
|
|
3
|
+
<span v-if="label" class="text-sm font-medium">{{ label }}</span>
|
|
4
|
+
|
|
5
|
+
<div class="flex items-center" role="radiogroup" :aria-label="label">
|
|
6
|
+
<Button
|
|
7
|
+
v-for="value in max"
|
|
8
|
+
:key="value"
|
|
9
|
+
class="text-2xl transition-transform hover:scale-110 disabled:hover:scale-100"
|
|
10
|
+
:class="variants[state(value)]"
|
|
11
|
+
size="icon"
|
|
12
|
+
variant="ghost"
|
|
13
|
+
role="radio"
|
|
14
|
+
:aria-checked="value === modelValue"
|
|
15
|
+
:aria-label="`${value} of ${max}`"
|
|
16
|
+
:disabled="readonly || disabled"
|
|
17
|
+
@click="$emit('update:modelValue', value)"
|
|
18
|
+
>
|
|
19
|
+
<Icon name="star" />
|
|
20
|
+
</Button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import Button from "./button.vue";
|
|
27
|
+
import Icon from "./icon.vue";
|
|
28
|
+
import type { RatingState } from "../../components/rating.ts";
|
|
29
|
+
|
|
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
|
+
}>();
|
|
43
|
+
|
|
44
|
+
defineEmits<{
|
|
45
|
+
"update:modelValue": [value: number];
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
function state(value: number): RatingState {
|
|
49
|
+
return value <= modelValue ? "active" : "inactive";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const variants: Record<RatingState, string> = {
|
|
53
|
+
active: "text-amber-400",
|
|
54
|
+
inactive: "text-neutral-300 dark:text-neutral-700",
|
|
55
|
+
};
|
|
56
|
+
</script>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col gap-2">
|
|
3
|
+
<span v-if="label" class="text-sm font-medium">
|
|
4
|
+
{{ label }}
|
|
5
|
+
</span>
|
|
6
|
+
|
|
7
|
+
<input v-if="name" type="hidden" :name="name" :value="modelValue" :required="required" />
|
|
8
|
+
|
|
9
|
+
<Popover
|
|
10
|
+
v-model="open"
|
|
11
|
+
class="w-full"
|
|
12
|
+
content-class="min-w-56 p-1"
|
|
13
|
+
trigger-class="w-full min-w-48"
|
|
14
|
+
trigger-layout="left"
|
|
15
|
+
match-trigger-width
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
>
|
|
18
|
+
<template #trigger>
|
|
19
|
+
<span class="min-w-0 flex-1 truncate text-left" :class="valueVariants[valueState]">
|
|
20
|
+
<slot name="value" :option="selected">
|
|
21
|
+
{{ selected?.label ?? placeholder ?? "Select" }}
|
|
22
|
+
</slot>
|
|
23
|
+
</span>
|
|
24
|
+
|
|
25
|
+
<Icon name="arrow-down-01" class="opacity-60" />
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<div role="listbox" :aria-label="label">
|
|
29
|
+
<Button
|
|
30
|
+
v-for="option in options"
|
|
31
|
+
:key="option.value"
|
|
32
|
+
class="w-full"
|
|
33
|
+
variant="ghost"
|
|
34
|
+
layout="left"
|
|
35
|
+
:disabled="option.disabled"
|
|
36
|
+
role="option"
|
|
37
|
+
:aria-selected="option.value === modelValue"
|
|
38
|
+
@click="select(option)"
|
|
39
|
+
>
|
|
40
|
+
<span class="flex w-full items-center justify-between gap-4 text-left">
|
|
41
|
+
<slot name="option" :option="option" :selected="option.value === modelValue">
|
|
42
|
+
{{ option.label }}
|
|
43
|
+
</slot>
|
|
44
|
+
|
|
45
|
+
<Icon v-if="option.value === modelValue" name="tick-02" />
|
|
46
|
+
</span>
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
</Popover>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { computed, ref } from "vue";
|
|
55
|
+
import type { SelectOption, SelectValue, SelectValueState } from "../../components/select.ts";
|
|
56
|
+
import Button from "./button.vue";
|
|
57
|
+
import Icon from "./icon.vue";
|
|
58
|
+
import Popover from "./popover.vue";
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
modelValue = undefined,
|
|
62
|
+
options,
|
|
63
|
+
label = undefined,
|
|
64
|
+
placeholder = undefined,
|
|
65
|
+
name = undefined,
|
|
66
|
+
required = false,
|
|
67
|
+
disabled = false,
|
|
68
|
+
} = defineProps<{
|
|
69
|
+
modelValue?: SelectValue;
|
|
70
|
+
options: Array<SelectOption>;
|
|
71
|
+
label?: string;
|
|
72
|
+
placeholder?: string;
|
|
73
|
+
name?: string;
|
|
74
|
+
required?: boolean;
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
}>();
|
|
77
|
+
|
|
78
|
+
const emit = defineEmits<{
|
|
79
|
+
"update:modelValue": [value: SelectValue];
|
|
80
|
+
}>();
|
|
81
|
+
|
|
82
|
+
const open = ref(false);
|
|
83
|
+
|
|
84
|
+
const selected = computed(() => options.find((option) => option.value === modelValue));
|
|
85
|
+
|
|
86
|
+
const valueState = computed<SelectValueState>(() => (selected.value ? "selected" : "placeholder"));
|
|
87
|
+
|
|
88
|
+
const valueVariants: Record<SelectValueState, string> = {
|
|
89
|
+
placeholder: "opacity-60",
|
|
90
|
+
selected: "",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function select(option: SelectOption) {
|
|
94
|
+
if (!option.disabled) {
|
|
95
|
+
emit("update:modelValue", option.value);
|
|
96
|
+
open.value = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="shrink-0"
|
|
4
|
+
:class="[`bg-${config.light.border}`, `dark:bg-${config.dark.border}`, variants[selected]]"
|
|
5
|
+
:role="decorative ? 'none' : 'separator'"
|
|
6
|
+
:aria-orientation="decorative ? undefined : selected"
|
|
7
|
+
/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import config from "#config/ui";
|
|
12
|
+
import { computed } from "vue";
|
|
13
|
+
import type { DividerVariant, SeparatorOrientation } from "../../components/separator.ts";
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
orientation = "horizontal",
|
|
17
|
+
variant = undefined,
|
|
18
|
+
decorative = false,
|
|
19
|
+
} = defineProps<{
|
|
20
|
+
orientation?: SeparatorOrientation;
|
|
21
|
+
variant?: DividerVariant;
|
|
22
|
+
decorative?: boolean;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const selected = computed(() => variant ?? orientation);
|
|
26
|
+
|
|
27
|
+
const variants: Record<SeparatorOrientation, string> = {
|
|
28
|
+
horizontal: "h-px w-full",
|
|
29
|
+
vertical: "h-full min-h-5 w-px",
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<dialog
|
|
3
|
+
ref="sheet"
|
|
4
|
+
class="m-0 h-full max-h-none max-w-none border-0 bg-transparent p-0 backdrop:bg-black/50"
|
|
5
|
+
:class="positions[side]"
|
|
6
|
+
@close="$emit('update:modelValue', false)"
|
|
7
|
+
@click="$event.target === $event.currentTarget && ($event.currentTarget as HTMLDialogElement).close()"
|
|
8
|
+
>
|
|
9
|
+
<Card :title="title" :description="description" class="h-full w-full shadow-2xl">
|
|
10
|
+
<template #actions>
|
|
11
|
+
<Button
|
|
12
|
+
size="icon"
|
|
13
|
+
variant="ghost"
|
|
14
|
+
icon-prefix="multiplication-sign"
|
|
15
|
+
:aria-label="closeLabel"
|
|
16
|
+
@click="sheet?.close()"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<div class="flex-1 overflow-auto">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<template v-if="$slots.footer" #footer>
|
|
25
|
+
<slot name="footer" />
|
|
26
|
+
</template>
|
|
27
|
+
</Card>
|
|
28
|
+
</dialog>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { useTemplateRef, watchEffect } from "vue";
|
|
33
|
+
import type { SheetSide } from "../../components/sheet.ts";
|
|
34
|
+
import Button from "./button.vue";
|
|
35
|
+
import Card from "./card.vue";
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
modelValue = false,
|
|
39
|
+
side = "right",
|
|
40
|
+
title = undefined,
|
|
41
|
+
description = undefined,
|
|
42
|
+
closeLabel = "Close",
|
|
43
|
+
} = defineProps<{
|
|
44
|
+
modelValue?: boolean;
|
|
45
|
+
side?: SheetSide;
|
|
46
|
+
title?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
closeLabel?: string;
|
|
49
|
+
}>();
|
|
50
|
+
|
|
51
|
+
defineEmits<{
|
|
52
|
+
"update:modelValue": [value: boolean];
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const sheet = useTemplateRef("sheet");
|
|
56
|
+
|
|
57
|
+
const positions: Record<SheetSide, string> = {
|
|
58
|
+
top: "inset-x-0 top-0 h-auto w-full max-h-[85vh]",
|
|
59
|
+
right: "inset-y-0 right-0 w-[min(28rem,90vw)]",
|
|
60
|
+
bottom: "inset-x-0 bottom-0 h-auto w-full max-h-[85vh]",
|
|
61
|
+
left: "inset-y-0 left-0 w-[min(28rem,90vw)]",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
watchEffect(() => {
|
|
65
|
+
if (modelValue && !sheet.value?.open) {
|
|
66
|
+
sheet.value?.showModal();
|
|
67
|
+
} else if (!modelValue && sheet.value?.open) {
|
|
68
|
+
sheet.value.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="inline-flex items-start gap-3" :class="interactionVariants[interaction]">
|
|
3
|
+
<input v-if="name" type="hidden" :name="name" :value="modelValue ? 'on' : 'off'" />
|
|
4
|
+
|
|
5
|
+
<button
|
|
6
|
+
class="relative mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed"
|
|
7
|
+
:class="[variants[state], `outline-${config.light.ring}`, `dark:outline-${config.dark.ring}`]"
|
|
8
|
+
type="button"
|
|
9
|
+
role="switch"
|
|
10
|
+
:aria-checked="modelValue"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
@click="$emit('update:modelValue', !modelValue)"
|
|
13
|
+
>
|
|
14
|
+
<span
|
|
15
|
+
class="block size-5 translate-x-0.5 rounded-full bg-white shadow transition-transform"
|
|
16
|
+
:class="thumbVariants[state]"
|
|
17
|
+
/>
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<span v-if="label || description || $slots.default" class="flex flex-col gap-0.5">
|
|
21
|
+
<span class="text-sm font-medium">
|
|
22
|
+
<slot> {{ label }} </slot>
|
|
23
|
+
</span>
|
|
24
|
+
|
|
25
|
+
<span v-if="description" class="text-sm opacity-60"> {{ description }} </span>
|
|
26
|
+
</span>
|
|
27
|
+
</label>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import config from "#config/ui";
|
|
32
|
+
import { computed } from "vue";
|
|
33
|
+
import type { ControlInteraction } from "../../components/control.ts";
|
|
34
|
+
import type { SwitchState } from "../../components/switch.ts";
|
|
35
|
+
|
|
36
|
+
const {
|
|
37
|
+
modelValue = false,
|
|
38
|
+
label = undefined,
|
|
39
|
+
description = undefined,
|
|
40
|
+
name = undefined,
|
|
41
|
+
disabled = false,
|
|
42
|
+
} = defineProps<{
|
|
43
|
+
modelValue?: boolean;
|
|
44
|
+
label?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
defineEmits<{
|
|
51
|
+
"update:modelValue": [value: boolean];
|
|
52
|
+
}>();
|
|
53
|
+
|
|
54
|
+
const interaction = computed<ControlInteraction>(() => (disabled ? "disabled" : "enabled"));
|
|
55
|
+
|
|
56
|
+
const state = computed<SwitchState>(() => (modelValue ? "checked" : "unchecked"));
|
|
57
|
+
|
|
58
|
+
const interactionVariants: Record<ControlInteraction, string> = {
|
|
59
|
+
disabled: "cursor-not-allowed opacity-50",
|
|
60
|
+
enabled: "cursor-pointer",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const thumbVariants: Record<SwitchState, string> = {
|
|
64
|
+
checked: "translate-x-5",
|
|
65
|
+
unchecked: "",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const variants: Record<SwitchState, Array<string>> = {
|
|
69
|
+
checked: [`bg-${config.light.primary}`, `dark:bg-${config.dark.primary}`],
|
|
70
|
+
unchecked: [`bg-${config.light.input}`, `dark:bg-${config.dark.input}`],
|
|
71
|
+
};
|
|
72
|
+
</script>
|