mycontext-cli 4.2.7 → 4.2.8
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/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +19 -60
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init-interactive.d.ts +20 -0
- package/dist/commands/init-interactive.d.ts.map +1 -1
- package/dist/commands/init-interactive.js +168 -5
- package/dist/commands/init-interactive.js.map +1 -1
- package/dist/config/shadcn-catalog.json +93 -0
- package/dist/core/brain/BrainClient.d.ts +1 -1
- package/dist/core/brain/BrainClient.d.ts.map +1 -1
- package/dist/core/brain/BrainClient.js +5 -5
- package/dist/core/brain/BrainClient.js.map +1 -1
- package/dist/doctor/DoctorEngine.d.ts.map +1 -1
- package/dist/doctor/DoctorEngine.js +21 -11
- package/dist/doctor/DoctorEngine.js.map +1 -1
- package/dist/doctor/rules/dead-code-rules.d.ts.map +1 -1
- package/dist/doctor/rules/dead-code-rules.js +33 -0
- package/dist/doctor/rules/dead-code-rules.js.map +1 -1
- package/dist/doctor/rules/instantdb-rules.d.ts.map +1 -1
- package/dist/doctor/rules/instantdb-rules.js +278 -69
- package/dist/doctor/rules/instantdb-rules.js.map +1 -1
- package/dist/doctor/rules/nextjs-rules.d.ts.map +1 -1
- package/dist/doctor/rules/nextjs-rules.js +53 -3
- package/dist/doctor/rules/nextjs-rules.js.map +1 -1
- package/dist/package.json +4 -2
- package/dist/services/ComponentInferenceEngine.d.ts +66 -0
- package/dist/services/ComponentInferenceEngine.d.ts.map +1 -0
- package/dist/services/ComponentInferenceEngine.js +302 -0
- package/dist/services/ComponentInferenceEngine.js.map +1 -0
- package/dist/services/ComponentRegistry.d.ts +61 -0
- package/dist/services/ComponentRegistry.d.ts.map +1 -0
- package/dist/services/ComponentRegistry.js +128 -0
- package/dist/services/ComponentRegistry.js.map +1 -0
- package/dist/services/InferenceEngine.js +1 -1
- package/dist/services/ProjectScanner.d.ts.map +1 -1
- package/dist/services/ProjectScanner.js +15 -1
- package/dist/services/ProjectScanner.js.map +1 -1
- package/dist/services/ScaffoldEngine.d.ts +87 -0
- package/dist/services/ScaffoldEngine.d.ts.map +1 -0
- package/dist/services/ScaffoldEngine.js +409 -0
- package/dist/services/ScaffoldEngine.js.map +1 -0
- package/dist/services/ScaffoldPreview.d.ts +62 -0
- package/dist/services/ScaffoldPreview.d.ts.map +1 -0
- package/dist/services/ScaffoldPreview.js +292 -0
- package/dist/services/ScaffoldPreview.js.map +1 -0
- package/dist/services/TemplateEngine.d.ts +136 -0
- package/dist/services/TemplateEngine.d.ts.map +1 -0
- package/dist/services/TemplateEngine.js +483 -0
- package/dist/services/TemplateEngine.js.map +1 -0
- package/dist/services/TemplateHelpers.d.ts +9 -0
- package/dist/services/TemplateHelpers.d.ts.map +1 -0
- package/dist/services/TemplateHelpers.js +212 -0
- package/dist/services/TemplateHelpers.js.map +1 -0
- package/dist/templates/actions/auth-actions.ts.hbs +140 -0
- package/dist/templates/actions/crud-actions.ts.hbs +113 -0
- package/dist/templates/components/auth/login-form.tsx.hbs +67 -0
- package/dist/templates/components/auth/login-skeleton.tsx.hbs +24 -0
- package/dist/templates/components/auth/register-form.tsx.hbs +116 -0
- package/dist/templates/components/crud/entity-card.tsx.hbs +71 -0
- package/dist/templates/components/crud/entity-form.tsx.hbs +158 -0
- package/dist/templates/components/crud/entity-skeleton.tsx.hbs +90 -0
- package/dist/templates/components/crud/entity-table.tsx.hbs +129 -0
- package/dist/templates/components/ui/button.tsx.hbs +53 -0
- package/dist/templates/components/ui/card.tsx.hbs +68 -0
- package/dist/templates/components/ui/input.tsx.hbs +33 -0
- package/dist/templates/components/ui/label.tsx.hbs +20 -0
- package/dist/templates/components/ui/skeleton.tsx.hbs +15 -0
- package/dist/templates/components/ui/theme-provider.tsx.hbs +66 -0
- package/dist/templates/components/ui/theme-toggle.tsx.hbs +30 -0
- package/dist/templates/config/app.css.hbs +150 -0
- package/dist/templates/layouts/dashboard-layout.tsx.hbs +69 -0
- package/dist/templates/layouts/error.tsx.hbs +51 -0
- package/dist/templates/layouts/loading.tsx.hbs +22 -0
- package/dist/templates/layouts/not-found.tsx.hbs +24 -0
- package/dist/templates/layouts/root-layout.tsx.hbs +40 -0
- package/dist/templates/lib/instant.ts.hbs +19 -0
- package/dist/templates/lib/utils.ts.hbs +24 -0
- package/dist/templates/pages/auth/login-page.tsx.hbs +30 -0
- package/dist/templates/pages/auth/register-page.tsx.hbs +38 -0
- package/dist/templates/pages/crud/create-page.tsx.hbs +42 -0
- package/dist/templates/pages/crud/detail-page.tsx.hbs +90 -0
- package/dist/templates/pages/crud/list-page.tsx.hbs +60 -0
- package/dist/templates/pages/crud/loading.tsx.hbs +13 -0
- package/dist/templates/pages/landing-page.tsx.hbs +111 -0
- package/dist/types/asl.d.ts +1 -1
- package/dist/types/asl.d.ts.map +1 -1
- package/dist/types/living-context.d.ts +1 -1
- package/dist/types/living-context.d.ts.map +1 -1
- package/dist/utils/FileGenerator.js +3 -3
- package/dist/utils/FileGenerator.js.map +1 -1
- package/dist/utils/generateTypesFromSchema.d.ts +47 -0
- package/dist/utils/generateTypesFromSchema.d.ts.map +1 -0
- package/dist/utils/generateTypesFromSchema.js +298 -0
- package/dist/utils/generateTypesFromSchema.js.map +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
|
4
|
+
|
|
5
|
+
const entityCardVariants = cva(
|
|
6
|
+
'transition-all',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'hover:shadow-md',
|
|
11
|
+
interactive: 'cursor-pointer hover:shadow-md hover:border-primary',
|
|
12
|
+
selected: 'border-primary shadow-md',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: 'default',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export interface EntityCardProps extends VariantProps<typeof entityCardVariants> {
|
|
22
|
+
title: string
|
|
23
|
+
description?: string
|
|
24
|
+
metadata?: React.ReactNode
|
|
25
|
+
actions?: React.ReactNode
|
|
26
|
+
children?: React.ReactNode
|
|
27
|
+
onClick?: () => void
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function EntityCard({
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
metadata,
|
|
35
|
+
actions,
|
|
36
|
+
children,
|
|
37
|
+
variant,
|
|
38
|
+
onClick,
|
|
39
|
+
className,
|
|
40
|
+
}: EntityCardProps) {
|
|
41
|
+
return (
|
|
42
|
+
<Card
|
|
43
|
+
className={cn(entityCardVariants({ variant }), className)}
|
|
44
|
+
onClick={onClick}
|
|
45
|
+
role={onClick ? 'button' : undefined}
|
|
46
|
+
tabIndex={onClick ? 0 : undefined}
|
|
47
|
+
onKeyDown={onClick ? (e) => {
|
|
48
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
onClick()
|
|
51
|
+
}
|
|
52
|
+
} : undefined}
|
|
53
|
+
>
|
|
54
|
+
<CardHeader>
|
|
55
|
+
<div className="flex items-start justify-between">
|
|
56
|
+
<div className="flex-1 space-y-1">
|
|
57
|
+
<CardTitle>{title}</CardTitle>
|
|
58
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
59
|
+
</div>
|
|
60
|
+
{actions && <div className="ml-4">{actions}</div>}
|
|
61
|
+
</div>
|
|
62
|
+
</CardHeader>
|
|
63
|
+
{(metadata || children) && (
|
|
64
|
+
<CardContent>
|
|
65
|
+
{metadata && <div className="space-y-2">{metadata}</div>}
|
|
66
|
+
{children}
|
|
67
|
+
</CardContent>
|
|
68
|
+
)}
|
|
69
|
+
</Card>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { Label } from '@/components/ui/label'
|
|
8
|
+
|
|
9
|
+
interface EntityFormProps {
|
|
10
|
+
entityName: string
|
|
11
|
+
entityLower: string
|
|
12
|
+
fields: Array<{
|
|
13
|
+
name: string
|
|
14
|
+
type: string
|
|
15
|
+
required?: boolean
|
|
16
|
+
label: string
|
|
17
|
+
}>
|
|
18
|
+
initialData?: Record<string, any>
|
|
19
|
+
action: (formData: FormData) => Promise<{ success: boolean; error?: string }>
|
|
20
|
+
submitLabel?: string
|
|
21
|
+
onSuccess?: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function EntityForm({
|
|
25
|
+
entityName,
|
|
26
|
+
entityLower,
|
|
27
|
+
fields,
|
|
28
|
+
initialData,
|
|
29
|
+
action,
|
|
30
|
+
submitLabel,
|
|
31
|
+
onSuccess,
|
|
32
|
+
}: EntityFormProps) {
|
|
33
|
+
const router = useRouter()
|
|
34
|
+
const [isPending, startTransition] = useTransition()
|
|
35
|
+
const [error, setError] = useState<string | null>(null)
|
|
36
|
+
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
|
|
37
|
+
|
|
38
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
setError(null)
|
|
41
|
+
setFieldErrors({})
|
|
42
|
+
|
|
43
|
+
const formData = new FormData(e.currentTarget)
|
|
44
|
+
|
|
45
|
+
startTransition(async () => {
|
|
46
|
+
const result = await action(formData)
|
|
47
|
+
if (result.error) {
|
|
48
|
+
setError(result.error)
|
|
49
|
+
} else {
|
|
50
|
+
if (onSuccess) {
|
|
51
|
+
onSuccess()
|
|
52
|
+
} else {
|
|
53
|
+
router.push(`/${entityLower}s`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const renderField = (field: typeof fields[0]) => {
|
|
60
|
+
const value = initialData?.[field.name] || ''
|
|
61
|
+
|
|
62
|
+
switch (field.type) {
|
|
63
|
+
case 'string':
|
|
64
|
+
case 'text':
|
|
65
|
+
return (
|
|
66
|
+
<Input
|
|
67
|
+
id={field.name}
|
|
68
|
+
name={field.name}
|
|
69
|
+
type="text"
|
|
70
|
+
defaultValue={value}
|
|
71
|
+
required={field.required}
|
|
72
|
+
disabled={isPending}
|
|
73
|
+
error={fieldErrors[field.name]}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
case 'number':
|
|
77
|
+
return (
|
|
78
|
+
<Input
|
|
79
|
+
id={field.name}
|
|
80
|
+
name={field.name}
|
|
81
|
+
type="number"
|
|
82
|
+
defaultValue={value}
|
|
83
|
+
required={field.required}
|
|
84
|
+
disabled={isPending}
|
|
85
|
+
error={fieldErrors[field.name]}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
case 'boolean':
|
|
89
|
+
return (
|
|
90
|
+
<input
|
|
91
|
+
id={field.name}
|
|
92
|
+
name={field.name}
|
|
93
|
+
type="checkbox"
|
|
94
|
+
defaultChecked={value}
|
|
95
|
+
disabled={isPending}
|
|
96
|
+
className="size-4 rounded border-border"
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
case 'date':
|
|
100
|
+
return (
|
|
101
|
+
<Input
|
|
102
|
+
id={field.name}
|
|
103
|
+
name={field.name}
|
|
104
|
+
type="date"
|
|
105
|
+
defaultValue={value}
|
|
106
|
+
required={field.required}
|
|
107
|
+
disabled={isPending}
|
|
108
|
+
error={fieldErrors[field.name]}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
default:
|
|
112
|
+
return (
|
|
113
|
+
<Input
|
|
114
|
+
id={field.name}
|
|
115
|
+
name={field.name}
|
|
116
|
+
type="text"
|
|
117
|
+
defaultValue={value}
|
|
118
|
+
required={field.required}
|
|
119
|
+
disabled={isPending}
|
|
120
|
+
error={fieldErrors[field.name]}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
128
|
+
{fields.map((field) => (
|
|
129
|
+
<div key={field.name} className="space-y-2">
|
|
130
|
+
<Label htmlFor={field.name} required={field.required}>
|
|
131
|
+
{field.label}
|
|
132
|
+
</Label>
|
|
133
|
+
{renderField(field)}
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
|
|
137
|
+
{error && (
|
|
138
|
+
<p className="text-sm text-destructive" role="alert">
|
|
139
|
+
{error}
|
|
140
|
+
</p>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<div className="flex justify-end gap-4">
|
|
144
|
+
<Button
|
|
145
|
+
type="button"
|
|
146
|
+
variant="outline"
|
|
147
|
+
onClick={() => router.back()}
|
|
148
|
+
disabled={isPending}
|
|
149
|
+
>
|
|
150
|
+
Cancel
|
|
151
|
+
</Button>
|
|
152
|
+
<Button type="submit" disabled={isPending}>
|
|
153
|
+
{isPending ? 'Saving...' : (submitLabel || `Save ${entityName}`)}
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
</form>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
2
|
+
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
|
3
|
+
|
|
4
|
+
export function EntityListSkeleton({ count = 3 }: { count?: number }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="space-y-4">
|
|
7
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
8
|
+
<Card key={i}>
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<Skeleton className="h-6 w-3/4" />
|
|
11
|
+
<Skeleton className="h-4 w-full" />
|
|
12
|
+
</CardHeader>
|
|
13
|
+
<CardContent>
|
|
14
|
+
<div className="space-y-2">
|
|
15
|
+
<Skeleton className="h-4 w-1/2" />
|
|
16
|
+
<Skeleton className="h-4 w-2/3" />
|
|
17
|
+
</div>
|
|
18
|
+
</CardContent>
|
|
19
|
+
</Card>
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function EntityDetailSkeleton() {
|
|
26
|
+
return (
|
|
27
|
+
<div className="space-y-6">
|
|
28
|
+
<div className="space-y-2">
|
|
29
|
+
<Skeleton className="h-10 w-3/4" />
|
|
30
|
+
<Skeleton className="h-4 w-full" />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<Card>
|
|
34
|
+
<CardHeader>
|
|
35
|
+
<Skeleton className="h-6 w-1/4" />
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent className="space-y-4">
|
|
38
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
39
|
+
<div key={i} className="space-y-2">
|
|
40
|
+
<Skeleton className="h-4 w-1/4" />
|
|
41
|
+
<Skeleton className="h-10 w-full" />
|
|
42
|
+
</div>
|
|
43
|
+
))}
|
|
44
|
+
</CardContent>
|
|
45
|
+
</Card>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function EntityFormSkeleton() {
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-6">
|
|
53
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
54
|
+
<div key={i} className="space-y-2">
|
|
55
|
+
<Skeleton className="h-4 w-1/4" />
|
|
56
|
+
<Skeleton className="h-10 w-full" />
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
<div className="flex justify-end gap-4">
|
|
60
|
+
<Skeleton className="h-10 w-24" />
|
|
61
|
+
<Skeleton className="h-10 w-32" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function EntityTableSkeleton({ rows = 5 }: { rows?: number }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="rounded-md border">
|
|
70
|
+
<div className="border-b bg-muted/50 p-4">
|
|
71
|
+
<div className="flex gap-4">
|
|
72
|
+
<Skeleton className="h-4 w-32" />
|
|
73
|
+
<Skeleton className="h-4 w-24" />
|
|
74
|
+
<Skeleton className="h-4 w-28" />
|
|
75
|
+
<Skeleton className="h-4 w-20" />
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
79
|
+
<div key={i} className="border-b p-4 last:border-0">
|
|
80
|
+
<div className="flex gap-4">
|
|
81
|
+
<Skeleton className="h-4 w-32" />
|
|
82
|
+
<Skeleton className="h-4 w-24" />
|
|
83
|
+
<Skeleton className="h-4 w-28" />
|
|
84
|
+
<Skeleton className="h-4 w-20" />
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { ArrowUpDown, MoreHorizontal } from 'lucide-react'
|
|
7
|
+
|
|
8
|
+
interface Column<T> {
|
|
9
|
+
key: keyof T
|
|
10
|
+
label: string
|
|
11
|
+
sortable?: boolean
|
|
12
|
+
render?: (value: any, item: T) => React.ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface EntityTableProps<T extends { id: string }> {
|
|
16
|
+
data: T[]
|
|
17
|
+
columns: Column<T>[]
|
|
18
|
+
entityLower: string
|
|
19
|
+
onDelete?: (id: string) => Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function EntityTable<T extends { id: string }>({
|
|
23
|
+
data,
|
|
24
|
+
columns,
|
|
25
|
+
entityLower,
|
|
26
|
+
onDelete,
|
|
27
|
+
}: EntityTableProps<T>) {
|
|
28
|
+
const [sortKey, setSortKey] = useState<keyof T | null>(null)
|
|
29
|
+
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
|
30
|
+
|
|
31
|
+
const handleSort = (key: keyof T) => {
|
|
32
|
+
if (sortKey === key) {
|
|
33
|
+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
|
34
|
+
} else {
|
|
35
|
+
setSortKey(key)
|
|
36
|
+
setSortOrder('asc')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sortedData = [...data].sort((a, b) => {
|
|
41
|
+
if (!sortKey) return 0
|
|
42
|
+
|
|
43
|
+
const aVal = a[sortKey]
|
|
44
|
+
const bVal = b[sortKey]
|
|
45
|
+
|
|
46
|
+
if (aVal === bVal) return 0
|
|
47
|
+
|
|
48
|
+
const comparison = aVal < bVal ? -1 : 1
|
|
49
|
+
return sortOrder === 'asc' ? comparison : -comparison
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="rounded-md border">
|
|
54
|
+
<table className="w-full">
|
|
55
|
+
<thead className="border-b bg-muted/50">
|
|
56
|
+
<tr>
|
|
57
|
+
{columns.map((column) => (
|
|
58
|
+
<th
|
|
59
|
+
key={String(column.key)}
|
|
60
|
+
className="h-12 px-4 text-left align-middle font-medium text-muted-foreground"
|
|
61
|
+
>
|
|
62
|
+
{column.sortable ? (
|
|
63
|
+
<Button
|
|
64
|
+
variant="ghost"
|
|
65
|
+
size="sm"
|
|
66
|
+
onClick={() => handleSort(column.key)}
|
|
67
|
+
className="-ml-3"
|
|
68
|
+
>
|
|
69
|
+
{column.label}
|
|
70
|
+
<ArrowUpDown className="ml-2 size-4" />
|
|
71
|
+
</Button>
|
|
72
|
+
) : (
|
|
73
|
+
column.label
|
|
74
|
+
)}
|
|
75
|
+
</th>
|
|
76
|
+
))}
|
|
77
|
+
<th className="h-12 px-4 text-left align-middle font-medium text-muted-foreground">
|
|
78
|
+
Actions
|
|
79
|
+
</th>
|
|
80
|
+
</tr>
|
|
81
|
+
</thead>
|
|
82
|
+
<tbody>
|
|
83
|
+
{sortedData.length === 0 ? (
|
|
84
|
+
<tr>
|
|
85
|
+
<td
|
|
86
|
+
colSpan={columns.length + 1}
|
|
87
|
+
className="h-24 text-center text-muted-foreground"
|
|
88
|
+
>
|
|
89
|
+
No items found
|
|
90
|
+
</td>
|
|
91
|
+
</tr>
|
|
92
|
+
) : (
|
|
93
|
+
sortedData.map((item) => (
|
|
94
|
+
<tr key={item.id} className="border-b transition-colors hover:bg-muted/50">
|
|
95
|
+
{columns.map((column) => (
|
|
96
|
+
<td key={String(column.key)} className="p-4 align-middle">
|
|
97
|
+
{column.render
|
|
98
|
+
? column.render(item[column.key], item)
|
|
99
|
+
: String(item[column.key])}
|
|
100
|
+
</td>
|
|
101
|
+
))}
|
|
102
|
+
<td className="p-4 align-middle">
|
|
103
|
+
<div className="flex items-center gap-2">
|
|
104
|
+
<Button variant="ghost" size="sm" asChild>
|
|
105
|
+
<Link href={`/${entityLower}s/${item.id}`}>View</Link>
|
|
106
|
+
</Button>
|
|
107
|
+
<Button variant="ghost" size="sm" asChild>
|
|
108
|
+
<Link href={`/${entityLower}s/${item.id}/edit`}>Edit</Link>
|
|
109
|
+
</Button>
|
|
110
|
+
{onDelete && (
|
|
111
|
+
<Button
|
|
112
|
+
variant="ghost"
|
|
113
|
+
size="sm"
|
|
114
|
+
onClick={() => onDelete(item.id)}
|
|
115
|
+
className="text-destructive hover:text-destructive"
|
|
116
|
+
>
|
|
117
|
+
Delete
|
|
118
|
+
</Button>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
))
|
|
124
|
+
)}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
11
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
12
|
+
outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground',
|
|
13
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
14
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
15
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: 'h-10 px-4 py-2',
|
|
19
|
+
sm: 'h-9 rounded-md px-3',
|
|
20
|
+
lg: 'h-11 rounded-md px-8',
|
|
21
|
+
icon: 'size-10',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default',
|
|
26
|
+
size: 'default',
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export interface ButtonProps
|
|
32
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
33
|
+
VariantProps<typeof buttonVariants> {
|
|
34
|
+
asChild?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Button({
|
|
38
|
+
className,
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
asChild = false,
|
|
42
|
+
ref,
|
|
43
|
+
...props
|
|
44
|
+
}: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) {
|
|
45
|
+
const Comp = asChild ? Slot : 'button'
|
|
46
|
+
return (
|
|
47
|
+
<Comp
|
|
48
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
49
|
+
ref={ref}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
4
|
+
|
|
5
|
+
export function Card({ className, ...props }: CardProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className={cn(
|
|
9
|
+
'rounded-lg border border-border bg-card text-card-foreground shadow-sm',
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
18
|
+
|
|
19
|
+
export function CardHeader({ className, ...props }: CardHeaderProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
|
|
29
|
+
|
|
30
|
+
export function CardTitle({ className, ...props }: CardTitleProps) {
|
|
31
|
+
return (
|
|
32
|
+
<h3
|
|
33
|
+
className={cn(
|
|
34
|
+
'text-2xl font-semibold leading-none tracking-tight',
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}
|
|
43
|
+
|
|
44
|
+
export function CardDescription({ className, ...props }: CardDescriptionProps) {
|
|
45
|
+
return (
|
|
46
|
+
<p
|
|
47
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
54
|
+
|
|
55
|
+
export function CardContent({ className, ...props }: CardContentProps) {
|
|
56
|
+
return <div className={cn('p-6 pt-0', className)} {...props} />
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
60
|
+
|
|
61
|
+
export function CardFooter({ className, ...props }: CardFooterProps) {
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
error?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Input({ className, type, error, ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="relative">
|
|
10
|
+
<input
|
|
11
|
+
type={type}
|
|
12
|
+
className={cn(
|
|
13
|
+
'flex h-10 w-full rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background',
|
|
14
|
+
'file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
|
15
|
+
'placeholder:text-muted-foreground',
|
|
16
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
17
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
18
|
+
error && 'border-destructive focus-visible:ring-destructive',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
ref={ref}
|
|
22
|
+
aria-invalid={!!error}
|
|
23
|
+
aria-describedby={error ? `${props.id}-error` : undefined}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
{error && (
|
|
27
|
+
<p id={`${props.id}-error`} className="mt-1 text-sm text-destructive" role="alert">
|
|
28
|
+
{error}
|
|
29
|
+
</p>
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
4
|
+
required?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Label({ className, required, children, ...props }: LabelProps) {
|
|
8
|
+
return (
|
|
9
|
+
<label
|
|
10
|
+
className={cn(
|
|
11
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
{children}
|
|
17
|
+
{required && <span className="ml-1 text-destructive">*</span>}
|
|
18
|
+
</label>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
export interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
4
|
+
|
|
5
|
+
export function Skeleton({ className, ...props }: SkeletonProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className={cn(
|
|
9
|
+
'animate-pulse rounded-md bg-muted',
|
|
10
|
+
className
|
|
11
|
+
)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
}
|