@eduardoalvarez/arrecife 0.3.0 → 0.5.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 (41) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +121 -1
  3. package/dist/brand/index.cjs +29 -10
  4. package/dist/brand/index.d.cts +9 -1
  5. package/dist/brand/index.d.ts +9 -1
  6. package/dist/brand/index.js +4 -2
  7. package/dist/chart/index.cjs +287 -0
  8. package/dist/chart/index.d.cts +127 -0
  9. package/dist/chart/index.d.ts +127 -0
  10. package/dist/chart/index.js +103 -0
  11. package/dist/chunk-KPZNNMV5.js +83 -0
  12. package/dist/chunk-MLKGABMK.js +7 -0
  13. package/dist/chunk-NHS7ETKJ.js +27 -0
  14. package/dist/{chunk-I7GYZD2A.js → chunk-TSPJOM6K.js} +5 -7
  15. package/dist/chunk-UOWIDFCB.js +81 -0
  16. package/dist/{chunk-VSMW2CT4.js → chunk-VPT32GPG.js} +2 -1
  17. package/dist/{chunk-ND45OQOC.js → chunk-YZ2SDOVZ.js} +29 -35
  18. package/dist/chunk-ZEOQKRQ7.js +19 -0
  19. package/dist/form/index.cjs +301 -0
  20. package/dist/form/index.d.cts +86 -0
  21. package/dist/form/index.d.ts +86 -0
  22. package/dist/form/index.js +84 -0
  23. package/dist/index.cjs +1094 -293
  24. package/dist/index.d.cts +452 -37
  25. package/dist/index.d.ts +452 -37
  26. package/dist/index.js +963 -373
  27. package/dist/label-DuTvJGxD.d.cts +14 -0
  28. package/dist/label-DuTvJGxD.d.ts +14 -0
  29. package/dist/og/index.js +2 -1
  30. package/dist/shiki/index.js +2 -1
  31. package/dist/tema/index.cjs +94 -0
  32. package/dist/tema/index.d.cts +110 -0
  33. package/dist/tema/index.d.ts +110 -0
  34. package/dist/tema/index.js +2 -0
  35. package/dist/tokens/index.cjs +6 -0
  36. package/dist/tokens/index.d.cts +33 -1
  37. package/dist/tokens/index.d.ts +33 -1
  38. package/dist/tokens/index.js +3 -2
  39. package/dist/tokens/theme.css +43 -0
  40. package/llms.txt +351 -44
  41. package/package.json +31 -2
@@ -1,30 +1,8 @@
1
1
  import { aletas, RUTA_ASSETS, poses, caras } from './chunk-E3OMP2DL.js';
2
- import { spacing, control, radius, typeScale, naming } from './chunk-I7GYZD2A.js';
3
- import { clsx } from 'clsx';
4
- import { extendTailwindMerge } from 'tailwind-merge';
2
+ import { cn } from './chunk-NHS7ETKJ.js';
3
+ import { naming, tagline } from './chunk-TSPJOM6K.js';
5
4
  import { jsx, jsxs } from 'react/jsx-runtime';
6
5
 
7
- var kebab = (nombre) => nombre.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
8
- var escalas = Object.keys(typeScale);
9
- var radios = Object.keys(radius);
10
- var espacios = Object.keys(spacing).map(kebab);
11
- var controles = Object.keys(control).map((nombre) => `control-${kebab(nombre)}`);
12
- var twMerge = extendTailwindMerge({
13
- extend: {
14
- classGroups: {
15
- "font-size": [{ text: escalas }],
16
- rounded: [{ rounded: radios }],
17
- p: [{ p: espacios }],
18
- px: [{ px: [...espacios, ...controles] }],
19
- py: [{ py: espacios }],
20
- gap: [{ gap: espacios }],
21
- size: [{ size: controles }]
22
- }
23
- }
24
- });
25
- function cn(...inputs) {
26
- return twMerge(clsx(inputs));
27
- }
28
6
  function Isotipo({
29
7
  sobre = "oscuro",
30
8
  basePath = RUTA_ASSETS,
@@ -49,20 +27,36 @@ function Logo({
49
27
  sobre = "oscuro",
50
28
  basePath = RUTA_ASSETS,
51
29
  soloIsotipo = false,
30
+ conLema = false,
52
31
  className,
53
32
  ...props
54
33
  }) {
34
+ const conAleta = /* @__PURE__ */ jsx(
35
+ Isotipo,
36
+ {
37
+ sobre,
38
+ basePath,
39
+ alt: soloIsotipo ? naming.wordmark : "",
40
+ className: conLema ? "h-9 w-auto" : "h-7 w-auto"
41
+ }
42
+ );
43
+ const wordmark = /* @__PURE__ */ jsx("span", { className: "text-ui font-display text-text-primary font-bold tracking-[-0.02em]", children: naming.wordmark });
44
+ if (soloIsotipo) {
45
+ return /* @__PURE__ */ jsx("span", { className: cn("gap-step-xs inline-flex items-center", className), ...props, children: conAleta });
46
+ }
47
+ if (conLema) {
48
+ return /* @__PURE__ */ jsxs("span", { className: cn("gap-step-sm inline-flex items-center", className), ...props, children: [
49
+ conAleta,
50
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "bg-hairline h-8 w-px shrink-0" }),
51
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col", children: [
52
+ wordmark,
53
+ /* @__PURE__ */ jsx("span", { className: "text-eyebrow font-mono text-text-muted uppercase", children: tagline.corto })
54
+ ] })
55
+ ] });
56
+ }
55
57
  return /* @__PURE__ */ jsxs("span", { className: cn("gap-step-xs inline-flex items-center", className), ...props, children: [
56
- /* @__PURE__ */ jsx(
57
- Isotipo,
58
- {
59
- sobre,
60
- basePath,
61
- alt: soloIsotipo ? naming.wordmark : "",
62
- className: "h-7 w-auto"
63
- }
64
- ),
65
- soloIsotipo ? null : /* @__PURE__ */ jsx("span", { className: "text-ui font-display text-text-primary font-bold tracking-[-0.02em]", children: naming.wordmark })
58
+ conAleta,
59
+ wordmark
66
60
  ] });
67
61
  }
68
62
  function Mascota({
@@ -100,4 +94,4 @@ function CaraDeMascota({
100
94
  );
101
95
  }
102
96
 
103
- export { CaraDeMascota, Isotipo, Logo, Mascota, cn };
97
+ export { CaraDeMascota, Isotipo, Logo, Mascota };
@@ -0,0 +1,19 @@
1
+ import { cn } from './chunk-NHS7ETKJ.js';
2
+ import * as LabelPrimitive from '@radix-ui/react-label';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ function Label({ className, ...props }) {
6
+ return /* @__PURE__ */ jsx(
7
+ LabelPrimitive.Root,
8
+ {
9
+ className: cn(
10
+ "text-label font-sans text-text-secondary select-none",
11
+ "peer-disabled:opacity-50 group-data-[disabled]:opacity-50",
12
+ className
13
+ ),
14
+ ...props
15
+ }
16
+ );
17
+ }
18
+
19
+ export { Label };
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactHookForm = require('react-hook-form');
5
+ var reactSlot = require('@radix-ui/react-slot');
6
+ var clsx = require('clsx');
7
+ var tailwindMerge = require('tailwind-merge');
8
+ var LabelPrimitive = require('@radix-ui/react-label');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+ var classVarianceAuthority = require('class-variance-authority');
11
+
12
+ function _interopNamespace(e) {
13
+ if (e && e.__esModule) return e;
14
+ var n = Object.create(null);
15
+ if (e) {
16
+ Object.keys(e).forEach(function (k) {
17
+ if (k !== 'default') {
18
+ var d = Object.getOwnPropertyDescriptor(e, k);
19
+ Object.defineProperty(n, k, d.get ? d : {
20
+ enumerable: true,
21
+ get: function () { return e[k]; }
22
+ });
23
+ }
24
+ });
25
+ }
26
+ n.default = e;
27
+ return Object.freeze(n);
28
+ }
29
+
30
+ var LabelPrimitive__namespace = /*#__PURE__*/_interopNamespace(LabelPrimitive);
31
+
32
+ // src/form/index.tsx
33
+ var typeScale = {
34
+ display: { family: "display", size: 76, lineHeight: 0.96, weight: 800, tracking: "-0.035em" },
35
+ /** Métricas grandes. Números, no prosa: interlineado 1 y sin descendentes. */
36
+ stat: { family: "display", size: 46, lineHeight: 1, weight: 800, tracking: "-0.035em" },
37
+ h1: { family: "display", size: 44, lineHeight: 1.05, weight: 700, tracking: "-0.03em" },
38
+ h2: { family: "display", size: 30, lineHeight: 1.1, weight: 600, tracking: "-0.02em" },
39
+ h3: { family: "display", size: 25, lineHeight: 1.15, weight: 600, tracking: "-0.02em" },
40
+ body: { family: "sans", size: 18, lineHeight: 1.75, weight: 400 },
41
+ /**
42
+ * 17px. El botón grande y la bajada de las páginas internas.
43
+ *
44
+ * Entra como escalón propio porque el documento lo usa DOS veces y en piezas
45
+ * distintas: «lg 15px 30px r12» con texto de 17, y «h1 44/700 · bajada 17px ·
46
+ * párrafo de contexto 15px». Un solo uso no habría justificado partir la
47
+ * distancia entre `ui` (15) y `body` (18); dos sí.
48
+ */
49
+ lead: { family: "sans", size: 17, lineHeight: 1.5, weight: 400 },
50
+ ui: { family: "sans", size: 15, lineHeight: 1.6, weight: 400 },
51
+ label: { family: "sans", size: 13, lineHeight: 1.5, weight: 500 },
52
+ /**
53
+ * La escala de las etiquetas de estado: sans 12.5/500, cuadrada.
54
+ *
55
+ * Es medio píxel por debajo de `label`, y esta vez el medio píxel sí importa:
56
+ * una etiqueta de estado va dentro de una tabla o al lado de un título, y a 13
57
+ * competía con el texto que acompaña. El documento la da en 12.5 y se ve.
58
+ */
59
+ tag: { family: "sans", size: 12.5, lineHeight: 1.4, weight: 500 },
60
+ /**
61
+ * La escala de categoría y métrica: mono 11.5.
62
+ *
63
+ * Rompe el suelo de `limits.minScreenPx`, y es a propósito. Ese suelo protege
64
+ * al TEXTO —lo que se lee en una frase—, y una píldora de una palabra no es
65
+ * texto corrido: es una marca. El contraste sigue medido y pasa AA (plancton
66
+ * 5.57:1 sobre abismo), que es la parte que no se negocia.
67
+ *
68
+ * A 13 las tres familias de etiqueta salían del tamaño de un botón pequeño y
69
+ * pesaban más que el título que acompañan. Ver `docs/decisiones.md`.
70
+ */
71
+ chip: { family: "mono", size: 11.5, lineHeight: 1.4, weight: 400 },
72
+ /**
73
+ * Mono SIN transformar: fechas, rutas, versiones, nombres de archivo, la firma
74
+ * del footer y las métricas de etiqueta. Es la mayoría del mono del sistema.
75
+ *
76
+ * Existe porque `eyebrow` lleva `uppercase` de fábrica y no es una preferencia
77
+ * que se pueda apagar: es la escala del eyebrow. Un `18 ago 2026` en versalitas
78
+ * o un `pose-laptop-coffee.png` en mayúsculas están mal, y sin este escalón la
79
+ * única salida era un `normal-case` en cada sitio de uso.
80
+ *
81
+ * El documento dice 12.5. Son 13 por la misma razón por la que `textMuted` se
82
+ * corrigió: `limits.minScreenPx` es 13 y plancton «nunca bajo 13px». Esta
83
+ * escala es justo donde se escriben las metas en muted, así que ponerla en
84
+ * 12.5 habría dejado ilegible su uso más común por medio píxel.
85
+ */
86
+ meta: { family: "mono", size: 13, lineHeight: 1.6, weight: 400 },
87
+ eyebrow: { family: "mono", size: 12, tracking: "0.12em", transform: "uppercase" }
88
+ };
89
+ var radius = {
90
+ chip: 6,
91
+ control: 10,
92
+ card: 14,
93
+ panel: 16,
94
+ pill: 999
95
+ };
96
+ var control = {
97
+ sm: 14,
98
+ md: 22,
99
+ lg: 30,
100
+ /** Botón de icono: cuadrado, sin texto. */
101
+ icon: 42
102
+ };
103
+ var spacing = {
104
+ stepXs: 8,
105
+ stepSm: 12,
106
+ stepMd: 16,
107
+ stepLg: 26,
108
+ stepXl: 40,
109
+ section: 96
110
+ };
111
+
112
+ // src/lib/cn.ts
113
+ var kebab = (nombre) => nombre.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
114
+ var escalas = Object.keys(typeScale);
115
+ var radios = Object.keys(radius);
116
+ var espacios = Object.keys(spacing).map(kebab);
117
+ var controles = Object.keys(control).map((nombre) => `control-${kebab(nombre)}`);
118
+ var twMerge = tailwindMerge.extendTailwindMerge({
119
+ extend: {
120
+ classGroups: {
121
+ "font-size": [{ text: escalas }],
122
+ rounded: [{ rounded: radios }],
123
+ p: [{ p: espacios }],
124
+ px: [{ px: [...espacios, ...controles] }],
125
+ py: [{ py: espacios }],
126
+ gap: [{ gap: espacios }],
127
+ size: [{ size: controles }]
128
+ }
129
+ }
130
+ });
131
+ function cn(...inputs) {
132
+ return twMerge(clsx.clsx(inputs));
133
+ }
134
+ function Label({ className, ...props }) {
135
+ return /* @__PURE__ */ jsxRuntime.jsx(
136
+ LabelPrimitive__namespace.Root,
137
+ {
138
+ className: cn(
139
+ "text-label font-sans text-text-secondary select-none",
140
+ "peer-disabled:opacity-50 group-data-[disabled]:opacity-50",
141
+ className
142
+ ),
143
+ ...props
144
+ }
145
+ );
146
+ }
147
+ var ETIQUETA = {
148
+ display: "h1",
149
+ stat: "p",
150
+ h1: "h1",
151
+ h2: "h2",
152
+ h3: "h3",
153
+ body: "p",
154
+ lead: "p",
155
+ ui: "p",
156
+ label: "span",
157
+ tag: "span",
158
+ meta: "p",
159
+ chip: "span",
160
+ eyebrow: "p"
161
+ };
162
+ var texto = classVarianceAuthority.cva("", {
163
+ variants: {
164
+ variant: {
165
+ display: "text-display font-display",
166
+ // Métricas grandes: 46px de display. Es la única escala pensada para un
167
+ // número, y por eso el interlineado es 1.
168
+ stat: "text-stat font-display",
169
+ h1: "text-h1 font-display",
170
+ h2: "text-h2 font-display",
171
+ h3: "text-h3 font-display",
172
+ body: "text-body font-sans",
173
+ // 17px: la bajada de las páginas internas y el botón grande.
174
+ lead: "text-lead font-sans",
175
+ ui: "text-ui font-sans",
176
+ label: "text-label font-sans",
177
+ // Las dos escalas de etiqueta. `tag` es el estado (sans 12.5/500) y `chip`
178
+ // la categoría y la métrica (mono 11.5). Ninguna transforma el texto.
179
+ tag: "text-tag font-sans",
180
+ chip: "text-chip font-mono",
181
+ // Mono SIN transformar: fechas, rutas, versiones, nombres de archivo y la
182
+ // firma del footer. Es el escalón que faltaba, y por el que cada sitio de
183
+ // uso terminaba escribiendo `variant="eyebrow" className="normal-case"`.
184
+ meta: "text-meta font-mono",
185
+ // `uppercase` no cabe en un token de tamaño: text-transform no es un
186
+ // modificador de --text-*. Por eso lo pone la variante y no theme.css.
187
+ eyebrow: "text-eyebrow font-mono uppercase"
188
+ },
189
+ tone: {
190
+ primary: "text-text-primary",
191
+ secondary: "text-text-secondary",
192
+ muted: "text-text-muted",
193
+ accent: "text-accent",
194
+ warm: "text-warm",
195
+ success: "text-success",
196
+ warning: "text-warning",
197
+ error: "text-error"
198
+ }
199
+ },
200
+ defaultVariants: { variant: "body", tone: "primary" }
201
+ });
202
+ function Text({
203
+ className,
204
+ variant,
205
+ tone,
206
+ as,
207
+ asChild = false,
208
+ measure,
209
+ children,
210
+ ...props
211
+ }) {
212
+ const escala = variant ?? "body";
213
+ const limitar = measure ?? escala === "body";
214
+ const clases = cn(texto({ variant, tone }), limitar && "max-w-measure", className);
215
+ if (asChild) {
216
+ return /* @__PURE__ */ jsxRuntime.jsx(reactSlot.Slot, { className: clases, ...props, children });
217
+ }
218
+ const Etiqueta = as ?? ETIQUETA[escala];
219
+ return /* @__PURE__ */ jsxRuntime.jsx(Etiqueta, { className: clases, ...props, children });
220
+ }
221
+ var Form = reactHookForm.FormProvider;
222
+ var Campo = react.createContext(null);
223
+ var Item = react.createContext(null);
224
+ function FormField(props) {
225
+ return /* @__PURE__ */ jsxRuntime.jsx(Campo.Provider, { value: { name: props.name }, children: /* @__PURE__ */ jsxRuntime.jsx(reactHookForm.Controller, { ...props }) });
226
+ }
227
+ function useFormField() {
228
+ const campo = react.useContext(Campo);
229
+ const item = react.useContext(Item);
230
+ const { getFieldState } = reactHookForm.useFormContext();
231
+ const estado = reactHookForm.useFormState({ name: campo?.name ?? "" });
232
+ if (!campo) throw new Error("useFormField se usa dentro de un <FormField>.");
233
+ if (!item) throw new Error("useFormField se usa dentro de un <FormItem>.");
234
+ return {
235
+ name: campo.name,
236
+ id: item.id,
237
+ idDescripcion: `${item.id}-descripcion`,
238
+ idMensaje: `${item.id}-mensaje`,
239
+ ...getFieldState(campo.name, estado)
240
+ };
241
+ }
242
+ function FormItem({ className, ...props }) {
243
+ const id = react.useId();
244
+ return /* @__PURE__ */ jsxRuntime.jsx(Item.Provider, { value: { id }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("gap-step-xs flex flex-col", className), ...props }) });
245
+ }
246
+ function FormLabel({ className, ...props }) {
247
+ const { id } = useFormField();
248
+ return /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: id, className, ...props });
249
+ }
250
+ function FormControl({ ...props }) {
251
+ const { error, id, idDescripcion, idMensaje } = useFormField();
252
+ return /* @__PURE__ */ jsxRuntime.jsx(
253
+ reactSlot.Slot,
254
+ {
255
+ id,
256
+ "aria-describedby": error ? `${idDescripcion} ${idMensaje}` : idDescripcion,
257
+ "aria-invalid": error ? true : void 0,
258
+ ...props
259
+ }
260
+ );
261
+ }
262
+ function FormDescription({ className, ...props }) {
263
+ const { idDescripcion } = useFormField();
264
+ return /* @__PURE__ */ jsxRuntime.jsx(
265
+ Text,
266
+ {
267
+ as: "p",
268
+ variant: "label",
269
+ tone: "muted",
270
+ id: idDescripcion,
271
+ className: cn("font-normal", className),
272
+ ...props
273
+ }
274
+ );
275
+ }
276
+ function FormMessage({ className, children, ...props }) {
277
+ const { error, idMensaje } = useFormField();
278
+ const cuerpo = error?.message ? String(error.message) : children;
279
+ if (!cuerpo) return null;
280
+ return /* @__PURE__ */ jsxRuntime.jsx(
281
+ Text,
282
+ {
283
+ as: "p",
284
+ variant: "label",
285
+ tone: "error",
286
+ id: idMensaje,
287
+ className,
288
+ ...props,
289
+ children: cuerpo
290
+ }
291
+ );
292
+ }
293
+
294
+ exports.Form = Form;
295
+ exports.FormControl = FormControl;
296
+ exports.FormDescription = FormDescription;
297
+ exports.FormField = FormField;
298
+ exports.FormItem = FormItem;
299
+ exports.FormLabel = FormLabel;
300
+ exports.FormMessage = FormMessage;
301
+ exports.useFormField = useFormField;
@@ -0,0 +1,86 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef } from 'react';
3
+ import * as react_hook_form from 'react-hook-form';
4
+ import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
5
+ import { Slot } from '@radix-ui/react-slot';
6
+ import { L as Label } from '../label-DuTvJGxD.cjs';
7
+ import '@radix-ui/react-label';
8
+
9
+ /**
10
+ * La capa que ata los controles a un formulario con validación y mensajes.
11
+ *
12
+ * La librería ya traía `Input`, `Label`, `Checkbox`, `RadioGroup`, `Select` y
13
+ * `Textarea`, y ninguno sabía nada del otro: el `htmlFor`, el `aria-describedby`
14
+ * del mensaje de error y el `aria-invalid` había que cablearlos a mano en cada
15
+ * campo de cada proyecto. Eso se olvida, y cuando se olvida el fallo es que un
16
+ * lector de pantalla no anuncia por qué el campo está en rojo.
17
+ *
18
+ * Se publica en `@eduardoalvarez/arrecife/form`, NO en la raíz, y eso es
19
+ * deliberado. React Hook Form es una dependencia de pares opcional: solo uno de
20
+ * los cinco proyectos la usa, y si esto colgara del índice principal, los otros
21
+ * cuatro tendrían que instalarla para que su bundler resolviera un import que
22
+ * nunca ejecutan. Es la misma razón por la que `./og` y `./shiki` viven aparte,
23
+ * mirada desde el otro lado: allí se saca React del camino, aquí se saca RHF.
24
+ *
25
+ * La forma es la de shadcn —`FormField` sobre `Controller`, contexto de campo y
26
+ * contexto de item— porque el proyecto que la consume ya está escrito contra
27
+ * ella y reinventarla solo le costaría una migración. Lo que cambia es el
28
+ * vocabulario visual: los ids, la escala y el tono salen del sistema.
29
+ */
30
+ declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>({ children, watch, getValues, getErrors, getFieldState, setError, clearErrors, setValue, setValues, trigger, formState, resetField, reset, resetDefaultValues, handleSubmit, unregister, control, register, setFocus, subscribe, }: react_hook_form.FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React.JSX.Element;
31
+ /**
32
+ * Un campo controlado. Envuelve el `Controller` de RHF y además publica el
33
+ * nombre en contexto, que es de donde lo leen la etiqueta y el mensaje sin que
34
+ * haya que repetirlo tres veces.
35
+ */
36
+ declare function FormField<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>(props: ControllerProps<TFieldValues, TName>): react.JSX.Element;
37
+ /**
38
+ * Lo que necesita cualquier pieza del campo: el nombre, los tres ids y el estado
39
+ * de validación.
40
+ *
41
+ * Tira si se usa fuera de un `FormField` o de un `FormItem`, con el mensaje que
42
+ * dice cuál de los dos falta. Devolver algo a medias sería peor: el campo se
43
+ * renderizaría sin `aria-describedby` y nada avisaría.
44
+ */
45
+ declare function useFormField(): {
46
+ invalid: boolean;
47
+ isDirty: boolean;
48
+ isTouched: boolean;
49
+ isValidating: boolean;
50
+ error?: react_hook_form.FieldError;
51
+ name: string;
52
+ id: string;
53
+ idDescripcion: string;
54
+ idMensaje: string;
55
+ };
56
+ /** La caja del campo: etiqueta, control, ayuda y mensaje, en columna. */
57
+ declare function FormItem({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
58
+ /**
59
+ * La etiqueta NO se tiñe de rojo cuando el campo falla.
60
+ *
61
+ * El sistema deja el color semántico en el borde y en el glifo, y el texto en un
62
+ * token de texto: el borde del control ya está en `error` y el mensaje de abajo
63
+ * también, así que teñir además la etiqueta son tres rojos para un solo fallo.
64
+ */
65
+ declare function FormLabel({ className, ...props }: ComponentPropsWithoutRef<typeof Label>): react.JSX.Element;
66
+ /**
67
+ * Envuelve al control y le cablea los atributos: el `id` que la etiqueta apunta,
68
+ * el `aria-describedby` con la ayuda y el mensaje, y el `aria-invalid`.
69
+ *
70
+ * Es un `Slot`, así que el hijo puede ser cualquiera de los controles del
71
+ * sistema —`Input`, `Textarea`, `SelectTrigger`— sin que esto sepa cuál.
72
+ */
73
+ declare function FormControl({ ...props }: ComponentPropsWithoutRef<typeof Slot>): react.JSX.Element;
74
+ /** La ayuda del campo. Se anuncia siempre, haya error o no. */
75
+ declare function FormDescription({ className, ...props }: ComponentPropsWithoutRef<'p'>): react.JSX.Element;
76
+ /**
77
+ * El mensaje de validación. Sin error no renderiza nada: un hueco reservado
78
+ * para el fallo desplaza el resto del formulario cada vez que aparece.
79
+ *
80
+ * Toma el texto del error de RHF; `children` sirve para un mensaje que no venga
81
+ * del esquema —el 409 que devuelve el servidor y que ningún validador de
82
+ * cliente puede anticipar—.
83
+ */
84
+ declare function FormMessage({ className, children, ...props }: ComponentPropsWithoutRef<'p'>): react.JSX.Element | null;
85
+
86
+ export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
@@ -0,0 +1,86 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef } from 'react';
3
+ import * as react_hook_form from 'react-hook-form';
4
+ import { FieldValues, FieldPath, ControllerProps } from 'react-hook-form';
5
+ import { Slot } from '@radix-ui/react-slot';
6
+ import { L as Label } from '../label-DuTvJGxD.js';
7
+ import '@radix-ui/react-label';
8
+
9
+ /**
10
+ * La capa que ata los controles a un formulario con validación y mensajes.
11
+ *
12
+ * La librería ya traía `Input`, `Label`, `Checkbox`, `RadioGroup`, `Select` y
13
+ * `Textarea`, y ninguno sabía nada del otro: el `htmlFor`, el `aria-describedby`
14
+ * del mensaje de error y el `aria-invalid` había que cablearlos a mano en cada
15
+ * campo de cada proyecto. Eso se olvida, y cuando se olvida el fallo es que un
16
+ * lector de pantalla no anuncia por qué el campo está en rojo.
17
+ *
18
+ * Se publica en `@eduardoalvarez/arrecife/form`, NO en la raíz, y eso es
19
+ * deliberado. React Hook Form es una dependencia de pares opcional: solo uno de
20
+ * los cinco proyectos la usa, y si esto colgara del índice principal, los otros
21
+ * cuatro tendrían que instalarla para que su bundler resolviera un import que
22
+ * nunca ejecutan. Es la misma razón por la que `./og` y `./shiki` viven aparte,
23
+ * mirada desde el otro lado: allí se saca React del camino, aquí se saca RHF.
24
+ *
25
+ * La forma es la de shadcn —`FormField` sobre `Controller`, contexto de campo y
26
+ * contexto de item— porque el proyecto que la consume ya está escrito contra
27
+ * ella y reinventarla solo le costaría una migración. Lo que cambia es el
28
+ * vocabulario visual: los ids, la escala y el tono salen del sistema.
29
+ */
30
+ declare const Form: <TFieldValues extends FieldValues, TContext = any, TTransformedValues = TFieldValues>({ children, watch, getValues, getErrors, getFieldState, setError, clearErrors, setValue, setValues, trigger, formState, resetField, reset, resetDefaultValues, handleSubmit, unregister, control, register, setFocus, subscribe, }: react_hook_form.FormProviderProps<TFieldValues, TContext, TTransformedValues>) => React.JSX.Element;
31
+ /**
32
+ * Un campo controlado. Envuelve el `Controller` de RHF y además publica el
33
+ * nombre en contexto, que es de donde lo leen la etiqueta y el mensaje sin que
34
+ * haya que repetirlo tres veces.
35
+ */
36
+ declare function FormField<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>(props: ControllerProps<TFieldValues, TName>): react.JSX.Element;
37
+ /**
38
+ * Lo que necesita cualquier pieza del campo: el nombre, los tres ids y el estado
39
+ * de validación.
40
+ *
41
+ * Tira si se usa fuera de un `FormField` o de un `FormItem`, con el mensaje que
42
+ * dice cuál de los dos falta. Devolver algo a medias sería peor: el campo se
43
+ * renderizaría sin `aria-describedby` y nada avisaría.
44
+ */
45
+ declare function useFormField(): {
46
+ invalid: boolean;
47
+ isDirty: boolean;
48
+ isTouched: boolean;
49
+ isValidating: boolean;
50
+ error?: react_hook_form.FieldError;
51
+ name: string;
52
+ id: string;
53
+ idDescripcion: string;
54
+ idMensaje: string;
55
+ };
56
+ /** La caja del campo: etiqueta, control, ayuda y mensaje, en columna. */
57
+ declare function FormItem({ className, ...props }: ComponentPropsWithoutRef<'div'>): react.JSX.Element;
58
+ /**
59
+ * La etiqueta NO se tiñe de rojo cuando el campo falla.
60
+ *
61
+ * El sistema deja el color semántico en el borde y en el glifo, y el texto en un
62
+ * token de texto: el borde del control ya está en `error` y el mensaje de abajo
63
+ * también, así que teñir además la etiqueta son tres rojos para un solo fallo.
64
+ */
65
+ declare function FormLabel({ className, ...props }: ComponentPropsWithoutRef<typeof Label>): react.JSX.Element;
66
+ /**
67
+ * Envuelve al control y le cablea los atributos: el `id` que la etiqueta apunta,
68
+ * el `aria-describedby` con la ayuda y el mensaje, y el `aria-invalid`.
69
+ *
70
+ * Es un `Slot`, así que el hijo puede ser cualquiera de los controles del
71
+ * sistema —`Input`, `Textarea`, `SelectTrigger`— sin que esto sepa cuál.
72
+ */
73
+ declare function FormControl({ ...props }: ComponentPropsWithoutRef<typeof Slot>): react.JSX.Element;
74
+ /** La ayuda del campo. Se anuncia siempre, haya error o no. */
75
+ declare function FormDescription({ className, ...props }: ComponentPropsWithoutRef<'p'>): react.JSX.Element;
76
+ /**
77
+ * El mensaje de validación. Sin error no renderiza nada: un hueco reservado
78
+ * para el fallo desplaza el resto del formulario cada vez que aparece.
79
+ *
80
+ * Toma el texto del error de RHF; `children` sirve para un mensaje que no venga
81
+ * del esquema —el 409 que devuelve el servidor y que ningún validador de
82
+ * cliente puede anticipar—.
83
+ */
84
+ declare function FormMessage({ className, children, ...props }: ComponentPropsWithoutRef<'p'>): react.JSX.Element | null;
85
+
86
+ export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
@@ -0,0 +1,84 @@
1
+ import { Label } from '../chunk-ZEOQKRQ7.js';
2
+ import { Text } from '../chunk-UOWIDFCB.js';
3
+ import { cn } from '../chunk-NHS7ETKJ.js';
4
+ import '../chunk-TSPJOM6K.js';
5
+ import '../chunk-MLKGABMK.js';
6
+ import { createContext, useContext, useId } from 'react';
7
+ import { FormProvider, Controller, useFormContext, useFormState } from 'react-hook-form';
8
+ import { Slot } from '@radix-ui/react-slot';
9
+ import { jsx } from 'react/jsx-runtime';
10
+
11
+ var Form = FormProvider;
12
+ var Campo = createContext(null);
13
+ var Item = createContext(null);
14
+ function FormField(props) {
15
+ return /* @__PURE__ */ jsx(Campo.Provider, { value: { name: props.name }, children: /* @__PURE__ */ jsx(Controller, { ...props }) });
16
+ }
17
+ function useFormField() {
18
+ const campo = useContext(Campo);
19
+ const item = useContext(Item);
20
+ const { getFieldState } = useFormContext();
21
+ const estado = useFormState({ name: campo?.name ?? "" });
22
+ if (!campo) throw new Error("useFormField se usa dentro de un <FormField>.");
23
+ if (!item) throw new Error("useFormField se usa dentro de un <FormItem>.");
24
+ return {
25
+ name: campo.name,
26
+ id: item.id,
27
+ idDescripcion: `${item.id}-descripcion`,
28
+ idMensaje: `${item.id}-mensaje`,
29
+ ...getFieldState(campo.name, estado)
30
+ };
31
+ }
32
+ function FormItem({ className, ...props }) {
33
+ const id = useId();
34
+ return /* @__PURE__ */ jsx(Item.Provider, { value: { id }, children: /* @__PURE__ */ jsx("div", { className: cn("gap-step-xs flex flex-col", className), ...props }) });
35
+ }
36
+ function FormLabel({ className, ...props }) {
37
+ const { id } = useFormField();
38
+ return /* @__PURE__ */ jsx(Label, { htmlFor: id, className, ...props });
39
+ }
40
+ function FormControl({ ...props }) {
41
+ const { error, id, idDescripcion, idMensaje } = useFormField();
42
+ return /* @__PURE__ */ jsx(
43
+ Slot,
44
+ {
45
+ id,
46
+ "aria-describedby": error ? `${idDescripcion} ${idMensaje}` : idDescripcion,
47
+ "aria-invalid": error ? true : void 0,
48
+ ...props
49
+ }
50
+ );
51
+ }
52
+ function FormDescription({ className, ...props }) {
53
+ const { idDescripcion } = useFormField();
54
+ return /* @__PURE__ */ jsx(
55
+ Text,
56
+ {
57
+ as: "p",
58
+ variant: "label",
59
+ tone: "muted",
60
+ id: idDescripcion,
61
+ className: cn("font-normal", className),
62
+ ...props
63
+ }
64
+ );
65
+ }
66
+ function FormMessage({ className, children, ...props }) {
67
+ const { error, idMensaje } = useFormField();
68
+ const cuerpo = error?.message ? String(error.message) : children;
69
+ if (!cuerpo) return null;
70
+ return /* @__PURE__ */ jsx(
71
+ Text,
72
+ {
73
+ as: "p",
74
+ variant: "label",
75
+ tone: "error",
76
+ id: idMensaje,
77
+ className,
78
+ ...props,
79
+ children: cuerpo
80
+ }
81
+ );
82
+ }
83
+
84
+ export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };