14 KiB
Eidos Box
Universal box-model utility. Eidos-native: cada prop mapea a una
custom property --box-* emitida inline en un único <div data-box>,
y la receta cae con revert-layer a la cascada normal cuando no se
fija. Box es la primitiva sobre la que se componen Flex, Grid, Stack,
Group, Wrap, Container y Section.
Superficie
<Box padding={4} gap={3} display="flex" maxWidth={600}>…</Box>
<!-- Como item de un Grid/Flex padre: -->
<Box gridColumn="1 / 3" placeSelf="center">…</Box>
Baseline
Origen: air/components/layout/box (rama morfo-runtime).
Adaptaciones para eidos: drop del prefijo air-, selectores
[data-box] directos, vars renombradas a --box-*, sustitución de
var(--air-space-N) por var(--space-N) y resolución reactiva de
props responsive vía ActiveEidos.require().resolve().
Decisión clave introducida en 66c6897c: los props de grid item
placement (gridColumn, gridRow, gridArea, placeSelf) viven
en Box, no en Grid. Son props del HIJO que declara su slot, no del
container. Air los puso en Grid — corregido durante el port.
Comparativa
| Capacidad | UIX (eidos) | Radix Themes | Chakra UI | Mantine |
|---|---|---|---|---|
| Display | Sí | Sí | Sí | Sí |
| Width / height + min/max | Sí | Sí | Sí | Sí |
| Padding / margin (+ shorthands X/Y/per-side) | Sí (paddingX, paddingY, paddingTop…) |
Sí (p, px, pt…) |
Sí | Sí |
| Position + insets | Sí | Sí | Sí | Sí |
| Overflow + axis variants | Sí | Sí | Sí | Sí |
Flex item (flex, grow, shrink, basis, order) |
Sí | Sí | Sí | — (en Flex) |
Grid item (gridColumn, gridRow, gridArea, placeSelf) |
Sí (movido desde Grid 2026-05-22) | Sí | Sí | — |
align-self / justify-self |
Sí | Sí | Sí | — |
| Background / color / shadow | No — UIX separa layout de visual treatment | Sí (bg, style) |
Sí (bg, color) |
Sí (bg) |
as prop para semantic tag |
No — root fijo <div> (batch 1) |
Sí | Sí (vía polymorphic ref) | Sí (component) |
| Responsive prop syntax | Sí (ResponsiveProp via Eidos) |
Sí ({ initial: …, sm: … }) |
Sí | Sí |
| Visibilidad por breakpoint | Sí — DESMONTA (visibleFrom/hiddenFrom) |
Sí, por CSS (hideFrom/hideBelow) |
Sí, por CSS | Sí, por CSS (hiddenFrom/visibleFrom) |
Container-side flex/grid (alignItems, justifyContent, templateColumns…) |
No — viven en Flex/Grid (split Radix) | Split Radix | Mezclado en Box | Split Radix |
Decisiones
-
Item-side props en Box, container-side en Flex/Grid — split estilo Radix Themes / Mantine. Chakra y MUI mezclan todo en Box, pero el split deja un API más predecible.
-
No
bg/color/shadow— UIX separa estructura de tratamiento visual. Esos props viven en componentes convariant/color(avatar, dialog, button…). Box es sólo layout. -
Root→<div>fijo — sinasprop en batch 1asdesde 2026-08-19 (F21). Los «≥2 casos reales» que la decisión pedía llegaron: una columna de enlaces de pie que debía serul/li, y dieciséis blocks del tier envolviendoSectionen un<section>escrito a mano sólo para poder nombrarlo. La lista de elementos es cerrada (BoxElement: contenedores, nunca void) — es una escotilla SEMÁNTICA, no un prop de tag libre. Defaultdiv, así que ningún consumidor anterior cambia; el modelo de caja, las vars y la receta son idénticos rinda lo que rinda. -
Inline styles, no classnames generados — el style attr lista cada
--box-*declarada; debugger en DevTools muestra exactamente qué propiedades activas tiene cada Box. -
revert-layercomo fallback — props sin setear no fuerzaninitial, dejan que la cascada normal aplique. Permite mezclar Box con cualquier otro CSS sin "ganar" sobre estilos no relacionados. -
visibleFrom/hiddenFromNO renderizan, no ocultan (2026-08-18) — la diferencia entera condisplay={{ base: 'none' }}. Un{#if}alrededor de la raíz: el box no está en el DOM, sus efectos no corren y el media de dentro no se pide. Por herencia lo ganan los once contenedores que componen Box (Section, Container, Flex, Grid, Stack, Group, Wrap, AutoGrid, Surface, AspectRatio, Float), sin tocar sus ficheros — doce con Box.Ninguna referencia hace esto: Mantine (
hiddenFrom/visibleFrom), Chakra y Panda (hideFrom/hideBelow), Framer y Webflow ocultan todos con CSS y dejan el DOM montado — Webflow lo documenta («hidden elements still load initially, consuming bandwidth») y Framer llega a duplicar secciones enteras por breakpoint. Eligieron CSS porque con SSR una puerta JS pinta la variante equivocada en el HTML (MUI: «returns a default matches during the first mount»; Vuetify #17252: «major layout shifts during hydration»), y el precio de evitarlo es adivinar el ancho por User-Agent o Client Hints.Aquí ese precio no existe: el proyecto se despliega como SPA (
adapter-static+fallback, y ninguna ruta declaraprerender = true), así que el primer render ocurre en el navegador con elinnerWidthreal. La contrapartida que SÍ se paga está escrita en el tipo: cruzar un breakpoint destruye el subárbol y su estado. Cuando el estado deba sobrevivir,display.Sólo en DESARROLLO verás otra cosa, y conviene saberlo antes de abrir un issue: el dev server de SvelteKit sí renderiza en servidor, donde el ancho es 0, así que el HTML inicial trae la variante
basey el navegador empieza a descargar su media antes de que la hidratación la pode. Medido: convisibleFrom="md"el HTML del dev no contiene la sección, conhiddenFromsí, y elvideo.mp4de dentro se pide una vez. En producción no ocurre: el build es un200.htmlde ~1,8 KB, shell puro, sin una sola sección.Verificado en Chrome real (2026-08-18): a 2133px conviven correctamente
visibleFrom="md"presente,hiddenFrom="md"ausente y un rangosm–lgausente; al cerrar la puerta en vivo desaparecen del DOM la sección, su<video>y el<input>; al reabrirla el input vuelve vacío — la renuncia, medida, no supuesta. -
El shorthand
flexse EXPANDE en el wrapper, no en el recipe (2026-08-17) —box.cssdeclaraflex-grow/flex-shrink/flex-basisdesde sus vars; unflex:shorthand al lado lo borraba aquel de los tres cuya var estuviera sin poner, porquerevert-layer(eidos no envía@layer) devuelve la propiedad a su valor inicial. Medido:<Box flex={2}>computaba0 1 autoy se quedaba en el ancho de su contenido dentro de una fila flex; inyectando--box-grow: 2en el mismo nodo pasaba a2 1 auto. El prop sigue igual y ahora funciona:expandFlexShorthand(lib/layout-helpers.ts, con la gramática de CSS pinneada en test) escribe las tres vars, y ungrow/shrink/basisexplícito sigue ganando. El token público--box-flexdesaparece con el shorthand: era un nombre que ya no lee nadie. La misma trampa queaffix/types.tsdocumenta paraposition.
Eventos Sema
Box declara 0 eventos. Es una primitiva pasiva: no commit, no emerge, no reacciona a nada — sólo estiliza hijos vía cascade. Componentes que animan/cambian estado al aparecer componen Box con una primitiva interactiva (popover, drawer, collapsible) que sí posee los verbos sema relevantes.
Gaps
| Gap | Disposición | Detalle |
|---|---|---|
as prop para semantic tag (section, article, main, aside…) |
diferir | Esperamos ≥2 casos reales antes de añadir polimorfismo. Hoy se envuelve Box en el tag semántico. |
bg / color shorthand sobre tokens UIX |
descartar | Box es layout puro. Los componentes con variant/color son los que llevan tratamiento visual. |
borderRadius / boxShadow props |
descartar | Idem — escapar vía style= cuando se necesite un one-off. |
visibleFrom/hiddenFrom en Card, Banner, ScrollArea y Sticky |
diferir | Rinden su propia raíz (svelte:element, <header>, un Provider), así que no heredan la puerta. La regla es la del as: se añaden a mano cuando aparezcan ≥2 casos reales; hasta entonces, envolver en <Box visibleFrom>. |
Puente de SSR para visibleFrom/hiddenFrom |
diferir, con disparador | Si algún día una ruta activa prerender/ssr, el HTML llevará la variante base. El paso es: estampar data-visible-from/data-hidden-from, emitir dos reglas @media por breakpoint en render-css.ts (junto a las de tipografía) y pasar la puerta a hidratado ? resuelto : true. Hoy sería un atributo que nadie lee. |
flexBasis (alias de basis) |
diferir | Posible alias por familiaridad con devs Chakra. Bajo prioridad. |
| Documentación per-prop con ejemplos visuales individuales | implementar | En la pasada de docs final. Hoy el demo cubre los casos comunes. |
Referencias
- Radix Themes Box: https://www.radix-ui.com/themes/docs/components/box
- Chakra UI Box: https://chakra-ui.com/docs/components/box
- Mantine Box: https://mantine.dev/core/box/
- MUI Box: https://mui.com/system/react-box/
Passive justification
Visual-only primitive (scope: ['eidos'] en el morfo). Una sola
part Provider que emite el marker [data-box], sin estados, sin
data-attrs específicos, sin ARIA, sin keyboard. El recipe consume
las CSS variables que el componente escribe inline. No hay nada que
animar, ningún ciclo de vida que enviar a sema.