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,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 };
|