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.
- package/README.md +129 -0
- package/package.json +113 -0
- package/src/_services/api/axios.factory.ts +97 -0
- package/src/_services/api/list-query.ts +26 -0
- package/src/_services/api/schema.d.ts +2527 -0
- package/src/_services/auth/index.ts +127 -0
- package/src/_utils/advanced-filter.ts +386 -0
- package/src/_utils/calendar.ts +103 -0
- package/src/_utils/crop-image.ts +65 -0
- package/src/_utils/filter.ts +313 -0
- package/src/_utils/format.ts +35 -0
- package/src/_utils/initials.ts +24 -0
- package/src/_utils/password.ts +7 -0
- package/src/components/app-shell.tsx +314 -0
- package/src/components/breadcrumbs.tsx +55 -0
- package/src/components/index.ts +7 -0
- package/src/components/language-selector.tsx +51 -0
- package/src/components/password-field.tsx +40 -0
- package/src/components/theme-toggle.tsx +27 -0
- package/src/components/ui/alert.tsx +27 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +86 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/column-visibility-modal.tsx +110 -0
- package/src/components/ui/confirm-dialog.tsx +69 -0
- package/src/components/ui/data-table.tsx +401 -0
- package/src/components/ui/date-range-picker.tsx +250 -0
- package/src/components/ui/dialog.tsx +138 -0
- package/src/components/ui/dropdown-menu.tsx +108 -0
- package/src/components/ui/field.tsx +20 -0
- package/src/components/ui/filter-chips.tsx +88 -0
- package/src/components/ui/filter-field.tsx +137 -0
- package/src/components/ui/filter-group.tsx +155 -0
- package/src/components/ui/filter-rule.tsx +221 -0
- package/src/components/ui/filter-sheet.tsx +201 -0
- package/src/components/ui/index.ts +30 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/row-actions.tsx +32 -0
- package/src/components/ui/select.tsx +97 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +119 -0
- package/src/components/ui/sonner.tsx +28 -0
- package/src/components/ui/spinner.tsx +12 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +84 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/user-avatar.tsx +54 -0
- package/src/contexts/auth-context.tsx +90 -0
- package/src/contexts/color-mode-context.tsx +105 -0
- package/src/contexts/i18n-context.tsx +76 -0
- package/src/contexts/index.ts +6 -0
- package/src/contexts/socket-context.tsx +153 -0
- package/src/contexts/toast-context.tsx +40 -0
- package/src/features/audit/audit-screen.tsx +299 -0
- package/src/features/audit/hooks/use-audit-trail.ts +68 -0
- package/src/features/audit/services/audit.service.ts +24 -0
- package/src/features/dashboard/dashboard-screen.tsx +73 -0
- package/src/features/login/components/backup-codes-dialog.tsx +66 -0
- package/src/features/login/components/index.ts +5 -0
- package/src/features/login/components/login-form.tsx +62 -0
- package/src/features/login/components/two-factor-setup.tsx +71 -0
- package/src/features/login/components/two-factor-verify.tsx +56 -0
- package/src/features/login/hooks/use-login-flow.ts +119 -0
- package/src/features/login/login-screen.tsx +83 -0
- package/src/features/login/validation/schemas.ts +30 -0
- package/src/features/logs/hooks/use-error-logs.ts +65 -0
- package/src/features/logs/hooks/use-request-logs.ts +65 -0
- package/src/features/logs/logs-screen.tsx +31 -0
- package/src/features/logs/panels/error-logs-panel.tsx +183 -0
- package/src/features/logs/panels/index.ts +3 -0
- package/src/features/logs/panels/request-logs-panel.tsx +124 -0
- package/src/features/logs/services/logs.service.ts +68 -0
- package/src/features/notifications/components/notification-item.tsx +67 -0
- package/src/features/notifications/hooks/use-notifications.ts +78 -0
- package/src/features/notifications/notifications-center.tsx +119 -0
- package/src/features/notifications/services/notifications.service.ts +38 -0
- package/src/features/profile/components/avatar-crop-dialog.tsx +133 -0
- package/src/features/profile/components/index.ts +2 -0
- package/src/features/profile/force-password-change.tsx +114 -0
- package/src/features/profile/profile-screen.tsx +385 -0
- package/src/features/rbac/panels/actions-panel.tsx +20 -0
- package/src/features/rbac/panels/catalog-panel.tsx +243 -0
- package/src/features/rbac/panels/groups-panel.tsx +269 -0
- package/src/features/rbac/panels/index.ts +8 -0
- package/src/features/rbac/panels/permissions-panel.tsx +204 -0
- package/src/features/rbac/panels/resources-panel.tsx +20 -0
- package/src/features/rbac/panels/roles-panel.tsx +224 -0
- package/src/features/rbac/panels/scopes-panel.tsx +20 -0
- package/src/features/rbac/rbac-screen.tsx +80 -0
- package/src/features/rbac/services/rbac.service.ts +110 -0
- package/src/features/recovery/forgot-password-screen.tsx +69 -0
- package/src/features/recovery/reset-password-screen.tsx +163 -0
- package/src/features/users/components/user-form-dialog.tsx +187 -0
- package/src/features/users/hooks/use-users.ts +181 -0
- package/src/features/users/services/users.service.ts +83 -0
- package/src/features/users/users-screen.tsx +331 -0
- package/src/hooks/use-column-visibility.ts +54 -0
- package/src/hooks/use-confirm.tsx +70 -0
- package/src/hooks/use-filter-schema.ts +85 -0
- package/src/hooks/use-filters.ts +148 -0
- package/src/hooks/use-list-query.ts +69 -0
- package/src/hooks/use-request.ts +114 -0
- package/src/i18n/messages/en.ts +296 -0
- package/src/i18n/messages/pt.ts +305 -0
- package/src/index.ts +60 -0
- package/src/lib/utils.ts +6 -0
- package/src/middleware.ts +42 -0
- package/src/styles/core.css +142 -0
- 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
|
+
}
|