Add Eidos dialog reference comparison

active-uix
dev 5 months ago
parent 458996882e
commit 0b399cc6e8

@ -13,6 +13,23 @@ Fecha de corte: 2026-05-17. Rama: `active-uix`.
- `npm run check` -> 0 errores, 0 warnings.
- `node --import tsx/esm scripts/eidos-lint-all.ts` -> 0 invalid, sin drift
hotspots.
- Regla nueva para continuar Eidos componente por componente:
- La primera referencia es Air en la rama anterior
(`glm-5:src/uix/air/components/{name}`) cuando exista.
- Despues se compara con Soma/Morfo actuales y con referentes externos
relevantes (Radix/Radix Themes, Ark UI, Bits UI, shadcn-svelte y React Aria
si aplica).
- Cada componente Eidos debe tener tabla en `components/{name}/README.md`
con gaps y decisiones antes de tocar wrapper/recipe/tokens.
- Objetivo: no dejar Eidos por debajo de Air ni de las plataformas de
referencia en funcionalidades reales.
- Primer componente auditado con el nuevo protocolo:
- `Dialog`: comparado contra Air, Radix, Ark UI, Bits UI y shadcn-svelte.
- Decision: no se crea `Dialog.Positioner`; `Content.position` cubre esa
responsabilidad visual sobre la grid canonica 3x3.
- 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.
- Componentes Eidos nuevos desde Soma:
- `meter`
- `progress`

@ -330,6 +330,28 @@ si un alias publico queda sin consumidor real, el test
---
## Comparativa obligatoria por componente
Ningun componente Eidos se declara cerrado solo por envolver Soma. Antes de
implementar o revisar un componente:
1. Leer Air en la rama anterior (`glm-5:src/uix/air/components/{name}`) cuando
exista. Air es la primera baseline visual.
2. Leer Soma/Morfo actuales para separar comportamiento, ARIA, estado,
traducciones y data-attrs de la superficie visual Eidos.
3. Comparar contra referentes externos relevantes: Radix/Radix Themes, Ark UI,
Bits UI, shadcn-svelte y React Aria cuando aplique.
4. Crear/actualizar `components/{name}/README.md` con tabla de funcionalidades,
gaps y decisiones. Cada `⚠️` / `❌` debe acabar en una decision explicita:
implementar ahora, diferir a v2 o descartar por no pertenecer a Eidos.
5. Solo despues tocar wrapper, recipe o tokens.
El objetivo no es copiar APIs, sino que Eidos no quede por debajo de Air ni de
los referentes en funcionalidades reales. Si una capacidad pertenece a Soma, la
tabla debe decirlo; si es visual, Eidos debe cubrirla o justificar el gap.
---
## Forma legacy (CSS-only, retired)
Hubo una fase anterior cuando eidos sólo emitía CSS (`accordion.css`,

@ -0,0 +1,68 @@
# Dialog
Visual dialog wrapper over Soma's dialog primitive.
## Baseline
Air exposed `Root`, `Trigger`, `Portal`, `Overlay`, `Content`, `Header`,
`Footer`, `Title`, `Description` and `Close`. The only visual prop on
`Content` was responsive `size: 'sm' | 'md' | 'lg' | 'full'`; contextual
motion lived in Air's motion helpers. Eidos keeps the same anatomy under the
disciplined option C shape (`<Dialog>` + attached parts), adds `xl`, panel
positioning and explicit dimension overrides, while Soma owns behavior.
## Comparison
| Feature | Air | Radix | Ark UI | Bits UI | shadcn-svelte | Eidos |
| --- | :--: | :--: | :--: | :--: | :--: | :--: |
| Compound API | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Portal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Overlay / backdrop | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Header / footer visual shells | ✅ | example | example | custom | ✅ | ✅ |
| Responsive visual size | ✅ | ✅ | — | — | class | ✅ |
| Explicit width / height overrides | ❌ | ✅ | — | — | class | ✅ |
| Panel positioning | ❌ | custom CSS | ✅ `Positioner` | custom CSS | class | ✅ |
| Modal / non-modal | ✅ | ✅ | ✅ | ✅ | primitive | ✅ Soma |
| Alert dialog role | ✅ | separate primitive | ✅ | props | separate wrapper | ✅ Soma |
| Focus trap / restore | ✅ | ✅ | ✅ | ✅ | primitive | ✅ Soma |
| Escape / outside policies | ✅ | callbacks | ✅ | ✅ | primitive | ✅ Soma |
| Nested dialogs | ✅ | basic | ✅ | ✅ | primitive | ✅ Soma |
| Force mount for animation | ✅ | ✅ | ✅ | ✅ | primitive | ✅ Soma |
| Scroll lock | ✅ | ✅ | ✅ | ✅ | primitive | ✅ Soma |
## Decisions
- **No `Dialog.Positioner` part**. Ark separates `Positioner`; Eidos exposes
`Content.position` over the canonical 3x3 `Position` grid instead. A new part
would duplicate a visual responsibility already represented by `Content`.
- **Dimension overrides belong to Eidos**. `width`, `minWidth`, `maxWidth`,
`height`, `minHeight` and `maxHeight` are visual-only props on
`Dialog.Content`, serialized as private CSS variables. Soma remains unaware
of them.
- **Behavior stays in Soma**. Modal/non-modal mode, alertdialog role, focus,
dismissal, nesting, scroll lock and force mount are inherited from Soma.
## Eidos API
```svelte
<Dialog bind:open>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay overlayOpacity={0.52} />
<Dialog.Content
size="lg"
position="top-center"
maxWidth="min(90vw, 720px)"
maxHeight="calc(100dvh - 4rem)"
>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
```

@ -15,6 +15,7 @@
* from the morfo runtime.
*/
import { ActiveEidos } from '$uix/eidos';
import { composeInlineStyle } from '$uix/eidos/lib/style';
import * as Dialog from '$soma/components/dialog';
import type { DialogContentProps } from './types';
@ -25,6 +26,13 @@
let {
size = 'md',
position = 'middle-center',
width,
minWidth,
maxWidth,
height,
minHeight,
maxHeight,
style,
children: bodyContent,
...rest
}: DialogContentProps = $props();
@ -34,10 +42,41 @@
const resolvedSize = $derived(eidos.resolve(size, 'md'));
const resolvedPosition = $derived(eidos.resolve(position, 'middle-center'));
const isSheet = $derived(resolvedSize !== 'full' && eidos.isBelow('sm'));
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('--_dialog-content-width-override', serializeLength(eidos.resolve(width))),
dimensionVar('--_dialog-content-min-width-override', serializeLength(eidos.resolve(minWidth))),
dimensionVar('--_dialog-content-max-width-override', serializeLength(eidos.resolve(maxWidth))),
dimensionVar('--_dialog-content-height-override', serializeLength(eidos.resolve(height))),
dimensionVar('--_dialog-content-min-height-override', serializeLength(eidos.resolve(minHeight))),
dimensionVar('--_dialog-content-max-height-override', serializeLength(eidos.resolve(maxHeight)))
]
.filter(Boolean)
.join(' ')
);
const composedStyle = $derived(composeInlineStyle(style, dimensionsStyle));
</script>
<Dialog.Content
{...rest}
style={composedStyle}
data-size={resolvedSize}
data-position={resolvedPosition}
data-sheet={isSheet ? '' : undefined}

@ -96,8 +96,9 @@
/* ── Content ───────────────────────────────────────────────────────────── */
[data-dialog-content] {
--_dialog-width: var(--dialog-content-width-md);
--_dialog-size-width: var(--dialog-content-width-md);
--_dialog-padding: var(--dialog-content-padding-md);
--_dialog-width: var(--_dialog-content-width-override, var(--_dialog-size-width));
position: fixed;
left: 50%;
@ -108,7 +109,11 @@
flex-direction: column;
gap: var(--dialog-stack-gap);
width: min(calc(100vw - var(--dialog-content-width-inset)), var(--_dialog-width));
max-height: var(--dialog-content-max-height);
min-width: var(--_dialog-content-min-width-override, auto);
max-width: var(--_dialog-content-max-width-override, none);
height: var(--_dialog-content-height-override, auto);
min-height: var(--_dialog-content-min-height-override, auto);
max-height: var(--_dialog-content-max-height-override, var(--dialog-content-max-height));
padding: var(--_dialog-padding);
overflow: auto;
border: var(--dialog-content-border-width) solid var(--dialog-content-border);
@ -221,22 +226,22 @@
/* ── Sizing ────────────────────────────────────────────────────────────── */
[data-dialog-content][data-size='sm'] {
--_dialog-width: var(--dialog-content-width-sm);
--_dialog-size-width: var(--dialog-content-width-sm);
--_dialog-padding: var(--dialog-content-padding-sm);
}
[data-dialog-content][data-size='md'] {
--_dialog-width: var(--dialog-content-width-md);
--_dialog-size-width: var(--dialog-content-width-md);
--_dialog-padding: var(--dialog-content-padding-md);
}
[data-dialog-content][data-size='lg'] {
--_dialog-width: var(--dialog-content-width-lg);
--_dialog-size-width: var(--dialog-content-width-lg);
--_dialog-padding: var(--dialog-content-padding-lg);
}
[data-dialog-content][data-size='xl'] {
--_dialog-width: var(--dialog-content-width-xl);
--_dialog-size-width: var(--dialog-content-width-xl);
--_dialog-padding: var(--dialog-content-padding-full);
border-radius: var(--dialog-content-full-radius);
}
@ -244,7 +249,7 @@
[data-dialog-content][data-size='full'] {
inset: var(--dialog-content-full-inset);
translate: none;
width: auto;
width: var(--_dialog-content-width-override, auto);
border-radius: var(--dialog-content-full-radius);
--_dialog-padding: var(--dialog-content-padding-full);
}

@ -66,5 +66,6 @@ export type {
DialogFooterProps as FooterProps,
DialogSize,
DialogPosition,
DialogClosePosition
DialogClosePosition,
DialogCssLength
} from './types';

@ -26,6 +26,7 @@ export type DialogSize = Extract<Size, 'sm' | 'md' | 'lg' | 'xl' | 'full'>;
* blocking modal. Default `'middle-center'`.
*/
export type DialogPosition = Position;
export type DialogCssLength = string | number;
export type DialogProps = SomaDialogProviderProps;
export type DialogTriggerProps = SomaDialogTriggerProps;
@ -55,6 +56,23 @@ export type DialogContentProps = SomaDialogContentProps & {
* @default 'middle-center'
*/
position?: ResponsiveProp<DialogPosition>;
/**
* Explicit content width override. Number values are serialized as px;
* strings accept any CSS length/expression (`'42rem'`, `'min(90vw, 720px)'`).
* This mirrors the Radix Themes dimension escape hatch without moving
* layout ownership to Soma.
*/
width?: ResponsiveProp<DialogCssLength>;
/** Minimum content width override. Number values are serialized as px. */
minWidth?: ResponsiveProp<DialogCssLength>;
/** Maximum content width override. Number values are serialized as px. */
maxWidth?: ResponsiveProp<DialogCssLength>;
/** Explicit content height override. Number values are serialized as px. */
height?: ResponsiveProp<DialogCssLength>;
/** Minimum content height override. Number values are serialized as px. */
minHeight?: ResponsiveProp<DialogCssLength>;
/** Maximum content height override. Number values are serialized as px. */
maxHeight?: ResponsiveProp<DialogCssLength>;
};
export type DialogOverlayProps = SomaDialogOverlayProps & {

Loading…
Cancel
Save

Powered by TurnKey Linux.