@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.
Files changed (74) hide show
  1. package/README.md +52 -0
  2. package/dist/components/auth-card.d.ts +10 -0
  3. package/dist/components/auth-card.d.ts.map +1 -0
  4. package/dist/components/confirm-dialog.d.ts +15 -0
  5. package/dist/components/confirm-dialog.d.ts.map +1 -0
  6. package/dist/components/data-table-wrapper.d.ts +16 -0
  7. package/dist/components/data-table-wrapper.d.ts.map +1 -0
  8. package/dist/components/empty-state.d.ts +11 -0
  9. package/dist/components/empty-state.d.ts.map +1 -0
  10. package/dist/components/form-dialog.d.ts +14 -0
  11. package/dist/components/form-dialog.d.ts.map +1 -0
  12. package/dist/components/form-field.d.ts +12 -0
  13. package/dist/components/form-field.d.ts.map +1 -0
  14. package/dist/components/mobile-card-list.d.ts +12 -0
  15. package/dist/components/mobile-card-list.d.ts.map +1 -0
  16. package/dist/components/mode-toggle.d.ts +2 -0
  17. package/dist/components/mode-toggle.d.ts.map +1 -0
  18. package/dist/components/page-header.d.ts +10 -0
  19. package/dist/components/page-header.d.ts.map +1 -0
  20. package/dist/components/pagination.d.ts +10 -0
  21. package/dist/components/pagination.d.ts.map +1 -0
  22. package/dist/components/responsive-data-view.d.ts +14 -0
  23. package/dist/components/responsive-data-view.d.ts.map +1 -0
  24. package/dist/components/search-input.d.ts +7 -0
  25. package/dist/components/search-input.d.ts.map +1 -0
  26. package/dist/components/stat-card.d.ts +11 -0
  27. package/dist/components/stat-card.d.ts.map +1 -0
  28. package/dist/components/status-dot.d.ts +8 -0
  29. package/dist/components/status-dot.d.ts.map +1 -0
  30. package/dist/components/table-skeleton.d.ts +7 -0
  31. package/dist/components/table-skeleton.d.ts.map +1 -0
  32. package/dist/components/theme-provider.d.ts +14 -0
  33. package/dist/components/theme-provider.d.ts.map +1 -0
  34. package/dist/components/toaster.d.ts +5 -0
  35. package/dist/components/toaster.d.ts.map +1 -0
  36. package/dist/hooks/use-disclosure.d.ts +8 -0
  37. package/dist/hooks/use-disclosure.d.ts.map +1 -0
  38. package/dist/hooks/use-toast.d.ts +5 -0
  39. package/dist/hooks/use-toast.d.ts.map +1 -0
  40. package/dist/index.cjs +620 -0
  41. package/dist/index.cjs.map +1 -0
  42. package/dist/index.d.ts +23 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +561 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/lib/api-error.d.ts +2 -0
  47. package/dist/lib/api-error.d.ts.map +1 -0
  48. package/dist/lib/utils.d.ts +3 -0
  49. package/dist/lib/utils.d.ts.map +1 -0
  50. package/dist/styles.css +45 -0
  51. package/package.json +80 -0
  52. package/src/components/auth-card.tsx +25 -0
  53. package/src/components/confirm-dialog.tsx +58 -0
  54. package/src/components/data-table-wrapper.tsx +65 -0
  55. package/src/components/empty-state.tsx +22 -0
  56. package/src/components/form-dialog.tsx +48 -0
  57. package/src/components/form-field.tsx +26 -0
  58. package/src/components/mobile-card-list.tsx +54 -0
  59. package/src/components/mode-toggle.tsx +47 -0
  60. package/src/components/page-header.tsx +22 -0
  61. package/src/components/pagination.tsx +31 -0
  62. package/src/components/responsive-data-view.tsx +31 -0
  63. package/src/components/search-input.tsx +36 -0
  64. package/src/components/stat-card.tsx +35 -0
  65. package/src/components/status-dot.tsx +16 -0
  66. package/src/components/table-skeleton.tsx +20 -0
  67. package/src/components/theme-provider.tsx +69 -0
  68. package/src/components/toaster.tsx +31 -0
  69. package/src/hooks/use-disclosure.ts +9 -0
  70. package/src/hooks/use-toast.ts +30 -0
  71. package/src/index.ts +29 -0
  72. package/src/lib/api-error.ts +7 -0
  73. package/src/lib/utils.ts +6 -0
  74. package/src/styles.css +45 -0
@@ -0,0 +1,3 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -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"}
@@ -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
+ }