@sincpro/web-ui 1.5.0 → 1.7.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/dist/components/attachments/Attachments.d.ts +56 -0
- package/dist/components/attachments/Attachments.js +93 -0
- package/dist/components/attachments/files.d.ts +28 -0
- package/dist/components/attachments/files.js +100 -0
- package/dist/components/attachments/index.d.ts +2 -0
- package/dist/components/attachments/index.js +3 -0
- package/dist/components/flow/StepFlow.d.ts +37 -0
- package/dist/components/flow/StepFlow.js +74 -0
- package/dist/components/flow/index.d.ts +1 -0
- package/dist/components/flow/index.js +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/lines/LineItems.d.ts +14 -3
- package/dist/components/lines/LineItems.js +6 -4
- package/dist/components/listing/SortMenu.d.ts +24 -20
- package/dist/components/listing/SortMenu.js +51 -91
- package/dist/components/listing/index.d.ts +2 -1
- package/dist/components/listing/index.js +2 -1
- package/dist/components/listing/modelCells.d.ts +7 -0
- package/dist/components/listing/modelCells.js +56 -0
- package/dist/components/listing/modelColumns.d.ts +9 -0
- package/dist/components/listing/modelColumns.js +59 -14
- package/dist/components/search/index.d.ts +1 -1
- package/dist/components/search/index.js +1 -1
- package/dist/components/search/model.d.ts +1 -0
- package/dist/components/search/model.js +7 -0
- package/dist/components/search/query.js +2 -3
- package/dist/foundations/copy.d.ts +131 -0
- package/dist/foundations/copy.es.js +86 -0
- package/dist/foundations/copy.js +86 -0
- package/dist/foundations/form.d.ts +18 -1
- package/dist/foundations/form.js +6 -3
- package/dist/primitives/button/Button.js +3 -1
- package/dist/primitives/field/CardNumberInput.d.ts +41 -0
- package/dist/primitives/field/CardNumberInput.js +117 -0
- package/dist/primitives/field/CodeInput.d.ts +18 -0
- package/dist/primitives/field/CodeInput.js +37 -0
- package/dist/primitives/field/ColorInput.d.ts +21 -0
- package/dist/primitives/field/ColorInput.js +42 -0
- package/dist/primitives/field/ContactInputs.d.ts +18 -0
- package/dist/primitives/field/ContactInputs.js +70 -0
- package/dist/primitives/field/DurationInput.d.ts +20 -0
- package/dist/primitives/field/DurationInput.js +54 -0
- package/dist/primitives/field/Field.d.ts +24 -4
- package/dist/primitives/field/Field.js +17 -4
- package/dist/primitives/field/ImageInput.d.ts +51 -0
- package/dist/primitives/field/ImageInput.js +267 -0
- package/dist/primitives/field/PasswordInput.d.ts +29 -0
- package/dist/primitives/field/PasswordInput.js +44 -0
- package/dist/primitives/field/PatternInput.d.ts +23 -0
- package/dist/primitives/field/PatternInput.js +48 -0
- package/dist/primitives/field/RichTextEditor.d.ts +18 -0
- package/dist/primitives/field/RichTextEditor.js +224 -0
- package/dist/primitives/field/RichTextInput.d.ts +30 -0
- package/dist/primitives/field/RichTextInput.js +38 -0
- package/dist/primitives/field/SignatureInput.d.ts +34 -0
- package/dist/primitives/field/SignatureInput.js +215 -0
- package/dist/primitives/field/card.d.ts +33 -0
- package/dist/primitives/field/card.js +173 -0
- package/dist/primitives/field/clipboard.js +3 -2
- package/dist/primitives/field/contact.d.ts +17 -0
- package/dist/primitives/field/contact.js +65 -0
- package/dist/primitives/field/duration.d.ts +5 -0
- package/dist/primitives/field/duration.js +48 -0
- package/dist/primitives/field/index.d.ts +14 -0
- package/dist/primitives/field/index.js +20 -0
- package/dist/primitives/field/mask.d.ts +33 -0
- package/dist/primitives/field/mask.js +56 -0
- package/dist/primitives/field/media.d.ts +99 -0
- package/dist/primitives/field/media.js +136 -0
- package/dist/primitives/field/pattern.d.ts +10 -0
- package/dist/primitives/field/pattern.js +68 -0
- package/dist/primitives/menu/Menu.d.ts +16 -0
- package/dist/primitives/menu/Menu.js +26 -8
- package/dist/primitives/menu/index.d.ts +1 -1
- package/dist/primitives/menu/index.js +1 -1
- package/dist/primitives/table/DataTable.d.ts +5 -3
- package/dist/primitives/table/DataTable.js +2 -4
- package/dist/primitives/table/EditableTable.d.ts +49 -7
- package/dist/primitives/table/EditableTable.js +321 -82
- package/dist/primitives/table/Table.d.ts +5 -8
- package/dist/primitives/table/Table.js +63 -15
- package/dist/primitives/table/index.d.ts +3 -2
- package/dist/primitives/table/index.js +2 -1
- package/dist/primitives/table/rowWindow.d.ts +3 -0
- package/dist/primitives/table/rowWindow.js +4 -0
- package/dist/primitives/table/sorting.d.ts +42 -0
- package/dist/primitives/table/sorting.js +110 -0
- package/dist/primitives/value/RichText.d.ts +10 -0
- package/dist/primitives/value/RichText.js +130 -0
- package/dist/primitives/value/Value.d.ts +20 -2
- package/dist/primitives/value/Value.js +26 -7
- package/dist/primitives/value/index.d.ts +3 -1
- package/dist/primitives/value/index.js +4 -1
- package/dist/primitives/value/rounding.d.ts +9 -0
- package/dist/primitives/value/rounding.js +48 -0
- package/dist/tokens.css +103 -0
- package/dist/views/list/List.d.ts +5 -4
- package/dist/views/list/ListView.js +2 -2
- package/dist/views/list/types.d.ts +6 -6
- package/package.json +9 -1
|
@@ -110,6 +110,14 @@ export const SPANISH_COPY = {
|
|
|
110
110
|
unset: "sin definir",
|
|
111
111
|
none: "Ninguna",
|
|
112
112
|
changed: "modificado",
|
|
113
|
+
showSecret: "Mostrar contraseña",
|
|
114
|
+
hideSecret: "Ocultar contraseña",
|
|
115
|
+
capsLock: "Bloq Mayús está activado",
|
|
116
|
+
secretSet: "contraseña establecida",
|
|
117
|
+
newTab: "se abre en otra pestaña",
|
|
118
|
+
hours: "h",
|
|
119
|
+
unit: "Unidad",
|
|
120
|
+
palette: "Paleta",
|
|
113
121
|
},
|
|
114
122
|
choice: {
|
|
115
123
|
noResults: "Sin resultados",
|
|
@@ -260,12 +268,44 @@ export const SPANISH_COPY = {
|
|
|
260
268
|
field: "Copiar «{label}»",
|
|
261
269
|
empty: "No hay nada que copiar",
|
|
262
270
|
},
|
|
271
|
+
richText: {
|
|
272
|
+
toolbar: "Formato del texto",
|
|
273
|
+
bold: "Negrita",
|
|
274
|
+
italic: "Cursiva",
|
|
275
|
+
underline: "Subrayado",
|
|
276
|
+
strike: "Tachado",
|
|
277
|
+
heading: "Título",
|
|
278
|
+
subheading: "Subtítulo",
|
|
279
|
+
bulletList: "Lista con viñetas",
|
|
280
|
+
orderedList: "Lista numerada",
|
|
281
|
+
quote: "Cita",
|
|
282
|
+
link: "Enlace",
|
|
283
|
+
linkUrl: "Dirección del enlace",
|
|
284
|
+
applyLink: "Aplicar",
|
|
285
|
+
removeLink: "Quitar enlace",
|
|
286
|
+
badLink: "Escribí una web (empresa.bo), un correo o un teléfono.",
|
|
287
|
+
table: "Insertar tabla",
|
|
288
|
+
undo: "Deshacer",
|
|
289
|
+
redo: "Rehacer",
|
|
290
|
+
clear: "Quitar formato",
|
|
291
|
+
mentions: "Personas",
|
|
292
|
+
noMatches: "No coincide nadie",
|
|
293
|
+
},
|
|
263
294
|
sorting: {
|
|
264
295
|
label: "Ordenar",
|
|
265
296
|
by: "ordenar por",
|
|
266
297
|
asc: "ascendente",
|
|
267
298
|
desc: "descendente",
|
|
268
299
|
clear: "sin orden",
|
|
300
|
+
addHint: "Mayús+clic para agregarla como desempate",
|
|
301
|
+
place: "orden {place}, {direction}",
|
|
302
|
+
add: "Agregar criterio",
|
|
303
|
+
none: "Sin orden: las filas van como llegaron.",
|
|
304
|
+
flip: "Invertir el orden por {field}",
|
|
305
|
+
remove: "Quitar el orden por {field}",
|
|
306
|
+
move: "Mover el orden por {field}",
|
|
307
|
+
more: "{label} +{count}",
|
|
308
|
+
then: "luego por",
|
|
269
309
|
},
|
|
270
310
|
time: { justNow: "recién", invalid: "fecha inválida", noDate: "sin fecha" },
|
|
271
311
|
calendar: {
|
|
@@ -324,6 +364,18 @@ export const SPANISH_COPY = {
|
|
|
324
364
|
finish: "Terminar",
|
|
325
365
|
busy: "Procesando…",
|
|
326
366
|
},
|
|
367
|
+
stepFlow: {
|
|
368
|
+
steps: "Pasos",
|
|
369
|
+
position: "Paso {index} de {total}",
|
|
370
|
+
announce: "{position}: {title}",
|
|
371
|
+
done: "hecho",
|
|
372
|
+
pending: "pendiente",
|
|
373
|
+
cancel: "Cancelar",
|
|
374
|
+
back: "Atrás",
|
|
375
|
+
next: "Siguiente",
|
|
376
|
+
finish: "Terminar",
|
|
377
|
+
working: "Procesando…",
|
|
378
|
+
},
|
|
327
379
|
report: { open: "Abrir {name}", close: "Cerrar {name}", concept: "Concepto" },
|
|
328
380
|
builder: {
|
|
329
381
|
filter: "Filtro",
|
|
@@ -478,6 +530,20 @@ export const SPANISH_COPY = {
|
|
|
478
530
|
createTag: "Crear «{name}»",
|
|
479
531
|
editCell: "Editar {column}",
|
|
480
532
|
},
|
|
533
|
+
attachments: {
|
|
534
|
+
label: "Adjuntos",
|
|
535
|
+
empty: "Todavía no hay archivos adjuntos.",
|
|
536
|
+
emptyAdd: "Todavía no hay archivos adjuntos. Soltá uno acá o elegilo.",
|
|
537
|
+
add: "Soltá otro archivo acá o elegilo.",
|
|
538
|
+
download: "Descargar",
|
|
539
|
+
remove: "Quitar el archivo",
|
|
540
|
+
removed: "Se quitó «{name}».",
|
|
541
|
+
undo: "Deshacer",
|
|
542
|
+
uploading: "Subiendo {name}",
|
|
543
|
+
cancelUpload: "Cancelar la subida",
|
|
544
|
+
failed: "No se pudo subir el archivo.",
|
|
545
|
+
retry: "Reintentar",
|
|
546
|
+
},
|
|
481
547
|
theme: {
|
|
482
548
|
label: "Tema",
|
|
483
549
|
system: "Sistema",
|
|
@@ -517,4 +583,24 @@ export const SPANISH_COPY = {
|
|
|
517
583
|
to: "a",
|
|
518
584
|
empty: "vacío",
|
|
519
585
|
},
|
|
586
|
+
media: {
|
|
587
|
+
dropImage: "Soltá una imagen acá, pegala o elegila.",
|
|
588
|
+
chooseImage: "Elegir imagen",
|
|
589
|
+
changeImage: "Cambiar imagen",
|
|
590
|
+
removeImage: "Quitar imagen",
|
|
591
|
+
notImage: "El archivo no es una imagen. Elegí un PNG, JPG o WebP.",
|
|
592
|
+
crop: "Recorte de la imagen",
|
|
593
|
+
cropHelp: "Arrastrá la imagen o movela con las flechas; + y − acercan y alejan.",
|
|
594
|
+
zoomIn: "Acercar",
|
|
595
|
+
zoomOut: "Alejar",
|
|
596
|
+
applyCrop: "Aplicar recorte",
|
|
597
|
+
cancel: "Cancelar",
|
|
598
|
+
signaturePad: "Área de firma",
|
|
599
|
+
signatureHelp: "Firmá con el mouse o con el dedo sobre la línea.",
|
|
600
|
+
clear: "Limpiar",
|
|
601
|
+
typeName: "Escribir mi nombre",
|
|
602
|
+
drawInstead: "Dibujar la firma",
|
|
603
|
+
typedName: "Nombre para la firma",
|
|
604
|
+
signature: "Firma",
|
|
605
|
+
},
|
|
520
606
|
};
|
package/dist/foundations/copy.js
CHANGED
|
@@ -86,6 +86,14 @@ export const ENGLISH_COPY = {
|
|
|
86
86
|
unset: "not set",
|
|
87
87
|
none: "None",
|
|
88
88
|
changed: "changed",
|
|
89
|
+
showSecret: "Show password",
|
|
90
|
+
hideSecret: "Hide password",
|
|
91
|
+
capsLock: "Caps Lock is on",
|
|
92
|
+
secretSet: "password set",
|
|
93
|
+
newTab: "opens in a new tab",
|
|
94
|
+
hours: "h",
|
|
95
|
+
unit: "Unit",
|
|
96
|
+
palette: "Palette",
|
|
89
97
|
},
|
|
90
98
|
choice: {
|
|
91
99
|
noResults: "No results",
|
|
@@ -208,12 +216,44 @@ export const ENGLISH_COPY = {
|
|
|
208
216
|
field: "Copy «{label}»",
|
|
209
217
|
empty: "Nothing to copy",
|
|
210
218
|
},
|
|
219
|
+
richText: {
|
|
220
|
+
toolbar: "Text formatting",
|
|
221
|
+
bold: "Bold",
|
|
222
|
+
italic: "Italic",
|
|
223
|
+
underline: "Underline",
|
|
224
|
+
strike: "Strikethrough",
|
|
225
|
+
heading: "Heading",
|
|
226
|
+
subheading: "Subheading",
|
|
227
|
+
bulletList: "Bulleted list",
|
|
228
|
+
orderedList: "Numbered list",
|
|
229
|
+
quote: "Quote",
|
|
230
|
+
link: "Link",
|
|
231
|
+
linkUrl: "Link address",
|
|
232
|
+
applyLink: "Apply",
|
|
233
|
+
removeLink: "Remove link",
|
|
234
|
+
badLink: "Write a website (example.com), an email or a phone number.",
|
|
235
|
+
table: "Insert table",
|
|
236
|
+
undo: "Undo",
|
|
237
|
+
redo: "Redo",
|
|
238
|
+
clear: "Clear formatting",
|
|
239
|
+
mentions: "People",
|
|
240
|
+
noMatches: "Nobody matches",
|
|
241
|
+
},
|
|
211
242
|
sorting: {
|
|
212
243
|
label: "Sort",
|
|
213
244
|
by: "sort by",
|
|
214
245
|
asc: "ascending",
|
|
215
246
|
desc: "descending",
|
|
216
247
|
clear: "no order",
|
|
248
|
+
addHint: "Shift+click to add as a tie-breaker",
|
|
249
|
+
place: "sort {place}, {direction}",
|
|
250
|
+
add: "Add a criterion",
|
|
251
|
+
none: "No order: the rows go as they came.",
|
|
252
|
+
flip: "Flip the order by {field}",
|
|
253
|
+
remove: "Remove the order by {field}",
|
|
254
|
+
move: "Move the order by {field}",
|
|
255
|
+
more: "{label} +{count}",
|
|
256
|
+
then: "then by",
|
|
217
257
|
},
|
|
218
258
|
dates: {
|
|
219
259
|
last: "last {count} {unit}",
|
|
@@ -310,6 +350,18 @@ export const ENGLISH_COPY = {
|
|
|
310
350
|
finish: "Finish",
|
|
311
351
|
busy: "Working…",
|
|
312
352
|
},
|
|
353
|
+
stepFlow: {
|
|
354
|
+
steps: "Steps",
|
|
355
|
+
position: "Step {index} of {total}",
|
|
356
|
+
announce: "{position}: {title}",
|
|
357
|
+
done: "done",
|
|
358
|
+
pending: "to do",
|
|
359
|
+
cancel: "Cancel",
|
|
360
|
+
back: "Back",
|
|
361
|
+
next: "Next",
|
|
362
|
+
finish: "Finish",
|
|
363
|
+
working: "Working…",
|
|
364
|
+
},
|
|
313
365
|
report: {
|
|
314
366
|
open: "Open {name}",
|
|
315
367
|
close: "Close {name}",
|
|
@@ -473,6 +525,20 @@ export const ENGLISH_COPY = {
|
|
|
473
525
|
createTag: "Create «{name}»",
|
|
474
526
|
editCell: "Edit {column}",
|
|
475
527
|
},
|
|
528
|
+
attachments: {
|
|
529
|
+
label: "Attachments",
|
|
530
|
+
empty: "No files attached yet.",
|
|
531
|
+
emptyAdd: "No files attached yet. Drop one here, or choose it.",
|
|
532
|
+
add: "Drop another file here, or choose one.",
|
|
533
|
+
download: "Download",
|
|
534
|
+
remove: "Remove the file",
|
|
535
|
+
removed: "Removed «{name}».",
|
|
536
|
+
undo: "Undo",
|
|
537
|
+
uploading: "Uploading {name}",
|
|
538
|
+
cancelUpload: "Cancel the upload",
|
|
539
|
+
failed: "The file could not be uploaded.",
|
|
540
|
+
retry: "Try again",
|
|
541
|
+
},
|
|
476
542
|
theme: {
|
|
477
543
|
label: "Theme",
|
|
478
544
|
system: "System",
|
|
@@ -512,6 +578,26 @@ export const ENGLISH_COPY = {
|
|
|
512
578
|
to: "to",
|
|
513
579
|
empty: "empty",
|
|
514
580
|
},
|
|
581
|
+
media: {
|
|
582
|
+
dropImage: "Drop an image here, paste it, or choose one.",
|
|
583
|
+
chooseImage: "Choose image",
|
|
584
|
+
changeImage: "Change image",
|
|
585
|
+
removeImage: "Remove image",
|
|
586
|
+
notImage: "That file is not an image. Choose a PNG, JPG or WebP.",
|
|
587
|
+
crop: "Image crop",
|
|
588
|
+
cropHelp: "Drag the image, or move it with the arrow keys; + and − zoom in and out.",
|
|
589
|
+
zoomIn: "Zoom in",
|
|
590
|
+
zoomOut: "Zoom out",
|
|
591
|
+
applyCrop: "Apply crop",
|
|
592
|
+
cancel: "Cancel",
|
|
593
|
+
signaturePad: "Signature pad",
|
|
594
|
+
signatureHelp: "Sign with the mouse or a finger over the line.",
|
|
595
|
+
clear: "Clear",
|
|
596
|
+
typeName: "Type my name instead",
|
|
597
|
+
drawInstead: "Draw the signature",
|
|
598
|
+
typedName: "Name for the signature",
|
|
599
|
+
signature: "Signature",
|
|
600
|
+
},
|
|
515
601
|
};
|
|
516
602
|
/* The mechanism is `catalog.tsx`, the same one an application uses for its own words. */
|
|
517
603
|
const ui = createCopy(ENGLISH_COPY);
|
|
@@ -18,7 +18,24 @@ export type FieldIds = {
|
|
|
18
18
|
invalid: boolean;
|
|
19
19
|
};
|
|
20
20
|
export declare const FieldIdsContext: import("react").Context<FieldIds | null>;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* What a place WITHOUT a `Field` says to the control in it: what describes it and whether it
|
|
23
|
+
* is in error — no id, no label.
|
|
24
|
+
*
|
|
25
|
+
* Context: a cell of an editable table has no label of its own (its control carries an
|
|
26
|
+
* `aria-label`, the column's name) but it can be wrong — «la cantidad supera el stock» — and
|
|
27
|
+
* that has to reach the control the same way a field's error does, or the error is a red
|
|
28
|
+
* cell nobody hears. The table provides this around each control it edits; every control
|
|
29
|
+
* that reads `useFieldIds` (the package's and an application's own) gets `aria-invalid` and
|
|
30
|
+
* `aria-describedby` without the table knowing what control it is.
|
|
31
|
+
*/
|
|
32
|
+
export type ControlState = {
|
|
33
|
+
describedBy?: string;
|
|
34
|
+
invalid?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export declare const ControlStateContext: import("react").Context<ControlState | null>;
|
|
37
|
+
/** The ids of the enclosing `Field` — or, outside one, the state of the place the control
|
|
38
|
+
* sits in (a cell) — resolved against the control's own props. */
|
|
22
39
|
export declare function useFieldIds(own: {
|
|
23
40
|
id?: string;
|
|
24
41
|
describedBy?: string;
|
package/dist/foundations/form.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
4
|
export const FieldIdsContext = createContext(null);
|
|
5
|
-
|
|
5
|
+
export const ControlStateContext = createContext(null);
|
|
6
|
+
/** The ids of the enclosing `Field` — or, outside one, the state of the place the control
|
|
7
|
+
* sits in (a cell) — resolved against the control's own props. */
|
|
6
8
|
export function useFieldIds(own) {
|
|
7
9
|
const field = useContext(FieldIdsContext);
|
|
10
|
+
const place = useContext(ControlStateContext);
|
|
8
11
|
return {
|
|
9
12
|
id: own.id ?? field?.id,
|
|
10
13
|
labelledBy: field?.labelId,
|
|
11
|
-
describedBy: own.describedBy ?? field?.describedBy,
|
|
12
|
-
invalid: own.invalid ?? field?.invalid ?? false,
|
|
14
|
+
describedBy: own.describedBy ?? field?.describedBy ?? place?.describedBy,
|
|
15
|
+
invalid: own.invalid ?? field?.invalid ?? place?.invalid ?? false,
|
|
13
16
|
};
|
|
14
17
|
}
|
|
15
18
|
const Ctx = createContext("edit");
|
|
@@ -195,7 +195,9 @@ const TONE = {
|
|
|
195
195
|
chrome: "text-chrome-bad hover:bg-chrome-bad/12",
|
|
196
196
|
},
|
|
197
197
|
};
|
|
198
|
-
|
|
198
|
+
/* `aria-pressed` is a TOGGLE's state — bold on, a filter applied: it reads as the action
|
|
199
|
+
tint, the same as a chosen option, so «on» is seen and not only heard. */
|
|
200
|
+
const BOX = "inline-flex shrink-0 items-center justify-center rounded-pill transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary aria-pressed:bg-primary/12 aria-pressed:text-primary";
|
|
199
201
|
const SQUARE = {
|
|
200
202
|
sm: "size-(--size-control-sm)",
|
|
201
203
|
md: "size-(--size-control-md)",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type CardBrand } from "./card.js";
|
|
2
|
+
import { type ControlProps } from "./Field.js";
|
|
3
|
+
export declare function CardNumberInput({ id: ownId, value, onChange, placeholder, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, mode, brand: givenBrand, masked, "aria-label": ariaLabel, }: ControlProps & {
|
|
4
|
+
/** The digits, without spaces. Whatever else comes in (a pasted «4242-4242…») is dropped. */
|
|
5
|
+
value: string;
|
|
6
|
+
onChange?: (digits: string) => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** The brand when the digits cannot say it — a stored card known only by its last four. */
|
|
9
|
+
brand?: CardBrand;
|
|
10
|
+
/** In read mode, only the last four. On by default. */
|
|
11
|
+
masked?: boolean;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* The expiry of a card, «MM/AA». The value is the four digits («1228»); the slash is drawn.
|
|
15
|
+
*
|
|
16
|
+
* Context: the month that can only be one digit gets its zero while typing — «4» is April,
|
|
17
|
+
* so «04/» — which is what a card's face says and what saves a keystroke. The caret is kept
|
|
18
|
+
* by digit, like `CardNumberInput`. Whether it already EXPIRED is the form's to say, with
|
|
19
|
+
* `isExpiryValid`: good through the last day of its month.
|
|
20
|
+
*/
|
|
21
|
+
export declare function CardExpiryInput({ mode, value, onChange, placeholder, ...props }: ControlProps & {
|
|
22
|
+
/** The four digits, MMAA. */
|
|
23
|
+
value: string;
|
|
24
|
+
onChange?: (digits: string) => void;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
/**
|
|
28
|
+
* The security code on the back of a card (on the front of an Amex): three digits, four for
|
|
29
|
+
* Amex — `brand` decides, so it can be the one `CardNumberInput` recognised.
|
|
30
|
+
*
|
|
31
|
+
* Read mode never shows it: a fixed row of dots, as long as the brand's code, and the copy
|
|
32
|
+
* button finds nothing to copy. It is asked once and not stored — a form that keeps it is
|
|
33
|
+
* outside what a card network allows.
|
|
34
|
+
*/
|
|
35
|
+
export declare function CardCodeInput({ mode, value, onChange, brand, placeholder, ...props }: ControlProps & {
|
|
36
|
+
value: string;
|
|
37
|
+
onChange?: (digits: string) => void;
|
|
38
|
+
/** The card's brand, for its length. */
|
|
39
|
+
brand?: CardBrand | null;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useFieldIds, useFormMode } from "../../foundations/form.js";
|
|
4
|
+
import { FieldGroup } from "./aria.js";
|
|
5
|
+
import { cardBrand, cardBrandName, cardCodeLength, cardDigits, cardMaxDigits, formatCardNumber, formatExpiry, maskCardNumber, } from "./card.js";
|
|
6
|
+
import { controlClasses, nameOf, ReadValue, } from "./Field.js";
|
|
7
|
+
import { maskedChange, useMaskCaret } from "./mask.js";
|
|
8
|
+
import { CreditCard } from "lucide-react";
|
|
9
|
+
import { Input, TextField as AriaTextField } from "react-aria-components";
|
|
10
|
+
/**
|
|
11
|
+
* A payment card number: typed in the brand's groups, the brand named as it is recognised.
|
|
12
|
+
*
|
|
13
|
+
* Context: the value is the DIGITS (`card.ts`); the spaces are drawn. Formatting while
|
|
14
|
+
* typing has one trap — inserting a space moves the text under the caret, and a caret that
|
|
15
|
+
* jumps to the end on every fourth digit makes correcting the middle of a number impossible.
|
|
16
|
+
* So the caret is kept by DIGIT, not by character: after each change it goes back after the
|
|
17
|
+
* same number of digits it was after. Deleting a space deletes the digit before it, which is
|
|
18
|
+
* what the person meant.
|
|
19
|
+
*
|
|
20
|
+
* What it does not decide is whether the number is WRONG: `isValidCardNumber` (Luhn, length
|
|
21
|
+
* by brand) is exported for the form to say it in the field's `error`, when the person tries
|
|
22
|
+
* — not on every keystroke, while the number is still half typed.
|
|
23
|
+
*
|
|
24
|
+
* Read mode shows the brand and the last four («Visa •••• 4242»): a stored card is shown that
|
|
25
|
+
* way everywhere. `masked={false}` shows it whole, for the one screen that has to. An
|
|
26
|
+
* application that only keeps the last four passes those and the `brand`.
|
|
27
|
+
*/
|
|
28
|
+
const isDigit = (char) => /\d/.test(char);
|
|
29
|
+
export function CardNumberInput({ id: ownId, value, onChange, placeholder, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, mode, brand: givenBrand, masked = true, "aria-label": ariaLabel, }) {
|
|
30
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
31
|
+
id: ownId,
|
|
32
|
+
describedBy: ownDescribedBy,
|
|
33
|
+
invalid: ownInvalid,
|
|
34
|
+
});
|
|
35
|
+
const digits = cardDigits(value);
|
|
36
|
+
const brand = givenBrand ?? cardBrand(digits);
|
|
37
|
+
const formatted = formatCardNumber(digits, brand);
|
|
38
|
+
const { input, keep } = useMaskCaret(formatted, isDigit);
|
|
39
|
+
if (useFormMode(mode) === "read") {
|
|
40
|
+
const shown = masked ? maskCardNumber(digits) : formatted;
|
|
41
|
+
return (_jsx(ReadValue, { mono: true, value: digits === "" ? null : brand === null ? shown : `${cardBrandName(brand)} ${shown}` }));
|
|
42
|
+
}
|
|
43
|
+
const change = (raw) => {
|
|
44
|
+
const { next, caret } = maskedChange({
|
|
45
|
+
raw,
|
|
46
|
+
at: input.current?.selectionStart ?? raw.length,
|
|
47
|
+
current: digits,
|
|
48
|
+
drawn: formatted,
|
|
49
|
+
clean: cardDigits,
|
|
50
|
+
max: cardMaxDigits(cardDigits(raw), givenBrand),
|
|
51
|
+
});
|
|
52
|
+
keep(caret);
|
|
53
|
+
onChange?.(next);
|
|
54
|
+
};
|
|
55
|
+
return (_jsx(AriaTextField, { id: id, "aria-labelledby": labelledBy, "aria-label": nameOf(labelledBy, ariaLabel, placeholder), value: formatted, onChange: change, inputMode: "numeric", autoComplete: "cc-number", isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: _jsxs(FieldGroup, { isInvalid: invalid, isDisabled: disabled, children: [_jsx(CreditCard, { "aria-hidden": true, className: "ml-2.5 size-(--size-icon-sm) shrink-0 text-muted", strokeWidth: 2 }), _jsx(Input, { ref: input, "aria-describedby": describedBy, placeholder: placeholder, spellCheck: false, className: "h-full min-w-0 flex-1 bg-transparent px-2 font-mono text-meta text-ink tabular-nums outline-0 placeholder:text-muted disabled:cursor-not-allowed" }), brand === null ? null : (
|
|
56
|
+
/* The brand as it is recognised: a wrong first digit shows up here, before the
|
|
57
|
+
sixteenth. Read with the number, as part of what the field holds. */
|
|
58
|
+
_jsx("span", { className: "mr-2.5 shrink-0 text-micro font-medium whitespace-nowrap text-muted", children: cardBrandName(brand) }))] }) }));
|
|
59
|
+
}
|
|
60
|
+
/** The box of a short card datum: numeric keyboard, its autofill, digits in tabular mono. */
|
|
61
|
+
function CardDatumBox({ id: ownId, value, onChange, placeholder, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, autoComplete, inputRef, }) {
|
|
62
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
63
|
+
id: ownId,
|
|
64
|
+
describedBy: ownDescribedBy,
|
|
65
|
+
invalid: ownInvalid,
|
|
66
|
+
});
|
|
67
|
+
return (_jsx(AriaTextField, { id: id, "aria-labelledby": labelledBy, "aria-label": nameOf(labelledBy, ariaLabel, placeholder), value: value, onChange: onChange, inputMode: "numeric", autoComplete: autoComplete, isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: _jsx(Input, { ...(inputRef === undefined ? {} : { ref: inputRef }), "aria-describedby": describedBy, placeholder: placeholder, spellCheck: false, className: `${controlClasses(invalid)} font-mono tabular-nums` }) }));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The expiry of a card, «MM/AA». The value is the four digits («1228»); the slash is drawn.
|
|
71
|
+
*
|
|
72
|
+
* Context: the month that can only be one digit gets its zero while typing — «4» is April,
|
|
73
|
+
* so «04/» — which is what a card's face says and what saves a keystroke. The caret is kept
|
|
74
|
+
* by digit, like `CardNumberInput`. Whether it already EXPIRED is the form's to say, with
|
|
75
|
+
* `isExpiryValid`: good through the last day of its month.
|
|
76
|
+
*/
|
|
77
|
+
export function CardExpiryInput({ mode, value, onChange, placeholder = "MM/AA", ...props }) {
|
|
78
|
+
const digits = cardDigits(value).slice(0, 4);
|
|
79
|
+
const drawn = formatExpiry(digits);
|
|
80
|
+
const { input, keep } = useMaskCaret(drawn, isDigit);
|
|
81
|
+
if (useFormMode(mode) === "read") {
|
|
82
|
+
return _jsx(ReadValue, { mono: true, value: digits === "" ? null : drawn });
|
|
83
|
+
}
|
|
84
|
+
const change = (raw) => {
|
|
85
|
+
let { next, caret } = maskedChange({
|
|
86
|
+
raw,
|
|
87
|
+
at: input.current?.selectionStart ?? raw.length,
|
|
88
|
+
current: digits,
|
|
89
|
+
drawn,
|
|
90
|
+
clean: cardDigits,
|
|
91
|
+
max: 4,
|
|
92
|
+
});
|
|
93
|
+
/* Only while TYPING: deleting the zero of «04» must not put it back. */
|
|
94
|
+
if (raw.length > drawn.length && /^[2-9]/.test(next)) {
|
|
95
|
+
next = `0${next}`.slice(0, 4);
|
|
96
|
+
caret = Math.min(caret + 1, next.length);
|
|
97
|
+
}
|
|
98
|
+
keep(caret);
|
|
99
|
+
onChange?.(next);
|
|
100
|
+
};
|
|
101
|
+
return (_jsx(CardDatumBox, { ...props, value: drawn, onChange: change, placeholder: placeholder, autoComplete: "cc-exp", inputRef: input }));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The security code on the back of a card (on the front of an Amex): three digits, four for
|
|
105
|
+
* Amex — `brand` decides, so it can be the one `CardNumberInput` recognised.
|
|
106
|
+
*
|
|
107
|
+
* Read mode never shows it: a fixed row of dots, as long as the brand's code, and the copy
|
|
108
|
+
* button finds nothing to copy. It is asked once and not stored — a form that keeps it is
|
|
109
|
+
* outside what a card network allows.
|
|
110
|
+
*/
|
|
111
|
+
export function CardCodeInput({ mode, value, onChange, brand, placeholder, ...props }) {
|
|
112
|
+
const length = cardCodeLength(brand);
|
|
113
|
+
if (useFormMode(mode) === "read") {
|
|
114
|
+
return (_jsx(ReadValue, { value: value === "" ? null : (_jsx("span", { "aria-hidden": true, "data-copy-skip": "", className: "tracking-widest", children: "•".repeat(length) })) }));
|
|
115
|
+
}
|
|
116
|
+
return (_jsx(CardDatumBox, { ...props, value: cardDigits(value).slice(0, length), onChange: (raw) => onChange?.(cardDigits(raw).slice(0, length)), placeholder: placeholder ?? "•".repeat(length), autoComplete: "cc-csc" }));
|
|
117
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ControlProps } from "./Field.js";
|
|
2
|
+
/**
|
|
3
|
+
* A verification code of N characters — the one an SMS or an email sends — drawn as N boxes.
|
|
4
|
+
*
|
|
5
|
+
* Context: the boxes are only drawn. Underneath is ONE input, transparent over them: six
|
|
6
|
+
* inputs that pass the focus from one to the next break pasting the whole code, the
|
|
7
|
+
* device's «fill from SMS» (`autoComplete="one-time-code"`), and a screen reader, which
|
|
8
|
+
* hears six fields with no name. One input does all three, and the boxes show where the next
|
|
9
|
+
* character goes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function CodeInput({ id: ownId, value, onChange, length, alphanumeric, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, mode, "aria-label": ariaLabel, }: ControlProps & {
|
|
12
|
+
value: string;
|
|
13
|
+
onChange?: (code: string) => void;
|
|
14
|
+
/** How many characters. */
|
|
15
|
+
length?: number;
|
|
16
|
+
/** Letters too, in capitals. Digits only by default. */
|
|
17
|
+
alphanumeric?: boolean;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useFieldIds, useFormMode } from "../../foundations/form.js";
|
|
4
|
+
import { nameOf, ReadValue } from "./Field.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Input, TextField as AriaTextField } from "react-aria-components";
|
|
7
|
+
/**
|
|
8
|
+
* A verification code of N characters — the one an SMS or an email sends — drawn as N boxes.
|
|
9
|
+
*
|
|
10
|
+
* Context: the boxes are only drawn. Underneath is ONE input, transparent over them: six
|
|
11
|
+
* inputs that pass the focus from one to the next break pasting the whole code, the
|
|
12
|
+
* device's «fill from SMS» (`autoComplete="one-time-code"`), and a screen reader, which
|
|
13
|
+
* hears six fields with no name. One input does all three, and the boxes show where the next
|
|
14
|
+
* character goes.
|
|
15
|
+
*/
|
|
16
|
+
export function CodeInput({ id: ownId, value, onChange, length = 6, alphanumeric = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, mode, "aria-label": ariaLabel, }) {
|
|
17
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
18
|
+
id: ownId,
|
|
19
|
+
describedBy: ownDescribedBy,
|
|
20
|
+
invalid: ownInvalid,
|
|
21
|
+
});
|
|
22
|
+
const [focused, setFocused] = useState(false);
|
|
23
|
+
const clean = (text) => (alphanumeric
|
|
24
|
+
? text.replace(/[^a-z\d]/gi, "").toUpperCase()
|
|
25
|
+
: text.replace(/\D/g, "")).slice(0, length);
|
|
26
|
+
const code = clean(value);
|
|
27
|
+
if (useFormMode(mode) === "read") {
|
|
28
|
+
return _jsx(ReadValue, { mono: true, value: code === "" ? null : code });
|
|
29
|
+
}
|
|
30
|
+
/* The box the next character goes into; the last one once the code is whole. */
|
|
31
|
+
const current = Math.min(code.length, length - 1);
|
|
32
|
+
return (_jsxs(AriaTextField, { id: id, "aria-labelledby": labelledBy, "aria-label": nameOf(labelledBy, ariaLabel), value: code, onChange: (next) => onChange?.(clean(next)), inputMode: alphanumeric ? "text" : "numeric", autoComplete: "one-time-code", isInvalid: invalid, isDisabled: disabled, className: "relative inline-flex", children: [_jsx("span", { "aria-hidden": true, className: "flex gap-1.5", children: Array.from({ length }, (_, index) => (_jsx("span", { className: `flex size-(--size-control-md) items-center justify-center rounded-control border bg-surface font-mono text-lead text-ink transition-colors duration-(--duration-instant) ${invalid
|
|
33
|
+
? "border-danger"
|
|
34
|
+
: focused && index === current
|
|
35
|
+
? "border-primary outline-2 outline-offset-1 outline-primary"
|
|
36
|
+
: "border-line-strong"} ${disabled ? "bg-ground text-muted" : ""}`, children: code[index] ?? "" }, index))) }), _jsx(Input, { "aria-describedby": describedBy, spellCheck: false, onFocus: () => setFocused(true), onBlur: () => setFocused(false), className: "absolute inset-0 size-full cursor-text bg-transparent text-transparent caret-transparent outline-0 selection:bg-transparent disabled:cursor-not-allowed" })] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ControlProps } from "./Field.js";
|
|
2
|
+
/**
|
|
3
|
+
* A color: the label of a tag, the color of a stage, a brand color — as a hex «#1F6FEB».
|
|
4
|
+
*
|
|
5
|
+
* Context: it is React Aria's colour set. `ColorField` takes the hex typed or pasted (with or
|
|
6
|
+
* without `#`) and reads it back normalised; `ColorSwatch` draws it with the colour's NAME
|
|
7
|
+
* as its accessible name («azul oscuro vívido»), so a screen reader hears a colour and not
|
|
8
|
+
* six hex digits; `ColorSwatchPicker` is the row of `swatches` the application offers — its
|
|
9
|
+
* own palette, never one baked in here — moved through with the arrow keys.
|
|
10
|
+
*
|
|
11
|
+
* The colours are DATA (what the record holds), so they are drawn from the value and not
|
|
12
|
+
* from the tokens; everything around them is the system's.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ColorInput({ id: ownId, value, onChange, swatches, placeholder, describedBy: ownDescribedBy, invalid: ownInvalid, disabled, mode, "aria-label": ariaLabel, }: ControlProps & {
|
|
15
|
+
/** «#RRGGBB»; `""` is no colour. */
|
|
16
|
+
value: string;
|
|
17
|
+
onChange?: (hex: string) => void;
|
|
18
|
+
/** The application's palette, offered above the field. */
|
|
19
|
+
swatches?: readonly string[];
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { useFieldIds, useFormMode } from "../../foundations/form.js";
|
|
5
|
+
import { FieldGroup } from "./aria.js";
|
|
6
|
+
import { nameOf, ReadValue } from "./Field.js";
|
|
7
|
+
import { ColorField, ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem, Input, parseColor, } from "react-aria-components";
|
|
8
|
+
/** A hex the color parser takes, or `null`. */
|
|
9
|
+
function colorOf(value) {
|
|
10
|
+
try {
|
|
11
|
+
return value.trim() === "" ? null : parseColor(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const SWATCH = "size-(--size-icon-md) shrink-0 rounded-mark border border-line-strong";
|
|
18
|
+
/**
|
|
19
|
+
* A color: the label of a tag, the color of a stage, a brand color — as a hex «#1F6FEB».
|
|
20
|
+
*
|
|
21
|
+
* Context: it is React Aria's colour set. `ColorField` takes the hex typed or pasted (with or
|
|
22
|
+
* without `#`) and reads it back normalised; `ColorSwatch` draws it with the colour's NAME
|
|
23
|
+
* as its accessible name («azul oscuro vívido»), so a screen reader hears a colour and not
|
|
24
|
+
* six hex digits; `ColorSwatchPicker` is the row of `swatches` the application offers — its
|
|
25
|
+
* own palette, never one baked in here — moved through with the arrow keys.
|
|
26
|
+
*
|
|
27
|
+
* The colours are DATA (what the record holds), so they are drawn from the value and not
|
|
28
|
+
* from the tokens; everything around them is the system's.
|
|
29
|
+
*/
|
|
30
|
+
export function ColorInput({ id: ownId, value, onChange, swatches, placeholder = "#RRGGBB", describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, mode, "aria-label": ariaLabel, }) {
|
|
31
|
+
const copy = useUiCopy();
|
|
32
|
+
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
33
|
+
id: ownId,
|
|
34
|
+
describedBy: ownDescribedBy,
|
|
35
|
+
invalid: ownInvalid,
|
|
36
|
+
});
|
|
37
|
+
const color = colorOf(value);
|
|
38
|
+
if (useFormMode(mode) === "read") {
|
|
39
|
+
return (_jsx(ReadValue, { mono: true, value: color === null ? null : (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(ColorSwatch, { color: color, className: SWATCH }), color.toString("hex")] })) }));
|
|
40
|
+
}
|
|
41
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 flex-col gap-1.5", children: [swatches === undefined || swatches.length === 0 ? null : (_jsx(ColorSwatchPicker, { "aria-label": copy.field.palette, ...(color === null ? {} : { value: color }), onChange: (next) => onChange?.(next.toString("hex")), className: "flex flex-wrap gap-1", children: swatches.map((one) => (_jsx(ColorSwatchPickerItem, { color: one, isDisabled: disabled, className: "rounded-mark p-0.5 outline-0 data-focus-visible:outline-2 data-focus-visible:outline-primary data-selected:ring-2 data-selected:ring-primary", children: _jsx(ColorSwatch, { className: SWATCH }) }, one))) })), _jsx(ColorField, { id: id, "aria-labelledby": labelledBy, "aria-label": nameOf(labelledBy, ariaLabel, placeholder), value: color, onChange: (next) => onChange?.(next === null ? "" : next.toString("hex")), isInvalid: invalid, isDisabled: disabled, className: "w-full min-w-0", children: _jsxs(FieldGroup, { isInvalid: invalid, isDisabled: disabled, children: [color === null ? (_jsx("span", { "aria-hidden": true, className: `ml-2 ${SWATCH} bg-ground` })) : (_jsx(ColorSwatch, { color: color, className: `ml-2 ${SWATCH}` })), _jsx(Input, { "aria-describedby": describedBy, placeholder: placeholder, spellCheck: false, className: "h-full min-w-0 flex-1 bg-transparent px-2 font-mono text-meta text-ink uppercase outline-0 placeholder:text-muted disabled:cursor-not-allowed" })] }) })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ControlProps } from "./Field.js";
|
|
2
|
+
type AddressProps = ControlProps & {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange?: (next: string) => void;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
};
|
|
7
|
+
/** An email: the email keyboard and autofill; in read mode, a `mailto:` link. */
|
|
8
|
+
export declare function EmailInput({ mode, ...props }: AddressProps): import("react").JSX.Element;
|
|
9
|
+
/** A phone as people write it — «+591 7 123-4567», kept as typed. The phone keyboard and
|
|
10
|
+
* autofill; in read mode a `tel:` link, and with `whatsapp` a second link to write there. */
|
|
11
|
+
export declare function PhoneInput({ mode, whatsapp, ...props }: AddressProps & {
|
|
12
|
+
/** Also offer WhatsApp in read mode — the number needs its country code. */
|
|
13
|
+
whatsapp?: boolean;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
15
|
+
/** A website: the URL keyboard; in read mode a link that opens in a new tab, read without
|
|
16
|
+
* its `https://`. A bare «sincpro.com.bo» is kept as typed and linked with `https://`. */
|
|
17
|
+
export declare function UrlInput({ mode, ...props }: AddressProps): import("react").JSX.Element;
|
|
18
|
+
export {};
|