@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.
Files changed (82) hide show
  1. package/dist/components/breadcrumb.d.ts +4 -0
  2. package/dist/components/breadcrumb.js +1 -0
  3. package/dist/components/button.d.ts +18 -0
  4. package/dist/components/button.js +1 -0
  5. package/dist/components/card.d.ts +5 -0
  6. package/dist/components/card.js +1 -0
  7. package/dist/components/checkbox.d.ts +8 -0
  8. package/dist/components/checkbox.js +1 -0
  9. package/dist/components/collection.d.ts +6 -0
  10. package/dist/components/collection.js +1 -0
  11. package/dist/components/container.d.ts +4 -0
  12. package/dist/components/container.js +1 -0
  13. package/dist/components/control.d.ts +1 -0
  14. package/dist/components/control.js +1 -0
  15. package/dist/components/counter.d.ts +8 -0
  16. package/dist/components/counter.js +1 -0
  17. package/dist/components/dialog.d.ts +6 -0
  18. package/dist/components/dialog.js +1 -0
  19. package/dist/components/field.d.ts +7 -0
  20. package/dist/components/field.js +1 -0
  21. package/dist/components/heading.d.ts +7 -0
  22. package/dist/components/heading.js +1 -0
  23. package/dist/components/icon.d.ts +3 -0
  24. package/dist/components/icon.js +1 -0
  25. package/dist/components/input.d.ts +12 -0
  26. package/dist/components/input.js +1 -0
  27. package/dist/components/link.d.ts +6 -0
  28. package/dist/components/link.js +1 -0
  29. package/dist/components/navigate.d.ts +7 -0
  30. package/dist/components/navigate.js +1 -0
  31. package/dist/components/popover.d.ts +10 -0
  32. package/dist/components/popover.js +1 -0
  33. package/dist/components/progress.d.ts +5 -0
  34. package/dist/components/progress.js +1 -0
  35. package/dist/components/radio.d.ts +10 -0
  36. package/dist/components/radio.js +1 -0
  37. package/dist/components/range.d.ts +8 -0
  38. package/dist/components/range.js +1 -0
  39. package/dist/components/rating.d.ts +8 -0
  40. package/dist/components/rating.js +1 -0
  41. package/dist/components/select.d.ts +16 -0
  42. package/dist/components/select.js +1 -0
  43. package/dist/components/separator.d.ts +11 -0
  44. package/dist/components/separator.js +1 -0
  45. package/dist/components/sheet.d.ts +8 -0
  46. package/dist/components/sheet.js +1 -0
  47. package/dist/components/switch.d.ts +8 -0
  48. package/dist/components/switch.js +1 -0
  49. package/dist/components/upload.d.ts +10 -0
  50. package/dist/components/upload.js +1 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.js +1 -0
  53. package/dist/module/index.d.ts +35 -0
  54. package/dist/module/index.js +123 -0
  55. package/dist/types/index.d.ts +20 -0
  56. package/dist/vue/components/breadcrumb.vue +20 -0
  57. package/dist/vue/components/button.vue +124 -0
  58. package/dist/vue/components/card.vue +53 -0
  59. package/dist/vue/components/checkbox.vue +54 -0
  60. package/dist/vue/components/collection.vue +51 -0
  61. package/dist/vue/components/container.vue +19 -0
  62. package/dist/vue/components/counter.vue +61 -0
  63. package/dist/vue/components/dialog.vue +76 -0
  64. package/dist/vue/components/field.vue +38 -0
  65. package/dist/vue/components/heading.vue +34 -0
  66. package/dist/vue/components/icon.vue +49 -0
  67. package/dist/vue/components/input.vue +82 -0
  68. package/dist/vue/components/link.vue +33 -0
  69. package/dist/vue/components/navigate.vue +30 -0
  70. package/dist/vue/components/popover.vue +123 -0
  71. package/dist/vue/components/progress.vue +44 -0
  72. package/dist/vue/components/radio.vue +59 -0
  73. package/dist/vue/components/range.vue +56 -0
  74. package/dist/vue/components/rating.vue +56 -0
  75. package/dist/vue/components/select.vue +99 -0
  76. package/dist/vue/components/separator.vue +31 -0
  77. package/dist/vue/components/sheet.vue +71 -0
  78. package/dist/vue/components/switch.vue +72 -0
  79. package/dist/vue/components/upload.vue +133 -0
  80. package/dist/vue/index.d.ts +25 -0
  81. package/dist/vue/index.js +25 -0
  82. 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>