@motor-hero/ui-kit 0.5.1
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/README.md +52 -0
- package/dist/components/auth-card.d.ts +10 -0
- package/dist/components/auth-card.d.ts.map +1 -0
- package/dist/components/confirm-dialog.d.ts +15 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/data-table-wrapper.d.ts +16 -0
- package/dist/components/data-table-wrapper.d.ts.map +1 -0
- package/dist/components/empty-state.d.ts +11 -0
- package/dist/components/empty-state.d.ts.map +1 -0
- package/dist/components/form-dialog.d.ts +14 -0
- package/dist/components/form-dialog.d.ts.map +1 -0
- package/dist/components/form-field.d.ts +12 -0
- package/dist/components/form-field.d.ts.map +1 -0
- package/dist/components/mobile-card-list.d.ts +12 -0
- package/dist/components/mobile-card-list.d.ts.map +1 -0
- package/dist/components/mode-toggle.d.ts +2 -0
- package/dist/components/mode-toggle.d.ts.map +1 -0
- package/dist/components/page-header.d.ts +10 -0
- package/dist/components/page-header.d.ts.map +1 -0
- package/dist/components/pagination.d.ts +10 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/responsive-data-view.d.ts +14 -0
- package/dist/components/responsive-data-view.d.ts.map +1 -0
- package/dist/components/search-input.d.ts +7 -0
- package/dist/components/search-input.d.ts.map +1 -0
- package/dist/components/stat-card.d.ts +11 -0
- package/dist/components/stat-card.d.ts.map +1 -0
- package/dist/components/status-dot.d.ts +8 -0
- package/dist/components/status-dot.d.ts.map +1 -0
- package/dist/components/table-skeleton.d.ts +7 -0
- package/dist/components/table-skeleton.d.ts.map +1 -0
- package/dist/components/theme-provider.d.ts +14 -0
- package/dist/components/theme-provider.d.ts.map +1 -0
- package/dist/components/toaster.d.ts +5 -0
- package/dist/components/toaster.d.ts.map +1 -0
- package/dist/hooks/use-disclosure.d.ts +8 -0
- package/dist/hooks/use-disclosure.d.ts.map +1 -0
- package/dist/hooks/use-toast.d.ts +5 -0
- package/dist/hooks/use-toast.d.ts.map +1 -0
- package/dist/index.cjs +620 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +561 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-error.d.ts +2 -0
- package/dist/lib/api-error.d.ts.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/styles.css +45 -0
- package/package.json +80 -0
- package/src/components/auth-card.tsx +25 -0
- package/src/components/confirm-dialog.tsx +58 -0
- package/src/components/data-table-wrapper.tsx +65 -0
- package/src/components/empty-state.tsx +22 -0
- package/src/components/form-dialog.tsx +48 -0
- package/src/components/form-field.tsx +26 -0
- package/src/components/mobile-card-list.tsx +54 -0
- package/src/components/mode-toggle.tsx +47 -0
- package/src/components/page-header.tsx +22 -0
- package/src/components/pagination.tsx +31 -0
- package/src/components/responsive-data-view.tsx +31 -0
- package/src/components/search-input.tsx +36 -0
- package/src/components/stat-card.tsx +35 -0
- package/src/components/status-dot.tsx +16 -0
- package/src/components/table-skeleton.tsx +20 -0
- package/src/components/theme-provider.tsx +69 -0
- package/src/components/toaster.tsx +31 -0
- package/src/hooks/use-disclosure.ts +9 -0
- package/src/hooks/use-toast.ts +30 -0
- package/src/index.ts +29 -0
- package/src/lib/api-error.ts +7 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAQ,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* @motor-hero/ui-kit - Zinc theme tokens */
|
|
2
|
+
:root {
|
|
3
|
+
--background: 0 0% 100%;
|
|
4
|
+
--foreground: 240 10% 3.9%;
|
|
5
|
+
--card: 0 0% 100%;
|
|
6
|
+
--card-foreground: 240 10% 3.9%;
|
|
7
|
+
--popover: 0 0% 100%;
|
|
8
|
+
--popover-foreground: 240 10% 3.9%;
|
|
9
|
+
--primary: 240 5.9% 10%;
|
|
10
|
+
--primary-foreground: 0 0% 98%;
|
|
11
|
+
--secondary: 240 4.8% 95.9%;
|
|
12
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
13
|
+
--muted: 240 4.8% 95.9%;
|
|
14
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
15
|
+
--accent: 240 4.8% 95.9%;
|
|
16
|
+
--accent-foreground: 240 5.9% 10%;
|
|
17
|
+
--destructive: 0 84.2% 60.2%;
|
|
18
|
+
--destructive-foreground: 0 0% 98%;
|
|
19
|
+
--border: 240 5.9% 90%;
|
|
20
|
+
--input: 240 5.9% 90%;
|
|
21
|
+
--ring: 240 5.9% 10%;
|
|
22
|
+
--radius: 0.5rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dark {
|
|
26
|
+
--background: 240 10% 3.9%;
|
|
27
|
+
--foreground: 0 0% 98%;
|
|
28
|
+
--card: 240 10% 3.9%;
|
|
29
|
+
--card-foreground: 0 0% 98%;
|
|
30
|
+
--popover: 240 10% 3.9%;
|
|
31
|
+
--popover-foreground: 0 0% 98%;
|
|
32
|
+
--primary: 0 0% 98%;
|
|
33
|
+
--primary-foreground: 240 5.9% 10%;
|
|
34
|
+
--secondary: 240 3.7% 15.9%;
|
|
35
|
+
--secondary-foreground: 0 0% 98%;
|
|
36
|
+
--muted: 240 3.7% 15.9%;
|
|
37
|
+
--muted-foreground: 240 5% 64.9%;
|
|
38
|
+
--accent: 240 3.7% 15.9%;
|
|
39
|
+
--accent-foreground: 0 0% 98%;
|
|
40
|
+
--destructive: 0 62.8% 30.6%;
|
|
41
|
+
--destructive-foreground: 0 0% 98%;
|
|
42
|
+
--border: 240 3.7% 15.9%;
|
|
43
|
+
--input: 240 3.7% 15.9%;
|
|
44
|
+
--ring: 240 4.9% 83.9%;
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@motor-hero/ui-kit",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
|
|
5
|
+
"author": "MotorHero",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"ui-kit",
|
|
10
|
+
"shadcn-ui",
|
|
11
|
+
"tailwindcss",
|
|
12
|
+
"components",
|
|
13
|
+
"motorhero"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://ui.motorhero.com.br",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.cjs",
|
|
18
|
+
"module": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./styles": "./dist/styles.css"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/motor-hero/motor-hero-ui-kit.git"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup && tsc --emitDeclarationOnly --outDir dist -p tsconfig.build.json",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"lint": "tsc --noEmit",
|
|
45
|
+
"docs:dev": "vite --config docs/vite.config.ts",
|
|
46
|
+
"docs:build": "vite build --config docs/vite.config.ts",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"clsx": "^2.0.0",
|
|
51
|
+
"lucide-react": ">=0.400.0",
|
|
52
|
+
"react": "^19.0.0",
|
|
53
|
+
"react-dom": "^19.0.0",
|
|
54
|
+
"tailwind-merge": ">=2.0.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@radix-ui/react-alert-dialog": "^1.1.0",
|
|
58
|
+
"@radix-ui/react-dropdown-menu": "^2.1.0",
|
|
59
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
60
|
+
"class-variance-authority": "^0.7.0",
|
|
61
|
+
"sonner": "^2.0.7"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
65
|
+
"@types/react": "^19.0.0",
|
|
66
|
+
"@types/react-dom": "^19.0.0",
|
|
67
|
+
"@vitejs/plugin-react-swc": "^3.11.0",
|
|
68
|
+
"clsx": "^2.0.0",
|
|
69
|
+
"highlight.js": "^11.11.1",
|
|
70
|
+
"lucide-react": "^1.17.0",
|
|
71
|
+
"react": "^19.0.0",
|
|
72
|
+
"react-dom": "^19.0.0",
|
|
73
|
+
"tailwind-merge": "^3.6.0",
|
|
74
|
+
"tailwindcss": "^4.3.0",
|
|
75
|
+
"tsup": "^8.0.0",
|
|
76
|
+
"tw-animate-css": "^1.4.0",
|
|
77
|
+
"typescript": "~5.9.3",
|
|
78
|
+
"vite": "^6.4.2"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface AuthCardProps {
|
|
4
|
+
title: string
|
|
5
|
+
description?: string
|
|
6
|
+
children: ReactNode
|
|
7
|
+
footer?: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AuthCard({ title, description, children, footer }: AuthCardProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
13
|
+
<div className="w-full max-w-sm rounded-lg border bg-card p-6 shadow-sm">
|
|
14
|
+
<div className="mb-6 text-center">
|
|
15
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
16
|
+
{description && (
|
|
17
|
+
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
18
|
+
)}
|
|
19
|
+
</div>
|
|
20
|
+
<div className="space-y-4">{children}</div>
|
|
21
|
+
{footer && <div className="mt-4">{footer}</div>}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as AlertDialog from "@radix-ui/react-alert-dialog"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
interface ConfirmDialogProps {
|
|
5
|
+
open: boolean
|
|
6
|
+
onOpenChange: (open: boolean) => void
|
|
7
|
+
onConfirm: () => void
|
|
8
|
+
title: string
|
|
9
|
+
description: ReactNode
|
|
10
|
+
confirmLabel?: string
|
|
11
|
+
cancelLabel?: string
|
|
12
|
+
loading?: boolean
|
|
13
|
+
variant?: "default" | "destructive"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ConfirmDialog({
|
|
17
|
+
open,
|
|
18
|
+
onOpenChange,
|
|
19
|
+
onConfirm,
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
confirmLabel = "Confirmar",
|
|
23
|
+
cancelLabel = "Cancelar",
|
|
24
|
+
loading = false,
|
|
25
|
+
variant = "default",
|
|
26
|
+
}: ConfirmDialogProps) {
|
|
27
|
+
return (
|
|
28
|
+
<AlertDialog.Root open={open} onOpenChange={onOpenChange}>
|
|
29
|
+
<AlertDialog.Portal>
|
|
30
|
+
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
|
31
|
+
<AlertDialog.Content className="fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg">
|
|
32
|
+
<div className="flex flex-col space-y-2 text-center sm:text-left">
|
|
33
|
+
<AlertDialog.Title className="text-lg font-semibold">{title}</AlertDialog.Title>
|
|
34
|
+
<AlertDialog.Description className="text-sm text-muted-foreground">
|
|
35
|
+
{description}
|
|
36
|
+
</AlertDialog.Description>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
39
|
+
<AlertDialog.Cancel className="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 cursor-pointer">
|
|
40
|
+
{cancelLabel}
|
|
41
|
+
</AlertDialog.Cancel>
|
|
42
|
+
<AlertDialog.Action
|
|
43
|
+
onClick={onConfirm}
|
|
44
|
+
disabled={loading}
|
|
45
|
+
className={`inline-flex h-9 items-center justify-center rounded-md px-4 text-sm font-medium shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 cursor-pointer ${
|
|
46
|
+
variant === "destructive"
|
|
47
|
+
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
48
|
+
: "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
49
|
+
}`}
|
|
50
|
+
>
|
|
51
|
+
{loading ? "Aguarde..." : confirmLabel}
|
|
52
|
+
</AlertDialog.Action>
|
|
53
|
+
</div>
|
|
54
|
+
</AlertDialog.Content>
|
|
55
|
+
</AlertDialog.Portal>
|
|
56
|
+
</AlertDialog.Root>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface DataTableWrapperProps {
|
|
4
|
+
children: ReactNode
|
|
5
|
+
isEmpty: boolean
|
|
6
|
+
isLoading: boolean
|
|
7
|
+
emptyIcon?: ReactNode
|
|
8
|
+
emptyTitle?: string
|
|
9
|
+
emptyDescription?: string
|
|
10
|
+
page?: number
|
|
11
|
+
onPageChange?: (page: number) => void
|
|
12
|
+
hasNextPage?: boolean
|
|
13
|
+
hasPreviousPage?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DataTableWrapper({
|
|
17
|
+
children,
|
|
18
|
+
isEmpty,
|
|
19
|
+
isLoading,
|
|
20
|
+
emptyIcon,
|
|
21
|
+
emptyTitle = "Nenhum registro encontrado",
|
|
22
|
+
emptyDescription,
|
|
23
|
+
page,
|
|
24
|
+
onPageChange,
|
|
25
|
+
hasNextPage = false,
|
|
26
|
+
hasPreviousPage = false,
|
|
27
|
+
}: DataTableWrapperProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-4">
|
|
30
|
+
<div className="overflow-x-auto rounded-md border">{children}</div>
|
|
31
|
+
|
|
32
|
+
{!isLoading && isEmpty && (
|
|
33
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
34
|
+
{emptyIcon && <div className="mb-4 text-muted-foreground">{emptyIcon}</div>}
|
|
35
|
+
<h3 className="text-lg font-semibold tracking-tight">{emptyTitle}</h3>
|
|
36
|
+
{emptyDescription && (
|
|
37
|
+
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{emptyDescription}</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{page !== undefined && onPageChange && (
|
|
43
|
+
<div className="flex items-center justify-end gap-4">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={() => onPageChange(page - 1)}
|
|
47
|
+
disabled={!hasPreviousPage}
|
|
48
|
+
className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
49
|
+
>
|
|
50
|
+
Anterior
|
|
51
|
+
</button>
|
|
52
|
+
<span className="text-sm text-muted-foreground">Página {page}</span>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => onPageChange(page + 1)}
|
|
56
|
+
disabled={!hasNextPage}
|
|
57
|
+
className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
58
|
+
>
|
|
59
|
+
Próximo
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface EmptyStateProps {
|
|
4
|
+
icon?: ReactNode
|
|
5
|
+
title: string
|
|
6
|
+
description?: string
|
|
7
|
+
action?: ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className={`flex flex-col items-center justify-center py-16 text-center ${className ?? ""}`}>
|
|
14
|
+
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
|
|
15
|
+
<h3 className="text-lg font-semibold tracking-tight">{title}</h3>
|
|
16
|
+
{description && (
|
|
17
|
+
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
|
|
18
|
+
)}
|
|
19
|
+
{action && <div className="mt-4">{action}</div>}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface FormDialogLayoutProps {
|
|
4
|
+
title: string
|
|
5
|
+
children: ReactNode
|
|
6
|
+
onSubmit: (e: React.FormEvent) => void
|
|
7
|
+
submitLabel?: string
|
|
8
|
+
cancelLabel?: string
|
|
9
|
+
onCancel: () => void
|
|
10
|
+
isSubmitting?: boolean
|
|
11
|
+
isDisabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function FormDialogLayout({
|
|
15
|
+
title,
|
|
16
|
+
children,
|
|
17
|
+
onSubmit,
|
|
18
|
+
submitLabel = "Salvar",
|
|
19
|
+
cancelLabel = "Cancelar",
|
|
20
|
+
onCancel,
|
|
21
|
+
isSubmitting = false,
|
|
22
|
+
isDisabled = false,
|
|
23
|
+
}: FormDialogLayoutProps) {
|
|
24
|
+
return (
|
|
25
|
+
<form onSubmit={onSubmit}>
|
|
26
|
+
<div className="flex flex-col space-y-1.5 text-center sm:text-left">
|
|
27
|
+
<h2 className="text-lg font-semibold leading-none tracking-tight">{title}</h2>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="space-y-4 py-4">{children}</div>
|
|
30
|
+
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={onCancel}
|
|
34
|
+
className="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
35
|
+
>
|
|
36
|
+
{cancelLabel}
|
|
37
|
+
</button>
|
|
38
|
+
<button
|
|
39
|
+
type="submit"
|
|
40
|
+
disabled={isSubmitting || isDisabled}
|
|
41
|
+
className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
42
|
+
>
|
|
43
|
+
{isSubmitting ? "Salvando..." : submitLabel}
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</form>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface FormFieldProps {
|
|
4
|
+
label: string
|
|
5
|
+
htmlFor?: string
|
|
6
|
+
error?: string
|
|
7
|
+
required?: boolean
|
|
8
|
+
children: ReactNode
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FormField({ label, htmlFor, error, required, children, className }: FormFieldProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={`space-y-2 ${className ?? ""}`}>
|
|
15
|
+
<label
|
|
16
|
+
htmlFor={htmlFor}
|
|
17
|
+
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
18
|
+
>
|
|
19
|
+
{label}
|
|
20
|
+
{required && <span className="ml-1 text-destructive">*</span>}
|
|
21
|
+
</label>
|
|
22
|
+
{children}
|
|
23
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface MobileCardListProps<T> {
|
|
4
|
+
data: T[]
|
|
5
|
+
renderCard: (item: T, index: number) => ReactNode
|
|
6
|
+
keyExtractor: (item: T) => string
|
|
7
|
+
isLoading?: boolean
|
|
8
|
+
loadingCount?: number
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MobileCardList<T>({
|
|
13
|
+
data,
|
|
14
|
+
renderCard,
|
|
15
|
+
keyExtractor,
|
|
16
|
+
isLoading = false,
|
|
17
|
+
loadingCount = 5,
|
|
18
|
+
className,
|
|
19
|
+
}: MobileCardListProps<T>) {
|
|
20
|
+
if (isLoading) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
23
|
+
{Array.from({ length: loadingCount }).map((_, i) => (
|
|
24
|
+
<div key={i} className="rounded-xl border p-4">
|
|
25
|
+
<div className="space-y-3">
|
|
26
|
+
<div className="flex justify-between">
|
|
27
|
+
<div className="h-5 w-32 animate-pulse rounded bg-muted" />
|
|
28
|
+
<div className="h-5 w-16 animate-pulse rounded bg-muted" />
|
|
29
|
+
</div>
|
|
30
|
+
<div className="h-4 w-48 animate-pulse rounded bg-muted" />
|
|
31
|
+
<div className="flex justify-between">
|
|
32
|
+
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
|
33
|
+
<div className="h-4 w-20 animate-pulse rounded bg-muted" />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
44
|
+
{data.map((item, index) => (
|
|
45
|
+
<div
|
|
46
|
+
key={keyExtractor(item)}
|
|
47
|
+
className="rounded-xl border p-4 transition-all duration-150 hover:border-foreground/20 active:scale-[0.99]"
|
|
48
|
+
>
|
|
49
|
+
{renderCard(item, index)}
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
|
2
|
+
import { Moon, Sun, Monitor } from "lucide-react"
|
|
3
|
+
import { useTheme } from "./theme-provider"
|
|
4
|
+
|
|
5
|
+
export function ModeToggle() {
|
|
6
|
+
const { setTheme } = useTheme()
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<DropdownMenu.Root>
|
|
10
|
+
<DropdownMenu.Trigger asChild>
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
14
|
+
aria-label="Alternar tema"
|
|
15
|
+
>
|
|
16
|
+
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
|
17
|
+
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
|
18
|
+
</button>
|
|
19
|
+
</DropdownMenu.Trigger>
|
|
20
|
+
<DropdownMenu.Portal>
|
|
21
|
+
<DropdownMenu.Content
|
|
22
|
+
align="end"
|
|
23
|
+
className="z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
|
24
|
+
>
|
|
25
|
+
<DropdownMenu.Item
|
|
26
|
+
className="flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent"
|
|
27
|
+
onClick={() => setTheme("light")}
|
|
28
|
+
>
|
|
29
|
+
<Sun className="h-4 w-4" /> Claro
|
|
30
|
+
</DropdownMenu.Item>
|
|
31
|
+
<DropdownMenu.Item
|
|
32
|
+
className="flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent"
|
|
33
|
+
onClick={() => setTheme("dark")}
|
|
34
|
+
>
|
|
35
|
+
<Moon className="h-4 w-4" /> Escuro
|
|
36
|
+
</DropdownMenu.Item>
|
|
37
|
+
<DropdownMenu.Item
|
|
38
|
+
className="flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent"
|
|
39
|
+
onClick={() => setTheme("system")}
|
|
40
|
+
>
|
|
41
|
+
<Monitor className="h-4 w-4" /> Sistema
|
|
42
|
+
</DropdownMenu.Item>
|
|
43
|
+
</DropdownMenu.Content>
|
|
44
|
+
</DropdownMenu.Portal>
|
|
45
|
+
</DropdownMenu.Root>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface PageHeaderProps {
|
|
4
|
+
title: string
|
|
5
|
+
description?: string
|
|
6
|
+
action?: ReactNode
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PageHeader({ title, description, action, className }: PageHeaderProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={`flex items-center justify-between ${className ?? ""}`}>
|
|
13
|
+
<div>
|
|
14
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
15
|
+
{description && (
|
|
16
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
{action && <div>{action}</div>}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface PaginationProps {
|
|
2
|
+
page: number
|
|
3
|
+
onPageChange: (page: number) => void
|
|
4
|
+
hasNextPage: boolean
|
|
5
|
+
hasPreviousPage: boolean
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Pagination({ page, onPageChange, hasNextPage, hasPreviousPage, className }: PaginationProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={`flex items-center justify-end gap-4 ${className ?? ""}`}>
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
onClick={() => onPageChange(page - 1)}
|
|
15
|
+
disabled={!hasPreviousPage}
|
|
16
|
+
className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
17
|
+
>
|
|
18
|
+
Anterior
|
|
19
|
+
</button>
|
|
20
|
+
<span className="text-sm text-muted-foreground">Página {page}</span>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => onPageChange(page + 1)}
|
|
24
|
+
disabled={!hasNextPage}
|
|
25
|
+
className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
26
|
+
>
|
|
27
|
+
Próximo
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface ResponsiveDataViewProps {
|
|
4
|
+
table: ReactNode
|
|
5
|
+
cards: ReactNode
|
|
6
|
+
isEmpty: boolean
|
|
7
|
+
isLoading: boolean
|
|
8
|
+
emptyIcon?: ReactNode
|
|
9
|
+
emptyTitle?: string
|
|
10
|
+
emptyDescription?: string
|
|
11
|
+
pagination?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ResponsiveDataView({
|
|
15
|
+
table, cards, isEmpty, isLoading, emptyIcon, emptyTitle = "Nenhum registro encontrado", emptyDescription, pagination,
|
|
16
|
+
}: ResponsiveDataViewProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="space-y-4">
|
|
19
|
+
<div className="hidden overflow-x-auto rounded-md border md:block">{table}</div>
|
|
20
|
+
<div className="md:hidden">{cards}</div>
|
|
21
|
+
{!isLoading && isEmpty && (
|
|
22
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
23
|
+
{emptyIcon && <div className="mb-4 text-muted-foreground">{emptyIcon}</div>}
|
|
24
|
+
<h3 className="text-lg font-semibold tracking-tight">{emptyTitle}</h3>
|
|
25
|
+
{emptyDescription && <p className="mt-1 max-w-sm text-sm text-muted-foreground">{emptyDescription}</p>}
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
{pagination}
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
containerClassName?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
|
|
8
|
+
({ containerClassName, className, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<div className={`relative flex-1 ${containerClassName ?? ""}`}>
|
|
11
|
+
<svg
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
width="16"
|
|
14
|
+
height="16"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
|
22
|
+
>
|
|
23
|
+
<circle cx="11" cy="11" r="8" />
|
|
24
|
+
<path d="m21 21-4.3-4.3" />
|
|
25
|
+
</svg>
|
|
26
|
+
<input
|
|
27
|
+
ref={ref}
|
|
28
|
+
type="text"
|
|
29
|
+
className={`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 pl-10 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${className ?? ""}`}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
SearchInput.displayName = "SearchInput"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
interface StatCardProps {
|
|
4
|
+
label: string
|
|
5
|
+
value: ReactNode
|
|
6
|
+
detail?: string
|
|
7
|
+
icon?: ReactNode
|
|
8
|
+
isLoading?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function StatCard({ label, value, detail, icon, isLoading }: StatCardProps) {
|
|
12
|
+
if (isLoading) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
|
15
|
+
<div className="flex items-center justify-between pb-2">
|
|
16
|
+
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
|
|
17
|
+
<div className="h-4 w-4 animate-pulse rounded bg-muted" />
|
|
18
|
+
</div>
|
|
19
|
+
<div className="mt-2 h-7 w-16 animate-pulse rounded bg-muted" />
|
|
20
|
+
<div className="mt-1 h-4 w-20 animate-pulse rounded bg-muted" />
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
|
27
|
+
<div className="flex items-center justify-between pb-2">
|
|
28
|
+
<span className="text-sm font-medium text-muted-foreground">{label}</span>
|
|
29
|
+
{icon && <span className="text-muted-foreground">{icon}</span>}
|
|
30
|
+
</div>
|
|
31
|
+
<div className="text-2xl font-bold">{value}</div>
|
|
32
|
+
{detail && <p className="text-xs text-muted-foreground">{detail}</p>}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface StatusDotProps {
|
|
2
|
+
active: boolean
|
|
3
|
+
label?: string
|
|
4
|
+
className?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function StatusDot({ active, label, className }: StatusDotProps) {
|
|
8
|
+
return (
|
|
9
|
+
<span className={`inline-flex items-center gap-2 ${className ?? ""}`}>
|
|
10
|
+
<span
|
|
11
|
+
className={`h-2 w-2 rounded-full ${active ? "bg-green-500" : "bg-red-500"}`}
|
|
12
|
+
/>
|
|
13
|
+
{label && <span>{label}</span>}
|
|
14
|
+
</span>
|
|
15
|
+
)
|
|
16
|
+
}
|