rl-core-front 0.15.2 → 0.15.5
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
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
|
+
};
|
|
@@ -79,6 +79,14 @@ interface DataTableProps<T extends RowData> {
|
|
|
79
79
|
loading?: boolean;
|
|
80
80
|
/** Ações à esquerda da toolbar (ex.: botão "Novo"). */
|
|
81
81
|
toolbar?: React.ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* Ações à direita, antes de "Filtros" e "Colunas".
|
|
84
|
+
*
|
|
85
|
+
* É onde mora o que age sobre **a listagem inteira** — exportar, por
|
|
86
|
+
* exemplo. À esquerda ficam as ações de criar; misturar as duas coisas na
|
|
87
|
+
* mesma ponta faz o botão de exportar parecer mais um "novo".
|
|
88
|
+
*/
|
|
89
|
+
toolbarEnd?: React.ReactNode;
|
|
82
90
|
pageSizeOptions?: number[];
|
|
83
91
|
/** Paginação no servidor: informe rowCount, pagination e onPaginationChange. */
|
|
84
92
|
manualPagination?: boolean;
|
|
@@ -115,6 +123,18 @@ interface DataTableProps<T extends RowData> {
|
|
|
115
123
|
* indistinguível de bug, e manda o usuário procurar defeito onde não há.
|
|
116
124
|
*/
|
|
117
125
|
emptyMessage?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Esconde o rodapé de contagem, paginação e "por página".
|
|
128
|
+
*
|
|
129
|
+
* Para a listagem que já vem inteira numa resposta — um cadastro de vinte
|
|
130
|
+
* linhas não precisa de página. Sem o rodapé a tabela mostra **todas** as
|
|
131
|
+
* linhas, senão esconder a navegação esconderia registros.
|
|
132
|
+
*
|
|
133
|
+
* Ignorado quando a paginação é do servidor (`manualPagination`): ali a
|
|
134
|
+
* tabela só tem em mãos a página atual, e tirar o rodapé faria as outras
|
|
135
|
+
* sumirem sem aviso.
|
|
136
|
+
*/
|
|
137
|
+
hideFooter?: boolean;
|
|
118
138
|
/**
|
|
119
139
|
* Arrastar a linha pela alça para reordenar a listagem.
|
|
120
140
|
*
|
|
@@ -172,6 +192,7 @@ export function DataTable<T extends RowData>({
|
|
|
172
192
|
storageKey,
|
|
173
193
|
loading,
|
|
174
194
|
toolbar,
|
|
195
|
+
toolbarEnd,
|
|
175
196
|
pageSizeOptions = [10, 20, 50],
|
|
176
197
|
manualPagination,
|
|
177
198
|
rowCount,
|
|
@@ -184,6 +205,7 @@ export function DataTable<T extends RowData>({
|
|
|
184
205
|
filters,
|
|
185
206
|
hiddenFilterFields,
|
|
186
207
|
emptyMessage,
|
|
208
|
+
hideFooter = false,
|
|
187
209
|
onRowReorder,
|
|
188
210
|
}: DataTableProps<T>): JSX.Element {
|
|
189
211
|
const { t } = useI18n();
|
|
@@ -202,6 +224,14 @@ export function DataTable<T extends RowData>({
|
|
|
202
224
|
*/
|
|
203
225
|
const [dragOffset, setDragOffset] = useState(0);
|
|
204
226
|
const dragStartY = useRef(0);
|
|
227
|
+
/*
|
|
228
|
+
A altura da linha na mão, medida no momento em que ela é pega.
|
|
229
|
+
|
|
230
|
+
É o tanto que as linhas do caminho se afastam para abrir o buraco onde ela
|
|
231
|
+
vai cair. Medida e não fixa: a altura de uma linha depende do conteúdo, e um
|
|
232
|
+
número chutado abriria um vão maior ou menor que a linha que vem.
|
|
233
|
+
*/
|
|
234
|
+
const [dragHeight, setDragHeight] = useState(0);
|
|
205
235
|
|
|
206
236
|
const { hiddenColumns, saveHiddenColumns } = useColumnVisibility(storageKey);
|
|
207
237
|
const columnVisibility = useMemo<ColumnVisibilityState>(() => {
|
|
@@ -220,6 +250,16 @@ export function DataTable<T extends RowData>({
|
|
|
220
250
|
},
|
|
221
251
|
);
|
|
222
252
|
|
|
253
|
+
/*
|
|
254
|
+
Rodapé escondido é lista inteira na tela: sem a navegação, um `pageSize` de
|
|
255
|
+
10 esconderia da décima primeira linha em diante sem nada dizer. Não vale
|
|
256
|
+
para a paginação do servidor, onde a tabela só tem a página atual em mãos.
|
|
257
|
+
*/
|
|
258
|
+
const semRodape = hideFooter && !manualPagination;
|
|
259
|
+
const paginacaoLocal = semRodape
|
|
260
|
+
? { pageIndex: 0, pageSize: Math.max(data.length, 1) }
|
|
261
|
+
: internalPagination;
|
|
262
|
+
|
|
223
263
|
const table = useTable({
|
|
224
264
|
features: dataTableFeatures,
|
|
225
265
|
data,
|
|
@@ -228,7 +268,7 @@ export function DataTable<T extends RowData>({
|
|
|
228
268
|
state: {
|
|
229
269
|
columnVisibility,
|
|
230
270
|
sorting: manualSorting ? sorting : internalSorting,
|
|
231
|
-
pagination: manualPagination ? pagination :
|
|
271
|
+
pagination: manualPagination ? pagination : paginacaoLocal,
|
|
232
272
|
},
|
|
233
273
|
onColumnVisibilityChange: (updater) => {
|
|
234
274
|
const next =
|
|
@@ -282,6 +322,9 @@ export function DataTable<T extends RowData>({
|
|
|
282
322
|
// A captura mantém os eventos na alça mesmo com o dedo já longe dela;
|
|
283
323
|
// sem ela, o arrasto morre no primeiro pixel fora do botão.
|
|
284
324
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
325
|
+
setDragHeight(
|
|
326
|
+
event.currentTarget.closest("tr")?.getBoundingClientRect().height ?? 0,
|
|
327
|
+
);
|
|
285
328
|
dragStartY.current = event.clientY;
|
|
286
329
|
setDragOffset(0);
|
|
287
330
|
setDraggedRow(rowId);
|
|
@@ -289,35 +332,36 @@ export function DataTable<T extends RowData>({
|
|
|
289
332
|
};
|
|
290
333
|
|
|
291
334
|
/**
|
|
292
|
-
*
|
|
335
|
+
* Onde a linha está agora: quantas linhas ela já subiu ou desceu.
|
|
293
336
|
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
337
|
+
* Conta pelo deslocamento, e não por quem está embaixo do ponteiro. Quem
|
|
338
|
+
* está embaixo do ponteiro é a **própria linha arrastada** — ela viaja com o
|
|
339
|
+
* dedo —, e perguntar ao ponto devolvia origem e destino iguais: o arrasto
|
|
340
|
+
* terminava sem gravar nada. Além disso as linhas paradas se movem para
|
|
341
|
+
* abrir o vão, então o que o ponto responde muda durante o gesto e a
|
|
342
|
+
* escolha do destino ficaria oscilando entre duas linhas.
|
|
297
343
|
*/
|
|
298
344
|
const moveRowDrag = (event: ReactPointerEvent<HTMLButtonElement>): void => {
|
|
299
|
-
if (!draggedRow) {
|
|
345
|
+
if (!draggedRow || dragHeight === 0) {
|
|
300
346
|
return;
|
|
301
347
|
}
|
|
302
348
|
|
|
303
|
-
|
|
349
|
+
const offset = event.clientY - dragStartY.current;
|
|
350
|
+
setDragOffset(offset);
|
|
304
351
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
352
|
+
const rows = table.getRowModel().rows;
|
|
353
|
+
const from = rows.findIndex((row) => row.id === draggedRow);
|
|
354
|
+
// Meia linha de deslocamento já troca de lugar: é o ponto em que a linha
|
|
355
|
+
// arrastada cobre mais a vizinha do que o próprio lugar de origem.
|
|
356
|
+
const destino = Math.min(
|
|
357
|
+
Math.max(from + Math.round(offset / dragHeight), 0),
|
|
358
|
+
rows.length - 1,
|
|
359
|
+
);
|
|
309
360
|
|
|
310
|
-
|
|
311
|
-
setTargetRow(id);
|
|
312
|
-
}
|
|
361
|
+
setTargetRow(rows[destino]?.id ?? draggedRow);
|
|
313
362
|
};
|
|
314
363
|
|
|
315
|
-
/**
|
|
316
|
-
* Onde a linha saiu e onde ela está agora, na página desenhada.
|
|
317
|
-
*
|
|
318
|
-
* Os dois só para saber de que lado a guia de destino aparece — descendo,
|
|
319
|
-
* embaixo do alvo; subindo, em cima dele.
|
|
320
|
-
*/
|
|
364
|
+
/** Onde a linha saiu e onde ela está agora, na página desenhada. */
|
|
321
365
|
const draggedIndex = table
|
|
322
366
|
.getRowModel()
|
|
323
367
|
.rows.findIndex((row) => row.id === draggedRow);
|
|
@@ -325,6 +369,30 @@ export function DataTable<T extends RowData>({
|
|
|
325
369
|
.getRowModel()
|
|
326
370
|
.rows.findIndex((row) => row.id === targetRow);
|
|
327
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Quanto uma linha parada se desloca para abrir o buraco.
|
|
374
|
+
*
|
|
375
|
+
* As linhas entre a origem e o destino sobem (ou descem) a altura de uma
|
|
376
|
+
* linha, e o vão aparece exatamente onde a linha vai cair. É isso que
|
|
377
|
+
* responde "para onde isto está indo" durante o gesto — um traço de destino
|
|
378
|
+
* diz o mesmo em teoria, mas exige ler a tabela para achá-lo.
|
|
379
|
+
*/
|
|
380
|
+
const rowShift = (index: number): number => {
|
|
381
|
+
if (draggedRow === null || draggedIndex < 0 || targetIndex < 0) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (index > draggedIndex && index <= targetIndex) {
|
|
386
|
+
return -dragHeight;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (index >= targetIndex && index < draggedIndex) {
|
|
390
|
+
return dragHeight;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return 0;
|
|
394
|
+
};
|
|
395
|
+
|
|
328
396
|
/** Soltou: avisa quem chamou e devolve a tabela ao estado parado. */
|
|
329
397
|
const endRowDrag = (): void => {
|
|
330
398
|
if (draggedRow && targetRow && draggedRow !== targetRow) {
|
|
@@ -368,6 +436,7 @@ export function DataTable<T extends RowData>({
|
|
|
368
436
|
{toolbar}
|
|
369
437
|
</div>
|
|
370
438
|
<div className="flex flex-wrap items-center gap-2">
|
|
439
|
+
{toolbarEnd}
|
|
371
440
|
{/* Filtro antes de colunas: filtrar é a ação do dia a dia, escolher
|
|
372
441
|
coluna se faz uma vez e a tela guarda. */}
|
|
373
442
|
{filtersEnabled && (
|
|
@@ -467,17 +536,9 @@ export function DataTable<T extends RowData>({
|
|
|
467
536
|
</TableRow>
|
|
468
537
|
)}
|
|
469
538
|
{!loading &&
|
|
470
|
-
table.getRowModel().rows.map((row) => {
|
|
539
|
+
table.getRowModel().rows.map((row, index) => {
|
|
471
540
|
const dragging = draggedRow === row.id;
|
|
472
|
-
|
|
473
|
-
// embaixo do alvo; subindo, em cima dele. É ela que responde
|
|
474
|
-
// "onde isto vai cair" antes de soltar.
|
|
475
|
-
const guide =
|
|
476
|
-
draggedRow !== null && targetRow === row.id && !dragging
|
|
477
|
-
? targetIndex > draggedIndex
|
|
478
|
-
? "border-b-2 border-b-primary"
|
|
479
|
-
: "border-t-2 border-t-primary"
|
|
480
|
-
: undefined;
|
|
541
|
+
const shift = dragging ? 0 : rowShift(index);
|
|
481
542
|
|
|
482
543
|
return (
|
|
483
544
|
<TableRow
|
|
@@ -487,14 +548,30 @@ export function DataTable<T extends RowData>({
|
|
|
487
548
|
// A linha viaja com o dedo, destacada do resto da tabela —
|
|
488
549
|
// é o que diz que o arrasto pegou, e qual linha pegou.
|
|
489
550
|
dragging &&
|
|
490
|
-
"relative z-20 bg-card shadow-
|
|
551
|
+
"relative z-20 bg-card shadow-xl ring-1 ring-primary/50",
|
|
491
552
|
draggedRow !== null && "select-none",
|
|
492
|
-
|
|
553
|
+
/*
|
|
554
|
+
A transição existe só enquanto o arrasto dura, e só nas
|
|
555
|
+
linhas paradas: na que segue o dedo ela viraria atraso,
|
|
556
|
+
e depois de soltar ela animaria a volta ao zero por
|
|
557
|
+
cima da lista já reordenada — dois movimentos ao mesmo
|
|
558
|
+
tempo, que é o tremor que se vê ao largar a linha.
|
|
559
|
+
*/
|
|
560
|
+
draggedRow !== null &&
|
|
561
|
+
!dragging &&
|
|
562
|
+
"transition-transform duration-200",
|
|
493
563
|
)}
|
|
494
564
|
style={
|
|
495
565
|
dragging
|
|
496
|
-
? {
|
|
497
|
-
|
|
566
|
+
? {
|
|
567
|
+
// O `scale` é o "levantou da mesa": a linha na mão
|
|
568
|
+
// fica maior que as paradas, como o card que sai
|
|
569
|
+
// da coluna no quadro.
|
|
570
|
+
transform: `translateY(${dragOffset}px) scale(1.01)`,
|
|
571
|
+
}
|
|
572
|
+
: shift !== 0
|
|
573
|
+
? { transform: `translateY(${shift}px)` }
|
|
574
|
+
: undefined
|
|
498
575
|
}
|
|
499
576
|
>
|
|
500
577
|
{reorderable && (
|
|
@@ -550,73 +627,75 @@ export function DataTable<T extends RowData>({
|
|
|
550
627
|
uma vez fica na ponta, e a navegação, que se usa o tempo todo, no
|
|
551
628
|
centro. Em tela estreita eles empilham em vez de espremer.
|
|
552
629
|
*/}
|
|
553
|
-
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
item
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
<Button
|
|
596
|
-
variant="ghost"
|
|
597
|
-
size="icon"
|
|
598
|
-
aria-label={t("table.nextPage")}
|
|
599
|
-
disabled={!table.getCanNextPage()}
|
|
600
|
-
onClick={() => table.nextPage()}
|
|
601
|
-
>
|
|
602
|
-
<ChevronRight className="h-4 w-4" />
|
|
603
|
-
</Button>
|
|
604
|
-
</div>
|
|
630
|
+
{!semRodape && (
|
|
631
|
+
<div className="flex flex-col items-center gap-3 border-t border-border px-4 py-3 text-sm text-muted-foreground sm:flex-row sm:justify-between">
|
|
632
|
+
<span className="sm:flex-1">
|
|
633
|
+
{total} {t("table.results")}
|
|
634
|
+
</span>
|
|
635
|
+
|
|
636
|
+
<div className="flex items-center gap-1">
|
|
637
|
+
<Button
|
|
638
|
+
variant="ghost"
|
|
639
|
+
size="icon"
|
|
640
|
+
aria-label={t("table.previousPage")}
|
|
641
|
+
disabled={!table.getCanPreviousPage()}
|
|
642
|
+
onClick={() => table.previousPage()}
|
|
643
|
+
>
|
|
644
|
+
<ChevronLeft className="h-4 w-4" />
|
|
645
|
+
</Button>
|
|
646
|
+
|
|
647
|
+
{pageRange(pageIndex, pageCount).map((item, posicao) =>
|
|
648
|
+
item === "gap" ? (
|
|
649
|
+
// O buraco não é botão: `key` pela posição porque duas elipses
|
|
650
|
+
// são indistinguíveis entre si.
|
|
651
|
+
<span
|
|
652
|
+
key={`gap-${posicao}`}
|
|
653
|
+
className="px-1 text-muted-foreground"
|
|
654
|
+
>
|
|
655
|
+
…
|
|
656
|
+
</span>
|
|
657
|
+
) : (
|
|
658
|
+
<Button
|
|
659
|
+
key={item}
|
|
660
|
+
variant={item === pageIndex + 1 ? "default" : "ghost"}
|
|
661
|
+
size="icon"
|
|
662
|
+
// Quadrado de 40px como base, mas cresce com o número: a
|
|
663
|
+
// página 128 não pode vazar do botão.
|
|
664
|
+
className="w-auto min-w-10 px-2 tabular-nums"
|
|
665
|
+
aria-current={item === pageIndex + 1 ? "page" : undefined}
|
|
666
|
+
onClick={() => table.setPageIndex(item - 1)}
|
|
667
|
+
>
|
|
668
|
+
{item}
|
|
669
|
+
</Button>
|
|
670
|
+
),
|
|
671
|
+
)}
|
|
605
672
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
<
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
673
|
+
<Button
|
|
674
|
+
variant="ghost"
|
|
675
|
+
size="icon"
|
|
676
|
+
aria-label={t("table.nextPage")}
|
|
677
|
+
disabled={!table.getCanNextPage()}
|
|
678
|
+
onClick={() => table.nextPage()}
|
|
679
|
+
>
|
|
680
|
+
<ChevronRight className="h-4 w-4" />
|
|
681
|
+
</Button>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<div className="sm:flex-1 sm:text-right">
|
|
685
|
+
<select
|
|
686
|
+
value={pageSize}
|
|
687
|
+
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
|
688
|
+
className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
689
|
+
>
|
|
690
|
+
{pageSizeOptions.map((n) => (
|
|
691
|
+
<option key={n} value={n}>
|
|
692
|
+
{n} {t("table.perPage")}
|
|
693
|
+
</option>
|
|
694
|
+
))}
|
|
695
|
+
</select>
|
|
696
|
+
</div>
|
|
618
697
|
</div>
|
|
619
|
-
|
|
698
|
+
)}
|
|
620
699
|
</Card>
|
|
621
700
|
|
|
622
701
|
{storageKey && (
|
|
@@ -6,12 +6,15 @@ import { useState } from "react";
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
addMonths,
|
|
9
|
+
dayPlaceholder,
|
|
9
10
|
formatDay,
|
|
10
11
|
fromIsoDay,
|
|
11
12
|
isSameDay,
|
|
12
13
|
isSameMonth,
|
|
14
|
+
maskDay,
|
|
13
15
|
monthGrid,
|
|
14
16
|
monthLabel,
|
|
17
|
+
parseTypedDay,
|
|
15
18
|
today,
|
|
16
19
|
toIsoDay,
|
|
17
20
|
weekdayLabels,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
import { Button } from "#core/components/ui/button";
|
|
20
23
|
import {
|
|
21
24
|
Popover,
|
|
25
|
+
PopoverAnchor,
|
|
22
26
|
PopoverContent,
|
|
23
27
|
PopoverTrigger,
|
|
24
28
|
} from "#core/components/ui/popover";
|
|
@@ -46,6 +50,10 @@ export interface DatePickerProps {
|
|
|
46
50
|
* O valor continua sendo a string ISO do dia, e não `Date`: é o que o backend
|
|
47
51
|
* grava (`char(10)`) e o que evita a viagem por fuso que transforma dia 1º às
|
|
48
52
|
* 00:00 locais no dia 30 em UTC.
|
|
53
|
+
*
|
|
54
|
+
* O campo é **digitável**: `1108` já é 11 de agosto, e quem sabe a data não
|
|
55
|
+
* precisa navegar meses no calendário para dizê-la. O calendário continua ali,
|
|
56
|
+
* no ícone — é o caminho de quem procura o dia, não de quem já o conhece.
|
|
49
57
|
*/
|
|
50
58
|
export function DatePicker({
|
|
51
59
|
value,
|
|
@@ -56,15 +64,55 @@ export function DatePicker({
|
|
|
56
64
|
}: DatePickerProps): JSX.Element {
|
|
57
65
|
const { t, locale } = useI18n();
|
|
58
66
|
const [open, setOpen] = useState(false);
|
|
67
|
+
/*
|
|
68
|
+
O que está sendo digitado, enquanto ainda não é uma data.
|
|
69
|
+
|
|
70
|
+
Sem este rascunho o campo teria de apagar cada tecla que não fecha um dia
|
|
71
|
+
válido — digitar `1` já voltaria para o valor gravado, e não haveria como
|
|
72
|
+
chegar ao `11`.
|
|
73
|
+
*/
|
|
74
|
+
const [typed, setTyped] = useState<string | null>(null);
|
|
59
75
|
|
|
60
76
|
const selected = fromIsoDay(value);
|
|
61
77
|
const [month, setMonth] = useState<Date>(() => selected ?? today());
|
|
62
78
|
|
|
63
79
|
const pickDay = (day: Date): void => {
|
|
64
80
|
onChange(toIsoDay(day));
|
|
81
|
+
setTyped(null);
|
|
65
82
|
setOpen(false);
|
|
66
83
|
};
|
|
67
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Cada tecla: a máscara põe as barras, e a data sai assim que existir.
|
|
87
|
+
*
|
|
88
|
+
* O ano em branco é o do dia que está no campo — ou o de hoje, quando não há
|
|
89
|
+
* nenhum. Campo esvaziado limpa o valor: apagar é uma escolha, não um
|
|
90
|
+
* rascunho pela metade.
|
|
91
|
+
*/
|
|
92
|
+
const digitar = (raw: string): void => {
|
|
93
|
+
// O texto que estava lá antes: é ele que diz se a tecla foi um dígito novo
|
|
94
|
+
// ou um apagar — e o campo se comporta diferente nos dois casos.
|
|
95
|
+
const texto = maskDay(raw, locale, typed ?? (value ? formatDay(value, locale) : ""));
|
|
96
|
+
setTyped(texto);
|
|
97
|
+
|
|
98
|
+
if (texto === "") {
|
|
99
|
+
onChange(undefined);
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const iso = parseTypedDay(
|
|
105
|
+
texto,
|
|
106
|
+
locale,
|
|
107
|
+
(selected ?? today()).getFullYear(),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (iso) {
|
|
111
|
+
onChange(iso);
|
|
112
|
+
setMonth(fromIsoDay(iso) ?? month);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
68
116
|
const dayClass = (day: Date): string => {
|
|
69
117
|
if (selected && isSameDay(day, selected)) {
|
|
70
118
|
return "bg-primary text-primary-foreground font-semibold";
|
|
@@ -77,16 +125,41 @@ export function DatePicker({
|
|
|
77
125
|
|
|
78
126
|
return (
|
|
79
127
|
<Popover open={open} onOpenChange={setOpen}>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
>
|
|
85
|
-
<
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
{/*
|
|
129
|
+
A âncora é a linha inteira, e não o ícone: presa ao botão, o painel
|
|
130
|
+
abriria alinhado a ele e com a largura dele — um calendário de 16px.
|
|
131
|
+
*/}
|
|
132
|
+
<PopoverAnchor asChild>
|
|
133
|
+
<div
|
|
134
|
+
className={cn(
|
|
135
|
+
"flex h-10 w-full items-center rounded-md border border-input bg-transparent shadow-sm transition-colors hover:border-ring focus-within:ring-2 focus-within:ring-ring",
|
|
136
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<input
|
|
140
|
+
aria-label={label}
|
|
141
|
+
value={typed ?? (value ? formatDay(value, locale) : "")}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
// Só dígitos, e o teclado do celular abre no numérico: a máscara é
|
|
144
|
+
// quem escreve as barras.
|
|
145
|
+
inputMode="numeric"
|
|
146
|
+
placeholder={placeholder ?? dayPlaceholder(locale)}
|
|
147
|
+
onChange={(event) => digitar(event.target.value)}
|
|
148
|
+
// Sai do campo e o rascunho some: `11/0` não é data, e deixá-lo
|
|
149
|
+
// escrito faria o campo mostrar uma coisa e o formulário guardar
|
|
150
|
+
// outra.
|
|
151
|
+
onBlur={() => setTyped(null)}
|
|
152
|
+
className="h-full w-full min-w-0 bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
|
|
153
|
+
/>
|
|
154
|
+
<PopoverTrigger
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
aria-label={t("filters.openCalendar")}
|
|
157
|
+
className="flex h-full shrink-0 items-center px-3 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed"
|
|
158
|
+
>
|
|
159
|
+
<Calendar className="h-4 w-4" />
|
|
160
|
+
</PopoverTrigger>
|
|
161
|
+
</div>
|
|
162
|
+
</PopoverAnchor>
|
|
90
163
|
|
|
91
164
|
<PopoverContent
|
|
92
165
|
// Largura do campo, e não fixa: o painel é a continuação dele, e
|
|
@@ -160,6 +233,7 @@ export function DatePicker({
|
|
|
160
233
|
size="sm"
|
|
161
234
|
onClick={() => {
|
|
162
235
|
onChange(undefined);
|
|
236
|
+
setTyped(null);
|
|
163
237
|
setOpen(false);
|
|
164
238
|
}}
|
|
165
239
|
>
|
|
@@ -4,6 +4,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
|
4
4
|
import { Expand, Shrink, X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
+
import { Button, type ButtonProps } from "#core/components/ui/button";
|
|
7
8
|
import {
|
|
8
9
|
EXPAND_SHORTCUT_KEY,
|
|
9
10
|
EXPAND_SHORTCUT_LABEL,
|
|
@@ -11,6 +12,25 @@ import {
|
|
|
11
12
|
} from "#core/hooks/use-shortcut";
|
|
12
13
|
import { cn } from "#core/lib/utils";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Como pedir para fechar o modal **passando pela guarda**.
|
|
17
|
+
*
|
|
18
|
+
* O `Esc`, o clique fora e o `X` passam pelo `DialogContent`, que pergunta
|
|
19
|
+
* antes de descartar o que foi preenchido. O botão "Cancelar" mora na tela e
|
|
20
|
+
* chamava o `onClose` dela direto — mesma saída, sem a pergunta. Este contexto
|
|
21
|
+
* é o caminho que faltava: quem pede daqui recebe a mesma confirmação.
|
|
22
|
+
*/
|
|
23
|
+
const DialogCloseRequestContext = React.createContext<() => void>(() => {});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pede o fechamento do modal em que o componente está.
|
|
27
|
+
*
|
|
28
|
+
* Fora de um `DialogContent` não faz nada — nunca quebra, só não tem o que
|
|
29
|
+
* fechar.
|
|
30
|
+
*/
|
|
31
|
+
const useDialogCloseRequest = (): (() => void) =>
|
|
32
|
+
React.useContext(DialogCloseRequestContext);
|
|
33
|
+
|
|
14
34
|
const Dialog = DialogPrimitive.Root;
|
|
15
35
|
const DialogTrigger = DialogPrimitive.Trigger;
|
|
16
36
|
const DialogPortal = DialogPrimitive.Portal;
|
|
@@ -248,6 +268,22 @@ const DialogContent = React.forwardRef<
|
|
|
248
268
|
setConfirmandoSaida(true);
|
|
249
269
|
};
|
|
250
270
|
|
|
271
|
+
/**
|
|
272
|
+
* A saída pedida de dentro do corpo — é o que o "Cancelar" usa.
|
|
273
|
+
*
|
|
274
|
+
* Sem evento para segurar: aqui o fechamento nem começa, ou é a pergunta que
|
|
275
|
+
* aparece, ou é o `Close` escondido que fecha de uma vez.
|
|
276
|
+
*/
|
|
277
|
+
const pedirSaida = React.useCallback((): void => {
|
|
278
|
+
if (unsavedChanges) {
|
|
279
|
+
setConfirmandoSaida(true);
|
|
280
|
+
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fecharRef.current?.click();
|
|
285
|
+
}, [unsavedChanges]);
|
|
286
|
+
|
|
251
287
|
// O cabeçalho sai da lista para dividir a primeira linha com os botões; o
|
|
252
288
|
// resto segue na ordem em que o chamador escreveu.
|
|
253
289
|
const items = React.Children.toArray(children);
|
|
@@ -338,7 +374,9 @@ const DialogContent = React.forwardRef<
|
|
|
338
374
|
</DialogPrimitive.Close>
|
|
339
375
|
</div>
|
|
340
376
|
</div>
|
|
341
|
-
{
|
|
377
|
+
<DialogCloseRequestContext.Provider value={pedirSaida}>
|
|
378
|
+
{body}
|
|
379
|
+
</DialogCloseRequestContext.Provider>
|
|
342
380
|
|
|
343
381
|
<DialogPrimitive.Close ref={fecharRef} className="hidden" tabIndex={-1} />
|
|
344
382
|
</DialogPrimitive.Content>
|
|
@@ -467,8 +505,49 @@ const DialogDescription = React.forwardRef<
|
|
|
467
505
|
));
|
|
468
506
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
469
507
|
|
|
508
|
+
/**
|
|
509
|
+
* O "Cancelar" do rodapé — a mesma saída do `Esc`, com a mesma pergunta.
|
|
510
|
+
*
|
|
511
|
+
* Existe porque o botão que a tela escrevia à mão chamava o `onClose` dela
|
|
512
|
+
* direto: fechava sem perguntar nada, e o formulário preenchido ia junto.
|
|
513
|
+
* Aqui o fechamento é pedido ao modal, que decide se pergunta antes.
|
|
514
|
+
*
|
|
515
|
+
* O texto vem de fora — este arquivo não depende do provider de i18n, e passar
|
|
516
|
+
* a depender quebraria todo uso fora dele.
|
|
517
|
+
*
|
|
518
|
+
* ```tsx
|
|
519
|
+
* <DialogFooter>
|
|
520
|
+
* <DialogCancel>{t("common.cancel")}</DialogCancel>
|
|
521
|
+
* <Button type="submit">{t("common.save")}</Button>
|
|
522
|
+
* </DialogFooter>
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
const DialogCancel = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
526
|
+
({ variant = "outline", type = "button", onClick, ...props }, ref) => {
|
|
527
|
+
const pedirSaida = useDialogCloseRequest();
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<Button
|
|
531
|
+
ref={ref}
|
|
532
|
+
type={type}
|
|
533
|
+
variant={variant}
|
|
534
|
+
onClick={(event) => {
|
|
535
|
+
onClick?.(event);
|
|
536
|
+
|
|
537
|
+
if (!event.defaultPrevented) {
|
|
538
|
+
pedirSaida();
|
|
539
|
+
}
|
|
540
|
+
}}
|
|
541
|
+
{...props}
|
|
542
|
+
/>
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
DialogCancel.displayName = "DialogCancel";
|
|
547
|
+
|
|
470
548
|
export {
|
|
471
549
|
Dialog,
|
|
550
|
+
DialogCancel,
|
|
472
551
|
DialogClose,
|
|
473
552
|
DialogContent,
|
|
474
553
|
DialogDescription,
|
|
@@ -479,4 +558,5 @@ export {
|
|
|
479
558
|
DialogPortal,
|
|
480
559
|
DialogTitle,
|
|
481
560
|
DialogTrigger,
|
|
561
|
+
useDialogCloseRequest,
|
|
482
562
|
};
|
package/src/i18n/messages/en.ts
CHANGED
package/src/i18n/messages/pt.ts
CHANGED