@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,2 @@
1
+ declare const _default: import("unbuild").BuildConfig[];
2
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { defineBuildConfig } from "unbuild";
2
+ export default defineBuildConfig({
3
+ entries: [
4
+ {
5
+ builder: "mkdist",
6
+ input: "./",
7
+ outDir: "./dist",
8
+ pattern: ["**/*.ts", "**/*.vue"],
9
+ declaration: true
10
+ }
11
+ ],
12
+ clean: true,
13
+ externals: ["vue", "zod", "clsx", "tailwind-merge"]
14
+ });
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div
3
+ role="alert"
4
+ :class="cn(
5
+ 'relative w-full rounded-lg border p-4',
6
+ variant_classes,
7
+ props.class
8
+ )"
9
+ >
10
+ <slot />
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from "vue"
16
+ import { cn } from "../../utils/cn"
17
+ import { useUI } from "../../composables/useUIConfig"
18
+ import type { AlertVariant } from "../../types/alert"
19
+
20
+ interface Props {
21
+ variant?: AlertVariant
22
+ class?: string
23
+ }
24
+
25
+ const props = defineProps<Props>()
26
+
27
+ const ui_config = useUI("alert")
28
+
29
+ const variant_classes = computed(() => {
30
+ const variant_map: Record<AlertVariant, string> = {
31
+ default: "bg-background text-foreground",
32
+ info: "border-blue-200 bg-blue-50 text-blue-900 [&>svg]:text-blue-600",
33
+ success: "border-green-200 bg-green-50 text-green-900 [&>svg]:text-green-600",
34
+ warning: "border-yellow-200 bg-yellow-50 text-yellow-900 [&>svg]:text-yellow-600",
35
+ destructive: "border-destructive/50 text-destructive bg-destructive/10 [&>svg]:text-destructive"
36
+ }
37
+ return variant_map[props.variant ?? ui_config.default_variant]
38
+ })
39
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { AlertDescriptionProps } from "../../types/alert"
10
+
11
+ const props = defineProps<AlertDescriptionProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
3
+ <slot />
4
+ </h5>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { AlertTitleProps } from "../../types/alert"
10
+
11
+ const props = defineProps<AlertTitleProps>()
12
+ </script>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <button
3
+ :class="composable.base_classes.value"
4
+ :disabled="composable.is_disabled.value"
5
+ v-bind="composable.aria_attributes.value"
6
+ @click="handleClick"
7
+ >
8
+ <slot name="icon-left" />
9
+ <span v-if="composable.is_loading.value" class="animate-spin">
10
+ <slot name="loading">
11
+ <svg
12
+ class="h-4 w-4"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ fill="none"
15
+ viewBox="0 0 24 24"
16
+ >
17
+ <circle
18
+ class="opacity-25"
19
+ cx="12"
20
+ cy="12"
21
+ r="10"
22
+ stroke="currentColor"
23
+ stroke-width="4"
24
+ />
25
+ <path
26
+ class="opacity-75"
27
+ fill="currentColor"
28
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
29
+ />
30
+ </svg>
31
+ </slot>
32
+ </span>
33
+ <slot />
34
+ <slot name="icon-right" />
35
+ </button>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { toRef } from "vue"
40
+ import { useButton } from "../../composables/useButton"
41
+ import type { ButtonProps } from "../../types/button"
42
+
43
+ const props = withDefaults(defineProps<ButtonProps>(), {
44
+ disabled: false,
45
+ loading: false
46
+ })
47
+
48
+ const emit = defineEmits<{
49
+ click: [event: MouseEvent]
50
+ }>()
51
+
52
+ const composable = useButton(toRef(() => props))
53
+
54
+ const handleClick = (event: MouseEvent) => {
55
+ if (!composable.is_disabled.value && !composable.is_loading.value) {
56
+ emit("click", event)
57
+ }
58
+ }
59
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div :class="composable.base_classes.value">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { toRef } from "vue"
9
+ import { useCard } from "../../composables/useCard"
10
+ import type { CardProps } from "../../types/card"
11
+
12
+ const props = defineProps<CardProps>()
13
+
14
+ const composable = useCard(toRef(() => props))
15
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('p-6 pt-0', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { CardContentProps } from "../../types/card"
10
+
11
+ const props = defineProps<CardContentProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <p :class="cn('text-sm text-muted-foreground', props.class)">
3
+ <slot />
4
+ </p>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { CardDescriptionProps } from "../../types/card"
10
+
11
+ const props = defineProps<CardDescriptionProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('flex items-center p-6 pt-0', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { CardFooterProps } from "../../types/card"
10
+
11
+ const props = defineProps<CardFooterProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('flex flex-col space-y-1.5 p-6', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { CardHeaderProps } from "../../types/card"
10
+
11
+ const props = defineProps<CardHeaderProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <h3 :class="cn('text-2xl font-semibold leading-none tracking-tight', props.class)">
3
+ <slot />
4
+ </h3>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { CardTitleProps } from "../../types/card"
10
+
11
+ const props = defineProps<CardTitleProps>()
12
+ </script>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ :class="composable.base_classes.value"
5
+ :disabled="composable.is_disabled.value"
6
+ :data-state="dataState"
7
+ v-bind="composable.aria_attributes.value"
8
+ @click="handleClick"
9
+ >
10
+ <span
11
+ v-if="modelValue || composable.is_indeterminate.value"
12
+ :class="composable.indicator_classes.value"
13
+ >
14
+ <svg
15
+ v-if="composable.is_indeterminate.value"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ viewBox="0 0 24 24"
18
+ fill="none"
19
+ stroke="currentColor"
20
+ stroke-width="3"
21
+ stroke-linecap="round"
22
+ >
23
+ <line x1="5" y1="12" x2="19" y2="12" />
24
+ </svg>
25
+ <svg
26
+ v-else
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ stroke-width="3"
32
+ stroke-linecap="round"
33
+ stroke-linejoin="round"
34
+ >
35
+ <polyline points="20 6 9 17 4 12" />
36
+ </svg>
37
+ </span>
38
+ </button>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import { computed, toRef } from "vue"
43
+ import { useCheckbox } from "../../composables/useCheckbox"
44
+ import type { CheckboxProps } from "../../types/checkbox"
45
+
46
+ interface Props extends CheckboxProps {
47
+ modelValue?: boolean
48
+ }
49
+
50
+ const props = withDefaults(defineProps<Props>(), {
51
+ modelValue: false,
52
+ disabled: false,
53
+ indeterminate: false
54
+ })
55
+
56
+ const emit = defineEmits<{
57
+ "update:modelValue": [value: boolean]
58
+ }>()
59
+
60
+ const checked = toRef(() => props.modelValue)
61
+ const composable = useCheckbox(toRef(() => props), checked)
62
+
63
+ const dataState = computed(() => {
64
+ if (props.indeterminate) return "indeterminate"
65
+ return props.modelValue ? "checked" : "unchecked"
66
+ })
67
+
68
+ const handleClick = () => {
69
+ if (!composable.is_disabled.value) {
70
+ emit("update:modelValue", !props.modelValue)
71
+ }
72
+ }
73
+ </script>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="dialog">
4
+ <div
5
+ v-if="is_open"
6
+ class="fixed inset-0 z-50 flex items-center justify-center"
7
+ >
8
+ <div
9
+ class="fixed inset-0 bg-black/80"
10
+ @click="handleOverlayClick"
11
+ />
12
+ <div
13
+ class="relative z-50 grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg"
14
+ role="dialog"
15
+ aria-modal="true"
16
+ >
17
+ <slot />
18
+ <button
19
+ v-if="show_close_button"
20
+ class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
21
+ @click="close"
22
+ >
23
+ <svg
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ width="24"
26
+ height="24"
27
+ viewBox="0 0 24 24"
28
+ fill="none"
29
+ stroke="currentColor"
30
+ stroke-width="2"
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"
33
+ class="h-4 w-4"
34
+ >
35
+ <path d="M18 6 6 18" />
36
+ <path d="m6 6 12 12" />
37
+ </svg>
38
+ <span class="sr-only">閉じる</span>
39
+ </button>
40
+ </div>
41
+ </div>
42
+ </Transition>
43
+ </Teleport>
44
+ </template>
45
+
46
+ <script setup lang="ts">
47
+ import { computed, provide, type InjectionKey } from "vue"
48
+
49
+ interface Props {
50
+ open?: boolean
51
+ modal?: boolean
52
+ showCloseButton?: boolean
53
+ }
54
+
55
+ const props = withDefaults(defineProps<Props>(), {
56
+ open: false,
57
+ modal: true,
58
+ showCloseButton: true
59
+ })
60
+
61
+ const emit = defineEmits<{
62
+ "update:open": [value: boolean]
63
+ }>()
64
+
65
+ const is_open = computed({
66
+ get: () => props.open,
67
+ set: (value) => emit("update:open", value)
68
+ })
69
+
70
+ const show_close_button = computed(() => props.showCloseButton)
71
+
72
+ const close = () => {
73
+ is_open.value = false
74
+ }
75
+
76
+ const handleOverlayClick = () => {
77
+ if (props.modal) {
78
+ close()
79
+ }
80
+ }
81
+
82
+ export interface DialogContext {
83
+ close: () => void
84
+ }
85
+
86
+ export const DialogKey: InjectionKey<DialogContext> = Symbol("dialog")
87
+
88
+ provide(DialogKey, { close })
89
+ </script>
90
+
91
+ <style scoped>
92
+ .dialog-enter-active,.dialog-leave-active{transition:opacity .2s ease}.dialog-enter-from,.dialog-leave-to{opacity:0}
93
+ </style>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <p :class="cn('text-sm text-muted-foreground', props.class)">
3
+ <slot />
4
+ </p>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { DialogDescriptionProps } from "../../types/dialog"
10
+
11
+ const props = defineProps<DialogDescriptionProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { DialogFooterProps } from "../../types/dialog"
10
+
11
+ const props = defineProps<DialogFooterProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div :class="cn('flex flex-col space-y-1.5 text-center sm:text-left', props.class)">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { DialogHeaderProps } from "../../types/dialog"
10
+
11
+ const props = defineProps<DialogHeaderProps>()
12
+ </script>
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <h2 :class="cn('text-lg font-semibold leading-none tracking-tight', props.class)">
3
+ <slot />
4
+ </h2>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+ import type { DialogTitleProps } from "../../types/dialog"
10
+
11
+ const props = defineProps<DialogTitleProps>()
12
+ </script>
@@ -0,0 +1,33 @@
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 { useDropdown, type UseDropdownReturn } from "../../composables/useDropdown"
10
+ import type { DropdownSide, DropdownAlign } from "../../types/dropdown"
11
+
12
+ export interface Props {
13
+ side?: DropdownSide
14
+ align?: DropdownAlign
15
+ class?: string
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ side: "bottom",
20
+ align: "start"
21
+ })
22
+
23
+ export const DROPDOWN_CONTEXT_KEY: InjectionKey<UseDropdownReturn> = Symbol("dropdown-context")
24
+
25
+ const dropdown_props = ref({
26
+ side: props.side,
27
+ align: props.align
28
+ })
29
+
30
+ const dropdown = useDropdown(dropdown_props as Ref<typeof dropdown_props.value>)
31
+
32
+ provide(DROPDOWN_CONTEXT_KEY, dropdown)
33
+ </script>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <Transition name="dropdown">
3
+ <div
4
+ v-if="is_open"
5
+ ref="content_element"
6
+ :id="dropdown_id"
7
+ role="menu"
8
+ :aria-labelledby="`${dropdown_id}-trigger`"
9
+ :style="position_styles"
10
+ :class="cn(base_classes, props.class)"
11
+ tabindex="-1"
12
+ @keydown="handleContentKeyDown"
13
+ >
14
+ <slot />
15
+ </div>
16
+ </Transition>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { inject, ref, watchEffect, nextTick, watch } from "vue"
21
+ import { cn } from "../../utils/cn"
22
+ import { DROPDOWN_CONTEXT_KEY } from "./DropdownMenu.vue"
23
+
24
+ export interface Props {
25
+ class?: string
26
+ }
27
+
28
+ const props = defineProps<Props>()
29
+
30
+ const context = inject(DROPDOWN_CONTEXT_KEY)
31
+
32
+ if (!context) {
33
+ throw new Error("DropdownMenuContent must be used within a DropdownMenu component")
34
+ }
35
+
36
+ const {
37
+ is_open,
38
+ content_ref,
39
+ dropdown_id,
40
+ position_styles,
41
+ handleContentKeyDown
42
+ } = context
43
+
44
+ const content_element = ref<HTMLElement | null>(null)
45
+
46
+ watchEffect(() => {
47
+ content_ref.value = content_element.value
48
+ })
49
+
50
+ watch(is_open, async (open) => {
51
+ if (open) {
52
+ await nextTick()
53
+ content_element.value?.focus()
54
+ }
55
+ })
56
+
57
+ const base_classes = cn(
58
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1",
59
+ "text-popover-foreground shadow-md outline-none",
60
+ "animate-in fade-in-0 zoom-in-95"
61
+ )
62
+ </script>
63
+
64
+ <style scoped>
65
+ .dropdown-enter-active,.dropdown-leave-active{transition:opacity .15s ease,transform .15s ease}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:scale(.95)}
66
+ </style>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div
3
+ role="menuitem"
4
+ :tabindex="disabled ? -1 : 0"
5
+ :class="cn(item_classes, props.class)"
6
+ :aria-disabled="disabled"
7
+ :data-highlighted="is_active ? '' : undefined"
8
+ :data-disabled="disabled ? '' : undefined"
9
+ @click="handleClick"
10
+ @mouseenter="handleMouseEnter"
11
+ @keydown.enter="handleClick"
12
+ @keydown.space.prevent="handleClick"
13
+ >
14
+ <slot />
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { inject, computed, onMounted } from "vue"
20
+ import { cn } from "../../utils/cn"
21
+ import { DROPDOWN_CONTEXT_KEY } from "./DropdownMenu.vue"
22
+
23
+ export interface Props {
24
+ disabled?: boolean
25
+ destructive?: boolean
26
+ class?: string
27
+ }
28
+
29
+ const props = withDefaults(defineProps<Props>(), {
30
+ disabled: false,
31
+ destructive: false
32
+ })
33
+
34
+ const emit = defineEmits<{
35
+ select: []
36
+ }>()
37
+
38
+ const context = inject(DROPDOWN_CONTEXT_KEY)
39
+
40
+ if (!context) {
41
+ throw new Error("DropdownMenuItem must be used within a DropdownMenu component")
42
+ }
43
+
44
+ const { active_item_index, close, registerItem, setActiveItem } = context
45
+
46
+ let item_index = -1
47
+
48
+ onMounted(() => {
49
+ if (!props.disabled) {
50
+ item_index = registerItem()
51
+ }
52
+ })
53
+
54
+ const is_active = computed(() => {
55
+ return active_item_index.value === item_index && !props.disabled
56
+ })
57
+
58
+ const handleClick = (): void => {
59
+ if (props.disabled) return
60
+ emit("select")
61
+ close()
62
+ }
63
+
64
+ const handleMouseEnter = (): void => {
65
+ if (!props.disabled && item_index >= 0) {
66
+ setActiveItem(item_index)
67
+ }
68
+ }
69
+
70
+ const item_classes = computed(() => cn(
71
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
72
+ "transition-colors focus:bg-accent focus:text-accent-foreground",
73
+ "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
74
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
75
+ props.destructive && "text-destructive focus:text-destructive"
76
+ ))
77
+ </script>
@@ -0,0 +1,20 @@
1
+ <template>
2
+ <div
3
+ :class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
4
+ >
5
+ <slot />
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { cn } from "../../utils/cn"
11
+
12
+ export interface Props {
13
+ inset?: boolean
14
+ class?: string
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ inset: false
19
+ })
20
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <div
3
+ role="separator"
4
+ :class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { cn } from "../../utils/cn"
10
+
11
+ export interface Props {
12
+ class?: string
13
+ }
14
+
15
+ const props = defineProps<Props>()
16
+ </script>