@eduardoalvarez/arrecife 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/Proskynete/arrecife/compare/v0.3.0...v0.4.0) (2026-08-31)
4
+
5
+
6
+ ### 🚀 Novedades
7
+
8
+ * **components:** ScrollingProgressBar y las cinco ranuras que faltaban ([52325cc](https://github.com/Proskynete/arrecife/commit/52325ccff7eedbda39d449c98386668df7a518bf))
9
+ * **form:** la capa de formulario y el chasis de gráficas, en su subruta ([4a40857](https://github.com/Proskynete/arrecife/commit/4a40857e9e9efc8bbe4011bf72f6313347c97541))
10
+ * **primitives:** Accordion, AlertDialog y el Toast imperativo ([239120d](https://github.com/Proskynete/arrecife/commit/239120d510ea395159719581ef69c26734d82441))
11
+ * **tema:** el modo claro y oscuro, sin parpadeo y sin React ([c99995c](https://github.com/Proskynete/arrecife/commit/c99995c8eae6313ece6ffdcf927d9120f6050866))
12
+ * **tokens:** la paleta de series de gráfica, cuatro colores por tono ([b660a30](https://github.com/Proskynete/arrecife/commit/b660a30376c794be82f6a11808d9de175a31d5d9))
13
+
14
+
15
+ ### 🐛 Correcciones
16
+
17
+ * **ci:** check:exports no veía React dentro de un chunk ([3354ab1](https://github.com/Proskynete/arrecife/commit/3354ab12feb867a81480e1c3b612280c40f765ad))
18
+
19
+
20
+ ### 📚 Documentación
21
+
22
+ * los dos requisitos de consumo que no dan ningún error ([0a3a7fd](https://github.com/Proskynete/arrecife/commit/0a3a7fd71124aa2dafa3b70df4a90f8fd080544d))
23
+ * main está protegida y así se trabaja con ella ([a625a49](https://github.com/Proskynete/arrecife/commit/a625a499ba871ebf7f13a8f7f0cc9f7fbfff17fc))
24
+ * mergear saltándose la regla se pide, no pasa solo ([c3aca08](https://github.com/Proskynete/arrecife/commit/c3aca08d3926885edea1f500200f0a7496ab13bb))
25
+
3
26
  ## [0.3.0](https://github.com/Proskynete/arrecife/compare/v0.2.0...v0.3.0) (2026-08-31)
4
27
 
5
28
 
package/README.md CHANGED
@@ -64,6 +64,25 @@ mismo objeto `tokens`: la fuente no cambia.
64
64
  El modo oscuro es el primario y es el default. Un proyecto en modo claro declara
65
65
  `data-theme="light"` en `<html>`; uno oscuro no necesita declarar nada.
66
66
 
67
+ **Si además vas a usar componentes, falta una línea, y sin ella no falla nada.**
68
+ Tailwind no escanea `node_modules`, así que purga todas las clases que emiten los
69
+ componentes: `border-hairline`, `rounded-pill` y `p-step-lg` resuelven a la nada.
70
+ No hay error en consola, no hay aviso en el build, no hay clase sin definir — la
71
+ tarjeta simplemente sale con borde `currentColor` y la píldora, cuadrada.
72
+
73
+ ```css
74
+ @import "tailwindcss";
75
+ @import "@eduardoalvarez/arrecife/tokens/theme.css";
76
+
77
+ /* Sin esto, los componentes se montan sin ningún estilo del sistema. */
78
+ @source "../node_modules/@eduardoalvarez/arrecife/dist";
79
+ ```
80
+
81
+ La ruta es relativa al archivo CSS donde va la directiva, así que en un proyecto
82
+ con la hoja en `src/styles/` sube dos niveles y no uno. Lo detectaron los tests
83
+ E2E del blog, no el build, y hasta la 0.3.0 esto solo estaba escrito en
84
+ `llms.txt` — el archivo que lee un agente y no una persona.
85
+
67
86
  > **Vienes de la 0.2.0 o anterior.** Los cinco escalones de espaciado se
68
87
  > renombraron: `p-md` es ahora `p-step-md`, `gap-sm` es `gap-step-sm`. Es un
69
88
  > cambio incompatible, y si tu proyecto usa `max-w-sm`, `max-w-md` o `max-w-lg`,
@@ -101,6 +120,49 @@ solución es declararlo con el nombre que pide el token:
101
120
  }
102
121
  ```
103
122
 
123
+ #### En Next, con `next/font`
124
+
125
+ Es el mismo fallo por otra puerta, y muerde a los dos proyectos Next. `next/font`
126
+ registra cada familia bajo un nombre GENERADO —`__Geist_a1b2c3`— y la expone como
127
+ una custom property; el nombre literal `"Geist"` que declaran los tokens no
128
+ existe en ningún `@font-face` de la página.
129
+
130
+ Importar `theme.css` sobrescribe `--font-sans` con ese literal, y las tres
131
+ familias caen a la fuente del sistema. En silencio: no hay 404, porque la fuente
132
+ sí se cargó — con otro nombre.
133
+
134
+ La solución es reafirmar las tres DESPUÉS del import, apuntando a las variables
135
+ que genera `next/font`:
136
+
137
+ ```ts
138
+ // app/fuentes.ts
139
+ import { Geist, Bricolage_Grotesque, JetBrains_Mono } from 'next/font/google';
140
+
141
+ export const sans = Geist({ subsets: ['latin'], variable: '--fuente-sans' });
142
+ export const display = Bricolage_Grotesque({ subsets: ['latin'], variable: '--fuente-display' });
143
+ export const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--fuente-mono' });
144
+ ```
145
+
146
+ ```css
147
+ @import "tailwindcss";
148
+ @import "@eduardoalvarez/arrecife/tokens/theme.css";
149
+ @source "../node_modules/@eduardoalvarez/arrecife/dist";
150
+
151
+ /* Después del import, o gana el literal que no está cargado. */
152
+ @theme {
153
+ --font-sans: var(--fuente-sans), ui-sans-serif, system-ui, sans-serif;
154
+ --font-display: var(--fuente-display), ui-sans-serif, system-ui, sans-serif;
155
+ --font-mono: var(--fuente-mono), ui-monospace, SFMono-Regular, Menlo, monospace;
156
+ }
157
+ ```
158
+
159
+ Las variables se llaman `--fuente-*` y no `--font-*` a propósito: `--font-sans`
160
+ es el nombre que Tailwind usa para SU token, y dárselo a `next/font` deja las dos
161
+ capas peleando por la misma propiedad.
162
+
163
+ El `variable` de cada familia va en la clase del `<html>`, como pide Next:
164
+ `className={`${sans.variable} ${display.variable} ${mono.variable}`}`.
165
+
104
166
  ### Mapa de tokens a utilidades
105
167
 
106
168
  | Token | Custom property | Utilidad |
@@ -191,6 +253,31 @@ Comprobado empaquetando la librería con `pnpm pack` e instalándola en un proye
191
253
  aparte: los tipos resuelven desde `dist/`, `./tokens` carga sin arrastrar React y
192
254
  `./tokens/theme.css` se resuelve por subruta.
193
255
 
256
+ ### Las dos subrutas que piden una dependencia
257
+
258
+ `./form` y `./chart` no cuelgan de la raíz, y es a propósito. Cada una pide una
259
+ dependencia de pares **opcional** —`react-hook-form` y `recharts`—, y colgarlas
260
+ del índice principal obligaría a los cinco proyectos a instalarlas para que su
261
+ bundler resolviera un import que cuatro de ellos nunca ejecutan.
262
+
263
+ Es la misma decisión que `./og` y `./shiki`, mirada desde el otro lado: allí se
264
+ saca React del camino de quien no lo monta; aquí se saca Recharts del camino de
265
+ quien no dibuja.
266
+
267
+ ```tsx
268
+ import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage }
269
+ from '@eduardoalvarez/arrecife/form';
270
+
271
+ import { ChartContainer, ChartTooltip, ChartTooltipContent, colorDeSerie }
272
+ from '@eduardoalvarez/arrecife/chart';
273
+ ```
274
+
275
+ `check:exports` verifica que las cuatro portables —`./tokens`, `./tema`, `./og` y
276
+ `./shiki`— no traen React en el `dist/` publicado, **siguiendo los imports
277
+ relativos**. Sin eso el check era papel mojado: con `treeshake` activo, cada
278
+ entrada portable queda en dos líneas que reexportan de un `chunk-XXXX.js`, y un
279
+ grep sobre esas dos líneas no encuentra React ni aunque el chunk lo importe.
280
+
194
281
  ## Scripts
195
282
 
196
283
  | | |
@@ -199,7 +286,7 @@ aparte: los tipos resuelven desde `dist/`, `./tokens` carga sin arrastrar React
199
286
  | `pnpm typecheck` | `tsc --noEmit` |
200
287
  | `pnpm lint` | ESLint, incluido el veto a hex literales fuera de `tokens.ts` |
201
288
  | `pnpm check:tokens` | falla si `src/tokens/` importa algo de fuera |
202
- | `pnpm test` | corre axe sobre las 161 stories, en los dos modos |
289
+ | `pnpm test` | compila Tailwind y corre axe sobre las 193 stories, en los dos modos |
203
290
  | `pnpm check:exports` | verifica que `dist/` tiene lo que `exports` promete |
204
291
  | `pnpm check:release` | valida `release-please-config.json` contra el esquema oficial |
205
292
  | `pnpm storybook` | genera los tokens y levanta Storybook en el 6006 |
@@ -1,3 +1,5 @@
1
- export { CaraDeMascota, Isotipo, Logo, Mascota } from '../chunk-ND45OQOC.js';
1
+ export { CaraDeMascota, Isotipo, Logo, Mascota } from '../chunk-TB3IYPOD.js';
2
2
  export { RUTA_ASSETS, aletas, caras, listaCaras, listaPoses, poses, usoDeCara } from '../chunk-E3OMP2DL.js';
3
- import '../chunk-I7GYZD2A.js';
3
+ import '../chunk-NHS7ETKJ.js';
4
+ import '../chunk-TSPJOM6K.js';
5
+ import '../chunk-MLKGABMK.js';
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ var recharts = require('recharts');
4
+ var clsx = require('clsx');
5
+ var tailwindMerge = require('tailwind-merge');
6
+ var reactSlot = require('@radix-ui/react-slot');
7
+ var classVarianceAuthority = require('class-variance-authority');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+
10
+ // src/chart/index.tsx
11
+ var typeScale = {
12
+ display: { family: "display", size: 76, lineHeight: 0.96, weight: 800, tracking: "-0.035em" },
13
+ /** Métricas grandes. Números, no prosa: interlineado 1 y sin descendentes. */
14
+ stat: { family: "display", size: 46, lineHeight: 1, weight: 800, tracking: "-0.035em" },
15
+ h1: { family: "display", size: 44, lineHeight: 1.05, weight: 700, tracking: "-0.03em" },
16
+ h2: { family: "display", size: 30, lineHeight: 1.1, weight: 600, tracking: "-0.02em" },
17
+ h3: { family: "display", size: 25, lineHeight: 1.15, weight: 600, tracking: "-0.02em" },
18
+ body: { family: "sans", size: 18, lineHeight: 1.75, weight: 400 },
19
+ /**
20
+ * 17px. El botón grande y la bajada de las páginas internas.
21
+ *
22
+ * Entra como escalón propio porque el documento lo usa DOS veces y en piezas
23
+ * distintas: «lg 15px 30px r12» con texto de 17, y «h1 44/700 · bajada 17px ·
24
+ * párrafo de contexto 15px». Un solo uso no habría justificado partir la
25
+ * distancia entre `ui` (15) y `body` (18); dos sí.
26
+ */
27
+ lead: { family: "sans", size: 17, lineHeight: 1.5, weight: 400 },
28
+ ui: { family: "sans", size: 15, lineHeight: 1.6, weight: 400 },
29
+ label: { family: "sans", size: 13, lineHeight: 1.5, weight: 500 },
30
+ /**
31
+ * La escala de las etiquetas de estado: sans 12.5/500, cuadrada.
32
+ *
33
+ * Es medio píxel por debajo de `label`, y esta vez el medio píxel sí importa:
34
+ * una etiqueta de estado va dentro de una tabla o al lado de un título, y a 13
35
+ * competía con el texto que acompaña. El documento la da en 12.5 y se ve.
36
+ */
37
+ tag: { family: "sans", size: 12.5, lineHeight: 1.4, weight: 500 },
38
+ /**
39
+ * La escala de categoría y métrica: mono 11.5.
40
+ *
41
+ * Rompe el suelo de `limits.minScreenPx`, y es a propósito. Ese suelo protege
42
+ * al TEXTO —lo que se lee en una frase—, y una píldora de una palabra no es
43
+ * texto corrido: es una marca. El contraste sigue medido y pasa AA (plancton
44
+ * 5.57:1 sobre abismo), que es la parte que no se negocia.
45
+ *
46
+ * A 13 las tres familias de etiqueta salían del tamaño de un botón pequeño y
47
+ * pesaban más que el título que acompañan. Ver `docs/decisiones.md`.
48
+ */
49
+ chip: { family: "mono", size: 11.5, lineHeight: 1.4, weight: 400 },
50
+ /**
51
+ * Mono SIN transformar: fechas, rutas, versiones, nombres de archivo, la firma
52
+ * del footer y las métricas de etiqueta. Es la mayoría del mono del sistema.
53
+ *
54
+ * Existe porque `eyebrow` lleva `uppercase` de fábrica y no es una preferencia
55
+ * que se pueda apagar: es la escala del eyebrow. Un `18 ago 2026` en versalitas
56
+ * o un `pose-laptop-coffee.png` en mayúsculas están mal, y sin este escalón la
57
+ * única salida era un `normal-case` en cada sitio de uso.
58
+ *
59
+ * El documento dice 12.5. Son 13 por la misma razón por la que `textMuted` se
60
+ * corrigió: `limits.minScreenPx` es 13 y plancton «nunca bajo 13px». Esta
61
+ * escala es justo donde se escriben las metas en muted, así que ponerla en
62
+ * 12.5 habría dejado ilegible su uso más común por medio píxel.
63
+ */
64
+ meta: { family: "mono", size: 13, lineHeight: 1.6, weight: 400 },
65
+ eyebrow: { family: "mono", size: 12, tracking: "0.12em", transform: "uppercase" }
66
+ };
67
+ var radius = {
68
+ chip: 6,
69
+ control: 10,
70
+ card: 14,
71
+ panel: 16,
72
+ pill: 999
73
+ };
74
+ var control = {
75
+ sm: 14,
76
+ md: 22,
77
+ lg: 30,
78
+ /** Botón de icono: cuadrado, sin texto. */
79
+ icon: 42
80
+ };
81
+ var spacing = {
82
+ stepXs: 8,
83
+ stepSm: 12,
84
+ stepMd: 16,
85
+ stepLg: 26,
86
+ stepXl: 40,
87
+ section: 96
88
+ };
89
+
90
+ // src/lib/cn.ts
91
+ var kebab = (nombre) => nombre.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
92
+ var escalas = Object.keys(typeScale);
93
+ var radios = Object.keys(radius);
94
+ var espacios = Object.keys(spacing).map(kebab);
95
+ var controles = Object.keys(control).map((nombre) => `control-${kebab(nombre)}`);
96
+ var twMerge = tailwindMerge.extendTailwindMerge({
97
+ extend: {
98
+ classGroups: {
99
+ "font-size": [{ text: escalas }],
100
+ rounded: [{ rounded: radios }],
101
+ p: [{ p: espacios }],
102
+ px: [{ px: [...espacios, ...controles] }],
103
+ py: [{ py: espacios }],
104
+ gap: [{ gap: espacios }],
105
+ size: [{ size: controles }]
106
+ }
107
+ }
108
+ });
109
+ function cn(...inputs) {
110
+ return twMerge(clsx.clsx(inputs));
111
+ }
112
+ var ETIQUETA = {
113
+ display: "h1",
114
+ stat: "p",
115
+ h1: "h1",
116
+ h2: "h2",
117
+ h3: "h3",
118
+ body: "p",
119
+ lead: "p",
120
+ ui: "p",
121
+ label: "span",
122
+ tag: "span",
123
+ meta: "p",
124
+ chip: "span",
125
+ eyebrow: "p"
126
+ };
127
+ var texto = classVarianceAuthority.cva("", {
128
+ variants: {
129
+ variant: {
130
+ display: "text-display font-display",
131
+ // Métricas grandes: 46px de display. Es la única escala pensada para un
132
+ // número, y por eso el interlineado es 1.
133
+ stat: "text-stat font-display",
134
+ h1: "text-h1 font-display",
135
+ h2: "text-h2 font-display",
136
+ h3: "text-h3 font-display",
137
+ body: "text-body font-sans",
138
+ // 17px: la bajada de las páginas internas y el botón grande.
139
+ lead: "text-lead font-sans",
140
+ ui: "text-ui font-sans",
141
+ label: "text-label font-sans",
142
+ // Las dos escalas de etiqueta. `tag` es el estado (sans 12.5/500) y `chip`
143
+ // la categoría y la métrica (mono 11.5). Ninguna transforma el texto.
144
+ tag: "text-tag font-sans",
145
+ chip: "text-chip font-mono",
146
+ // Mono SIN transformar: fechas, rutas, versiones, nombres de archivo y la
147
+ // firma del footer. Es el escalón que faltaba, y por el que cada sitio de
148
+ // uso terminaba escribiendo `variant="eyebrow" className="normal-case"`.
149
+ meta: "text-meta font-mono",
150
+ // `uppercase` no cabe en un token de tamaño: text-transform no es un
151
+ // modificador de --text-*. Por eso lo pone la variante y no theme.css.
152
+ eyebrow: "text-eyebrow font-mono uppercase"
153
+ },
154
+ tone: {
155
+ primary: "text-text-primary",
156
+ secondary: "text-text-secondary",
157
+ muted: "text-text-muted",
158
+ accent: "text-accent",
159
+ warm: "text-warm",
160
+ success: "text-success",
161
+ warning: "text-warning",
162
+ error: "text-error"
163
+ }
164
+ },
165
+ defaultVariants: { variant: "body", tone: "primary" }
166
+ });
167
+ function Text({
168
+ className,
169
+ variant,
170
+ tone,
171
+ as,
172
+ asChild = false,
173
+ measure,
174
+ children,
175
+ ...props
176
+ }) {
177
+ const escala = variant ?? "body";
178
+ const limitar = measure ?? escala === "body";
179
+ const clases = cn(texto({ variant, tone }), limitar && "max-w-measure", className);
180
+ if (asChild) {
181
+ return /* @__PURE__ */ jsxRuntime.jsx(reactSlot.Slot, { className: clases, ...props, children });
182
+ }
183
+ const Etiqueta = as ?? ETIQUETA[escala];
184
+ return /* @__PURE__ */ jsxRuntime.jsx(Etiqueta, { className: clases, ...props, children });
185
+ }
186
+ function colorDeSerie(indice) {
187
+ return `var(--color-serie-${Math.abs(indice) % 4 + 1})`;
188
+ }
189
+ var COLORES_DE_SERIE = [0, 1, 2, 3].map(colorDeSerie);
190
+ function ChartContainer({
191
+ label,
192
+ summary,
193
+ height = 320,
194
+ className,
195
+ children,
196
+ ...props
197
+ }) {
198
+ return /* @__PURE__ */ jsxRuntime.jsxs(
199
+ "figure",
200
+ {
201
+ "aria-label": label,
202
+ className: cn(
203
+ "w-full",
204
+ // La rejilla y los ejes, con los tokens del sistema en vez de los grises
205
+ // por defecto de Recharts. Van como variantes de descendiente porque los
206
+ // nodos los pinta la librería y no hay dónde ponerles una clase.
207
+ "[&_.recharts-cartesian-grid_line]:stroke-hairline",
208
+ "[&_.recharts-cartesian-axis-line]:stroke-hairline",
209
+ "[&_.recharts-cartesian-axis-tick_text]:fill-text-muted",
210
+ "[&_.recharts-cartesian-axis-tick_text]:font-mono",
211
+ "[&_.recharts-cartesian-axis-tick_text]:text-chip",
212
+ "[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-surface-raised",
213
+ "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-hairline-hover",
214
+ "[&_.recharts-sector]:outline-none",
215
+ "[&_.recharts-surface]:overflow-visible",
216
+ className
217
+ ),
218
+ ...props,
219
+ children: [
220
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height }, children: /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children }) }),
221
+ summary ? /* @__PURE__ */ jsxRuntime.jsx("figcaption", { className: "sr-only", children: summary }) : null
222
+ ]
223
+ }
224
+ );
225
+ }
226
+ function ChartTooltip(props) {
227
+ return /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, { isAnimationActive: false, ...props });
228
+ }
229
+ function ChartLegend(props) {
230
+ return /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, { ...props });
231
+ }
232
+ function ChartTooltipContent({
233
+ active,
234
+ payload,
235
+ label,
236
+ formatter,
237
+ hideLabel = false,
238
+ className
239
+ }) {
240
+ if (!active || !payload || payload.length === 0) return null;
241
+ return /* @__PURE__ */ jsxRuntime.jsxs(
242
+ "div",
243
+ {
244
+ className: cn(
245
+ "rounded-card border-border bg-surface shadow-standard p-step-sm gap-step-xs flex flex-col border",
246
+ className
247
+ ),
248
+ children: [
249
+ hideLabel || label === void 0 || label === null ? null : /* @__PURE__ */ jsxRuntime.jsx(Text, { as: "p", variant: "meta", tone: "muted", children: label }),
250
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "gap-step-xs flex flex-col", children: payload.map((item, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "gap-step-xs flex items-center", children: [
251
+ /* @__PURE__ */ jsxRuntime.jsx(
252
+ "span",
253
+ {
254
+ "aria-hidden": "true",
255
+ className: "rounded-chip size-2 shrink-0",
256
+ style: { backgroundColor: item.color ?? colorDeSerie(i) }
257
+ }
258
+ ),
259
+ /* @__PURE__ */ jsxRuntime.jsx(Text, { as: "span", variant: "label", tone: "secondary", className: "font-normal", children: item.name }),
260
+ /* @__PURE__ */ jsxRuntime.jsx(Text, { as: "span", variant: "label", className: "ml-auto font-mono tabular-nums", children: formatter ? formatter(item.value, item) : String(item.value ?? "") })
261
+ ] }, `${String(item.dataKey ?? item.name ?? i)}`)) })
262
+ ]
263
+ }
264
+ );
265
+ }
266
+ function ChartLegendContent({ payload, className }) {
267
+ if (!payload || payload.length === 0) return null;
268
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: cn("gap-step-md pt-step-sm flex flex-wrap items-center justify-center", className), children: payload.map((item, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "gap-step-xs flex items-center", children: [
269
+ /* @__PURE__ */ jsxRuntime.jsx(
270
+ "span",
271
+ {
272
+ "aria-hidden": "true",
273
+ className: "rounded-chip size-2 shrink-0",
274
+ style: { backgroundColor: item.color ?? colorDeSerie(i) }
275
+ }
276
+ ),
277
+ /* @__PURE__ */ jsxRuntime.jsx(Text, { as: "span", variant: "label", tone: "secondary", className: "font-normal", children: item.value })
278
+ ] }, `${String(item.dataKey ?? item.value ?? i)}`)) });
279
+ }
280
+
281
+ exports.COLORES_DE_SERIE = COLORES_DE_SERIE;
282
+ exports.ChartContainer = ChartContainer;
283
+ exports.ChartLegend = ChartLegend;
284
+ exports.ChartLegendContent = ChartLegendContent;
285
+ exports.ChartTooltip = ChartTooltip;
286
+ exports.ChartTooltipContent = ChartTooltipContent;
287
+ exports.colorDeSerie = colorDeSerie;
@@ -0,0 +1,127 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+ import { Legend, Tooltip } from 'recharts';
4
+
5
+ /**
6
+ * El chasis de las gráficas: contenedor, tooltip y leyenda con el vocabulario
7
+ * del sistema. Recharts pinta; esto decide cómo se ve.
8
+ *
9
+ * Se publica en `@eduardoalvarez/arrecife/chart` y NO en la raíz. Recharts pesa,
10
+ * y solo uno de los cinco proyectos dibuja métricas: colgarlo del índice
11
+ * principal obligaría a los otros cuatro a instalarlo para que su bundler
12
+ * resolviera un import que nunca ejecutan. Es dependencia de pares OPCIONAL, por
13
+ * el mismo criterio con el que `./og` y `./shiki` se mantienen sin React.
14
+ *
15
+ * Lo que esta capa aporta —y lo que se perdía copiando el `chart.tsx` de shadcn
16
+ * en cada proyecto— son tres cosas que se desincronizan solas:
17
+ *
18
+ * 1. La paleta de series sale de `tokens.series` y sigue el modo. Son cuatro
19
+ * y se distinguen por tono, no por luminosidad.
20
+ * 2. La rejilla, los ejes y el cursor usan `hairline` y `textMuted`, no los
21
+ * grises por defecto de Recharts, que no son de esta identidad.
22
+ * 3. El tooltip aparece donde va a quedarse: `isAnimationActive` apagado en
23
+ * todas partes, porque el sistema no anima posición.
24
+ *
25
+ * Las piezas de datos —`BarChart`, `Line`, `XAxis`— NO se reexportan. Son la API
26
+ * de Recharts, el proyecto ya la conoce y envolverlas sería una capa de nombres
27
+ * que hay que mantener sincronizada con la versión de la librería.
28
+ */
29
+ /**
30
+ * El color de la serie `indice`, como custom property.
31
+ *
32
+ * Devuelve `var(--color-serie-N)` y no el hexadecimal: leído en JS, el hex sería
33
+ * el del modo que había cuando se montó el componente y no cambiaría al alternar
34
+ * el tema. La variable la resuelve el navegador en cada pintura.
35
+ *
36
+ * Da la vuelta pasadas las cuatro. Que dos series compartan color es un fallo
37
+ * visible, y es la señal correcta: la gráfica tiene más categorías de las que
38
+ * esta identidad sabe distinguir, y lo que toca es agrupar en «otros».
39
+ */
40
+ declare function colorDeSerie(indice: number): string;
41
+ /** Las cuatro, en orden, para pasárselas de golpe a un `Pie` con `Cell`. */
42
+ declare const COLORES_DE_SERIE: string[];
43
+ type ChartContainerProps = Omit<ComponentPropsWithoutRef<'figure'>, 'title'> & {
44
+ /**
45
+ * Qué muestra la gráfica, en una frase. Obligatorio, como el `label` de
46
+ * `Progress`: un `<svg>` de barras sin nombre accesible no es «una gráfica sin
47
+ * etiqueta», es una región vacía.
48
+ */
49
+ label: string;
50
+ /**
51
+ * Lo que la gráfica dice, en palabras. Va en un `figcaption` oculto
52
+ * visualmente.
53
+ *
54
+ * No sustituye a la gráfica: la capa de accesibilidad de Recharts ya permite
55
+ * recorrer los puntos con el teclado. Es el titular —«sube de 24 a 52 con una
56
+ * caída en mayo»— que ningún recorrido punto a punto da.
57
+ */
58
+ summary?: ReactNode;
59
+ /** Alto en píxeles. Recharts necesita uno concreto para medir. */
60
+ height?: number;
61
+ children: ReactNode;
62
+ };
63
+ /**
64
+ * Envuelve la gráfica en un `<figure>` con nombre accesible y le da a Recharts
65
+ * el alto concreto que necesita para medirse.
66
+ *
67
+ * El `<figure>` NO lleva `role`: ya tiene el suyo implícito, y ponerle `group`
68
+ * encima es un rol no permitido para el elemento —axe lo señala—. Lo que sí
69
+ * lleva es `aria-label`, que es lo que lo nombra.
70
+ *
71
+ * Y el contenido NO va `aria-hidden`. Fue lo primero que se intentó, con el
72
+ * argumento de que anunciar cada tick no cuenta lo que la gráfica cuenta, y está
73
+ * mal por dos motivos: la capa de accesibilidad de Recharts hace la gráfica
74
+ * recorrible con el teclado, y esconder un subárbol que contiene elementos
75
+ * enfocables es una violación por sí misma —el foco entra en algo que no existe
76
+ * para quien escucha—. El resumen se suma a eso, no lo reemplaza.
77
+ */
78
+ declare function ChartContainer({ label, summary, height, className, children, ...props }: ChartContainerProps): react.JSX.Element;
79
+ /**
80
+ * El `Tooltip` de Recharts con los defectos del sistema: sin animación y con el
81
+ * cursor teñido de `surfaceRaised`.
82
+ *
83
+ * Se pasa `content={<ChartTooltipContent />}` para la caja.
84
+ */
85
+ declare function ChartTooltip(props: ComponentPropsWithoutRef<typeof Tooltip>): react.JSX.Element;
86
+ declare function ChartLegend(props: ComponentPropsWithoutRef<typeof Legend>): react.JSX.Element;
87
+ /**
88
+ * Lo que Recharts le pasa a un `content`, declarado aquí en corto.
89
+ *
90
+ * Los tipos de Recharts para esto son abiertos y arrastran genéricos que no
91
+ * aportan nada al sitio de uso; `any` está prohibido en el repo, así que se
92
+ * declara lo que de verdad se lee y el valor se trata como `unknown`.
93
+ */
94
+ type ChartPayloadItem = {
95
+ name?: string | number | undefined;
96
+ value?: unknown;
97
+ color?: string | undefined;
98
+ dataKey?: string | number | undefined;
99
+ };
100
+ type ChartTooltipContentProps = {
101
+ active?: boolean | undefined;
102
+ payload?: readonly ChartPayloadItem[] | undefined;
103
+ label?: ReactNode;
104
+ /** Formatea el valor. Sin ella se imprime tal cual: la librería no impone locale. */
105
+ formatter?: ((valor: unknown, item: ChartPayloadItem) => ReactNode) | undefined;
106
+ /** Oculta el encabezado, para una gráfica de una sola categoría. */
107
+ hideLabel?: boolean;
108
+ className?: string;
109
+ };
110
+ /**
111
+ * La caja del tooltip. Es una tarjeta del sistema —`surface`, borde de control,
112
+ * sombra estándar— y no la caja blanca de Recharts, que en modo oscuro es un
113
+ * rectángulo blanco encima de un panel oscuro.
114
+ *
115
+ * El punto de color es un cuadrado de 8px, no un círculo: es la misma marca que
116
+ * usa la leyenda, y en 8 píxeles un círculo y un cuadrado se distinguen peor
117
+ * entre sí que dos colores de la paleta.
118
+ */
119
+ declare function ChartTooltipContent({ active, payload, label, formatter, hideLabel, className, }: ChartTooltipContentProps): react.JSX.Element | null;
120
+ type ChartLegendContentProps = {
121
+ payload?: readonly ChartPayloadItem[] | undefined;
122
+ className?: string;
123
+ };
124
+ /** La leyenda con la misma marca cuadrada del tooltip y la escala `label`. */
125
+ declare function ChartLegendContent({ payload, className }: ChartLegendContentProps): react.JSX.Element | null;
126
+
127
+ export { COLORES_DE_SERIE, ChartContainer, type ChartContainerProps, ChartLegend, ChartLegendContent, type ChartLegendContentProps, type ChartPayloadItem, ChartTooltip, ChartTooltipContent, type ChartTooltipContentProps, colorDeSerie };