rl-core-front 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +129 -0
  2. package/package.json +113 -0
  3. package/src/_services/api/axios.factory.ts +97 -0
  4. package/src/_services/api/list-query.ts +26 -0
  5. package/src/_services/api/schema.d.ts +2527 -0
  6. package/src/_services/auth/index.ts +127 -0
  7. package/src/_utils/advanced-filter.ts +386 -0
  8. package/src/_utils/calendar.ts +103 -0
  9. package/src/_utils/crop-image.ts +65 -0
  10. package/src/_utils/filter.ts +313 -0
  11. package/src/_utils/format.ts +35 -0
  12. package/src/_utils/initials.ts +24 -0
  13. package/src/_utils/password.ts +7 -0
  14. package/src/components/app-shell.tsx +314 -0
  15. package/src/components/breadcrumbs.tsx +55 -0
  16. package/src/components/index.ts +7 -0
  17. package/src/components/language-selector.tsx +51 -0
  18. package/src/components/password-field.tsx +40 -0
  19. package/src/components/theme-toggle.tsx +27 -0
  20. package/src/components/ui/alert.tsx +27 -0
  21. package/src/components/ui/avatar.tsx +50 -0
  22. package/src/components/ui/badge.tsx +36 -0
  23. package/src/components/ui/button.tsx +62 -0
  24. package/src/components/ui/card.tsx +86 -0
  25. package/src/components/ui/checkbox.tsx +30 -0
  26. package/src/components/ui/column-visibility-modal.tsx +110 -0
  27. package/src/components/ui/confirm-dialog.tsx +69 -0
  28. package/src/components/ui/data-table.tsx +401 -0
  29. package/src/components/ui/date-range-picker.tsx +250 -0
  30. package/src/components/ui/dialog.tsx +138 -0
  31. package/src/components/ui/dropdown-menu.tsx +108 -0
  32. package/src/components/ui/field.tsx +20 -0
  33. package/src/components/ui/filter-chips.tsx +88 -0
  34. package/src/components/ui/filter-field.tsx +137 -0
  35. package/src/components/ui/filter-group.tsx +155 -0
  36. package/src/components/ui/filter-rule.tsx +221 -0
  37. package/src/components/ui/filter-sheet.tsx +201 -0
  38. package/src/components/ui/index.ts +30 -0
  39. package/src/components/ui/input.tsx +25 -0
  40. package/src/components/ui/label.tsx +23 -0
  41. package/src/components/ui/row-actions.tsx +32 -0
  42. package/src/components/ui/select.tsx +97 -0
  43. package/src/components/ui/separator.tsx +31 -0
  44. package/src/components/ui/sheet.tsx +119 -0
  45. package/src/components/ui/sonner.tsx +28 -0
  46. package/src/components/ui/spinner.tsx +12 -0
  47. package/src/components/ui/switch.tsx +29 -0
  48. package/src/components/ui/table.tsx +84 -0
  49. package/src/components/ui/tabs.tsx +52 -0
  50. package/src/components/user-avatar.tsx +54 -0
  51. package/src/contexts/auth-context.tsx +90 -0
  52. package/src/contexts/color-mode-context.tsx +105 -0
  53. package/src/contexts/i18n-context.tsx +76 -0
  54. package/src/contexts/index.ts +6 -0
  55. package/src/contexts/socket-context.tsx +153 -0
  56. package/src/contexts/toast-context.tsx +40 -0
  57. package/src/features/audit/audit-screen.tsx +299 -0
  58. package/src/features/audit/hooks/use-audit-trail.ts +68 -0
  59. package/src/features/audit/services/audit.service.ts +24 -0
  60. package/src/features/dashboard/dashboard-screen.tsx +73 -0
  61. package/src/features/login/components/backup-codes-dialog.tsx +66 -0
  62. package/src/features/login/components/index.ts +5 -0
  63. package/src/features/login/components/login-form.tsx +62 -0
  64. package/src/features/login/components/two-factor-setup.tsx +71 -0
  65. package/src/features/login/components/two-factor-verify.tsx +56 -0
  66. package/src/features/login/hooks/use-login-flow.ts +119 -0
  67. package/src/features/login/login-screen.tsx +83 -0
  68. package/src/features/login/validation/schemas.ts +30 -0
  69. package/src/features/logs/hooks/use-error-logs.ts +65 -0
  70. package/src/features/logs/hooks/use-request-logs.ts +65 -0
  71. package/src/features/logs/logs-screen.tsx +31 -0
  72. package/src/features/logs/panels/error-logs-panel.tsx +183 -0
  73. package/src/features/logs/panels/index.ts +3 -0
  74. package/src/features/logs/panels/request-logs-panel.tsx +124 -0
  75. package/src/features/logs/services/logs.service.ts +68 -0
  76. package/src/features/notifications/components/notification-item.tsx +67 -0
  77. package/src/features/notifications/hooks/use-notifications.ts +78 -0
  78. package/src/features/notifications/notifications-center.tsx +119 -0
  79. package/src/features/notifications/services/notifications.service.ts +38 -0
  80. package/src/features/profile/components/avatar-crop-dialog.tsx +133 -0
  81. package/src/features/profile/components/index.ts +2 -0
  82. package/src/features/profile/force-password-change.tsx +114 -0
  83. package/src/features/profile/profile-screen.tsx +385 -0
  84. package/src/features/rbac/panels/actions-panel.tsx +20 -0
  85. package/src/features/rbac/panels/catalog-panel.tsx +243 -0
  86. package/src/features/rbac/panels/groups-panel.tsx +269 -0
  87. package/src/features/rbac/panels/index.ts +8 -0
  88. package/src/features/rbac/panels/permissions-panel.tsx +204 -0
  89. package/src/features/rbac/panels/resources-panel.tsx +20 -0
  90. package/src/features/rbac/panels/roles-panel.tsx +224 -0
  91. package/src/features/rbac/panels/scopes-panel.tsx +20 -0
  92. package/src/features/rbac/rbac-screen.tsx +80 -0
  93. package/src/features/rbac/services/rbac.service.ts +110 -0
  94. package/src/features/recovery/forgot-password-screen.tsx +69 -0
  95. package/src/features/recovery/reset-password-screen.tsx +163 -0
  96. package/src/features/users/components/user-form-dialog.tsx +187 -0
  97. package/src/features/users/hooks/use-users.ts +181 -0
  98. package/src/features/users/services/users.service.ts +83 -0
  99. package/src/features/users/users-screen.tsx +331 -0
  100. package/src/hooks/use-column-visibility.ts +54 -0
  101. package/src/hooks/use-confirm.tsx +70 -0
  102. package/src/hooks/use-filter-schema.ts +85 -0
  103. package/src/hooks/use-filters.ts +148 -0
  104. package/src/hooks/use-list-query.ts +69 -0
  105. package/src/hooks/use-request.ts +114 -0
  106. package/src/i18n/messages/en.ts +296 -0
  107. package/src/i18n/messages/pt.ts +305 -0
  108. package/src/index.ts +60 -0
  109. package/src/lib/utils.ts +6 -0
  110. package/src/middleware.ts +42 -0
  111. package/src/styles/core.css +142 -0
  112. package/tailwind-preset.ts +103 -0
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { ChevronRight, Home } from "lucide-react";
4
+ import { usePathname, useRouter } from "next/navigation";
5
+ import type { JSX } from "react";
6
+
7
+ import { useI18n } from "#core/contexts";
8
+
9
+ const SEGMENT_LABELS: Record<string, string> = {
10
+ dashboard: "nav.dashboard",
11
+ usuarios: "nav.users",
12
+ rbac: "nav.rbac",
13
+ logs: "nav.logs",
14
+ perfil: "nav.profile",
15
+ };
16
+
17
+ export function Breadcrumbs(): JSX.Element {
18
+ const pathname = usePathname();
19
+ const router = useRouter();
20
+ const { t } = useI18n();
21
+
22
+ const segments = pathname.split("/").filter(Boolean);
23
+ const crumbs = segments.map((seg, i) => ({
24
+ label: SEGMENT_LABELS[seg] ? t(SEGMENT_LABELS[seg]) : seg,
25
+ href: "/" + segments.slice(0, i + 1).join("/"),
26
+ isLast: i === segments.length - 1,
27
+ }));
28
+
29
+ return (
30
+ <nav className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground">
31
+ <button
32
+ onClick={() => router.push("/dashboard")}
33
+ className="flex items-center gap-1 transition-colors hover:text-foreground"
34
+ >
35
+ <Home className="h-4 w-4" />
36
+ {t("nav.home")}
37
+ </button>
38
+ {crumbs.map((c) => (
39
+ <span key={c.href} className="flex items-center gap-1.5">
40
+ <ChevronRight className="h-4 w-4 text-muted-foreground/50" />
41
+ {c.isLast ? (
42
+ <span className="font-semibold text-foreground">{c.label}</span>
43
+ ) : (
44
+ <button
45
+ onClick={() => router.push(c.href)}
46
+ className="transition-colors hover:text-foreground"
47
+ >
48
+ {c.label}
49
+ </button>
50
+ )}
51
+ </span>
52
+ ))}
53
+ </nav>
54
+ );
55
+ }
@@ -0,0 +1,7 @@
1
+ // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
+ export * from "./app-shell";
3
+ export * from "./breadcrumbs";
4
+ export * from "./language-selector";
5
+ export * from "./password-field";
6
+ export * from "./theme-toggle";
7
+ export * from "./user-avatar";
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { Check, Languages } from "lucide-react";
4
+ import type { JSX } from "react";
5
+
6
+ import {
7
+ Button,
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from "#core/components/ui";
13
+ import { Locale, useI18n } from "#core/contexts";
14
+ import { cn } from "#core/lib/utils";
15
+
16
+ const LANGS: { code: Locale; label: string }[] = [
17
+ { code: "pt", label: "Português" },
18
+ { code: "en", label: "English" },
19
+ ];
20
+
21
+ export function LanguageSelector(): JSX.Element {
22
+ const { locale, setLocale, t } = useI18n();
23
+
24
+ return (
25
+ <DropdownMenu>
26
+ <DropdownMenuTrigger asChild>
27
+ <Button
28
+ variant="ghost"
29
+ size="icon"
30
+ aria-label="language"
31
+ title={t("common.language")}
32
+ >
33
+ <Languages className="h-5 w-5" />
34
+ </Button>
35
+ </DropdownMenuTrigger>
36
+ <DropdownMenuContent align="end">
37
+ {LANGS.map((l) => (
38
+ <DropdownMenuItem key={l.code} onClick={() => setLocale(l.code)}>
39
+ <Check
40
+ className={cn(
41
+ "h-4 w-4",
42
+ l.code === locale ? "opacity-100" : "opacity-0",
43
+ )}
44
+ />
45
+ {l.label}
46
+ </DropdownMenuItem>
47
+ ))}
48
+ </DropdownMenuContent>
49
+ </DropdownMenu>
50
+ );
51
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { Eye, EyeOff } from "lucide-react";
4
+ import { forwardRef, InputHTMLAttributes, useState } from "react";
5
+
6
+ import { Input } from "#core/components/ui";
7
+ import { cn } from "#core/lib/utils";
8
+
9
+ interface PasswordFieldProps extends InputHTMLAttributes<HTMLInputElement> {
10
+ error?: boolean;
11
+ }
12
+
13
+ export const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
14
+ function PasswordField({ className, error, ...props }, ref) {
15
+ const [show, setShow] = useState(false);
16
+ return (
17
+ <div className="relative">
18
+ <Input
19
+ ref={ref}
20
+ type={show ? "text" : "password"}
21
+ className={cn(
22
+ "pr-10",
23
+ error && "border-destructive focus-visible:ring-destructive",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ <button
29
+ type="button"
30
+ tabIndex={-1}
31
+ onClick={() => setShow((s) => !s)}
32
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
33
+ aria-label={show ? "ocultar senha" : "mostrar senha"}
34
+ >
35
+ {show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
36
+ </button>
37
+ </div>
38
+ );
39
+ },
40
+ );
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { Moon, Sun } from "lucide-react";
4
+ import type { JSX } from "react";
5
+
6
+ import { Button } from "#core/components/ui";
7
+ import { useColorMode, useI18n } from "#core/contexts";
8
+
9
+ export function ThemeToggle(): JSX.Element {
10
+ const { mode, toggle } = useColorMode();
11
+ const { t } = useI18n();
12
+ return (
13
+ <Button
14
+ variant="ghost"
15
+ size="icon"
16
+ onClick={toggle}
17
+ aria-label="toggle theme"
18
+ title={t("common.theme")}
19
+ >
20
+ {mode === "light" ? (
21
+ <Moon className="h-5 w-5" />
22
+ ) : (
23
+ <Sun className="h-5 w-5" />
24
+ )}
25
+ </Button>
26
+ );
27
+ }
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "#core/lib/utils";
5
+
6
+ const alertVariants = cva("rounded-md border px-3 py-2 text-sm", {
7
+ variants: {
8
+ variant: {
9
+ info: "border-primary/30 bg-primary/10 text-primary",
10
+ success: "border-success/30 bg-success/10 text-success",
11
+ warning: "border-warning/30 bg-warning/10 text-warning",
12
+ error: "border-destructive/30 bg-destructive/10 text-destructive",
13
+ },
14
+ },
15
+ defaultVariants: { variant: "info" },
16
+ });
17
+
18
+ export interface AlertProps
19
+ extends
20
+ React.HTMLAttributes<HTMLDivElement>,
21
+ VariantProps<typeof alertVariants> {}
22
+
23
+ export function Alert({ variant, className, ...props }: AlertProps): React.JSX.Element {
24
+ return (
25
+ <div className={cn(alertVariants({ variant }), className)} {...props} />
26
+ );
27
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "#core/lib/utils";
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
22
+
23
+ const AvatarImage = React.forwardRef<
24
+ React.ElementRef<typeof AvatarPrimitive.Image>,
25
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26
+ >(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Image
28
+ ref={ref}
29
+ className={cn("aspect-square h-full w-full", className)}
30
+ {...props}
31
+ />
32
+ ));
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34
+
35
+ const AvatarFallback = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
37
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Fallback
40
+ ref={ref}
41
+ className={cn(
42
+ "flex h-full w-full items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground",
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ ));
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49
+
50
+ export { Avatar, AvatarFallback,AvatarImage };
@@ -0,0 +1,36 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "#core/lib/utils";
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary/10 text-primary",
12
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
13
+ destructive: "border-transparent bg-destructive/10 text-destructive",
14
+ success: "border-transparent bg-success/15 text-success",
15
+ warning: "border-transparent bg-warning/15 text-warning",
16
+ outline: "text-foreground",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ },
23
+ );
24
+
25
+ export interface BadgeProps
26
+ extends
27
+ React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps): React.JSX.Element {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ );
34
+ }
35
+
36
+ export { Badge, badgeVariants };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "#core/lib/utils";
8
+
9
+ const buttonVariants = cva(
10
+ "inline-flex items-center justify-center gap-2 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 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
15
+ destructive:
16
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
17
+ outline:
18
+ "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
19
+ secondary:
20
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
21
+ ghost: "hover:bg-accent hover:text-accent-foreground",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-10 px-4 py-2",
26
+ sm: "h-9 rounded-md px-3",
27
+ lg: "h-11 rounded-md px-8",
28
+ icon: "h-10 w-10",
29
+ // Ícone de ação dentro de linha de tabela: seis deles com 40px deixavam
30
+ // a coluna larga demais.
31
+ iconSm: "h-8 w-8",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "default",
36
+ size: "default",
37
+ },
38
+ },
39
+ );
40
+
41
+ export interface ButtonProps
42
+ extends
43
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
44
+ VariantProps<typeof buttonVariants> {
45
+ asChild?: boolean;
46
+ }
47
+
48
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
49
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
50
+ const Comp = asChild ? Slot : "button";
51
+ return (
52
+ <Comp
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ ref={ref}
55
+ {...props}
56
+ />
57
+ );
58
+ },
59
+ );
60
+ Button.displayName = "Button";
61
+
62
+ export { Button, buttonVariants };
@@ -0,0 +1,86 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "#core/lib/utils";
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-xl border border-border bg-card text-card-foreground shadow-sm",
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ ));
18
+ Card.displayName = "Card";
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ));
30
+ CardHeader.displayName = "CardHeader";
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ "text-lg font-semibold leading-none tracking-tight",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ));
45
+ CardTitle.displayName = "CardTitle";
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLDivElement,
49
+ React.HTMLAttributes<HTMLDivElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ));
57
+ CardDescription.displayName = "CardDescription";
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ));
65
+ CardContent.displayName = "CardContent";
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ));
77
+ CardFooter.displayName = "CardFooter";
78
+
79
+ export {
80
+ Card,
81
+ CardContent,
82
+ CardDescription,
83
+ CardFooter,
84
+ CardHeader,
85
+ CardTitle,
86
+ };
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
4
+ import { Check } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "#core/lib/utils";
8
+
9
+ const Checkbox = React.forwardRef<
10
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
11
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12
+ >(({ className, ...props }, ref) => (
13
+ <CheckboxPrimitive.Root
14
+ ref={ref}
15
+ className={cn(
16
+ "peer h-4 w-4 shrink-0 rounded border border-primary shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17
+ className,
18
+ )}
19
+ {...props}
20
+ >
21
+ <CheckboxPrimitive.Indicator
22
+ className={cn("flex items-center justify-center text-current")}
23
+ >
24
+ <Check className="h-3.5 w-3.5" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ ));
28
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29
+
30
+ export { Checkbox };
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+ import { useEffect, useState } from "react";
5
+
6
+ import { Button } from "#core/components/ui/button";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "#core/components/ui/dialog";
14
+ import { Separator } from "#core/components/ui/separator";
15
+ import { Switch } from "#core/components/ui/switch";
16
+ import { useI18n } from "#core/contexts";
17
+
18
+ export interface ColumnVisibilityOption {
19
+ id: string;
20
+ label: string;
21
+ }
22
+
23
+ interface Props {
24
+ open: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ columns: ColumnVisibilityOption[];
27
+ hiddenColumns: string[];
28
+ onSave: (hiddenColumns: string[]) => void;
29
+ }
30
+
31
+ export function ColumnVisibilityModal({
32
+ open,
33
+ onOpenChange,
34
+ columns,
35
+ hiddenColumns,
36
+ onSave,
37
+ }: Props): JSX.Element {
38
+ const { t } = useI18n();
39
+ const [draftHidden, setDraftHidden] = useState<string[]>(hiddenColumns);
40
+
41
+ useEffect(() => {
42
+ if (open) {setDraftHidden(hiddenColumns);}
43
+ }, [open, hiddenColumns]);
44
+
45
+ const isVisible = (id: string): boolean => !draftHidden.includes(id);
46
+ const visibleCount = columns.length - draftHidden.length;
47
+ const isLastVisible = (id: string): boolean => visibleCount === 1 && isVisible(id);
48
+ const areAllVisible = draftHidden.length === 0;
49
+
50
+ const toggleColumn = (id: string): void => {
51
+ if (isLastVisible(id)) {return;}
52
+ setDraftHidden((cur) =>
53
+ cur.includes(id) ? cur.filter((c) => c !== id) : [...cur, id],
54
+ );
55
+ };
56
+
57
+ const toggleAll = (): void => {
58
+ if (!areAllVisible) {setDraftHidden([]);}
59
+ else {setDraftHidden(columns.slice(1).map((c) => c.id));}
60
+ };
61
+
62
+ const handleSave = (): void => {
63
+ onSave(draftHidden);
64
+ onOpenChange(false);
65
+ };
66
+
67
+ return (
68
+ <Dialog open={open} onOpenChange={onOpenChange}>
69
+ <DialogContent className="max-w-md">
70
+ <DialogHeader>
71
+ <DialogTitle>{t("table.columnsTitle")}</DialogTitle>
72
+ </DialogHeader>
73
+ <p className="text-sm text-muted-foreground">
74
+ {t("table.columnsHint")}
75
+ </p>
76
+
77
+ <div className="flex max-h-[55vh] flex-col divide-y divide-border overflow-y-auto">
78
+ <div className="flex items-center justify-between py-3">
79
+ <span className="text-sm font-semibold">{t("table.columns")}</span>
80
+ <Switch checked={areAllVisible} onCheckedChange={toggleAll} />
81
+ </div>
82
+ {columns.map((col) => (
83
+ <div
84
+ key={col.id}
85
+ className="flex items-center justify-between py-3"
86
+ >
87
+ <span className="text-sm">{col.label}</span>
88
+ <Switch
89
+ checked={isVisible(col.id)}
90
+ disabled={isLastVisible(col.id)}
91
+ title={
92
+ isLastVisible(col.id) ? t("table.atLeastOne") : undefined
93
+ }
94
+ onCheckedChange={() => toggleColumn(col.id)}
95
+ />
96
+ </div>
97
+ ))}
98
+ </div>
99
+
100
+ <Separator />
101
+ <DialogFooter className="gap-2">
102
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
103
+ {t("common.cancel")}
104
+ </Button>
105
+ <Button onClick={handleSave}>{t("common.save")}</Button>
106
+ </DialogFooter>
107
+ </DialogContent>
108
+ </Dialog>
109
+ );
110
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { Button } from "#core/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogForm,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "#core/components/ui/dialog";
15
+
16
+ export interface ConfirmDialogProps {
17
+ open: boolean;
18
+ title: string;
19
+ description?: string;
20
+ confirmLabel: string;
21
+ cancelLabel: string;
22
+ /** Pinta o botão de confirmar como destrutivo (excluir, resetar). */
23
+ destructive?: boolean;
24
+ loading?: boolean;
25
+ onConfirm: () => void;
26
+ onCancel: () => void;
27
+ }
28
+
29
+ /**
30
+ * Substitui o `confirm()` do browser: respeita o tema, fecha no `Esc`/`X` e
31
+ * confirma no `Enter` (o botão de confirmar é o submit e recebe o foco).
32
+ */
33
+ export function ConfirmDialog({
34
+ open,
35
+ title,
36
+ description,
37
+ confirmLabel,
38
+ cancelLabel,
39
+ destructive = false,
40
+ loading = false,
41
+ onConfirm,
42
+ onCancel,
43
+ }: ConfirmDialogProps): React.JSX.Element {
44
+ return (
45
+ <Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
46
+ <DialogContent className="max-w-sm">
47
+ <DialogHeader>
48
+ <DialogTitle>{title}</DialogTitle>
49
+ {description && <DialogDescription>{description}</DialogDescription>}
50
+ </DialogHeader>
51
+ <DialogForm onSubmit={onConfirm}>
52
+ <DialogFooter className="gap-2">
53
+ <Button type="button" variant="outline" onClick={onCancel}>
54
+ {cancelLabel}
55
+ </Button>
56
+ <Button
57
+ type="submit"
58
+ autoFocus
59
+ variant={destructive ? "destructive" : "default"}
60
+ disabled={loading}
61
+ >
62
+ {confirmLabel}
63
+ </Button>
64
+ </DialogFooter>
65
+ </DialogForm>
66
+ </DialogContent>
67
+ </Dialog>
68
+ );
69
+ }