Add Eidos popover reference comparison

active-uix
dev 5 months ago
parent 6707034fbd
commit 5c0137e57e

@ -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`

@ -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.

@ -52,5 +52,6 @@ export type {
PopoverCloseProps as CloseProps,
PopoverAnchorProps as AnchorProps,
PopoverOverlayProps as OverlayProps,
PopoverSize
PopoverSize,
PopoverCssLength
} from './types';

@ -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));
</script>
<Popover.Content {...rest} data-size={resolvedSize}>
<Popover.Content {...rest} style={composedStyle} data-size={resolvedSize}>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}

@ -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 ─────────────────────────────

@ -16,6 +16,8 @@ import type {
*/
export type PopoverSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl'>;
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<PopoverSize>;
/**
* 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<boolean>;
/** Inline-size override for the floating panel. Numbers serialize as px. */
width?: ResponsiveProp<PopoverCssLength>;
/** Minimum inline-size constraint. Numbers serialize as px. */
minWidth?: ResponsiveProp<PopoverCssLength>;
/** Maximum inline-size constraint. Numbers serialize as px. */
maxWidth?: ResponsiveProp<PopoverCssLength>;
/** Block-size override for the floating panel. Numbers serialize as px. */
height?: ResponsiveProp<PopoverCssLength>;
/** Minimum block-size constraint. Numbers serialize as px. */
minHeight?: ResponsiveProp<PopoverCssLength>;
/** Maximum block-size constraint. Numbers serialize as px. */
maxHeight?: ResponsiveProp<PopoverCssLength>;
};

Loading…
Cancel
Save

Powered by TurnKey Linux.