|
|
# Plan de corrección — herencia / escala (Eidos) · **v2, corregido por diseño + referencias**
|
|
|
|
|
|
> **Compañero de** [`inherit_audit.md`](inherit_audit.md). · **Fecha:** 2026-06-27
|
|
|
> · **Excluidos:** `words/**`, `palabras/**`, `chronos/**` (WIP).
|
|
|
>
|
|
|
> **Esta v2 reemplaza la doctrina de radio de la v1.** La v1 acoplaba el radio al
|
|
|
> `size` del control (sesgo de statu quo, anclado en `toggle`). La investigación de
|
|
|
> 10 frameworks de referencia (Radix, Material 3, Apple HIG, Ant, MUI, Tailwind,
|
|
|
> Carbon, Fluent, Chakra, Mantine — verificada adversarialmente) demostró que **eso
|
|
|
> es un outlier**: nadie escala el radio sin tope con el size. La doctrina correcta,
|
|
|
> decidida por el usuario, es el **modelo Radix** (radio = eje propio, desacoplado
|
|
|
> del size) + **herencia concéntrica del padre** (modelo Apple).
|
|
|
>
|
|
|
> Cada decisión va con cita de diseño (`THEMING.md`/RFC) **o** de referencia (URL).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 0. Doctrina raíz (corregida)
|
|
|
|
|
|
> **Un componente deriva su GEOMETRÍA + TIPO + ICONO del primitivo de size
|
|
|
> `--size-{k}-*` (que compone densidad×scaling); si no fija su size, HEREDA el del
|
|
|
> padre (publishers deliberados, cadena Ant). El RADIO es un eje propio
|
|
|
> —default por arquetipo × factor de tema `radius` × override `rounded`—
|
|
|
> DESACOPLADO del size, y se HEREDA del padre de forma CONCÉNTRICA cuando anida
|
|
|
> (`inner = max(0, outer − gap)`, modelo Apple).**
|
|
|
|
|
|
**Decisiones cerradas:**
|
|
|
- ✅ `--size-md-font-size = 16px` (1:1; el texto de control sigue la escala tipográfica — campo "font-in-size" de Chakra/Ant/Radix). El compact-md 14px queda muerto.
|
|
|
- ✅ **Radio = modelo Radix** (Opción A): eje propio, desacoplado del `size`.
|
|
|
|
|
|
**Reencuadre clave:** tu **regla 5** ("las esquinas heredan del padre; los radios concéntricos") **no** pide "el radio escala con el size propio" (eso era R4, ya cubierto por el size recipe). Pide **herencia del padre + concentricidad** — que es exactamente el modelo de Apple (`ContainerRelativeShape`/`ConcentricRectangle`) y el helper `[data-shape-nest]` que eidos **ya tiene**. El falso problema "cap-at-md vs full-scale" desaparece: el radio no es función del size.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. Qué fijan el diseño + las referencias (resumen citado)
|
|
|
|
|
|
| Eje | Referencia (cita) | eidos hoy | Acción |
|
|
|
|---|---|---|---|
|
|
|
| **scaling** (R1) | Radix `scaling` 90-110% escala todo incl. tipo ([radix/spacing](https://www.radix-ui.com/themes/docs/theme/spacing)). | `--scaling` idéntico. | ✅ validado; solo higiene (literales). |
|
|
|
| **densidad** (R2) | M3/Carbon/Fluent: altura/spacing cambian, **texto estable** ([m3/density](https://m3.material.io/foundations/layout/understanding-layout/density)). | density compact/comfortable/spacious, texto estable. | ✅ validado. **eidos por delante**: tener densidad **Y** scaling es la unión que ningún referente ofrece. |
|
|
|
| **size → internos** (R4) | Chakra/Ant/Radix meten font en el size recipe ([chakra/sizes](https://chakra-ui.com/docs/theming/sizes)). | recipes por-size (font incl.). | ✅ 1:1 (md=16) confirma el campo. |
|
|
|
| **heredar size** (R3) | Ant `ConfigProvider.componentSize` global; cadena `explícito > grupo > contexto > md` ([ant/config](https://ant.design/components/config-provider/)). Publishers = contenedores **concretos** (Form, input-group, group). | hardcode `md`; solo 3 contextos ad-hoc. | **WS-A**. |
|
|
|
| **radio** (R5.a/b) | Radix: radio = factor de tema × paso por rol, **independiente del size**; `full`→pill, checkbox capado ([radix/radius](https://www.radix-ui.com/themes/docs/theme/radius)). Ningún referente escala radio sin tope. | 3 filosofías; `STATIC_SIZE` lo escala 7 pasos (**outlier**). | **WS-B** (modelo Radix). |
|
|
|
| **esquinas concéntricas** (R5) | Apple `inner = max(0, outer − gap)`, hijo deriva del padre ([apple/ConcentricRectangle](https://developer.apple.com/documentation/swiftui/concentricrectangle)); canon web idéntico ([Cloud Four](https://cloudfour.com/thinks/the-math-behind-nesting-rounded-corners/)). | `[data-shape-nest]` = ese modelo, **reference-grade**, pero solo 5/80. | **WS-C**. |
|
|
|
|
|
|
---
|
|
|
|
|
|
## 2. Workstreams
|
|
|
|
|
|
### WS-A — Herencia ambiental de `size` (R3) · esfuerzo L · pilot `Form→Field`
|
|
|
|
|
|
**Verificado.** Todo wrapper resuelve con fallback literal `'md'` (`eidos.resolve(size,'md')`, `active-eidos.svelte.ts:540`), sin mirar al padre. Solo 3 contextos ad-hoc (`dialog/context.ts`, `lib/list-surface-context.ts`, `toggle-group/context.ts`). 124 archivos usan el idioma.
|
|
|
|
|
|
**Diseño (modelo Ant, no "toda caja emite"):**
|
|
|
1. Nuevo `src/uix/eidos/lib/ui-size-context.ts` (espejo de `list-surface-context.ts`, getter reactivo) + `clampSize<S>(size, allowed, fallback)` para el subset por componente.
|
|
|
2. **Publishers DELIBERADOS** (no cualquier caja): `Form`, los grupos (button-group/toggle-group ya), overlays (Dialog/Drawer/Popover content como provider de su propio size), y un **provider global tipo `ConfigProvider.componentSize`** (la superficie "size por defecto de la app"). Esto es lo que las referencias hacen; un `<Button>` cualquiera **no** debe rebroadcast su size a descendientes no relacionados.
|
|
|
3. **Cadena de prioridad (Ant, documentar):** `prop explícita > grupo/compound más cercano > provider ambiental > 'md'`. Reader: `eidos.resolve(size ?? group?.size ?? ambient?.getSize(), 'md')` — la ambiental **como fallback de `resolve`**, nunca como `size ?? ctx` (rompería `resolveResponsiveProp` en los 124 sitios).
|
|
|
4. **Cambio de API:** lectores pasan de `size = 'md'` a `size?: Size` (default `undefined`) para detectar "no fijado". *(sign-off)*
|
|
|
5. **Cap-at-md** (`THEMING.md:560`) aplica a su ámbito real —derivación contenedor→parte en **overlays** (un dialog `full` no engorda su Close)— **no** necesariamente a la herencia en-flujo (`<Form size="lg">` probablemente quiere controles `lg`). Política por tipo de publisher. *(sign-off)*
|
|
|
6. **Portal-reset:** los roots de overlay resetean el contexto a su propio size (no filtran el del Form ancestro al panel portalizado). *(recomendado: sí)*
|
|
|
|
|
|
**Pilot:** `Form` SET + `Field`/`Checkbox` READ; verificar en navegador `<Form size="lg">` → controles `lg`. Luego el resto.
|
|
|
|
|
|
### WS-B — Radio = modelo Radix (R5.a/b) · esfuerzo M · pilot `button` + `card`
|
|
|
|
|
|
**El radio es un eje propio, desacoplado del `size`.** Cuatro piezas (Radix verbatim):
|
|
|
1. **Default por arquetipo/rol** — cada componente tiene un radio de reposo por su arquetipo (control, chip, panel…), no por su size. Encaja con el vocabulario de arquetipos ya existente (`ARCHETYPE_COHERENCE_AUDIT`).
|
|
|
2. **× factor de tema `radius`** (`none|small|medium|large|full`) — **verificar si existe**; si no, **añadir** un `--radius-factor` global à la Radix (`--radius-factor` 0/0.75/1/1.5/1.5 sobre la escala `--radius-*`). Es la perilla de tema que falta.
|
|
|
3. **Override `rounded` per-instancia** — `button`/`card` ya lo tienen (`button.css:216-220`, `card.css:133-134`); **generalizarlo** como la jaula abierta (= prop `radius` de Radix/Mantine/Chakra).
|
|
|
4. **`full`/pill = cápsula** (caso especial size-aware, autocapado: ½ altura / 9999px) con **caps por parte** (checkbox nunca totalmente redondo, modelo Radix).
|
|
|
|
|
|
**Migración (DES-acoplar, lo contrario de la v1):**
|
|
|
- **Quitar** el rebinding de radio de las cascadas `[data-size]` — empezando por `toggle` (`toggle.css:64-98` rebindea `--_toggle-radius` por size: **ese era el outlier**, se elimina). El radio pasa a venir del default de arquetipo + factor + `rounded`.
|
|
|
- **Sacar el radio del bundle de size:** `STATIC_SIZE.radius` / `--size-{k}-radius` dejan de ser fuente del radio (eran el outlier). Se repurposan como tabla de default-por-arquetipo o se podan.
|
|
|
- Unifica las 3 filosofías bajo la de Radix; `button`/`card` quedan como la referencia (ya casi lo eran).
|
|
|
|
|
|
**Pilot:** `button` + `card` (los más cercanos a Radix). Verificar esquinas a xs..xl + que `rounded`/`full` siguen ganando. Es **cambio visual** → screenshots por size. *(sign-off del look)*
|
|
|
|
|
|
### WS-C — Esquinas concéntricas heredadas del padre (R5) · esfuerzo M · pilot `dropdown-menu`
|
|
|
|
|
|
**Verificado.** El helper `[data-shape-nest] { border-radius: max(0px, calc(var(--shape-outer-radius) − var(--shape-nest-gap))) }` (`render-css.ts:1153-1159`) es **el modelo de Apple, reference-grade**. Adoptan 5; debería ser ~20-40 (los que de verdad anidan una superficie redondeada en un padre redondeado con padding).
|
|
|
|
|
|
**Diseño:** dos partes — el padre SETea `--shape-outer-radius` (+ `--shape-nest-gap` = su padding); el hijo añade `data-shape-nest` y borra su `border-radius` propio. Es **la herencia del padre que pide tu regla 5**, y compone con WS-B (un parte usa concéntrico cuando anida; el default de arquetipo cuando no).
|
|
|
|
|
|
**Adoptar** (los que anidan de verdad): items de `dropdown-menu`/`context-menu`/`listbox`/`navigation-menu` (in-bar), cards en cards, contenido interno de paneles, etc. **Excluir:** flush/gap-cero (radio compartido legítimo), y padres con radio `full`/9999px (`SHAPE_ENGINE_RFC §5`: el cálculo es ilegible a 9999 → excluir dialog/drawer `full`/`xl`).
|
|
|
|
|
|
**Pilot:** `dropdown-menu` (espejar `command.css:230-232`). Verificar concéntrico en navegador. Luego el resto, uno a uno.
|
|
|
|
|
|
### WS-D — Consumir el bundle de size (geometría/tipo/icono) + fijar font · esfuerzo S→M
|
|
|
|
|
|
**Verificado.** El bundle `--size-{k}-*` se emite (`render-css.ts:1328`) con **0 consumidores**; cada recipe re-declara el mapeo (≈417 tokens). `THEMING.md:615-616` dice que consumirlo es el follow-up previsto.
|
|
|
1. **Inmediato:** `STATIC_SIZE.fontSize` → 1:1 (`md:'sm'→'md'`, etc., `static.ts:423`); actualizar test + regen `generated/base.css`. Mata la mentira 14px.
|
|
|
2. **Consumir** en los recipes los slots **control-height / font-size / icon-size / padding / gap** (= "size recipe" de Chakra; mata la duplicación). Recipes que se desvían a propósito (ladder de px de `button`) quedan explícitos.
|
|
|
3. **El slot `radius` SALE del bundle** (WS-B): el radio ya no es size-keyed.
|
|
|
|
|
|
### WS-E — Densidad + scaling: validar + higiene · esfuerzo S
|
|
|
|
|
|
eidos está **alineado y por delante** (ambos ejes; ningún referente tiene los dos). Acción = documentar + higiene:
|
|
|
- Documentar que densidad = texto estable y scaling = escala texto (mantener ortogonales), con pisos de a11y (targets ≥ tamaño mínimo) — recomendación M3/Carbon.
|
|
|
- Tokenizar literales residuales: `carousel` indicadores `0.375rem…` → tokens (`carousel.css:128-142`); revisar grosor de barra `progress`/`meter` (`base.ts:1030`).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 3. Guards de CI (extender `recipe-css-contract.test.ts`)
|
|
|
|
|
|
1. **(WS-A)** todo wrapper con prop `size` resuelve vía la cadena ambiental (no `'md'` literal suelto).
|
|
|
2. **(WS-B)** ningún recipe rebindea `radius` en sus bloques `[data-size]` (el radio está desacoplado del size — **invierte el guard equivocado de la v1**).
|
|
|
3. **(WS-C)** un hijo redondeado anidado en un padre redondeado con padding usa `data-shape-nest` o está en la lista de exención (flush / pills / `full`).
|
|
|
4. **(WS-D/E)** sin literales px/rem en tokens de geometría/tipo de recipe; todo `--size-{k}-{slot}` emitido tiene ≥1 consumidor (o se poda).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 4. Secuencia (pilot-first siempre; nunca cascada)
|
|
|
|
|
|
`WS-D paso 1 (font 1:1, trivial)` → **`WS-B (radio Radix, pilot button/card)`** → **`WS-C (concéntrico, pilot dropdown-menu)`** → **`WS-A (size ambiental, pilot Form→Field)`** → `WS-D pasos 2-3 (consumir bundle)` → `WS-E (docs+higiene)`.
|
|
|
|
|
|
B y C son R5 (van juntos: B fija el default de arquetipo, C la herencia concéntrica). A es independiente (R3) y puede solaparse. D-paso-1 primero por trivial; D-pasos-2-3 tras B (que saca el radio del bundle).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 5. Verificación
|
|
|
|
|
|
- **Tipos/lógica:** `npm run check` + `npx vitest run src/uix/eidos` (+ scope del componente).
|
|
|
- **Visual (obligatorio en B y C):** screenshot/preview **por size**; el radio es visible y `npm run check` no lo valida. Medir, no ojímetro (precedente toggle-group: computed values bit-a-bit).
|
|
|
- **Pilot-first**: un componente, verificar en navegador, luego propagar.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 6. Sign-offs restantes (tuyos)
|
|
|
|
|
|
1. `size?: Size` (default `undefined`) en lectores — cambio de API pública.
|
|
|
2. **Provider global `componentSize`** (à la Ant ConfigProvider) además de los publishers por-contenedor — ¿lo incluimos como superficie "size por defecto de la app"?
|
|
|
3. **Cap-at-md**: ¿solo overlays (un `full` no engorda su parte) y la herencia en-flujo (`Form size="lg"`) sigue 1:1? (recomendado).
|
|
|
4. **Factor de tema `radius`** (none…full, à la Radix): ¿lo añadimos si no existe? (recomendado — es la perilla que cierra el modelo Radix).
|
|
|
5. Cambio visual de WS-B (esquinas de button/card dejan de moverse con el size; pasan a default-de-arquetipo + `rounded`).
|