reactigoded 1.0.0-beta.26

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 (177) hide show
  1. package/CHANGELOG.md +3001 -0
  2. package/LICENSE +21 -0
  3. package/README.md +556 -0
  4. package/dist/Toast-FZUdYH1L.js +594 -0
  5. package/dist/cn.js +7 -0
  6. package/dist/components/Accordion/Accordion.d.ts +49 -0
  7. package/dist/components/Accordion/AccordionContent.d.ts +15 -0
  8. package/dist/components/Accordion/AccordionContext.d.ts +59 -0
  9. package/dist/components/Accordion/AccordionHeader.d.ts +22 -0
  10. package/dist/components/Accordion/AccordionItem.d.ts +11 -0
  11. package/dist/components/Accordion/index.d.ts +8 -0
  12. package/dist/components/Alert/Alert.d.ts +39 -0
  13. package/dist/components/Alert/index.d.ts +2 -0
  14. package/dist/components/AlertDialog/AlertDialogClose.d.ts +64 -0
  15. package/dist/components/AlertDialog/AlertDialogContent.d.ts +58 -0
  16. package/dist/components/AlertDialog/index.d.ts +14 -0
  17. package/dist/components/Avatar/Avatar.d.ts +62 -0
  18. package/dist/components/Avatar/AvatarGroup.d.ts +13 -0
  19. package/dist/components/Avatar/index.d.ts +4 -0
  20. package/dist/components/Badge/Badge.d.ts +33 -0
  21. package/dist/components/Badge/index.d.ts +2 -0
  22. package/dist/components/Breadcrumb/Breadcrumb.d.ts +14 -0
  23. package/dist/components/Breadcrumb/BreadcrumbItem.d.ts +21 -0
  24. package/dist/components/Breadcrumb/index.d.ts +4 -0
  25. package/dist/components/Button/Button.d.ts +61 -0
  26. package/dist/components/Button/IconButton.d.ts +48 -0
  27. package/dist/components/Button/index.d.ts +4 -0
  28. package/dist/components/Card/Card.d.ts +79 -0
  29. package/dist/components/Card/CardBody.d.ts +8 -0
  30. package/dist/components/Card/CardDivider.d.ts +8 -0
  31. package/dist/components/Card/CardFooter.d.ts +8 -0
  32. package/dist/components/Card/CardHeader.d.ts +8 -0
  33. package/dist/components/Card/CardImage.d.ts +15 -0
  34. package/dist/components/Card/index.d.ts +12 -0
  35. package/dist/components/Checkbox/Checkbox.d.ts +47 -0
  36. package/dist/components/Checkbox/index.d.ts +2 -0
  37. package/dist/components/Chip/Chip.d.ts +38 -0
  38. package/dist/components/Chip/index.d.ts +2 -0
  39. package/dist/components/Dialog/Dialog.d.ts +80 -0
  40. package/dist/components/Dialog/DialogBody.d.ts +8 -0
  41. package/dist/components/Dialog/DialogClose.d.ts +61 -0
  42. package/dist/components/Dialog/DialogContent.d.ts +36 -0
  43. package/dist/components/Dialog/DialogContext.d.ts +1 -0
  44. package/dist/components/Dialog/DialogFooter.d.ts +8 -0
  45. package/dist/components/Dialog/DialogHeader.d.ts +13 -0
  46. package/dist/components/Dialog/DialogTrigger.d.ts +44 -0
  47. package/dist/components/Dialog/index.d.ts +16 -0
  48. package/dist/components/Divider/Divider.d.ts +34 -0
  49. package/dist/components/Divider/index.d.ts +2 -0
  50. package/dist/components/Input/ErrorText.d.ts +13 -0
  51. package/dist/components/Input/Helper.d.ts +11 -0
  52. package/dist/components/Input/Input.d.ts +35 -0
  53. package/dist/components/Input/InputAddon.d.ts +8 -0
  54. package/dist/components/Input/InputGroup.d.ts +17 -0
  55. package/dist/components/Input/Label.d.ts +12 -0
  56. package/dist/components/Input/index.d.ts +12 -0
  57. package/dist/components/NativeSelect/NativeSelect.d.ts +41 -0
  58. package/dist/components/NativeSelect/index.d.ts +2 -0
  59. package/dist/components/Navbar/Navbar.d.ts +46 -0
  60. package/dist/components/Navbar/NavbarActions.d.ts +10 -0
  61. package/dist/components/Navbar/NavbarLink.d.ts +13 -0
  62. package/dist/components/Navbar/NavbarLogo.d.ts +22 -0
  63. package/dist/components/Navbar/NavbarMenuButton.d.ts +15 -0
  64. package/dist/components/Navbar/NavbarNav.d.ts +14 -0
  65. package/dist/components/Navbar/index.d.ts +12 -0
  66. package/dist/components/Pagination/Pagination.d.ts +77 -0
  67. package/dist/components/Pagination/index.d.ts +2 -0
  68. package/dist/components/Progress/Progress.d.ts +63 -0
  69. package/dist/components/Progress/index.d.ts +2 -0
  70. package/dist/components/Radio/Radio.d.ts +28 -0
  71. package/dist/components/Radio/index.d.ts +2 -0
  72. package/dist/components/Rating/Rating.d.ts +85 -0
  73. package/dist/components/Rating/index.d.ts +2 -0
  74. package/dist/components/Sidebar/Sidebar.d.ts +28 -0
  75. package/dist/components/Sidebar/SidebarContext.d.ts +32 -0
  76. package/dist/components/Sidebar/SidebarDivider.d.ts +8 -0
  77. package/dist/components/Sidebar/SidebarFooter.d.ts +8 -0
  78. package/dist/components/Sidebar/SidebarHeader.d.ts +13 -0
  79. package/dist/components/Sidebar/SidebarItem.d.ts +42 -0
  80. package/dist/components/Sidebar/SidebarNav.d.ts +13 -0
  81. package/dist/components/Sidebar/SidebarSection.d.ts +11 -0
  82. package/dist/components/Sidebar/SidebarToggle.d.ts +17 -0
  83. package/dist/components/Sidebar/index.d.ts +16 -0
  84. package/dist/components/Skeleton/Skeleton.d.ts +60 -0
  85. package/dist/components/Skeleton/index.d.ts +2 -0
  86. package/dist/components/Slider/Slider.d.ts +44 -0
  87. package/dist/components/Slider/index.d.ts +2 -0
  88. package/dist/components/Slot/Slot.d.ts +1 -0
  89. package/dist/components/Slot/composeEventHandlers.d.ts +1 -0
  90. package/dist/components/Slot/composeRefs.d.ts +1 -0
  91. package/dist/components/Slot/index.d.ts +3 -0
  92. package/dist/components/Spinner/Spinner.d.ts +31 -0
  93. package/dist/components/Spinner/index.d.ts +2 -0
  94. package/dist/components/Stepper/Step.d.ts +17 -0
  95. package/dist/components/Stepper/Stepper.d.ts +97 -0
  96. package/dist/components/Stepper/index.d.ts +4 -0
  97. package/dist/components/Switch/Switch.d.ts +53 -0
  98. package/dist/components/Switch/index.d.ts +2 -0
  99. package/dist/components/Table/Table.d.ts +74 -0
  100. package/dist/components/Table/index.d.ts +2 -0
  101. package/dist/components/Tabs/Tabs.d.ts +41 -0
  102. package/dist/components/Tabs/TabsContent.d.ts +15 -0
  103. package/dist/components/Tabs/TabsContext.d.ts +61 -0
  104. package/dist/components/Tabs/TabsList.d.ts +10 -0
  105. package/dist/components/Tabs/TabsTrigger.d.ts +15 -0
  106. package/dist/components/Tabs/index.d.ts +8 -0
  107. package/dist/components/Textarea/Textarea.d.ts +51 -0
  108. package/dist/components/Textarea/index.d.ts +2 -0
  109. package/dist/components/ThemeToggle/ThemeToggle.d.ts +43 -0
  110. package/dist/components/ThemeToggle/index.d.ts +2 -0
  111. package/dist/components/Timeline/Timeline.d.ts +20 -0
  112. package/dist/components/Timeline/TimelineItem.d.ts +32 -0
  113. package/dist/components/Timeline/index.d.ts +4 -0
  114. package/dist/components/Toast/Toast.d.ts +47 -0
  115. package/dist/components/Toast/ToastContext.d.ts +93 -0
  116. package/dist/components/Toast/ToastProvider.d.ts +116 -0
  117. package/dist/components/Toast/index.d.ts +6 -0
  118. package/dist/components/floating/Menu/Menu.d.ts +67 -0
  119. package/dist/components/floating/Menu/MenuContent.d.ts +60 -0
  120. package/dist/components/floating/Menu/MenuContext.d.ts +78 -0
  121. package/dist/components/floating/Menu/MenuItem.d.ts +47 -0
  122. package/dist/components/floating/Menu/MenuLabel.d.ts +6 -0
  123. package/dist/components/floating/Menu/MenuSeparator.d.ts +6 -0
  124. package/dist/components/floating/Menu/MenuTrigger.d.ts +53 -0
  125. package/dist/components/floating/Menu/index.d.ts +12 -0
  126. package/dist/components/floating/Menu/menuSelectors.d.ts +15 -0
  127. package/dist/components/floating/Tooltip/Tooltip.d.ts +194 -0
  128. package/dist/components/floating/Tooltip/index.d.ts +2 -0
  129. package/dist/components/floating/index.d.ts +3 -0
  130. package/dist/components/floating/primitives/FloatingTreeRoot.d.ts +55 -0
  131. package/dist/components/floating/primitives/index.d.ts +2 -0
  132. package/dist/components/floating/primitives/useFloatingNode.d.ts +1 -0
  133. package/dist/components/index.d.ts +38 -0
  134. package/dist/hooks/useControllableState.d.ts +152 -0
  135. package/dist/hooks/useTheme.d.ts +44 -0
  136. package/dist/index.d.ts +36 -0
  137. package/dist/index.js +1843 -0
  138. package/dist/server-safe.d.ts +69 -0
  139. package/dist/server-safe.js +47 -0
  140. package/dist/styles/igoded-base.css +1 -0
  141. package/dist/styles/igoded-components.css +1 -0
  142. package/dist/styles/igoded-design.css +1 -0
  143. package/dist/styles/igoded-fonts.css +1 -0
  144. package/dist/styles/igoded-reset.css +1 -0
  145. package/dist/styles/igoded-state-css.css +1 -0
  146. package/dist/styles/igoded-tokens.css +1 -0
  147. package/dist/styles/index.css +3 -0
  148. package/dist/styles/state/active.css +1 -0
  149. package/dist/styles/state/checked.css +1 -0
  150. package/dist/styles/state/default.css +1 -0
  151. package/dist/styles/state/disabled.css +1 -0
  152. package/dist/styles/state/empty.css +1 -0
  153. package/dist/styles/state/first-child.css +1 -0
  154. package/dist/styles/state/first-of-type.css +1 -0
  155. package/dist/styles/state/focus-visible.css +1 -0
  156. package/dist/styles/state/focus-within.css +1 -0
  157. package/dist/styles/state/focus.css +1 -0
  158. package/dist/styles/state/group-hover.css +1 -0
  159. package/dist/styles/state/hover.css +1 -0
  160. package/dist/styles/state/in-range.css +1 -0
  161. package/dist/styles/state/indeterminate.css +1 -0
  162. package/dist/styles/state/invalid.css +1 -0
  163. package/dist/styles/state/last-child.css +1 -0
  164. package/dist/styles/state/last-of-type.css +1 -0
  165. package/dist/styles/state/nth-child.css +1 -0
  166. package/dist/styles/state/only-child.css +1 -0
  167. package/dist/styles/state/optional.css +1 -0
  168. package/dist/styles/state/other.css +1 -0
  169. package/dist/styles/state/out-of-range.css +1 -0
  170. package/dist/styles/state/placeholder-shown.css +1 -0
  171. package/dist/styles/state/read-only.css +1 -0
  172. package/dist/styles/state/read-write.css +1 -0
  173. package/dist/styles/state/required.css +1 -0
  174. package/dist/styles/state/target.css +1 -0
  175. package/dist/styles/state/valid.css +1 -0
  176. package/dist/utils/cn.d.ts +11 -0
  177. package/package.json +282 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,3001 @@
1
+ # Changelog
2
+
3
+ Todos los cambios notables de este paquete se documentan aquí.
4
+
5
+ Formato basado en [Keep a Changelog](https://keepachangelog.com/),
6
+ versionado [SemVer](https://semver.org/lang/es/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Cambiado — BREAKING (última ventana antes de 1.0)
11
+
12
+ - Renombres de API pública para eliminar colisiones de nombre antes de congelar 1.0:
13
+ - Clases `.ig-tooltip-color-*` → `.ig-tooltip-*` (el infijo `-color-` era dialecto local; la doc ya prometía la forma sin él).
14
+ - Clase `.ig-navbar-brand` → `.ig-navbar-logo` y componente `NavbarBrand` → `NavbarLogo` (`brand` colisionaba con el eje de rol de color; el slot es el logo).
15
+ - Clases `.ig-input-error`/`.ig-input-success` → `.ig-input-invalid`/`.ig-input-valid` y valores del prop `state` (`"invalid"`/`"valid"`), alineados con `aria-invalid`.
16
+
17
+ ### Añadido
18
+
19
+ - **Garantía `@server-safe` validada contra Vercel Edge REAL (#18)**: el catálogo de globals permitidos se medía contra `@edge-runtime/vm` (sandbox sobre Node que filtra globals Node-shared, ~95% fiel). Un probe de deploy real a Vercel Edge producción (`scripts/runtime-oracle/vercel/`, `typeof <bare>` — el único test fiel dado que el objeto-global de Edge es exótico) cerró ese ~5%: **3 globals que `@edge-runtime/vm` reportaba presentes por fuga de Node (`WeakRef`, `FinalizationRegistry`, `DOMException`) NO existen en Vercel Edge real** → restados de `SAFE_GLOBALS` (nuevo set `EDGE_MISSING_REAL`). Cierra un falso-negativo latente (0 módulos los usaban) que habría dejado pasar un `new WeakRef(...)` bare que crashea en producción Edge. Premisas del catálogo (createObjectURL/WASM/eval/elu) confirmadas 6/6 en el Edge real.
20
+ - **Freeze de API pública (§5.13)**: la superficie estable de 1.0 (clases de componente, data-attributes de estado, tokens Tier-2) se congela en `src/_audit/public-api-names.json`, protegida por el gate `scripts/check-public-api-names.mjs` (`⊆ dist`, encadenado en `verify:unit`). Editar el freeze exige bump MAJOR. La capa utility (`state.css`) y los tokens Tier-1/Tier-3 quedan fuera del freeze **por declaración explícita** (ver CSSAPI.mdx + DesignTokens.mdx).
21
+
22
+ ## [1.0.0-beta.26] — 2026-05-29 (bloque claudegate5 / beta.27 cerrado)
23
+
24
+ Cierra el bloque **claudegate5 / beta.27** (25 PRs entre #105 y
25
+ #129) con 3 ejes principales:
26
+
27
+ - **Slot pattern DS-wide (D14)**: refactor `asChild` en las 4
28
+ familias con triggers (Dialog / AlertDialog / Tooltip / Menu).
29
+ Patrón canónico Radix/shadcn habilitado en todo el DS.
30
+ - **EOPT widening completo (#155)**: 307 props públicas opcionales
31
+ ensanchadas a `?: T | undefined` para consumers con
32
+ `exactOptionalPropertyTypes: true`. 2 fronteras documentadas
33
+ explícitamente (sección final del bloque).
34
+ - **Red de seguridad cross-platform (#151)**: matriz CI 4 combos
35
+ (ubuntu/windows × Node 22.12/24), test catálogo `CLIENT_GLOBALS`
36
+ vs Node runtime (#150), gates de payload reales (#160), peer
37
+ `@floating-ui/react ^0.27` pre-1.0 documentado (#156).
38
+
39
+ Tras este release queda el gate review claudegate6 (auditoría AI con
40
+ cruce A+B de dos auditores independientes) antes de tagear
41
+ `1.0.0-rc.1` (API congelada para 1.x). beta.26 puede aún recibir bug
42
+ fixes o ajustes pre-rc.1 si el gate identifica blockers.
43
+
44
+
45
+ ### BREAKING (D14 Bloque B beta.27)
46
+
47
+ - **`DialogAction` ELIMINADO**. El patrón canónico para CTAs del footer
48
+ ahora es `<DialogClose asChild>` con un `<Button>` del consumer como
49
+ child. Migración:
50
+
51
+ ```diff
52
+ <DialogFooter>
53
+ - <DialogAction className="ig-btn ig-btn-secondary">Cancelar</DialogAction>
54
+ - <DialogAction className="ig-btn ig-btn-brand">Aceptar</DialogAction>
55
+ + <DialogClose asChild>
56
+ + <Button variant="secondary">Cancelar</Button>
57
+ + </DialogClose>
58
+ + <DialogClose asChild>
59
+ + <Button variant="brand">Aceptar</Button>
60
+ + </DialogClose>
61
+ </DialogFooter>
62
+ ```
63
+
64
+ Cierra la asimetría léxica `DialogClose styled / DialogAction unstyled /
65
+ AlertDialogClose clónico` identificada en el cruce reviews beta.25.
66
+
67
+ - **`AlertDialogClose` cambia default render** a icon-button "×" styled
68
+ (coherente con `DialogClose`). Antes era unstyled `<button>` para usar
69
+ con `className="ig-btn ig-btn-*"`. Migración para CTAs del footer:
70
+
71
+ ```diff
72
+ <AlertDialogFooter>
73
+ - <AlertDialogClose className="ig-btn ig-btn-secondary">Cancelar</AlertDialogClose>
74
+ - <AlertDialogClose className="ig-btn ig-btn-danger">Sí, borrar</AlertDialogClose>
75
+ + <AlertDialogClose asChild>
76
+ + <Button variant="secondary">Cancelar</Button>
77
+ + </AlertDialogClose>
78
+ + <AlertDialogClose asChild>
79
+ + <Button variant="danger">Sí, borrar</Button>
80
+ + </AlertDialogClose>
81
+ </AlertDialogFooter>
82
+ ```
83
+
84
+ ### Added (D14 Bloque B beta.27)
85
+
86
+ - **`asChild` prop en `DialogTrigger`, `DialogClose`, `AlertDialogClose`**.
87
+ Cuando `asChild={true}`, el componente clona el child del consumer y le
88
+ inyecta semantics (aria + onClick handler) sin renderizar wrapper
89
+ propio. Patrón canónico Radix/shadcn. Internamente usa el primitive
90
+ `<Slot>` de D14 Bloque A (PR #110).
91
+
92
+ ```tsx
93
+ <DialogTrigger asChild>
94
+ <Button variant="brand">Abrir modal</Button>
95
+ </DialogTrigger>
96
+ ```
97
+
98
+ `AlertDialogTrigger` es alias de `DialogTrigger`, así que también
99
+ obtiene `asChild` automáticamente.
100
+
101
+ Sin `asChild`, comportamiento idéntico a 1.0.0-beta.25 (backwards-compat
102
+ para DialogTrigger y DialogClose; AlertDialogClose es BREAKING — ver arriba).
103
+
104
+ - **`<Slot>` primitive interno** en `src/components/Slot/` — no exportado
105
+ al consumer, solo usado por components del DS via `asChild` opt-in.
106
+ Ver D14 + PR #110 para diseño completo.
107
+
108
+ - **`asChild` prop en `MenuTrigger`** (D14 Bloque D beta.27).
109
+ Misma semántica que `DialogTrigger`/`DialogClose`/`AlertDialogClose`: si
110
+ `true`, clona el child del consumer y le aplica id + ARIA (haspopup,
111
+ expanded, controls) + handlers FUI (click + keyboard) sin renderizar
112
+ un `<button>` propio ni aplicar `ig-menu-trigger`. Permite usar
113
+ cualquier elemento como trigger:
114
+
115
+ ```tsx
116
+ <Menu>
117
+ <MenuTrigger asChild>
118
+ <Button variant="brand">Abrir menú</Button>
119
+ </MenuTrigger>
120
+ <MenuContent>
121
+ <MenuItem>Editar</MenuItem>
122
+ </MenuContent>
123
+ </Menu>
124
+ ```
125
+
126
+ Sin `asChild`, comportamiento idéntico a 1.0.0-beta.25 (backwards-compat).
127
+
128
+ **Cierra el refactor Slot DS-wide** (D14): 4 familias migradas (Dialog,
129
+ AlertDialog, Tooltip, Menu) — todas las que existen con triggers en
130
+ 1.0. Las 13 familias futuras del roadmap `floating/README.md`
131
+ (Popover, HoverCard, ContextMenu, Combobox, etc.) se diseñarán ya
132
+ nativas con Slot en 1.1.0+.
133
+
134
+ - **`Tooltip` forwardea outer Slot props al child final** (D14 Bloque C beta.27).
135
+ `TooltipProps` admite ahora un index signature (cualquier prop adicional
136
+ no consumida por Tooltip — `text`, `placement`, `variant`, `children`,
137
+ delays, `container` — se forwardea via Slot interno al elemento child
138
+ clonado). Habilita el patrón nested asChild:
139
+
140
+ ```tsx
141
+ <DialogClose asChild>
142
+ <Tooltip text="Cancela y cierra">
143
+ <Button variant="danger">×</Button>
144
+ </Tooltip>
145
+ </DialogClose>
146
+ ```
147
+
148
+ El outer Slot de `DialogClose` clona Tooltip pasando close `onClick` +
149
+ ref; Tooltip los recibe en su nuevo `...rest` y los forwardea al
150
+ `<Button>` final via `<Slot>` interno. Pre-Bloque C, esos props se
151
+ dropeaban silenciosamente (codex P2 round 2 sobre PR #109 cazó el bug en el
152
+ doc-only plan).
153
+
154
+ API público de Tooltip preservado: `text`, `placement`, `variant`,
155
+ `children` (ReactElement), delays, `container` siguen igual.
156
+
157
+ ### Added (resto del bloque beta.27)
158
+
159
+ - **EOPT widening — 307 props opcionales ensanchadas a `?: T | undefined`** (#155, PR #114). Habilita el patrón consumer `<Comp prop={cond ? val : undefined}>` bajo `exactOptionalPropertyTypes: true`. Reparto:
160
+ - 271 CLASE 1 (codemod AST mecánico).
161
+ - 36 CLASE 2 (widening + tests runtime sobre los 12 componentes con `useControllableState`).
162
+ - 11 OUT OF SCOPE (3 discriminadores `?: undefined` literal + 8 exclusiones `?: never`).
163
+
164
+ Inventario AST: `scripts/eopt-classify.mjs`. Verifier: `scripts/eopt-verify-class2.mjs`. Codemod: `scripts/eopt-codemod.mjs`. Tests runtime: `src/__tests__/eopt-undefined-uncontrolled.test.tsx`. Fronteras NO cubiertas documentadas más abajo.
165
+
166
+ - **Rating form-value bridge nativo (#153, PR #117)**. `Rating` añade `name?: string`. Cuando se proporciona, renderiza `<input type="hidden" name={name} value={value}>` adyacente al `role="radiogroup"`. El valor espeja el efectivo (controlled o uncontrolled). Mantiene el canal-de-forma WCAG (#102 — buttons en `radiogroup`).
167
+
168
+ ```tsx
169
+ <form action="/rate" method="post">
170
+ <Rating name="score" defaultValue={3} max={5} />
171
+ <button type="submit">Enviar</button>
172
+ </form>
173
+ ```
174
+
175
+ Cierra la asimetría con los otros 5 inputs del DS (`Checkbox`, `Switch`, `Radio`, `Slider`, `NativeSelect`) que serializan vía `<input>` nativo heredado.
176
+
177
+ - **CI matriz Windows + Node [22.12, 24]** (#151, PR #121). `.github/workflows/verify.yml` corre 4 combos: ubuntu/windows × Node 22.12/24. Cross-OS gates: `scripts/check-no-dev-warns.mjs` (reemplaza el step inline `grep -cF` POSIX-only), helpers `crossOsResolve/crossOsRelative/crossOsDirname` en `scripts/check-server-safe-markers.mjs` que preservan Windows drive letters (codex P1).
178
+
179
+ - **Test catálogo `CLIENT_GLOBALS` vs Node runtime cross-version** (#150, PR #122). `src/__tests__/server-safe-catalog-vs-node.test.ts` verifica en runtime real (Node 22.12 + 24, ubuntu + windows) que el catálogo del gate `@server-safe` no tiene falsos positivos sobre globals que Node provee. 4 overlaps documentados (`globalThis`/`process`/`Buffer`/`navigator`) con rationale multi-runtime.
180
+
181
+ - **Server-safe marker AST** (#158, PR #118). El gate `check-server-safe-markers.mjs` detecta el marker `@server-safe` vía TypeScript Compiler API (JSDoc tag), no via substring. Cierra 3 vectores de falsos positivos: string literals, line comments en prosa, block comments no-JSDoc. Tests: `src/__tests__/server-safe-marker-ast-detection.test.ts`.
182
+
183
+ - **Consumer-pack tarball-real-install gate** (PR #108). `scripts/test-consumer-pack.mjs` hace `npm pack` → instala en sandbox → corre `tsc --noEmit` con bundler + NodeNext. Caza issues que solo emergen con el flow end-to-end real: exports field interpretado por node/npm, peer deps resueltos, `.d.ts` paths bajo NodeNext.
184
+
185
+ - **Smuggling cross-módulo gate (HIGH-2)** (PR #106). El gate `@server-safe` ahora sigue imports transitivos (depth-first) para cazar bypasses que importan un util sucio desde un archivo `@server-safe`. Inventario completo en `scripts/check-server-safe-markers.mjs` (orquestador `checkFileWithImports`).
186
+
187
+ - **NodeNext + CLIENT_GLOBALS ampliado + JSDoc Menu (HIGH bloque)** (PR #105). El gate detecta acceso bare a 46 client globals (catálogo ampliado vs los 6 originales). `CLIENT_GLOBALS` exportado para uso en tests. Resolución alias en NodeNext robustecida con cascada de extensiones `.ts`/`.tsx`/`/index.ts`/`/index.tsx`.
188
+
189
+ - **D13 — name reservations pre-rc.1** (PR #107). Reservas léxicas para los componentes/props que el roadmap 1.1+ va a añadir (Popover/HoverCard/ContextMenu/Combobox y subsystem `Form`/`Field`/`FormError`). Documentadas en `docs/decisions/D13-name-reservations-pre-rc1.md`.
190
+
191
+ - **Fixture Playwright contraste pares componente** (#152, PR #129). `src/stories/ContrastPairs.stories.tsx` mide ΔE OKLab sobre DOM real (Chromium vía vitest-browser-playwright + Storybook) y asserta vs `deltaE_at_decision` con drift_tolerance 5%. Complementa el gate `check-component-contrast.mjs` (CSS resolver) con cobertura runtime DOM — caza regresiones de cascade/specificity.
192
+
193
+ - **Audit fixtures `navigator` wholesale denylist** (#164, PR #125). 10 tests en `src/_audit/server-safe-gate.test.ts` que prueban el wholesale denylist de `navigator.X` (sin allowlist por property name). Blindaje del invariante para que una regresión que saque `navigator` del catálogo salte por dos lados (catálogo + audit).
194
+
195
+ ### Changed
196
+
197
+ - **ΔE error_threshold blindado 0.05 → 0.07 + axis-kobalium dark ratificado** (#154, PR #115). `scripts/perceptual-allowlist.json` v2: `error_threshold` subido a 0.07 sin recalibrar tokens. `axis-kobalium dark` (ΔE 0.0522) ratificado como excepción consciente vía allowlist (drift gate sigue activo). Cualquier par nuevo bajo 0.07 sin allowlist explícita rompe CI. SKILL.md alineado (PRs #115, #116).
198
+
199
+ - **DialogContext marcado `@internal`** (#162, PR #120). Los 4 exports del módulo (`DialogContextValue`, `DialogContext`, `useDialogContextOptional`, `useDialogContextRequired`) son `@internal`. `stripInternal: true` borra todo el `.d.ts` publicado (`export {}`). El bundle JS runtime sigue intacto. Verificado vía consumer-pack con `skipLibCheck: false`. El deep-import `AlertDialogClose → @/components/Dialog/DialogContext` es arquitectura D8 documentada (no deuda).
200
+
201
+ - **StepProps internal split completado** (#161, PR #119). Las 4 props que el `Stepper` inyecta vía `cloneElement` (`index`/`active`/`complete`/`labeled`) movidas de `StepProps` (público) a `StepInternalProps` (`@internal`). Justo con las 3 que ya estaban (`interactive`/`onActivate`/`onStepKeyDown`), `StepInternalProps` agrupa las 7 props internas. `StepProps` publicado queda con `label?: ReactNode` + `ref` + `extends HTMLAttributes`.
202
+
203
+ - **Size-budget mide payload real (no shim)** (#160, PR #126). `package.json` `size-limit` entries para JS bundles ahora incluyen el lazy chunk vía glob `dist/!(index|server-safe|cn).js` (extglob) — captura cualquier chunk transitivo que Rollup emita. Limits ajustados al payload real: `JS bundle ESM` 18.11 KB gz / 20 KB limit; `JS bundle server-safe ESM` 4.42 KB gz / 8 KB limit (vs 16 KB anterior cosmético).
204
+
205
+ - **Toast container JSDoc alineado con Tooltip/MenuContent** (#165, PR #127). `ToastProvider.container` documenta el patrón Dialog-nesting (top-layer del browser) + ejemplo Dialog-ref + referencia a `docs/decisions/C-02-modal-tooltip-portal.md`. Cierra la asimetría documental entre los 3 componentes que aceptan `container`.
206
+
207
+ ### Fixed
208
+
209
+ - **`dist/cn.d.ts` huérfano eliminado del tarball** (#159, PR #123). El `vite-plugin-dts` emite un `.d.ts` por cada entry, pero `exports` field apunta a `./dist/utils/cn.d.ts` (tsc-emitted, con sourcemap). Helper `rmFile` añadido a `scripts/clean-internal-dist.mjs`.
210
+
211
+ - **Dev-vulns devDep cero** (#163, PR #124). `npm audit` reportaba 3 vulns (`brace-expansion`, `tmp`, `ws`) en devDeps. Verificado `npm audit --omit=dev` = 0 vulns pre/post-fix (runtime nunca afectado). Bumps: brace-expansion 5.0.5 → 5.0.6, tmp 0.2.5 → 0.2.7, ws 8.20.0 → 8.21.0. 0 cambios en `package.json` (solo lockfile).
212
+
213
+ ### Documentation
214
+
215
+ - **Peer `@floating-ui/react ^0.27` pre-1.0 risk** (#156, PR #128). Sección nueva en README sobre la semántica especial del caret (^) en versiones 0.x: `^0.27.0` matchea SOLO `0.27.x` (NO `0.28.0`). Documenta la política del DS (mantener `^0.27.0` cerrado hasta verify FUI 0.28 compat) + recomendación al consumer (pinear `~0.27.0` si usa FUI directamente). Alineado con `docs/decisions/D10-fui-peer-dep-verify.md`.
216
+
217
+ ### EOPT widening — fronteras (consumer-facing)
218
+
219
+ El widening `?: T | undefined` de #155 cubre **307 props del DS**. Hay 2 clases NO cubiertas que el consumer EOPT debe conocer.
220
+
221
+ #### 1. Discriminantes con literal `?: undefined`
222
+
223
+ `MenuItem.href`, `SidebarItem.href`, `NavbarBrand.AsDiv.href`. NO ensanchadas porque rompería el discriminated union — son los literal `undefined` que activan la rama `button`/`div`:
224
+
225
+ ```tsx
226
+ <MenuItem href={undefined}>Acción button</MenuItem>
227
+ <MenuItem href="/perfil">Acción anchor</MenuItem>
228
+ ```
229
+
230
+ #### 2. Exclusiones con `?: never`
231
+
232
+ 8 props del inventory `scripts/eopt-classify.mjs --json` marcadas `OUT_OF_SCOPE_NEVER`. NO ensanchadas porque `never` es el mecanismo de exclusión de variantes en discriminated unions.
233
+
234
+ Inventario completo + clasificación AST en `scripts/eopt-classify.mjs`. Ver PR #114 / commit `9bdaf9a` del widening.
235
+
236
+ ## [1.0.0-beta.25] — 2026-05-25 (gate review cruce cycle complete)
237
+
238
+ Plan **claudegate4** + cruce de gate reviews independientes (GPT Codex +
239
+ Claude Opus 4.7 v2 + Opus 4.7 genérico) cierra los 5 blockers + 2 HIGH del
240
+ cruce sobre `1.0.0-beta.24`. 8 PRs mergeados (#96–#101 + #103), 2 fallos
241
+ WCAG cerrados (1.4.11 AA + 1.4.1 A), 2 codex P1/P2 cazados sobre mis
242
+ propios fixes, 1 hallazgo nuevo (alias `@/` en `.d.ts`) descubierto y
243
+ resuelto durante el ciclo, 1 decision doc nuevo (D12 i18n).
244
+
245
+ ### Added
246
+
247
+ - **`Rating.getStarLabel` prop**: override del aria-label por estrella
248
+ individual (`(n: number) => string`). Cierra el hueco i18n
249
+ detectado en el gate review beta.25 — el label del grupo ya era
250
+ overrideable vía `aria-label`, los hijos no lo eran. Default ES
251
+ permanece (`"1 estrella"` / `"N estrellas"`). Additive, no breaking.
252
+
253
+ ### Changed
254
+
255
+ - **i18n formalización (D12)**: defaults ES quedan registrados como
256
+ decisión arquitectónica en `docs/decisions/D12-es-defaults-i18n.md`.
257
+ Tabla de overrides en `docs/CSSAPI.mdx` ampliada con Avatar,
258
+ SkeletonContainer y Rating (faltaban). Comments
259
+ `// i18n: ES default deliberado (D12). Override: {prop}.` añadidos
260
+ junto a cada string ES default en código (Alert, Chip, DialogClose,
261
+ Pagination, Progress, Stepper, Toast, Avatar, Skeleton, Rating).
262
+ No cambia comportamiento — solo descubribilidad para auditorías
263
+ futuras.
264
+ - **Rating contraste empty (gate review beta.25 HIGH)**: el estado
265
+ empty se comunicaba via `opacity: 0.32` sobre el color filled. Codex
266
+ midió en igoded.es 1.885:1 light / 2.165:1 dark — ambos `< 3:1`
267
+ (WCAG 1.4.11 para UI components). La opacity también atenuaba el
268
+ focus-ring del button focusable. Reemplazo: capa semántica de
269
+ foreground atenuado (`.ig-star { color: var(--ig-text-muted) }`),
270
+ que pasa ≥3:1 en ambos temas (light 6.67:1, dark 5.90:1) y deja el
271
+ focus-ring intacto al 100% contraste.
272
+ - **Rating canal de forma (issue #102, WCAG 1.4.1 nivel A)**: la
273
+ diferenciación filled/empty viajaba solo por hue (rutilus cálido vs
274
+ text-muted purple-gray, luminancia casi idéntica → 1.85:1 light /
275
+ 1.88:1 dark). En daltonismo, escala de grises o `forced-colors:
276
+ active`, las 5 estrellas se ven iguales — el rating deja de
277
+ comunicar su valor. Reemplazo: glifo state-driven via `::before
278
+ { content }` — filled `★` (U+2605 BLACK STAR), empty `☆` (U+2606
279
+ WHITE STAR). Dos canales independientes (forma + color),
280
+ ortogonales: sobrevive a forced-colors por code point distinto.
281
+ Cambio sintáctico de la API CSS-only: el consumer ya NO escribe el
282
+ carácter dentro del `<span class="ig-star">` (queda vacío + recomendado
283
+ `aria-hidden="true"`); el glifo lo aporta CSS. Markup React JSX no
284
+ cambia (Rating sigue rendering el button, ahora sin children).
285
+
286
+ ### Removed
287
+
288
+ - **`--ig-rating-empty` (token público)**: muerto desde beta.24 (la
289
+ implementación lo abandonó por opacity-based). Sin consumers reales
290
+ pre-1.0.0, lo eliminamos para reducir surface API. La asimetría con
291
+ `--ig-rating-filled` (que sí queda público porque permite override
292
+ del cardinal `rutilus`) es deliberada: el empty consume la capa
293
+ semántica de foreground atenuado (`--ig-text-muted`) por ser un
294
+ color flat sin justificación de customización individual.
295
+
296
+ ## [1.0.0-beta.24] — 2026-05-19 (D1 server-safe infrastructure complete)
297
+
298
+ Plan **claudegate3** (post-RC1 gate review) cierra el bloque D1 con
299
+ 4 sub-tasks. Pre-tag de `1.0.0-rc.1` queda únicamente FREEZE-CHECK.
300
+
301
+ ### Added
302
+
303
+ **`DialogAction` — botón unstyled para CTAs del footer del Dialog**:
304
+
305
+ Mirror exacto de `AlertDialogClose` para la familia Dialog. Cierra el
306
+ modal via contexto (`setOpen(false)`) pero NO aplica ninguna clase
307
+ base. Diseñado para botones de acción del `DialogFooter` (Cancelar,
308
+ Aceptar, Entendido, etc.) donde el consumer pasa `className="ig-btn
309
+ ig-btn-..."` y espera que esa sea la única clase visual.
310
+
311
+ **Por qué se añade ahora**: usar `<DialogClose className="ig-btn
312
+ ig-btn-brand">Aceptar</DialogClose>` mezclaba la clase base de
313
+ `DialogClose` (`ig-dialog-close`, que fuerza `2rem × 2rem`, `padding: 0`
314
+ para la "×" del header) con la clase del Button del DS → footer
315
+ descuadrado. `AlertDialogClose` ya tenía esta forma desde D8 (codex P1
316
+ sobre PR #87) pero el Dialog quedó sin el equivalente, así que el
317
+ patrón natural del consumer producía un visual roto.
318
+
319
+ Migración consumer (no breaking, paralelo a DialogClose):
320
+
321
+ ```tsx
322
+ // Antes (footer descuadrado por clases en conflicto):
323
+ <DialogFooter>
324
+ <DialogClose className="ig-btn ig-btn-secondary">Cancelar</DialogClose>
325
+ <DialogClose className="ig-btn ig-btn-brand">Aceptar</DialogClose>
326
+ </DialogFooter>
327
+
328
+ // Ahora (sizing del Button respetado):
329
+ <DialogFooter>
330
+ <DialogAction className="ig-btn ig-btn-secondary">Cancelar</DialogAction>
331
+ <DialogAction className="ig-btn ig-btn-brand">Aceptar</DialogAction>
332
+ </DialogFooter>
333
+ ```
334
+
335
+ `<DialogClose />` queda como el botón "×" del header (sigue intocado).
336
+ Stories actualizadas (Default + Information). Tests añadidos: cierre
337
+ via context, no aplica `ig-dialog-close`, `onClick.preventDefault()`
338
+ bloquea el cierre.
339
+
340
+ ### Fixed
341
+
342
+ **Rating — distinción visual filled/empty robusta + contraste light mode**:
343
+
344
+ Patrón canónico Material UI / Mantine / shadcn: ambos estados usan
345
+ el mismo glifo `★` solid; la diferenciación es por **opacity** sobre
346
+ el mismo color base (`--ig-rating-filled`):
347
+
348
+ - Vacía: ★ a opacity 0.32 sobre `--ig-rating-filled`.
349
+ - Llena: ★ a opacity 1.
350
+
351
+ Por qué este patrón:
352
+
353
+ - El glifo solid (`★`) tiene antialiasing más visible que el outline
354
+ (`☆`) sobre fondos near-white. En light mode el ☆ outline en
355
+ `--ig-rating-empty` (#685080 sobre #faf9fc) leía casi como bg.
356
+ - Mantener el mismo color base preserva el matiz coherente (cobre
357
+ rutilus) en ambos estados. La opacity baja conserva el hue sin
358
+ desaturarlo, evitando blend con el fondo.
359
+ - La diferencia visual filled vs empty es ahora ratio 3:1 (1.0 vs
360
+ 0.32), independiente del tema activo.
361
+
362
+ Limpieza adicional: removidas reglas CSS-only que duplicaban el hover
363
+ preview (`.ig-rating:not(.ig-rating-readonly):hover .ig-star` y
364
+ `.ig-rating:not(.ig-rating-readonly) .ig-star:hover ~ .ig-star`).
365
+ Eran redundantes — `setHover` en JSX ya aplica `ig-star-filled` a las
366
+ estrellas en el preview. La regla con sibling combinator `~` tenía
367
+ specificity (0,5,0) que sobreescribía el estado, produciendo
368
+ visual inconsistente cuando el cursor permanecía sobre el rating
369
+ tras el click. Ahora el `ig-star-filled` class (estado JSX) es la
370
+ única fuente de verdad.
371
+
372
+ `--ig-rating-empty` token mantenido en `igoded-tokens.css` por
373
+ backward-compat con consumers que lo extendieran, pero el CSS del
374
+ componente ya no lo referencia.
375
+
376
+ ### Added (beta.24)
377
+
378
+ **D1-P3 — `reactigoded/server-safe` entry + `react-server` conditional export**:
379
+
380
+ Nuevo subpath export `reactigoded/server-safe` + condicional automático
381
+ `"react-server"` en `package.json#exports["."]` que re-exporta solo los
382
+ 36 componentes marcados `@server-safe` (verificados por el gate
383
+ `test:server-safe-markers`).
384
+
385
+ Consumer pattern (Next.js App Router Server Component, Astro server
386
+ island, Remix loader, etc.):
387
+
388
+ ```tsx
389
+ // app/page.tsx (server, sin "use client")
390
+ import { Button, CardBody, CardHeader } from "reactigoded";
391
+ // → resuelve a dist/server-safe.js via react-server condition
392
+ ```
393
+
394
+ Componentes interactivos (Dialog/Menu/Toast Provider/Tooltip/Switch/
395
+ ThemeToggle) siguen importables desde el barrel root cuando el
396
+ consumer está en Client Component (`"use client"`).
397
+
398
+ CI gates añadidos:
399
+
400
+ - `test:publint`: validador de exports config (sustituye attw temporalmente
401
+ por bug fflate en ARM64 — ver `docs/known-issues.md`).
402
+ - `test:rsc-fixture`: `tsc -p fixtures/rsc/tsconfig.json --noEmit` con
403
+ `customConditions: ["react-server"]` + paths a `dist/server-safe.d.ts`.
404
+ Cubre regresiones del subset server-safe.
405
+ - `test:server-safe-markers` (retroactivo de D1-P1): añadido al
406
+ workflow `verify.yml` que antes solo lo corría localmente.
407
+
408
+ Sin cambios breaking — el barrel root sigue exportando todos los componentes.
409
+
410
+ ### Changed (BREAKING — pre-RC1, beta.24)
411
+
412
+ **D1-P4 — ESM-only package**:
413
+
414
+ reactigoded ahora es **ESM-only**. La rama CJS de `package.json#exports`
415
+ queda eliminada, junto con los emits `dist/*.cjs` y los campos `main`
416
+ + `module` del manifest. El paquete sigue declarando `"type": "module"`
417
+ (introducido en beta.22).
418
+
419
+ Migración consumer:
420
+
421
+ ```js
422
+ // Antes (CJS, ya no soportado):
423
+ const { Button } = require("reactigoded");
424
+
425
+ // Ahora (ESM):
426
+ import { Button } from "reactigoded";
427
+
428
+ // Si necesitas usarlo desde un CJS file legacy, Node ≥22 soporta
429
+ // require() sincrónico de ESM nativamente:
430
+ const { Button } = require("reactigoded");
431
+
432
+ // O dynamic import:
433
+ const { Button } = await import("reactigoded");
434
+ ```
435
+
436
+ `package.json#exports` simplificado:
437
+
438
+ ```json
439
+ {
440
+ ".": {
441
+ "react-server": {
442
+ "types": "./dist/server-safe.d.ts",
443
+ "default": "./dist/server-safe.js"
444
+ },
445
+ "default": {
446
+ "types": "./dist/index.d.ts",
447
+ "default": "./dist/index.js"
448
+ }
449
+ },
450
+ "./server-safe": { "types": "...", "default": "./dist/server-safe.js" },
451
+ "./cn": { "types": "...", "default": "./dist/cn.js" }
452
+ }
453
+ ```
454
+
455
+ Razones técnicas:
456
+
457
+ - **Coherencia**: el DS ya era ESM-first (`"type": "module"` desde beta.22),
458
+ el output CJS era un fallback que pocos consumers usaban.
459
+ - **publint clean**: el warning `pkg.exports["."].require.types` sobre
460
+ types ESM interpretadas en condición require queda eliminado (no más
461
+ rama `require`).
462
+ - **Bundle output**: menos artefactos en `dist/` (no `*.cjs`), tamaño
463
+ total del paquete publicado más ligero.
464
+ - **Tooling moderno**: Vite, Next.js > 13, Astro, Remix, Bun, Deno
465
+ soportan ESM nativamente. Node 22 LTS (mínimo del DS desde
466
+ `"engines": { "node": ">=22.12.0" }`) soporta `require()` sincrónico
467
+ de ESM.
468
+
469
+ CI workflow actualizado: `dist/index.cjs` removido del check de
470
+ "Bundle has no dev warns".
471
+
472
+ ---
473
+
474
+ **D1-P2 — clsx promoted from dependency to peerDependency**:
475
+
476
+ `clsx` (^2.1.0) ahora es **peer-dependency obligatoria**. Antes era una
477
+ dep regular bundleada — el consumer tenía `clsx` instalada de forma
478
+ implícita. A partir de beta.24, el consumer debe instalarla
479
+ explícitamente en su árbol de deps.
480
+
481
+ Migración:
482
+
483
+ ```bash
484
+ npm install clsx@^2.1.0
485
+ # o
486
+ pnpm add clsx@^2.1.0
487
+ # o
488
+ yarn add clsx@^2.1.0
489
+ ```
490
+
491
+ Razones técnicas:
492
+
493
+ - **Deduplicación**: la mayoría de consumers (apps con Tailwind, shadcn/ui,
494
+ proyectos existentes que ya combinan classNames) ya tienen `clsx` en su
495
+ árbol. Bundle-arla en reactigoded duplica el módulo en runtime sin valor
496
+ funcional.
497
+ - **Bundle size**: el ESM principal pierde ~500B gz, el CJS un poco más.
498
+ - **Coherencia D1 server-safe**: con `@server-safe` enforced en beta.24,
499
+ los chunks publicados están auditados — externalizar peer-deps refuerza
500
+ que el árbol de bundling sea predecible para SSR/RSC consumers.
501
+
502
+ Decisión documentada inline en `package.json#peerDependencies` y
503
+ `vite.lib.config.ts#rollupOptions.external`. La decisión previa
504
+ (beta.20 "clsx bundleado permanente") queda invalidada por el plan D1.
505
+
506
+ ---
507
+
508
+ **D2 + D7 — Menu Full FUI portal + namespace reorg** (B1-PR3):
509
+
510
+ - **Menu file move**: `src/components/Menu/` → `src/components/floating/Menu/`.
511
+ Public API root barrel `reactigoded` sin cambio (símbolo `Menu` se
512
+ exporta idéntico). Internal cross-component imports actualizados.
513
+ - **Menu Full FUI portal**: `<FloatingPortal>` + `floatingStyles` inline +
514
+ `data-side` / `data-align` / `data-state` attributes (split Radix-style).
515
+ Unmount-on-close (no más CSS-hidden).
516
+ - **Modifier classes eliminadas**: `.ig-menu-right`, `.ig-menu-up`,
517
+ `.ig-menu-open`, `:focus-within` JS-less fallback. Consumers que las
518
+ stylizaban en su CSS deben migrar a `[data-side]` / `[data-align]`
519
+ attribute selectors en MenuContent.
520
+ - **Menu requires JS**: alineado con Tooltip/Dialog/Accordion. No más
521
+ CSS-only fallback `:focus-within`.
522
+ - **floating/ namespace agrupado**: `src/components/floating/index.ts`
523
+ nuevo barrel re-exporta `./primitives + ./Tooltip + ./Menu`. En
524
+ `src/components/index.ts` los 3 wildcards floating sueltos se colapsan
525
+ a uno (`export * from "./floating"`). Sin cambio para consumer.
526
+ - **useFloatingNode internal definitive**: retirado del wildcard
527
+ re-export de `floating/primitives/index.ts`. JSDoc declaraba @example
528
+ interno pero export * lo exponía. Internal consumers ya importan via
529
+ path directo `@/components/floating/primitives/useFloatingNode`.
530
+ Consumers que (accidentalmente) lo importaban desde `"reactigoded"`
531
+ deben removerlo — uso público era unintended.
532
+
533
+ Decision docs: `docs/decisions/D2-menu-portal.md`,
534
+ `docs/decisions/D7-floating-namespace.md`,
535
+ `src/components/floating/README.md`. C-03 doc status actualizado
536
+ "diferida a 1.1.0" → "DONE en beta.24".
537
+
538
+ ---
539
+
540
+ C.2 DS-wide rename pattern: callbacks aligned al prop name local cuando
541
+ el prop tiene nombre de dominio (no "value" arbitrario). Sub-patrón DS
542
+ articulado en `docs/decisions/D3-callback-rename-cw.md` y
543
+ `docs/decisions/D4-sidebar-api.md`.
544
+
545
+ **Pagination** — prop rename + callback rename:
546
+ - `currentPage?: number` → `page?: number`.
547
+ - `defaultPage` queda.
548
+ - `onValueChange?: (page: number) => void` →
549
+ `onPageChange?: (page: number) => void`.
550
+ - Param signature `(page: number)` queda.
551
+
552
+ Migration consumer:
553
+ ```diff
554
+ - <Pagination currentPage={p} onValueChange={setP} totalPages={N} />
555
+ + <Pagination page={p} onPageChange={setP} totalPages={N} />
556
+ ```
557
+
558
+ **Stepper** — callback rename solo:
559
+ - Prop `active` queda. Callback `onValueChange` → `onActiveChange`.
560
+ - Param signature `(next: number)` queda.
561
+
562
+ ```diff
563
+ - <Stepper active={s} onValueChange={setS}>...</Stepper>
564
+ + <Stepper active={s} onActiveChange={setS}>...</Stepper>
565
+ ```
566
+
567
+ **ThemeToggle** — callback rename solo:
568
+ - Props `theme` + `defaultTheme` quedan. Callback `onValueChange` →
569
+ `onThemeChange`.
570
+
571
+ ```diff
572
+ - <ThemeToggle theme={t} onValueChange={setT} />
573
+ + <ThemeToggle theme={t} onThemeChange={setT} />
574
+ ```
575
+
576
+ **Sidebar** — callback rename + SidebarItem a11y refactor:
577
+ - Props `collapsed` + `defaultCollapsed` quedan. Callback `onValueChange`
578
+ → `onCollapsedChange`.
579
+ - CSS class `.ig-sidebar-collapsed` queda. ARIA `aria-expanded={!collapsed}`
580
+ queda (encapsulado en SidebarToggle).
581
+ - **SidebarItem** ahora consume `useSidebar()` context para aplicar
582
+ `aria-label` condicionalmente: aplicado solo en estado collapsed
583
+ (ARIA APG fix — eliminado anti-pattern de nombre accesible duplicado
584
+ en estado expanded).
585
+ - **SidebarItem pasa a `"use client"`** (consume context). Sale de
586
+ candidatos `@server-safe` D1 P1.
587
+ - **SidebarItem REQUIERE estar dentro de `<Sidebar>`** (D11.4 regla
588
+ DS-wide: hooks que requieren ancestor lanzan).
589
+
590
+ `useSidebar` JSDoc reescrito eliminando `@example PersistSidebar`
591
+ (anti-pattern declaraba uso público mientras bundle no expone el hook
592
+ desde B-04 RC1). Pointer a README sección "Persisting Sidebar state"
593
+ (a añadir en B1-PR1 sweep) para patrón consumer-facing con controlled
594
+ mode external + guard SSR-safe en localStorage.
595
+
596
+ **Componentes aligned (sin cambios)**: Accordion, Rating, Slider, Tabs
597
+ — sus props se llaman `value`, callbacks `onValueChange` son aligned
598
+ localmente.
599
+
600
+ ---
601
+
602
+ **D6 — Dialog Full compound refactor** (B2-PR2):
603
+
604
+ **BREAKING**. `Dialog` pasa de ser un componente monolítico (era el
605
+ `<dialog>` HTML nativo con children directos) a Provider compound
606
+ Radix-style. La lógica del modal real vive en el nuevo `<DialogContent>`;
607
+ `<DialogTrigger>` (nuevo) abre el modal con ARIA disclosure correcto.
608
+
609
+ API nueva:
610
+
611
+ ```tsx
612
+ <Dialog
613
+ open?={x} // controlled (opcional, era obligatorio)
614
+ defaultOpen?={false} // uncontrolled (nuevo)
615
+ onOpenChange?={fn}
616
+ >
617
+ <DialogTrigger>Abrir</DialogTrigger> // nuevo, aria-haspopup/-controls/-expanded
618
+ <DialogContent // nuevo, el <dialog> real
619
+ size? backdrop? closeOnBackdrop?
620
+ closeOnEsc? loading? ref? className?
621
+ >
622
+ <DialogHeader />
623
+ <DialogBody />
624
+ <DialogClose /> // ahora auto-cierra via contexto
625
+ </DialogContent>
626
+ </Dialog>
627
+ ```
628
+
629
+ **Migration mecánica** consumer:
630
+
631
+ ```diff
632
+ - <Dialog open={x} onOpenChange={fn} size="md">
633
+ - <DialogHeader />
634
+ - <DialogBody>texto</DialogBody>
635
+ - <DialogClose onClick={() => setOpen(false)} />
636
+ - </Dialog>
637
+ + <Dialog open={x} onOpenChange={fn}>
638
+ + <DialogContent size="md">
639
+ + <DialogHeader />
640
+ + <DialogBody>texto</DialogBody>
641
+ + <DialogClose />
642
+ + </DialogContent>
643
+ + </Dialog>
644
+ ```
645
+
646
+ Opcional: para casos sencillos, eliminar `useState` externo + `Button`
647
+ + `onClick` y usar `<DialogTrigger>` + `defaultOpen`:
648
+
649
+ ```diff
650
+ - const [open, setOpen] = useState(false);
651
+ - <Button onClick={() => setOpen(true)}>Abrir</Button>
652
+ - <Dialog open={open} onOpenChange={setOpen}>
653
+ - <DialogHeader />
654
+ - </Dialog>
655
+ + <Dialog defaultOpen={false}>
656
+ + <DialogTrigger className="ig-btn ig-btn-brand">Abrir</DialogTrigger>
657
+ + <DialogContent>
658
+ + <DialogHeader />
659
+ + </DialogContent>
660
+ + </Dialog>
661
+ ```
662
+
663
+ Cambios técnicos:
664
+ - `Dialog` ahora es Provider puro (sin DOM). `useControllableState`
665
+ interno con `SUPPRESS_NO_HANDLER_WARN` en modo controlled-presentational
666
+ (`active` sin callback, decisión consciente del consumer).
667
+ - `DialogContent` hereda TODAS las props visuales antiguas de `Dialog`:
668
+ `size`, `backdrop`, `closeOnBackdrop`, `closeOnEsc`, `loading`, `ref`,
669
+ `className`, `onPointerDown`/`onClick` chained. H-02 drag-out parity
670
+ preservado.
671
+ - `DialogTrigger` es `<button>` plano con `aria-haspopup="dialog"` +
672
+ `aria-controls={contentId}` + `aria-expanded={open}` automáticos.
673
+ Chainea `onClick` consumer; `preventDefault` bloquea apertura.
674
+ - `DialogClose` consume el contexto y llama `setOpen(false)` automático.
675
+ Sigue siendo tolerante fuera de `<Dialog>` (no rompe, simplemente no
676
+ cierra nada — responsabilidad del consumer via `onClick`).
677
+ - Alias backward-compat de tipos: `DialogSize` y `DialogBackdrop` ahora
678
+ son alias de `DialogContentSize` / `DialogContentBackdrop`. Consumers
679
+ que importaban los tipos del barrel siguen funcionando.
680
+ - Prop `onClose` (deprecated en B-02 RC1) sigue disparando en cierres
681
+ via wrapper en setOpen; eliminada en 2.0.
682
+
683
+ Decision doc: `docs/decisions/D6-dialog-compound.md`.
684
+
685
+ ---
686
+
687
+ **D1-P1 — `@server-safe` JSDoc marker + suite SSR ejecutable** (D1 server-safe, parte 1/4):
688
+
689
+ Establece la infraestructura declarativa + ejecutable para SSR / React
690
+ Server Components compatibility. Sin breaking — solo añade marker
691
+ JSDoc + gate script + double-render test.
692
+
693
+ - **JSDoc `@server-safe`** en 36 componentes — todos los archivos sin
694
+ `"use client"` directive en `src/components/`. Includes:
695
+ `Avatar/AvatarGroup`, `Badge`, `Breadcrumb/BreadcrumbItem`,
696
+ `Button`/`IconButton`, `Card`+subcomponentes, `Chip`, `Divider`,
697
+ `Dialog/DialogBody/DialogFooter`, `Input`+`Label`/`Helper`/etc.,
698
+ `Navbar`+subcomponentes, `Progress`, `Radio`, `Sidebar` (subs
699
+ presentacionales), `Skeleton`, `Spinner`, `Stepper`/`Step`, `Table`,
700
+ `Timeline`/`TimelineItem`, `Toast` (item; el Provider es client).
701
+ - **Gate** `scripts/check-server-safe-markers.mjs` +
702
+ `npm run test:server-safe-markers` (encadenado en `verify:unit`):
703
+ enforza que archivos `@server-safe` NO declaren `"use client"` y NO
704
+ accedan a `document.X`/`window.X`/`navigator.X`/`process.X`/`Buffer.X`
705
+ sin guard `typeof X !== "undefined"`.
706
+ - **Test double-render idempotence** en `__ssr__.test.tsx`: dos
707
+ invocaciones de `renderToString(jsx)` con el mismo JSX producen el
708
+ mismo HTML. Skip components con `useId` (Accordion, Dialog, Menu,
709
+ Sidebar, Tabs, Tooltip, Toast) por semántica de `useId` entre
710
+ invocaciones independientes.
711
+ - Decision doc: `docs/decisions/D1-P1-server-safe-marker.md`.
712
+
713
+ Componentes adicionales se marcarán incrementalmente post-RC1 conforme
714
+ se audite cada uno.
715
+
716
+ ---
717
+
718
+ **MEDIUMs consolidados beta.24** (B3-PR1):
719
+
720
+ Batch de fixes individuales identificados en el gate review claudegate3
721
+ sección IV (EXC-A1/A2). Atómicos para bisect-friendliness — un commit
722
+ por finding:
723
+
724
+ - **M-04 (Tooltip.extractText)**: investigado y descartado tras
725
+ análisis (codex P2 round 2 sobre PR #89). El finding original asumía
726
+ que `join("")` rompía spacing en `<>Hello <b>World</b>!</>`, pero
727
+ JSX YA preserva los espacios en los strings literales adyacentes
728
+ (`children = ["Hello ", <b>"World"</b>, "!"]`). Inyectar `" "` rompía
729
+ casos legítimos de tokens contiguos (`<>v<code>1</code>.<code>2</code></>`
730
+ → "v 1 . 2" en lugar de "v1.2"). El behavior actual `join("")` es
731
+ correcto — el consumer es dueño de los espacios. Añadido test
732
+ regression guard "preserva adjacencia de tokens".
733
+ - **M-05 (Label required)**: el asterisco usaba `style={{ marginLeft: 4 }}`
734
+ inline (CSP issue). Migrado a CSS class `ig-label-required-mark` con
735
+ margin + color del token `--ig-malum`. **Bonus fix**: el CSS
736
+ `.ig-label-required::after` también renderizaba un asterisco — junto
737
+ al `<span>*</span>` del JSX producía DOBLE asterisco visible. Fix
738
+ inicial (eliminar `::after`) rompía el caso CSS-only API (HTML
739
+ estático sin React perdía el marker — codex P2 round 2). Fix final:
740
+ `:has(.ig-label-required-mark)` condiciona el `::after` a aplicar
741
+ SOLO cuando el span React no está, preservando ambas APIs (React +
742
+ CSS-only) sin doble asterisco.
743
+ - **M-07 (landmark registries)**: `useLandmarkRegistry` +
744
+ `useTopLevelLandmarkCheck` mantienen estado module-level (Map y
745
+ contador). Sin reset entre tests acumulaban entries. Los helpers
746
+ `__reset*ForTests` existían pero solo se invocaban manualmente en
747
+ sus tests dedicados. Centralizados en `src/test/setup.ts` `afterEach`
748
+ para que todos los tests hereden el cleanup.
749
+ - **M-10 (D-02 doc grep)**: `docs/decisions/D-02-forwardref-migration-done.md`
750
+ afirmaba `grep -rn "forwardRef" → vacío esperado` pero realidad
751
+ muestra 11 matches (dev-warn references + test fixtures, no DS-side
752
+ usage). Refinado al call-form `grep -rn "forwardRef("` que sí
753
+ retorna 0 y captura el invariante real ("ningún componente del DS
754
+ INVOCA forwardRef").
755
+
756
+ Findings descartados sin cambio (audit confirmó OK):
757
+ - **M-06 (useA11yWarnInput)**: hook ya robusto.
758
+ - **M-08 (Toast timers)**: cleanup ya correcto via `afterEach`.
759
+ - **M-09 (Twin act() warning)**: no encontrado en logs.
760
+ - **M-11 (C-03 doc DONE)**: ya marcado tras D2/D7 beta.24.
761
+ - **M-13 (Switch indeterminate)**: ya conforme H-15 WAI-ARIA 1.2.
762
+ - **M-14 (Stepper test consolidation)**: refactor cosmético; deferido
763
+ a iteración futura — los tests cubren regresiones reales (codex P1+P2
764
+ históricos) y consolidar pierde trazabilidad por marginal LOC saving.
765
+
766
+ Verify cold: 836/836 sin regresión.
767
+
768
+ ---
769
+
770
+ **README sweep masivo post-beta.24** (B1-PR1):
771
+
772
+ `README.md` y `docs/CSSAPI.mdx` tenían drift acumulado de los renames
773
+ y refactors de beta.24. Sweep sistemático:
774
+
775
+ - **Pagination API** (D3): `currentPage`/`onValueChange` → `page`/
776
+ `onPageChange` en README sección "Controlled vs uncontrolled" y
777
+ CSSAPI.mdx § Pagination.
778
+ - **Dialog compound** (D6): tabla de componentes, ejemplos de uso,
779
+ sección SSR/hydration, CSS API. `Modal*` → `Dialog*` / `DialogContent`
780
+ / `DialogTrigger` / etc. La línea "Modal es controlled-only" eliminada
781
+ (ya no aplica: Dialog soporta uncontrolled con DialogTrigger).
782
+ - **AlertDialog family** (D8): añadido a tabla de componentes + tabla
783
+ CSS API + sección SSR (`role="alertdialog"` + `closeOnBackdrop=false`
784
+ default mencionado).
785
+ - **Menu rename** (D2/D7): `OptionsMenu*` → `Menu*` (refactor Full FUI
786
+ beta.24 + namespace reorg).
787
+ - **Tabs subcomponentes**: `TabList`/`Tab`/`TabPanel` → `TabsList`/
788
+ `TabsTrigger`/`TabsContent` (alineado con exports reales del barrel).
789
+ - **D9 budget claim**: "~14 KB gz" → "~16 KB gz (budget 20 KB ESM con
790
+ headroom)".
791
+ - **Tabla controlled/uncontrolled DS-wide** añadida bajo § Patrones
792
+ recurrentes, listando los 10 componentes con la convención
793
+ `{prop}?` + `default{Prop}?` + `on{Prop}Change` cerrada en beta.24.
794
+ - **D11 hooks disposition**: sección "Hooks públicos del DS" ya
795
+ existía; el bloque B-04 sobre context hooks privados se renombró a
796
+ D11 para reflejar la disposición canónica beta.24.
797
+
798
+ Solo cambios docs. Cero runtime. Verify cold 836/836 sin regresión.
799
+
800
+ ---
801
+
802
+ **D8 — AlertDialog family** (B2-PR3):
803
+
804
+ Nueva familia para confirmaciones destructivas / acciones que demandan
805
+ atención consciente. Hereda toda la infraestructura compound de Dialog
806
+ (D6); el único componente con comportamiento propio es
807
+ `<AlertDialogContent>`. Los demás (`AlertDialog`, `AlertDialogTrigger`,
808
+ `AlertDialogHeader/Body/Footer/Close`) son aliases directos de los
809
+ equivalentes Dialog — cero overhead, cero divergence.
810
+
811
+ ```tsx
812
+ <AlertDialog defaultOpen={false}>
813
+ <AlertDialogTrigger className="ig-btn ig-btn-danger">
814
+ Borrar permanentemente
815
+ </AlertDialogTrigger>
816
+ <AlertDialogContent>
817
+ <AlertDialogHeader>
818
+ <h2>Confirmar borrado</h2>
819
+ </AlertDialogHeader>
820
+ <AlertDialogBody>Esta acción es irreversible.</AlertDialogBody>
821
+ <AlertDialogFooter>
822
+ <AlertDialogClose className="ig-btn ig-btn-secondary">
823
+ Cancelar
824
+ </AlertDialogClose>
825
+ <AlertDialogClose className="ig-btn ig-btn-danger">
826
+ Sí, borrar
827
+ </AlertDialogClose>
828
+ </AlertDialogFooter>
829
+ </AlertDialogContent>
830
+ </AlertDialog>
831
+ ```
832
+
833
+ Diferencias de `AlertDialogContent` vs `DialogContent`:
834
+
835
+ 1. **`role="alertdialog"`** (override del `role="dialog"` automático del
836
+ `<dialog>` HTML). WAI-ARIA APG: indica que el modal demanda atención
837
+ consciente del usuario.
838
+ 2. **`closeOnBackdrop={false}` por defecto** (vs `true` en
839
+ `DialogContent`). Click outside NO cierra — el usuario debe pulsar
840
+ Cancel o Confirm explícitamente. Override por consumer si su caso
841
+ es más suave: `<AlertDialogContent closeOnBackdrop />`.
842
+
843
+ `closeOnEsc` sigue `true` (atajo ergonómico universal).
844
+
845
+ 7 tests cubren las diferencias D8 + composición end-to-end. Decision
846
+ doc: `docs/decisions/D8-alert-dialog.md`. No breaking — D8 es API
847
+ nueva pura.
848
+
849
+ ---
850
+
851
+ ### Fixed (non-breaking, beta.24)
852
+
853
+ **H-03 — Progress CSP-friendly via CSS custom property** (B2-PR5):
854
+
855
+ Pre-fix, `Progress.tsx` emitía el porcentaje runtime como propiedad
856
+ CSS arbitraria en el style attribute:
857
+
858
+ ```html
859
+ <div class="ig-progress-bar" style="width: 42%"></div>
860
+ ```
861
+
862
+ Esto rompía CSP estricto sin `'unsafe-inline'` en `style-src`. M-08
863
+ (RC1) lo había documentado como excepción legítima; la auditoría
864
+ beta.24 lo re-clasificó como bug con mitigación canónica disponible.
865
+
866
+ Fix: sustituir style attribute con propiedad arbitraria por un único
867
+ **CSS custom property**:
868
+
869
+ ```html
870
+ <div class="ig-progress-bar" style="--ig-progress-percent: 42%"></div>
871
+ ```
872
+
873
+ Stylesheet ahora consume la variable: `.ig-progress-bar { width:
874
+ var(--ig-progress-percent, 0%); }`. La lógica visual vive en
875
+ stylesheet; el componente solo pasa datos. Auditores CSP modernos
876
+ y políticas con `'unsafe-hashes'` tratan `style="--var: value"`
877
+ distinto a inline rules visuales — `'unsafe-hashes'` sobre el
878
+ conjunto finito de valores `--ig-progress-percent` (0%..100%) es
879
+ ahora viable. Patrón canónico Radix/Mantine/MUI Joy.
880
+
881
+ **API React sin cambio**. Consumers que pasaban `value`/`max`/etc.
882
+ no notan nada. CSS-only consumers que copiaban el snippet con
883
+ `style="width: X%"` ven el snippet en docs actualizado a
884
+ `style="--ig-progress-percent: X%"`; el viejo formato sigue
885
+ funcionando (specificity del inline style gana al stylesheet) pero
886
+ NO es CSP-friendly.
887
+
888
+ Decision doc: `docs/decisions/H-03-progress-csp-css-var.md`.
889
+
890
+ ---
891
+
892
+ **D11 — Disposición pública de hooks del DS** (B2-PR8):
893
+
894
+ Cierra la disposición canónica de los símbolos hook/factory expuestos
895
+ por `"reactigoded"`. Sin breaking — solo formaliza intención con JSDoc
896
+ tags + un alias adicional + documentación.
897
+
898
+ - **Nuevo alias `UseToastReturn`** en `src/components/Toast/ToastContext.ts`,
899
+ re-exportado desde `Toast/index.ts`. Sigue la convención DS-wide
900
+ `Use{Name}Return` (cf. `UseThemeReturn`, `UseControllableStateReturn`).
901
+ El interfaz underlying `ToastContextValue` queda exportado para mocks
902
+ / adapters; el nombre canónico que el consumer tipa al destructurar
903
+ `useToast()` es `UseToastReturn`.
904
+ - **JSDoc `@public`** en `useTheme`, `useControllableState`, `useToast`,
905
+ `FloatingTreeRoot`. Declaración de intención: estos cuatro símbolos
906
+ son API pública estable 1.0.
907
+ - **JSDoc `@internal`** en `useFloatingNode` con pointer explícito a
908
+ `FloatingTreeRoot` como entrypoint público para anidación de floats.
909
+ El hook YA estaba retirado del wildcard re-export (D7.4 beta.24); el
910
+ tag explicita la intención.
911
+ - **README sección "Hooks públicos del DS"** (bajo § Patrones recurrentes)
912
+ con tabla de los cuatro símbolos públicos + nota explícita de qué
913
+ hooks NO son públicos (`useDropdown` / `useTabs` / `useFloatingNode`
914
+ / `useSidebar` / etc.).
915
+
916
+ Patrón organizacional: hooks generic (`useTheme`, `useControllableState`)
917
+ viven en `src/hooks/` y se exportan explícitamente desde `src/index.ts`.
918
+ Hooks family-specific (`useToast`, `FloatingTreeRoot`) viven en
919
+ `src/components/<Family>/` y se exportan via la barrel chain de la
920
+ familia. Razonamiento detallado: `docs/decisions/D11-hooks-disposition.md`.
921
+
922
+ ---
923
+
924
+ **D9 — size-limit re-baseline + budget creep policy + H-07 gate ejecutable** (B2-PR7):
925
+
926
+ Re-baseline de los budgets `size-limit` del JS bundle tras los cambios
927
+ de beta.24 (Menu Full FUI portal, callback renames, Avatar fallback,
928
+ Progress CSP, hooks disposition). El margen ESM había caído a 0.18 KB
929
+ (1.1%) — funcionalmente cero, cualquier PR mediano rompía CI por scope
930
+ creep accidental. CSS budgets se mantienen en su zona 23-30% headroom
931
+ post-H-13 (sin drift en beta.24).
932
+
933
+ **Nuevos budgets JS** (25-30% headroom paralelo, política H-13):
934
+
935
+ | Entry | Real beta.24 | Anterior | **Nuevo** | Headroom |
936
+ |---|---|---|---|---|
937
+ | JS ESM (gzip) | 15.82 KB | 16 KB | **20 KB** | 26.4% |
938
+ | JS CJS (gzip) | 14.06 KB | 15 KB | **18 KB** | 28.0% |
939
+
940
+ CSS budgets sin cambio.
941
+
942
+ **Budget creep policy** documentada (`D9-size-limit-baseline.md`):
943
+ - Headroom target 25-30%.
944
+ - Subir budget solo por feature documentada o dep upstream con
945
+ justificación + cálculo de nuevo budget en el mismo PR.
946
+ - NO subir budget por refactor, dep bumps cosméticos, comodidad de
947
+ iteración o "CI falla, fix rápido".
948
+
949
+ **H-07 gate ejecutable**: `scripts/check-state-css-exclusion.mjs` +
950
+ `npm run test:state-css-exclusion`, encadenado en `verify:unit`
951
+ post-build. Verifica que ninguna utility de `state.css` (clases con
952
+ prefijo `hover:ig-`, `focus:ig-`, `active:ig-`, `disabled:ig-`,
953
+ `checked:ig-`, `default:ig-`, `empty:ig-`, `first-child:ig-`,
954
+ `last-child:ig-`) aparezca en `dist/index.js` ni `dist/index.cjs`. Si
955
+ un componente del DS empezara accidentalmente a referenciarlas (713 KB
956
+ gz standalone), el gate explota pre-publish. Cierra el vector de
957
+ escape que H-07 había dejado como deuda (decisión "conservar" sin
958
+ gate ejecutable de invariante).
959
+
960
+ Decision docs: `docs/decisions/D9-size-limit-baseline.md` (nuevo),
961
+ `docs/decisions/H-07-state-css-and-future.md` (actualizado con
962
+ pointer al gate).
963
+
964
+ ---
965
+
966
+ **H-08 — hydrateRoot SSR tests** (B2-PR6):
967
+
968
+ Extensión de la suite SSR (`src/__ssr__.test.tsx`) con 4 nuevos casos
969
+ que ejercitan el **ciclo completo** server → cliente: `renderToString`
970
+ → mount en `<div>` DOM → `hydrateRoot` con `onRecoverableError` →
971
+ assert no mismatch + DOM intacto post-hidratación. Cubre la categoría
972
+ de bugs que `renderToString` solo no caza:
973
+
974
+ 1. **Hydration mismatch**: HTML server difiere del primer paint
975
+ cliente. React reconstruye el subtree y pierde estado/handlers.
976
+ 2. **useId divergente**: si la posición de tree es no-determinística
977
+ entre server y cliente, los IDs no matchean. React 19 garantiza
978
+ determinismo si el tree es idéntico — este test confirma que los
979
+ componentes del DS lo son.
980
+ 3. **DOM mutation post-hydrate**: effect síncrono mal diseñado
981
+ sobreescribe el HTML del server.
982
+
983
+ **Selección de 4 componentes representativos** (no toda la suite — los
984
+ 38 `renderToString` cases ya cubren server-safety; hidratación es spot
985
+ check del ciclo completo):
986
+
987
+ - **Button** — golden path simple, sin context ni useId.
988
+ - **Card (compound)** — contenedor con children compound. Caso layout.
989
+ - **Accordion (controlled state)** — estado `open/closed` interno +
990
+ `useId()` para aria-controls. Stress test snapshot ↔ hydrate sync.
991
+ - **Tabs (compound + useId)** — `useId()` para trigger/panel pairing.
992
+ Caso histórico de mismatch en DSs que no aíslan ID counter.
993
+
994
+ **Componentes excluidos** (decisión consciente, documentada en el
995
+ test): Dialog/Menu/Tooltip/Toast (client-only por design, "use client"
996
+ granular + portales — el consumer NO los server-renderiza),
997
+ ThemeToggle/Switch indeterminate (`useSyncExternalStore` con server
998
+ snapshot fijo, ya cubierto trivialmente por renderToString).
999
+
1000
+ Sin `act()` en el test: `hydrateRoot` commitea sincrónicamente y
1001
+ `onRecoverableError` se llama durante el commit; los assertions son
1002
+ sincronos (array push + DOM snapshot), no dependen de effects
1003
+ post-commit.
1004
+
1005
+ ---
1006
+
1007
+ **D5 — Stepper `defaultActive` + modo uncontrolled** (B2-PR1):
1008
+
1009
+ Cierra el último componente con estado público del DS que rompía el
1010
+ patrón controlled/uncontrolled DS-wide. Pre-D5, `active` era
1011
+ obligatorio; consumers debían crear `useState` externo incluso para
1012
+ casos sencillos. Ahora alineado con Accordion/Pagination/Sidebar/
1013
+ Tabs/Switch/Slider/Rating/ThemeToggle.
1014
+
1015
+ API (no breaking — `active` deja de ser obligatorio):
1016
+
1017
+ ```ts
1018
+ export interface StepperProps {
1019
+ active?: number; // ← era obligatorio
1020
+ defaultActive?: number; // ← nuevo (default 0)
1021
+ onActiveChange?: (next: number) => void; // ← sin cambio
1022
+ }
1023
+ ```
1024
+
1025
+ Semántica de modos:
1026
+ | Props pasados | Modo | Interactive |
1027
+ |---|---|---|
1028
+ | `active=N` | Controlled | NO (sin callback no hay vector de cambio) |
1029
+ | `active=N, onActiveChange=fn` | Controlled | SÍ |
1030
+ | `defaultActive=N` (o nada) | Uncontrolled | SÍ (estado interno) |
1031
+ | `defaultActive=N, onActiveChange=fn` | Uncontrolled + observer | SÍ |
1032
+
1033
+ Implementación: `useControllableState<number>` interno. Keyboard
1034
+ handlers + click invocan `setActive(nextIdx)` que resuelve a:
1035
+ - Controlled: forward a `onActiveChange`.
1036
+ - Uncontrolled: actualizar state interno + dispatch a `onActiveChange`
1037
+ como observer.
1038
+
1039
+ 7 tests nuevos en `Stepper.test.tsx` cubren todos los modos +
1040
+ regression guard "controlled sin callback queda presentational".
1041
+ Story `Uncontrolled` añadida con play test que valida estado
1042
+ interno via click sin useState externo.
1043
+
1044
+ Decision doc: `docs/decisions/D5-stepper-defaultactive.md`. Cierra
1045
+ la simetría del DS — todos los componentes con estado interno ahora
1046
+ siguen el mismo patrón `{prop}?` + `default{Prop}?` + `on{Prop}Change`.
1047
+
1048
+ ## [1.0.0-beta.23] — 2026-05-16 (RC1 gate review + post-audit codex)
1049
+
1050
+ Cierra el playbook RC1 completo: **17 PRs técnicos** ejecutados sobre la
1051
+ branch `rc1-gate-fixes` (Menu/FUI, Modal→Dialog, Tabs Radix alignment,
1052
+ ThemeSwitch→ThemeToggle, "use client" granular, Tooltip 12 placements,
1053
+ 3 stories cruzadas, useControllableState updater + breaking semantic,
1054
+ Tooltip dev-warn 4-layer + extractText, Dialog drag-out parity,
1055
+ `reactigoded/cn` subpath, Skeleton ARIA soak) + **audit post-RC1
1056
+ con codex sobre 16 PRs antiguos** que descubrió 5 findings adicionales
1057
+ (todos resueltos antes de tagear).
1058
+
1059
+ **19 PRs auditados por codex / 5 findings reales / 0 issues abiertos
1060
+ en main al cerrar la beta.**
1061
+
1062
+ Última pre-release antes de FREEZE-CHECK final + `1.0.0-rc.1`.
1063
+
1064
+ ### Internal — bump de deps a latest
1065
+
1066
+ 22 paquetes actualizados a su última versión (minors + patches, sin
1067
+ breaking detectado en verify cold 794/794):
1068
+
1069
+ | Paquete | Antes | Después |
1070
+ |---|---|---|
1071
+ | Storybook ecosystem (`storybook`, `addon-a11y`, `addon-docs`, `addon-themes`, `addon-vitest`, `react-vite`, `eslint-plugin-storybook`) | 10.3.6 | **10.4.0** |
1072
+ | `vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` | 4.1.5 | 4.1.6 |
1073
+ | `vite` | 8.0.10 | 8.0.13 |
1074
+ | `eslint` | 10.3.0 | 10.4.0 |
1075
+ | `react`, `react-dom` | 19.2.5 | 19.2.6 |
1076
+ | `chromatic` | 16.6.3 | 16.10.1 |
1077
+ | `playwright` | 1.59.1 | 1.60.0 |
1078
+ | `globals` | 17.5.0 | 17.6.0 |
1079
+ | `@types/node` | 25.6.0 | 25.8.0 |
1080
+ | `@vitejs/plugin-react` | 6.0.1 | 6.0.2 |
1081
+ | `postcss` | 8.5.13 | 8.5.14 |
1082
+ | `typescript-eslint` | 8.59.1 | 8.59.3 |
1083
+
1084
+ **Storybook 10.4 highlights aprovechables**:
1085
+ - Sidebar review tools: status filtering, URL-persisted filters,
1086
+ clearer review signals (útil para gate humano FREEZE-CHECK).
1087
+ - A11y: `aria-live` announcements en el propio Storybook, contrast en
1088
+ forced colors mode, ArgsTable borders Windows HCM.
1089
+ - Addon-Docs: reset story button, avoid re-render de Source blocks (perf).
1090
+ - Addon-Vitest: `provide-API` para injectar values en stories.
1091
+
1092
+ **Patch local mantenido**: `patches/eslint-plugin-jest-dom+5.5.0.patch`
1093
+ sigue necesario — upstream v5.5.0 sigue inconsistente entre uso del
1094
+ helper `getSourceCode(context)` y llamadas directas `context.getSourceCode()`
1095
+ (broken en ESLint v9). El patch sustituye las directas por
1096
+ `context.sourceCode` (sin breaking aún en ESLint v8 porque el wrapper
1097
+ upstream ya hace el fallback). Re-evaluar en upgrades futuros del
1098
+ plugin.
1099
+
1100
+ **`engines.node` raised a `>=22.12.0`** (codex P1 sobre PR #76):
1101
+ Vite 8.0.13 exige `^20.19.0 || >=22.12.0`. Nuestro `>=22` previo
1102
+ incluía Node 22.0–22.11 en el declared support pero fallaría en
1103
+ install/runtime. Floor alineado al peor caso del ecosystem.
1104
+
1105
+ ### Changed (BREAKING — pre-RC1)
1106
+
1107
+ - **`useControllableState.setValue` en modo controlled ya NO advance el
1108
+ pending entre chained calls [codex P1 post-audit sobre PR #70]**:
1109
+ pre-fix, dos `setValue(p => p + 1)` chained en modo controlled desde
1110
+ `value=10` emitían `onChange(11)` y `onChange(22)` — el segundo
1111
+ partía del pending advanced.
1112
+
1113
+ Post-fix: emiten `onChange(11)` y `onChange(20)` — ambos parten del
1114
+ último committed value externo. Esto es semánticamente correcto: en
1115
+ controlled, el padre es la fuente de verdad. Si rechaza el commit
1116
+ (no actualiza `value` prop), no hay re-render y el pending no debe
1117
+ avanzar — si avanzara, el segundo `onChange` daría payload
1118
+ inconsistente con el state real.
1119
+
1120
+ Consumers afectados: uso de chained updater functions con `setValue`
1121
+ en modo controlled (prop `value` definido). Verificado: cero
1122
+ consumers internos del DS dependen de este patrón. Mitigación
1123
+ external consumer: pasar valor calculado externamente en lugar de
1124
+ chaining, o usar modo uncontrolled si el chaining era intencional.
1125
+
1126
+ ### Fixed
1127
+
1128
+ - **`Tooltip`: sr-only span renderiza texto plano via `extractText`
1129
+ [codex P1 post-audit sobre PR #52]**: pre-fix, `text={ReactNode}`
1130
+ con contenido interactivo (`<button>`/`<a>`) se renderizaba completo
1131
+ en el sr-only span. Solución previa (`inert` en el span) rompía
1132
+ `aria-describedby` al excluir el subárbol del a11y tree.
1133
+
1134
+ Fix: `extractText()` extrae texto plano del ReactNode recursivamente.
1135
+ El sr-only renderiza solo `string` — sin focusables posibles → no se
1136
+ necesita `inert`. Resuelve simultáneamente:
1137
+ - Bug original (focus trap invisible con ReactNode interactivo).
1138
+ - Bug post-audit (`inert` rompe accessible name del trigger).
1139
+
1140
+ El portal flotante mantiene el `ReactNode` completo con `inert`
1141
+ (decoración visual aislada).
1142
+
1143
+ - **`Stepper`: guard idx-mismatch en focus management
1144
+ [codex P1 post-audit sobre PR #19]**: si el padre rechaza una
1145
+ transición de `active` (no commitea el cambio), `focusTargetIdxRef`
1146
+ quedaba stale apuntando al idx rechazado. Una transición posterior
1147
+ distinta podía mover focus al idx antiguo.
1148
+
1149
+ Fix: guard `if (idx !== clampedActive) return` antes de aplicar
1150
+ focus — solo movemos focus si el commit del padre coincide con la
1151
+ intent del keydown.
1152
+
1153
+ - **`Pagination`: `defaultPage` se clampa al inicializar state, no
1154
+ solo en render [codex P2 post-audit sobre PR #19]**: pre-fix,
1155
+ `defaultPage` fuera de rango se almacenaba raw en state interno y
1156
+ solo se clampaba en render. Si `totalPages` aumentaba después, la
1157
+ UI saltaba al `defaultPage` stale.
1158
+
1159
+ Fix: pre-clamp de `defaultPage` contra `totalPages` antes de pasar
1160
+ al hook.
1161
+
1162
+ - **`Avatar`: fallback de img preserva accessible name via `alt`
1163
+ [codex P2 post-audit sobre PR #36]**: pre-fix, cuando la imagen
1164
+ fallaba a cargar y caía a `initials`, el span de initials estaba
1165
+ `aria-hidden="true"` y el wrapper sin role/aria-label — el SR
1166
+ quedaba mudo si el consumer no había pasado `aria-label` explícito.
1167
+
1168
+ Fix: en fallback mode, el wrapper recibe `role="img"` +
1169
+ `aria-label={alt ?? ariaLabel}`. Avatares solo decorativos (sin
1170
+ `alt` ni `ariaLabel`) siguen siendo silenciosos por decisión
1171
+ consciente del consumer.
1172
+
1173
+ ### Added
1174
+
1175
+ - **Soak del cambio ARIA de `Skeleton` (beta.22) [M-02]**: cobertura de
1176
+ tests + Storybook ampliada para validar el patrón ARIA del breaking
1177
+ change introducido en beta.22 (cada `Skeleton` standalone es
1178
+ decorativo; el announcement al SR vive en `SkeletonContainer`).
1179
+
1180
+ - Story `ConContainer` recibe play test real verificando contrato
1181
+ completo (único `role=status` + `aria-busy` + `aria-live=polite` +
1182
+ `aria-label` + Skeletons internos con `role=presentation`).
1183
+ - 2 stories nuevas: `SkeletonsStandaloneNoAnuncian` (consciente:
1184
+ sin container, cero announcements) y `MultiplesContainersParalelos`
1185
+ (cada container con status independiente).
1186
+ - 5 tests nuevos en `describe("Skeleton — M-02 ARIA soak")`:
1187
+ container vacío sigue anunciando, múltiples paralelos independientes,
1188
+ anidación preserva ambos status, standalone fuera del a11y tree,
1189
+ role inmutable (regresión guard).
1190
+ - Test extra en `AllStates regression` verifica invariante "sin
1191
+ container, cero announcements" — convierte el placebo legacy en
1192
+ aserción significativa.
1193
+
1194
+ axe-core ahora ejecuta sobre 3 stories del patrón en CI.
1195
+
1196
+ - **Subpath export `reactigoded/cn` para tree-shaking [H-12]**: pre-fix,
1197
+ `import { cn } from "reactigoded"` arrastraba el bundle entero
1198
+ (~64 KB ungz / 16 KB gz) incluyendo `createPortal` de `react-dom`
1199
+ vía Toast — los bundlers consumers no tree-shaken bundles
1200
+ monolíticos. Build multi-entry de Vite separa `cn` como entry
1201
+ independiente.
1202
+
1203
+ ```ts
1204
+ // Antes: arrastra todo el DS
1205
+ import { cn } from "reactigoded"; // 16 KB gz
1206
+
1207
+ // Ahora: ~0.4 KB gz (wrapper + clsx)
1208
+ import { cn } from "reactigoded/cn";
1209
+ ```
1210
+
1211
+ El import desde el root (`reactigoded`) sigue funcionando — no es
1212
+ breaking. La subpath solo añade una opción más eficiente para
1213
+ consumers que solo necesitan `cn` (utility libraries, build
1214
+ helpers, etc).
1215
+
1216
+ Output del build:
1217
+ - `dist/cn.js` 70 B (re-export)
1218
+ - `dist/cn-<hash>.js` 0.55 KB (chunk clsx compartido con el root bundle)
1219
+
1220
+ ### Fixed
1221
+
1222
+ - **`Dialog`: drag-out parity tracking — selección de texto ya no cierra
1223
+ el modal [H-02]**: cuando el usuario hacía `mousedown` sobre el contenido
1224
+ del Dialog (para seleccionar texto) y arrastraba hasta soltar sobre el
1225
+ backdrop, el browser disparaba un `click` event con `target === currentTarget`
1226
+ (el `<dialog>` propio) — el handler interpretaba ese click como "click
1227
+ en backdrop" y cerraba el modal, abandonando la selección.
1228
+
1229
+ Fix con `pointerdownTargetRef`: registra el target del `pointerdown` y
1230
+ solo considera "click en backdrop" cuando AMBOS el pointerdown Y el
1231
+ click final tienen target = dialog (no si pointerdown empezó en
1232
+ contenido). El fallback `pointerdownTarget === null` preserva el
1233
+ comportamiento para clicks programáticos sin pointerdown previo
1234
+ (tests, consumer-side `.click()` calls).
1235
+
1236
+ Sin cambio de API — fix interno transparente.
1237
+
1238
+ ### Added
1239
+
1240
+ - **`Tooltip` dev-warn cuando custom child no forwardea ref [M-07.2]**:
1241
+ cuando el `children` del Tooltip es un componente custom que ignora
1242
+ el ref (sin `React.forwardRef` o sin aceptar `ref` como prop normal
1243
+ en React 19), Floating UI no puede medir el trigger ni montar el
1244
+ portal — el tooltip queda inerte al hover/focus pero el sr-only
1245
+ span con `aria-describedby` sigue funcionando. Pre-RC1 el síntoma
1246
+ era silencioso (consumer no sabía por qué el tooltip "no abre").
1247
+
1248
+ Implementación con **approach de 4 capas** (cero magic numbers en
1249
+ camino crítico):
1250
+
1251
+ 1. **Static analysis O(1)** sobre `children.type`:
1252
+ - `string` (DOM intrinsic): `guaranteed_ok` → cero runtime.
1253
+ - `$$typeof === REACT_FORWARD_REF_TYPE`: `guaranteed_ok`.
1254
+ - `memo(...)`: recurse en el inner type.
1255
+ - `function`: `ambiguous` (React 19 ref-as-prop posible).
1256
+ 2. **Probe sticky** en el ref: `useRef(false)` que se setea a `true`
1257
+ la primera vez que recibe un Element no-null. Una vez true, no se
1258
+ resetea — cero false positives en lazy mounting.
1259
+ 3. **Sentinel dev-only** con `display: contents` envolviendo el child
1260
+ (solo si `verdict === 'ambiguous'`). Listeners `mouseenter`/`focus`
1261
+ en capture-phase via `addEventListener` nativo (React no expone
1262
+ `onMouseEnterCapture` porque mouseenter no bubblea). Captura el
1263
+ intent del usuario incluso si el child no propaga handlers — sin
1264
+ depender de tiempo arbitrario.
1265
+ 4. **Safety net 2000ms** como fallback solo para el caso edge "dev
1266
+ observa pero no interactúa". Honestamente generoso (cubre
1267
+ `React.lazy`, Suspense, fetch lentos, `requestIdleCallback`).
1268
+
1269
+ Detecta los **3 modos de fallo** del Tooltip (no solo el del ref):
1270
+ child que no forwardea ref, child que no propaga handlers (`...rest`
1271
+ ignorado), child que renderiza null permanentemente.
1272
+
1273
+ Mensaje del warn (incluye nombre del componente):
1274
+
1275
+ ```
1276
+ [reactigoded] <Tooltip>: el child <MyCustom> no expone su nodo DOM
1277
+ via ref. El tooltip no puede medir el trigger ni abrirse al hover/
1278
+ focus. Usa React.forwardRef (React <19) o acepta `ref` como prop
1279
+ normal (React 19+) y pásalo al elemento DOM root del componente.
1280
+ aria-describedby sigue funcionando — el SR anuncia el texto del
1281
+ tooltip pero el portal visual no aparece.
1282
+ ```
1283
+
1284
+ No-op en producción (gated por `import.meta.env.DEV`).
1285
+
1286
+ - **`useControllableState.setValue` acepta updater function [M-06]**: el
1287
+ setter del hook ahora acepta valor directo `T` **o** una función
1288
+ updater `(prev: T) => T` — mismo patrón que `useState` de React.
1289
+
1290
+ ```ts
1291
+ const { value, setValue } = useControllableState<boolean>({
1292
+ defaultValue: false,
1293
+ });
1294
+
1295
+ // Antes: solo valor directo (depende del closure de `value`).
1296
+ setValue(!value);
1297
+
1298
+ // Ahora también: updater (no depende del closure).
1299
+ setValue((prev) => !prev);
1300
+ ```
1301
+
1302
+ Útil para updates encadenados en handlers que no quieren capturar
1303
+ `value` en el closure (`onClick`/`onKeyDown` con múltiples acciones).
1304
+
1305
+ Funciona en los 3 modos:
1306
+ - **uncontrolled**: la updater recibe el `internalValue` actual.
1307
+ - **controlled**: la updater recibe el `value` controlado externo y
1308
+ el resuelto se pasa a `onChange` (sin tocar state interno).
1309
+ - **derive**: la updater recibe el valor derivado y el resuelto se
1310
+ pasa a `setDerivedValue`.
1311
+
1312
+ Sin breaking — `setValue(directValue)` sigue funcionando.
1313
+
1314
+ - **`Tooltip` stories cruzadas + play tests reales [H-08]**: cobertura
1315
+ Storybook ampliada con 1 story cruzada nueva (`TooltipDentroDeTabs`)
1316
+ y play tests reescritos en stories existentes para sustituir las
1317
+ asserts placebo (counting spans, leyendo `aria-describedby` sin abrir
1318
+ el portal) por **interacciones reales**:
1319
+
1320
+ - `A11yInteraction`: play ahora hace `userEvent.hover`, verifica que
1321
+ el portal monta en `document.body` con `.ig-tooltip` y el texto
1322
+ correcto, dispara `{Escape}` y verifica que el portal desmonta —
1323
+ y que el sr-only persiste como referente estable de `aria-describedby`.
1324
+ - `AllStates`: el `expect(spans).toHaveLength` legacy se mantiene
1325
+ como sanity check pero se añade hover real sobre `top` →
1326
+ verify `.ig-tooltip.ig-tooltip-place-top` en `document.body` con
1327
+ texto → unhover → verify desmonte.
1328
+ - `TooltipDentroDeModal` (story cruzada existente): nuevo play que
1329
+ abre el Dialog, hace hover en el botón danger, verifica que el
1330
+ portal del tooltip monta dentro del top-layer del `<dialog>` (no
1331
+ detrás del backdrop) y dispara Escape para cerrar solo el tooltip.
1332
+ - `TooltipDentroDeTabs` (story cruzada nueva): cambia entre paneles
1333
+ de `Tabs`, verifica que el tooltip del panel inactivo desaparece
1334
+ del DOM al cambiar, y que el del panel activo aparece al hover.
1335
+
1336
+ Coverage cruzada total: 3 stories (`WithCustomContainer` con `<dialog>`
1337
+ nativo + `TooltipDentroDeModal` con `<Dialog>` del DS + `TooltipDentroDeTabs`
1338
+ con `<Tabs>` del DS). axe-core ahora ejecutará a11y checks sobre estas
1339
+ combinaciones en CI.
1340
+
1341
+ - **`Tooltip` ampliado a los 12 placements de Floating UI [M-04]**: el
1342
+ type `TooltipPlacement` pasa de 4 (`top` / `bottom` / `left` / `right`)
1343
+ a los **12 valores nativos** de Floating UI — 4 sides × 3 alignments
1344
+ (base, `-start`, `-end`):
1345
+
1346
+ ```ts
1347
+ type TooltipPlacement =
1348
+ | "top" | "top-start" | "top-end"
1349
+ | "right" | "right-start" | "right-end"
1350
+ | "bottom" | "bottom-start" | "bottom-end"
1351
+ | "left" | "left-start" | "left-end";
1352
+ ```
1353
+
1354
+ Sin cambios CSS — las clases `.ig-tooltip-place-*` siguen siendo
1355
+ hooks informativos vacíos (Floating UI posiciona vía inline styles).
1356
+ La nueva clase `.ig-tooltip-place-top-start` y resto se emiten
1357
+ automáticamente.
1358
+
1359
+ Sin breaking — los 4 placements antiguos siguen siendo válidos. Solo
1360
+ amplía el conjunto de opciones permitidas.
1361
+
1362
+ Tests: `describe.each` ampliado a los 12 placements.
1363
+ Stories: nuevo layout 3x3 + alas mostrando los 12 placements simultáneamente.
1364
+
1365
+ ### Changed
1366
+
1367
+ - **`"use client"` granular por archivo en lugar de global [H-09]**: el
1368
+ barrel `src/index.ts` ya no lleva `"use client";` global. En su lugar,
1369
+ los **48 archivos** que dependen de hooks React, browser APIs o context
1370
+ interno llevan la directive en su propio archivo. Los **44 archivos
1371
+ restantes** (Card subcomponents, Dialog body/footer/close, Sidebar
1372
+ items, Navbar pieces, Skeleton, Spinner, Badge, Progress, Divider,
1373
+ Timeline, etc. — ~49% del DS) son server-safe y pueden renderizarse
1374
+ desde React Server Components sin forzar boundary client en el árbol
1375
+ consumer.
1376
+
1377
+ Impacto consumers RSC: árboles puramente presentacionales (p.ej.
1378
+ `<Card><CardBody>...</CardBody></Card>`) ya no escalan el "client
1379
+ boundary" innecesariamente. Para componentes interactivos (`Switch`,
1380
+ `Tabs`, `Dialog`, `Toast`, etc.) el comportamiento es idéntico — la
1381
+ directive sigue presente, ahora a nivel archivo.
1382
+
1383
+ Sin impacto en SPA / Vite dev / Storybook — la directive es no-op
1384
+ fuera de bundlers RSC-aware (Next.js App Router, Remix, etc.).
1385
+
1386
+ ### Breaking
1387
+
1388
+ - **`ThemeSwitch` → `ThemeToggle` (rename JS, sin cambios CSS)**: el componente
1389
+ se renombra a `ThemeToggle` para reflejar fielmente su comportamiento (toggle
1390
+ binario light/dark) y desacoplar el nombre público de la implementación
1391
+ interna (que sigue usando `<Switch>` por dentro). Alinea con la industria
1392
+ (Radix, ShadCN, MUI usan `ThemeToggle`/`ThemeButton`).
1393
+
1394
+ Migración consumers:
1395
+ ```diff
1396
+ - import { ThemeSwitch } from "reactigoded";
1397
+ - <ThemeSwitch defaultTheme="dark" />
1398
+ + import { ThemeToggle } from "reactigoded";
1399
+ + <ThemeToggle defaultTheme="dark" />
1400
+ ```
1401
+
1402
+ Tipo de props renombrado en paralelo: `ThemeSwitchProps` → `ThemeToggleProps`.
1403
+
1404
+ **CSS sin cambios**: `ThemeToggle` sigue emitiendo `.ig-switch` (delega
1405
+ en `<Switch>` internamente). Consumers que customicen vía CSS de
1406
+ `.ig-switch` no se ven afectados.
1407
+
1408
+ - **Tabs API alineada a Radix puro (rename + wrapper eliminado)**: la API
1409
+ pública se simplifica a `<Tabs>`, `<TabsList>`, `<TabsTrigger>`, `<TabsContent>`
1410
+ con composición 100% Radix-style. Cierra inconsistencia interna del DS y
1411
+ alinea con la industria (Radix, ShadCN, Ark UI).
1412
+
1413
+ | Antes | Después | Notas |
1414
+ |---|---|---|
1415
+ | `Tab` | `TabsTrigger` | trigger del tab |
1416
+ | `TabList` | `TabsList` | coherencia plural con `TabsContent`/`TabsContext` |
1417
+ | `TabPanel` | `TabsContent` | panel asociado al `value` |
1418
+ | `<TabsContent>` (wrapper sin lógica) | ❌ eliminado | era Caso 1 (`<div ig-tabs-content>` puro). Padding-top movido a `.ig-tabs-content` (el panel renombrado). |
1419
+
1420
+ API final 100% Radix-style:
1421
+ ```tsx
1422
+ <Tabs>
1423
+ <TabsList>
1424
+ <TabsTrigger value="a">A</TabsTrigger>
1425
+ </TabsList>
1426
+ <TabsContent value="a">…</TabsContent>
1427
+ </Tabs>
1428
+ ```
1429
+
1430
+ CSS coordinado:
1431
+ - `.ig-tab` → `.ig-tabs-trigger` (con sufijo `-active`)
1432
+ - `.ig-tab-panel` → `.ig-tabs-content` (con sufijo `-active`)
1433
+ - `.ig-tabs-list` SIN cambio (ya plural pre-RC1)
1434
+ - `.ig-tabs-content` (wrapper original) eliminada — el panel renombrado
1435
+ hereda el nombre.
1436
+ - Utilities `.ig-tab-1/2/4/8` (tab-size CSS) **SIN cambios** — son
1437
+ typography utilities, no componente Tabs.
1438
+
1439
+ Tokens CSS-variable `--ig-tab-*` **mantenidos** (no renombrados) por
1440
+ compatibilidad con custom CSS de consumers (mismo criterio que
1441
+ `--ig-z-modal` en PR 3).
1442
+
1443
+ Razón última: paralelismo con `Menu`/`MenuTrigger`/`MenuContent`
1444
+ post-PR Menu — los compounds del DS siguen el mismo patrón
1445
+ estructural `<X>` / `<XTrigger>` / `<XContent>`.
1446
+
1447
+ Migración:
1448
+ ```diff
1449
+ - import { Tabs, TabList, Tab, TabPanel, TabsContent } from "reactigoded";
1450
+ + import { Tabs, TabsList, TabsTrigger, TabsContent } from "reactigoded";
1451
+
1452
+ - <Tabs>
1453
+ - <TabList>
1454
+ - <Tab value="a">A</Tab>
1455
+ - </TabList>
1456
+ - <TabsContent>
1457
+ - <TabPanel value="a">...</TabPanel>
1458
+ - </TabsContent>
1459
+ - </Tabs>
1460
+ + <Tabs>
1461
+ + <TabsList>
1462
+ + <TabsTrigger value="a">A</TabsTrigger>
1463
+ + </TabsList>
1464
+ + <TabsContent value="a">...</TabsContent>
1465
+ + </Tabs>
1466
+ ```
1467
+
1468
+ - **`Modal` → `Dialog` (rename + regularización CSS-JS)**: el componente JS
1469
+ se renombra a `Dialog` para alinear con el HTML element nativo `<dialog>`
1470
+ (que ya usaba internamente) y con la industria (Radix, Ark UI, HeadlessUI,
1471
+ MUI usan `Dialog`). Las clases CSS ya estaban nombradas `.ig-dialog-*`
1472
+ (alineación previa), por lo que NO cambian. Este rename regulariza la
1473
+ inconsistencia legacy: componente `Modal` (JS) emitiendo clases
1474
+ `.ig-dialog-*` (CSS).
1475
+
1476
+ Renames JS:
1477
+
1478
+ | Antes | Después |
1479
+ |---|---|
1480
+ | `Modal` | `Dialog` |
1481
+ | `ModalBody` | `DialogBody` |
1482
+ | `ModalHeader` | `DialogHeader` |
1483
+ | `ModalFooter` | `DialogFooter` |
1484
+ | `ModalClose` | `DialogClose` |
1485
+ | `ModalProps` (y `Modal*Props` subcomponentes) | `DialogProps` (y `Dialog*Props`) |
1486
+ | `ModalContext` (interno) | `DialogContext` (interno) |
1487
+ | `ModalSize` / `ModalBackdrop` (types) | `DialogSize` / `DialogBackdrop` |
1488
+
1489
+ Las **clases CSS `.ig-dialog-*` SIN cambios** (ya alineadas pre-RC1).
1490
+ Tokens `--ig-z-modal` / `--ig-z-modal-backdrop` se mantienen como
1491
+ utilities z-index genéricas (no renombradas para preservar
1492
+ compatibilidad con custom CSS de consumers).
1493
+
1494
+ Migración:
1495
+ ```diff
1496
+ - import { Modal, ModalBody, ModalHeader, ModalFooter, ModalClose } from "reactigoded";
1497
+ + import { Dialog, DialogBody, DialogHeader, DialogFooter, DialogClose } from "reactigoded";
1498
+ ```
1499
+
1500
+ - **`Menu` internals migrados a Floating UI (C-03)**: `Menu` reescrito sobre la
1501
+ capa `floating/primitives/` (PR #62). Reemplaza ~600 LOC hand-rolled de
1502
+ navegación por hooks composables:
1503
+
1504
+ - `useListNavigation` con `focusItemOnOpen: 'auto'` (APG menu pattern: arrows
1505
+ abren menu + focan primer/último item; click abre sin focar).
1506
+ - `useTypeahead` (focus por primera letra, APG).
1507
+ - `useDismiss({ bubbles: { escapeKey: true } })` (cascade dismiss via
1508
+ `<FloatingTreeRoot>`).
1509
+ - `FloatingFocusManager` con `initialFocus={-1}` + `returnFocus` (defiere
1510
+ focus management a useListNavigation, devuelve foco al trigger al cerrar).
1511
+ - `useFloatingNode` para registrar el Menu en el FloatingTree (cascade dismiss
1512
+ cuando hay descendientes flotantes como Tooltip dentro de MenuItem).
1513
+
1514
+ API pública (`open`/`defaultOpen`/`onOpenChange`/`placement`/`direction`/
1515
+ `closeOnSelect`) y compound shape (`Menu`/`MenuTrigger`/`MenuContent`/
1516
+ `MenuItem`/`MenuSeparator`/`MenuLabel`) sin cambio. Las clases CSS
1517
+ `.ig-menu-*` mantienen mismo contrato.
1518
+
1519
+ Breaking de tipos en callbacks de `MenuItem`:
1520
+ - `onClick` y `onKeyDown` cambian de `MouseEvent<HTMLAnchorElement>` /
1521
+ `MouseEvent<HTMLButtonElement>` (según rama) a `MouseEvent<HTMLElement>`
1522
+ (genérico). Mismo para `KeyboardEvent`. Audit pre-RC1 verificó cero usage
1523
+ de API anchor/button-specific (`.href`, `.disabled`, etc.) en callbacks
1524
+ de MenuItem en repo + cero consumers reales.
1525
+
1526
+ Migración consumers futuros que necesiten API element-specific:
1527
+ ```tsx
1528
+ onClick={(e) => {
1529
+ if (e.currentTarget instanceof HTMLAnchorElement) {
1530
+ e.currentTarget.href; // narrow type-safe
1531
+ }
1532
+ }}
1533
+ ```
1534
+
1535
+ - **`OptionsMenu` → `Menu` + 5 subcomponentes renombrados (B-01 redefinido)**:
1536
+ alineación con la industria. 5 de 7 librerías top (MUI, Mantine, Ark UI,
1537
+ HeadlessUI, Chakra) usan el nombre canónico `Menu`. B-01 original (rename
1538
+ `Dropdown` → `OptionsMenu` pre-RC1) liberaba el nombre `Dropdown` para una
1539
+ migración FUI futura; la decisión 2026-05-10 unifica directamente a `Menu`.
1540
+
1541
+ | Antes | Después | Notas |
1542
+ |---|---|---|
1543
+ | `OptionsMenu` | `Menu` | core |
1544
+ | `OptionsMenuTrigger` | `MenuTrigger` | core |
1545
+ | `OptionsMenuContent` | `MenuContent` | core |
1546
+ | `OptionsMenuItem` | `MenuItem` | core |
1547
+ | `OptionsMenuDivider` | `MenuSeparator` | rol ARIA correcto `separator` |
1548
+ | `OptionsMenuHeader` | `MenuLabel` | label de sección dentro del menu |
1549
+
1550
+ CSS coordinado:
1551
+ - `.ig-options-menu-*` → `.ig-menu-*`
1552
+ - `.ig-options-menu-divider-*` → `.ig-menu-separator-*`
1553
+ - `.ig-options-menu-header-*` → `.ig-menu-label-*`
1554
+
1555
+ Migración:
1556
+ ```diff
1557
+ - import { OptionsMenu, OptionsMenuTrigger, OptionsMenuItem } from "reactigoded";
1558
+ + import { Menu, MenuTrigger, MenuItem } from "reactigoded";
1559
+ ```
1560
+
1561
+ - **`Tooltip` Slot pattern (D-01 / M-05 / B-03 / H-01)**: el componente
1562
+ ya **no envuelve al child en `<span class="ig-tooltip-wrapper">`**.
1563
+ El render emite el child clonado + un `<span class="ig-sr-only"
1564
+ role="tooltip" inert>` sibling + el portal flotante. La API pública
1565
+ de props (`text`, `placement`, `variant`, `openDelay`, `closeDelay`,
1566
+ `container`) no cambia, **pero**:
1567
+
1568
+ - **CSS público**: la clase `.ig-tooltip-wrapper` ya no se emite. Si
1569
+ tenías reglas CSS dirigidas al wrapper (layout, espaciado), aplica
1570
+ los estilos al child directamente o envuélvelo manualmente en tu
1571
+ propio span/div.
1572
+ - **Tipos de `TooltipProps`**: dejan de extender
1573
+ `HTMLAttributes<HTMLSpanElement>`. `className`, `ref` y
1574
+ `...rest` HTML props del wrapper desaparecen del tipo. Si pasabas
1575
+ `<Tooltip className="..." ref={spanRef}>`, el TS error es
1576
+ intencional — esos props no tenían destino fiable post-Slot.
1577
+ - **Layout**: si el child es block-level (e.g. `<div>`, `<table>`),
1578
+ ahora respeta el flujo natural (antes, el wrapper `display:
1579
+ contents` lo neutralizaba pero introducía un nodo silencioso).
1580
+
1581
+ Razón: el wrapper rompía block-level layouts del consumer y obligaba
1582
+ a CSS extra para corregirlo. M-05 y D-01 lo señalaban; en RC1 se
1583
+ cierran.
1584
+
1585
+ ### Añadido
1586
+
1587
+ - **`FloatingTreeRoot` y `useFloatingNode`** (B-03 / H-01): nueva capa
1588
+ `floating/primitives/` con un wrapper opt-in (`<FloatingTreeRoot>`)
1589
+ que habilita cascade dismiss entre `Tooltip` y futuros floats
1590
+ (`Popover`, `HoverCard`, `Dropdown` FUI 1.x+) cuando se anidan. Sin
1591
+ el root, los floats funcionan independientes (sin regresión).
1592
+
1593
+ ```tsx
1594
+ import { FloatingTreeRoot } from "reactigoded";
1595
+
1596
+ function App() {
1597
+ return (
1598
+ <FloatingTreeRoot>
1599
+ <RouterProvider router={router} />
1600
+ </FloatingTreeRoot>
1601
+ );
1602
+ }
1603
+ ```
1604
+
1605
+ El hook `useFloatingNode()` devuelve `{ nodeId, parentId }` para que
1606
+ componentes flotantes del DS se registren en el árbol activo.
1607
+ `Tooltip` ya lo consume internamente.
1608
+
1609
+ ### Notas operativas
1610
+
1611
+ - **Sin publicación a npm pre-RC1** (M-03 / cf. `docs/RC1_DECISIONS.md`
1612
+ § "Actualización 2026-05-06: pausa operativa"). Pre-RC1 el paquete
1613
+ se consume vía `git clone` + `npm link`. Iván decidió pausar
1614
+ `npm publish --tag beta` de `1.0.0-beta.22` hasta tener capacidad
1615
+ operativa para sostener el mantenimiento (issues, PRs de consumers,
1616
+ semver discipline). El branch `rc1-gate-fixes` ya cerró los 18
1617
+ Blockers + 8 Highs del gate review; la calidad del código no
1618
+ requiere publicación inmediata para ser real.
1619
+ - **Tag git `v1.0.0-rc.1`** se creará tras la sesión de FREEZE-CHECK
1620
+ para trazabilidad histórica del repo, **sin acompañar de
1621
+ `npm publish`** mientras la pausa siga vigente.
1622
+ - **Reactivación de la publicación** seguirá el procedimiento
1623
+ documentado en `docs/RC1_DECISIONS.md`: bumpear versión, revertir
1624
+ commit B-01-followup (banner del README), `npm publish --tag rc`.
1625
+
1626
+ ## [1.0.0-beta.22] — 2026-05-06 (saneamiento RC1)
1627
+
1628
+ Cierra los 18 Blockers + 8 Highs del audit RC1
1629
+ (`rc1-gate-review-reactigoded.md`) más limpieza adicional descubierta
1630
+ durante el ciclo. **Última pre-release antes de `1.0.0-rc.1`**.
1631
+
1632
+ ### Breaking
1633
+
1634
+ - **Sidebar**: prop `ariaLabel` eliminada por consistencia con el resto
1635
+ del DS (Pagination, Spinner, Stepper, TabList, Rating, Timeline ya
1636
+ habían eliminado la prop separada en beta.4 y posteriores). Sidebar
1637
+ era el último outlier. Migration:
1638
+ ```diff
1639
+ - <Sidebar ariaLabel="Navegación principal">…</Sidebar>
1640
+ + <Sidebar aria-label="Navegación principal">…</Sidebar>
1641
+ ```
1642
+ Sin override sigue cayendo al default ES `"Navegación lateral"`.
1643
+ - **Skeleton**: cambio del patrón ARIA. `Skeleton` ahora es decorativo
1644
+ (`role="presentation"` + `aria-hidden="true"`). Para anunciar carga
1645
+ al lector de pantalla, envuelve un grupo en
1646
+ `<SkeletonContainer label="...">`, que dispara UN solo aviso
1647
+ (`role="status"` + `aria-busy` + `aria-live="polite"`). El patrón
1648
+ anterior generaba spam de "status busy" en SR cuando había varios
1649
+ `Skeleton`. Migration:
1650
+ ```diff
1651
+ - <Skeleton variant="text" />
1652
+ - <Skeleton variant="text" />
1653
+ + <SkeletonContainer label="Cargando lista">
1654
+ + <Skeleton variant="text" />
1655
+ + <Skeleton variant="text" />
1656
+ + </SkeletonContainer>
1657
+ ```
1658
+ El layout del wrapper es neutro (`display: contents`): no añade caja
1659
+ al flujo, los hijos se posicionan como si el container no existiese.
1660
+
1661
+ ### Added
1662
+
1663
+ - **`SkeletonContainer`** [B-12]: nuevo wrapper a11y para grupos de
1664
+ `Skeleton`. Props: `label?: string` (default ES
1665
+ `"Cargando contenido…"`), `children`. Acepta `aria-label` directo
1666
+ vía rest (gana sobre `label`).
1667
+ - **`Pagination` uncontrolled state sync** [B-18]: cuando `totalPages`
1668
+ baja por debajo del page interno, el componente sincroniza `page`
1669
+ al clamped current con `silent: true` (no dispara `onValueChange`).
1670
+ Cuando `totalPages` vuelve a subir, NO "salta" al page viejo.
1671
+ - **`Tabs` controlled inválido — fallback tab stop** [H-26]: cuando
1672
+ `value` no matchea ningún Tab montado, el primer Tab registrado
1673
+ recibe `tabIndex=0` para mantener el tablist navegable por
1674
+ teclado. `aria-selected` permanece `false` en todos. Mensaje de
1675
+ warn dev actualizado.
1676
+ - **`useControllableState` warn re-aplicado** [B-08-followup en
1677
+ beta.21]: ahora con escape hatch `__suppressNoHandlerWarn` que se
1678
+ elimina del `.d.ts` publicado vía `stripInternal`.
1679
+ - **`Card.ref` polimórfica tipada** [B-03]: `ref` infiere desde
1680
+ `as`. `<Card as="a" ref={r}>` → `r` es `RefObject<HTMLAnchorElement>`.
1681
+ - **Skeleton + Stepper data-step-index** [H-25]: el dot interactivo
1682
+ del Stepper expone `data-step-index` (implementation detail, NO
1683
+ API pública) para que el effect post-commit de focus management
1684
+ resuelva por índice lógico, no por orden DOM.
1685
+
1686
+ ### Fixed
1687
+
1688
+ - **Bundle prod sin `console.*` dev warns** [B-07]: 9 componentes
1689
+ migrados de `if (!isDev())` a `if (!import.meta.env.DEV)` para
1690
+ que esbuild/Vite hagan DCE de los warn dev en build de producción.
1691
+ Verificado: `grep -c console.* dist/index.js` = 0; `grep -c
1692
+ console.* dist/index.cjs` = 0. CI ahora gated por step explícito.
1693
+ - **`ThemeSwitch` respeta `<html data-theme>` pre-puesto** [B-08]:
1694
+ el derive lee `<html data-theme>` antes de caer a `defaultTheme`,
1695
+ así que un script anti-flash del consumer
1696
+ (`<html data-theme="light">` antes de hidratar) ya no se sobreescribe.
1697
+ 6 tests añadidos (incluido SSR `renderToString` smoke + SSR test
1698
+ versión A que stuba `globalThis.document` y valida el branch
1699
+ `typeof document === "undefined"` del derive).
1700
+ - **`Stepper` focus management sin `setTimeout`** [H-25]: el focus
1701
+ al nuevo step ya no usa `setTimeout(0)`; ahora usa un
1702
+ `useEffect([active])` post-commit y `data-step-index` para resolver
1703
+ el target de forma robusta contra conditional rendering, Steps
1704
+ decorativos sin role=button, o CSS reordering.
1705
+ - **`Slider` warn dev cuando `value=NaN`** [H-27]: el path controlled
1706
+ no-finito ya no es silencioso; emite warn dev-only similar al que
1707
+ existía para `defaultValue` no-finito.
1708
+ - **`Sidebar` blindaje a11y** [B-09]: `role`/`aria-busy`/`aria-live`
1709
+ en `<aside>` no pueden ser overrideados por consumer (extracción
1710
+ explícita de `aria-label` desde rest). Mismo patrón canónico del DS.
1711
+ - **`Card.ref` con `Ref<unknown>`** [B-03]: pasa a
1712
+ `ComponentPropsWithRef<C>["ref"]`. Consumers en strict TS configs
1713
+ ya no reciben tipo laxo.
1714
+ - **Sitio (igoded.es / Storybook)**:
1715
+ - `<html lang="es">` forzado runtime + estático [B-04 + B-04-followup].
1716
+ - Dedupe de `<title>` y `<meta name="description">` [B-05]. Causa
1717
+ raíz arreglada: metas estáticas consolidadas en
1718
+ `.storybook/manager-head.html` (eran duplicadas con
1719
+ `main.ts:managerHead()`); el script runtime queda como red de
1720
+ seguridad defensiva.
1721
+
1722
+ ### Internal
1723
+
1724
+ - **`stripInternal: true`** en `tsconfig.build.json` [B-02]:
1725
+ `__suppressNoHandlerWarn` y cualquier miembro `@internal` no
1726
+ aparecen en el `.d.ts` publicado.
1727
+ - **Excludes del dts publicado** [B-06]: `src/test-utils/`,
1728
+ `src/stories/`, helpers internos (`env.ts` borrado, `useIsoLayoutEffect`,
1729
+ `mergeDescribedBy`) ya no viajan al tarball. Script
1730
+ `scripts/clean-internal-dist.mjs` post-build defensivo.
1731
+ - **`vite-env.d.ts` aislado** [B-07-followup]: nuevo
1732
+ `src/_internal-env.d.ts` con SOLO `ImportMetaEnv` para que tsc.build
1733
+ resuelva `import.meta.env.DEV` sin contaminar el dts publicado con
1734
+ `declare module "*.css"`.
1735
+ - **MDX foundations canónicamente en `docs/`** [M-06]: 6 páginas
1736
+ movidas de `src/stories/` a `docs/`. Storybook `main.ts` añade
1737
+ `../docs/**/*.mdx` a `stories`.
1738
+ - **`"use client";` en bundle** [B-17]: directiva añadida vía
1739
+ `output.banner` en `vite.lib.config.ts` (además del source) para
1740
+ garantizar que llega al bundle final tras minify. Soporta consumers
1741
+ Next.js App Router en Server Components.
1742
+ - **CI verify pipeline reforzado**:
1743
+ - `test:scope-leaks --strict` [B-15] añadido como step.
1744
+ - `verify:unit` ahora encadena `test:unit:ci` (isolate + forks),
1745
+ no `test:unit` [B-16].
1746
+ - Bundle dev-warn guard con greps explícitos [B-07-followup].
1747
+ - Chromatic sin `--auto-accept-changes=main` ni
1748
+ `--exit-zero-on-changes` [H-02]: revisión visual humana
1749
+ obligatoria pre-RC1.
1750
+ - **Build optimizado** [H-04]: `vite.lib.config.ts`
1751
+ `build.minify: "esbuild"`. Shave ~15% del bundle gzipped.
1752
+ - **Perceptual allowlist** [B-13]: reversión consciente del tripwire
1753
+ `dark axis-kobalium` (ΔE 0.0522) introducido en c8a5202 (beta.18).
1754
+ El tripwire nunca fue operativo (`error_threshold=0.05 < 0.0522`).
1755
+ NO se tocan tokens. Anti-regresión:
1756
+ `src/_audit/perceptual-allowlist.test.ts` falla CI si la entrada
1757
+ desaparece sin razón documentada.
1758
+ - **`merge-refs` consistente**: Stepper, Checkbox y Switch usan
1759
+ ahora el mismo patrón `useCallback(setRefs, [ref])`.
1760
+ - **Higiene del repo**: `.claude/`, `.notes-*`, `.release-*`,
1761
+ `BLOQUEOS.md`, `SESION-RESUMEN*.md` añadidos a `.gitignore`.
1762
+ `src/utils/env.ts` (huérfano post-B-07) borrado.
1763
+
1764
+ ### Docs
1765
+
1766
+ - **Pagination** uncontrolled documentado en README [B-14].
1767
+ - **`Introduction.mdx`**: banner pre-publicación recomendando
1768
+ `npm install reactigoded@beta` [B-11].
1769
+ - **`state.css` size** corregida en 4 sitios:
1770
+ `~6.5 MB sin gzip / ~700 KB gzipped` (antes "7.1 MB") [B-11].
1771
+ - **JSDoc** de `Rating`, `Tabs`, `TabsContext`: ejemplos migrados
1772
+ a `aria-label` HTML estándar (antes mostraban API obsoleta
1773
+ `ariaLabel`) [B-10].
1774
+ - **`docs/DS_AUTOSUFFICIENCY_DEBT.md`** capa 1.4 marcada cerrada
1775
+ [H-06].
1776
+ - **CHANGELOG** referencia stale a `BLOQUEOS.md` reemplazada por
1777
+ puntero a `docs/DS_AUTOSUFFICIENCY_DEBT.md` [H-07].
1778
+ - **`docs/RC1_DECISIONS.md`**: registro de las 4 decisiones humanas
1779
+ B1-B4 confirmadas explícitamente.
1780
+ - **`docs/POST_RC1_BACKLOG.md`** + **`docs/RC1_FOUND_DURING_FIX.md`**:
1781
+ trackers de la deuda diferida y los hallazgos descubiertos durante
1782
+ el ciclo.
1783
+ - **README**: sección "Desarrollo" documenta el requisito de
1784
+ `npm ci --legacy-peer-deps` [H-08].
1785
+
1786
+ ### Bundle stats (size-limit, gzip)
1787
+
1788
+ | Bundle | Tamaño | Límite | Uso |
1789
+ |---|---|---|---|
1790
+ | JS ESM | 13.96 KB | 16 KB | 87% |
1791
+ | JS CJS | 12.39 KB | 15 KB | 83% |
1792
+ | state.css | 713.54 KB | 800 KB | 89% |
1793
+ | components.css | 28.05 KB | 75 KB | 37% |
1794
+ | tokens.css | 6.53 KB | 30 KB | 22% |
1795
+
1796
+ JS ESM bajó de 14.91 KB (beta.21) a 13.96 KB tras añadir
1797
+ `build.minify: "esbuild"` (H-04), absorbiendo además el coste del
1798
+ `SkeletonContainer` nuevo y los 5 tests de hook adicionales.
1799
+
1800
+ ## [1.0.0-beta.21] — 2026-05-05
1801
+
1802
+ ### Added
1803
+ - **`useControllableState`** dev warn re-aplicado vía Option E
1804
+ (escape hatch interno `__suppressNoHandlerWarn`). Avisa cuando
1805
+ un componente está en modo controlled (`value` definido) sin
1806
+ `onChange` y sin el flag de suppress. Una vez por instancia.
1807
+ Tras el revert en beta.20 por falsos positivos en
1808
+ `Rating.SoloLectura`, ahora cada componente con modo
1809
+ display-only legítimo (Rating con `readOnly`) suprime el warn
1810
+ vía el flag interno.
1811
+ - **Audit consistente del hook** en los 9 componentes que lo
1812
+ usan: Rating y Slider ahora pasan `onChange: onValueChange` al
1813
+ hook (eliminando llamadas duplicadas locales). Switch usa
1814
+ `__suppressNoHandlerWarn: true` permanente porque su `onChange`
1815
+ recibe `ChangeEvent` (no boolean) y tiene un warn local más
1816
+ específico.
1817
+ - **Script `scripts/check-css-scope-leaks.mjs`** + allowlist
1818
+ `scope-leak-allowlist.json`. Cierra deuda capa 3.1 (PRIORIDAD
1819
+ ALTA). Detecta riesgo de scope-leak: clases globales
1820
+ modificadoras (`.ig-X-active`, `.ig-X-danger`…) emitidas en >1
1821
+ elemento del mismo componente. Modo `--strict` integrado en
1822
+ `verify:unit` pipeline. Run inicial: 420 candidatas / 6
1823
+ allowlisted (audit case-by-case con razón documentada) / 0
1824
+ riesgos nuevos.
1825
+
1826
+ ### Tests
1827
+ - Hook: 5 tests del warn (dispara/no dispara/escape hatch/una
1828
+ vez por instancia).
1829
+ - Rating: 2 tests anti-regresión (`readOnly` no genera warn;
1830
+ controlled sin readOnly y sin onValueChange sí lo genera).
1831
+
1832
+ ### Internal
1833
+ - `Rating.stories.tsx AllStates`: instancias `<Rating value={N} />`
1834
+ marcadas `readOnly` (eran galería visual, no interactivas).
1835
+ - README.md: actualizado a "última publicación: 1.0.0-beta.20".
1836
+
1837
+ ## [1.0.0-beta.20] — 2026-05-05
1838
+
1839
+ ### Added
1840
+ - **`Card`**: prop polimórfica `as` con genéricos TS
1841
+ (`<C extends ElementType = "div">`). Acepta strings HTML
1842
+ (`as="article"`, `as="a"` con `href` tipado) y componentes
1843
+ (`as={Link}` para react-router/next). Las props específicas del
1844
+ elemento subyacente se tipan automáticamente. Story `Polimorfica`
1845
+ añadida.
1846
+ - **`Pagination`**: modo **uncontrolled** vía `useControllableState`.
1847
+ `currentPage` y `onValueChange` ahora opcionales; `defaultPage`
1848
+ (default 1) inicializa el state interno. Patrón consistente con
1849
+ Tabs / Accordion / OptionsMenu / etc. Story `Uncontrolled` añadida.
1850
+ - **`Stepper`**: modo **interactive** opt-in con `onValueChange`.
1851
+ Cuando se pasa el callback, cada step se vuelve focuseable
1852
+ (`role="button"` + roving tabIndex) y soporta keyboard nav
1853
+ completo: ArrowLeft/Right/Up/Down, Home/End, Enter/Space. Sin
1854
+ `onValueChange` permanece presentational (backwards compatible).
1855
+ Nueva clase `.ig-step-interactive`. `aria-label="Paso N"` en
1856
+ cada dot interactivo.
1857
+ - **`Progress`**: props `loadingLabel` (default `"Cargando"`) y
1858
+ `formatLabel?: (percent: number) => string` para i18n del
1859
+ `aria-label`. Cierra el inventario de strings hardcoded
1860
+ user-facing del DS.
1861
+ - **`AllStates` matrix Ola 2 (16 componentes)**: stories
1862
+ `chromatic.modes light+dark` para Alert, Radio, Rating, Slider,
1863
+ Stepper, Pagination, Tooltip, Timeline, Accordion, Sidebar,
1864
+ Navbar, ThemeSwitch, Toast, OptionsMenu, Modal, Input compound,
1865
+ Table, Textarea, NativeSelect. Cobertura visual completa en Chromatic
1866
+ bajo dual-mode.
1867
+ - **`useControllableState` derive mode**: `ThemeSwitch` migrado
1868
+ a `useControllableState({ derive, setDerivedValue, ... })`.
1869
+ Consolida los 9 componentes con state controlled/uncontrolled
1870
+ bajo el mismo hook + soporte explícito para fuente de verdad
1871
+ externa.
1872
+ - **`docs/CSSAPI.mdx`**: nueva sección "i18n y a11y strings" con
1873
+ tabla de overrides + patrón de uso con `react-i18next`. Notas
1874
+ sobre `Card.as` polimórfica y `Pagination` controlled+uncontrolled.
1875
+ - **`docs/DS_AUTOSUFFICIENCY_DEBT.md`**: capa 6 nueva
1876
+ "Convenciones de tests unit". 6.1 ✅ regla anti-`console.error`
1877
+ mock para warnings React (causa: vitest `isolate: false` +
1878
+ React dedupe). 6.2 ✅ nota DOM Switch (`ig-switch` en `<label>`).
1879
+ - **`src/test-utils/`**: utilities `queryAllByRoleSafe()` y
1880
+ `expectAtLeast()` exportadas (capa 1.5/1.6 debt doc).
1881
+ - **Tests del CONTRATO** de `useControllableState` (4 tests
1882
+ hook-first cubriendo ambos modos × ambas direcciones de
1883
+ transición controlled↔uncontrolled). Smokes en componentes
1884
+ con input nativo (Rating, Slider, Switch).
1885
+
1886
+ ### Changed
1887
+ - **`Pagination`**: `currentPage` y `onValueChange` pasaron de
1888
+ required a opcionales. Cambio API público pre-1.0.0 (sin
1889
+ breaking productivo). Consumers que ya pasaban ambos siguen
1890
+ funcionando idénticamente.
1891
+ - **`Stepper`**: clase global `.ig-step-active` migrada a selector
1892
+ compound `.ig-step.ig-step-active` para evitar scope-leak al
1893
+ wrapper `.ig-step-item.ig-step-active` (bug latente desde beta.5
1894
+ detectado en sub-Bloque A: dejaba label en contraste 1.02).
1895
+ Anti-regresión test añadido.
1896
+ - Defaults ES de strings user-facing (`Alert.closeLabel`,
1897
+ `Chip.removeLabel`, `ModalClose aria-label`, `Pagination.prevLabel`/
1898
+ `nextLabel`, `Progress.loadingLabel`, `Stepper aria-label`,
1899
+ `Toast.closeLabel`) declarados intencionales en `CSSAPI.mdx`.
1900
+ Audience inicial hispanohablante; cambio a EN se reevaluará en
1901
+ 1.1.0 si demanda real lo justifica.
1902
+ - **`Slider` / `Switch`** tests viejos de transición controlled↔
1903
+ uncontrolled que afirmaban sobre warning de React vía
1904
+ `console.error` mock reemplazados por assertions sobre
1905
+ comportamiento observable. Los anteriores eran flaky por orden:
1906
+ vitest `isolate: false` + React dedupe de warnings dev por
1907
+ proceso. Documentado como regla en debt doc capa 6.1.
1908
+
1909
+ ### Fixed
1910
+ - **`Stepper`**: scope-leak CSS de `.ig-step-active` (ver Changed).
1911
+ - Documentación JSDoc de `Switch` clarifica estructura DOM
1912
+ (`ig-switch` en `<label>` wrapper, NO en `<input>`).
1913
+
1914
+ ### Internal
1915
+ - Stories storybook con `chromatic.modes light+dark` ya
1916
+ proporcionan dual-theme matrix sin duplicar archivos test:storybook.
1917
+ - Tracking de decisiones diferidas y deuda RC1 vive en
1918
+ `docs/DS_AUTOSUFFICIENCY_DEBT.md`.
1919
+ - Decisión arquitectónica registrada: warn dev de
1920
+ `useControllableState` para `value=` sin `onChange=` diferido a
1921
+ rc.1 con escape hatch interno `__suppressNoHandlerWarn`.
1922
+ Intento beta.20 (commit 2975e19) revertido por falsos positivos
1923
+ en `Rating.SoloLectura` (readOnly legítimo). Diseño Option E
1924
+ documentado en debt doc sección 1.4.
1925
+
1926
+ ### Bundle stats (size-limit, gzip)
1927
+
1928
+ | Bundle | Tamaño | Límite | Uso |
1929
+ |---|---|---|---|
1930
+ | JS ESM | 14.91 KB | 15 KB | 99.4% |
1931
+ | JS CJS | 12.85 KB | 15 KB | 85.7% |
1932
+ | `tokens.css` | 6.53 KB | 30 KB | 21.8% |
1933
+ | `components.css` | 28.05 KB | 75 KB | 37.4% |
1934
+ | `base.css` | 453 B | 2 KB | 22.7% |
1935
+ | `reset.css` | 924 B | 2 KB | 46.2% |
1936
+ | `fonts.css` | 142 B | 1 KB | 14.2% |
1937
+ | `state.css` | 713.54 KB | 800 KB | 89.2% |
1938
+ | `design.css` (meta) | 70 B | 2 KB | 3.5% |
1939
+
1940
+ JS ESM al 99% del budget — beta.21/rc.1 debería revisar headroom o
1941
+ subir el límite ante crecimiento de features (Card.as polimórfica
1942
+ + Stepper keyboard nav son los principales nuevos). state.css cerca
1943
+ del límite (89%) — es natural por el growth de utilities pseudo-class.
1944
+
1945
+ ## [1.0.0-beta.19] — 2026-05-04
1946
+
1947
+ ### Breaking
1948
+ - **Tooltip**: clases CSS migradas a prefijos únicos para evitar
1949
+ colisión semántica.
1950
+ - `ig-tooltip-{top,right,bottom,left}` → `ig-tooltip-place-{...}`
1951
+ - `ig-tooltip-{brand,secondary,success,warning,danger,info}` →
1952
+ `ig-tooltip-color-{...}`
1953
+ Solo afecta a consumers vanilla con clases hardcoded; React API
1954
+ intacta. Migración automática vía `scripts/migrate-tooltip-prefixes.mjs`.
1955
+ - **OptionsMenu**: clase de estado abierto migrada de `.open` a
1956
+ `.ig-options-menu-open` para respetar la convención de namespace `ig-*`.
1957
+ - **Stepper**: prop `defaultActive` eliminada (era engañosa — el
1958
+ componente nunca fue uncontrolled). Migración:
1959
+ `<Stepper defaultActive={X}>` → `<Stepper active={X}>`. Si necesitas
1960
+ uncontrolled, mantén `[step, setStep]` en el consumer.
1961
+ - **Navbar**: dos booleans `sticky`/`fixed` mutuamente excluyentes
1962
+ reemplazados por un único `position?: 'sticky' | 'fixed'`.
1963
+ Migración:
1964
+
1965
+ | Antes | Después |
1966
+ |---|---|
1967
+ | `<Navbar sticky>` | `<Navbar position="sticky">` |
1968
+ | `<Navbar fixed>` | `<Navbar position="fixed">` |
1969
+ | `<Navbar sticky fixed>` | (TS error, antes silencioso) |
1970
+
1971
+ ### Fixed
1972
+ - **OptionsMenu**: keyboard open salta items con `aria-disabled="true"`
1973
+ desde el trigger (fix incompleto del bug original que solo cubría
1974
+ `OptionsMenuItem`). Selector compartido en `optionsMenuSelectors.ts`.
1975
+ - **Tabs**: sin `value`/`defaultValue` selecciona el PRIMER tab
1976
+ registrado, no el último. El auto-select usa `setSelectedRaw` con
1977
+ `{ silent: true }` — ya no dispara `onValueChange`.
1978
+ - **Pagination**: clamp completo de `currentPage`, `totalPages` y
1979
+ `siblingCount` fuera de rango (NaN, Infinity, negativos). Separación
1980
+ `prevLabel`/`nextLabel` (children visible) de
1981
+ `prevAriaLabel`/`nextAriaLabel` (a11y).
1982
+ - **Progress**: guards para `max ≤ 0` o no-finite.
1983
+ - **Slider**: guard NaN.
1984
+ - **Card**: `tabIndex={0}` automático cuando actsAsButton (con override
1985
+ explícito respetado).
1986
+ - **Rating**: `readOnly` aplica `aria-readonly` al radiogroup, no
1987
+ `disabled` a los radios (mejora a11y para SR).
1988
+ - **Checkbox**/**Switch**/**Tab**: `useLayoutEffect` con fallback
1989
+ `useEffect` en SSR (no `() => {}`). Util compartida
1990
+ `src/utils/useIsoLayoutEffect.ts`.
1991
+ - **Tab**: keyboard nav usa `closest('[role="tablist"]')` en lugar de
1992
+ `parentElement` (robustez ante wrappers).
1993
+ - **Tooltip**: warn dev-only si `children` no es elemento React válido.
1994
+ - **README**: clases para migración de `<button>` corregidas
1995
+ (`.ig-btn*`, no `.ig-button*`).
1996
+ - **igoded-design.css**: cabecera `info → axis` corregida a
1997
+ `info → kobalium`.
1998
+ - **manager.ts** y stories Card/Avatar: hex de paleta actualizados a
1999
+ la vigente desde beta.16 (`#5eded5` → `#3ae2f7`,
2000
+ `#d4c2f9` → `#d2bff7`).
2001
+ - **SKILL.md**: tabla ΔE OKLab regenerada con cifras reales (eran de
2002
+ paleta pre-beta.16).
2003
+ - **styles.test.ts**: test "7 cardinales" verificaba 6, faltaba
2004
+ kobalium.
2005
+ - **mixColors** en `check-component-contrast.mjs`: shortest-arc para
2006
+ hue circular (bug latente que no se disparaba con tokens actuales
2007
+ pero podía con futuros).
2008
+
2009
+ ### Added
2010
+ - **`useControllableState<T>`** hook centralizado para patrón
2011
+ controlled/uncontrolled, con setter `silent` opt-in. **9
2012
+ componentes migrados**: Switch, Sidebar, Slider, Rating, Accordion
2013
+ (single + multiple), Alert, OptionsMenu, Tabs. 9 tests del hook
2014
+ (incluyendo `setValue` con `{ silent: true }` para auto-selects y
2015
+ rehidratación de fuentes externas no-de-usuario).
2016
+ - **AllStates Ola 1** — 14 stories matrix con `chromatic.modes`
2017
+ light + dark: Button, Input, Checkbox, Switch, Tabs, Progress,
2018
+ Card, Badge, Spinner, Skeleton, Divider, Breadcrumb, Avatar, Chip.
2019
+ Util compartida `src/stories/_matrix.tsx` con `MatrixGrid`. 28
2020
+ snapshots Chromatic. Ola 2 (16 componentes interactivos +
2021
+ compounds) en beta.20.
2022
+ - **Tests regresión visual AllStates** — 14 tests vía `composeStory`,
2023
+ uno por componente Ola 1, anti-regresión de "alguien borra una
2024
+ variant".
2025
+ - **`Fundamentos/CSS API pública`** — MDX exhaustiva con tabla
2026
+ detallada y ejemplo HTML por los 32 componentes para consumers
2027
+ vanilla. Linkada desde el README.
2028
+ - **`Fundamentos/Catálogo AllStates`** — índice MDX que lista los
2029
+ matrices visuales por ola.
2030
+ - **`scripts/perceptual-allowlist.json`** con 2 excepciones
2031
+ documentadas (`laurus-vitreus` LIGHT 0.0847,
2032
+ `malum-rutilus` DARK 0.0706).
2033
+ - **`check-component-contrast.mjs --print-perceptual-table`** modo
2034
+ debug para regenerar tabla del SKILL.
2035
+ - **Tests añadidos** (no exhaustivo): OptionsMenu aria-disabled (×2),
2036
+ Tabs auto-select silent (×4), Pagination clamps (×6),
2037
+ Pagination prev/nextAriaLabel (×2), Progress guards (×6),
2038
+ Card tabIndex (×3), Rating aria-readonly (×2),
2039
+ useControllableState (×9), Tab keyboard wrapped, Spinner.label i18n
2040
+ (×3), Sidebar.ariaLabel i18n (×3).
2041
+ - **`.github/workflows/verify.yml`** — gate CI completo (lint,
2042
+ typecheck, test:unit, test:contrast, build, verify:size) + job
2043
+ separado `storybook` con playwright.
2044
+ - **`size-limit`** budget para `igoded-state-css.css` (techo 800 KB
2045
+ gzip).
2046
+ - **`src/utils/env.ts`** — `isDev()` helper sin global augmentation.
2047
+ - **`src/utils/useIsoLayoutEffect.ts`** — patrón canónico SSR-safe.
2048
+ - **i18n explícitas**: `Spinner.label`, `Sidebar.ariaLabel`,
2049
+ `Avatar.statusLabel`, `Pagination.prevAriaLabel`/`nextAriaLabel`.
2050
+ - **`scripts/migrate-tooltip-prefixes.mjs`** — pase con PostCSS AST
2051
+ para la migración de Tooltip.
2052
+
2053
+ ### Changed
2054
+ - **`scripts/check-component-contrast.mjs`**: Check 3 ΔE OKLab
2055
+ promovido a ERROR con umbral 0.05 + WARN umbral 0.10. Allowlist
2056
+ explícita con drift detection (95% del valor de decisión). Cómputo
2057
+ automático de los pares de cardinales por tema (30 totales).
2058
+ - **`dist/env.d.ts`** ya no se publica al consumer
2059
+ (`tsconfig.build.json` lo excluye explícitamente). Componentes
2060
+ ahora usan `isDev()` util en lugar de `import.meta.env.DEV` directo.
2061
+ - **`Modal`**: `ModalContextValue` removido del barrel export (era
2062
+ huérfano sin `ModalContext` ni `useModal()` exportados).
2063
+ - **CSS publicados minificados con esbuild** (state.css 6.3 MB →
2064
+ 713 KB gzipped, components.css 28 KB gzipped).
2065
+ - **`design.css`** size-limit budget bajado de 5 KB a 2 KB (real
2066
+ ~70 B gzipped).
2067
+ - **`tokens.css`** size-limit budget subido de 25 KB a 30 KB para
2068
+ margen.
2069
+
2070
+ ### Removed
2071
+ - CSS huérfano: `.ig-modal`, `.ig-modal-backdrop` en
2072
+ `igoded-components.css` (3 ocurrencias residuales del cleanup
2073
+ beta.0; el componente Modal usa `<dialog>`/`.ig-dialog`).
2074
+ - Referencias prematuras a `1.0.0-rc.{1,2,3}` en código y docs
2075
+ (sustituidas por `1.0.0-beta.8` o `pre-1.0.0-rc.1` según contexto).
2076
+ - `Stepper.defaultActive` (era prop muerta).
2077
+ - `src/env.d.ts` (sustituido por `src/utils/env.ts` con `isDev()`).
2078
+
2079
+ ### Notes
2080
+ - **2 componentes saltados en migración a `useControllableState`**
2081
+ por razones técnicas documentadas:
2082
+ - **`ThemeSwitch`**: triple fuente de truth (controlled prop +
2083
+ override interno + storage cross-tab vía `useSyncExternalStore`).
2084
+ El componente original deriva el valor en render directamente
2085
+ de las fuentes; el hook genérico introduciría un `useState`
2086
+ interno que en happy-dom provoca loop infinito por la cadena
2087
+ `setItem → StorageEvent → snapshot revaluation → setState →
2088
+ setItem`. El patrón actual es idiomático para este caso, no es
2089
+ deuda técnica. Para post-RC1: extender el hook con modo
2090
+ `derive: () => T` opt-in o crear
2091
+ `useControllableStateWithStorage` dedicado.
2092
+ - **`Modal`**: controlled-only puro (`open: boolean` requerido,
2093
+ sin `defaultOpen`). El hook no aporta valor; siempre sería
2094
+ `isControlled=true`.
2095
+ - **`Checkbox`**: NO tiene `useState` interno — delega checked
2096
+ al `<input type="checkbox">` nativo. La migración añadiría
2097
+ complejidad sin valor.
2098
+ - **Migration guide útil — `useControllableState` API**:
2099
+
2100
+ ```ts
2101
+ const { value, setValue } = useControllableState({ value, defaultValue, onChange });
2102
+
2103
+ // Acción del usuario (dispara onChange):
2104
+ setValue(newValue);
2105
+
2106
+ // Auto-select interno o rehidratación post-mount (NO dispara onChange):
2107
+ setValue(newValue, { silent: true });
2108
+ ```
2109
+
2110
+ - **Tag retroactivo `v1.0.0-beta.0`** apuntado al commit
2111
+ `8a1c7ef5f030ba7bf227b53a0b8469fa442149e3` (`feat: rewrite as
2112
+ TypeScript + React 19 design system`, 2026-05-01) para
2113
+ trazabilidad histórica completa de la rama 1.0.x.
2114
+
2115
+ ### Bundle stats (gzip)
2116
+
2117
+ | Archivo | Real | Budget | Margen |
2118
+ |--------------------------------------------|----------|---------|--------|
2119
+ | `dist/index.js` (ESM) | 14.37 KB | 15 KB | 4% |
2120
+ | `dist/index.cjs` | 12.36 KB | 15 KB | 18% |
2121
+ | `dist/styles/igoded-design.css` | 70 B | 2 KB | 97% |
2122
+ | `dist/styles/igoded-tokens.css` | 6.53 KB | 30 KB | 78% |
2123
+ | `dist/styles/igoded-components.css` | 28.05 KB | 75 KB | 63% |
2124
+ | `dist/styles/igoded-base.css` | 453 B | 2 KB | 78% |
2125
+ | `dist/styles/igoded-reset.css` | 924 B | 2 KB | 55% |
2126
+ | `dist/styles/igoded-fonts.css` | 142 B | 1 KB | 86% |
2127
+ | `dist/styles/igoded-state-css.css` | 713.5 KB | 800 KB | 11% |
2128
+
2129
+ ### Test stats
2130
+ - Suite unit final: **515** tests (`vitest run --project unit`).
2131
+ - 36 archivos de test, 100% verde.
2132
+ - 14 nuevos AllStates regression tests (composeStory + selectores
2133
+ CSS).
2134
+
2135
+ ## [1.0.0-beta.18] — 2026-05-03
2136
+
2137
+ ### Changed
2138
+ - **Test cleanup pre-RC1**: refactor masivo de tests unit.
2139
+ - **Variantes/sizes/forma cosmética**: colapsadas en `describe.each`
2140
+ parametrizado. Antes había 10 asserts sueltos por componente
2141
+ (uno por valor del eje); ahora un solo bloque parametrizado
2142
+ cubre todos los valores con una assert densa. Misma señal de
2143
+ contrato CSS, código 5× más compacto. Aplicado en Badge, Button,
2144
+ Card, Checkbox, Chip, Divider, Input, Modal, Navbar, Progress,
2145
+ Radio, Spinner, Switch, Tabs, Tooltip, Avatar, Rating.
2146
+ - **Estados funcionales con efecto observable**: `loading` del
2147
+ Button ahora expone `aria-busy` además de la clase, y el test
2148
+ valida ARIA + behavior (no solo clase). Combo `loading + disabled`
2149
+ cubierto explícitamente.
2150
+ - **Negative asserts** (`not.toHaveClass(default)`, `not.toHaveClass(success)`
2151
+ cuando outline) eliminados — paranoia frágil que rompía con
2152
+ cualquier rename.
2153
+ - **`className` merge desde consumer**: añadido test por componente
2154
+ (Badge, Button, Switch, Checkbox, Radio) verificando que las
2155
+ clases del consumer se mergean sin pisar las del componente. Bug
2156
+ recurrente que Chromatic no detecta.
2157
+ - **Stories `play()` duplicadas**: eliminadas `Checkbox.ToggleInteraction`
2158
+ y `Radio.SelectInteraction` (mismo aserto que el unit test, 10×
2159
+ más lentas).
2160
+
2161
+ ### Added
2162
+ - `Button` ahora expone `aria-busy={loading}` (antes solo clase).
2163
+ Mejora a11y para SR.
2164
+
2165
+ ### Test stats
2166
+ - Suite unit final tras refactor: **453** (`vitest run --project unit`).
2167
+ Con `describe.each` el conteo bruto sube por la expansión de cada
2168
+ parametrización; lo relevante es que el contrato CSS/ARIA queda
2169
+ cubierto con menos archivos y asserts más densos.
2170
+ - Asserts cosméticos sueltos eliminados: ~80.
2171
+ - Tests transversales nuevos: 5 (`className` merge), 1 combo de
2172
+ estado peligroso (`loading + disabled`).
2173
+ - Nota: a partir de esta versión NO publicamos delta numérico
2174
+ contra la versión anterior; el conteo puede oscilar libremente
2175
+ con cada refactor de parametrización y no es una métrica
2176
+ contractual del paquete.
2177
+
2178
+ ## [1.0.0-beta.17] — 2026-05-03
2179
+
2180
+ ### Fixed
2181
+ - **Textarea / NativeSelect estados `error` y `success` invisibles**: las
2182
+ reglas `.ig-input-error` / `.ig-input-success` (línea 6227) estaban
2183
+ declaradas ANTES de `.ig-textarea`, `.ig-textarea-auto` y `.ig-native-select`
2184
+ (líneas 6306, 6343, 6376), todas con la misma especificidad 0,1,0.
2185
+ Las reglas posteriores definían `border-color` y pisaban al estado de
2186
+ validación. Resultado: pasar `state="error"` o `state="success"` no
2187
+ cambiaba el borde del componente. Fix: reglas con doble clase (0,2,0)
2188
+ para `.ig-textarea.ig-input-error`, `.ig-native-select.ig-input-error`, etc.
2189
+ Input no estaba afectado porque `.ig-input` se declara antes de las
2190
+ reglas de estado.
2191
+
2192
+ ## [1.0.0-beta.16] — 2026-05-03
2193
+
2194
+ ### Changed
2195
+ - **Vitreus reposicionado** de H≈194.83° (cyan puro sRGB, artefacto
2196
+ histórico de la paleta) a H=207.50° (centro perceptual matemático
2197
+ verde↔azul en OKLCH, punto medio entre verde Hering ~145° y azul
2198
+ Hering ~270°). Hex: `#053a40` (lux) / `#3ae2f7` (nox). Decisión
2199
+ fundacional — vitreus es el color brand del DS y su identidad
2200
+ declarada es "el azul-verde matemáticamente medio en OKLCH"; el hex
2201
+ anterior no cumplía esa intención. Contraste AAA preservado en los
2202
+ 5 fondos del tema en ambos modos (LIGHT 11.78, DARK 11.91 sobre
2203
+ fundus opuesto). Las alphas (`--ig-vitreus-alpha-{10,20,30,50,70}`)
2204
+ y `--ig-text-heading` heredan automáticamente porque están definidas
2205
+ con `color-mix` sobre `--ig-vitreus`. Re-baseline de Chromatic
2206
+ requerida para snapshots con brand, text-heading o sus alphas.
2207
+
2208
+ ### Fixed
2209
+ - **Badge default vs Badge pill**: ambos usaban `--ig-rounded-full` y
2210
+ eran visualmente idénticos. Ahora el default es `--ig-rounded-md`
2211
+ (chip estilo etiqueta, esquinas suavemente redondeadas) y `pill`
2212
+ mantiene `--ig-rounded-full` (cápsula).
2213
+
2214
+ ## [1.0.0-beta.15] — 2026-05-03
2215
+
2216
+ ### Fixed
2217
+ - **Elevación en LIGHT** (`--ig-bg-surface`, `--ig-bg-elevated`): los
2218
+ tokens mezclaban con `white` para "elevarse", pero como el body en
2219
+ LIGHT es `--ig-fundus-lux` = `#faf9fc` (casi blanco) el resultado era
2220
+ prácticamente igual al body. Cards, accordions, dropdowns, modals,
2221
+ toasts, sidebars y tabs se solapaban con el fondo. Ahora la elevación
2222
+ en LIGHT se consigue OSCURECIENDO ligeramente con tinte azulado
2223
+ coherente (`--ig-cinis-lux` 4% para surface, 8% para elevated). DARK
2224
+ intacto.
2225
+
2226
+ ## [1.0.0-beta.14] — 2026-05-03
2227
+
2228
+ ### Fixed
2229
+ - **Divider con texto + variante** pintaba el `<div>` padre entero
2230
+ con el color de variante. La regla `.ig-divider-brand
2231
+ { background-color: var(--ig-vitreus); }` (del divider básico
2232
+ horizontal `<hr>`) matcheaba también al
2233
+ `<div class="ig-divider-with-text ig-divider-brand">` por compartir
2234
+ la clase modifier. Resultado: caja cyan brillante con el texto cyan
2235
+ encima — invisible. Fix: scope a `.ig-divider.ig-divider-brand` para
2236
+ que solo aplique al divider básico (que sí lleva la clase base
2237
+ `.ig-divider`).
2238
+
2239
+ ## [1.0.0-beta.13] — 2026-05-03
2240
+
2241
+ ### Changed
2242
+ - **Divider con texto variante** invierte la jerarquía de color:
2243
+ ahora el TEXTO recibe el accent (`vitreus`, `axis`, ...) y las
2244
+ líneas se quedan en `--ig-border-subtle`. Antes (beta.10–12) las
2245
+ líneas iban con accent y el texto en `cinis`; las dos líneas
2246
+ alineadas creaban una banda continua perceptual y el texto quedaba
2247
+ apagado. Patrón Material UI: las líneas son sutiles, el color
2248
+ semántico vive en el label.
2249
+
2250
+ ## [1.0.0-beta.12] — 2026-05-03
2251
+
2252
+ ### Removed
2253
+ - Stories aisladas `Indeterminate` de `Checkbox` y `Switch`.
2254
+ Confundían — el click no parecía hacer nada porque el sticky
2255
+ behavior re-aplicaba `indeterminate=true` sin un parent que
2256
+ gestione state. La nueva story `MasterSelectAll` (beta.11) ya
2257
+ muestra el indeterminate en su estado inicial y demuestra el
2258
+ patrón canónico.
2259
+
2260
+ ## [1.0.0-beta.11] — 2026-05-03
2261
+
2262
+ ### Added
2263
+ - Stories `Componentes/Checkbox/MasterSelectAll` y
2264
+ `Componentes/Switch/MasterSelectAll` con el patrón canónico de
2265
+ indeterminate (maestro + grupo de hijos). El `indeterminate` no es
2266
+ un tercer estado del toggle, es una etiqueta visual derivada del
2267
+ estado de los hijos — patrón usado por GitHub, Gmail, Material UI
2268
+ checkbox group, Ant Design Tree, etc.
2269
+
2270
+ ### Reverted
2271
+ - Cambios en `Checkbox` y `Switch` glyph publicados en `beta.10`
2272
+ (`var(--ig-fundus)` adaptativo): diagnóstico erróneo, el reporte
2273
+ "indeterminate no hace nada" era de comportamiento (sticky
2274
+ behavior), no de contraste visual. El glyph vuelve a
2275
+ `var(--ig-fundus-lux)`. El cambio del Divider en `beta.10` se
2276
+ mantiene.
2277
+
2278
+ ## [1.0.0-beta.10] — 2026-05-03
2279
+
2280
+ ### Changed
2281
+ - **Divider con texto variante**: el texto ya no toma el color del
2282
+ accent (era idéntico a las líneas y se confundía visualmente). Va
2283
+ en `--ig-cinis`. (Posteriormente revisado en `beta.13`: ahora el
2284
+ texto SÍ va en accent y las líneas en subtle.)
2285
+
2286
+ ### Fixed (luego revertido en beta.11 — diagnóstico erróneo)
2287
+ - ~~Glyph del Checkbox/Switch (`tick`, línea de indeterminate, thumb)
2288
+ cambiado a `var(--ig-fundus)` adaptativo. Pretendía resolver bajo
2289
+ contraste blanco-sobre-cyan en DARK; resultó no ser el bug
2290
+ reportado.~~
2291
+
2292
+ ## [1.0.0-beta.9] — 2026-05-03
2293
+
2294
+ ### Fixed
2295
+ - **Progress bar invisible**: el componente renderizaba el bar como
2296
+ `<span class="ig-progress-bar">`. Como `<span>` es `display: inline`
2297
+ por defecto y el CSS no le aplicaba `display: block`, el `width`
2298
+ inline se ignoraba — todas las variantes se veían iguales (solo el
2299
+ track gris del fondo). Cambiado a `<div>`, alineado con la doc del
2300
+ CSS que ya prescribía `<div>`.
2301
+
2302
+ ## [1.0.0-beta.8] — 2026-05-03
2303
+
2304
+ Consolida el trabajo que estuvo bajo `rc.1` / `rc.2` / `rc.3` (tags
2305
+ borrados, no llegaron a publicarse) más los fixes finales sobre
2306
+ `Progress` y `Checkbox`/`Switch` detectados al revisar el visual.
2307
+
2308
+ ### Added
2309
+ - **Prop `indeterminate?: boolean`** en `Checkbox` y `Switch`. Hasta
2310
+ `beta.7` el tercer estado solo se podía aplicar via ref-callback
2311
+ (`ref={el => { if (el) el.indeterminate = true }}`), patrón
2312
+ documentado pero poco ergonómico. Ahora basta `<Checkbox indeterminate>`
2313
+ / `<Switch indeterminate>`. Internamente un `useEffect` sincroniza la
2314
+ prop con `el.indeterminate` nativo (la propiedad del DOM, ya que
2315
+ `indeterminate` no es atributo HTML válido). El ref-callback sigue
2316
+ funcionando — los componentes ahora usan un `internalRef` y forwardean
2317
+ al ref del consumer si se pasa.
2318
+ - **El estado `indeterminate` es sticky tras click**. El navegador limpia
2319
+ `el.indeterminate` automáticamente cuando el usuario clica un checkbox
2320
+ con `.indeterminate=true` (toggle nativo a checked/unchecked). Si la
2321
+ prop sigue `true`, `handleChange` re-aplica `el.indeterminate=true` —
2322
+ solo el parent decide cuándo salir del estado mixto pasando
2323
+ `indeterminate={false}`. Sin esto, tras el primer click el visual
2324
+ pasaba a checked normal aunque la prop dijera "parcial".
2325
+ - **`aria-checked="mixed"`** automático en `Checkbox`/`Switch` cuando
2326
+ `indeterminate=true`, para que NVDA/JAWS/VoiceOver anuncien el estado
2327
+ parcial correctamente.
2328
+ - **Visual `:indeterminate`** en `Checkbox` y `Switch`. Hasta `beta.7`
2329
+ el `<input>` nativo se ocultaba detrás de un overlay y las reglas CSS
2330
+ solo cubrían `:checked` / `:focus-visible` / `:disabled`, así que
2331
+ `el.indeterminate=true` no tenía diferencia visual.
2332
+ - **Checkbox**: fondo lleno (color de variante, igual que `:checked`)
2333
+ y glyph "−" (línea horizontal centrada).
2334
+ - **Switch**: thumb centrado en el track con background de variante
2335
+ (Material-like — comunica "ni on ni off" sin glyphs adicionales).
2336
+ - Las 6 variantes (`brand`/`secondary`/`success`/`warning`/`danger`/`info`)
2337
+ tienen su color correspondiente en ambos.
2338
+ - **Story `Switch.Indeterminate`** (no existía).
2339
+ - **Story `Checkbox.Indeterminate`** refactorizada: usa
2340
+ `args: { indeterminate: true }` en vez del ref-callback (mejor ejemplo).
2341
+
2342
+ ### Changed
2343
+ - **Progress bars siempre usan los `-nox`** (cardinales brillantes) en
2344
+ ambos temas, no los adaptativos `var(--ig-{role})`. Razón: en LIGHT
2345
+ los `-lux` tienen luminosidad y chroma demasiado bajos para
2346
+ distinguirse en una franja de 8-12 px (las 6 variantes se veían
2347
+ "todas oscuras" y costaba diferenciar hue). El track sigue adaptativo
2348
+ via `var(--ig-progress-track)`. Los demás componentes mantienen
2349
+ tokens adaptativos porque tienen texto encima donde el contraste
2350
+ WCAG sí importa; en `Progress` el bar es bloque sólido sin texto.
2351
+ - **`Progress.Variantes`** story rediseñada — antes 5 barras
2352
+ `size="md"` (8 px) sin label. Ahora 6 barras `size="lg"` (12 px)
2353
+ con `<span class="ig-story-label">` arriba de cada una. `play()`
2354
+ afirma via `getComputedStyle` que las 6 barras tienen
2355
+ `background-color` distinto — regresión si alguien rompe la cascade
2356
+ `.ig-progress-{v} .ig-progress-bar`.
2357
+ - **8 stories de Interacción propagan `args`** — `Radio.SelectInteraction`,
2358
+ `Modal.OpenInteraction`, `Accordion.Interaction`,
2359
+ `Sidebar.ToggleInteraction`, `Stepper.Interactivo`,
2360
+ `Tabs.KeyboardNavInteraction`, `Toast.FireInteraction`,
2361
+ `Tooltip.A11yInteraction` tenían `render: () => (<Comp prop="…" />)`
2362
+ con valores hardcoded, lo que dejaba el panel Controls desconectado
2363
+ del render. Ahora `render: (args) => (<Comp {...args} … />)` con
2364
+ overrides finales solo donde el `play()` necesita un valor concreto
2365
+ (state controlado, `defaultValue` predecible, payload del toast).
2366
+ Cuando hace falta un default distinto al del meta se añade
2367
+ `args: {…}` a nivel de story. `FireToastButton` se inlinea para
2368
+ leer `args.title/message/variant`.
2369
+ - **`laurus` recalibrado de H=149° a H≈140°** para subir la separación
2370
+ perceptual ΔE OKLab `laurus ↔ vitreus` en LIGHT de ≈0.054 (rozaba el
2371
+ umbral del Check 3 = 0.05) a ≈0.074. Hex nuevos:
2372
+ `--ig-laurus-lux: #143d0a`, `--ig-laurus-nox: #6aed4a` (antes
2373
+ `#113d1c` / `#5eeb82`). Geometría dual preservada (L_lux=0.319,
2374
+ L_nox=0.841, ΔH=0.11°, suma 1.160). El alias `--ig-success` sigue
2375
+ apuntando a `--ig-laurus`, ningún consumer que use rol `success` debe
2376
+ tocar nada. Las alphas `--ig-laurus-alpha-{10,20,30,50,70}` siguen
2377
+ siendo `color-mix` y arrastran el cambio automáticamente.
2378
+ Visualmente: más vegetal puro, menos "menta caribeña". Re-baseline
2379
+ de Chromatic obligatorio.
2380
+
2381
+ ### Fixed (Storybook only — no afecta al paquete publicado)
2382
+ - **Toggle light/dark del toolbar de Storybook funciona también en
2383
+ páginas MDX puras** (`Contrast`, `DesignTokens`, `Introduction`,
2384
+ `Spacing`, `Variants`). Antes el toggle solo afectaba a las Stories
2385
+ CSF; las MDX se quedaban en el último `data-theme` aplicado por la
2386
+ Story anterior. Causa raíz: el decorator `withThemeByDataAttribute`
2387
+ solo se ejecuta cuando se monta una Story; las MDX sin `<Story of=…>`
2388
+ ni `<Canvas>` que referencie un export son páginas estáticas que no
2389
+ disparan decorators. Fix: `.storybook/preview-head.html` se suscribe
2390
+ al canal `__STORYBOOK_ADDONS_CHANNEL__` y reacciona al evento
2391
+ `updateGlobals` aplicando `data-theme` al `<html>` del iframe.
2392
+ Idempotente con el decorator existente.
2393
+
2394
+ ### Documentation
2395
+ - **README** bloque "Estado": ahora documenta `1.0.0-beta.8` con la
2396
+ paleta final + rename `cyaneus → kobalium` (de `beta.7`) +
2397
+ recalibración de `laurus` + nueva prop `indeterminate` +
2398
+ Progress bars `-nox`.
2399
+ - **README** ejemplo de `Pagination` corregido —
2400
+ `ariaLabel="Pagination"` → `aria-label="Pagination"` (era
2401
+ inconsistente con la migración a `aria-*` estándar de `beta.4`).
2402
+ - **`igoded-components.css`** cabecera: `info → kobalium (cyan)` →
2403
+ `info → kobalium (cobalt blue)` (residuo del sed `cyaneus → kobalium`
2404
+ de `beta.7` que dejó `(cyan)` colgando). Aclarado "6 roles UI (el
2405
+ sistema tiene 7 cardinales)" para no confundir cardinales con roles.
2406
+ - **Migration note ampliada** sobre el rename `cyaneus → kobalium` de
2407
+ `beta.7` distinguiendo:
2408
+ - **API estable** (no tocar nada): `var(--ig-info)`, `.ig-bg-info`,
2409
+ `.ig-text-info`, `.ig-alert-info`, `.ig-badge-info`, `.ig-btn-info`,
2410
+ etc. (30+ clases con sufijo de **rol**, no de cardinal).
2411
+ - **Primitivos internos** (renombrar si los usabas directo):
2412
+ `--ig-cyaneus` / `--ig-cyaneus-{lux,nox}` /
2413
+ `--ig-cyaneus-alpha-{10,20,30,50,70}` / `--ig-text-on-cyaneus` →
2414
+ `--ig-kobalium*` / `--ig-text-on-kobalium`.
2415
+
2416
+ ### Self-correction
2417
+ - La entrada de `1.0.0-beta.6` decía que `.ig-story-canvas` se movía a
2418
+ `.storybook/preview-head.html`. La ruta correcta es
2419
+ `.storybook/storybook.css` (consumido como `className` desde
2420
+ `.storybook/preview.tsx`). El `preview-head.html` no se tocó en
2421
+ `beta.6` — sigue como en `beta.5` con la inyección de
2422
+ `data-theme="dark"`.
2423
+
2424
+ ## [1.0.0-beta.7] — 2026-05-03
2425
+
2426
+ ### Renamed (BREAKING — token rename + hue reassignment)
2427
+ - **`cyaneus` → `kobalium`** y **reasignación de hue de cyan H≈214° a
2428
+ cobalt blue H≈260°**. Razón: la separación perceptual ΔE OKLab entre
2429
+ `cyaneus` y `vitreus` (brand teal) era marginal (≈0.054 en dark), lo
2430
+ que producía confusión visual cuando ambos cardinales aparecían
2431
+ contiguos. Con kobalium en H≈260° la separación pasa a ≈0.123 (cómoda).
2432
+ El nombre cambia porque "cyaneus" describía un cyan que ya no existe
2433
+ como color real; "kobalium" (latinización de cobalto) refleja el nuevo
2434
+ hue. Hex nuevos: `--ig-kobalium-lux: #082e6d`, `--ig-kobalium-nox: #b1ccf7`
2435
+ (ambos cumplen geometría dual L_lux≈0.32, L_nox≈0.84, ΔH≤10°, AAA contra
2436
+ los 5 fondos del tema).
2437
+
2438
+ **Migración**:
2439
+ - Si tu código usa solo el alias `var(--ig-info)`, **no tocar nada** —
2440
+ el alias sigue funcionando y ahora apunta internamente a `--ig-kobalium`.
2441
+ - Si tu código referencia literalmente `--ig-cyaneus`, `--ig-cyaneus-lux`,
2442
+ `--ig-cyaneus-nox`, `--ig-cyaneus-alpha-{10,20,30,50,70}` o
2443
+ `--ig-text-on-cyaneus`, **renombrar a `--ig-kobalium*`** /
2444
+ `--ig-text-on-kobalium`.
2445
+ - Re-baseline de Chromatic obligatorio: el cambio de hex altera todas
2446
+ las stories que usan info/cyaneus en cualquier forma.
2447
+
2448
+ ### Added
2449
+ - **Check 3 (warning) en `scripts/check-component-contrast.mjs`**:
2450
+ separación perceptual ΔE OKLab ≥ 0.05 entre pares de cardinales de UI
2451
+ activa (los 6 cardinales menos `cinis`, que es texto del cuerpo). Útil
2452
+ para detectar cuándo un cardinal nuevo entra en zona de confusión con
2453
+ uno existente. **No falla CI** — solo imprime aviso. Razón de no
2454
+ promocionar a error en esta beta: hay pares preexistentes (p.ej.
2455
+ `vitreus↔laurus` en light, ΔE≈0.054) que rozan el umbral; auditarlos
2456
+ con cabeza fría requiere su propio commit.
2457
+
2458
+ ### Changed
2459
+ - Doc cabecera de `igoded-tokens.css`, `SKILL.md`, `Introduction.mdx`,
2460
+ `Variants.mdx`, `Palette.stories.tsx`: actualizada la descripción de
2461
+ kobalium (cobalto, H≈260°) y la migration note desde cyaneus.
2462
+
2463
+ ## [1.0.0-beta.6] — 2026-05-03
2464
+
2465
+ ### Changed (BREAKING — visual)
2466
+ - **`reset.css` ya no estiliza `<button>` con la marca**. Hasta beta.4
2467
+ el reset aplicaba `background: var(--ig-vitreus); color: var(--ig-text-on-vitreus)`
2468
+ a todo `<button>` sin clase. Desde beta.5 los `<button>` que importas
2469
+ con `reset.css` salen `background: transparent; color: inherit;
2470
+ border: 0; padding: 0; cursor: pointer;`. Si tu app dependía de que
2471
+ cualquier `<button>` nativo apareciese con look brand "gratis", ahora
2472
+ los verás transparentes sobre el fondo de su contenedor. Migración:
2473
+ añade la clase `.ig-button` (o variante `.ig-button-primary`,
2474
+ `.ig-button-secondary`…) a esos `<button>`. Razón del cambio: evitar
2475
+ combinaciones bg/color heredadas que rompían contraste cuando un
2476
+ wrapper aplicaba sus propios colores (caso real: SidebarItem con
2477
+ texto `cinis` sobre `<button>` con bg vitreus → ratio 1.06).
2478
+
2479
+ ### Documentation
2480
+ - `igoded-components.css` cabecera: `info → axis` corregido a
2481
+ `info → cyaneus`.
2482
+ - `igoded-tokens.css` "CUÁNDO USAR": `info` quitado de la línea de Axis
2483
+ y añadido a una línea propia para Cyaneus.
2484
+ - `SKILL.md` y `README.md`: documentado el scope real de
2485
+ `npm run test:contrast` (qué cubre y qué no).
2486
+
2487
+ ### Added
2488
+ - Bloque `@media (forced-colors: active)` quirúrgico en
2489
+ `igoded-components.css` para estados `*-active`/`*-selected` (Tabs,
2490
+ Pagination, Stepper, Chip, Sidebar, Navbar) → `Highlight`/`HighlightText`,
2491
+ e inputs (`.ig-input`, `.ig-native-select`, `.ig-textarea`) → `Field`/`FieldText`.
2492
+ Complementa el mapeo semántico de `igoded-tokens.css` sin duplicarlo.
2493
+ - Wrapper canvas de Storybook movido de inline-style a clase
2494
+ `.ig-story-canvas` en `.storybook/preview-head.html`.
2495
+
2496
+ ## [1.0.0-beta.5] — 2026-05-03
2497
+
2498
+ ### Changed (BREAKING)
2499
+ - **Rediseño total de la paleta cardinal — geometría OKLCH uniforme**.
2500
+ Los 14 hex de los pares `{cardinal}-{lux,nox}` (vitreus, axis, cinis,
2501
+ rutilus, laurus, malum, cyaneus) cambian para cumplir
2502
+ `L_lux ≈ 0.32 ± 0.04`, `L_nox ≈ 0.84 ± 0.04`, `ΔH OKLCH ≤ 10°` entre
2503
+ lux/nox del mismo cardinal y AAA frente a los 5 fondos del tema.
2504
+ Diferencia visible más fuerte: `vitreus-nox` pasa de teal pastel
2505
+ `#5eded5` a cyan eléctrico `#30e6e6`; `laurus-nox` a verde brillante
2506
+ `#5eeb82`; `axis-lux` a violeta `#411271`. Resto: cambios sutiles.
2507
+ `--ig-fundus-{lux,nox}` se mantienen.
2508
+ - **`--ig-bg-{surface,sunken,elevated,muted}` derivados de `fundus`**.
2509
+ Ahora se generan vía `color-mix(in oklch, var(--ig-fundus-{lux,nox}), …)`.
2510
+ Si cambias `fundus`, todo el tema acompaña automáticamente. Output
2511
+ visual ±1 punto del previo.
2512
+ - **`engines.node`**: `>=20` → `>=22`. Node 20 (Iron) llegó a EOL el
2513
+ 2026-04-30 y ya no recibe parches de seguridad. El VPS de despliegue
2514
+ (Debian 13) corre Node 22.22 LTS (Jod), que pasa a ser el floor
2515
+ soportado oficialmente.
2516
+
2517
+ ### Fixed (a11y / contraste)
2518
+ - 32 violaciones WCAG AA por texto `--ig-fundus-lux` hardcoded sobre
2519
+ fondos cardinales adaptativos en Badge (×6), Button outline:hover (×6),
2520
+ Tabs Pills (×6), Pagination active (×1), Stepper dot active+complete
2521
+ (×2) y Chip selected (×2). Ahora todos usan `--ig-text-on-{role}`,
2522
+ que invierte automáticamente entre claro/oscuro según el tema.
2523
+ - **Reset CSS de `<button>`**: ya no fuerza `background: vitreus +
2524
+ color: text-on-vitreus`. Pasa a `transparent + inherit` para que
2525
+ cualquier wrapper (SidebarItem, otros) pueda aplicar sus colores sin
2526
+ riesgo de mezcla incoherente (texto cinis sobre fondo vitreus =
2527
+ ratio 1.06 que el runner cazó tras el fix de Bloque 1).
2528
+ - **Runner storybook+axe local**: deja de ser ciego al modo dark.
2529
+ Antes, `useEffect` del theme decorator aplicaba `data-theme="dark"`
2530
+ POST-paint y chromium headless evaluaba en light (vía
2531
+ `prefers-color-scheme: light`), donde no había violaciones. Fix
2532
+ con `.storybook/preview-head.html` que inyecta `data-theme="dark"`
2533
+ antes del primer paint del iframe.
2534
+ - **Token huérfano**: eliminado `--ig-text-on-cinis` (cinis nunca se
2535
+ usa como background, solo como color de texto-body).
2536
+ - **`--ig-text-muted` en light**: ajustado a `#5e5667` (ratio 5.06 sobre
2537
+ el nuevo `bg-muted`). Antes daba 4.45 (< 4.5 AA).
2538
+
2539
+ ### Added
2540
+ - **`scripts/check-component-contrast.mjs`** como guardrail CI: parsea
2541
+ `igoded-components.css` con postcss y valida (a) WCAG ≥ 4.5 en cada
2542
+ par bg/color resuelto en ambos temas y (b) geometría OKLCH dual
2543
+ (ΔH ≤ 10°, L_lux/nox y suma dentro de banda). Conectado a
2544
+ `npm run test:contrast` y `npm run verify`.
2545
+ - Story `Fundamentos/Paleta` (visualización viva de los 7 cardinales
2546
+ con hex, OKLCH (L,C,H) y ratios contra `fundus`).
2547
+ - `:root { color-scheme: dark light }` + por tema, para que los
2548
+ scrollbars y form-controls del UA sigan el tema activo.
2549
+ - Bloque `@media (forced-colors: active)` que mapea los semánticos
2550
+ críticos a system-color keywords (CanvasText, Canvas, LinkText,
2551
+ GrayText) para Windows High Contrast Mode.
2552
+ - Token `--ig-theme-transition` con override en
2553
+ `@media (prefers-reduced-motion: reduce)`.
2554
+ - Cabecera explícita en `igoded-tokens.css` documentando los 3 tiers
2555
+ de tokens (primitivos / semánticos / escalas universales) y los
2556
+ guardrails de CI.
2557
+ - `SKILL.md` con las reglas inviolables del DS (geometría dual, uso de
2558
+ `text-on-*`, prohibición de `fundus-*` directo, separación
2559
+ info/secondary, escala neutral universal).
2560
+
2561
+ ## [1.0.0-beta.4] — 2026-05-02
2562
+
2563
+ Última pasada de pulido pre-`1.0.0`. Cierra los hallazgos de la auditoría
2564
+ externa sobre beta.3 + endurecimiento adicional (size budgets, dev-warnings,
2565
+ config split lib vs dev/storybook, registry de Tabs robusto frente a
2566
+ defaultValue inválido).
2567
+
2568
+ ### Fixed (a11y / regression / robustness)
2569
+ - **Rating**: `value` o `defaultValue` fuera de `[0, max]` se clampa
2570
+ silenciosamente. `value=10 max=5` antes dejaba todos los radios con
2571
+ `tabIndex=-1` (tablist sin tab stop accesible). Ahora va al tope. `max`
2572
+ no entero se redondea hacia abajo, mínimo 1.
2573
+ - **Tabs `defaultValue` inválido**: si el value pasado no matchea ningún
2574
+ `<Tab>` montado, fallback al primero registrado tras un useEffect
2575
+ post-mount (no durante el registro tab-a-tab — eso producía un falso
2576
+ positivo cuando los Tabs se montaban en orden alfabético). En modo
2577
+ controlled NO se auto-corrige; solo console.warn dev-only.
2578
+ - **Tab.tsx pre-register con `useLayoutEffect`** (en cliente; SSR cae a
2579
+ noop): el primer paint visible ya tiene tab activo, sin flicker.
2580
+ - **Slider**: `defaultValue` array (legalmente permitido por
2581
+ `InputHTMLAttributes` aunque `<input type="range">` no lo soporta) ya no
2582
+ se reenvía al DOM como `[object Array]`. Filtramos a number/string
2583
+ válidos.
2584
+ - **Modal**: el effect de sincronización `open` ahora resetea
2585
+ `closingFromSyncRef` en su cleanup. No hay leak real (state muere con
2586
+ unmount), pero queda explícitamente correcto frente a re-mounts en
2587
+ desmontaje rápido (navegación SPA durante animación de cierre).
2588
+ - **Reset.css `<button>` color**: usaba `--ig-fundus-lux` fijo que daba
2589
+ contraste bajo en dark (texto casi blanco sobre teal claro). Ahora usa
2590
+ `--ig-text-on-vitreus` adaptativo. Se aplicó en beta.3 pero faltaba
2591
+ validación AA en dark — verificada ahora con axe.
2592
+
2593
+ ### Added
2594
+ - **`vite.lib.config.ts` separado** del `vite.config.ts` general. El
2595
+ build de librería (`npm run build`) usa explícitamente esta config con
2596
+ `dts` plugin + `copyDesignSystemStyles`. Más robusto que el guard env
2597
+ `STORYBOOK !== "true"` de beta.3 (Storybook ya nunca puede contaminar
2598
+ el build de librería, independientemente de qué env vars setee).
2599
+ - **`size-limit` en CI** con budgets por archivo:
2600
+ - `dist/index.{js,cjs}` → ≤ 15 KB gzipped (real: ~10.7 KB ESM)
2601
+ - `dist/styles/igoded-design.css` → ≤ 5 KB (real: 1 KB, es solo @import)
2602
+ - `dist/styles/igoded-tokens.css` → ≤ 25 KB (real: 22 KB)
2603
+ - `dist/styles/igoded-components.css` → ≤ 75 KB (real: 67 KB)
2604
+ - `dist/styles/igoded-base.css` → ≤ 2 KB (real: 1.4 KB)
2605
+ - `dist/styles/igoded-reset.css` → ≤ 2 KB (real: 1.6 KB)
2606
+ - `dist/styles/igoded-fonts.css` → ≤ 1 KB (real: 0.9 KB)
2607
+ Script `npm run verify:size`. Forma parte de `npm run verify`.
2608
+ - **Card**: dev-only `console.warn` si `interactive` + `onClick` está
2609
+ presente pero sin `role="button"`. Avisa una vez por instancia. La
2610
+ card sin role no activa por teclado, este caso suele ser un descuido.
2611
+ - **`Fundamentos/Variantes` (nueva MDX)**: visualización del mapeo
2612
+ `variant` → cardinal con los 7 colores nuevos (incluye `cyaneus`).
2613
+ Documentación de override por consumer + ratios WCAG AA.
2614
+ - **JSDoc `@example` en hooks públicos**: `useTheme`, `useToast`,
2615
+ `useTabs`, `useAccordion`, `useAccordionItem`, `useOptionsMenu`,
2616
+ `useSidebar`. Mejora autocompletado en LSP del consumer.
2617
+ - **Test SSR-hydration ThemeSwitch**: `localStorage.theme="light"` gana
2618
+ al default `dark` y NO se sobreescribe en el primer effect.
2619
+ - **`src/env.d.ts`**: declaración tipada de `import.meta.env.DEV/PROD`
2620
+ para que los warnings dev-only de Card/Tabs typechequen sin pisar el
2621
+ `vite-env.d.ts` (que está excluido del build para no exportar
2622
+ declaraciones de módulos `*.css`/`*.svg` al consumer).
2623
+
2624
+ ### Changed (BREAKING — momento ideal pre-1.0)
2625
+ - **`ariaLabel` prop → `aria-label` HTML estándar** en 9 componentes:
2626
+ `Pagination`, `Spinner`, `Stepper`, `Rating`, `Sidebar`, `SidebarNav`,
2627
+ `NavbarNav`, `Tabs.TabList`, `Breadcrumb`, `Progress`, `Avatar`,
2628
+ `Timeline`. Cada uno extrae `aria-label` del rest con fallback ES por
2629
+ defecto. Coherente con el resto del DS (ThemeSwitch ya usaba esto
2630
+ desde beta.3) y con la convención web.
2631
+ - **Migration**: rename en tu JSX `ariaLabel={...}` → `aria-label={...}`.
2632
+ - **`docs.defaultName: "Documentación"` → `"Docs"`** (ya estaba en beta.3,
2633
+ reconfirmado: URLs ASCII, sin %C3%B3n).
2634
+ - **README CSS imports actualizado**: 7 entradas → 8 entradas (incluye
2635
+ `fonts.css`); la versión del estado pasa de beta.2 a beta.4.
2636
+ - **README script anti-flash de tema**: fallback `|| "light"` →
2637
+ `prefers-color-scheme: dark` con fallback `dark` (alineado con el
2638
+ branding dark-first del DS). Try/catch para entornos sin localStorage.
2639
+ - **`tokens.css` comentarios**: "FUNDUS + 6 CARDINALES" → "FUNDUS + 7
2640
+ CARDINALES" + tabla de los 7 incluyendo `CYANEUS`. Mención obsoleta a
2641
+ `@font-face` en tokens.css eliminada (vive en fonts.css desde beta.3).
2642
+ - **`package.json#keywords`**: quitado `"headless"` (engañoso, el DS es
2643
+ CSS-first opinionated). Añadidos `"design-tokens"`, `"css-first"`,
2644
+ `"themeable"`, `"dark-mode"`.
2645
+ - **`vite.config.ts` simplificado**: ya no contiene `build.lib` ni `dts`
2646
+ (eso vive en `vite.lib.config.ts`). Se queda con plugins comunes
2647
+ (react, optimizeDeps) compartidos por playground dev y Storybook.
2648
+
2649
+ ### Migration desde beta.3
2650
+ 1. **Si tu JSX usa la prop `ariaLabel`** en alguno de estos componentes,
2651
+ renombra a `aria-label`:
2652
+ ```diff
2653
+ - <Stepper ariaLabel="Checkout">
2654
+ + <Stepper aria-label="Checkout">
2655
+ - <Pagination ariaLabel="Paginación principal">
2656
+ + <Pagination aria-label="Paginación principal">
2657
+ - <TabList ariaLabel="Cuenta">
2658
+ + <TabList aria-label="Cuenta">
2659
+ ```
2660
+ La búsqueda+reemplazo es trivial: tu IDE te marcará los errores TS
2661
+ (la prop ya no existe en los tipos).
2662
+ 2. **Si tu app inyecta el script anti-flash de tema** del README,
2663
+ actualízalo al nuevo (system-aware + dark fallback) — el viejo
2664
+ pisaba la preferencia del usuario en sistemas dark-first.
2665
+
2666
+ ### Coverage
2667
+ - Statements 92.29% · Branches 86.44% · Functions 95.17% · Lines 95.11%
2668
+ - 339 unit tests + 179 storybook (axe-a11y) = 518 tests verdes.
2669
+
2670
+ ## [1.0.0-beta.3] — 2026-05-02
2671
+
2672
+ Pasada agresiva pre-`1.0.0`: a11y real, SSR-safe, naming/types fix y un
2673
+ nuevo color cardinal para diferenciar `info` de `secondary`.
2674
+
2675
+ ### Fixed (a11y / regression)
2676
+ - **Input/NativeSelect/Textarea**: `aria-describedby` que el consumer pasara vía
2677
+ `{...rest}` se sobreescribía a `undefined` cuando no se pasaba la prop
2678
+ `describedBy`. La propia story `FormularioCompleto` estaba rota.
2679
+ Solucionado con un nuevo helper `mergeDescribedBy(native, prop)` que
2680
+ concatena ambos. +6 tests de regresión.
2681
+ - **Rating**: añadido **roving tabindex + keyboard nav completo** (←/→/↑/↓,
2682
+ Home, End, Space, Enter) para cumplir el patrón WAI-ARIA APG de
2683
+ radiogroup. Antes solo respondía a click. +9 tests de regresión.
2684
+ - **ToastProvider SSR**: ya no produce hydration mismatch. Antes el
2685
+ servidor renderizaba inline y el primer paint cliente ya pintaba portal.
2686
+ Ahora arranca inline (idéntico al server) y conmuta al portal en el
2687
+ primer `useEffect` post-mount.
2688
+ - **OptionsMenu a11y**: el selector de navegación excluye también
2689
+ `[aria-disabled="true"]` (anchors no tienen `disabled` HTML); items
2690
+ aria-disabled bloquean activación por click y por Enter/Space; **button**
2691
+ menuitem ahora también tiene `tabIndex={-1}` (antes anchor sí lo tenía
2692
+ y button no, inconsistente).
2693
+ - **Tabs sin `defaultValue`**: el tablist quedaba sin tab stop si el
2694
+ consumer no pasaba `value`/`defaultValue` (todos los tabs `tabIndex=-1`).
2695
+ Ahora un registry interno selecciona el primer `Tab` montado de forma
2696
+ automática.
2697
+ - **Modal `onClose` doble disparo**: cuando el consumer hacía
2698
+ `setOpen(false)`, el effect llamaba `dialog.close()` que disparaba el
2699
+ evento `close` nativo y volvía a invocar `onClose`. Añadido flag
2700
+ `closingFromSyncRef` que distingue cierre user-driven de cierre por
2701
+ sincronización con la prop.
2702
+ - **ThemeSwitch `aria-label`**: ahora se puede sobrescribir vía rest
2703
+ (i18n). Antes el hardcoded ganaba al rest por orden de spread.
2704
+ - **Slider `defaultValue` string/array**: `defaultValue="60"` (string)
2705
+ dejaba el state interno en `0` mientras el `<input>` mostraba 60.
2706
+ Ahora normaliza a número finito; arrays se ignoran (no soportados por
2707
+ `<input type="range">`).
2708
+
2709
+ ### Added
2710
+ - **`igoded-fonts.css` (nuevo, opt-in)** — `@import` de Google Fonts
2711
+ (Electrolize/Saira/JetBrains Mono). **Antes vivía dentro de
2712
+ `tokens.css`**, lo que metía un request remoto en el bundle de
2713
+ cualquier consumer. Ahora `tokens.css` declara solo los `--ig-font-*`
2714
+ con fallback `system-ui`/`monospace`. Storybook lo importa
2715
+ explícitamente en `preview.tsx`. Expuesto como
2716
+ `reactigoded/styles/fonts.css`.
2717
+ - **Color cardinal `cyaneus`** (cian-azul) — nuevo `--ig-cyaneus-{lux,nox}`
2718
+ + `--ig-text-on-cyaneus-{lux,nox}`. `--ig-info` ahora apunta a `cyaneus`
2719
+ en vez de a `axis` (que es violet/secondary). Antes `secondary` e `info`
2720
+ eran visualmente idénticos; ahora son colores distintos. Verificado
2721
+ WCAG AA en light y dark.
2722
+ - **`mergeDescribedBy` helper** en `src/utils/`. Tests propios + integrados
2723
+ en Input/NativeSelect/Textarea.
2724
+ - **Tabs `register` API** en `TabsContext` para auto-selección del primer
2725
+ Tab.
2726
+ - **Modal `closingFromSyncRef`** flag interno.
2727
+ - **`test:unit:ci` script** (vitest con `--isolate --pool=forks`) para CI
2728
+ estricto. El `test:unit` por defecto sigue con `isolate=false` por el
2729
+ workaround WSL.
2730
+ - **4 stories interactivas con `play`**: `Input/TypeInteraction`,
2731
+ `NativeSelect/ChangeInteraction`, `Slider/KeyboardInteraction`,
2732
+ `Stepper/Interactivo` (ahora con play que verifica `aria-current`).
2733
+
2734
+ ### Changed
2735
+ - **`vite.config.ts`** ahora sólo activa el modo lib build cuando
2736
+ `command === "build" && mode === "production" && STORYBOOK !== "true"`.
2737
+ Antes `mode === "production"` solo, lo que disparaba `dts` plugin y
2738
+ `copyDesignSystemStyles` también durante `storybook build` (cada deploy
2739
+ de igoded.es).
2740
+ - **`docs.defaultName: "Documentación"` → `"Docs"`**: el unicode `ó`
2741
+ generaba URLs `componentes-x--documentaci%C3%B3n` feas y problemáticas
2742
+ para search/SEO. Las páginas MDX de Foundations siguen en español.
2743
+ - **Tema dark-first uniforme**: `useTheme` (ya estaba dark), `ThemeSwitch`
2744
+ (era light) y Storybook `withThemeByDataAttribute` (era light) ahora
2745
+ todos default `"dark"`. Coherente con el branding.
2746
+ - **Card type assertion**: `onClick?.(event as unknown as MouseEvent)`
2747
+ desde el handler de Enter/Space sustituido por `event.currentTarget.click()`,
2748
+ que dispara un MouseEvent auténtico.
2749
+ - **Alert** `style={{flex:1, minWidth:0}}` inline → clase
2750
+ `.ig-alert-content` publicada en `components.css`.
2751
+ - **Card story `Interactiva`** quita `ig-story-clickable` (la clase
2752
+ `.ig-card-interactive` ya implica `cursor: pointer`, y la helper de
2753
+ storybook.css confundía a consumers que copiaran el código).
2754
+ - **`eslint.config.js`** comentario `postinstall` → `prepare` (coincide
2755
+ con el script real de `package.json`).
2756
+ - **README** métricas obsoletas (~285/~175) → texto sin números fijos.
2757
+
2758
+ ### Migration
2759
+ - Si confiabas en el `@import` de Google Fonts dentro de `design.css` /
2760
+ `tokens.css`, ahora también necesitas:
2761
+ ```ts
2762
+ import "reactigoded/styles/fonts.css";
2763
+ ```
2764
+ …o self-host con `next/font`/`@fontsource/*` y override de los
2765
+ `--ig-font-*`.
2766
+ - **Cambio visual: `info` ahora es CYAN, no violet.** Si tu app usa
2767
+ `<Toast variant="info">`, `<Alert variant="info">`, `<Badge variant="info">`,
2768
+ `<Card variant="info">`, `.ig-btn-info`, `.ig-bg-info`, etc., verás un
2769
+ color cian-azul (`--ig-cyaneus`) en vez del violet (`--ig-axis`) de antes.
2770
+ La razón: en beta.0–beta.2, `secondary` e `info` apuntaban ambos a `axis`
2771
+ y eran visualmente idénticos. Ahora `info` tiene su propio cardinal
2772
+ `cyaneus` (WCAG AA verificado en light + dark). Si necesitas el aspecto
2773
+ anterior, sobreescribe `--ig-info: var(--ig-axis)` en tu `:root`.
2774
+ - Si usabas `<Toast variant="default">` en el primitivo (no lo deberías
2775
+ desde beta.1, ya estaba renombrado a `neutral`), nada cambia aquí.
2776
+ - Si `defaultValue` de Slider venía como string, antes el value visible no
2777
+ cuadraba con el internal — ahora cuadra. Si dependías del bug, comprueba.
2778
+
2779
+ ## [1.0.0-beta.2] — 2026-05-02
2780
+
2781
+ Refactor de la arquitectura CSS y pulido completo del catálogo Storybook.
2782
+
2783
+ ### Added
2784
+ - **`igoded-base.css` (nuevo, ~3 KB)** — globales mínimos del DS:
2785
+ `box-sizing` universal, `html` (scroll-behavior, scrollbar-gutter,
2786
+ accent-color, caret-color), scrollbar tematizada (`::-webkit-scrollbar*`),
2787
+ `::selection`, `@media (prefers-reduced-motion: reduce)`,
2788
+ `(prefers-contrast: more)`, `(forced-colors: active)`. Standalone, depende
2789
+ solo de tokens. Expuesto como `reactigoded/styles/base.css`.
2790
+ - **`igoded-components.css` (nuevo, ~270 KB)** — utilities + componentes
2791
+ (clases `.ig-*`). Sin selectores globales. Depende de tokens + base.
2792
+ Expuesto como `reactigoded/styles/components.css`.
2793
+ - `.storybook/manager.ts` no era nuevo; sí lo es **`managerHead`** en
2794
+ `main.ts`: meta description, og:title/description/url/type, theme-color,
2795
+ canonical, twitter:card, y un MutationObserver que reescribe
2796
+ `<title>` `"… ⋅ Storybook"` → `"… · Igoded Design System"` en cada
2797
+ navegación. Sin polling.
2798
+ - `docs.defaultName: "Documentación"` (era `"Docs"`).
2799
+ - Helpers en `.storybook/storybook.css`: `.ig-story-card-{sm,md,lg}`,
2800
+ `.ig-story-clickable`, `.ig-story-min-h-{sm,md,lg}`,
2801
+ `.ig-foundation-{grid,swatch,token-row,contrast,typo}` para los MDX.
2802
+ - README: tabla detallada de globales que aplica `base.css`, escenarios
2803
+ de import por caso de uso (5 filas).
2804
+
2805
+ ### Changed
2806
+ - **`igoded-design.css` ahora es solo un meta-importer** que hace
2807
+ `@import` de `tokens.css` + `base.css` + `components.css`. Backward
2808
+ compat: un consumer que importa `design.css` recibe lo mismo que antes.
2809
+ - **`igoded-tokens.css` es ahora 100% variables CSS**: el
2810
+ `*, *::before, *::after { box-sizing: border-box }` se movió a
2811
+ `base.css`. Tokens.css ya no tiene ningún selector global.
2812
+ - **Stories renombradas (ES uniforme)**:
2813
+ - `Switch.Toggle` → `InteracciónToggle` (no era prop literal).
2814
+ - `Accordion.Controlled` / `Sidebar.Controlled` /
2815
+ `ThemeSwitch.Controlled` → `Controlado` (consistencia con
2816
+ `Switch.Controlado` y `Rating.Controlado`).
2817
+ - **Card stories**: 4 inline `style={{ maxWidth, cursor }}` → clases
2818
+ helper `.ig-story-card-md`, `.ig-story-card-lg`, `.ig-story-clickable`.
2819
+ - **MDX foundations** (`Spacing.mdx`, `Contrast.mdx`, `DesignTokens.mdx`)
2820
+ migrados de inline styles a clases `.ig-foundation-*`. Markup más
2821
+ limpio y consistente entre páginas.
2822
+ - **`igoded-design.css` cabecera reescrita**: las ~190 líneas de índice
2823
+ legacy con números de línea desactualizados ("RESET BASE OPCIONAL...215"
2824
+ apuntaban a líneas que ya no existen) sustituidas por bloque conciso
2825
+ que documenta la arquitectura modular real.
2826
+
2827
+ ### Migración (sin breaking)
2828
+ - `import "reactigoded/styles/design.css"` sigue funcionando igual que en
2829
+ beta.1. Internamente ahora `@import`a 3 archivos en vez de 2.
2830
+ - Si importabas `tokens.css` esperando el `box-sizing` global, ahora
2831
+ importa `base.css` también (`tokens.css` + `base.css` reproduce el
2832
+ comportamiento anterior).
2833
+
2834
+ ## [1.0.0-beta.1] — 2026-05-02
2835
+
2836
+ Pasada de pulido orientada a percepción del catálogo Storybook + saneamiento
2837
+ de la API pública antes del `1.0.0`.
2838
+
2839
+ ### Added
2840
+ - `igoded-tokens.css` (nuevo, ~98 KB) — solo variables `--ig-*`, keyframes y
2841
+ `@font-face`, sin clases de componentes. Útil para consumers que construyen
2842
+ su propia capa de componentes sobre los tokens del DS.
2843
+ Expuesto como `reactigoded/styles/tokens.css`.
2844
+ - `igoded-reset.css` (nuevo, ~5 KB) — estilos por defecto para HTML nativo
2845
+ (h1-h6, p, a, button, input, table…). **Opt-in**, no se importa
2846
+ automáticamente con `design.css`. Expuesto como
2847
+ `reactigoded/styles/reset.css`.
2848
+ - `Button.appearance="link"` — quinta apariencia (aplica `ig-btn-link`,
2849
+ ignora variant).
2850
+ - `Card.appearance="outline"|"filled"` — sustituye al flag `filled` (ver
2851
+ Changed).
2852
+ - Card activación Enter/Space automática cuando `interactive` +
2853
+ `role="button"` + `onClick`. Encadenable con `onKeyDown` del consumer
2854
+ (preventDefault cancela). +6 tests.
2855
+ - `.storybook/manager.ts` con branding Igoded (createTheme dark,
2856
+ Vitreus/Axis colors, brandTitle "Igoded Design System", fonts Saira +
2857
+ JetBrains Mono).
2858
+ - `.storybook/storybook.css` con clases helper `ig-story-{stack,row,grid,
2859
+ frame,form,shell,label}` para layouts de stories. NO se publica al paquete.
2860
+ - Foundations docs MDX: `Spacing.mdx` (visualización de los 25 tokens
2861
+ `--ig-space-*`, equivalencias Tailwind, guidance) y `Contrast.mdx`
2862
+ (9 pares texto+fondo verificados WCAG AA + garantías a11y de CI).
2863
+
2864
+ ### Changed
2865
+ - **CSS reset extraído** de `igoded-design.css` a `igoded-reset.css` opt-in.
2866
+ En `design.css` queda solo `*, *::before, *::after { box-sizing: border-box }`
2867
+ (necesario para el sizing de los componentes). Eliminado el comentario
2868
+ legacy "ESTÁ COMENTADO A PROPÓSITO" que mentía sobre el estado del reset.
2869
+ - **CSS modular split**: `igoded-design.css` ahora `@import`a
2870
+ `igoded-tokens.css` internamente. Self-containment preservado (consumer
2871
+ que importa solo `design.css` recibe lo mismo que antes).
2872
+ - **Storybook preview**: eliminado wrapper global con padding/100vh que
2873
+ rompía `layout: "fullscreen"` en Navbar/Sidebar. Eliminado import de
2874
+ `state.css` (7.1 MB innecesarios en HMR). Backgrounds reactivados con
2875
+ 4 valores tokenizados (base/surface/muted/sunken).
2876
+ - **Stories en español uniforme**: 28 archivos renombrados
2877
+ `Default→PorDefecto`, `Variants→Variantes`, `Sizes→Tamaños`. Otros nombres
2878
+ ya en ES (`ConImagen`, `Compuesta`, `Interactiva`, etc.) o nombres
2879
+ técnicos propios (`Pills`, `Vertical`, `KeepMounted`, `BackdropBlur`)
2880
+ intactos.
2881
+ - **External assets fuera del catálogo**: pravatar/placehold.co reemplazados
2882
+ por SVG data URIs inline; 11 emojis de Sidebar reemplazados por SVG inline
2883
+ feather-style (consistencia visual entre SO/navegadores).
2884
+ - **Storybook propFilter** excluye HTML attributes heredados
2885
+ (HTMLAttributes, AriaAttributes, DOMAttributes,
2886
+ {Button,Input,Textarea,NativeSelect,Anchor}HTMLAttributes) — Controls panel
2887
+ muestra solo props del propio package.
2888
+ - **Glass Navbar story**: gradiente Tailwind genérico
2889
+ (#4f46e5/#ec4899/#f59e0b) → gradientes con tokens
2890
+ `--ig-vitreus-alpha-*` + `--ig-axis-alpha-*` sobre `--ig-bg-base`.
2891
+ - **~40 inline styles repetidos** en stories migrados a clases
2892
+ `.ig-story-*`. Los inline styles component-internal (sizing de Card,
2893
+ padding de inputs internos, márgenes decorativos) se conservan.
2894
+ - Comentario WCAG en `igoded-design.css:437` corregido AAA → AA (coincide
2895
+ con keyword `wcag-aa` y los memos reales de tokens).
2896
+
2897
+ ### Breaking
2898
+ - `Button.variant="outline"|"ghost"|"link"` → migrar a
2899
+ `Button.appearance="outline"|"ghost"|"link"`. `ButtonVariant` se reduce a
2900
+ los 6 colores semánticos. Razón: dos ejes ortogonales
2901
+ (color × estilo visual) en vez de mezclar 9 valores en uno.
2902
+ - `Card.filled` (boolean flag) → migrar a `Card.appearance="filled"`. El
2903
+ comportamiento default (`appearance="outline"`) es idéntico.
2904
+ - `Toast.variant="default"` → renombrado a `"neutral"` por consistencia con
2905
+ Alert. Comportamiento idéntico (no añade clase de variant).
2906
+ - Estilos para HTML nativo (`<h1>`, `<p>`, `<a>`, `<table>`…) ya **no se
2907
+ cargan** desde `reactigoded/styles/design.css`. Si los necesitas, importa
2908
+ también `reactigoded/styles/reset.css`.
2909
+
2910
+ ## [1.0.0-beta.0] — 2026-05-01
2911
+
2912
+ Cambios desde la migración inicial JSX→TSX. Agrupa el trabajo de las 6
2913
+ auditorías profundas previas a `1.0.0`.
2914
+
2915
+ ### Added
2916
+ - 32 componentes React 19 + TypeScript estricto, con tests unit
2917
+ (happy-dom) + Storybook tests (Chromium real + axe-a11y) + 16 stories
2918
+ con `play` (interaction tests).
2919
+ - Compound components: `Accordion`, `Card`, `OptionsMenu`, `Input`, `Modal`,
2920
+ `Navbar`, `Sidebar`, `Stepper`, `Table`, `Tabs`, `Timeline`, `Toast`.
2921
+ - Hooks públicos: `useTheme`, `useToast`, `useAccordion`,
2922
+ `useAccordionItem`, `useOptionsMenu`, `useSidebar`, `useTabs`.
2923
+ - API controlled+uncontrolled en `Accordion`, `Alert`, `OptionsMenu`,
2924
+ `Sidebar`, `Slider`, `Switch`, `Tabs`, `ThemeSwitch`, `Rating`.
2925
+ - `Modal.loading` (aplica `ig-dialog-loading` + `aria-busy`).
2926
+ - `Badge.dot` (modo punto sin texto, con `role="img"` automático).
2927
+ - `Button.appearance="solid"|"outline"|"ghost"` combinable con variant
2928
+ color (12 clases CSS antes huérfanas ahora expuestas).
2929
+ - `Input/NativeSelect/Textarea.describedBy` (string|string[]) — auto-`aria-describedby`.
2930
+ - `Slider.onValueChange(v:number)` — alternativa al `onChange` nativo.
2931
+ - `Slider aria-valuetext` automático cuando hay `formatValue`.
2932
+ - `Modal.aria-labelledby` automático vía `ModalContext`+`ModalHeader`.
2933
+ - `Switch role="switch"` + `aria-checked` (refactor desde checkbox plano).
2934
+ - `Tooltip.aria-describedby` concatena con el del child existente.
2935
+ - `BreadcrumbItem` discriminated union por `current`.
2936
+ - `Step.aria-current` en el span del círculo (no en el div wrapper).
2937
+ - `Textarea.auto` con `field-sizing: content` (Chrome/Edge 123+,
2938
+ Safari 17.4+; Firefox cae al `rows` por defecto).
2939
+ - `browserslist` declarado en package.json.
2940
+ - Stories `play` (interaction) en 16 componentes interactivos.
2941
+ - Tests "fuera de provider" para los 5 hooks de context.
2942
+ - Tests de cleanup en unmount: OptionsMenu limpia listeners globales,
2943
+ ToastProvider limpia timers.
2944
+ - Tests de transición controlled↔uncontrolled (warning de React).
2945
+ - `scripts/strip-orphan-css.mjs` — limpia con postcss utilities
2946
+ pseudo-class del `state-css` que apuntan a tokens eliminados.
2947
+
2948
+ ### Changed
2949
+ - `Toast.closable` → `Toast.dismissible` (alineado con `Alert`).
2950
+ - `Rating.onChange` → `Rating.onValueChange` (consistencia con `Tabs`,
2951
+ `Accordion`, `Slider`).
2952
+ - `Spinner.label` / `Progress.label` / `Rating.label` → `ariaLabel`
2953
+ (estandarización a camelCase).
2954
+ - `Stepper.active` ahora opcional + `defaultActive`.
2955
+ - `Alert.open` ahora opcional + `defaultOpen` + `onOpenChange`.
2956
+ - `dist/styles/index.css` ahora usa `@import` (no concatenación física —
2957
+ ahorra 7.85 MB en el unpacked).
2958
+ - Publish sin sourcemaps de JS (`.js.map`/`.cjs.map`); `.d.ts.map` sí se
2959
+ publica.
2960
+ - `tsconfig.json#exactOptionalPropertyTypes: true`.
2961
+ - Sección CSS "78b. DIALOG" renombrada a "78. DIALOG / MODAL" (la 78
2962
+ legacy se eliminó).
2963
+ - `Tabs`, `Sidebar`, `OptionsMenu`, `Modal`, `Accordion`, `ToastProvider`
2964
+ con `Provider value` memoizado (`useMemo`+`useCallback`).
2965
+ - `OptionsMenu.setOpen` ahora `useCallback` con deps correctas (era stale
2966
+ closure cuando `onOpenChange` cambiaba entre renders).
2967
+ - `Button.disabled || loading` (antes `??`, permitía que `disabled={false}`
2968
+ anulara `loading={true}`).
2969
+ - `{...rest}` ahora se aplica ANTES de role/aria/tabIndex/disabled/hidden
2970
+ en todos los componentes, evitando que un consumer pueda sobreescribir
2971
+ atributos críticos accidentalmente.
2972
+
2973
+ ### Removed
2974
+ - `.ig-modal-*` (variante legacy con `<div>` — el componente React usa
2975
+ `<dialog>` nativo). 11 clases CSS + 7 tokens `--ig-modal-*` + 164
2976
+ utilities pseudo-class autogeneradas.
2977
+ - `.ig-input-{brand,secondary,info,danger,warning}` (variantes color
2978
+ nunca expuestas en TS).
2979
+ - `dist/test/setup.d.ts` del publish (Vitest setup, no público).
2980
+ - `useState` muerto en `Stepper` (era presentational, no necesitaba
2981
+ estado interno).
2982
+ - Cast `alt={alt ?? ""}` en `Avatar` (`alt` ya required en el tipo
2983
+ `AvatarImage`).
2984
+
2985
+ ### Fixed
2986
+ - 4 missings de barrel export: `useOptionsMenu`+`OptionsMenuContextValue`,
2987
+ `useTabs`+`TabsContextValue`, `SidebarContextValue`,
2988
+ `ToastContextValue`, `ModalContextValue`.
2989
+ - Colisión de `Theme` (definido en dos sitios) — ahora única fuente en
2990
+ `hooks/useTheme.ts`.
2991
+ - `Badge.outline-{variant}` y `Badge.pill` referenciaban CSS inexistente.
2992
+ - `Spinner.size="xs"` referenciaba CSS inexistente.
2993
+ - `TabPanel.tabIndex={isActive ? 0 : -1}` (antes siempre 0, panel oculto
2994
+ era focusable).
2995
+ - Tooltip ya no sobreescribe `aria-describedby` del child existente.
2996
+
2997
+ ### Métricas finales
2998
+ - Pack tarball: 860 KB.
2999
+ - 32 unit test files / 285 tests + 32 storybook test files / 175 tests.
3000
+ - 0 TSX→CSS missings, 0 hooks/contexts sin barrel, 0 CSS huérfanas reales.
3001
+ - Bundle ESM: 50 KB / gzip 11 KB para 74 componentes.