rl-core-front 0.15.4 → 0.15.6

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.15.4",
3
+ "version": "0.15.6",
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",
@@ -66,16 +66,19 @@ export const today = (): Date => {
66
66
  };
67
67
 
68
68
  /**
69
- * Grade do mês: 6 semanas × 7 dias, começando na segunda-feira.
69
+ * Grade do mês: 6 semanas × 7 dias, começando no domingo.
70
+ *
71
+ * Domingo na primeira coluna porque é como o calendário é lido aqui — o de
72
+ * parede, o do celular e o do banco começam nele, e uma grade que começa na
73
+ * segunda faz quem procura o dia 12 contar coluna por coluna.
70
74
  *
71
75
  * Seis semanas fixas de propósito — com 5 ou 6 conforme o mês, o painel muda de
72
76
  * altura ao navegar e o calendário "pula" na tela.
73
77
  */
74
78
  export const monthGrid = (month: Date): Date[] => {
75
79
  const first = dayOf(month.getFullYear(), month.getMonth(), 1);
76
- // getDay(): 0 = domingo. Queremos segunda como primeira coluna.
77
- const offset = (first.getDay() + 6) % 7;
78
- const start = addDays(first, -offset);
80
+ // getDay(): 0 = domingo, que é a primeira coluna.
81
+ const start = addDays(first, -first.getDay());
79
82
  return Array.from({ length: 42 }, (_, index) => addDays(start, index));
80
83
  };
81
84
 
@@ -85,12 +88,12 @@ export const monthLabel = (month: Date, locale: string): string =>
85
88
  month,
86
89
  );
87
90
 
88
- /** Iniciais dos dias da semana, de segunda a domingo, no locale. */
91
+ /** Iniciais dos dias da semana, de domingo a sábado, no locale. */
89
92
  export const weekdayLabels = (locale: string): string[] => {
90
93
  const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
91
- // 2026-06-01 é uma segunda-feira — âncora só para varrer a semana.
94
+ // Dia 0 de junho é 31/05/2026, um domingo — âncora só para varrer a semana.
92
95
  return Array.from({ length: 7 }, (_, index) =>
93
- formatter.format(dayOf(2026, 5, 1 + index)).replace(".", ""),
96
+ formatter.format(dayOf(2026, 5, index)).replace(".", ""),
94
97
  );
95
98
  };
96
99
 
@@ -101,3 +104,204 @@ export const formatDay = (value: string, locale: string): string => {
101
104
  ? new Intl.DateTimeFormat(locale, { dateStyle: "short" }).format(date)
102
105
  : value;
103
106
  };
107
+
108
+ /**
109
+ * A ordem em que o locale escreve dia, mês e ano.
110
+ *
111
+ * Sai do próprio `Intl` e não de uma tabela: `pt-BR` escreve 11/08/2026 e
112
+ * `en-US`, 08/11/2026 — a mesma sequência de dígitos significa dias
113
+ * diferentes, e é ela que o campo digitado precisa respeitar.
114
+ */
115
+ export const dayFieldOrder = (locale: string): ("day" | "month" | "year")[] =>
116
+ new Intl.DateTimeFormat(locale, { dateStyle: "short" })
117
+ .formatToParts(dayOf(2026, 7, 11))
118
+ .map((part) => part.type)
119
+ .filter((type): type is "day" | "month" | "year" =>
120
+ type === "day" || type === "month" || type === "year",
121
+ );
122
+
123
+ /** `dd/mm/aaaa` no locale — o que o campo mostra enquanto está vazio. */
124
+ export const dayPlaceholder = (locale: string): string => {
125
+ const letra = { day: "dd", month: "mm", year: locale.startsWith("pt") ? "aaaa" : "yyyy" };
126
+
127
+ return dayFieldOrder(locale)
128
+ .map((field) => letra[field])
129
+ .join("/");
130
+ };
131
+
132
+ /** Quantos dígitos cabem em cada campo da data, e o maior valor de cada um. */
133
+ const DAY_FIELD_SIZES: Record<"day" | "month" | "year", number> = {
134
+ day: 2,
135
+ month: 2,
136
+ year: 4,
137
+ };
138
+ const DAY_FIELD_MAX: Record<"day" | "month" | "year", number> = {
139
+ day: 31,
140
+ month: 12,
141
+ // O ano não tem teto que ajude enquanto se digita: 2 é o começo de 2026.
142
+ year: Infinity,
143
+ };
144
+
145
+ /** O campo formado e quantos dígitos ele consumiu — o resto vai para o próximo. */
146
+ interface FittedField {
147
+ value: string;
148
+ used: number;
149
+ }
150
+
151
+ /**
152
+ * O campo enquanto ele é digitado, dentro do que existe no calendário.
153
+ *
154
+ * Três regras, todas para o campo nunca mostrar o que não existe:
155
+ *
156
+ * - dígito que não pode **começar** valor nenhum fecha o campo com zero na
157
+ * frente — no mês, `3` só poderia virar 30 a 39, então já vira `03`;
158
+ * - dígito que estoura o par não é engolido, transborda: `3` e `2` no dia
159
+ * viram `3/2`, que é o que quem digitou `3` e `2` queria dizer;
160
+ * - `00` não é dia nem mês.
161
+ *
162
+ * O zero na frente só entra quando se está **digitando**. Apagando, ele
163
+ * devolveria na hora o dígito recém-tirado, e o campo ficaria impossível de
164
+ * corrigir.
165
+ */
166
+ const fitDayField = (
167
+ digits: string,
168
+ max: number,
169
+ size: number,
170
+ autoZero: boolean,
171
+ ): FittedField => {
172
+ const primeiro = digits.slice(0, 1);
173
+
174
+ if (autoZero && primeiro !== "" && Number(primeiro) * 10 > max) {
175
+ return { value: `0${primeiro}`, used: 1 };
176
+ }
177
+
178
+ const cheio = digits.slice(0, size);
179
+
180
+ // `00` não é dia nem mês, e o segundo zero não serve para o campo seguinte:
181
+ // some, em vez de virar um mês zero logo ali.
182
+ if (cheio.length === size && Number(cheio) === 0) {
183
+ return { value: cheio.slice(0, size - 1), used: size };
184
+ }
185
+
186
+ if (cheio.length === size && Number(cheio) > max) {
187
+ return { value: cheio.slice(0, size - 1), used: size - 1 };
188
+ }
189
+
190
+ return { value: cheio, used: cheio.length };
191
+ };
192
+
193
+ /**
194
+ * Põe as barras enquanto se digita: `1108` vira `11/08`.
195
+ *
196
+ * Campo a campo, e não recontando os dígitos do começo: apagar o `3` de
197
+ * `31/08/2026` deixa `1/08/2026`, e não `10/82/026` — o que sobra de um campo
198
+ * cheio transborda para o seguinte, mas nada é puxado de volta.
199
+ *
200
+ * O locale entra porque é ele que diz qual campo é o dia e qual é o mês, e os
201
+ * dois têm tetos diferentes: `13` é dia e não é mês. O texto anterior entra
202
+ * para separar quem digita de quem apaga — ver `fitDayField`.
203
+ */
204
+ export const maskDay = (raw: string, locale: string, previous = ""): string => {
205
+ const order = dayFieldOrder(locale);
206
+ const partes = raw.replace(/[^\d/]/g, "").split("/").slice(0, order.length);
207
+ const apagando = raw.length < previous.length;
208
+ const saida: string[] = [];
209
+ let resto = "";
210
+
211
+ for (let posicao = 0; posicao < order.length; posicao += 1) {
212
+ const campo = order[posicao];
213
+ const tamanho = DAY_FIELD_SIZES[campo];
214
+ const digits = `${resto}${partes[posicao] ?? ""}`.replace(/\D/g, "");
215
+ const { value, used } = fitDayField(
216
+ digits,
217
+ DAY_FIELD_MAX[campo],
218
+ tamanho,
219
+ !apagando,
220
+ );
221
+
222
+ saida.push(value);
223
+
224
+ /*
225
+ O que sobra só empurra os campos seguintes quando eles ainda estão
226
+ vazios.
227
+
228
+ Digitando do zero, empurrar é o que faz `1108` virar `11/08`. Mas
229
+ corrigindo o dia de uma data já escrita, o dígito recusado empurrava o
230
+ resto inteiro: trocar o `31` de `31/02/2056` por `32` devolvia
231
+ `3/02/0220`, com mês e ano remontados a partir do que sobrou. Aqui ele
232
+ simplesmente não entra — a data continua a que estava, com o campo em
233
+ correção mais curto.
234
+
235
+ A exceção é o excedente de tamanho: quem digitou mais dígitos do que o
236
+ campo comporta (colando, ou apagando uma barra) quis mesmo dizer o campo
237
+ seguinte.
238
+ */
239
+ const temConteudoDepois = partes
240
+ .slice(posicao + 1)
241
+ .some((parte) => parte !== "");
242
+ const sobraPorTamanho = digits.length > tamanho;
243
+
244
+ resto = temConteudoDepois && !sobraPorTamanho ? "" : digits.slice(used);
245
+
246
+ // Sem transbordo e sem mais nada digitado, a data acaba aqui: acrescentar
247
+ // um campo vazio escreveria a barra que ninguém pediu.
248
+ if (resto === "" && partes.length <= posicao + 1) {
249
+ break;
250
+ }
251
+ }
252
+
253
+ return saida.join("/");
254
+ };
255
+
256
+ /**
257
+ * O que foi digitado vira `"yyyy-MM-dd"`, ou `null` enquanto não for uma data.
258
+ *
259
+ * Lê os campos separados pelas barras, na ordem do locale: `1/8/2026` é uma
260
+ * data tão boa quanto `01/08/2026`, e exigir o zero à esquerda faria o campo
261
+ * recusar o que ele mesmo acabou de mostrar.
262
+ *
263
+ * O ano pode faltar: `1108` é 11 de agosto do ano de referência — quase todo
264
+ * lançamento é do ano corrente, e obrigar a digitar `2026` toda vez é o tipo
265
+ * de exigência que faz o campo ser abandonado pelo calendário. Ano começado
266
+ * (`202`) ainda não é ano, e não vale como digitado.
267
+ *
268
+ * A conferência é de existência, não de formato: `31/02` casa com a máscara e
269
+ * não existe, e o `Date` do JavaScript o transformaria em 03/03 em silêncio.
270
+ */
271
+ export const parseTypedDay = (
272
+ text: string,
273
+ locale: string,
274
+ fallbackYear: number,
275
+ ): string | null => {
276
+ const campos = maskDay(text, locale).split("/");
277
+
278
+ if (campos.length < 2) {
279
+ return null;
280
+ }
281
+
282
+ const order = dayFieldOrder(locale);
283
+ const campoDe = (field: "day" | "month" | "year"): string =>
284
+ campos[order.indexOf(field)] ?? "";
285
+
286
+ const anoDigitado = campoDe("year");
287
+
288
+ if (anoDigitado !== "" && anoDigitado.length < 4) {
289
+ return null;
290
+ }
291
+
292
+ const day = Number(campoDe("day"));
293
+ const month = Number(campoDe("month"));
294
+ const year = anoDigitado === "" ? fallbackYear : Number(anoDigitado);
295
+
296
+ if (!day || !month) {
297
+ return null;
298
+ }
299
+
300
+ const date = dayOf(year, month - 1, day);
301
+ const existe =
302
+ date.getFullYear() === year &&
303
+ date.getMonth() === month - 1 &&
304
+ date.getDate() === day;
305
+
306
+ return existe ? toIsoDay(date) : null;
307
+ };