@soave/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/build.config.d.ts +2 -0
  2. package/dist/build.config.mjs +14 -0
  3. package/dist/components/ui/Alert.vue +39 -0
  4. package/dist/components/ui/AlertDescription.vue +12 -0
  5. package/dist/components/ui/AlertTitle.vue +12 -0
  6. package/dist/components/ui/Button.vue +59 -0
  7. package/dist/components/ui/Card.vue +15 -0
  8. package/dist/components/ui/CardContent.vue +12 -0
  9. package/dist/components/ui/CardDescription.vue +12 -0
  10. package/dist/components/ui/CardFooter.vue +12 -0
  11. package/dist/components/ui/CardHeader.vue +12 -0
  12. package/dist/components/ui/CardTitle.vue +12 -0
  13. package/dist/components/ui/Checkbox.vue +73 -0
  14. package/dist/components/ui/Dialog.vue +93 -0
  15. package/dist/components/ui/DialogDescription.vue +12 -0
  16. package/dist/components/ui/DialogFooter.vue +12 -0
  17. package/dist/components/ui/DialogHeader.vue +12 -0
  18. package/dist/components/ui/DialogTitle.vue +12 -0
  19. package/dist/components/ui/DropdownMenu.vue +33 -0
  20. package/dist/components/ui/DropdownMenuContent.vue +66 -0
  21. package/dist/components/ui/DropdownMenuItem.vue +77 -0
  22. package/dist/components/ui/DropdownMenuLabel.vue +20 -0
  23. package/dist/components/ui/DropdownMenuSeparator.vue +16 -0
  24. package/dist/components/ui/DropdownMenuTrigger.vue +38 -0
  25. package/dist/components/ui/FileInput.vue +153 -0
  26. package/dist/components/ui/FormError.vue +20 -0
  27. package/dist/components/ui/FormField.vue +12 -0
  28. package/dist/components/ui/FormInput.vue +46 -0
  29. package/dist/components/ui/FormLabel.vue +19 -0
  30. package/dist/components/ui/FormTextarea.vue +39 -0
  31. package/dist/components/ui/Input.vue +49 -0
  32. package/dist/components/ui/Popover.vue +36 -0
  33. package/dist/components/ui/PopoverContent.vue +62 -0
  34. package/dist/components/ui/PopoverTrigger.vue +36 -0
  35. package/dist/components/ui/RadioGroup.vue +42 -0
  36. package/dist/components/ui/RadioItem.vue +41 -0
  37. package/dist/components/ui/Select.vue +55 -0
  38. package/dist/components/ui/SelectContent.vue +29 -0
  39. package/dist/components/ui/SelectItem.vue +51 -0
  40. package/dist/components/ui/SelectTrigger.vue +38 -0
  41. package/dist/components/ui/SelectValue.vue +16 -0
  42. package/dist/components/ui/Sheet.vue +140 -0
  43. package/dist/components/ui/SheetDescription.vue +15 -0
  44. package/dist/components/ui/SheetFooter.vue +15 -0
  45. package/dist/components/ui/SheetHeader.vue +15 -0
  46. package/dist/components/ui/SheetTitle.vue +15 -0
  47. package/dist/components/ui/Switch.vue +43 -0
  48. package/dist/components/ui/Textarea.vue +50 -0
  49. package/dist/components/ui/Toast.vue +107 -0
  50. package/dist/components/ui/Toaster.vue +80 -0
  51. package/dist/components/ui/Tooltip.vue +42 -0
  52. package/dist/components/ui/TooltipContent.vue +68 -0
  53. package/dist/components/ui/TooltipTrigger.vue +39 -0
  54. package/dist/components/ui/UIProvider.vue +19 -0
  55. package/dist/components/ui/index.d.ts +52 -0
  56. package/dist/components/ui/index.mjs +52 -0
  57. package/dist/composables/index.d.ts +17 -0
  58. package/dist/composables/index.mjs +17 -0
  59. package/dist/composables/useButton.d.ts +8 -0
  60. package/dist/composables/useButton.mjs +49 -0
  61. package/dist/composables/useCard.d.ts +8 -0
  62. package/dist/composables/useCard.mjs +24 -0
  63. package/dist/composables/useCheckbox.d.ts +7 -0
  64. package/dist/composables/useCheckbox.mjs +51 -0
  65. package/dist/composables/useDialog.d.ts +6 -0
  66. package/dist/composables/useDialog.mjs +19 -0
  67. package/dist/composables/useDropdown.d.ts +24 -0
  68. package/dist/composables/useDropdown.mjs +170 -0
  69. package/dist/composables/useFileInput.d.ts +6 -0
  70. package/dist/composables/useFileInput.mjs +152 -0
  71. package/dist/composables/useForm.d.ts +7 -0
  72. package/dist/composables/useForm.mjs +159 -0
  73. package/dist/composables/useInput.d.ts +8 -0
  74. package/dist/composables/useInput.mjs +52 -0
  75. package/dist/composables/usePopover.d.ts +20 -0
  76. package/dist/composables/usePopover.mjs +113 -0
  77. package/dist/composables/useRadio.d.ts +7 -0
  78. package/dist/composables/useRadio.mjs +55 -0
  79. package/dist/composables/useSelect.d.ts +17 -0
  80. package/dist/composables/useSelect.mjs +71 -0
  81. package/dist/composables/useSwitch.d.ts +7 -0
  82. package/dist/composables/useSwitch.mjs +50 -0
  83. package/dist/composables/useTextarea.d.ts +7 -0
  84. package/dist/composables/useTextarea.mjs +50 -0
  85. package/dist/composables/useTheme.d.ts +15 -0
  86. package/dist/composables/useTheme.mjs +89 -0
  87. package/dist/composables/useToast.d.ts +11 -0
  88. package/dist/composables/useToast.mjs +64 -0
  89. package/dist/composables/useTooltip.d.ts +23 -0
  90. package/dist/composables/useTooltip.mjs +125 -0
  91. package/dist/composables/useUIConfig.d.ts +28 -0
  92. package/dist/composables/useUIConfig.mjs +36 -0
  93. package/dist/constants/errors.d.ts +22 -0
  94. package/dist/constants/errors.mjs +18 -0
  95. package/dist/constants/index.d.ts +2 -0
  96. package/dist/constants/index.mjs +2 -0
  97. package/dist/constants/logs.d.ts +17 -0
  98. package/dist/constants/logs.mjs +17 -0
  99. package/dist/index.d.ts +5 -0
  100. package/dist/index.mjs +5 -0
  101. package/dist/types/alert.d.ts +15 -0
  102. package/dist/types/alert.mjs +0 -0
  103. package/dist/types/button.d.ts +20 -0
  104. package/dist/types/button.mjs +0 -0
  105. package/dist/types/card.d.ts +23 -0
  106. package/dist/types/card.mjs +0 -0
  107. package/dist/types/checkbox.d.ts +19 -0
  108. package/dist/types/checkbox.mjs +0 -0
  109. package/dist/types/config.d.ts +30 -0
  110. package/dist/types/config.mjs +15 -0
  111. package/dist/types/dialog.d.ts +29 -0
  112. package/dist/types/dialog.mjs +0 -0
  113. package/dist/types/dropdown.d.ts +27 -0
  114. package/dist/types/dropdown.mjs +0 -0
  115. package/dist/types/file-input.d.ts +35 -0
  116. package/dist/types/file-input.mjs +0 -0
  117. package/dist/types/form.d.ts +70 -0
  118. package/dist/types/form.mjs +0 -0
  119. package/dist/types/index.d.ts +20 -0
  120. package/dist/types/index.mjs +20 -0
  121. package/dist/types/input.d.ts +27 -0
  122. package/dist/types/input.mjs +0 -0
  123. package/dist/types/popover.d.ts +15 -0
  124. package/dist/types/popover.mjs +0 -0
  125. package/dist/types/radio.d.ts +29 -0
  126. package/dist/types/radio.mjs +1 -0
  127. package/dist/types/select.d.ts +36 -0
  128. package/dist/types/select.mjs +1 -0
  129. package/dist/types/sheet.d.ts +11 -0
  130. package/dist/types/sheet.mjs +0 -0
  131. package/dist/types/switch.d.ts +17 -0
  132. package/dist/types/switch.mjs +0 -0
  133. package/dist/types/textarea.d.ts +25 -0
  134. package/dist/types/textarea.mjs +0 -0
  135. package/dist/types/theme.d.ts +43 -0
  136. package/dist/types/theme.mjs +42 -0
  137. package/dist/types/toast.d.ts +38 -0
  138. package/dist/types/toast.mjs +0 -0
  139. package/dist/types/tooltip.d.ts +25 -0
  140. package/dist/types/tooltip.mjs +0 -0
  141. package/dist/types/utils.d.ts +12 -0
  142. package/dist/types/utils.mjs +0 -0
  143. package/dist/utils/cn.d.ts +6 -0
  144. package/dist/utils/cn.mjs +5 -0
  145. package/dist/utils/deepMerge.d.ts +6 -0
  146. package/dist/utils/deepMerge.mjs +18 -0
  147. package/dist/utils/index.d.ts +2 -0
  148. package/dist/utils/index.mjs +2 -0
  149. package/package.json +53 -0
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <button
3
+ ref="trigger_element"
4
+ type="button"
5
+ :aria-expanded="is_open"
6
+ :aria-controls="dropdown_id"
7
+ :aria-haspopup="true"
8
+ @click="handleTriggerClick"
9
+ @keydown="handleTriggerKeyDown"
10
+ >
11
+ <slot />
12
+ </button>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { inject, ref, watchEffect } from "vue"
17
+ import { DROPDOWN_CONTEXT_KEY } from "./DropdownMenu.vue"
18
+
19
+ const context = inject(DROPDOWN_CONTEXT_KEY)
20
+
21
+ if (!context) {
22
+ throw new Error("DropdownMenuTrigger must be used within a DropdownMenu component")
23
+ }
24
+
25
+ const {
26
+ is_open,
27
+ trigger_ref,
28
+ dropdown_id,
29
+ handleTriggerClick,
30
+ handleTriggerKeyDown
31
+ } = context
32
+
33
+ const trigger_element = ref<HTMLElement | null>(null)
34
+
35
+ watchEffect(() => {
36
+ trigger_ref.value = trigger_element.value
37
+ })
38
+ </script>
@@ -0,0 +1,153 @@
1
+ <template>
2
+ <div :class="composable.base_classes.value">
3
+ <input
4
+ ref="input_ref"
5
+ type="file"
6
+ :accept="accept"
7
+ :multiple="multiple"
8
+ :disabled="composable.is_disabled.value"
9
+ class="sr-only"
10
+ @change="handleChange"
11
+ />
12
+
13
+ <div
14
+ :class="composable.dropzone_classes.value"
15
+ v-bind="composable.aria_attributes.value"
16
+ @click="composable.openFilePicker"
17
+ @dragenter="composable.handleDragEnter"
18
+ @dragleave="composable.handleDragLeave"
19
+ @dragover.prevent
20
+ @drop="composable.handleDrop"
21
+ >
22
+ <slot name="dropzone">
23
+ <svg
24
+ class="w-10 h-10 mb-3 text-muted-foreground"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ stroke-width="1.5"
30
+ stroke-linecap="round"
31
+ stroke-linejoin="round"
32
+ >
33
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
34
+ <polyline points="17 8 12 3 7 8" />
35
+ <line x1="12" y1="3" x2="12" y2="15" />
36
+ </svg>
37
+ <p class="text-sm text-muted-foreground">
38
+ <span class="font-semibold text-primary">クリック</span>
39
+ または ドラッグ&ドロップ
40
+ </p>
41
+ <p v-if="accept" class="mt-1 text-xs text-muted-foreground">
42
+ {{ accept }}
43
+ </p>
44
+ </slot>
45
+ </div>
46
+
47
+ <p
48
+ v-if="composable.error.value"
49
+ class="mt-2 text-sm text-destructive"
50
+ >
51
+ {{ composable.error.value }}
52
+ </p>
53
+
54
+ <ul
55
+ v-if="composable.files.value.length > 0"
56
+ class="mt-4 space-y-2"
57
+ >
58
+ <li
59
+ v-for="(file, index) in composable.files.value"
60
+ :key="file.name + index"
61
+ class="flex items-center gap-3 p-2 rounded-md border bg-muted/50"
62
+ >
63
+ <img
64
+ v-if="file.preview_url"
65
+ :src="file.preview_url"
66
+ :alt="file.name"
67
+ class="w-10 h-10 object-cover rounded"
68
+ />
69
+ <svg
70
+ v-else
71
+ class="w-10 h-10 text-muted-foreground"
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ stroke-width="1.5"
77
+ >
78
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
79
+ <polyline points="14 2 14 8 20 8" />
80
+ </svg>
81
+ <div class="flex-1 min-w-0">
82
+ <p class="text-sm font-medium truncate">{{ file.name }}</p>
83
+ <p class="text-xs text-muted-foreground">
84
+ {{ formatSize(file.size) }}
85
+ </p>
86
+ </div>
87
+ <button
88
+ type="button"
89
+ class="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
90
+ @click.stop="composable.removeFile(index)"
91
+ >
92
+ <svg
93
+ class="w-4 h-4"
94
+ xmlns="http://www.w3.org/2000/svg"
95
+ viewBox="0 0 24 24"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ >
100
+ <line x1="18" y1="6" x2="6" y2="18" />
101
+ <line x1="6" y1="6" x2="18" y2="18" />
102
+ </svg>
103
+ </button>
104
+ </li>
105
+ </ul>
106
+ </div>
107
+ </template>
108
+
109
+ <script setup lang="ts">
110
+ import { ref, toRef, watch } from "vue"
111
+ import { useFileInput } from "../../composables/useFileInput"
112
+ import type { FileInputProps, FileInfo } from "../../types/file-input"
113
+
114
+ interface Props extends FileInputProps {
115
+ modelValue?: File | File[] | null
116
+ }
117
+
118
+ const props = withDefaults(defineProps<Props>(), {
119
+ multiple: false,
120
+ disabled: false
121
+ })
122
+
123
+ const emit = defineEmits<{
124
+ "update:modelValue": [value: File | File[] | null]
125
+ }>()
126
+
127
+ const input_ref = ref<HTMLInputElement | null>(null)
128
+ const composable = useFileInput(toRef(() => props), input_ref)
129
+
130
+ const formatSize = (bytes: number): string => {
131
+ if (bytes < 1024) return `${bytes}B`
132
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
133
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
134
+ }
135
+
136
+ const handleChange = (event: Event) => {
137
+ const target = event.target as HTMLInputElement
138
+ composable.handleFiles(target.files)
139
+ target.value = ""
140
+ }
141
+
142
+ watch(
143
+ () => composable.files.value,
144
+ (new_files: FileInfo[]) => {
145
+ if (props.multiple) {
146
+ emit("update:modelValue", new_files.map(f => f.file))
147
+ } else {
148
+ emit("update:modelValue", new_files[0]?.file ?? null)
149
+ }
150
+ },
151
+ { deep: true }
152
+ )
153
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <p
3
+ v-if="$slots.default"
4
+ :id="id"
5
+ :class="cn('text-sm font-medium text-destructive', props.class)"
6
+ >
7
+ <slot />
8
+ </p>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import { cn } from "../../utils/cn"
13
+ import type { FormErrorProps } from "../../types/form"
14
+
15
+ interface Props extends FormErrorProps {
16
+ id?: string
17
+ }
18
+
19
+ const props = defineProps<Props>()
20
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('space-y-2', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { FormFieldProps } from "../../types/form"
10
+
11
+ const props = defineProps<FormFieldProps>()
12
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <Input
3
+ :id="id"
4
+ :type="type"
5
+ :size="size"
6
+ :placeholder="placeholder"
7
+ :disabled="disabled"
8
+ :readonly="readonly"
9
+ :error="error"
10
+ :error_id="error_id"
11
+ :model-value="modelValue"
12
+ :class="props.class"
13
+ @update:model-value="emit('update:modelValue', $event)"
14
+ @blur="emit('blur', $event)"
15
+ />
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import Input from "./Input.vue"
20
+ import type { InputType, InputSize } from "../../types/input"
21
+
22
+ interface Props {
23
+ id?: string
24
+ type?: InputType
25
+ size?: InputSize
26
+ placeholder?: string
27
+ disabled?: boolean
28
+ readonly?: boolean
29
+ error?: string
30
+ error_id?: string
31
+ modelValue?: string | number
32
+ class?: string
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ type: "text",
37
+ size: "md",
38
+ disabled: false,
39
+ readonly: false
40
+ })
41
+
42
+ const emit = defineEmits<{
43
+ "update:modelValue": [value: string | number]
44
+ blur: [event: FocusEvent]
45
+ }>()
46
+ </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <label
3
+ :for="props.for"
4
+ :class="cn(
5
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
6
+ props.class
7
+ )"
8
+ >
9
+ <slot />
10
+ <span v-if="props.required" class="text-destructive ml-1">*</span>
11
+ </label>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { cn } from "../../utils/cn"
16
+ import type { FormLabelProps } from "../../types/form"
17
+
18
+ const props = defineProps<FormLabelProps>()
19
+ </script>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <Textarea
3
+ :id="id"
4
+ :placeholder="placeholder"
5
+ :disabled="disabled"
6
+ :readonly="readonly"
7
+ :error="error"
8
+ :error_id="error_id"
9
+ :rows="rows"
10
+ :resize="resize"
11
+ :model-value="modelValue"
12
+ :class="props.class"
13
+ @update:model-value="emit('update:modelValue', $event)"
14
+ @blur="emit('blur', $event)"
15
+ />
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import Textarea from "./Textarea.vue"
20
+ import type { TextareaProps } from "../../types/textarea"
21
+
22
+ interface Props extends TextareaProps {
23
+ id?: string
24
+ modelValue?: string
25
+ class?: string
26
+ }
27
+
28
+ const props = withDefaults(defineProps<Props>(), {
29
+ rows: 3,
30
+ resize: "vertical",
31
+ disabled: false,
32
+ readonly: false
33
+ })
34
+
35
+ const emit = defineEmits<{
36
+ "update:modelValue": [value: string]
37
+ blur: [event: FocusEvent]
38
+ }>()
39
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <input
3
+ :id="id"
4
+ :type="type"
5
+ :class="composable.base_classes.value"
6
+ :disabled="composable.is_disabled.value"
7
+ :readonly="composable.is_readonly.value"
8
+ :placeholder="placeholder"
9
+ :value="modelValue"
10
+ v-bind="composable.aria_attributes.value"
11
+ @input="handleInput"
12
+ @focus="composable.handleFocus"
13
+ @blur="handleBlur"
14
+ />
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { toRef } from "vue"
19
+ import { useInput } from "../../composables/useInput"
20
+ import type { InputProps, InputType } from "../../types/input"
21
+
22
+ interface Props extends InputProps {
23
+ id?: string
24
+ modelValue?: string | number
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ type: "text" as InputType,
29
+ disabled: false,
30
+ readonly: false
31
+ })
32
+
33
+ const emit = defineEmits<{
34
+ "update:modelValue": [value: string | number]
35
+ blur: [event: FocusEvent]
36
+ }>()
37
+
38
+ const composable = useInput(toRef(() => props))
39
+
40
+ const handleInput = (event: Event) => {
41
+ const target = event.target as HTMLInputElement
42
+ emit("update:modelValue", target.value)
43
+ }
44
+
45
+ const handleBlur = (event: FocusEvent) => {
46
+ composable.handleBlur()
47
+ emit("blur", event)
48
+ }
49
+ </script>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <div class="relative inline-block" :class="props.class">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { provide, ref, type InjectionKey, type Ref } from "vue"
9
+ import { usePopover, type UsePopoverReturn } from "../../composables/usePopover"
10
+ import type { PopoverSide, PopoverAlign } from "../../types/popover"
11
+
12
+ export interface Props {
13
+ side?: PopoverSide
14
+ align?: PopoverAlign
15
+ modal?: boolean
16
+ class?: string
17
+ }
18
+
19
+ const props = withDefaults(defineProps<Props>(), {
20
+ side: "bottom",
21
+ align: "center",
22
+ modal: false
23
+ })
24
+
25
+ export const POPOVER_CONTEXT_KEY: InjectionKey<UsePopoverReturn> = Symbol("popover-context")
26
+
27
+ const popover_props = ref({
28
+ side: props.side,
29
+ align: props.align,
30
+ modal: props.modal
31
+ })
32
+
33
+ const popover = usePopover(popover_props as Ref<typeof popover_props.value>)
34
+
35
+ provide(POPOVER_CONTEXT_KEY, popover)
36
+ </script>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <Transition name="popover">
3
+ <div
4
+ v-if="is_open"
5
+ ref="content_element"
6
+ :id="popover_id"
7
+ role="dialog"
8
+ aria-modal="false"
9
+ :style="position_styles"
10
+ :class="cn(base_classes, props.class)"
11
+ tabindex="-1"
12
+ >
13
+ <slot />
14
+ </div>
15
+ </Transition>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { inject, ref, watchEffect, onMounted } from "vue"
20
+ import { cn } from "../../utils/cn"
21
+ import { POPOVER_CONTEXT_KEY } from "./Popover.vue"
22
+
23
+ export interface Props {
24
+ class?: string
25
+ }
26
+
27
+ const props = defineProps<Props>()
28
+
29
+ const context = inject(POPOVER_CONTEXT_KEY)
30
+
31
+ if (!context) {
32
+ throw new Error("PopoverContent must be used within a Popover component")
33
+ }
34
+
35
+ const {
36
+ is_open,
37
+ content_ref,
38
+ popover_id,
39
+ position_styles
40
+ } = context
41
+
42
+ const content_element = ref<HTMLElement | null>(null)
43
+
44
+ watchEffect(() => {
45
+ content_ref.value = content_element.value
46
+ })
47
+
48
+ onMounted(() => {
49
+ if (is_open.value && content_element.value) {
50
+ content_element.value.focus()
51
+ }
52
+ })
53
+
54
+ const base_classes = cn(
55
+ "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
56
+ "animate-in fade-in-0 zoom-in-95"
57
+ )
58
+ </script>
59
+
60
+ <style scoped>
61
+ .popover-enter-active,.popover-leave-active{transition:opacity .2s ease,transform .2s ease}.popover-enter-from,.popover-leave-to{opacity:0;transform:scale(.95)}
62
+ </style>
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <button
3
+ ref="trigger_element"
4
+ type="button"
5
+ :aria-expanded="is_open"
6
+ :aria-controls="popover_id"
7
+ :aria-haspopup="true"
8
+ @click="handleTriggerClick"
9
+ >
10
+ <slot />
11
+ </button>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { inject, ref, watchEffect } from "vue"
16
+ import { POPOVER_CONTEXT_KEY } from "./Popover.vue"
17
+
18
+ const context = inject(POPOVER_CONTEXT_KEY)
19
+
20
+ if (!context) {
21
+ throw new Error("PopoverTrigger must be used within a Popover component")
22
+ }
23
+
24
+ const {
25
+ is_open,
26
+ trigger_ref,
27
+ popover_id,
28
+ handleTriggerClick
29
+ } = context
30
+
31
+ const trigger_element = ref<HTMLElement | null>(null)
32
+
33
+ watchEffect(() => {
34
+ trigger_ref.value = trigger_element.value
35
+ })
36
+ </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div
3
+ role="radiogroup"
4
+ :class="cn(
5
+ 'grid gap-2',
6
+ orientation === 'horizontal' && 'grid-flow-col'
7
+ )"
8
+ >
9
+ <slot />
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { provide, toRef } from "vue"
15
+ import { cn } from "../../utils/cn"
16
+ import type { RadioGroupProps, RadioGroupContext } from "../../types/radio"
17
+ import { RADIO_GROUP_KEY } from "../../types/radio"
18
+
19
+ interface Props extends RadioGroupProps {
20
+ modelValue?: string
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ modelValue: "",
25
+ disabled: false,
26
+ orientation: "vertical"
27
+ })
28
+
29
+ const emit = defineEmits<{
30
+ "update:modelValue": [value: string]
31
+ }>()
32
+
33
+ const context: RadioGroupContext = {
34
+ model_value: toRef(() => props.modelValue),
35
+ disabled: toRef(() => props.disabled),
36
+ updateValue: (value: string) => {
37
+ emit("update:modelValue", value)
38
+ }
39
+ }
40
+
41
+ provide(RADIO_GROUP_KEY, context)
42
+ </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ :class="composable.base_classes.value"
5
+ :disabled="composable.is_disabled.value"
6
+ v-bind="composable.aria_attributes.value"
7
+ @click="handleClick"
8
+ >
9
+ <span
10
+ v-if="composable.is_checked.value"
11
+ :class="composable.indicator_classes.value"
12
+ >
13
+ <svg
14
+ class="fill-current"
15
+ viewBox="0 0 24 24"
16
+ >
17
+ <circle cx="12" cy="12" r="6" />
18
+ </svg>
19
+ </span>
20
+ </button>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { inject, toRef } from "vue"
25
+ import { useRadioItem } from "../../composables/useRadio"
26
+ import type { RadioItemProps, RadioGroupContext } from "../../types/radio"
27
+ import { RADIO_GROUP_KEY } from "../../types/radio"
28
+
29
+ const props = withDefaults(defineProps<RadioItemProps>(), {
30
+ disabled: false
31
+ })
32
+
33
+ const context = inject<RadioGroupContext>(RADIO_GROUP_KEY)
34
+ const composable = useRadioItem(toRef(() => props))
35
+
36
+ const handleClick = () => {
37
+ if (!composable.is_disabled.value && context) {
38
+ context.updateValue(props.value)
39
+ }
40
+ }
41
+ </script>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="relative">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { provide, ref, toRef } from "vue"
9
+ import type { SelectProps, SelectContext, SelectSize } from "../../types/select"
10
+ import { SELECT_KEY } from "../../types/select"
11
+
12
+ interface Props extends SelectProps {
13
+ modelValue?: string
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ modelValue: "",
18
+ size: "md" as SelectSize,
19
+ disabled: false,
20
+ placeholder: "Select..."
21
+ })
22
+
23
+ const emit = defineEmits<{
24
+ "update:modelValue": [value: string]
25
+ }>()
26
+
27
+ const is_open = ref(false)
28
+
29
+ const context: SelectContext = {
30
+ model_value: toRef(() => props.modelValue),
31
+ is_open,
32
+ disabled: toRef(() => props.disabled),
33
+ size: toRef(() => props.size ?? "md"),
34
+ placeholder: toRef(() => props.placeholder ?? "Select..."),
35
+ updateValue: (value: string) => {
36
+ emit("update:modelValue", value)
37
+ is_open.value = false
38
+ },
39
+ open: () => {
40
+ if (!props.disabled) {
41
+ is_open.value = true
42
+ }
43
+ },
44
+ close: () => {
45
+ is_open.value = false
46
+ },
47
+ toggle: () => {
48
+ if (!props.disabled) {
49
+ is_open.value = !is_open.value
50
+ }
51
+ }
52
+ }
53
+
54
+ provide(SELECT_KEY, context)
55
+ </script>
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ v-if="context?.is_open.value"
5
+ class="fixed inset-0 z-40"
6
+ @click="context?.close()"
7
+ />
8
+ <div
9
+ v-if="context?.is_open.value"
10
+ :class="composable.base_classes.value"
11
+ :data-state="context?.is_open.value ? 'open' : 'closed'"
12
+ role="listbox"
13
+ >
14
+ <div class="p-1">
15
+ <slot />
16
+ </div>
17
+ </div>
18
+ </Teleport>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { inject } from "vue"
23
+ import { useSelectContent } from "../../composables/useSelect"
24
+ import type { SelectContext } from "../../types/select"
25
+ import { SELECT_KEY } from "../../types/select"
26
+
27
+ const context = inject<SelectContext>(SELECT_KEY)
28
+ const composable = useSelectContent()
29
+ </script>