|
|
4 months ago | |
|---|---|---|
| .. | ||
| README.md | 5 months ago | |
| banner-close.svelte | 5 months ago | |
| banner.css | 4 months ago | |
| banner.svelte | 4 months ago | |
| index.ts | 5 months ago | |
| types.ts | 5 months ago | |
README.md
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
<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:
intentmapped to the canonical UIXColorRole(8 values:primary | secondary | neutral | affirm | fulfill | risk | threat | loss).variantfrom the sharedChipVariantvocab (soft | solid | outline | ghost).sizenarrowed from canonicalSizetosm | md | lg— smaller doesn't read; larger turns the strip into a hero (out of scope).- Optional
Banner.Closepart. 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", notrole="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 properrole="status"/aria-live="polite"contract. Forcingrole="alert"on a static strip would be aggressive overreach.- Composition, not boolean
dismissible— pattern matches the rest of UIX (DatePicker, Dialog, Drawer all dropped*Buttonboolean 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. Thefeedback_user_design_overrides_canon_vocabularyrule applies: a generic landmark strip doesn't carry an intrinsic commit semantic. intentuses 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 sameColorRole). The mapping from conventionalinfo / success / warning / erroris 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-suppliedaria-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>.