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,221 @@
1
+ "use client";
2
+
3
+ import { X } from "lucide-react";
4
+ import type { JSX } from "react";
5
+
6
+ import type { AdvancedRule } from "#core/_utils/advanced-filter";
7
+ import {
8
+ LIST_OPERATORS,
9
+ NULLABILITY_OPERATORS,
10
+ RANGE_OPERATORS,
11
+ } from "#core/_utils/advanced-filter";
12
+ import type { FilterField } from "#core/_utils/filter";
13
+ import { Button } from "#core/components/ui/button";
14
+ import { DateRangePicker } from "#core/components/ui/date-range-picker";
15
+ import { Input } from "#core/components/ui/input";
16
+ import { useI18n } from "#core/contexts";
17
+
18
+ export interface FilterRuleRowProps {
19
+ rule: AdvancedRule;
20
+ schema: FilterField[];
21
+ onChange: (rule: AdvancedRule) => void;
22
+ onRemove: () => void;
23
+ /** Rótulo traduzido de cada campo, resolvido pelo painel. */
24
+ labelFor: (field: FilterField) => string;
25
+ }
26
+
27
+ const SELECT_CLASS =
28
+ "h-9 rounded-md border border-input bg-transparent px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
29
+
30
+ /**
31
+ * Uma linha do modo avançado: campo, operador e valor.
32
+ *
33
+ * O operador é escolhido pelo usuário — é o que separa este modo do simples,
34
+ * onde ele é implícito no tipo do campo.
35
+ */
36
+ export function FilterRuleRow({
37
+ rule,
38
+ schema,
39
+ onChange,
40
+ onRemove,
41
+ labelFor,
42
+ }: FilterRuleRowProps): JSX.Element {
43
+ const { t } = useI18n();
44
+ const field = schema.find((item) => item.field === rule.field) ?? schema[0];
45
+
46
+ const changeField = (name: string): void => {
47
+ const next = schema.find((item) => item.field === name);
48
+ if (!next) {
49
+ return;
50
+ }
51
+ // Operador e valor pertencem ao campo antigo: trocar o campo os invalida.
52
+ onChange({
53
+ ...rule,
54
+ field: name,
55
+ operator: next.operators[0],
56
+ value: "",
57
+ value2: undefined,
58
+ values: undefined,
59
+ });
60
+ };
61
+
62
+ const isDate = field?.type === "DATE" || field?.type === "DATETIME";
63
+
64
+ const valueControl = (): JSX.Element => {
65
+ if (NULLABILITY_OPERATORS.includes(rule.operator)) {
66
+ return (
67
+ <select
68
+ aria-label={t("filters.value")}
69
+ className={`${SELECT_CLASS} flex-1`}
70
+ value={rule.value}
71
+ onChange={(event) => onChange({ ...rule, value: event.target.value })}
72
+ >
73
+ <option value="">{t("filters.null")}</option>
74
+ <option value="true">{t("common.yes")}</option>
75
+ <option value="false">{t("common.no")}</option>
76
+ </select>
77
+ );
78
+ }
79
+
80
+ if (RANGE_OPERATORS.includes(rule.operator)) {
81
+ if (isDate) {
82
+ return (
83
+ <div className="flex-1">
84
+ <DateRangePicker
85
+ label={labelFor(field)}
86
+ value={{ from: rule.value, to: rule.value2 }}
87
+ onChange={(range) =>
88
+ onChange({ ...rule, value: range.from ?? "", value2: range.to })
89
+ }
90
+ />
91
+ </div>
92
+ );
93
+ }
94
+ return (
95
+ <div className="flex flex-1 items-center gap-2">
96
+ <Input
97
+ className="h-9"
98
+ aria-label={t("filters.from")}
99
+ placeholder={t("filters.from")}
100
+ value={rule.value}
101
+ onChange={(event) => onChange({ ...rule, value: event.target.value })}
102
+ />
103
+ <span className="text-sm text-muted-foreground">–</span>
104
+ <Input
105
+ className="h-9"
106
+ aria-label={t("filters.to")}
107
+ placeholder={t("filters.to")}
108
+ value={rule.value2 ?? ""}
109
+ onChange={(event) => onChange({ ...rule, value2: event.target.value })}
110
+ />
111
+ </div>
112
+ );
113
+ }
114
+
115
+ if (field?.options?.length) {
116
+ const multiple = LIST_OPERATORS.includes(rule.operator);
117
+ const selected = multiple ? (rule.values ?? []) : [rule.value];
118
+ return (
119
+ <select
120
+ multiple={multiple}
121
+ aria-label={t("filters.value")}
122
+ className={`${SELECT_CLASS} flex-1 ${multiple ? "h-20" : ""}`}
123
+ value={multiple ? selected : (selected[0] ?? "")}
124
+ onChange={(event) => {
125
+ if (multiple) {
126
+ const values = Array.from(event.target.selectedOptions, (o) => o.value);
127
+ onChange({ ...rule, values, value: values.join(", ") });
128
+ return;
129
+ }
130
+ onChange({ ...rule, value: event.target.value });
131
+ }}
132
+ >
133
+ {!multiple && <option value="">—</option>}
134
+ {field.options.map((option) => (
135
+ <option key={option.value} value={option.value}>
136
+ {option.label}
137
+ </option>
138
+ ))}
139
+ </select>
140
+ );
141
+ }
142
+
143
+ if (isDate) {
144
+ return (
145
+ <div className="flex-1">
146
+ <DateRangePicker
147
+ label={labelFor(field)}
148
+ // Operador de ponta única: só a primeira data conta.
149
+ value={{ from: rule.value }}
150
+ onChange={(range) => onChange({ ...rule, value: range.from ?? "" })}
151
+ presets={[]}
152
+ />
153
+ </div>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <Input
159
+ className="h-9 flex-1"
160
+ aria-label={t("filters.value")}
161
+ placeholder={
162
+ LIST_OPERATORS.includes(rule.operator)
163
+ ? t("filters.listHint")
164
+ : undefined
165
+ }
166
+ value={rule.value}
167
+ onChange={(event) =>
168
+ onChange({ ...rule, value: event.target.value, values: undefined })
169
+ }
170
+ />
171
+ );
172
+ };
173
+
174
+ return (
175
+ <div className="flex flex-wrap items-center gap-2">
176
+ <select
177
+ aria-label={t("filters.field")}
178
+ className={SELECT_CLASS}
179
+ value={rule.field}
180
+ onChange={(event) => changeField(event.target.value)}
181
+ >
182
+ {schema.map((item) => (
183
+ <option key={item.field} value={item.field}>
184
+ {labelFor(item)}
185
+ </option>
186
+ ))}
187
+ </select>
188
+
189
+ <select
190
+ aria-label={t("filters.operator")}
191
+ className={SELECT_CLASS}
192
+ value={rule.operator}
193
+ onChange={(event) =>
194
+ onChange({
195
+ ...rule,
196
+ operator: event.target.value as AdvancedRule["operator"],
197
+ values: undefined,
198
+ })
199
+ }
200
+ >
201
+ {(field?.operators ?? []).map((operator) => (
202
+ <option key={operator} value={operator}>
203
+ {t(`filters.operators.${operator.replace("$", "")}`)}
204
+ </option>
205
+ ))}
206
+ </select>
207
+
208
+ {valueControl()}
209
+
210
+ <Button
211
+ type="button"
212
+ variant="ghost"
213
+ size="iconSm"
214
+ aria-label={t("filters.removeRule")}
215
+ onClick={onRemove}
216
+ >
217
+ <X className="h-4 w-4" />
218
+ </Button>
219
+ </div>
220
+ );
221
+ }
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ import type { JSX } from "react";
4
+ import { useEffect, useState } from "react";
5
+
6
+ import type { AdvancedGroup } from "#core/_utils/advanced-filter";
7
+ import { countConditions, initialTree } from "#core/_utils/advanced-filter";
8
+ import type { FilterDraftValue, FilterField, FilterValues } from "#core/_utils/filter";
9
+ import { activeFilterCount } from "#core/_utils/filter";
10
+ import { Button } from "#core/components/ui/button";
11
+ import { FilterFieldControl } from "#core/components/ui/filter-field";
12
+ import { FilterGroupBlock } from "#core/components/ui/filter-group";
13
+ import { Separator } from "#core/components/ui/separator";
14
+ import {
15
+ Sheet,
16
+ SheetBody,
17
+ SheetContent,
18
+ SheetDescription,
19
+ SheetFooter,
20
+ SheetHeader,
21
+ SheetTitle,
22
+ } from "#core/components/ui/sheet";
23
+ import { Spinner } from "#core/components/ui/spinner";
24
+ import { useI18n } from "#core/contexts";
25
+ import type { FilterMode, FiltersState } from "#core/hooks/use-filters";
26
+ import { cn } from "#core/lib/utils";
27
+
28
+ export interface FilterSheetProps {
29
+ open: boolean;
30
+ onOpenChange: (open: boolean) => void;
31
+ filters: FiltersState;
32
+ }
33
+
34
+ /**
35
+ * Rótulo do campo: chave de i18n quando existir, senão o `label` que o backend
36
+ * mandou (em português). É o que deixa o `en` melhorar aos poucos sem que uma
37
+ * listagem nova precise de tradução para funcionar.
38
+ */
39
+ export const useFieldLabel = (): ((field: FilterField) => string) => {
40
+ const { t } = useI18n();
41
+ return (field: FilterField): string => {
42
+ const key = `filters.fields.${field.field}`;
43
+ const translated = t(key);
44
+ return translated === key ? field.label : translated;
45
+ };
46
+ };
47
+
48
+ /**
49
+ * Painel de filtros, nos dois modos.
50
+ *
51
+ * Trabalha sobre um rascunho e só devolve no "Aplicar" — cada tecla digitada
52
+ * não pode virar uma requisição.
53
+ */
54
+ export function FilterSheet({
55
+ open,
56
+ onOpenChange,
57
+ filters,
58
+ }: FilterSheetProps): JSX.Element {
59
+ const { t } = useI18n();
60
+ const labelFor = useFieldLabel();
61
+ const { schema, loading, mode, values, tree, validation } = filters;
62
+
63
+ const [draft, setDraft] = useState<FilterValues>(values);
64
+ const [draftTree, setDraftTree] = useState<AdvancedGroup | null>(tree);
65
+ const [draftMode, setDraftMode] = useState<FilterMode>(mode);
66
+
67
+ useEffect(() => {
68
+ if (open) {
69
+ setDraft(values);
70
+ setDraftTree(tree);
71
+ setDraftMode(mode);
72
+ }
73
+ }, [open, values, tree, mode]);
74
+
75
+ const setField = (field: string, value: FilterDraftValue): void => {
76
+ setDraft((current) => ({ ...current, [field]: value }));
77
+ };
78
+
79
+ const switchMode = (next: FilterMode): void => {
80
+ setDraftMode(next);
81
+ if (next === "advanced") {
82
+ setDraftTree((current) => current ?? initialTree(schema));
83
+ }
84
+ };
85
+
86
+ const apply = (): void => {
87
+ filters.setMode(draftMode);
88
+ if (draftMode === "advanced") {
89
+ if (draftTree) {
90
+ filters.setTree(draftTree);
91
+ }
92
+ } else {
93
+ filters.setValues(draft);
94
+ }
95
+ onOpenChange(false);
96
+ };
97
+
98
+ const clear = (): void => {
99
+ setDraft({});
100
+ setDraftTree(initialTree(schema));
101
+ if (draftMode === "advanced") {
102
+ filters.setTree(initialTree(schema));
103
+ } else {
104
+ filters.setValues({});
105
+ }
106
+ onOpenChange(false);
107
+ };
108
+
109
+ const activeCount =
110
+ draftMode === "advanced"
111
+ ? draftTree
112
+ ? countConditions(draftTree)
113
+ : 0
114
+ : activeFilterCount(draft);
115
+
116
+ const modes: { id: FilterMode; label: string }[] = [
117
+ { id: "simple", label: t("filters.simple") },
118
+ { id: "advanced", label: t("filters.advanced") },
119
+ ];
120
+
121
+ return (
122
+ <Sheet open={open} onOpenChange={onOpenChange}>
123
+ <SheetContent
124
+ aria-describedby={undefined}
125
+ className={cn(draftMode === "advanced" && "sm:max-w-2xl")}
126
+ >
127
+ <SheetHeader>
128
+ <SheetTitle>{t("filters.title")}</SheetTitle>
129
+ <SheetDescription>
130
+ {draftMode === "advanced" ? t("filters.advancedHint") : t("filters.hint")}
131
+ </SheetDescription>
132
+ </SheetHeader>
133
+
134
+ <div className="inline-flex overflow-hidden rounded-md border border-border">
135
+ {modes.map((item) => (
136
+ <button
137
+ key={item.id}
138
+ type="button"
139
+ onClick={() => switchMode(item.id)}
140
+ className={cn(
141
+ "px-3 py-1.5 text-sm transition-colors",
142
+ draftMode === item.id
143
+ ? "bg-primary text-primary-foreground"
144
+ : "hover:bg-accent",
145
+ )}
146
+ >
147
+ {item.label}
148
+ </button>
149
+ ))}
150
+ </div>
151
+
152
+ <SheetBody className="-mx-1 space-y-5 px-1">
153
+ {loading && <Spinner className="mx-auto mt-8" />}
154
+
155
+ {!loading && schema.length === 0 && (
156
+ <p className="mt-8 text-center text-sm text-muted-foreground">
157
+ {t("filters.empty")}
158
+ </p>
159
+ )}
160
+
161
+ {!loading && schema.length > 0 && draftMode === "simple" && (
162
+ <>
163
+ {schema.map((field) => (
164
+ <FilterFieldControl
165
+ key={field.field}
166
+ field={field}
167
+ label={labelFor(field)}
168
+ value={draft[field.field] ?? {}}
169
+ onChange={(value) => setField(field.field, value)}
170
+ />
171
+ ))}
172
+ </>
173
+ )}
174
+
175
+ {!loading && schema.length > 0 && draftMode === "advanced" && draftTree && (
176
+ <FilterGroupBlock
177
+ group={draftTree}
178
+ schema={schema}
179
+ labelFor={labelFor}
180
+ onChange={setDraftTree}
181
+ />
182
+ )}
183
+ </SheetBody>
184
+
185
+ {draftMode === "advanced" && !validation.valid && validation.reason && (
186
+ <p className="text-sm text-destructive">
187
+ {t(`filters.${validation.reason}`)}
188
+ </p>
189
+ )}
190
+
191
+ <Separator />
192
+ <SheetFooter>
193
+ <Button variant="outline" onClick={clear} disabled={activeCount === 0}>
194
+ {t("filters.clear")}
195
+ </Button>
196
+ <Button onClick={apply}>{t("filters.apply")}</Button>
197
+ </SheetFooter>
198
+ </SheetContent>
199
+ </Sheet>
200
+ );
201
+ }
@@ -0,0 +1,30 @@
1
+ // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
+ export * from "./alert";
3
+ export * from "./avatar";
4
+ export * from "./badge";
5
+ export * from "./button";
6
+ export * from "./card";
7
+ export * from "./checkbox";
8
+ export * from "./column-visibility-modal";
9
+ export * from "./confirm-dialog";
10
+ export * from "./data-table";
11
+ export * from "./date-range-picker";
12
+ export * from "./dialog";
13
+ export * from "./dropdown-menu";
14
+ export * from "./field";
15
+ export * from "./filter-chips";
16
+ export * from "./filter-field";
17
+ export * from "./filter-group";
18
+ export * from "./filter-rule";
19
+ export * from "./filter-sheet";
20
+ export * from "./input";
21
+ export * from "./label";
22
+ export * from "./row-actions";
23
+ export * from "./select";
24
+ export * from "./separator";
25
+ export * from "./sheet";
26
+ export * from "./sonner";
27
+ export * from "./spinner";
28
+ export * from "./switch";
29
+ export * from "./table";
30
+ export * from "./tabs";
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { cn } from "#core/lib/utils";
6
+
7
+ const Input = React.forwardRef<
8
+ HTMLInputElement,
9
+ React.InputHTMLAttributes<HTMLInputElement>
10
+ >(({ className, type, ...props }, ref) => {
11
+ return (
12
+ <input
13
+ type={type}
14
+ className={cn(
15
+ "flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
16
+ className,
17
+ )}
18
+ ref={ref}
19
+ {...props}
20
+ />
21
+ );
22
+ });
23
+ Input.displayName = "Input";
24
+
25
+ export { Input };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import * as LabelPrimitive from "@radix-ui/react-label";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "#core/lib/utils";
7
+
8
+ const Label = React.forwardRef<
9
+ React.ElementRef<typeof LabelPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <LabelPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ ));
21
+ Label.displayName = LabelPrimitive.Root.displayName;
22
+
23
+ export { Label };
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "#core/lib/utils";
4
+
5
+ export interface RowActionsProps {
6
+ children: React.ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * Grupo de botões de ação de uma linha de tabela: centralizado e sem folga
12
+ * entre os botões.
13
+ *
14
+ * Só cuida do arranjo — o tamanho de cada botão é escolha dele
15
+ * (`size="iconSm"` nos de ícone), e quem impede a coluna de esticar é o
16
+ * `meta: { shrink: true }` na definição dela.
17
+ */
18
+ export function RowActions({
19
+ children,
20
+ className,
21
+ }: RowActionsProps): React.JSX.Element {
22
+ return (
23
+ <div
24
+ className={cn(
25
+ "flex items-center justify-center gap-0.5",
26
+ className,
27
+ )}
28
+ >
29
+ {children}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import * as SelectPrimitive from "@radix-ui/react-select";
4
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "#core/lib/utils";
8
+
9
+ const Select = SelectPrimitive.Root;
10
+ const SelectGroup = SelectPrimitive.Group;
11
+ const SelectValue = SelectPrimitive.Value;
12
+
13
+ const SelectTrigger = React.forwardRef<
14
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
15
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16
+ >(({ className, children, ...props }, ref) => (
17
+ <SelectPrimitive.Trigger
18
+ ref={ref}
19
+ className={cn(
20
+ "flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 focus:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 data-[placeholder]:text-muted-foreground",
21
+ className,
22
+ )}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <SelectPrimitive.Icon asChild>
27
+ <ChevronDown className="h-4 w-4 opacity-50" />
28
+ </SelectPrimitive.Icon>
29
+ </SelectPrimitive.Trigger>
30
+ ));
31
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
32
+
33
+ const SelectContent = React.forwardRef<
34
+ React.ElementRef<typeof SelectPrimitive.Content>,
35
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
36
+ >(({ className, children, position = "popper", ...props }, ref) => (
37
+ <SelectPrimitive.Portal>
38
+ <SelectPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
42
+ position === "popper" && "data-[side=bottom]:translate-y-1",
43
+ className,
44
+ )}
45
+ position={position}
46
+ {...props}
47
+ >
48
+ <SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
49
+ <ChevronUp className="h-4 w-4" />
50
+ </SelectPrimitive.ScrollUpButton>
51
+ <SelectPrimitive.Viewport
52
+ className={cn(
53
+ "p-1",
54
+ position === "popper" &&
55
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
56
+ )}
57
+ >
58
+ {children}
59
+ </SelectPrimitive.Viewport>
60
+ <SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
61
+ <ChevronDown className="h-4 w-4" />
62
+ </SelectPrimitive.ScrollDownButton>
63
+ </SelectPrimitive.Content>
64
+ </SelectPrimitive.Portal>
65
+ ));
66
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
67
+
68
+ const SelectItem = React.forwardRef<
69
+ React.ElementRef<typeof SelectPrimitive.Item>,
70
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
71
+ >(({ className, children, ...props }, ref) => (
72
+ <SelectPrimitive.Item
73
+ ref={ref}
74
+ className={cn(
75
+ "relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
76
+ className,
77
+ )}
78
+ {...props}
79
+ >
80
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
81
+ <SelectPrimitive.ItemIndicator>
82
+ <Check className="h-4 w-4" />
83
+ </SelectPrimitive.ItemIndicator>
84
+ </span>
85
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
86
+ </SelectPrimitive.Item>
87
+ ));
88
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
89
+
90
+ export {
91
+ Select,
92
+ SelectContent,
93
+ SelectGroup,
94
+ SelectItem,
95
+ SelectTrigger,
96
+ SelectValue,
97
+ };
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import * as SeparatorPrimitive from "@radix-ui/react-separator";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "#core/lib/utils";
7
+
8
+ const Separator = React.forwardRef<
9
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
+ >(
12
+ (
13
+ { className, orientation = "horizontal", decorative = true, ...props },
14
+ ref,
15
+ ) => (
16
+ <SeparatorPrimitive.Root
17
+ ref={ref}
18
+ decorative={decorative}
19
+ orientation={orientation}
20
+ className={cn(
21
+ "shrink-0 bg-border",
22
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ ),
28
+ );
29
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
30
+
31
+ export { Separator };