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 +1 -1
- package/src/_utils/calendar.ts +211 -7
- package/src/components/ui/data-table.tsx +632 -247
- package/src/components/ui/date-picker.tsx +84 -10
- package/src/components/ui/dialog.tsx +81 -1
- package/src/components/ui/table.tsx +30 -1
- package/src/hooks/use-column-visibility.ts +6 -1
- package/src/hooks/use-table-view.ts +54 -0
- package/src/i18n/messages/en.ts +5 -0
- package/src/i18n/messages/pt.ts +5 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
package/src/_utils/calendar.ts
CHANGED
|
@@ -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
|
|
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
|
|
77
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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,
|
|
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
|
+
};
|