You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/inherit_audit.md

22 KiB

Auditoría de herencia y escalado — capa Eidos / Theming

Fecha: 2026-06-27 · Alcance: src/uix/eidos (foundation + recipes + 80 componentes). Excluidos (tracks WIP, regla de proyecto): words/**, palabras/**, chronos/**.

Método: lectura directa de la foundation (lib/render-css.ts, lib/primitives/static.ts, lib/config-types.ts), de la doctrina (THEMING.md, SCALING_RFC.md, SHAPE_ENGINE_RFC.md, STRUCTURE_ENGINE_RFC.md, TYPOGRAPHY_ENGINE_RFC.md) y de los recipes (lib/recipes/base.ts) + CSS por componente. Cada hallazgo lleva evidencia archivo:línea. No se usaron agentes; los datos provienen de grep/lectura verificables.


0. Las 5 reglas auditadas

# Regla (enunciado del usuario) Veredicto global
R1 Los componentes deben escalar con el nivel de escala del tema (data-scaling). ✅ Cumplida en lo esencial (vía tokens) — 2 excepciones puntuales.
R2 Los componentes deben reaccionar a la densidad del tema (data-density). ✅ Cumplida en lo esencial (vía tokens) — mismas excepciones.
R3 Un componente sin size propio debe heredar el size de su padre. ❌ Incumplida de forma sistémica — ~todos hardcodean size='md'.
R4 Los sub-componentes internos deben escalar con el size (tipografía, controles, etc.). 🟡 Parcial — geometría + tipografía sí; el radio NO.
R5 Las formas de esquina/radios deben heredar/escalar (radios concéntricos). ❌ Incumplida — radio desacoplado del size; concéntrico casi sin adoptar.

Tesis de la auditoría: la disciplina de tokenización del framework hace que R1 y R2 se cumplan casi gratis (todo lo que es un token hereda --scaling y la densidad). Donde el sistema falla es en las tres reglas de herencia/coherencia de tamaño y forma (R3, R4-radio, R5): no existe herencia ambiental de size, el radio no forma parte de la cascada [data-size] salvo en toggle, y conviven tres filosofías de radio incompatibles sin doctrina única.


1. Cómo la foundation implementa cada eje (el contrato)

1.1 Scaling (zoom global data-scaling 90–110)

--scaling se emite en :root (render-css.ts:708 appendScalingDeclarations) y multiplica las métricas px en su calc:

  • font-size → calc(<size> * var(--scaling)) (render-css.ts:1028).
  • icon-size, blur → appendScaledMetricDeclarations (render-css.ts:260,266).
  • space, control-height → calc(value * var(--density-*-scale) * var(--scaling)) (render-css.ts:162-168).
  • NO escalan: radius, border-width, shadow, z, line-height (ratio), opacity, motion (decisión deliberada, SCALING_RFC.md:34-36).

→ Consecuencia: cualquier componente que dimensione con var(--font-size-*), var(--icon-size-*), var(--space-*), var(--control-height-*) (o tokens de recipe que los referencian) escala con --scaling automáticamente. Solo los literales px/rem quedan fuera.

1.2 Densidad (data-density compact/comfortable/spacious)

--density-space-scale + --density-control-scale (static.ts:169 STATIC_DENSITY) multiplican --space-* y --control-height-* (render-css.ts:162-168). La tipografía no lleva densidad por diseño (THEMING.md:687-694, paridad Radix).

→ Consecuencia: los componentes reaccionan a densidad si su padding/gap salen de --space-* y sus alturas de --control-height-*. Confirmado en los recipes: button.height-md → var(--control-height-md), field, combobox, etc. (base.ts:841,1065,1222).

1.3 Size (prop size discreto xxs…xxl)

La foundation emite un bundle --size-{k}-{control-height,font-size,icon-size, padding-inline,padding-block,gap,radius} (render-css.ts:1328 appendSizeDeclarations, desde STATIC_SIZE en static.ts:393). PERO ese bundle está huérfano: 0 consumidores (THEMING.md:605-606). En su lugar, cada recipe re-declara su propia familia de tokens por size (--{c}-height-{size}, --{c}-font-size-{size}, …) y el CSS rebindea --_{c}-* en bloques [data-size='X']. El patrón canónico (piloto) es toggle (toggle.css:64-98).

1.4 Radio / forma (shape)

  • Escala --radius-{none…full} (4/6/10/16/20px + pill) (static.ts:40).
  • STATIC_SIZE mapea cada size a un radio (xxs/xs→sm, sm/md→md, lg→lg, xl→xl, xxl→xxl) (static.ts:393-457) — pero ese mapeo viaja en el bundle --size-* huérfano, así que nadie lo consume.
  • Concéntrico: [data-shape-nest] deriva border-radius: max(0px, var(--shape-outer-radius) − var(--shape-nest-gap)) (render-css.ts:1153-1159, SHAPE_ENGINE_RFC.md Fase 2).
  • Familias data-shape='rounded|continuous|cut|scoop' + --shape-smoothing (superelipse) (render-css.ts:1140-1151).

2. Hallazgos por regla

R1 — Scaling · ✅ cumplida en lo esencial · severidad de los huecos: BAJA

Todo el dimensionado pasa por tokens scaling-coupled, así que el grueso cumple. Excepciones reales (literales que no siguen --scaling):

Evidencia Problema
carousel.css:128-142 --_carousel-indicator-size: 0.375rem … 0.75rem — literal rem por size, no token. Los puntos de paginación no siguen --scaling (sí el zoom de navegador, pero no el eje del tema).
base.ts:1030-1031 (meter/progress) height-md: '6px', height-lg: '8px' — grosor de barra en px fijo (no escala). Menor: es grosor visual, discutible.
Hairlines 1px/2px (separadores, indicadores) — button.css:245, command.css:153, navigation-menu.css:288, etc. Legítimo: un separador de 1px no debe escalar. No es violación.

R2 — Densidad · ✅ cumplida en lo esencial · severidad: BAJA

Padding/gap salen de --space-* y alturas de --control-height-* (ambos density-coupled). Confirmado en recipes (base.ts:341-352,481-525,841-842,1065-1076). Mismas excepciones que R1 (el rem de carousel tampoco lleva densidad; grosor de barra fijo). La tipografía no escala con densidad a propósito (correcto).

R3 — Herencia de size del padre · ❌ incumplida sistémica · severidad: ALTA

No existe herencia ambiental de size. Prácticamente todos los componentes hardcodean size = 'md' como default (grep: 90+ wrappers .svelte con size = 'md'). Un componente sin size no mira a su padre: fija md.

Las únicas propagaciones contenedor→parte (las excepciones que confirman la regla):

Mecanismo Evidencia Alcance
dialog/context.ts → Dialog.Close deriva el size del dialog (cap en su subset) dialog/context.ts:21-31, dialog-close.svelte:49 solo Dialog→Close
list-surface-context.ts — paneles flotantes anidados igualan el size de la superficie raíz lib/list-surface-context.ts:14-30 menús/popups anidados (SubContent, listbox combobox)
toggle-group/context.ts — propaga variant/size a los items (contexto de toggle-group) solo items de toggle-group

Casos que deberían heredar y NO lo hacen:

  • Un <Select> / <Switch> / <Checkbox> junto a hermanos lg o dentro de un bloque sizeado: salen md.
  • <Form size="lg"> no propaga el size a los <Field> / controles anidados por contexto ambiental — el recipe form solo sizea su propio gap + sus acciones (form.css:25-51,118-137), no inyecta un size a Fields arbitrarios.
  • Dialog.* / Drawer.* solo propagan al Close; el resto de partes internas no.

Causa raíz: no hay un token/contexto --ui-size ambiental ni un SizeContext genérico que un control lea cuando su prop size es undefined. El default es un literal 'md', no inherit.

R4 — Escalado interno con el size propio · 🟡 parcial · severidad: MEDIA

Lo que SÍ escala (verificado en ~35 componentes): por cada [data-size='X'] el componente rebindea altura, padding, gap, font-size, icon-size y sus dimensiones específicas (track/thumb/indicator/control-size/swatch/preview/day-size…). Ejemplos confirmados: button.css:111-154, field.css:56-78, select.css:55-80,198-220, switch.css:35-61, slider.css:10-36, radio-group.css:96-114,212-230, tabs.css:23-42, tag-group.css:20-41, toolbar.css:30-55, pagination.css:15-34, accordion.css:42-150, calendar.css:28-47, card.css:95-129, checkbox.css:34-62, combobox.css:109-135,293-315, feed.css:45-68, editable.css:23-49, file-upload.css:17-31, avatar.css:50-73, banner.css:38-54, tooltip.css:46-56. → La tipografía interna y la geometría de sub-controles escalan correctamente.

Lo que NO escala con el size:

  1. El radio — ausente de casi todas las cascadas [data-size] (ver R5). Solo toggle lo rebindea.
  2. Decoraciones con literal — carousel puntos en rem (R1).

R5 — Radio / forma · ❌ incumplida · severidad: ALTA

5.a — El radio NO escala con el size. En casi todos los componentes el radio es un token fijo, idéntico en todas las tallas:

Componente Radio Evidencia
field (toda la familia de campos) var(--field-control-radius) único field.css:191
select --select-trigger-radius / --select-content-radius fijos select.css:39,189
tabs --tabs-trigger/content/list/pills-*-radius fijos tabs.css:72,115,166,174,198
pagination --pagination-control-radius fijo pagination.css:52
tag-group --tag-group-item-radius fijo tag-group.css:70
stepper, toolbar, combobox, tooltip, listbox, menús, calendars var(--radius-*) fijo grep border-radius: var(--radius-

Único componente que acopla radio↔size: toggle — --_toggle-radius: var(--toggle-radius-{size}) rebindeado por [data-size] (toggle.css:7,70,79,88,97). Es el patrón correcto según STATIC_SIZE.

5.b — Incoherencia de filosofía: 3 modelos de radio coexisten sin doctrina.

  • size-coupled: toggle (radio sigue el size).
  • prop rounded independiente: button y card declaran --{c}-radius-{sm..xl} pero los cablean a data-rounded (default md), no a data-size (button.css:27,216-220; card.css:40,133-134). → un <Button size="xl"> conserva esquinas md.
  • token fijo único: todos los demás (sin rounded, sin acoplar a size).

El primitivo canónico que zanjaría esto — el campo radius por size de STATIC_SIZE y el token --size-{k}-radius (render-css.ts:1344) — está huérfano (0 consumidores) (THEMING.md:605). El contrato existe pero nadie lo usa.

5.c — Radios concéntricos: adopción casi nula. [data-shape-nest] / --shape-outer-radius solo aparece en 11 archivos (card-group, combobox, select, command, menubar + sus items). Las superficies anidadas más comunes — campo dentro de Form, card dentro de panel, contenido de Dialog/Drawer, calendar dentro de un picker, items de la mayoría de menús — no derivan su radio del padre. El concéntrico es opt-in por diseño (SHAPE_ENGINE_RFC.md:80), pero su penetración real es marginal.


3. Incoherencias estructurales transversales

  1. Bundle --size-* huérfano + contradictorio. La foundation emite --size-md-font-size: 14px (appendSizeDeclarations), pero la regla viva es el 1:1 (md = 16px) re-declarado en cada recipe (THEMING.md:605-606). El sistema mantiene un contrato de tamaño que (a) nadie consume y (b) miente sobre los valores actuales. Es deuda que invita a drift.
  2. Re-declaración del mapeo size→token en cada recipe. Como nadie consume --size-{k}-*, cada componente repite a mano height/px/gap/font/radius por size (decenas de tokens × 80 componentes). Un solo punto de verdad (consumir el bundle) eliminaría la duplicación — hoy el único guard contra drift es el test de "no literales px en font/icon" (THEMING.md:608-611), que no cubre padding/gap/radio.
  3. Sin herencia ambiental de size (R3) ni de forma (R5.c): los dos ejes que el usuario espera "heredables por defecto" son justo los que el sistema trata como per-instancia con default literal.

4. Matriz por componente

Leyenda: ✅ cumple · 🟡 parcial · ❌ incumple · — no aplica (sin eje de size/densidad). R1=scaling · R2=densidad · R3=hereda size del padre · R4=escalado interno con size · R5=radio escala/concéntrico.

4.1 Controles e inputs (tienen size)

Componente R1 R2 R3 R4 R5 Notas
toggle ✅ ✅ ❌ md ✅ ✅ Referencia: radio acoplado a size.
button ✅ ✅ ❌ md 🟡 ❌ radio por rounded, no size.
badge ✅ ✅ ❌ md 🟡 ❌ radio fijo / rounded.
checkbox ✅ ✅ ❌ md ✅ ❌ radio del box fijo.
radio-group ✅ ✅ ❌ md ✅ ❌ indicador circular (radio N/A), pero item radio fijo.
switch ✅ ✅ ❌ md ✅ ❌ track/thumb son pills (radio full, OK).
slider ✅ ✅ ❌ md ✅ ❌ track/thumb pills (OK).
field (genérico) ✅ ✅ ❌ md ✅ ❌ --field-control-radius único.
spin/number/css-field ✅ ✅ ❌ md ✅ ❌ comparten superficie spin-field.
date/time/color-field ✅ ✅ ❌ md ✅ ❌ segmentos + swatch escalan; radio fijo.
search/password-field ✅ ✅ ❌ md ✅ ❌ radio fijo.
pin-input ✅ ✅ ❌ md ✅ ❌ celdas: font escala (cell-font-size-*), radio fijo.
textarea ✅ ✅ ❌ md ✅ ❌ radio fijo.
tags-input ✅ ✅ ❌ md ✅ ❌ radio fijo.
editable ✅ ✅ ❌ md ✅ ❌ radio fijo.
select ✅ ✅ ❌ md ✅ ❌ trigger+content escalan; radios fijos.
combobox ✅ ✅ ❌ md ✅ ❌ usa concéntrico en items (data-shape-nest).
rating-group ✅ ✅ ❌ md ✅ — iconos (estrellas); radio N/A.
stepper ✅ ✅ ❌ md ✅ ❌ indicador/trigger/content radio fijo.

4.2 Acciones compuestas (composición sobre Button/Toggle)

Componente R1 R2 R3 R4 R5 Notas
toggle-group ✅ ✅ 🟡 ctx ✅ ❌ hereda size/variant del grupo (contexto) — único buen caso de R3 en controles; items son Toggle → radio podría seguir el size pero se aplana en grupo.
button-group ✅ ✅ ❌ md 🟡 ❌ radio en extremos via Button.
split-button ✅ ✅ ❌ md 🟡 ❌ compone Button.
fab ✅ ✅ ❌ md ✅ ❌ redondo (radio full, OK).
menu-dial / onion-menu ✅ ✅ ❌ md ✅ — radial; radio full.

4.3 Overlays y superficies (size = densidad/anchura del panel)

Componente R1 R2 R3 R4 R5 Notas
dialog ✅ ✅ 🟡 Close ✅ 🟡 propaga size al Close (ctx). Radio panel fijo (en full lo cambia, dialog.css:177).
drawer ✅ ✅ ❌ ✅ 🟡 width/height/padding por size; radio fijo.
popover ✅ ✅ ❌ md 🟡 ❌ radio --radius-md fijo.
tooltip ✅ ✅ ❌ md ✅ ❌ py/px/font por size; radio fijo.
dropdown/context-menu ✅ ✅ 🟡 ctx 🟡 ❌ list-surface ctx para anidados; usan data-shape-nest en items.
menubar / navigation-menu ✅ ✅ 🟡 ctx 🟡 ❌ concéntrico parcial.
command ✅ ✅ ❌ md ✅ ❌ input/item escalan; concéntrico en items.
listbox ✅ ✅ 🟡 ctx 🟡 ❌ radio fijo.
tabs ✅ ✅ ❌ md ✅ ❌ trigger/content/list/pills radio TODO fijo.
toolbar ✅ ✅ ❌ md ✅ ❌ controles escalan; radios fijos.
banner / toast / announce ✅ ✅ ❌ md ✅ ❌ radio fijo.

4.4 Pickers y calendarios

Componente R1 R2 R3 R4 R5 Notas
calendar / range-calendar ✅ ✅ ❌ md ✅ ❌ padding/control/day/font por size; celdas radio fijo.
month-grid / year-grid ✅ ✅ ❌ md ✅ ❌ escalan font/celda.
date/time/date-range/time-range-picker ✅ ✅ ❌ md ✅ ❌ reusan tokens de field+calendar por size.
color-picker ✅ ✅ ❌ md ✅ ❌ trigger/content/swatch por size.
gradient-builder / gradient-picker ✅ ✅ ❌ md 🟡 ❌ (nuevos, sin commitear) radio fijo.

4.5 Datos / navegación / feedback

Componente R1 R2 R3 R4 R5 Notas
accordion ✅ ✅ ❌ md ✅ ❌ trigger/content/indicator por size (incl. full); radio item fijo.
breadcrumb ✅ ✅ ❌ md ✅ — solo gap+font (sin radio relevante).
pagination ✅ ✅ ❌ md ✅ ❌ control radio fijo.
tag-group ✅ ✅ ❌ md ✅ ❌ item radio fijo.
tabs (ver 4.3)
table / tree-view / tree-grid / grid-list ✅ ✅ ❌ md ✅ ❌ densidad de fila por size; radios de celda/chip fijos.
feed ✅ ✅ ❌ md ✅ — padding/indent/font por size.
carousel 🟡 🟡 ❌ md 🟡 — indicadores en rem literal (carousel.css:128-142).
virtual-list / virtual-grid ✅ ✅ ❌ md ✅ — densidad por size.
progress / meter 🟡 🟡 ❌ md ✅ — grosor de barra px fijo (base.ts:1030); radio full (OK).
spinner / skeleton ✅ ✅ ❌ md ✅ — radio circular/sm.
avatar ✅ ✅ ❌ md ✅ 🟡 tamaño+font por size; radio via data-radius, no size.
card ✅ ✅ ❌ md ✅ ❌ título/desc/body+padding por size; radio por rounded.
card-group ✅ ✅ ❌ md ✅ 🟡 usa data-shape-nest (concéntrico).
metrics / timeline ✅ ✅ ❌ md ✅ ❌ radios fijos.
file-upload ✅ ✅ ❌ md ✅ ❌ gap/padding/control/preview por size.
form ✅ ✅ ❌ md 🟡 — sizea gap+acciones; no propaga size a Fields (R3).

4.6 Primitivas de layout / tipografía (sin eje size-densidad)

box · stack · flex · grid · wrap · group · float · aspect-ratio · auto-grid · container · section (layout) y text · heading · display · code · code-block · kbd · mark · highlight · link · separator (tipografía/inline):

  • R1/R2: ✅ — escalan vía var(--space-*) (layout) y var(--font-size-*) / var(--style-*) (tipografía), todos scaling/densidad-coupled.
  • R3/R4/R5: — — no exponen size discreto de control (excepto container default xl / section default lg, que son anchuras de layout, no densidad).
  • Radio relevante solo en code-block/kbd/mark/link → --radius-sm/md fijo (aceptable: no tienen eje de size).

5. Recomendaciones priorizadas

P0 — Herencia ambiental de size (cierra R3)

Introducir un contexto/cascada de size ambiental que un control lea cuando su prop size es undefined, en lugar de hardcodear 'md'. Dos vías:

  • Contexto Svelte genérico SizeContext (como list-surface-context.ts pero universal), set por contenedores sizeados (Form, Card, Toolbar, Dialog…) y leído por cada wrapper: size = props.size ?? ctx?.size ?? 'md'.
  • O un token CSS heredable --ui-size + data-size que cascadee por DOM (cuando no haya portal de por medio). Aplicar primero a Form → Field/controles (el caso más esperado) reutilizando la norma cap-en-md ya documentada (THEMING.md:560-579).

P1 — Acoplar el radio al size (cierra R5.a / R4-radio)

Decidir una doctrina de radio y aplicarla:

  • Opción canónica (recomendada): consumir el radius por size de STATIC_SIZE — añadir --_{c}-radius a cada cascada [data-size] como hace toggle. Resucita el contrato --size-{k}-radius hoy huérfano.
  • Mantener rounded como override explícito sobre ese default por size (no como sustituto), unificando button/card con el resto.

P1 — Eliminar el bundle --size-* huérfano o forzar su consumo

O bien (a) los recipes consumen --size-{k}-* en vez de re-declarar el mapeo (un punto de verdad, mata el drift y la mentira del 14px), o bien (b) se poda el bundle y se documenta que el canon vivo es el 1:1 por recipe. Hoy coexisten ambos y se contradicen (THEMING.md:605).

P2 — Ampliar radios concéntricos (R5.c)

Extender [data-shape-nest] a las superficies anidadas obvias: contenido de Dialog/Drawer, Field dentro de Form, calendar dentro de los pickers, items de menús que aún no lo usan.

P3 — Tokenizar los literales residuales (R1/R2)

  • carousel indicadores: 0.375rem…0.75rem → tokens (--icon-size-* o --space-*) para que sigan --scaling/densidad (carousel.css:128-142).
  • Evaluar grosor de barra progress/meter (base.ts:1030) — si debe seguir el zoom, pasarlo a un token escalado.

P3 — Guard de coherencia ampliado

El test actual solo prohíbe literales px en font-size/icon-size (THEMING.md:608). Ampliarlo a padding/gap/radius de recipe y a una regla "si hay [data-size], debe rebindear el radio" cerraría R4/R5 a nivel de CI.


6. Resumen de severidad

Hallazgo Regla Severidad Amplitud
Sin herencia ambiental de size (default literal md) R3 ALTA ~todos los componentes
Radio desacoplado del size (solo toggle lo acopla) R5.a / R4 ALTA ~todos los controles/superficies
3 filosofías de radio sin doctrina única R5.b ALTA (coherencia) toggle vs button/card vs resto
Bundle --size-* huérfano y contradictorio transversal MEDIA foundation + 80 recipes
Concéntrico casi sin adoptar R5.c MEDIA superficies anidadas
Literales rem/px (carousel, barras) R1/R2 BAJA 2–3 componentes

Conclusión: el sistema es fuerte en R1/R2 (la tokenización fuerza scaling+densidad casi gratis) y débil justo donde el usuario apunta: la herencia (R3 inexistente, R5.c marginal) y la coherencia del radio con el size (R5.a/R4, con una inconsistencia de fondo entre toggle, button/card y el resto). Las cuatro acciones P0–P1 resuelven el 80% del problema reutilizando mecanismos que el framework ya tiene a medio cablear (el contexto de size de dialog, el radius por size de STATIC_SIZE, el patrón toggle).

Powered by TurnKey Linux.