From 6707034fbdaf90ae2586dc67358ee4eeba2482dd Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 17 May 2026 17:35:25 +0200 Subject: [PATCH] Add Eidos drawer reference comparison --- continue.md | 11 +++++ src/uix/eidos/components/drawer/README.md | 48 +++++++++++++++++++ .../components/drawer/drawer-content.svelte | 46 +++++++++++++++++- src/uix/eidos/components/drawer/drawer.css | 30 +++++++----- src/uix/eidos/components/drawer/index.ts | 1 + src/uix/eidos/components/drawer/types.ts | 14 ++++++ 6 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 src/uix/eidos/components/drawer/README.md diff --git a/continue.md b/continue.md index 0ef528474..388498367 100644 --- a/continue.md +++ b/continue.md @@ -30,6 +30,17 @@ Fecha de corte: 2026-05-17. Rama: `active-uix`. - Decision: se anaden overrides visuales `width`, `minWidth`, `maxWidth`, `height`, `minHeight`, `maxHeight` en `Dialog.Content`, serializados como variables privadas del recipe. Soma sigue siendo dueno de comportamiento. +- Segundo componente auditado con el nuevo protocolo: + - `Drawer`: comparado contra Air (`glm-5`), Soma/Morfo actuales, Vaul, + shadcn-svelte y Ark UI Dialog como referencia de dialog/layer. + - Decision: no se crea `Drawer.Positioner`; la geometria edge-anchored vive + en `data-side` + `data-variant` y en la receta. + - Decision: no se implementa `shouldScaleBackground` de Vaul en Drawer local; + es un efecto global sobre el wrapper de app y requiere contrato de shell. + - Decision: se anaden overrides visuales `width`, `minWidth`, `maxWidth`, + `height`, `minHeight`, `maxHeight` en `Drawer.Content`, serializados como + variables privadas del recipe. El size canonico `sm/md/lg/full` sigue como + preset principal. - Componentes Eidos nuevos desde Soma: - `meter` - `progress` diff --git a/src/uix/eidos/components/drawer/README.md b/src/uix/eidos/components/drawer/README.md new file mode 100644 index 000000000..e4641a3a5 --- /dev/null +++ b/src/uix/eidos/components/drawer/README.md @@ -0,0 +1,48 @@ +# Eidos Drawer + +Fecha de revision: 2026-05-17. + +## Baseline + +Drawer parte de Air y se apoya en Soma para comportamiento. Eidos solo anade +superficie visual estable: `size`, medidas/lmites del panel y receta CSS. + +## Comparativa + +| Referencia | Superficie relevante | Estado Eidos | +| --- | --- | --- | +| Air `drawer` (`glm-5`) | `Root`, `Trigger`, `Portal`, `Overlay`, `Content`, `Header`, `Footer`, `Title`, `Description`, `Close`; `Content.size = sm/md/lg/full`; cuatro lados fisicos via Terra. | Cubierto. Se conserva la API compound y el `size` canonico. | +| Soma actual | `variant = overlay/inline/persistent`, `direction = top/bottom/left/right/start/end`, snap points, drag, `Handle`, dismissal, focus/scroll callbacks, `activeSnapPoint`, `minSize/maxSize`. | Cubierto por passthrough. Eidos no duplica estado ni gesture logic. | +| Vaul | `Root`, `Trigger`, `Portal`, `Overlay`, `Content`, `Handle`, `Title`, `Description`, `Close`; `modal`, `container`, `direction`, `dismissible`, `handleOnly`, `snapPoints`, `activeSnapPoint`, `fadeFromIndex`, `snapToSequentialPoint`. | Cubierto en lo esencial por Soma. `fadeFromIndex` queda como decision visual futura si el overlay necesita fade por indice. | +| shadcn-svelte | Wrapper sobre Vaul Svelte con `Root`, `Trigger`, `Content`, `Header`, `Footer`, `Title`, `Description`, `Close`; ejemplo de responsive dialog usando Dialog en desktop y Drawer en mobile. | Cubierto. Eidos mantiene `Header/Footer` como shells visuales y no introduce API flat. | +| Ark UI / Radix / Bits UI | No hay Drawer canonico con gestos equivalente; la referencia comparable es Dialog: portal, backdrop/overlay, content, title/description, close, focus trap, non-modal/lazy mount. | Cubierto desde Soma; Eidos no anade `Positioner` porque la geometria edge-anchored vive en la receta y en `direction`. | + +Fuentes externas consultadas: + +- Vaul API: https://vaul.emilkowal.ski/api +- Vaul snap points: https://vaul.emilkowal.ski/snap-points +- shadcn-svelte Drawer: https://tw3.shadcn-svelte.com/docs/components/drawer +- Ark UI Dialog: https://ark-ui.com/docs/components/dialog + +## Decisiones + +- No se reintroduce `Provider` publico ni API flat con snippets. +- No se crea `Drawer.Positioner`. Drawer no es Dialog centrado; su posicion + principal es edge-anchored y se deriva de `data-side` + `data-variant`. +- No se implementa `shouldScaleBackground` ahora. Es un efecto global sobre el + wrapper de la aplicacion y requiere un contrato explicito de ownership DOM, + no un estilo local de Drawer. +- `Drawer.Content` acepta `width`, `minWidth`, `maxWidth`, `height`, + `minHeight` y `maxHeight` como overrides visuales responsivos. Los valores se + serializan en variables privadas del recipe (`--_drawer-content-*`) para que + no entren en el contrato publico de tokens. +- `size` sigue siendo canonico y suficiente para la mayoria de usos; los + overrides son para layouts concretos: rail persistente, sheet de altura fija, + side panel ancho o constraints responsive. + +## Pendiente deliberado + +- Evaluar `fadeFromIndex` solo si el overlay de snap points necesita una + curva distinta por indice. Hoy Soma ya emite progreso y Eidos consume + opacidad de overlay. +- Evaluar background scaling como feature de capa/shell, no como recipe local. diff --git a/src/uix/eidos/components/drawer/drawer-content.svelte b/src/uix/eidos/components/drawer/drawer-content.svelte index 02743ee65..82e258ea3 100644 --- a/src/uix/eidos/components/drawer/drawer-content.svelte +++ b/src/uix/eidos/components/drawer/drawer-content.svelte @@ -10,19 +10,61 @@ * runtime. */ import { ActiveEidos } from '$uix/eidos'; + import { composeInlineStyle } from '$uix/eidos/lib/style'; import * as Drawer from '$soma/components/drawer'; import type { DrawerContentProps } from './types'; // `children` is renamed to `bodyContent` so the inner `{#snippet children}` // block can accept and forward Soma's `{ open, side, progress, isDragging }` // snippet args without shadowing the prop. - let { size = 'md', children: bodyContent, ...rest }: DrawerContentProps = $props(); + let { + size = 'md', + width, + minWidth, + maxWidth, + height, + minHeight, + maxHeight, + style, + children: bodyContent, + ...rest + }: DrawerContentProps = $props(); const eidos = ActiveEidos.require(); const resolvedSize = $derived(eidos.resolve(size, 'md')); + + function serializeLength(value: string | number | undefined): string | undefined { + if (value === undefined) return undefined; + if (typeof value === 'number') { + if (!Number.isFinite(value)) return undefined; + return value === 0 ? '0' : `${value}px`; + } + const trimmed = value.trim(); + return trimmed || undefined; + } + + function dimensionVar(name: string, value: string | number | undefined): string | undefined { + const serialized = serializeLength(value); + return serialized ? `${name}: ${serialized};` : undefined; + } + + const dimensionsStyle = $derived( + [ + dimensionVar('--_drawer-content-width-override', serializeLength(eidos.resolve(width))), + dimensionVar('--_drawer-content-min-width-override', serializeLength(eidos.resolve(minWidth))), + dimensionVar('--_drawer-content-max-width-override', serializeLength(eidos.resolve(maxWidth))), + dimensionVar('--_drawer-content-height-override', serializeLength(eidos.resolve(height))), + dimensionVar('--_drawer-content-min-height-override', serializeLength(eidos.resolve(minHeight))), + dimensionVar('--_drawer-content-max-height-override', serializeLength(eidos.resolve(maxHeight))) + ] + .filter(Boolean) + .join(' ') + ); + + const composedStyle = $derived(composeInlineStyle(style, dimensionsStyle)); - + {#snippet children(snippetProps)} {@render bodyContent?.(snippetProps)} {/snippet} diff --git a/src/uix/eidos/components/drawer/drawer.css b/src/uix/eidos/components/drawer/drawer.css index 706c9f986..db33643f0 100644 --- a/src/uix/eidos/components/drawer/drawer.css +++ b/src/uix/eidos/components/drawer/drawer.css @@ -75,8 +75,10 @@ /* ── Content (variant-shared base) ──────────────────────────────────────── */ [data-drawer-content] { - --_drawer-panel-width: var(--drawer-content-width-md); - --_drawer-panel-height: var(--drawer-content-height-md); + --_drawer-size-width: var(--drawer-content-width-md); + --_drawer-size-height: var(--drawer-content-height-md); + --_drawer-panel-width: var(--_drawer-content-width-override, var(--_drawer-size-width)); + --_drawer-panel-height: var(--_drawer-content-height-override, var(--_drawer-size-height)); --_drawer-padding: var(--drawer-content-padding-md); display: flex; @@ -84,6 +86,10 @@ gap: var(--drawer-stack-gap); padding: var(--_drawer-padding); overflow: auto; + min-inline-size: var(--_drawer-content-min-width-override, auto); + max-inline-size: var(--_drawer-content-max-width-override, none); + min-block-size: var(--_drawer-content-min-height-override, auto); + max-block-size: var(--_drawer-content-max-height-override, none); border: var(--drawer-content-border-width) solid var(--drawer-content-border); border-radius: var(--drawer-content-radius); background: var(--drawer-content-bg); @@ -123,20 +129,20 @@ /* ── Content sizing (variant-shared) ────────────────────────────────────── */ [data-drawer-content][data-size='sm'] { - --_drawer-panel-width: var(--drawer-content-width-sm); - --_drawer-panel-height: var(--drawer-content-height-sm); + --_drawer-size-width: var(--drawer-content-width-sm); + --_drawer-size-height: var(--drawer-content-height-sm); --_drawer-padding: var(--drawer-content-padding-sm); } [data-drawer-content][data-size='md'] { - --_drawer-panel-width: var(--drawer-content-width-md); - --_drawer-panel-height: var(--drawer-content-height-md); + --_drawer-size-width: var(--drawer-content-width-md); + --_drawer-size-height: var(--drawer-content-height-md); --_drawer-padding: var(--drawer-content-padding-md); } [data-drawer-content][data-size='lg'] { - --_drawer-panel-width: var(--drawer-content-width-lg); - --_drawer-panel-height: var(--drawer-content-height-lg); + --_drawer-size-width: var(--drawer-content-width-lg); + --_drawer-size-height: var(--drawer-content-height-lg); --_drawer-padding: var(--drawer-content-padding-lg); } @@ -187,8 +193,8 @@ [data-drawer-content][data-variant='overlay'][data-size='full'] { inset: var(--drawer-content-inset); - width: auto; - height: auto; + width: var(--_drawer-content-width-override, auto); + height: var(--_drawer-content-height-override, auto); } /* ── Variant 'inline' — fixed positioning, no backdrop, lower stack ─────── */ @@ -238,8 +244,8 @@ [data-drawer-content][data-variant='inline'][data-size='full'] { inset: var(--drawer-content-inset); - width: auto; - height: auto; + width: var(--_drawer-content-width-override, auto); + height: var(--_drawer-content-height-override, auto); } /* ── Variant 'persistent' — in flow, no insets ──────────────────────────── */ diff --git a/src/uix/eidos/components/drawer/index.ts b/src/uix/eidos/components/drawer/index.ts index 537af94f4..95ea43773 100644 --- a/src/uix/eidos/components/drawer/index.ts +++ b/src/uix/eidos/components/drawer/index.ts @@ -80,6 +80,7 @@ export type { DrawerHeaderProps as HeaderProps, DrawerFooterProps as FooterProps, DrawerSize, + DrawerCssLength, DrawerVariant, DrawerDirection, DrawerSide diff --git a/src/uix/eidos/components/drawer/types.ts b/src/uix/eidos/components/drawer/types.ts index 647e4f0d9..64387e283 100644 --- a/src/uix/eidos/components/drawer/types.ts +++ b/src/uix/eidos/components/drawer/types.ts @@ -24,6 +24,8 @@ export type DrawerSize = Extract; export type { DrawerVariant, DrawerDirection, DrawerSide }; +export type DrawerCssLength = string | number; + export type DrawerProps = SomaDrawerProviderProps; export type DrawerTriggerProps = SomaDrawerTriggerProps; export type DrawerHandleProps = SomaDrawerHandleProps; @@ -43,6 +45,18 @@ export type DrawerContentProps = SomaDrawerContentProps & { * @default 'md' */ size?: ResponsiveProp; + /** Width override for left/right drawers. Numbers serialize as px. */ + width?: ResponsiveProp; + /** Minimum inline-size constraint for the panel. Numbers serialize as px. */ + minWidth?: ResponsiveProp; + /** Maximum inline-size constraint for the panel. Numbers serialize as px. */ + maxWidth?: ResponsiveProp; + /** Height override for top/bottom drawers. Numbers serialize as px. */ + height?: ResponsiveProp; + /** Minimum block-size constraint for the panel. Numbers serialize as px. */ + minHeight?: ResponsiveProp; + /** Maximum block-size constraint for the panel. Numbers serialize as px. */ + maxHeight?: ResponsiveProp; }; export type DrawerOverlayProps = SomaDrawerOverlayProps & {