@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.
Files changed (101) hide show
  1. package/dist/components/attachments/Attachments.d.ts +56 -0
  2. package/dist/components/attachments/Attachments.js +93 -0
  3. package/dist/components/attachments/files.d.ts +28 -0
  4. package/dist/components/attachments/files.js +100 -0
  5. package/dist/components/attachments/index.d.ts +2 -0
  6. package/dist/components/attachments/index.js +3 -0
  7. package/dist/components/flow/StepFlow.d.ts +37 -0
  8. package/dist/components/flow/StepFlow.js +74 -0
  9. package/dist/components/flow/index.d.ts +1 -0
  10. package/dist/components/flow/index.js +2 -0
  11. package/dist/components/index.d.ts +2 -0
  12. package/dist/components/index.js +2 -0
  13. package/dist/components/lines/LineItems.d.ts +14 -3
  14. package/dist/components/lines/LineItems.js +6 -4
  15. package/dist/components/listing/SortMenu.d.ts +24 -20
  16. package/dist/components/listing/SortMenu.js +51 -91
  17. package/dist/components/listing/index.d.ts +2 -1
  18. package/dist/components/listing/index.js +2 -1
  19. package/dist/components/listing/modelCells.d.ts +7 -0
  20. package/dist/components/listing/modelCells.js +56 -0
  21. package/dist/components/listing/modelColumns.d.ts +9 -0
  22. package/dist/components/listing/modelColumns.js +59 -14
  23. package/dist/components/search/index.d.ts +1 -1
  24. package/dist/components/search/index.js +1 -1
  25. package/dist/components/search/model.d.ts +1 -0
  26. package/dist/components/search/model.js +7 -0
  27. package/dist/components/search/query.js +2 -3
  28. package/dist/foundations/copy.d.ts +131 -0
  29. package/dist/foundations/copy.es.js +86 -0
  30. package/dist/foundations/copy.js +86 -0
  31. package/dist/foundations/form.d.ts +18 -1
  32. package/dist/foundations/form.js +6 -3
  33. package/dist/primitives/button/Button.js +3 -1
  34. package/dist/primitives/field/CardNumberInput.d.ts +41 -0
  35. package/dist/primitives/field/CardNumberInput.js +117 -0
  36. package/dist/primitives/field/CodeInput.d.ts +18 -0
  37. package/dist/primitives/field/CodeInput.js +37 -0
  38. package/dist/primitives/field/ColorInput.d.ts +21 -0
  39. package/dist/primitives/field/ColorInput.js +42 -0
  40. package/dist/primitives/field/ContactInputs.d.ts +18 -0
  41. package/dist/primitives/field/ContactInputs.js +70 -0
  42. package/dist/primitives/field/DurationInput.d.ts +20 -0
  43. package/dist/primitives/field/DurationInput.js +54 -0
  44. package/dist/primitives/field/Field.d.ts +24 -4
  45. package/dist/primitives/field/Field.js +17 -4
  46. package/dist/primitives/field/ImageInput.d.ts +51 -0
  47. package/dist/primitives/field/ImageInput.js +267 -0
  48. package/dist/primitives/field/PasswordInput.d.ts +29 -0
  49. package/dist/primitives/field/PasswordInput.js +44 -0
  50. package/dist/primitives/field/PatternInput.d.ts +23 -0
  51. package/dist/primitives/field/PatternInput.js +48 -0
  52. package/dist/primitives/field/RichTextEditor.d.ts +18 -0
  53. package/dist/primitives/field/RichTextEditor.js +224 -0
  54. package/dist/primitives/field/RichTextInput.d.ts +30 -0
  55. package/dist/primitives/field/RichTextInput.js +38 -0
  56. package/dist/primitives/field/SignatureInput.d.ts +34 -0
  57. package/dist/primitives/field/SignatureInput.js +215 -0
  58. package/dist/primitives/field/card.d.ts +33 -0
  59. package/dist/primitives/field/card.js +173 -0
  60. package/dist/primitives/field/clipboard.js +3 -2
  61. package/dist/primitives/field/contact.d.ts +17 -0
  62. package/dist/primitives/field/contact.js +65 -0
  63. package/dist/primitives/field/duration.d.ts +5 -0
  64. package/dist/primitives/field/duration.js +48 -0
  65. package/dist/primitives/field/index.d.ts +14 -0
  66. package/dist/primitives/field/index.js +20 -0
  67. package/dist/primitives/field/mask.d.ts +33 -0
  68. package/dist/primitives/field/mask.js +56 -0
  69. package/dist/primitives/field/media.d.ts +99 -0
  70. package/dist/primitives/field/media.js +136 -0
  71. package/dist/primitives/field/pattern.d.ts +10 -0
  72. package/dist/primitives/field/pattern.js +68 -0
  73. package/dist/primitives/menu/Menu.d.ts +16 -0
  74. package/dist/primitives/menu/Menu.js +26 -8
  75. package/dist/primitives/menu/index.d.ts +1 -1
  76. package/dist/primitives/menu/index.js +1 -1
  77. package/dist/primitives/table/DataTable.d.ts +5 -3
  78. package/dist/primitives/table/DataTable.js +2 -4
  79. package/dist/primitives/table/EditableTable.d.ts +49 -7
  80. package/dist/primitives/table/EditableTable.js +321 -82
  81. package/dist/primitives/table/Table.d.ts +5 -8
  82. package/dist/primitives/table/Table.js +63 -15
  83. package/dist/primitives/table/index.d.ts +3 -2
  84. package/dist/primitives/table/index.js +2 -1
  85. package/dist/primitives/table/rowWindow.d.ts +3 -0
  86. package/dist/primitives/table/rowWindow.js +4 -0
  87. package/dist/primitives/table/sorting.d.ts +42 -0
  88. package/dist/primitives/table/sorting.js +110 -0
  89. package/dist/primitives/value/RichText.d.ts +10 -0
  90. package/dist/primitives/value/RichText.js +130 -0
  91. package/dist/primitives/value/Value.d.ts +20 -2
  92. package/dist/primitives/value/Value.js +26 -7
  93. package/dist/primitives/value/index.d.ts +3 -1
  94. package/dist/primitives/value/index.js +4 -1
  95. package/dist/primitives/value/rounding.d.ts +9 -0
  96. package/dist/primitives/value/rounding.js +48 -0
  97. package/dist/tokens.css +103 -0
  98. package/dist/views/list/List.d.ts +5 -4
  99. package/dist/views/list/ListView.js +2 -2
  100. package/dist/views/list/types.d.ts +6 -6
  101. 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
  };
@@ -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
- /** The ids of the enclosing `Field`, resolved against the control's own props. */
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;
@@ -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
- /** The ids of the enclosing `Field`, resolved against the control's own props. */
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
- 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";
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 {};