From 5c0137e57ed363ca90e9dbd10b996e9d2991eed6 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 17 May 2026 17:45:16 +0200 Subject: [PATCH] Add Eidos popover reference comparison --- continue.md | 13 +++++ src/uix/eidos/components/popover/README.md | 50 ++++++++++++++++++ src/uix/eidos/components/popover/index.ts | 3 +- .../components/popover/popover-content.svelte | 51 ++++++++++++++++++- src/uix/eidos/components/popover/popover.css | 26 ++++++++-- src/uix/eidos/components/popover/types.ts | 19 +++++++ 6 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 src/uix/eidos/components/popover/README.md diff --git a/continue.md b/continue.md index 388498367..d6001f477 100644 --- a/continue.md +++ b/continue.md @@ -41,6 +41,19 @@ Fecha de corte: 2026-05-17. Rama: `active-uix`. `height`, `minHeight`, `maxHeight` en `Drawer.Content`, serializados como variables privadas del recipe. El size canonico `sm/md/lg/full` sigue como preset principal. +- Tercer componente auditado con el nuevo protocolo: + - `Popover`: Air no tenia equivalente en `glm-5`, por tanto la referencia + local pasa a ser Soma/Morfo y la comparativa externa Radix, Ark UI, Bits UI + y shadcn-svelte. + - Decision: no se crea `Popover.Positioner`; la posicion pertenece a Soma + Floating y Eidos solo consume `data-side`, `data-align` y variables CSS. + - Decision: no se crean `Popover.Title`/`Description` hasta que exista caso + real; Popover suele contener UI arbitraria y el etiquetado puede vivir en + contenido consumidor. + - Decision: se anaden `matchAnchorWidth`, `width`, `minWidth`, `maxWidth`, + `height`, `minHeight`, `maxHeight` en `Popover.Content`, serializados como + variables privadas. La receta usa transform-origin y available-height de + Soma Floating. - Componentes Eidos nuevos desde Soma: - `meter` - `progress` diff --git a/src/uix/eidos/components/popover/README.md b/src/uix/eidos/components/popover/README.md new file mode 100644 index 000000000..12d805771 --- /dev/null +++ b/src/uix/eidos/components/popover/README.md @@ -0,0 +1,50 @@ +# Eidos Popover + +Fecha de revision: 2026-05-17. + +## Baseline + +Air no tenia `popover` en `glm-5:src/uix/air/components`; este componente no es +un port 1:1 de Air, sino una envoltura visual sobre la capacidad actual de +Soma/Morfo. + +## Comparativa + +| Referencia | Superficie relevante | Estado Eidos | +| --- | --- | --- | +| Air `glm-5` | No existe `popover` en Air. | Sin baseline Air. La referencia local es Soma/Morfo. | +| Soma actual | `Provider`, `Trigger`, `Content`, `Arrow`, `Close`, `Anchor`, `Overlay`; `modal`, hover open, focus/scroll/dismissal, side/align/offset/collision, custom anchor, Floating CSS vars. | Cubierto por passthrough. Eidos no duplica posicionamiento ni estado. | +| Radix Popover | Controlled/uncontrolled, `Trigger`, `Anchor`, `Portal`, `Content`, `Close`, `Arrow`; side/align/collision, modal/no-modal, focus managed, CSS vars para transform-origin, available width/height y trigger width. | Cubierto en comportamiento por Soma. Eidos consume `data-side` y ahora expone medidas visuales + `matchAnchorWidth`. | +| Ark UI Popover | `Root`, `Trigger`, `Anchor`, `Positioner`, `Arrow/ArrowTip`, `Content`, `Title`, `Description`, `CloseTrigger`; modal, close behavior, `positioning.sameWidth`, nested/multiple triggers, lazy mount. | Cubierto salvo `Positioner`/Title/Description como partes propias. `Positioner` queda internalizado en Soma Floating; Title/Description se dejan como contenido del usuario. | +| Bits UI / shadcn-svelte | Compound con `Root`, `Trigger`, `Portal`, `Overlay`, `Content`, `Close`, `Arrow`; hover open; `forceMount`; floating CSS vars; shadcn usa width por clases (`w-80`). | Cubierto. Eidos mantiene API compound y anade width constraints sin depender de clases externas. | + +Fuentes externas consultadas: + +- Radix Popover: https://www.radix-ui.com/primitives/docs/components/popover +- Ark UI Popover: https://ark-ui.com/docs/components/popover +- Bits UI Popover: https://bits-ui.com/docs/components/popover +- shadcn-svelte Popover: https://www.shadcn-svelte.com/docs/components/popover + +## Decisiones + +- No se crea `Popover.Positioner`. La posicion pertenece a Soma Floating; Eidos + consume `data-side`, `data-align` y las variables CSS publicadas por Soma. +- No se crean `Popover.Title` ni `Popover.Description` ahora. A diferencia de + Dialog/Drawer, Popover suele contener UI arbitraria y las referencias no + exigen esas partes para accesibilidad si el consumidor etiqueta su contenido. +- `Popover.Content` mantiene `size` canonico (`sm/md/lg/xl`) y anade + `matchAnchorWidth`, `width`, `minWidth`, `maxWidth`, `height`, `minHeight`, + `maxHeight` como props visuales responsivas. +- Las medidas se serializan como variables privadas del recipe + (`--_popover-content-*`); el contrato publico de tokens sigue siendo el de la + recipe. +- La receta usa `--soma-popover-content-transform-origin` para animaciones + origin-aware y `--soma-popover-content-available-height` como max-height por + defecto cuando Floating ya lo conoce. + +## Pendiente deliberado + +- Revisar `Popover.Title`/`Description` si aparece un caso real que necesite + partes canonicas para etiquetado consistente. +- Evaluar multiple trigger value solo si Soma decide modelarlo; Eidos no debe + inventar estado de trigger por encima de Soma. diff --git a/src/uix/eidos/components/popover/index.ts b/src/uix/eidos/components/popover/index.ts index b543a03a6..0bc07a05e 100644 --- a/src/uix/eidos/components/popover/index.ts +++ b/src/uix/eidos/components/popover/index.ts @@ -52,5 +52,6 @@ export type { PopoverCloseProps as CloseProps, PopoverAnchorProps as AnchorProps, PopoverOverlayProps as OverlayProps, - PopoverSize + PopoverSize, + PopoverCssLength } from './types'; diff --git a/src/uix/eidos/components/popover/popover-content.svelte b/src/uix/eidos/components/popover/popover-content.svelte index 69cafe39b..8e1537c1d 100644 --- a/src/uix/eidos/components/popover/popover-content.svelte +++ b/src/uix/eidos/components/popover/popover-content.svelte @@ -9,19 +9,66 @@ * `data-last-action` continue to come from the morfo runtime. */ import { ActiveEidos } from '$uix/eidos'; + import { composeInlineStyle } from '$uix/eidos/lib/style'; import * as Popover from '$soma/components/popover'; import type { PopoverContentProps } from './types'; // `children` is renamed so the inner `{#snippet children}` block // can accept and forward Soma's `{ open }` snippet args without // shadowing the prop. - let { size = 'md', children: bodyContent, ...rest }: PopoverContentProps = $props(); + let { + size = 'md', + matchAnchorWidth = false, + width, + minWidth, + maxWidth, + height, + minHeight, + maxHeight, + style, + children: bodyContent, + ...rest + }: PopoverContentProps = $props(); const eidos = ActiveEidos.require(); const resolvedSize = $derived(eidos.resolve(size, 'md')); + const resolvedMatchAnchorWidth = $derived(eidos.resolve(matchAnchorWidth, false)); + + 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( + [ + resolvedMatchAnchorWidth + ? '--_popover-match-anchor-width: var(--soma-popover-anchor-width);' + : undefined, + dimensionVar('--_popover-content-width-override', serializeLength(eidos.resolve(width))), + dimensionVar('--_popover-content-min-width-override', serializeLength(eidos.resolve(minWidth))), + dimensionVar('--_popover-content-max-width-override', serializeLength(eidos.resolve(maxWidth))), + dimensionVar('--_popover-content-height-override', serializeLength(eidos.resolve(height))), + dimensionVar('--_popover-content-min-height-override', serializeLength(eidos.resolve(minHeight))), + dimensionVar('--_popover-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/popover/popover.css b/src/uix/eidos/components/popover/popover.css index a1920c373..2d118748c 100644 --- a/src/uix/eidos/components/popover/popover.css +++ b/src/uix/eidos/components/popover/popover.css @@ -75,18 +75,34 @@ [data-popover-content] { --_popover-py: var(--popover-padding-y-md); --_popover-px: var(--popover-padding-x-md); - --_popover-width: var(--popover-min-width-md); + --_popover-base-width: var(--popover-min-width-md); + --_popover-width: var( + --_popover-content-width-override, + var(--_popover-match-anchor-width, var(--_popover-base-width)) + ); + --_popover-max-width: var(--_popover-content-max-width-override, var(--popover-max-width)); + --_popover-max-height: var( + --_popover-content-max-height-override, + var(--soma-popover-content-available-height, calc(100dvh - var(--space-4))) + ); display: grid; box-sizing: border-box; gap: var(--popover-gap); - inline-size: min(var(--_popover-width), var(--popover-max-width), calc(100vw - var(--space-4))); + inline-size: min(var(--_popover-width), var(--_popover-max-width), calc(100vw - var(--space-4))); + min-inline-size: var(--_popover-content-min-width-override, auto); + max-inline-size: min(var(--_popover-max-width), calc(100vw - var(--space-4))); + block-size: var(--_popover-content-height-override, auto); + min-block-size: var(--_popover-content-min-height-override, auto); + max-block-size: var(--_popover-max-height); padding: var(--_popover-py) var(--_popover-px); + overflow: auto; border: var(--popover-border-width) solid var(--popover-border); border-radius: var(--popover-radius); background: var(--popover-bg); color: var(--popover-color); box-shadow: var(--popover-shadow); + transform-origin: var(--soma-popover-content-transform-origin, center); font-family: var(--popover-font-family); font-size: var(--popover-font-size); font-weight: var(--popover-font-weight); @@ -97,19 +113,19 @@ [data-popover-content][data-size='sm'] { --_popover-py: var(--popover-padding-y-sm); --_popover-px: var(--popover-padding-x-sm); - --_popover-width: var(--popover-min-width-sm); + --_popover-base-width: var(--popover-min-width-sm); } [data-popover-content][data-size='lg'] { --_popover-py: var(--popover-padding-y-lg); --_popover-px: var(--popover-padding-x-lg); - --_popover-width: var(--popover-min-width-lg); + --_popover-base-width: var(--popover-min-width-lg); } [data-popover-content][data-size='xl'] { --_popover-py: var(--popover-padding-y-xl); --_popover-px: var(--popover-padding-x-xl); - --_popover-width: var(--popover-min-width-xl); + --_popover-base-width: var(--popover-min-width-xl); } /* ── Content entrance/exit per anchored side ───────────────────────────── diff --git a/src/uix/eidos/components/popover/types.ts b/src/uix/eidos/components/popover/types.ts index cc350322a..a4e80fb21 100644 --- a/src/uix/eidos/components/popover/types.ts +++ b/src/uix/eidos/components/popover/types.ts @@ -16,6 +16,8 @@ import type { */ export type PopoverSize = Extract; +export type PopoverCssLength = string | number; + export type PopoverProps = SomaPopoverProviderProps; export type PopoverTriggerProps = SomaPopoverTriggerProps; export type PopoverArrowProps = SomaPopoverArrowProps; @@ -33,4 +35,21 @@ export type PopoverContentProps = SomaPopoverContentProps & { * @default 'md' */ size?: ResponsiveProp; + /** + * Match the floating panel inline size to the active anchor / trigger width. + * Mirrors Ark's same-width pattern and Radix/Bits anchor-width CSS vars. + */ + matchAnchorWidth?: ResponsiveProp; + /** Inline-size override for the floating panel. Numbers serialize as px. */ + width?: ResponsiveProp; + /** Minimum inline-size constraint. Numbers serialize as px. */ + minWidth?: ResponsiveProp; + /** Maximum inline-size constraint. Numbers serialize as px. */ + maxWidth?: ResponsiveProp; + /** Block-size override for the floating panel. Numbers serialize as px. */ + height?: ResponsiveProp; + /** Minimum block-size constraint. Numbers serialize as px. */ + minHeight?: ResponsiveProp; + /** Maximum block-size constraint. Numbers serialize as px. */ + maxHeight?: ResponsiveProp; };