@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,51 @@
1
+ <template>
2
+ <div
3
+ :class="composable.base_classes.value"
4
+ :data-disabled="composable.is_disabled.value ? '' : undefined"
5
+ role="option"
6
+ :aria-selected="composable.is_selected.value"
7
+ @click="handleClick"
8
+ >
9
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
10
+ <svg
11
+ v-if="composable.is_selected.value"
12
+ class="h-4 w-4"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width="2"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ >
21
+ <polyline points="20 6 9 17 4 12" />
22
+ </svg>
23
+ </span>
24
+ <slot />
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { inject, toRef } from "vue"
30
+ import { useSelectItem } from "../../composables/useSelect"
31
+ import type { SelectContext } from "../../types/select"
32
+ import { SELECT_KEY } from "../../types/select"
33
+
34
+ interface Props {
35
+ value: string
36
+ disabled?: boolean
37
+ }
38
+
39
+ const props = withDefaults(defineProps<Props>(), {
40
+ disabled: false
41
+ })
42
+
43
+ const context = inject<SelectContext>(SELECT_KEY)
44
+ const composable = useSelectItem(toRef(() => props))
45
+
46
+ const handleClick = () => {
47
+ if (!composable.is_disabled.value && context) {
48
+ context.updateValue(props.value)
49
+ }
50
+ }
51
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ :class="composable.base_classes.value"
5
+ :disabled="composable.is_disabled.value"
6
+ :aria-expanded="context?.is_open.value"
7
+ aria-haspopup="listbox"
8
+ @click="handleClick"
9
+ >
10
+ <slot />
11
+ <svg
12
+ class="h-4 w-4 opacity-50"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width="2"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ >
21
+ <path d="m6 9 6 6 6-6" />
22
+ </svg>
23
+ </button>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { inject } from "vue"
28
+ import { useSelectTrigger } from "../../composables/useSelect"
29
+ import type { SelectContext } from "../../types/select"
30
+ import { SELECT_KEY } from "../../types/select"
31
+
32
+ const context = inject<SelectContext>(SELECT_KEY)
33
+ const composable = useSelectTrigger()
34
+
35
+ const handleClick = () => {
36
+ context?.toggle()
37
+ }
38
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <span :class="!hasValue && 'text-muted-foreground'">
3
+ <slot v-if="hasValue" />
4
+ <template v-else>{{ context?.placeholder.value }}</template>
5
+ </span>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { computed, inject } from "vue"
10
+ import type { SelectContext } from "../../types/select"
11
+ import { SELECT_KEY } from "../../types/select"
12
+
13
+ const context = inject<SelectContext>(SELECT_KEY)
14
+
15
+ const hasValue = computed(() => !!context?.model_value.value)
16
+ </script>
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="sheet-overlay">
4
+ <div
5
+ v-if="is_open"
6
+ class="fixed inset-0 z-50 bg-black/80"
7
+ @click="handleOverlayClick"
8
+ />
9
+ </Transition>
10
+
11
+ <Transition :name="transition_name">
12
+ <div
13
+ v-if="is_open"
14
+ ref="sheet_element"
15
+ role="dialog"
16
+ aria-modal="true"
17
+ :class="cn(base_classes, side_classes[side], props.class)"
18
+ tabindex="-1"
19
+ @keydown.escape="close"
20
+ >
21
+ <slot />
22
+
23
+ <button
24
+ v-if="show_close_button"
25
+ type="button"
26
+ 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"
27
+ aria-label="Close"
28
+ @click="close"
29
+ >
30
+ <svg
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ width="24"
33
+ height="24"
34
+ viewBox="0 0 24 24"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ stroke-width="2"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
40
+ >
41
+ <path d="M18 6 6 18" />
42
+ <path d="m6 6 12 12" />
43
+ </svg>
44
+ </button>
45
+ </div>
46
+ </Transition>
47
+ </Teleport>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { ref, computed, watch, provide, onMounted, onUnmounted, type InjectionKey } from "vue"
52
+ import { cn } from "../../utils/cn"
53
+ import type { SheetSide, SheetContext } from "../../types/sheet"
54
+
55
+ export interface Props {
56
+ open?: boolean
57
+ side?: SheetSide
58
+ show_close_button?: boolean
59
+ class?: string
60
+ }
61
+
62
+ const props = withDefaults(defineProps<Props>(), {
63
+ open: false,
64
+ side: "right",
65
+ show_close_button: true
66
+ })
67
+
68
+ const emit = defineEmits<{
69
+ "update:open": [value: boolean]
70
+ }>()
71
+
72
+ export const SHEET_CONTEXT_KEY: InjectionKey<SheetContext> = Symbol("sheet-context")
73
+
74
+ const is_open = ref(props.open)
75
+ const sheet_element = ref<HTMLElement | null>(null)
76
+
77
+ watch(() => props.open, (value) => {
78
+ is_open.value = value
79
+ })
80
+
81
+ const open = (): void => {
82
+ is_open.value = true
83
+ emit("update:open", true)
84
+ }
85
+
86
+ const close = (): void => {
87
+ is_open.value = false
88
+ emit("update:open", false)
89
+ }
90
+
91
+ const handleOverlayClick = (): void => {
92
+ close()
93
+ }
94
+
95
+ const handleKeyDown = (event: KeyboardEvent): void => {
96
+ if (event.key === "Escape" && is_open.value) {
97
+ close()
98
+ }
99
+ }
100
+
101
+ onMounted(() => {
102
+ document.addEventListener("keydown", handleKeyDown)
103
+ })
104
+
105
+ onUnmounted(() => {
106
+ document.removeEventListener("keydown", handleKeyDown)
107
+ })
108
+
109
+ watch(is_open, (open) => {
110
+ if (open) {
111
+ document.body.style.overflow = "hidden"
112
+ } else {
113
+ document.body.style.overflow = ""
114
+ }
115
+ })
116
+
117
+ const side = computed(() => props.side)
118
+
119
+ const transition_name = computed(() => `sheet-${side.value}`)
120
+
121
+ provide(SHEET_CONTEXT_KEY, {
122
+ is_open: is_open.value,
123
+ side: side.value,
124
+ open,
125
+ close
126
+ })
127
+
128
+ const base_classes = "fixed z-50 gap-4 bg-background p-6 shadow-lg"
129
+
130
+ const side_classes: Record<SheetSide, string> = {
131
+ top: "inset-x-0 top-0 border-b",
132
+ bottom: "inset-x-0 bottom-0 border-t",
133
+ left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
134
+ right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm"
135
+ }
136
+ </script>
137
+
138
+ <style scoped>
139
+ .sheet-overlay-enter-active,.sheet-overlay-leave-active{transition:opacity .3s ease}.sheet-overlay-enter-from,.sheet-overlay-leave-to{opacity:0}.sheet-bottom-enter-active,.sheet-bottom-leave-active,.sheet-left-enter-active,.sheet-left-leave-active,.sheet-right-enter-active,.sheet-right-leave-active,.sheet-top-enter-active,.sheet-top-leave-active{transition:transform .3s ease}.sheet-right-enter-from,.sheet-right-leave-to{transform:translateX(100%)}.sheet-left-enter-from,.sheet-left-leave-to{transform:translateX(-100%)}.sheet-top-enter-from,.sheet-top-leave-to{transform:translateY(-100%)}.sheet-bottom-enter-from,.sheet-bottom-leave-to{transform:translateY(100%)}
140
+ </style>
@@ -0,0 +1,15 @@
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
+
10
+ export interface Props {
11
+ class?: string
12
+ }
13
+
14
+ const props = defineProps<Props>()
15
+ </script>
@@ -0,0 +1,15 @@
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
+
10
+ export interface Props {
11
+ class?: string
12
+ }
13
+
14
+ const props = defineProps<Props>()
15
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div :class="cn('flex flex-col space-y-2 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
+
10
+ export interface Props {
11
+ class?: string
12
+ }
13
+
14
+ const props = defineProps<Props>()
15
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <h2 :class="cn('text-lg font-semibold text-foreground', props.class)">
3
+ <slot />
4
+ </h2>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { cn } from "../../utils/cn"
9
+
10
+ export interface Props {
11
+ class?: string
12
+ }
13
+
14
+ const props = defineProps<Props>()
15
+ </script>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ :class="composable.track_classes.value"
5
+ :disabled="composable.is_disabled.value"
6
+ :data-state="modelValue ? 'checked' : 'unchecked'"
7
+ v-bind="composable.aria_attributes.value"
8
+ @click="handleClick"
9
+ >
10
+ <span
11
+ :class="composable.thumb_classes.value"
12
+ :data-state="modelValue ? 'checked' : 'unchecked'"
13
+ />
14
+ </button>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { toRef } from "vue"
19
+ import { useSwitch } from "../../composables/useSwitch"
20
+ import type { SwitchProps } from "../../types/switch"
21
+
22
+ interface Props extends SwitchProps {
23
+ modelValue?: boolean
24
+ }
25
+
26
+ const props = withDefaults(defineProps<Props>(), {
27
+ modelValue: false,
28
+ disabled: false
29
+ })
30
+
31
+ const emit = defineEmits<{
32
+ "update:modelValue": [value: boolean]
33
+ }>()
34
+
35
+ const checked = toRef(() => props.modelValue)
36
+ const composable = useSwitch(toRef(() => props), checked)
37
+
38
+ const handleClick = () => {
39
+ if (!composable.is_disabled.value) {
40
+ emit("update:modelValue", !props.modelValue)
41
+ }
42
+ }
43
+ </script>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <textarea
3
+ :id="id"
4
+ :class="composable.base_classes.value"
5
+ :disabled="composable.is_disabled.value"
6
+ :readonly="composable.is_readonly.value"
7
+ :placeholder="placeholder"
8
+ :rows="rows"
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 { useTextarea } from "../../composables/useTextarea"
20
+ import type { TextareaProps } from "../../types/textarea"
21
+
22
+ interface Props extends TextareaProps {
23
+ id?: string
24
+ modelValue?: string
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ rows: 3,
29
+ resize: "vertical",
30
+ disabled: false,
31
+ readonly: false
32
+ })
33
+
34
+ const emit = defineEmits<{
35
+ "update:modelValue": [value: string]
36
+ blur: [event: FocusEvent]
37
+ }>()
38
+
39
+ const composable = useTextarea(toRef(() => props))
40
+
41
+ const handleInput = (event: Event) => {
42
+ const target = event.target as HTMLTextAreaElement
43
+ emit("update:modelValue", target.value)
44
+ }
45
+
46
+ const handleBlur = (event: FocusEvent) => {
47
+ composable.handleBlur()
48
+ emit("blur", event)
49
+ }
50
+ </script>
@@ -0,0 +1,107 @@
1
+ <template>
2
+ <div
3
+ :class="cn(base_classes, variant_classes[variant], props.class)"
4
+ :role="aria_role"
5
+ :aria-live="aria_live"
6
+ aria-atomic="true"
7
+ >
8
+ <div class="flex-1 space-y-1">
9
+ <div v-if="title" class="text-sm font-semibold">
10
+ {{ title }}
11
+ </div>
12
+ <div v-if="description" class="text-sm opacity-90">
13
+ {{ description }}
14
+ </div>
15
+ </div>
16
+
17
+ <div v-if="action || dismissible" class="flex items-center gap-2 ml-4">
18
+ <button
19
+ v-if="action"
20
+ type="button"
21
+ :class="action_button_classes"
22
+ @click="action.onClick"
23
+ >
24
+ {{ action.label }}
25
+ </button>
26
+
27
+ <button
28
+ v-if="dismissible"
29
+ type="button"
30
+ :class="dismiss_button_classes"
31
+ aria-label="Dismiss"
32
+ @click="emit('dismiss')"
33
+ >
34
+ <svg
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ width="16"
37
+ height="16"
38
+ viewBox="0 0 24 24"
39
+ fill="none"
40
+ stroke="currentColor"
41
+ stroke-width="2"
42
+ stroke-linecap="round"
43
+ stroke-linejoin="round"
44
+ >
45
+ <path d="M18 6 6 18" />
46
+ <path d="m6 6 12 12" />
47
+ </svg>
48
+ </button>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { computed } from "vue"
55
+ import { cn } from "../../utils/cn"
56
+ import type { ToastVariant, ToastAction } from "../../types/toast"
57
+
58
+ export interface Props {
59
+ title?: string
60
+ description?: string
61
+ variant?: ToastVariant
62
+ dismissible?: boolean
63
+ action?: ToastAction
64
+ class?: string
65
+ }
66
+
67
+ const props = withDefaults(defineProps<Props>(), {
68
+ variant: "default",
69
+ dismissible: true
70
+ })
71
+
72
+ const emit = defineEmits<{
73
+ dismiss: []
74
+ }>()
75
+
76
+ const base_classes = "pointer-events-auto flex items-start gap-4 rounded-lg border p-4 shadow-lg transition-all"
77
+
78
+ const variant_classes: Record<ToastVariant, string> = {
79
+ default: "bg-background text-foreground border-border",
80
+ success: "bg-green-50 text-green-900 border-green-200 dark:bg-green-950 dark:text-green-100 dark:border-green-800",
81
+ error: "bg-red-50 text-red-900 border-red-200 dark:bg-red-950 dark:text-red-100 dark:border-red-800",
82
+ warning: "bg-yellow-50 text-yellow-900 border-yellow-200 dark:bg-yellow-950 dark:text-yellow-100 dark:border-yellow-800",
83
+ info: "bg-blue-50 text-blue-900 border-blue-200 dark:bg-blue-950 dark:text-blue-100 dark:border-blue-800"
84
+ }
85
+
86
+ const aria_role = computed(() => {
87
+ return props.variant === "error" ? "alert" : "status"
88
+ })
89
+
90
+ const aria_live = computed(() => {
91
+ return props.variant === "error" ? "assertive" : "polite"
92
+ })
93
+
94
+ const action_button_classes = cn(
95
+ "inline-flex items-center justify-center rounded-md text-sm font-medium",
96
+ "h-8 px-3 ring-offset-background transition-colors",
97
+ "hover:bg-secondary focus-visible:outline-none focus-visible:ring-2",
98
+ "focus-visible:ring-ring focus-visible:ring-offset-2"
99
+ )
100
+
101
+ const dismiss_button_classes = cn(
102
+ "inline-flex items-center justify-center rounded-md",
103
+ "h-6 w-6 shrink-0 opacity-70 transition-opacity",
104
+ "hover:opacity-100 focus-visible:outline-none focus-visible:ring-2",
105
+ "focus-visible:ring-ring"
106
+ )
107
+ </script>
@@ -0,0 +1,80 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ :class="cn(container_classes, position_classes[position], props.class)"
5
+ aria-label="Notifications"
6
+ >
7
+ <TransitionGroup
8
+ name="toast"
9
+ tag="div"
10
+ :class="cn('flex flex-col', gap_class)"
11
+ >
12
+ <Toast
13
+ v-for="toast in visible_toasts"
14
+ :key="toast.id"
15
+ :title="toast.title"
16
+ :description="toast.description"
17
+ :variant="toast.variant"
18
+ :dismissible="toast.dismissible"
19
+ :action="toast.action"
20
+ @dismiss="handleDismiss(toast.id)"
21
+ />
22
+ </TransitionGroup>
23
+ </div>
24
+ </Teleport>
25
+ </template>
26
+
27
+ <script setup lang="ts">
28
+ import { computed, ref, watchEffect } from "vue"
29
+ import { cn } from "../../utils/cn"
30
+ import { useToast } from "../../composables/useToast"
31
+ import Toast from "./Toast.vue"
32
+ import type { ToastPosition, Toast as ToastType } from "../../types/toast"
33
+
34
+ export interface Props {
35
+ position?: ToastPosition
36
+ max_toasts?: number
37
+ gap?: number
38
+ class?: string
39
+ }
40
+
41
+ const props = withDefaults(defineProps<Props>(), {
42
+ position: "bottom-right",
43
+ max_toasts: 5,
44
+ gap: 8
45
+ })
46
+
47
+ const { toasts, dismiss } = useToast()
48
+
49
+ const internal_toasts = ref<ToastType[]>([])
50
+
51
+ watchEffect(() => {
52
+ internal_toasts.value = [...toasts]
53
+ })
54
+
55
+ const visible_toasts = computed(() => {
56
+ const sorted = [...internal_toasts.value].sort((a, b) => b.created_at - a.created_at)
57
+ return sorted.slice(0, props.max_toasts)
58
+ })
59
+
60
+ const handleDismiss = (id: string): void => {
61
+ dismiss(id)
62
+ }
63
+
64
+ const container_classes = "fixed z-[100] flex pointer-events-none p-4"
65
+
66
+ const position_classes: Record<ToastPosition, string> = {
67
+ "top-left": "top-0 left-0 flex-col",
68
+ "top-center": "top-0 left-1/2 -translate-x-1/2 flex-col items-center",
69
+ "top-right": "top-0 right-0 flex-col items-end",
70
+ "bottom-left": "bottom-0 left-0 flex-col-reverse",
71
+ "bottom-center": "bottom-0 left-1/2 -translate-x-1/2 flex-col-reverse items-center",
72
+ "bottom-right": "bottom-0 right-0 flex-col-reverse items-end"
73
+ }
74
+
75
+ const gap_class = computed(() => `gap-${props.gap / 4}`)
76
+ </script>
77
+
78
+ <style scoped>
79
+ .toast-enter-active,.toast-leave-active{transition:all .3s ease}.toast-enter-from,.toast-leave-to{opacity:0;transform:translateX(100%)}.toast-move{transition:transform .3s ease}
80
+ </style>
@@ -0,0 +1,42 @@
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, toRef, type InjectionKey, type Ref } from "vue"
9
+ import { useTooltip, type UseTooltipReturn } from "../../composables/useTooltip"
10
+ import type { TooltipSide, TooltipAlign } from "../../types/tooltip"
11
+
12
+ export interface Props {
13
+ side?: TooltipSide
14
+ align?: TooltipAlign
15
+ delay_duration?: number
16
+ skip_delay_duration?: number
17
+ disabled?: boolean
18
+ class?: string
19
+ }
20
+
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ side: "top",
23
+ align: "center",
24
+ delay_duration: 200,
25
+ skip_delay_duration: 100,
26
+ disabled: false
27
+ })
28
+
29
+ export const TOOLTIP_CONTEXT_KEY: InjectionKey<UseTooltipReturn> = Symbol("tooltip-context")
30
+
31
+ const tooltip_props = ref({
32
+ side: props.side,
33
+ align: props.align,
34
+ delay_duration: props.delay_duration,
35
+ skip_delay_duration: props.skip_delay_duration,
36
+ disabled: props.disabled
37
+ })
38
+
39
+ const tooltip = useTooltip(tooltip_props as Ref<typeof tooltip_props.value>)
40
+
41
+ provide(TOOLTIP_CONTEXT_KEY, tooltip)
42
+ </script>