rl-core-front 0.19.6 → 0.19.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rl-core-front",
3
- "version": "0.19.6",
3
+ "version": "0.19.7",
4
4
  "description": "Telas e componentes Next.js do core: login com 2FA, usuários, RBAC, auditoria, logs e listagens com filtro dinâmico",
5
5
  "author": "Rodrigo Liberti",
6
6
  "license": "MIT",
@@ -81,7 +81,7 @@ export function ColumnVisibilityModal({
81
81
  {t("table.columnsHint")}
82
82
  </p>
83
83
 
84
- <div className="flex max-h-[55vh] flex-col divide-y divide-border overflow-y-auto">
84
+ <div className="flex max-h-[55vh] flex-col divide-y divide-border overflow-y-auto overscroll-contain">
85
85
  <div className="flex items-center justify-between py-3">
86
86
  <span className="text-sm font-semibold">{t("table.columns")}</span>
87
87
  <Switch checked={areAllVisible} onCheckedChange={toggleAll} />
@@ -422,7 +422,7 @@ export function Combobox({
422
422
  * campo sem cobri-lo.
423
423
  */
424
424
  className={cn(
425
- "max-h-[min(18rem,var(--radix-dropdown-menu-content-available-height))] w-(--radix-dropdown-menu-trigger-width) overflow-y-auto p-1",
425
+ "max-h-[min(18rem,var(--radix-dropdown-menu-content-available-height))] w-(--radix-dropdown-menu-trigger-width) overflow-y-auto overscroll-contain p-1",
426
426
  !multiple && "data-[side=bottom]:-mt-(--radix-dropdown-menu-trigger-height)",
427
427
  )}
428
428
  align="start"
@@ -515,7 +515,7 @@ const DialogForm = React.forwardRef<
515
515
  vertical cortava a borda de baixo do último campo — que é o que
516
516
  aparece em todo modal com um campo colado no rodapé.
517
517
  `-m-1` devolve o recuo para o conteúdo seguir alinhado. */}
518
- <div className="-m-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-1 [scrollbar-width:thin]">
518
+ <div className="-m-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain p-1">
519
519
  {hasFooter ? items.slice(0, footerAt) : items}
520
520
  </div>
521
521
  {hasFooter && items.slice(footerAt)}
@@ -6,13 +6,31 @@ interface FieldProps {
6
6
  label?: string;
7
7
  error?: string;
8
8
  htmlFor?: string;
9
+ /**
10
+ * Marca o rótulo com o "*" de obrigatório.
11
+ *
12
+ * Fica no rótulo, e não no controle, porque é o único lugar comum a todos
13
+ * eles: `Input`, `Combobox`, `MoneyInput` e `DatePicker` desenham a caixa de
14
+ * um jeito cada um, e a marca precisa aparecer no mesmo ponto nos quatro.
15
+ */
16
+ required?: boolean;
9
17
  children: React.ReactNode;
10
18
  }
11
19
 
12
- export function Field({ label, error, htmlFor, children }: FieldProps): JSX.Element {
20
+ export function Field({
21
+ label,
22
+ error,
23
+ htmlFor,
24
+ required,
25
+ children,
26
+ }: FieldProps): JSX.Element {
13
27
  return (
14
28
  <div className="space-y-1.5">
15
- {label && <Label htmlFor={htmlFor}>{label}</Label>}
29
+ {label && (
30
+ <Label htmlFor={htmlFor} required={required}>
31
+ {label}
32
+ </Label>
33
+ )}
16
34
  {children}
17
35
  {error && <p className="text-xs font-medium text-destructive">{error}</p>}
18
36
  </div>
@@ -140,7 +140,7 @@ export function FilterFieldControl({
140
140
  <fieldset className="space-y-2">
141
141
  <legend className="text-sm font-medium">{label}</legend>
142
142
  {field.options?.length ? (
143
- <div className="max-h-44 space-y-2 overflow-y-auto rounded-md border border-border p-3">
143
+ <div className="max-h-44 space-y-2 overflow-y-auto overscroll-contain rounded-md border border-border p-3">
144
144
  {field.options.map((option) => (
145
145
  <label
146
146
  key={option.value}
@@ -5,10 +5,23 @@ import * as React from "react";
5
5
 
6
6
  import { cn } from "#core/lib/utils";
7
7
 
8
+ export interface LabelProps
9
+ extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> {
10
+ /**
11
+ * Campo obrigatório: acrescenta o "*" vermelho depois do texto.
12
+ *
13
+ * A marca é só visual (`aria-hidden`) — quem usa leitor de tela ouve a
14
+ * obrigatoriedade do próprio controle, então o campo também precisa do
15
+ * `required`/`aria-required`. Um asterisco lido em voz alta no meio do
16
+ * rótulo não diz nada; o atributo no controle diz.
17
+ */
18
+ required?: boolean;
19
+ }
20
+
8
21
  const Label = React.forwardRef<
9
22
  React.ElementRef<typeof LabelPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
11
- >(({ className, ...props }, ref) => (
23
+ LabelProps
24
+ >(({ className, required, children, ...props }, ref) => (
12
25
  <LabelPrimitive.Root
13
26
  ref={ref}
14
27
  className={cn(
@@ -16,7 +29,14 @@ const Label = React.forwardRef<
16
29
  className,
17
30
  )}
18
31
  {...props}
19
- />
32
+ >
33
+ {children}
34
+ {required && (
35
+ <span aria-hidden className="ml-0.5 text-destructive">
36
+ *
37
+ </span>
38
+ )}
39
+ </LabelPrimitive.Root>
20
40
  ));
21
41
  Label.displayName = LabelPrimitive.Root.displayName;
22
42
 
@@ -45,7 +45,7 @@ const PopoverContent = React.forwardRef<
45
45
  // com a última linha rente ao fim da tela.
46
46
  collisionPadding={8}
47
47
  className={cn(
48
- "z-50 max-h-(--radix-popover-content-available-height) overflow-y-auto rounded-xl border border-border bg-popover p-3 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
48
+ "z-50 max-h-(--radix-popover-content-available-height) overflow-y-auto overscroll-contain rounded-xl border border-border bg-popover p-3 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
49
49
  className,
50
50
  )}
51
51
  {...props}
@@ -66,7 +66,7 @@ const SheetBody = ({
66
66
  className,
67
67
  ...props
68
68
  }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element => (
69
- <div className={cn("flex-1 overflow-y-auto", className)} {...props} />
69
+ <div className={cn("flex-1 overflow-y-auto overscroll-contain", className)} {...props} />
70
70
  );
71
71
  SheetBody.displayName = "SheetBody";
72
72
 
@@ -46,7 +46,7 @@ export function LoginForm({
46
46
  onSubmit={handleSubmit((d) => onSubmit(d.email, d.password))}
47
47
  className="flex flex-col gap-4"
48
48
  >
49
- <Field label={t("auth.email")} error={errors.email?.message}>
49
+ <Field label={t("auth.email")} error={errors.email?.message} required>
50
50
  <Input
51
51
  type="email"
52
52
  autoComplete="email"
@@ -55,7 +55,7 @@ export function LoginForm({
55
55
  {...register("email")}
56
56
  />
57
57
  </Field>
58
- <Field label={t("auth.password")} error={errors.password?.message}>
58
+ <Field label={t("auth.password")} error={errors.password?.message} required>
59
59
  <PasswordField
60
60
  autoComplete="current-password"
61
61
  autoFocus={!!defaultEmail}
@@ -64,7 +64,7 @@ export function TwoFactorSetup({
64
64
  onSubmit={handleSubmit((d) => onConfirm(d.code))}
65
65
  className="flex flex-col gap-4"
66
66
  >
67
- <Field label={t("auth.codeApp")} error={errors.code?.message}>
67
+ <Field label={t("auth.codeApp")} error={errors.code?.message} required>
68
68
  <Input
69
69
  autoFocus
70
70
  inputMode="numeric"
@@ -35,7 +35,7 @@ export function TwoFactorVerify({ loading, onVerify, onRequestEmail }: Props): J
35
35
  onSubmit={handleSubmit((d) => onVerify(d.code))}
36
36
  className="flex flex-col gap-4"
37
37
  >
38
- <Field label={t("auth.codeAny")} error={errors.code?.message}>
38
+ <Field label={t("auth.codeAny")} error={errors.code?.message} required>
39
39
  <Input autoFocus {...register("code")} />
40
40
  </Field>
41
41
  <Button type="submit" size="lg" disabled={loading}>
@@ -79,6 +79,7 @@ export function ForcePasswordChange({
79
79
  <Field
80
80
  label={t("profile.currentPassword")}
81
81
  error={errors.currentPassword?.message}
82
+ required
82
83
  >
83
84
  <PasswordField
84
85
  error={!!errors.currentPassword}
@@ -88,6 +89,7 @@ export function ForcePasswordChange({
88
89
  <Field
89
90
  label={t("profile.newPassword")}
90
91
  error={errors.newPassword?.message}
92
+ required
91
93
  >
92
94
  <PasswordField
93
95
  error={!!errors.newPassword}
@@ -98,6 +100,7 @@ export function ForcePasswordChange({
98
100
  <Field
99
101
  label={t("profile.confirmPassword")}
100
102
  error={errors.confirm?.message}
103
+ required
101
104
  >
102
105
  <PasswordField
103
106
  error={!!errors.confirm}
@@ -249,13 +249,13 @@ export function ProfileScreen(): JSX.Element {
249
249
  </div>
250
250
 
251
251
  <h2 className="text-lg font-semibold">{t("profile.data")}</h2>
252
- <Field label={t("users.name")} error={formErrors.name}>
252
+ <Field label={t("users.name")} error={formErrors.name} required>
253
253
  <Input
254
254
  value={form.name}
255
255
  onChange={(e) => setForm({ ...form, name: e.target.value })}
256
256
  />
257
257
  </Field>
258
- <Field label={t("users.lastName")} error={formErrors.lastName}>
258
+ <Field label={t("users.lastName")} error={formErrors.lastName} required>
259
259
  <Input
260
260
  value={form.lastName}
261
261
  onChange={(e) => setForm({ ...form, lastName: e.target.value })}
@@ -354,6 +354,7 @@ export function ProfileScreen(): JSX.Element {
354
354
  <Field
355
355
  label={t("profile.currentPassword")}
356
356
  error={errors.currentPassword?.message}
357
+ required
357
358
  >
358
359
  <PasswordField
359
360
  error={!!errors.currentPassword}
@@ -363,6 +364,7 @@ export function ProfileScreen(): JSX.Element {
363
364
  <Field
364
365
  label={t("profile.newPassword")}
365
366
  error={errors.newPassword?.message}
367
+ required
366
368
  >
367
369
  <PasswordField
368
370
  error={!!errors.newPassword}
@@ -373,6 +375,7 @@ export function ProfileScreen(): JSX.Element {
373
375
  <Field
374
376
  label={t("profile.confirmPassword")}
375
377
  error={errors.confirm?.message}
378
+ required
376
379
  >
377
380
  <PasswordField
378
381
  error={!!errors.confirm}
@@ -219,7 +219,7 @@ export function CatalogPanel({
219
219
  </DialogTitle>
220
220
  </DialogHeader>
221
221
  <DialogForm onSubmit={() => void save()}>
222
- <Field label="Name">
222
+ <Field label="Name" required>
223
223
  <Input
224
224
  placeholder={namePlaceholder}
225
225
  value={form.name}
@@ -250,7 +250,7 @@ export function GroupsPanel(): JSX.Element {
250
250
  </DialogTitle>
251
251
  </DialogHeader>
252
252
  <DialogForm onSubmit={() => void save()}>
253
- <Field label={t("users.name")}>
253
+ <Field label={t("users.name")} required>
254
254
  <Input
255
255
  value={form.name}
256
256
  onChange={(e) => setForm({ ...form, name: e.target.value })}
@@ -180,7 +180,7 @@ export function PermissionsPanel(): JSX.Element {
180
180
  </DialogTitle>
181
181
  </DialogHeader>
182
182
  <DialogForm onSubmit={() => void save()}>
183
- <Field label="Code (resource:action:scope)">
183
+ <Field label="Code (resource:action:scope)" required>
184
184
  <Input
185
185
  placeholder="users:read:any"
186
186
  value={form.code}
@@ -201,7 +201,7 @@ export function RolesPanel(): JSX.Element {
201
201
  </DialogHeader>
202
202
  <DialogForm onSubmit={handleSubmit((values) => void save(values))}>
203
203
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
204
- <Field label={t("rbac.roleName")} error={errors.name?.message}>
204
+ <Field label={t("rbac.roleName")} error={errors.name?.message} required>
205
205
  <Input {...register("name")} />
206
206
  </Field>
207
207
  <Field
@@ -48,7 +48,7 @@ export function ForgotPasswordScreen(): JSX.Element {
48
48
  <p className="text-sm text-muted-foreground">
49
49
  {t("recovery.forgotHint")}
50
50
  </p>
51
- <Field label={t("auth.email")} error={errors.email?.message}>
51
+ <Field label={t("auth.email")} error={errors.email?.message} required>
52
52
  <Input type="email" autoFocus {...register("email")} />
53
53
  </Field>
54
54
  <Button type="submit" size="lg" disabled={loading}>
@@ -159,6 +159,7 @@ export function ResetPasswordScreen(): JSX.Element {
159
159
  <Field
160
160
  label={t("recovery.newPassword")}
161
161
  error={errors.newPassword?.message}
162
+ required
162
163
  >
163
164
  <PasswordField
164
165
  error={!!errors.newPassword}
@@ -169,6 +170,7 @@ export function ResetPasswordScreen(): JSX.Element {
169
170
  <Field
170
171
  label={t("recovery.confirmPassword")}
171
172
  error={errors.confirm?.message}
173
+ required
172
174
  >
173
175
  <PasswordField
174
176
  error={!!errors.confirm}
@@ -135,7 +135,7 @@ export function UserFormDialog({
135
135
 
136
136
  <DialogForm onSubmit={submit}>
137
137
  <div className="flex max-h-[70vh] flex-col gap-4 overflow-y-auto px-1">
138
- <Field label={t("users.name")} error={errors.name?.message}>
138
+ <Field label={t("users.name")} error={errors.name?.message} required>
139
139
  <Input {...register("name")} />
140
140
  </Field>
141
141
  <Field label={t("users.lastName")} error={errors.lastName?.message}>
@@ -154,7 +154,7 @@ export function UserFormDialog({
154
154
  </Field>
155
155
  )}
156
156
  />
157
- <Field label={t("users.email")} error={errors.email?.message}>
157
+ <Field label={t("users.email")} error={errors.email?.message} required>
158
158
  <Input type="email" {...register("email")} />
159
159
  </Field>
160
160
  {!isEdit && (
@@ -169,6 +169,7 @@ export function UserFormDialog({
169
169
  <Field
170
170
  label={t("users.team")}
171
171
  error={errors.groupId?.message}
172
+ required
172
173
  >
173
174
  <SimpleSelect
174
175
  value={field.value ?? ""}
@@ -180,6 +180,54 @@
180
180
  letter-spacing: -0.02em;
181
181
  }
182
182
 
183
+ /*
184
+ * Barra de rolagem — uma só, em todo lugar.
185
+ *
186
+ * A nativa é grossa (15px no macOS/Windows), tem trilho pintado e ignora o
187
+ * raio do que está rolando: dentro de um menu de 18rem, ela ocupa um décimo
188
+ * da largura e corta o canto arredondado. Aqui ela vira uma pílula fina sem
189
+ * trilho, na cor do texto secundário — presente enquanto se rola, discreta
190
+ * quando parada.
191
+ *
192
+ * As duas declarações não se somam: `scrollbar-width`/`scrollbar-color` são
193
+ * o padrão (Firefox, Chrome 121+, Safari 18.4+) e, onde valem, o browser
194
+ * ignora os pseudo-elementos `::-webkit-scrollbar` abaixo — que ficam para
195
+ * o WebKit mais antigo. Manter as duas é o que faz a barra ser igual nos
196
+ * dois lados.
197
+ *
198
+ * A cor sai de `--muted-foreground`, então ela acompanha o tema sozinha: no
199
+ * escuro clareia, no claro escurece, sem uma segunda regra em `.dark`.
200
+ */
201
+ * {
202
+ scrollbar-width: thin;
203
+ scrollbar-color: hsl(var(--muted-foreground) / 0.35) transparent;
204
+ }
205
+
206
+ ::-webkit-scrollbar {
207
+ width: 8px;
208
+ height: 8px;
209
+ }
210
+ ::-webkit-scrollbar-track {
211
+ background: transparent;
212
+ }
213
+ /*
214
+ * A borda transparente com `background-clip: content-box` é o que desenha a
215
+ * folga dos dois lados da pílula: sem ela a barra encosta na borda do menu.
216
+ */
217
+ ::-webkit-scrollbar-thumb {
218
+ background-color: hsl(var(--muted-foreground) / 0.35);
219
+ border: 2px solid transparent;
220
+ background-clip: content-box;
221
+ border-radius: 9999px;
222
+ }
223
+ ::-webkit-scrollbar-thumb:hover {
224
+ background-color: hsl(var(--muted-foreground) / 0.55);
225
+ }
226
+ /* O quadradinho onde as duas barras se encontram, que fica cinza por padrão. */
227
+ ::-webkit-scrollbar-corner {
228
+ background: transparent;
229
+ }
230
+
183
231
  /*
184
232
  * Preenchimento automático (gerenciador de senhas). O `background-color` do
185
233
  * campo preenchido é intocável por CSS de autor — o Chrome o declara com