@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.
- package/package.json +46 -0
- package/src/__test__/CrudCards.test.ts +118 -0
- package/src/__test__/CrudEmptyState.test.ts +82 -0
- package/src/__test__/CrudForm.test.ts +101 -0
- package/src/__test__/CrudShow.test.ts +135 -0
- package/src/__test__/CrudTable.test.ts +111 -0
- package/src/__test__/CrudToolbar.test.ts +102 -0
- package/src/__test__/setup.ts +43 -0
- package/src/__test__/useCrud.test.ts +349 -0
- package/src/components/CrudCards.vue +105 -0
- package/src/components/CrudDeleteDialog.vue +80 -0
- package/src/components/CrudEmptyState.vue +58 -0
- package/src/components/CrudFilters.vue +194 -0
- package/src/components/CrudForm.vue +232 -0
- package/src/components/CrudPage.vue +206 -0
- package/src/components/CrudPagination.vue +130 -0
- package/src/components/CrudSearch.vue +42 -0
- package/src/components/CrudShow.vue +216 -0
- package/src/components/CrudTable.vue +146 -0
- package/src/components/CrudToolbar.vue +86 -0
- package/src/components/InputError.vue +13 -0
- package/src/components/ui/button/Button.vue +27 -0
- package/src/components/ui/button/index.ts +36 -0
- package/src/components/ui/card/Card.vue +22 -0
- package/src/components/ui/card/CardAction.vue +17 -0
- package/src/components/ui/card/CardContent.vue +17 -0
- package/src/components/ui/card/CardDescription.vue +17 -0
- package/src/components/ui/card/CardFooter.vue +17 -0
- package/src/components/ui/card/CardHeader.vue +17 -0
- package/src/components/ui/card/CardTitle.vue +17 -0
- package/src/components/ui/card/index.ts +7 -0
- package/src/components/ui/checkbox/Checkbox.vue +37 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/combobox/ComboboxInput.vue +83 -0
- package/src/components/ui/combobox/index.ts +1 -0
- package/src/components/ui/dialog/Dialog.vue +17 -0
- package/src/components/ui/dialog/DialogClose.vue +14 -0
- package/src/components/ui/dialog/DialogContent.vue +49 -0
- package/src/components/ui/dialog/DialogDescription.vue +25 -0
- package/src/components/ui/dialog/DialogFooter.vue +15 -0
- package/src/components/ui/dialog/DialogHeader.vue +17 -0
- package/src/components/ui/dialog/DialogOverlay.vue +23 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/src/components/ui/dialog/DialogTitle.vue +25 -0
- package/src/components/ui/dialog/DialogTrigger.vue +14 -0
- package/src/components/ui/dialog/index.ts +10 -0
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +17 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +41 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +30 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +22 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +22 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +42 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +26 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +31 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +30 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +16 -0
- package/src/components/ui/dropdown-menu/index.ts +16 -0
- package/src/components/ui/input/Input.vue +33 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/label/Label.vue +28 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/select/Select.vue +15 -0
- package/src/components/ui/select/SelectContent.vue +49 -0
- package/src/components/ui/select/SelectGroup.vue +17 -0
- package/src/components/ui/select/SelectItem.vue +41 -0
- package/src/components/ui/select/SelectItemText.vue +12 -0
- package/src/components/ui/select/SelectLabel.vue +14 -0
- package/src/components/ui/select/SelectScrollDownButton.vue +22 -0
- package/src/components/ui/select/SelectScrollUpButton.vue +22 -0
- package/src/components/ui/select/SelectSeparator.vue +15 -0
- package/src/components/ui/select/SelectTrigger.vue +29 -0
- package/src/components/ui/select/SelectValue.vue +12 -0
- package/src/components/ui/select/index.ts +11 -0
- package/src/components/ui/separator/Separator.vue +28 -0
- package/src/components/ui/separator/index.ts +1 -0
- package/src/components/ui/spinner/Spinner.vue +17 -0
- package/src/components/ui/spinner/index.ts +1 -0
- package/src/components/ui/table/Table.vue +16 -0
- package/src/components/ui/table/TableBody.vue +14 -0
- package/src/components/ui/table/TableCaption.vue +14 -0
- package/src/components/ui/table/TableCell.vue +21 -0
- package/src/components/ui/table/TableEmpty.vue +34 -0
- package/src/components/ui/table/TableFooter.vue +14 -0
- package/src/components/ui/table/TableHead.vue +14 -0
- package/src/components/ui/table/TableHeader.vue +14 -0
- package/src/components/ui/table/TableRow.vue +14 -0
- package/src/components/ui/table/index.ts +9 -0
- package/src/composables/useCrud.ts +328 -0
- package/src/index.ts +33 -0
- package/src/lib/utils.ts +18 -0
- 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,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>
|