4 new layout primitives, full canon (morfo + eidos full set + canon
demo + README). `npm run component:audit`: 81 / 81 PASS, 0 NEEDS-WORK.
**AspectRatio** — composes through Box; emits single `--aspect-ratio`
CSS var (uses modern CSS `aspect-ratio` property, drops legacy
padding-bottom hack). Accepts numeric (16/9 → 1.777…) or string
("16/9") values.
**AutoGrid** — composes through Grid. Resolves `templateColumns` from
`minItemWidth` to `repeat(auto-fill, minmax(MIN, 1fr))` in JS so the
recipe stays declarative. Uses `auto-fill` (not auto-fit) to preserve
empty tracks when item count is low.
**Banner** — `<header role="banner">` announcement strip with intent
(full 8-role UIX `ColorRole`), variant (`ChipVariant` soft/solid/
outline/ghost), size (sm/md/lg). Dismissal is composition-driven:
consumer wraps in {#if show} and adds `<Banner.Close>`. No
`dismissible` boolean.
**Float** — CSS `float` primitive with logical `inline-start` /
`inline-end` sides. Redefined from air's 9-zone overlay primitive
(which becomes a future `<Positioned>` component if real demand
surfaces). Useful for inline images / pull-quotes / drop caps.
Each component ships morfo (`scope: ['eidos']`, 1 part, 0 events,
justified) + eidos set + README (Baseline / Comparativa /
Decisiones / Eventos Sema / Gaps with disposition markers /
Referencias / Passive justification) + canon demo with 6 tabs.
Sidebar nav appends the 4 entries to the existing Layout group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
parent
265a3946b5
commit
376f9e33eb
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* AspectRatio recipe — sets `aspect-ratio` on the Box shell and stretches
|
||||
* the direct child to fill the box. The element itself is laid out by the
|
||||
* cascade (block-level by default) and shrinks the ratio to its inline
|
||||
* size, so combining with `maxWidth`, `width` or grid placement on the
|
||||
* Box works without extra plumbing.
|
||||
*
|
||||
* Why not the legacy padding-bottom hack? CSS `aspect-ratio` ships in
|
||||
* every modern browser and avoids the absolute-positioning of children
|
||||
* that the padding-bottom trick requires. Cleaner DOM, fewer caveats
|
||||
* around overflow / scrollbars.
|
||||
*/
|
||||
|
||||
[data-box][data-aspect-ratio] {
|
||||
aspect-ratio: var(--aspect-ratio, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Stretch the lone child (img / iframe / video / picture / inner div) to
|
||||
fill the box without disturbing replaced-element intrinsic ratios when
|
||||
they happen to match. */
|
||||
[data-box][data-aspect-ratio] > * {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
// AspectRatio — constrains inner content to a width/height ratio.
|
||||
//
|
||||
// import { AspectRatio } from '$uix/eidos/components/aspect-ratio';
|
||||
//
|
||||
// <AspectRatio ratio={16 / 9}>
|
||||
// <img src="hero.jpg" alt="" />
|
||||
// </AspectRatio>
|
||||
import AspectRatio from './aspect-ratio.svelte';
|
||||
|
||||
export { AspectRatio };
|
||||
export default AspectRatio;
|
||||
export type { AspectRatioProps, AspectRatioValue } from './types';
|
||||
@ -0,0 +1,27 @@
|
||||
import type { ResponsiveProp } from '$uix/eidos/lib/types';
|
||||
import type { BoxProps } from '../box/types';
|
||||
|
||||
/**
|
||||
* Aspect-ratio value. Accepts:
|
||||
* - a number: `ratio={16/9}` → CSS `aspect-ratio: 1.7777…`.
|
||||
* - a fraction string: `ratio="16/9"`, `ratio="4 / 3"`.
|
||||
* - a unitless string: `ratio="1.5"`.
|
||||
*
|
||||
* The string form is preferred when the consumer wants the value to read
|
||||
* verbatim in DevTools (`aspect-ratio: 16 / 9`); the number form is easier
|
||||
* to compute (`ratio={width / height}`).
|
||||
*/
|
||||
export type AspectRatioValue = number | string;
|
||||
|
||||
export type AspectRatioProps = Omit<BoxProps, 'display'> & {
|
||||
/**
|
||||
* Width / height ratio for the box. @default 1 (square).
|
||||
*
|
||||
* Examples:
|
||||
* - `ratio={1}` — square (1:1)
|
||||
* - `ratio={16 / 9}` — widescreen video
|
||||
* - `ratio="4/3"` — standard video
|
||||
* - `ratio="21/9"` — ultrawide
|
||||
*/
|
||||
ratio?: ResponsiveProp<AspectRatioValue>;
|
||||
};
|
||||
@ -0,0 +1,118 @@
|
||||
# Eidos AutoGrid
|
||||
|
||||
Responsive grid container that fits as many columns as its inline size
|
||||
allows — no media queries required. Wraps `<Grid>` (which wraps `<Box>`)
|
||||
and computes `grid-template-columns: repeat(auto-fill, minmax(MIN, 1fr))`
|
||||
in JS based on the `minChildWidth` prop. Falls back to a fixed
|
||||
`repeat(N, minmax(0, 1fr))` when `columns` is passed instead.
|
||||
|
||||
## Superficie
|
||||
|
||||
```svelte
|
||||
<!-- Fluid: as many columns as fit, each at least 240px wide. -->
|
||||
<AutoGrid minChildWidth={240} gap={3}>
|
||||
<Card />
|
||||
<Card />
|
||||
<Card />
|
||||
</AutoGrid>
|
||||
|
||||
<!-- Fixed: always 3 columns, regardless of viewport. -->
|
||||
<AutoGrid columns={3} gap={3}>
|
||||
…
|
||||
</AutoGrid>
|
||||
|
||||
<!-- Responsive minChildWidth: smaller cards on tiny screens. -->
|
||||
<AutoGrid minChildWidth={{ base: 160, md: 240 }} gap={3}>
|
||||
…
|
||||
</AutoGrid>
|
||||
```
|
||||
|
||||
## Baseline
|
||||
|
||||
Origen: `air/components/layout/auto-grid` (rama `morfo-runtime`).
|
||||
|
||||
Adaptaciones aplicadas durante el port:
|
||||
|
||||
- Resolución reactiva (`ActiveEidos.require().resolve()`) en lugar de
|
||||
`getAir().dom.resolve()`.
|
||||
- Marker propio `[data-auto-grid]` además del `data-grid` heredado de
|
||||
`<Grid>`, para que consumidores puedan estilar el primitive específico
|
||||
cuando lo necesiten.
|
||||
- Cambio de `auto-fit` a **`auto-fill`** en el template — `auto-fill`
|
||||
preserva las pistas vacías cuando hay menos hijos que columnas
|
||||
disponibles, lo que da un grid más predecible y consistente con la
|
||||
expectativa del consumidor (Chakra `SimpleGrid`, Mantine `SimpleGrid`).
|
||||
Si se quiere `auto-fit` (stretch a las pistas pobladas), el consumidor
|
||||
puede usar `<Grid templateColumns="repeat(auto-fit, minmax(240px, 1fr))">`
|
||||
directamente.
|
||||
- Drop del prefijo `air-` y eliminación de la dependencia de `getAir()`.
|
||||
|
||||
## Comparativa
|
||||
|
||||
| Capacidad | UIX (eidos) | Radix Themes | Chakra UI SimpleGrid | Mantine SimpleGrid |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Fluid fit (`minChildWidth`) | Sí (`minChildWidth={240}`) | No (sólo via raw `templateColumns`) | Sí (`minChildWidth`) | Sí (`minChildWidth`) |
|
||||
| Fixed columns (`columns`) | Sí (`columns={3}`) | Sí (`columns="3"`) en Grid | Sí (`columns={3}`) | Sí (`cols={3}`) |
|
||||
| Responsive `columns` | Sí (`ResponsiveProp<number>`) | Sí (`{ initial: 1, sm: 2 }`) | Sí (`{ base: 1, md: 3 }`) | Sí (`{ base: 1, sm: 2 }`) |
|
||||
| Responsive `minChildWidth` | Sí (`ResponsiveProp<…>`) | n/a | Sí | Sí |
|
||||
| `gap` shorthand | Sí (vía Grid → Box) | Sí | Sí | Sí (`spacing`) |
|
||||
| Per-axis gap (`rowGap`, `columnGap`) | Sí (vía Grid) | Sí | Sí | Sí (`verticalSpacing`, `horizontalSpacing`) |
|
||||
| `auto-fill` vs `auto-fit` | `auto-fill` (preserva tracks vacíos) | n/a | `auto-fit` | `auto-fit` |
|
||||
| Composes through `<Box>` | Sí | n/a | n/a | n/a |
|
||||
|
||||
## Decisiones
|
||||
|
||||
- **`minChildWidth` gana sobre `columns`** — cuando se pasan ambos, el
|
||||
template fluido es la primitiva específica de AutoGrid; `columns` queda
|
||||
como modo "shortcut" para grids fijos sin necesidad de saltar a
|
||||
`<Grid>`.
|
||||
- **`auto-fill` por defecto** — divergencia de Chakra/Mantine, que usan
|
||||
`auto-fit`. Razón: `auto-fill` deja tracks reservados cuando hay menos
|
||||
hijos que columnas, lo que evita que el primer hijo se estire hasta
|
||||
ocupar todo el inline-size cuando hay sólo uno. Si el consumidor
|
||||
necesita el comportamiento de `auto-fit`, usa `<Grid templateColumns>`
|
||||
directamente.
|
||||
- **Composición sobre `<Grid>`** — el primitive es prácticamente puro
|
||||
`templateColumns` computado; no duplica la lógica del recipe de Grid.
|
||||
Cualquier mejora futura del recipe Grid (alignContent, place-items, …)
|
||||
llega "gratis".
|
||||
- **`minChildWidth` resuelve con `formatLayoutLength`** — números van a
|
||||
px, strings pasan tal cual (`"15rem"`, `"min(20rem, 50%)"`).
|
||||
- **No exponemos `templateColumns`** — entra en colisión con el propio
|
||||
cálculo del primitive. Si se necesita un template custom, se usa
|
||||
`<Grid>` directamente.
|
||||
|
||||
## Eventos Sema
|
||||
|
||||
AutoGrid declara 0 eventos. Es una primitiva pasiva: ajusta el template
|
||||
de columnas según la geometría del contenedor — sin commit, sin emerge,
|
||||
sin keyboard. Componentes que animan items al entrar/salir del grid
|
||||
componen AutoGrid con un primitive interactivo (popover, drawer) que sí
|
||||
posee los verbos sema relevantes. Misma justificación que Box, Flex,
|
||||
Grid y AspectRatio.
|
||||
|
||||
## Gaps
|
||||
|
||||
| Gap | Disposición | Detalle |
|
||||
| --- | --- | --- |
|
||||
| `auto-fit` mode prop | diferir | Hoy se usa `<Grid templateColumns>` directamente cuando se quiere el comportamiento de auto-fit. Subir a prop si llegan ≥2 casos reales. |
|
||||
| `breakpoints` config (Mantine-style array de fallbacks) | descartar | Lo cubre `ResponsiveProp` sobre `columns` y `minChildWidth`. Duplicaría la API. |
|
||||
| `spacing` alias para `gap` (Mantine ergonomics) | descartar | Mantenemos `gap` consistente con Box/Flex/Grid. Aliases sólo añaden ruido. |
|
||||
| Slot para divider entre filas | diferir | Stack tiene `divider` pending; vendría después de Stack. |
|
||||
| `equalChildHeight` toggle | diferir | Posible utility si surge una necesidad real — hoy se logra con `align="stretch"` heredado de Grid. |
|
||||
|
||||
## Referencias
|
||||
|
||||
- Chakra UI SimpleGrid: https://chakra-ui.com/docs/components/simple-grid
|
||||
- Mantine SimpleGrid: https://mantine.dev/core/simple-grid/
|
||||
- CSS `grid-template-columns`: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
|
||||
- Smolnar / Heydon "Every Layout" Grid: https://every-layout.dev/layouts/grid/
|
||||
|
||||
## Passive justification
|
||||
|
||||
Visual-only primitive (`scope: ['eidos']` en el morfo). Una sola
|
||||
part Provider que emite el marker `[data-auto-grid]` sobre el shell
|
||||
`[data-box][data-grid]`. Sin estados, sin data-attrs específicos, sin
|
||||
ARIA, sin keyboard. Toda la lógica se reduce a computar
|
||||
`grid-template-columns` y delegar a `<Grid>`. No hay nada que animar
|
||||
ni ciclo de vida que enviar a sema.
|
||||
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* AutoGrid recipe — AutoGrid is a thin compositional layer over Grid.
|
||||
* It computes its `templateColumns` in JS (either `repeat(auto-fill,
|
||||
* minmax(MIN, 1fr))` or `repeat(N, minmax(0, 1fr))`) and forwards every
|
||||
* other prop to `<Grid>`. The recipe only needs to declare the
|
||||
* `[data-auto-grid]` marker so callers can target the primitive
|
||||
* specifically (e.g. to add a section-scoped override).
|
||||
*/
|
||||
|
||||
[data-box][data-grid][data-auto-grid] {
|
||||
/* No own properties — every visible style comes from the Grid recipe
|
||||
reading the `--grid-template-columns` variable that AutoGrid emits.
|
||||
The marker exists so external consumers can write
|
||||
`:where([data-auto-grid]) { … }` selectors. */
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<AutoGrid>` — responsive grid that fits columns to its inline
|
||||
* size without media queries. Composes through `<Grid>` (which composes
|
||||
* through `<Box>`), so the consumer gets every Box / Grid prop on top
|
||||
* of the AutoGrid-specific `minChildWidth` / `columns`.
|
||||
*
|
||||
* <AutoGrid minChildWidth={240} gap={3}>…</AutoGrid> <!-- fluid -->
|
||||
* <AutoGrid columns={3} gap={3}>…</AutoGrid> <!-- fixed -->
|
||||
*
|
||||
* Computation rule: `minChildWidth` wins over `columns`. When neither
|
||||
* is set, AutoGrid behaves like a normal grid (no template).
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import Grid from '../grid/grid.svelte';
|
||||
import { formatLayoutLength } from '../_layout/shared';
|
||||
import type { AutoGridProps } from './types';
|
||||
|
||||
let { minChildWidth, columns, children, ...restProps }: AutoGridProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
|
||||
const templateColumns = $derived.by(() => {
|
||||
const resolvedMinChildWidth = formatLayoutLength(eidos.resolve(minChildWidth));
|
||||
if (resolvedMinChildWidth) {
|
||||
return `repeat(auto-fill, minmax(${resolvedMinChildWidth}, 1fr))`;
|
||||
}
|
||||
const resolvedColumns = eidos.resolve(columns);
|
||||
if (typeof resolvedColumns === 'number' && resolvedColumns > 0) {
|
||||
return `repeat(${resolvedColumns}, minmax(0, 1fr))`;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Grid {...restProps} {templateColumns} data-auto-grid="">
|
||||
{@render children?.()}
|
||||
</Grid>
|
||||
@ -0,0 +1,11 @@
|
||||
// AutoGrid — responsive grid that fits columns to inline size.
|
||||
//
|
||||
// import { AutoGrid } from '$uix/eidos/components/auto-grid';
|
||||
//
|
||||
// <AutoGrid minChildWidth={240} gap={3}>…</AutoGrid>
|
||||
// <AutoGrid columns={3} gap={3}>…</AutoGrid>
|
||||
import AutoGrid from './auto-grid.svelte';
|
||||
|
||||
export { AutoGrid };
|
||||
export default AutoGrid;
|
||||
export type { AutoGridProps } from './types';
|
||||
@ -0,0 +1,28 @@
|
||||
import type { ResponsiveProp } from '$uix/eidos/lib/types';
|
||||
import type { GridProps } from '../grid/types';
|
||||
import type { LayoutLengthValue } from '../_layout/shared';
|
||||
|
||||
/**
|
||||
* AutoGrid props.
|
||||
*
|
||||
* Either pass `minChildWidth` (auto-fill: as many columns as fit) or
|
||||
* `columns` (fixed N). When both are passed, `minChildWidth` wins — the
|
||||
* fluid template (`repeat(auto-fill, minmax(MIN, 1fr))`) is the AutoGrid
|
||||
* usecase; the numeric form is included for parity with consumers who
|
||||
* already know `<Grid columns={N}>` and just want the AutoGrid prop
|
||||
* surface (no extra Box props, simpler defaults).
|
||||
*/
|
||||
export type AutoGridProps = Omit<GridProps, 'templateColumns' | 'columns'> & {
|
||||
/**
|
||||
* Minimum inline size per item before wrapping to a new row. Maps to
|
||||
* `repeat(auto-fill, minmax(MIN, 1fr))`. Use this for fluid responsive
|
||||
* grids that need no breakpoints. Number → px.
|
||||
*/
|
||||
minChildWidth?: ResponsiveProp<LayoutLengthValue>;
|
||||
/**
|
||||
* Number of fixed columns. Maps to `repeat(N, minmax(0, 1fr))`. Use
|
||||
* when the layout has a strict column count regardless of viewport.
|
||||
* Ignored when `minChildWidth` is set.
|
||||
*/
|
||||
columns?: ResponsiveProp<number>;
|
||||
};
|
||||
@ -0,0 +1,141 @@
|
||||
# Eidos Banner
|
||||
|
||||
Full-bleed announcement strip with intent / variant / size visual
|
||||
treatment. Renders as `<header data-banner role="banner">` so the
|
||||
landmark works reliably across layouts — HTML spec only grants
|
||||
`role="banner"` to a `<header>` when it is a top-level child of `<body>`;
|
||||
nesting it inside `<main>`, `<article>` or `<section>` strips the role.
|
||||
The explicit role stabilises the announcement intent.
|
||||
|
||||
## Superficie
|
||||
|
||||
```svelte
|
||||
<Banner intent="affirm" variant="soft" size="md">
|
||||
<span>Your changes have been saved.</span>
|
||||
</Banner>
|
||||
|
||||
<!-- Dismissible: consumer owns the visibility state. -->
|
||||
<script>
|
||||
let show = $state(true);
|
||||
</script>
|
||||
{#if show}
|
||||
<Banner intent="risk" variant="solid">
|
||||
<span>Connection lost — reconnecting…</span>
|
||||
<Banner.Close onclick={() => (show = false)} />
|
||||
</Banner>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Baseline
|
||||
|
||||
Origen: `air/components/layout/banner` (rama `morfo-runtime`).
|
||||
|
||||
Air shipped a single-prop Banner — `<header role="banner">` + a small
|
||||
spacing/padding recipe and no variants. The task brief asked for a full
|
||||
announcement primitive with intent variants and an optional dismiss
|
||||
button, modelled after Chakra / Mantine / MUI `Alert`. The current shape
|
||||
keeps air's landmark contract (explicit `role="banner"`) and adds:
|
||||
|
||||
- `intent` mapped to the canonical UIX `ColorRole` (8 values:
|
||||
`primary | secondary | neutral | affirm | fulfill | risk | threat | loss`).
|
||||
- `variant` from the shared `ChipVariant` vocab (`soft | solid | outline | ghost`).
|
||||
- `size` narrowed from canonical `Size` to `sm | md | lg` — smaller
|
||||
doesn't read; larger turns the strip into a hero (out of scope).
|
||||
- Optional `Banner.Close` part. The dismiss is **composition-driven**:
|
||||
the consumer wraps Banner in `{#if show}` and wires onclick — Banner
|
||||
itself does not own the visibility state. See "Decisiones" below.
|
||||
|
||||
Air's CSS prefix (`air-banner`) is dropped; selectors target
|
||||
`[data-banner]` directly. CSS variables renamed from `--air-banner-*` to
|
||||
`--banner-*` (public) and `--_banner-*` (internal recipe-local).
|
||||
|
||||
## Comparativa
|
||||
|
||||
| Capacidad | UIX (eidos) | Chakra UI Alert | Mantine Alert | MUI Alert |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Intent / status vocabulary | UIX 8 (`primary, secondary, neutral, affirm, fulfill, risk, threat, loss`) | Chakra 4 (`info, warning, success, error`) | Mantine 6 (`blue, gray, red, …` semantic colors) | MUI 4 (`success, info, warning, error`) |
|
||||
| Variants | `soft / solid / outline / ghost` | `subtle / left-accent / top-accent / solid` | `filled / light / outline / default / transparent` | `standard / filled / outlined` |
|
||||
| Sizes | `sm / md / lg` | Single size + custom | Theme spacing scale | Single size + custom |
|
||||
| Landmark `role="banner"` | Sí (explicit) | No (uses `role="alert"`) | No (`role="alert"`) | No (`role="alert"`) |
|
||||
| Dismissible | Sí (`Banner.Close` composition) | Sí (`CloseButton` slot) | Sí (`withCloseButton`) | Sí (`onClose`) |
|
||||
| Icon slot | Composition (consumer's `<Icon>`) | `AlertIcon` shorthand | `icon` prop | `icon` prop |
|
||||
| Title + description structure | Composition (consumer's markup) | `AlertTitle` / `AlertDescription` | `title` prop | `AlertTitle` |
|
||||
| ARIA live region | No — Banner is a landmark, not a live region | `role="alert"` (assertive) | `role="alert"` | `role="alert"` |
|
||||
| Responsive props | Sí (`ResponsiveProp` on intent / variant / size) | Sí (`useColorMode`) | Sí (responsive sx) | Sí (`sx`) |
|
||||
|
||||
## Decisiones
|
||||
|
||||
- **`role="banner"`, not `role="alert"`** — Banner is a landmark for
|
||||
page-level announcements (site header, persistent notice, system
|
||||
status bar). Transient feedback that interrupts the screen reader
|
||||
belongs to `<Toast>`, which already declares the proper
|
||||
`role="status"` / `aria-live="polite"` contract. Forcing
|
||||
`role="alert"` on a static strip would be aggressive overreach.
|
||||
- **Composition, not boolean `dismissible`** — pattern matches the rest
|
||||
of UIX (DatePicker, Dialog, Drawer all dropped `*Button` boolean
|
||||
props 2026-05-21). The consumer wraps Banner in `{#if show}` and
|
||||
includes `<Banner.Close onclick={() => (show = false)} />` to render
|
||||
the dismiss button. Banner doesn't own visibility, doesn't reset its
|
||||
own state, doesn't fire a sema event when closed. The
|
||||
`feedback_user_design_overrides_canon_vocabulary` rule applies: a
|
||||
generic landmark strip doesn't carry an intrinsic commit semantic.
|
||||
- **`intent` uses UIX 8-role vocab, not Chakra's 4** — keeps the
|
||||
vocabulary consistent with the rest of the system (SearchField,
|
||||
Toast, Toggle, etc. all use the same `ColorRole`). The mapping from
|
||||
conventional `info / success / warning / error` is documented in the
|
||||
intent canon (`src/uix/intent.ts`).
|
||||
- **`variant` = `ChipVariant`** — reuses the canonical 4-value vocab
|
||||
instead of inventing a new union per component (Chakra ships 4,
|
||||
Mantine 5, MUI 3 — none with the same names). The same vocab powers
|
||||
Chip / Tag / Avatar.
|
||||
- **No icon / title / description slots** — composition over
|
||||
configuration. The consumer puts an Icon + heading + body inside
|
||||
Banner's children; flex/gap from the recipe handles the spacing.
|
||||
- **Close is eidos-only** — meets the four rules in
|
||||
`eidos/components/README.md` (no behavior, no aria contract beyond
|
||||
the user-supplied `aria-label`, no event-target role, no keyboard
|
||||
beyond a plain `<button>`). The morfo doesn't declare Close as a
|
||||
part because the close is the consumer's onclick — Banner doesn't
|
||||
own a commit semantic.
|
||||
|
||||
## Eventos Sema
|
||||
|
||||
Banner declara 0 eventos. Es una primitiva pasiva que conforma una
|
||||
superficie de anuncio. La dismissión, cuando aplica, vive en el handler
|
||||
`onclick` que el consumidor adjunta a `<Banner.Close>` — Banner no
|
||||
commite ni emerge. Eso evita inflar el contrato Morfo con un evento
|
||||
genérico que no aplica universalmente (banners persistentes, banners
|
||||
informativos sin dismiss, banners full-bleed sin botón). Misma
|
||||
justificación que Box / Flex / Grid: la primitiva no carga semánticas
|
||||
que no posee.
|
||||
|
||||
## Gaps
|
||||
|
||||
| Gap | Disposición | Detalle |
|
||||
| --- | --- | --- |
|
||||
| `role="alert"` automatic when `intent='threat'` | descartar | Banner es landmark; Toast cubre el live-region case. Forzar role=alert sería confuso. |
|
||||
| Icon slot dedicado | descartar | El consumidor compone `<Icon>` antes del texto; `gap` del recipe espacía. |
|
||||
| `AlertTitle` / `AlertDescription` parts | diferir | Pendiente si surgen ≥2 casos donde la composición plana sea ergonómicamente pobre. |
|
||||
| Animación de entrada/salida | diferir | Hoy se compone con `transition:slide` del consumidor o con `<Collapsible>`. Si llegan ≥2 casos donde Banner necesite owned motion, subir a Sema con la verb `surge`. |
|
||||
| `borderRadius` shorthand | implementar | Hoy via `style=`. Posible añadirlo como `--banner-radius` token público; ya existe en el recipe pero sin prop. |
|
||||
| Persistent dismissed-state (cookie / localStorage) | descartar | No es responsabilidad del componente de layout. El consumidor implementa la persistencia. |
|
||||
| `Banner.Close` as a real morfo part with sema close-cancel commit | diferir | Si se decide que la dismissión es semántica universal, subir Close a Morfo + Sema. Hoy es composition only. |
|
||||
|
||||
## Referencias
|
||||
|
||||
- Chakra UI Alert: https://chakra-ui.com/docs/components/alert
|
||||
- Mantine Alert: https://mantine.dev/core/alert/
|
||||
- MUI Alert: https://mui.com/material-ui/react-alert/
|
||||
- HTML landmark roles: https://www.w3.org/TR/wai-aria-1.2/#banner
|
||||
- Air Banner (original): morfo-runtime branch — `src/uix/air/components/layout/banner`
|
||||
|
||||
## Passive justification
|
||||
|
||||
Visual-only primitive (`scope: ['eidos']` en el morfo). Una sola
|
||||
part Provider que emite el marker `[data-banner]` + `data-intent` +
|
||||
`data-variant` + `data-size` + `role="banner"`. Sin estados internos, sin
|
||||
ARIA contracts dependientes de runtime, sin keyboard, sin commit
|
||||
semantics. La Close children es eidos-only — el consumidor handlea
|
||||
onclick para visibility. No hay nada que animar como verbo Sema; las
|
||||
transiciones de aparición/desaparición las orquesta el consumidor con
|
||||
`{#if}` + transitions de Svelte o componiendo con `<Collapsible>`.
|
||||
@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<Banner.Close>` — visually-styled dismiss button. Pure
|
||||
* eidos-only part: meets the four rules in
|
||||
* `eidos/components/README.md` (no behavior, no aria contract, no
|
||||
* event-target role, no keyboard). The consumer wires `onclick` to
|
||||
* their own visibility state — Banner does not own the dismissal.
|
||||
*
|
||||
* Defaults `aria-label="Dismiss"` so consumers don't have to remember,
|
||||
* but the consumer can override.
|
||||
*/
|
||||
import type { BannerCloseProps } from './types';
|
||||
|
||||
let {
|
||||
'aria-label': ariaLabel = 'Dismiss',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: BannerCloseProps = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
{...restProps}
|
||||
aria-label={ariaLabel}
|
||||
class={className}
|
||||
data-banner-close=""
|
||||
>
|
||||
{#if children}{@render children()}{:else}<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="14"
|
||||
height="14"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
d="M3 3l10 10M13 3L3 13"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>{/if}
|
||||
</button>
|
||||
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Banner recipe — full-bleed announcement strip. Reads `data-intent`
|
||||
* (the standard UIX color-role channel) and `data-variant` (soft / solid
|
||||
* / outline / ghost) to pick the right palette mix from the foundation
|
||||
* `--color-{intent}-*` tokens. `data-size` controls vertical density.
|
||||
*
|
||||
* Variant cascade:
|
||||
* soft — `--color-{intent}-track` bg + `--color-{intent}-text` fg
|
||||
* solid — `--color-{intent}-solid` bg + `--color-{intent}-contrast` fg
|
||||
* outline — transparent bg, `--color-{intent}-border` ring + text
|
||||
* ghost — transparent surface — text only
|
||||
*
|
||||
* `--banner-*` tokens are public overrides; `--_banner-*` are internal
|
||||
* recipe-local resolutions.
|
||||
*/
|
||||
|
||||
[data-banner] {
|
||||
/* Shared structural surface. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--banner-gap, var(--space-3));
|
||||
padding-block: var(--_banner-padding-block);
|
||||
padding-inline: var(--_banner-padding-inline);
|
||||
inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
box-sizing: border-box;
|
||||
font-size: var(--_banner-font-size);
|
||||
line-height: var(--font-line-height-sm, 1.4);
|
||||
background: var(--_banner-bg, transparent);
|
||||
color: var(--_banner-fg, inherit);
|
||||
border: var(--_banner-border-width, 0) solid var(--_banner-border-color, transparent);
|
||||
border-radius: var(--banner-radius, 0);
|
||||
}
|
||||
|
||||
/* ── Size ──────────────────────────────────────────────────────────── */
|
||||
|
||||
[data-banner][data-size='sm'] {
|
||||
--_banner-padding-block: var(--space-2);
|
||||
--_banner-padding-inline: var(--space-3);
|
||||
--_banner-font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
[data-banner][data-size='md'] {
|
||||
--_banner-padding-block: var(--space-3);
|
||||
--_banner-padding-inline: var(--space-4);
|
||||
--_banner-font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
[data-banner][data-size='lg'] {
|
||||
--_banner-padding-block: var(--space-4);
|
||||
--_banner-padding-inline: var(--space-5);
|
||||
--_banner-font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
/* ── Intent palette resolution ─────────────────────────────────────── */
|
||||
|
||||
[data-banner][data-intent='primary'] {
|
||||
--_banner-solid: var(--color-primary-solid);
|
||||
--_banner-solid-contrast: var(--color-primary-contrast);
|
||||
--_banner-track: var(--color-primary-track);
|
||||
--_banner-border: var(--color-primary-border);
|
||||
--_banner-text: var(--color-primary-text);
|
||||
}
|
||||
[data-banner][data-intent='secondary'] {
|
||||
--_banner-solid: var(--color-secondary-solid);
|
||||
--_banner-solid-contrast: var(--color-secondary-contrast);
|
||||
--_banner-track: var(--color-secondary-track);
|
||||
--_banner-border: var(--color-secondary-border);
|
||||
--_banner-text: var(--color-secondary-text);
|
||||
}
|
||||
[data-banner][data-intent='neutral'] {
|
||||
--_banner-solid: var(--color-neutral-solid);
|
||||
--_banner-solid-contrast: var(--color-neutral-contrast);
|
||||
--_banner-track: var(--color-neutral-track);
|
||||
--_banner-border: var(--color-neutral-border);
|
||||
--_banner-text: var(--color-neutral-text);
|
||||
}
|
||||
[data-banner][data-intent='affirm'] {
|
||||
--_banner-solid: var(--color-affirm-solid);
|
||||
--_banner-solid-contrast: var(--color-affirm-contrast);
|
||||
--_banner-track: var(--color-affirm-track);
|
||||
--_banner-border: var(--color-affirm-border);
|
||||
--_banner-text: var(--color-affirm-text);
|
||||
}
|
||||
[data-banner][data-intent='fulfill'] {
|
||||
--_banner-solid: var(--color-fulfill-solid);
|
||||
--_banner-solid-contrast: var(--color-fulfill-contrast);
|
||||
--_banner-track: var(--color-fulfill-track);
|
||||
--_banner-border: var(--color-fulfill-border);
|
||||
--_banner-text: var(--color-fulfill-text);
|
||||
}
|
||||
[data-banner][data-intent='risk'] {
|
||||
--_banner-solid: var(--color-risk-solid);
|
||||
--_banner-solid-contrast: var(--color-risk-contrast);
|
||||
--_banner-track: var(--color-risk-track);
|
||||
--_banner-border: var(--color-risk-border);
|
||||
--_banner-text: var(--color-risk-text);
|
||||
}
|
||||
[data-banner][data-intent='threat'] {
|
||||
--_banner-solid: var(--color-threat-solid);
|
||||
--_banner-solid-contrast: var(--color-threat-contrast);
|
||||
--_banner-track: var(--color-threat-track);
|
||||
--_banner-border: var(--color-threat-border);
|
||||
--_banner-text: var(--color-threat-text);
|
||||
}
|
||||
[data-banner][data-intent='loss'] {
|
||||
--_banner-solid: var(--color-loss-solid);
|
||||
--_banner-solid-contrast: var(--color-loss-contrast);
|
||||
--_banner-track: var(--color-loss-track);
|
||||
--_banner-border: var(--color-loss-border);
|
||||
--_banner-text: var(--color-loss-text);
|
||||
}
|
||||
|
||||
/* ── Variant treatment ─────────────────────────────────────────────── */
|
||||
|
||||
[data-banner][data-variant='soft'] {
|
||||
--_banner-bg: var(--_banner-track);
|
||||
--_banner-fg: var(--_banner-text);
|
||||
--_banner-border-width: 0;
|
||||
}
|
||||
|
||||
[data-banner][data-variant='solid'] {
|
||||
--_banner-bg: var(--_banner-solid);
|
||||
--_banner-fg: var(--_banner-solid-contrast);
|
||||
--_banner-border-width: 0;
|
||||
}
|
||||
|
||||
[data-banner][data-variant='outline'] {
|
||||
--_banner-bg: transparent;
|
||||
--_banner-fg: var(--_banner-text);
|
||||
--_banner-border-width: 1px;
|
||||
--_banner-border-color: var(--_banner-border);
|
||||
}
|
||||
|
||||
[data-banner][data-variant='ghost'] {
|
||||
--_banner-bg: transparent;
|
||||
--_banner-fg: var(--_banner-text);
|
||||
--_banner-border-width: 0;
|
||||
}
|
||||
|
||||
/* ── Close button (eidos-only part) ────────────────────────────────── */
|
||||
|
||||
[data-banner-close] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-inline-start: auto;
|
||||
inline-size: var(--space-6);
|
||||
block-size: var(--space-6);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm, var(--space-1));
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 120ms ease, background-color 120ms ease;
|
||||
}
|
||||
|
||||
[data-banner-close]:hover {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
[data-banner-close]:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 1px;
|
||||
opacity: 1;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<Banner>` — full-bleed announcement strip with intent /
|
||||
* variant / size visual treatment. Renders as
|
||||
* `<header data-banner role="banner">` with `data-intent`,
|
||||
* `data-variant`, and `data-size` driving the recipe. The explicit
|
||||
* `role="banner"` stabilises the landmark across layouts (HTML spec
|
||||
* grants the implicit role only to top-level `<header>` children of
|
||||
* `<body>`).
|
||||
*
|
||||
* <Banner intent="affirm" variant="soft" size="md">
|
||||
* <Icon name="check" />
|
||||
* <span>Your changes have been saved.</span>
|
||||
* <Banner.Close onclick={() => (show = false)} />
|
||||
* </Banner>
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import type { BannerProps } from './types';
|
||||
|
||||
let {
|
||||
intent = 'primary',
|
||||
variant = 'soft',
|
||||
size = 'md',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: BannerProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
|
||||
const resolvedIntent = $derived(eidos.resolve(intent) ?? 'primary');
|
||||
const resolvedVariant = $derived(eidos.resolve(variant) ?? 'soft');
|
||||
const resolvedSize = $derived(eidos.resolve(size) ?? 'md');
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_redundant_roles -->
|
||||
<header
|
||||
{...restProps}
|
||||
class={className}
|
||||
role="banner"
|
||||
data-banner=""
|
||||
data-intent={resolvedIntent}
|
||||
data-variant={resolvedVariant}
|
||||
data-size={resolvedSize}
|
||||
>
|
||||
{@render children?.()}
|
||||
</header>
|
||||
@ -0,0 +1,27 @@
|
||||
// Banner — full-bleed announcement strip with intent variants.
|
||||
//
|
||||
// import { Banner } from '$uix/eidos/components/banner';
|
||||
//
|
||||
// <Banner intent="affirm" variant="soft" size="md">
|
||||
// <span>Your changes have been saved.</span>
|
||||
// <Banner.Close onclick={() => (show = false)} />
|
||||
// </Banner>
|
||||
import BannerComponent from './banner.svelte';
|
||||
import Close from './banner-close.svelte';
|
||||
|
||||
type BannerNamespace = typeof BannerComponent & {
|
||||
Close: typeof Close;
|
||||
};
|
||||
|
||||
const Banner = BannerComponent as BannerNamespace;
|
||||
Banner.Close = Close;
|
||||
|
||||
export { Banner };
|
||||
export default Banner;
|
||||
export type {
|
||||
BannerProps,
|
||||
BannerCloseProps,
|
||||
BannerIntent,
|
||||
BannerVariant,
|
||||
BannerSize
|
||||
} from './types';
|
||||
@ -0,0 +1,56 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { ColorRole, ResponsiveProp, Size } from '$uix/eidos/lib/types';
|
||||
import type { ChipVariant } from '$uix/eidos/lib/types';
|
||||
|
||||
/**
|
||||
* Banner intent vocabulary. Mirrors the canonical UIX `ColorRole` set
|
||||
* (Intent + `primary` / `secondary` hierarchy roles). The recipe pulls
|
||||
* the matching `--color-{intent}-{slot}` tokens for each value.
|
||||
*/
|
||||
export type BannerIntent = ColorRole;
|
||||
|
||||
/**
|
||||
* Visual treatment family. Banner reuses the canonical `ChipVariant`
|
||||
* vocabulary (`soft` / `solid` / `outline` / `ghost`):
|
||||
* - `soft`: tinted background + matching text (default).
|
||||
* - `solid`: saturated background, contrasting text.
|
||||
* - `outline`: transparent background, accented border + text.
|
||||
* - `ghost`: transparent — text only, no surface tint.
|
||||
*/
|
||||
export type BannerVariant = ChipVariant;
|
||||
|
||||
/**
|
||||
* Size scale exposed by Banner. Narrowed from the canonical `Size`
|
||||
* scale to the values the recipe actually maps. `sm` / `md` / `lg` are
|
||||
* the meaningful announcement-strip heights; smaller doesn't read,
|
||||
* larger turns into a hero.
|
||||
*/
|
||||
export type BannerSize = Extract<Size, 'sm' | 'md' | 'lg'>;
|
||||
|
||||
export type BannerProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
||||
/**
|
||||
* Color intent for the announcement. Drives background, border, and
|
||||
* text via the matching `--color-{intent}-*` tokens. @default 'primary'
|
||||
*/
|
||||
intent?: ResponsiveProp<BannerIntent>;
|
||||
/**
|
||||
* Visual treatment family. @default 'soft'
|
||||
*/
|
||||
variant?: ResponsiveProp<BannerVariant>;
|
||||
/**
|
||||
* Vertical density. @default 'md'
|
||||
*/
|
||||
size?: ResponsiveProp<BannerSize>;
|
||||
/** External accessible name (forwarded to the `<header>`). */
|
||||
'aria-label'?: string;
|
||||
/** External label id (forwarded to the `<header>`). */
|
||||
'aria-labelledby'?: string;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
export type BannerCloseProps = Omit<HTMLAttributes<HTMLButtonElement>, 'children'> & {
|
||||
/** Accessible label for the dismiss button. @default 'Dismiss' */
|
||||
'aria-label'?: string;
|
||||
children?: Snippet;
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Float recipe — pulls the child to the start or end of the text flow.
|
||||
* Uses the modern `float: inline-{start | end}` logical-side values so
|
||||
* the primitive mirrors automatically in RTL contexts. The block-axis
|
||||
* margin is left to the standard Box `margin*` props so the consumer
|
||||
* can fine-tune top / bottom spacing case by case.
|
||||
*
|
||||
* `--float-gap` defaults to `var(--space-3)` when the consumer doesn't
|
||||
* pass `gap`. The margin sits on the side that faces the wrapped text,
|
||||
* so a `start`-side float gets `margin-inline-end`, and vice versa.
|
||||
*/
|
||||
|
||||
[data-box][data-float] {
|
||||
float: inline-start;
|
||||
margin-inline-end: var(--float-gap, var(--space-3));
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
[data-box][data-float][data-side='end'] {
|
||||
float: inline-end;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: var(--float-gap, var(--space-3));
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<Float>` — pulls its child to the start or end of the text
|
||||
* flow so adjacent inline content wraps around it. Composes through
|
||||
* `<Box>` and writes a `--float-side` / `--float-gap` pair that the
|
||||
* recipe consumes via `float: inline-{start | end}` + a logical
|
||||
* margin against the text. Typical uses: inline images, pull-quotes,
|
||||
* drop caps.
|
||||
*
|
||||
* <p>
|
||||
* <Float side="start" maxWidth={120}>
|
||||
* <img src="…" alt="…" />
|
||||
* </Float>
|
||||
* Long paragraph that wraps around the float…
|
||||
* </p>
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import Box from '../box/box.svelte';
|
||||
import { composeStyle, formatLayoutSpace, pushStyleVar } from '../_layout/shared';
|
||||
import type { FloatProps } from './types';
|
||||
|
||||
let {
|
||||
side = 'start',
|
||||
gap = 3,
|
||||
style,
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: FloatProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
|
||||
const resolvedSide = $derived(eidos.resolve(side) ?? 'start');
|
||||
const resolvedStyle = $derived.by(() => {
|
||||
const decls: string[] = [];
|
||||
pushStyleVar(decls, '--float-gap', formatLayoutSpace(eidos.resolve(gap)));
|
||||
return composeStyle(decls, style);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Box
|
||||
{...restProps}
|
||||
class={className}
|
||||
style={resolvedStyle}
|
||||
data-float=""
|
||||
data-side={resolvedSide}
|
||||
>
|
||||
{@render children?.()}
|
||||
</Box>
|
||||
@ -0,0 +1,15 @@
|
||||
// Float — pulls a child to the start or end of the text flow.
|
||||
//
|
||||
// import { Float } from '$uix/eidos/components/float';
|
||||
//
|
||||
// <p>
|
||||
// <Float side="start" maxWidth={120}>
|
||||
// <img src="…" alt="…" />
|
||||
// </Float>
|
||||
// Long paragraph that wraps around the float…
|
||||
// </p>
|
||||
import Float from './float.svelte';
|
||||
|
||||
export { Float };
|
||||
export default Float;
|
||||
export type { FloatProps, FloatSide } from './types';
|
||||
@ -0,0 +1,26 @@
|
||||
import type { ResponsiveProp } from '$uix/eidos/lib/types';
|
||||
import type { BoxProps } from '../box/types';
|
||||
import type { LayoutSpaceValue } from '../_layout/shared';
|
||||
|
||||
/**
|
||||
* Side the floated element occupies in the text flow.
|
||||
*
|
||||
* `start` (logical left in LTR, right in RTL) maps to
|
||||
* `float: inline-start`; `end` maps to `float: inline-end`. Use
|
||||
* logical sides so RTL layouts mirror correctly without extra plumbing.
|
||||
*/
|
||||
export type FloatSide = 'start' | 'end';
|
||||
|
||||
export type FloatProps = Omit<BoxProps, 'position'> & {
|
||||
/**
|
||||
* Which side of the text flow the child sits on. @default 'start'
|
||||
*/
|
||||
side?: ResponsiveProp<FloatSide>;
|
||||
/**
|
||||
* Inline margin between the floated child and the surrounding text.
|
||||
* Number → `var(--space-N)`. The block-axis margin defaults to zero
|
||||
* (the consumer adds bottom margin via the standard Box `margin*`
|
||||
* props when needed). @default 3
|
||||
*/
|
||||
gap?: ResponsiveProp<LayoutSpaceValue>;
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import type { Morfo } from '../types';
|
||||
|
||||
/**
|
||||
* AspectRatio — constrains the inner content to a width/height ratio
|
||||
* (layout primitive).
|
||||
*
|
||||
* Eidos-native: the recipe consumes a single CSS variable (`--aspect-ratio`)
|
||||
* that the component writes inline on a single `<div data-aspect-ratio>`
|
||||
* shell. The CSS `aspect-ratio` property does the work — no padding-bottom
|
||||
* hack, no absolutely-positioned inner element, no DOM gymnastics. The
|
||||
* inner content fills the box via `width:100%; height:100%`.
|
||||
*
|
||||
* Justification for 0-event surface: AspectRatio does not commit, emerge,
|
||||
* or react to anything. It is a pure visual / structural primitive that
|
||||
* shapes its child via `aspect-ratio` CSS. Adding events would manufacture
|
||||
* semantics the primitive doesn't carry. Same justification as `box.ts` /
|
||||
* `flex.ts` / `grid.ts`.
|
||||
*/
|
||||
export const aspectRatioMorfo = {
|
||||
name: 'AspectRatio',
|
||||
kebab: 'aspect-ratio',
|
||||
scope: ['eidos'],
|
||||
parts: [
|
||||
{
|
||||
name: 'Provider',
|
||||
kebab: 'provider',
|
||||
archetype: 'provider',
|
||||
kind: 'public',
|
||||
defaultElement: 'div',
|
||||
optional: false,
|
||||
data: [],
|
||||
aria: []
|
||||
}
|
||||
]
|
||||
} as const satisfies Morfo;
|
||||
@ -0,0 +1,33 @@
|
||||
import type { Morfo } from '../types';
|
||||
|
||||
/**
|
||||
* AutoGrid — responsive grid container that fits columns to inline size
|
||||
* without media queries (layout primitive).
|
||||
*
|
||||
* Eidos-native: composes through `<Grid>` (which already composes through
|
||||
* `<Box>`), so the provider renders a single `<div data-box data-grid
|
||||
* data-auto-grid>` shell. Every prop maps to a `--auto-grid-*`,
|
||||
* `--grid-*` or `--box-*` custom property — same pattern as the rest of
|
||||
* the layout primitives.
|
||||
*
|
||||
* Justification for 0-event surface: AutoGrid is a pure visual /
|
||||
* structural primitive that arranges children. No commit, no emerge, no
|
||||
* keyboard, no aria. Same passive justification as `box.ts` / `grid.ts`.
|
||||
*/
|
||||
export const autoGridMorfo = {
|
||||
name: 'AutoGrid',
|
||||
kebab: 'auto-grid',
|
||||
scope: ['eidos'],
|
||||
parts: [
|
||||
{
|
||||
name: 'Provider',
|
||||
kebab: 'provider',
|
||||
archetype: 'provider',
|
||||
kind: 'public',
|
||||
defaultElement: 'div',
|
||||
optional: false,
|
||||
data: [],
|
||||
aria: []
|
||||
}
|
||||
]
|
||||
} as const satisfies Morfo;
|
||||
@ -0,0 +1,46 @@
|
||||
import type { Morfo } from '../types';
|
||||
|
||||
/**
|
||||
* Banner — full-bleed announcement strip with intent variants
|
||||
* (layout primitive).
|
||||
*
|
||||
* Eidos-native: the provider is a `<header data-banner role="banner">`
|
||||
* shell. The intent prop maps to a `data-intent="…"` selector (the
|
||||
* standard UIX color-role channel for visual treatment) and the recipe
|
||||
* pulls the matching `--color-{intent}-*` tokens. The explicit role
|
||||
* stabilises the landmark across layouts — the HTML spec only grants
|
||||
* `role="banner"` to `<header>` when it is a top-level child of `<body>`;
|
||||
* nesting it inside `<main>`, `<article>`, or `<section>` loses the role.
|
||||
*
|
||||
* Justification for 0-event surface: Banner is a passive surface that
|
||||
* shapes an announcement. The optional Close child is a composition
|
||||
* concern (a `<button>` the consumer wires to their own visibility
|
||||
* state) — Banner itself does not own a commit, an emerge, a keyboard
|
||||
* contract, or any ARIA beyond the landmark role. Same passive
|
||||
* justification as `box.ts` / `flex.ts`.
|
||||
*
|
||||
* Close is intentionally NOT a morfo part: it is rendered as an
|
||||
* eidos-only wrapper that only adds visual treatment to a plain
|
||||
* `<button>` the consumer provides. The four rules in
|
||||
* `eidos/components/README.md → Partes Eidos-only` are met: no behavior,
|
||||
* no aria contract (the consumer supplies `aria-label`), no event target
|
||||
* role, no keyboard. The consumer handles the dismissal via their own
|
||||
* onclick — Banner does not commit or emerge anything.
|
||||
*/
|
||||
export const bannerMorfo = {
|
||||
name: 'Banner',
|
||||
kebab: 'banner',
|
||||
scope: ['eidos'],
|
||||
parts: [
|
||||
{
|
||||
name: 'Provider',
|
||||
kebab: 'provider',
|
||||
archetype: 'provider',
|
||||
kind: 'public',
|
||||
defaultElement: 'header',
|
||||
optional: false,
|
||||
data: [],
|
||||
aria: []
|
||||
}
|
||||
]
|
||||
} as const satisfies Morfo;
|
||||
@ -0,0 +1,40 @@
|
||||
import type { Morfo } from '../types';
|
||||
|
||||
/**
|
||||
* Float — CSS-`float` primitive that pulls a child to the start or end
|
||||
* of a text flow so adjacent inline content wraps around it (layout
|
||||
* primitive).
|
||||
*
|
||||
* Eidos-native: composes through `<Box>` and sets `float:inline-start`
|
||||
* / `float:inline-end` plus a margin from the surrounding text via
|
||||
* `--float-gap`. Typical use cases: inline images, pull-quotes, drop
|
||||
* caps, side notes in long-form content.
|
||||
*
|
||||
* Justification for 0-event surface: Float is a pure visual /
|
||||
* structural primitive. It changes how its child participates in the
|
||||
* surrounding text flow and nothing else — no commit, no emerge, no
|
||||
* keyboard, no ARIA. Same passive justification as `box.ts`.
|
||||
*
|
||||
* Note on naming: this is NOT the legacy `air/Float` 9-zone external
|
||||
* placement primitive (that one was an absolutely-positioned overlay
|
||||
* relative to an anchor). The CSS-`float` semantic is closer to Radix
|
||||
* Themes `Inset` and MUI's pull-quote pattern — see the README's
|
||||
* "Baseline" section for the design history.
|
||||
*/
|
||||
export const floatMorfo = {
|
||||
name: 'Float',
|
||||
kebab: 'float',
|
||||
scope: ['eidos'],
|
||||
parts: [
|
||||
{
|
||||
name: 'Provider',
|
||||
kebab: 'provider',
|
||||
archetype: 'provider',
|
||||
kind: 'public',
|
||||
defaultElement: 'div',
|
||||
optional: false,
|
||||
data: [],
|
||||
aria: []
|
||||
}
|
||||
]
|
||||
} as const satisfies Morfo;
|
||||
Loading…
Reference in new issue