@afurgeri/crud-vue 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 (95) hide show
  1. package/package.json +46 -0
  2. package/src/__test__/CrudCards.test.ts +118 -0
  3. package/src/__test__/CrudEmptyState.test.ts +82 -0
  4. package/src/__test__/CrudForm.test.ts +101 -0
  5. package/src/__test__/CrudShow.test.ts +135 -0
  6. package/src/__test__/CrudTable.test.ts +111 -0
  7. package/src/__test__/CrudToolbar.test.ts +102 -0
  8. package/src/__test__/setup.ts +43 -0
  9. package/src/__test__/useCrud.test.ts +349 -0
  10. package/src/components/CrudCards.vue +105 -0
  11. package/src/components/CrudDeleteDialog.vue +80 -0
  12. package/src/components/CrudEmptyState.vue +58 -0
  13. package/src/components/CrudFilters.vue +194 -0
  14. package/src/components/CrudForm.vue +232 -0
  15. package/src/components/CrudPage.vue +206 -0
  16. package/src/components/CrudPagination.vue +130 -0
  17. package/src/components/CrudSearch.vue +42 -0
  18. package/src/components/CrudShow.vue +216 -0
  19. package/src/components/CrudTable.vue +146 -0
  20. package/src/components/CrudToolbar.vue +86 -0
  21. package/src/components/InputError.vue +13 -0
  22. package/src/components/ui/button/Button.vue +27 -0
  23. package/src/components/ui/button/index.ts +36 -0
  24. package/src/components/ui/card/Card.vue +22 -0
  25. package/src/components/ui/card/CardAction.vue +17 -0
  26. package/src/components/ui/card/CardContent.vue +17 -0
  27. package/src/components/ui/card/CardDescription.vue +17 -0
  28. package/src/components/ui/card/CardFooter.vue +17 -0
  29. package/src/components/ui/card/CardHeader.vue +17 -0
  30. package/src/components/ui/card/CardTitle.vue +17 -0
  31. package/src/components/ui/card/index.ts +7 -0
  32. package/src/components/ui/checkbox/Checkbox.vue +37 -0
  33. package/src/components/ui/checkbox/index.ts +1 -0
  34. package/src/components/ui/combobox/ComboboxInput.vue +83 -0
  35. package/src/components/ui/combobox/index.ts +1 -0
  36. package/src/components/ui/dialog/Dialog.vue +17 -0
  37. package/src/components/ui/dialog/DialogClose.vue +14 -0
  38. package/src/components/ui/dialog/DialogContent.vue +49 -0
  39. package/src/components/ui/dialog/DialogDescription.vue +25 -0
  40. package/src/components/ui/dialog/DialogFooter.vue +15 -0
  41. package/src/components/ui/dialog/DialogHeader.vue +17 -0
  42. package/src/components/ui/dialog/DialogOverlay.vue +23 -0
  43. package/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  44. package/src/components/ui/dialog/DialogTitle.vue +25 -0
  45. package/src/components/ui/dialog/DialogTrigger.vue +14 -0
  46. package/src/components/ui/dialog/index.ts +10 -0
  47. package/src/components/ui/dropdown-menu/DropdownMenu.vue +17 -0
  48. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +41 -0
  49. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
  50. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +14 -0
  51. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +30 -0
  52. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +22 -0
  53. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +22 -0
  54. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +42 -0
  55. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +26 -0
  56. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  57. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  58. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +31 -0
  59. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +30 -0
  60. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +16 -0
  61. package/src/components/ui/dropdown-menu/index.ts +16 -0
  62. package/src/components/ui/input/Input.vue +33 -0
  63. package/src/components/ui/input/index.ts +1 -0
  64. package/src/components/ui/label/Label.vue +28 -0
  65. package/src/components/ui/label/index.ts +1 -0
  66. package/src/components/ui/select/Select.vue +15 -0
  67. package/src/components/ui/select/SelectContent.vue +49 -0
  68. package/src/components/ui/select/SelectGroup.vue +17 -0
  69. package/src/components/ui/select/SelectItem.vue +41 -0
  70. package/src/components/ui/select/SelectItemText.vue +12 -0
  71. package/src/components/ui/select/SelectLabel.vue +14 -0
  72. package/src/components/ui/select/SelectScrollDownButton.vue +22 -0
  73. package/src/components/ui/select/SelectScrollUpButton.vue +22 -0
  74. package/src/components/ui/select/SelectSeparator.vue +15 -0
  75. package/src/components/ui/select/SelectTrigger.vue +29 -0
  76. package/src/components/ui/select/SelectValue.vue +12 -0
  77. package/src/components/ui/select/index.ts +11 -0
  78. package/src/components/ui/separator/Separator.vue +28 -0
  79. package/src/components/ui/separator/index.ts +1 -0
  80. package/src/components/ui/spinner/Spinner.vue +17 -0
  81. package/src/components/ui/spinner/index.ts +1 -0
  82. package/src/components/ui/table/Table.vue +16 -0
  83. package/src/components/ui/table/TableBody.vue +14 -0
  84. package/src/components/ui/table/TableCaption.vue +14 -0
  85. package/src/components/ui/table/TableCell.vue +21 -0
  86. package/src/components/ui/table/TableEmpty.vue +34 -0
  87. package/src/components/ui/table/TableFooter.vue +14 -0
  88. package/src/components/ui/table/TableHead.vue +14 -0
  89. package/src/components/ui/table/TableHeader.vue +14 -0
  90. package/src/components/ui/table/TableRow.vue +14 -0
  91. package/src/components/ui/table/index.ts +9 -0
  92. package/src/composables/useCrud.ts +328 -0
  93. package/src/index.ts +33 -0
  94. package/src/lib/utils.ts +18 -0
  95. package/src/types/crud.ts +133 -0
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ import { Button } from './ui/button';
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuTrigger,
9
+ } from './ui/dropdown-menu';
10
+ import {
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableHead,
15
+ TableHeader,
16
+ TableRow,
17
+ } from './ui/table';
18
+ import type { CrudColumn, Paginator } from '../types/crud';
19
+ import { router } from '@inertiajs/vue3';
20
+ import { ArrowDown, ArrowUp, ArrowUpDown, MoreHorizontal, Pencil, Eye, Trash2 } from 'lucide-vue-next';
21
+ import { computed } from 'vue';
22
+
23
+ const props = defineProps<{
24
+ items: Paginator;
25
+ columns: CrudColumn[];
26
+ sortField: string;
27
+ sortDirection: 'asc' | 'desc';
28
+ showActions: boolean;
29
+ hasShow: boolean;
30
+ hasEdit: boolean;
31
+ hasDelete: boolean;
32
+ }>();
33
+
34
+ const emit = defineEmits<{
35
+ sort: [field: string];
36
+ delete: [item: Record<string, unknown>];
37
+ }>();
38
+
39
+ const visibleColumns = computed(() => props.columns.filter((c) => c.visible !== false));
40
+
41
+ function getSortIcon(field: string) {
42
+ if (props.sortField !== field) return ArrowUpDown;
43
+ return props.sortDirection === 'asc' ? ArrowUp : ArrowDown;
44
+ }
45
+
46
+ function getAlignClass(align?: string) {
47
+ switch (align) {
48
+ case 'center':
49
+ return 'text-center';
50
+ case 'right':
51
+ return 'text-right';
52
+ default:
53
+ return 'text-left';
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div class="relative overflow-x-auto">
60
+ <Table>
61
+ <TableHeader class="sticky top-0 z-10 bg-muted/50">
62
+ <TableRow class="hover:bg-transparent">
63
+ <TableHead
64
+ v-for="column in visibleColumns"
65
+ :key="column.key"
66
+ class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
67
+ :class="[getAlignClass(column.align), column.width ? column.width : '']"
68
+ >
69
+ <button
70
+ v-if="column.sortable"
71
+ class="inline-flex items-center gap-1.5 hover:text-foreground transition-colors"
72
+ @click="emit('sort', column.key)"
73
+ >
74
+ {{ column.title }}
75
+ <component
76
+ :is="getSortIcon(column.key)"
77
+ class="h-3.5 w-3.5"
78
+ :class="sortField === column.key ? 'text-primary' : 'text-muted-foreground/50'"
79
+ />
80
+ </button>
81
+ <span v-else>{{ column.title }}</span>
82
+ </TableHead>
83
+ <TableHead v-if="showActions" class="w-[60px] px-4 py-3" />
84
+ </TableRow>
85
+ </TableHeader>
86
+
87
+ <TableBody>
88
+ <TableRow
89
+ v-for="(item, idx) in items.data"
90
+ :key="(item.id as string | number) ?? idx"
91
+ class="border-b transition-colors hover:bg-muted/30"
92
+ >
93
+ <TableCell
94
+ v-for="column in visibleColumns"
95
+ :key="column.key"
96
+ class="px-4 py-3 text-sm"
97
+ :class="getAlignClass(column.align)"
98
+ >
99
+ <slot :name="`cell-${column.key}`" :item="item" :value="item[column.key]">
100
+ {{ item[column.key] }}
101
+ </slot>
102
+ </TableCell>
103
+
104
+ <TableCell v-if="showActions" class="px-2 py-3">
105
+ <slot name="actions" :item="item">
106
+ <DropdownMenu>
107
+ <DropdownMenuTrigger as-child>
108
+ <Button variant="ghost" size="icon" class="h-8 w-8">
109
+ <MoreHorizontal class="h-4 w-4" />
110
+ </Button>
111
+ </DropdownMenuTrigger>
112
+ <DropdownMenuContent align="end" class="w-36">
113
+ <DropdownMenuItem
114
+ v-if="hasShow && item.show_url"
115
+ @click="router.get(item.show_url as string)"
116
+ >
117
+ <Eye class="mr-2 h-4 w-4" />
118
+ View
119
+ </DropdownMenuItem>
120
+ <DropdownMenuItem
121
+ v-if="hasEdit && item.edit_url"
122
+ @click="router.get(item.edit_url as string)"
123
+ >
124
+ <Pencil class="mr-2 h-4 w-4" />
125
+ Edit
126
+ </DropdownMenuItem>
127
+ <DropdownMenuSeparator v-if="hasDelete && item.delete_url" />
128
+ <DropdownMenuItem
129
+ v-if="hasDelete && item.delete_url"
130
+ class="text-destructive focus:text-destructive"
131
+ @click="emit('delete', item)"
132
+ >
133
+ <Trash2 class="mr-2 h-4 w-4" />
134
+ Delete
135
+ </DropdownMenuItem>
136
+ </DropdownMenuContent>
137
+ </DropdownMenu>
138
+ </slot>
139
+ </TableCell>
140
+ </TableRow>
141
+
142
+ <!-- Empty state handled by CrudPage -->
143
+ </TableBody>
144
+ </Table>
145
+ </div>
146
+ </template>
@@ -0,0 +1,86 @@
1
+ <script setup lang="ts">
2
+ import { Button } from './ui/button';
3
+ import type { CrudFeatureCreate, CrudFeatureSearch } from '../types/crud';
4
+ import { router } from '@inertiajs/vue3';
5
+ import { Filter, Plus, X } from 'lucide-vue-next';
6
+ import CrudSearch from './CrudSearch.vue';
7
+
8
+ defineProps<{
9
+ title: string;
10
+ description?: string;
11
+ search: string;
12
+ searchConfig: CrudFeatureSearch | null;
13
+ createConfig: CrudFeatureCreate | null;
14
+ hasActiveFilters: boolean;
15
+ filtersVisible: boolean;
16
+ }>();
17
+
18
+ const emit = defineEmits<{
19
+ 'update:search': [value: string];
20
+ 'search:clear': [];
21
+ toggleFilters: [];
22
+ clearFilters: [];
23
+ }>();
24
+ </script>
25
+
26
+ <template>
27
+ <div class="flex flex-col gap-4 border-b px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
28
+ <div class="flex-1">
29
+ <slot name="title-prepend" />
30
+ <h2 class="text-xl font-bold tracking-tight text-foreground">{{ title }}</h2>
31
+ <p v-if="description" class="mt-1 text-sm text-muted-foreground">
32
+ {{ description }}
33
+ </p>
34
+ <slot name="title-append" />
35
+ </div>
36
+
37
+ <div class="flex items-center gap-2">
38
+ <!-- Search -->
39
+ <CrudSearch
40
+ v-if="searchConfig"
41
+ :model-value="search"
42
+ :placeholder="searchConfig.placeholder ?? 'Search...'"
43
+ @update:model-value="emit('update:search', $event)"
44
+ @clear="emit('search:clear')"
45
+ />
46
+
47
+ <!-- Filter toggle -->
48
+ <Button
49
+ variant="outline"
50
+ size="icon"
51
+ class="h-9 w-9 shrink-0"
52
+ :class="{
53
+ 'border-primary text-primary': hasActiveFilters,
54
+ }"
55
+ @click="emit('toggleFilters')"
56
+ >
57
+ <Filter class="h-4 w-4" />
58
+ </Button>
59
+
60
+ <!-- Active filter count -->
61
+ <span
62
+ v-if="hasActiveFilters"
63
+ class="inline-flex h-5 items-center gap-1 rounded-full bg-primary/10 px-2 text-xs font-medium text-primary"
64
+ >
65
+ Filters active
66
+ <button class="ml-0.5 hover:text-primary/70" @click="emit('clearFilters')">
67
+ <X class="h-3 w-3" />
68
+ </button>
69
+ </span>
70
+
71
+ <slot name="toolbar-actions" />
72
+
73
+ <!-- Create button -->
74
+ <Button
75
+ v-if="createConfig"
76
+ class="shrink-0"
77
+ @click="router.get(createConfig.url)"
78
+ >
79
+ <Plus class="mr-1.5 h-4 w-4" />
80
+ {{ createConfig.label ?? 'Create' }}
81
+ </Button>
82
+
83
+ <slot name="toolbar-append" />
84
+ </div>
85
+ </div>
86
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ message?: string;
4
+ }>();
5
+ </script>
6
+
7
+ <template>
8
+ <div v-show="message">
9
+ <p class="text-sm text-red-600 dark:text-red-500">
10
+ {{ message }}
11
+ </p>
12
+ </div>
13
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+ import { Primitive, type PrimitiveProps } from 'reka-ui'
5
+ import { type ButtonVariants, buttonVariants } from '.'
6
+
7
+ interface Props extends PrimitiveProps {
8
+ variant?: ButtonVariants['variant']
9
+ size?: ButtonVariants['size']
10
+ class?: HTMLAttributes['class']
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ as: 'button',
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <Primitive
20
+ data-slot="button"
21
+ :as="as"
22
+ :as-child="asChild"
23
+ :class="cn(buttonVariants({ variant, size }), props.class)"
24
+ >
25
+ <slot />
26
+ </Primitive>
27
+ </template>
@@ -0,0 +1,36 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+
3
+ export { default as Button } from './Button.vue'
4
+
5
+ export const buttonVariants = cva(
6
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
12
+ destructive:
13
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
14
+ outline:
15
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
16
+ secondary:
17
+ 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
18
+ ghost:
19
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
24
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
25
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
26
+ icon: 'size-9',
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: 'default',
31
+ size: 'default',
32
+ },
33
+ },
34
+ )
35
+
36
+ export type ButtonVariants = VariantProps<typeof buttonVariants>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ data-slot="card"
13
+ :class="
14
+ cn(
15
+ 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
16
+ props.class,
17
+ )
18
+ "
19
+ >
20
+ <slot />
21
+ </div>
22
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ data-slot="card-action"
13
+ :class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
14
+ >
15
+ <slot />
16
+ </div>
17
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ data-slot="card-content"
13
+ :class="cn('px-6', props.class)"
14
+ >
15
+ <slot />
16
+ </div>
17
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <p
12
+ data-slot="card-description"
13
+ :class="cn('text-muted-foreground text-sm', props.class)"
14
+ >
15
+ <slot />
16
+ </p>
17
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ data-slot="card-footer"
13
+ :class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
14
+ >
15
+ <slot />
16
+ </div>
17
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ data-slot="card-header"
13
+ :class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
14
+ >
15
+ <slot />
16
+ </div>
17
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '../../../lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <h3
12
+ data-slot="card-title"
13
+ :class="cn('leading-none font-semibold', props.class)"
14
+ >
15
+ <slot />
16
+ </h3>
17
+ </template>
@@ -0,0 +1,7 @@
1
+ export { default as Card } from './Card.vue'
2
+ export { default as CardAction } from './CardAction.vue'
3
+ export { default as CardContent } from './CardContent.vue'
4
+ export { default as CardDescription } from './CardDescription.vue'
5
+ export { default as CardFooter } from './CardFooter.vue'
6
+ export { default as CardHeader } from './CardHeader.vue'
7
+ export { default as CardTitle } from './CardTitle.vue'
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
3
+ import { cn } from '../../../lib/utils'
4
+ import { Check } from 'lucide-vue-next'
5
+ import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
6
+ import { computed, type HTMLAttributes } from 'vue'
7
+
8
+ const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
9
+ const emits = defineEmits<CheckboxRootEmits>()
10
+
11
+ const delegatedProps = computed(() => {
12
+ const { class: _, ...delegated } = props
13
+
14
+ return delegated
15
+ })
16
+
17
+ const forwarded = useForwardPropsEmits(delegatedProps, emits)
18
+ </script>
19
+
20
+ <template>
21
+ <CheckboxRoot
22
+ data-slot="checkbox"
23
+ v-bind="forwarded"
24
+ :class="
25
+ cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
26
+ props.class)"
27
+ >
28
+ <CheckboxIndicator
29
+ data-slot="checkbox-indicator"
30
+ class="flex items-center justify-center text-current transition-none"
31
+ >
32
+ <slot>
33
+ <Check class="size-3.5" />
34
+ </slot>
35
+ </CheckboxIndicator>
36
+ </CheckboxRoot>
37
+ </template>
@@ -0,0 +1 @@
1
+ export { default as Checkbox } from './Checkbox.vue'
@@ -0,0 +1,83 @@
1
+ <script setup lang="ts">
2
+ import type { ComboboxRootEmits, ComboboxRootProps } from 'reka-ui';
3
+ import {
4
+ ComboboxContent,
5
+ ComboboxEmpty,
6
+ ComboboxGroup,
7
+ ComboboxInput,
8
+ ComboboxItem,
9
+ ComboboxRoot,
10
+ ComboboxTrigger,
11
+ useFilter,
12
+ useForwardPropsEmits,
13
+ } from 'reka-ui';
14
+ import { cn } from '../../../lib/utils';
15
+ import { Check, ChevronsUpDown } from 'lucide-vue-next';
16
+ import { computed } from 'vue';
17
+
18
+ const props = defineProps<
19
+ ComboboxRootProps & {
20
+ options: { label: string; value: string | number }[];
21
+ placeholder?: string;
22
+ class?: string;
23
+ inputClass?: string;
24
+ }
25
+ >();
26
+
27
+ const emits = defineEmits<ComboboxRootEmits>();
28
+
29
+ const forwarded = useForwardPropsEmits(props, emits);
30
+
31
+ const { contains } = useFilter({ sensitivity: 'base' });
32
+
33
+ function filterFunction(items: unknown[], search: string) {
34
+ if (!search) return items;
35
+ return items.filter((item: unknown) => {
36
+ const typed = item as { label?: string; value?: unknown };
37
+ return contains(typed.label ?? '', search) || contains(String(typed.value ?? ''), search);
38
+ });
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <ComboboxRoot
44
+ v-bind="forwarded"
45
+ :filter-function="filterFunction"
46
+ class="relative"
47
+ >
48
+ <ComboboxTrigger
49
+ class="border-input data-[placeholder]:text-muted-foreground flex h-10 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
50
+ :class="props.class"
51
+ >
52
+ <ComboboxInput
53
+ :placeholder="placeholder ?? 'Search...'"
54
+ class="flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
55
+ :class="props.inputClass"
56
+ :display-value="(opt: any) => opt?.label ?? ''"
57
+ />
58
+ <ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
59
+ </ComboboxTrigger>
60
+
61
+ <ComboboxContent
62
+ class="bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 absolute z-50 mt-1 max-h-60 w-full min-w-[var(--reka-popper-anchor-width)] overflow-auto rounded-md border p-1 shadow-md outline-none"
63
+ >
64
+ <ComboboxEmpty class="py-2 text-center text-sm text-muted-foreground">
65
+ No results found.
66
+ </ComboboxEmpty>
67
+
68
+ <ComboboxGroup>
69
+ <ComboboxItem
70
+ v-for="opt in options"
71
+ :key="String(opt.value)"
72
+ :value="opt"
73
+ class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none"
74
+ >
75
+ <Check
76
+ class="mr-2 h-4 w-4 shrink-0 opacity-0 group-data-[state=checked]:opacity-100"
77
+ />
78
+ {{ opt.label }}
79
+ </ComboboxItem>
80
+ </ComboboxGroup>
81
+ </ComboboxContent>
82
+ </ComboboxRoot>
83
+ </template>
@@ -0,0 +1 @@
1
+ export { default as ComboboxInput } from './ComboboxInput.vue';
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
3
+
4
+ const props = defineProps<DialogRootProps>()
5
+ const emits = defineEmits<DialogRootEmits>()
6
+
7
+ const forwarded = useForwardPropsEmits(props, emits)
8
+ </script>
9
+
10
+ <template>
11
+ <DialogRoot
12
+ data-slot="dialog"
13
+ v-bind="forwarded"
14
+ >
15
+ <slot />
16
+ </DialogRoot>
17
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import { DialogClose, type DialogCloseProps } from 'reka-ui'
3
+
4
+ const props = defineProps<DialogCloseProps>()
5
+ </script>
6
+
7
+ <template>
8
+ <DialogClose
9
+ data-slot="dialog-close"
10
+ v-bind="props"
11
+ >
12
+ <slot />
13
+ </DialogClose>
14
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { cn } from '../../../lib/utils'
3
+ import { X } from 'lucide-vue-next'
4
+ import {
5
+ DialogClose,
6
+ DialogContent,
7
+ type DialogContentEmits,
8
+ type DialogContentProps,
9
+ DialogPortal,
10
+ useForwardPropsEmits,
11
+ } from 'reka-ui'
12
+ import { computed, type HTMLAttributes } from 'vue'
13
+ import DialogOverlay from './DialogOverlay.vue'
14
+
15
+ const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
16
+ const emits = defineEmits<DialogContentEmits>()
17
+
18
+ const delegatedProps = computed(() => {
19
+ const { class: _, ...delegated } = props
20
+
21
+ return delegated
22
+ })
23
+
24
+ const forwarded = useForwardPropsEmits(delegatedProps, emits)
25
+ </script>
26
+
27
+ <template>
28
+ <DialogPortal>
29
+ <DialogOverlay />
30
+ <DialogContent
31
+ data-slot="dialog-content"
32
+ v-bind="forwarded"
33
+ :class="
34
+ cn(
35
+ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
36
+ props.class,
37
+ )"
38
+ >
39
+ <slot />
40
+
41
+ <DialogClose
42
+ class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
43
+ >
44
+ <X />
45
+ <span class="sr-only">Close</span>
46
+ </DialogClose>
47
+ </DialogContent>
48
+ </DialogPortal>
49
+ </template>