diff --git a/src/uix/eidos/components/code-block/README.md b/src/uix/eidos/components/code-block/README.md new file mode 100644 index 000000000..04ffbcfcd --- /dev/null +++ b/src/uix/eidos/components/code-block/README.md @@ -0,0 +1,112 @@ +# Eidos CodeBlock + +Multi-line code block primitive. Renders a `
` body, with an optional header strip carrying a title,
+a language badge, and a copy-to-clipboard button. The copy button is
+a leaf utility — it does not emit Sema events.
+
+## Superficie
+
+```svelte
+
+ {`const x = 1;
+console.log(x);`}
+
+
+
+ {`npm run dev`}
+
+
+
+ {`Plain block with no chrome.`}
+
+```
+
+## Baseline
+
+Origen: `air/components/typography/code-block` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Separated from ``**: air shipped a `variant='block'` on
+ ``. Eidos splits them so the inline and block APIs stay
+ independent.
+- **`filename` → `title`**: generic name — the slot accepts whatever
+ label fits (file name, hostname, label, etc.).
+- **`copyable` → `showCopy`**: aligns with composition-over-visibility
+ ergonomics (the prop controls whether the button is included).
+- **Translator removed**: button labels are inline literal strings
+ ("Copy" / "Copied"). If localization is needed later, expose a
+ snippet-based slot.
+- **Variants added**: `surface` (default, filled), `outline`
+ (transparent + border), `ghost` (no chrome). Air had only one
+ visual treatment.
+- **Sizes**: air `1..12` → eidos `xxs..xl` via foundation tokens.
+- **Tokens**: `--air-code-block-*` → consume the foundation directly
+ (`--style-code-*`, `--color-neutral-*`, `--space-{n}`, `--radius-*`).
+
+## Comparativa
+
+| Capacidad | UIX CodeBlock | Radix Themes CodeBlock | Chakra | Mantine CodeHighlight |
+| --- | --- | --- | --- | --- |
+| Header (title + lang badge) | Sí | No (one component) | composable | Sí |
+| Copy button | Sí (`showCopy`) | No | composable | Sí (`withCopyButton`) |
+| Variants | surface / outline / ghost | size + variant | colorScheme | filled / outline |
+| Language hint | Sí (`language`) | No | No | Sí |
+| Syntax highlighting | No (reserved) | No | No | Sí (Shiki) |
+| Size override | Sí | Sí | Sí | Sí |
+| Line numbers | No | No | composable | Sí |
+
+## Decisiones
+
+- **Copy is a button, not a Sema event**: copy belongs to the
+ "utility" tier (clipboard write, no semantic transition). If a
+ consumer wants a celebrate-on-copy sound they can compose the
+ block inside a Toast affordance — the block itself stays passive.
+- **Showed copy by default**: docs are the dominant use case;
+ removing the button is the opt-out.
+- **Variant `surface` default**: filled neutral matches the
+ most-common docs look. `outline` for embedded contexts (inside a
+ Banner or Note), `ghost` for sub-component embedding.
+- **No syntax highlighting in core**: stamped `data-language` on the
+ inner `` so a Shiki/Prism action can hydrate later without
+ re-rendering the block.
+- **Tokens come from the foundation directly**: no per-component
+ `--code-block-*` aliases unless a real consumer drift emerges. The
+ recipe references `--style-code-*` and `--color-neutral-*`.
+
+## Eventos Sema
+
+CodeBlock declara 0 eventos. La copy button es una utility leaf, no
+evento semántico — copying not a transition the user "feels"
+beyond a brief affordance flip ("Copy" → "Copied"). Si surge un caso
+donde copy debe disparar sema (e.g. fulfilment of a tutorial step), el
+consumer compone CodeBlock dentro de un componente con sema (Form
+step, Stepper item) que posee el verbo `commit-fulfill`.
+
+## Gaps
+
+| Gap | Disposición | Detalle |
+| --- | --- | --- |
+| Syntax highlighting | **diferir** | Reservado vía `language` + `data-language`. Integración Shiki / Prism vía Svelte action separada. |
+| Line numbers | **diferir** | UI cosmético. Si emerge, añadir como `showLineNumbers` prop. |
+| Per-line diff (added/removed) | **diferir** | Complementa highlighting. Bajo prioridad. |
+| Translatable copy button labels | **diferir** | Hoy son literales. Si surge i18n requirement, exponer slot custom. |
+| Custom copy handler | **diferir** | `onclick` handler today; un `oncopy` prop puede llegar si emerge un caso real. |
+| Sema event on copy | **descartar** | Copy es utility leaf, no verbo perceptivo del primitive. |
+
+## Referencias
+
+- Radix Themes Code: https://www.radix-ui.com/themes/docs/components/code
+- Mantine CodeHighlight: https://mantine.dev/x/code-highlight/
+- Chakra UI Code (block usage): https://chakra-ui.com/docs/components/code
+- shadcn/ui block patterns: https://ui.shadcn.com/
+
+## Passive justification
+
+`scope: ['eidos']` — visual-only primitive. The morfo declares a
+single Provider part (`[data-code-block]`). The copy button is a
+recipe-internal leaf that exists only in eidos; per the 2-of-3 rule
+(only one layer would consume a `commit-copy` event) it stays out
+of the morfo contract. The block has no perceptual transitions
+beyond the brief "Copy → Copied" flip handled in local component
+state.
diff --git a/src/uix/eidos/components/code-block/code-block.css b/src/uix/eidos/components/code-block/code-block.css
new file mode 100644
index 000000000..ae5125e71
--- /dev/null
+++ b/src/uix/eidos/components/code-block/code-block.css
@@ -0,0 +1,126 @@
+/*
+ * CodeBlock recipe — multi-line code block with optional header.
+ *
+ * Variants:
+ * - `surface` filled with neutral element bg + border
+ * - `outline` transparent fill, border only
+ * - `ghost` transparent shell, no border (for sub-component embedding)
+ */
+
+[data-code-block] {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ color: var(--color-content-primary);
+}
+
+[data-code-block][data-variant='surface'] {
+ background: var(--color-neutral-element);
+ border: 1px solid var(--color-neutral-border);
+}
+
+[data-code-block][data-variant='outline'] {
+ background: transparent;
+ border: 1px solid var(--color-neutral-border);
+}
+
+[data-code-block][data-variant='ghost'] {
+ background: transparent;
+ border: 0;
+}
+
+[data-code-block-header] {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding-inline: var(--space-3);
+ padding-block: var(--space-1);
+ border-bottom: 1px solid var(--color-neutral-border);
+ background: var(--color-neutral-track);
+}
+
+[data-code-block][data-variant='ghost'] [data-code-block-header] {
+ background: transparent;
+ border-bottom-color: var(--color-neutral-border);
+}
+
+[data-code-block-title] {
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-content-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+[data-code-block-lang] {
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xxs);
+ color: var(--color-content-muted);
+ text-transform: lowercase;
+ letter-spacing: 0.04em;
+ white-space: nowrap;
+}
+
+/* When there is no title, the lang badge stays at the start. When both
+ * exist, the title takes the flex slot and the lang sits next to it.
+ */
+[data-code-block-header]:not(:has([data-code-block-title])) [data-code-block-lang] {
+ margin-inline-end: auto;
+}
+
+[data-code-block-copy] {
+ appearance: none;
+ font: inherit;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-content-secondary);
+ background: transparent;
+ border: 1px solid var(--color-neutral-border);
+ border-radius: var(--radius-sm);
+ padding-inline: var(--space-2);
+ padding-block: 2px;
+ cursor: pointer;
+ white-space: nowrap;
+ transition:
+ background 120ms ease,
+ border-color 120ms ease,
+ color 120ms ease;
+}
+
+[data-code-block-copy]:hover {
+ background: var(--color-neutral-hover);
+ color: var(--color-content-primary);
+}
+
+[data-code-block-copy]:focus-visible {
+ outline: 2px solid var(--color-primary-solid);
+ outline-offset: 2px;
+}
+
+[data-code-block-copy][data-copied] {
+ color: var(--color-affirm-text);
+ border-color: var(--color-affirm-border);
+}
+
+[data-code-block-pre] {
+ margin: 0;
+ padding: var(--space-3);
+ overflow-x: auto;
+ tab-size: 2;
+}
+
+[data-code-block-code] {
+ display: block;
+ font-family: var(--style-code-font-family);
+ font-size: var(--_code-block-font-size, var(--style-code-font-size));
+ font-weight: var(--style-code-font-weight);
+ line-height: var(--_code-block-line-height, var(--style-code-line-height));
+ letter-spacing: var(--style-code-letter-spacing);
+ color: inherit;
+ background: transparent;
+}
diff --git a/src/uix/eidos/components/code-block/code-block.svelte b/src/uix/eidos/components/code-block/code-block.svelte
new file mode 100644
index 000000000..18cad72a3
--- /dev/null
+++ b/src/uix/eidos/components/code-block/code-block.svelte
@@ -0,0 +1,103 @@
+
+
+
+ {#if hasHeader}
+
+ {#if title}
+ {title}
+ {/if}
+ {#if language}
+ {language}
+ {/if}
+ {#if showCopy}
+
+ {/if}
+
+ {/if}
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/code-block/index.ts b/src/uix/eidos/components/code-block/index.ts
new file mode 100644
index 000000000..ea2fd1825
--- /dev/null
+++ b/src/uix/eidos/components/code-block/index.ts
@@ -0,0 +1,21 @@
+// CodeBlock — eidos single-component multi-line code block.
+//
+// import { CodeBlock } from '$uix/eidos/components/code-block';
+//
+//
+// {`const x = 1;\nconsole.log(x);`}
+//
+//
+// Single-component primitive — `` IS the root. Ships an
+// optional header strip (title + language badge + copy button) and a
+// `` inner block. The copy button is a leaf utility; it
+// does not widen the morfo's surface because only eidos consumes it.
+import CodeBlockComponent from './code-block.svelte';
+
+const CodeBlock = CodeBlockComponent;
+
+export { CodeBlock };
+
+export default CodeBlock;
+
+export type { CodeBlockProps, CodeBlockSize, CodeBlockVariant } from './types';
diff --git a/src/uix/eidos/components/code-block/types.ts b/src/uix/eidos/components/code-block/types.ts
new file mode 100644
index 000000000..4c864b046
--- /dev/null
+++ b/src/uix/eidos/components/code-block/types.ts
@@ -0,0 +1,27 @@
+import type { Snippet } from 'svelte';
+import type { HTMLAttributes } from 'svelte/elements';
+import type { ResponsiveProp } from '$uix/eidos/lib/types';
+
+/** Discrete size scale for the block's code font. */
+export type CodeBlockSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+/** Visual chrome. */
+export type CodeBlockVariant = 'surface' | 'outline' | 'ghost';
+
+export type CodeBlockProps = Omit, 'style' | 'children' | 'title'> & {
+ /** Title shown in the header (typically a filename or label). */
+ title?: string;
+ /** Language hint shown as a badge in the header. Also stamped as `data-language` on the ``. */
+ language?: string;
+ /** Render a copy-to-clipboard button in the header. @default true */
+ showCopy?: boolean;
+ /** Visual chrome variant. @default 'surface' */
+ variant?: ResponsiveProp;
+ /** Code font size override. */
+ size?: ResponsiveProp;
+ /** Extra inline style. */
+ tagStyle?: string;
+ /** Extra class names. */
+ class?: string;
+ children?: Snippet;
+};
diff --git a/src/uix/eidos/components/code/README.md b/src/uix/eidos/components/code/README.md
new file mode 100644
index 000000000..e95b4e617
--- /dev/null
+++ b/src/uix/eidos/components/code/README.md
@@ -0,0 +1,90 @@
+# Eidos Code
+
+Inline monospace text primitive. Renders `` by default applying
+the foundation's `--style-code-*` tokens. Sits inline within prose —
+for keywords, identifiers, file names. Multi-line code blocks use
+the separate `` primitive.
+
+## Superficie
+
+```svelte
+Run npm install to install dependencies.
+Press Ctrl+K to open search.
+readonly id: string
+```
+
+## Baseline
+
+Origen: `air/components/typography/code` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Split inline vs block**: air shipped a single `` with
+ `variant: 'inline' | 'block'`. Eidos splits them — `` is
+ inline only, `` is the multi-line surface with optional
+ copy. This matches Radix Themes' Code/CodeBlock split and avoids
+ the runtime `if (variant === 'block')` branch.
+- **Sizes**: air `1..12` → eidos `xxs..xl`. The foundation already
+ emits `--style-code-font-size` (defaults to `sm`); overrides go
+ through `var(--font-size-{key})`.
+- **Visual variant added**: `plain` (default, no chrome), `soft`
+ (subtle pill), `outline`. Air's inline was always plain; the soft
+ pill is a Radix Themes / Chakra convenience for surfacing inline
+ code from surrounding prose.
+- **Color content-role slot**: same contract as Text.
+- **`language` retained**: stamped as `data-language` for future
+ syntax-highlighting hooks. No highlighting in this version.
+
+## Comparativa
+
+| Capacidad | UIX Code | Radix Themes Code | Chakra Code | Mantine Code |
+| --- | --- | --- | --- | --- |
+| Inline-only primitive | Sí | Sí | Sí | Sí |
+| Visual variants | plain / soft / outline | size + variant (`solid` / `soft` / `outline` / `surface`) | colorScheme + variant | block boolean |
+| Size override | Sí (`size`) | Sí (`size`) | Sí (`fontSize`) | Sí (`size`) |
+| Color override | Sí (content-role) | Sí (`color`) | Sí (free) | Sí (free) |
+| Language hint | Sí (`language`, reserved) | No | No | No |
+| Syntax highlighting | No (reserved hook) | No | No | No (Mantine has separate Highlight) |
+
+## Decisiones
+
+- **`` is inline only**: matches Radix Themes' split. The block
+ surface is `` with its own chrome (copy button, header).
+ Keeps the inline primitive a single CSS rule per variant.
+- **Default variant `plain`**: respect ambient typography. The `soft`
+ pill is opt-in when the consumer wants the code to surface from
+ prose.
+- **Background uses `--color-neutral-element`**: neutral palette so
+ Code doesn't accidentally inherit `primary` tints from a colored
+ ancestor. Consumer can override with `tagStyle`.
+- **Language hint is reserved**: stamped as `data-language` so a
+ future syntax-highlighting hook (a Svelte action or a layout-level
+ hydration pass) can target `` without
+ re-rendering the primitive.
+
+## Eventos Sema
+
+Code declara 0 eventos. Visual-only primitive — no estado interactivo.
+
+## Gaps
+
+| Gap | Disposición | Detalle |
+| --- | --- | --- |
+| Syntax highlighting | **diferir** | Reservado vía `language` prop + `data-language`. Capa de tooling separada (Shiki / Prism) integrada vía Svelte action. |
+| Click-to-copy on inline code | **descartar** | El primitive es passive. Si necesitas copy, usa `` (que ya lo lleva). |
+| Free color (hex/CSS) | **descartar** | Mantiene contrato de contraste; usar `tagStyle`. |
+| Diff coloring (added/removed line markers) | **diferir** | Pertenece a `` cuando llegue. |
+
+## Referencias
+
+- Radix Themes Code: https://www.radix-ui.com/themes/docs/components/code
+- Chakra UI Code: https://chakra-ui.com/docs/components/code
+- Mantine Code: https://mantine.dev/core/code/
+- MDN ``: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code
+
+## Passive justification
+
+Visual-only (`scope: ['eidos']`). Una sola part Provider, marker
+`[data-code]`. Sin estados, sin keyboard, sin ARIA propia (la
+semántica viene del tag `` que AT entienden nativamente). La
+recipe consume `--style-code-*` del foundation + `--_code-{axis}`
+overrides inline.
diff --git a/src/uix/eidos/components/code/code.css b/src/uix/eidos/components/code/code.css
new file mode 100644
index 000000000..b7ad4e080
--- /dev/null
+++ b/src/uix/eidos/components/code/code.css
@@ -0,0 +1,38 @@
+/*
+ * Code recipe — inline monospace text.
+ *
+ * Consumes `--style-code-*` from the foundation as the base typography.
+ * The `data-variant` flag picks a chrome treatment:
+ * - `plain` bare mono text, inherits color
+ * - `soft` subtle pill with a neutral element background
+ * - `outline` outlined pill (same shape, transparent fill)
+ *
+ * Per-axis overrides come from local `--_code-{axis}` custom properties.
+ */
+
+[data-code] {
+ font-family: var(--_code-font-family, var(--style-code-font-family));
+ font-size: var(--_code-font-size, var(--style-code-font-size));
+ font-weight: var(--_code-font-weight, var(--style-code-font-weight));
+ line-height: var(--_code-line-height, var(--style-code-line-height));
+ letter-spacing: var(--_code-letter-spacing, var(--style-code-letter-spacing));
+ color: var(--_code-color, var(--style-code-color));
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ border: 0;
+ border-radius: var(--radius-sm);
+ vertical-align: baseline;
+}
+
+[data-code][data-variant='soft'] {
+ background: var(--color-neutral-element);
+ padding-inline: 0.32em;
+ padding-block: 0.1em;
+}
+
+[data-code][data-variant='outline'] {
+ border: 1px solid var(--color-neutral-border);
+ padding-inline: 0.32em;
+ padding-block: 0.1em;
+}
diff --git a/src/uix/eidos/components/code/code.svelte b/src/uix/eidos/components/code/code.svelte
new file mode 100644
index 000000000..398bfd548
--- /dev/null
+++ b/src/uix/eidos/components/code/code.svelte
@@ -0,0 +1,63 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/code/index.ts b/src/uix/eidos/components/code/index.ts
new file mode 100644
index 000000000..2e3b64255
--- /dev/null
+++ b/src/uix/eidos/components/code/index.ts
@@ -0,0 +1,18 @@
+// Code — eidos single-component inline monospace primitive.
+//
+// import { Code } from '$uix/eidos/components/code';
+//
+// const x = 1
+// npm run dev
+//
+// Single-component primitive — `` IS the root. Renders as
+// `` by default. For multi-line blocks, use ``.
+import CodeComponent from './code.svelte';
+
+const Code = CodeComponent;
+
+export { Code };
+
+export default Code;
+
+export type { CodeProps, CodeSize, CodeVariant, CodeColor } from './types';
diff --git a/src/uix/eidos/components/code/types.ts b/src/uix/eidos/components/code/types.ts
new file mode 100644
index 000000000..1e7479211
--- /dev/null
+++ b/src/uix/eidos/components/code/types.ts
@@ -0,0 +1,39 @@
+import type { Snippet } from 'svelte';
+import type { HTMLAttributes } from 'svelte/elements';
+import type { ResponsiveProp } from '$uix/eidos/lib/types';
+
+/** Discrete size scale. */
+export type CodeSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+/** Visual chrome. `plain` = no background (uses code font only). `soft` = subtle bg pill. `outline` = outline pill. */
+export type CodeVariant = 'plain' | 'soft' | 'outline';
+
+/** Content-role color slot. */
+export type CodeColor = 'primary' | 'secondary' | 'muted' | 'disabled' | 'on-solid';
+
+export type CodeProps = Omit, 'style' | 'children'> & {
+ /**
+ * Visual treatment.
+ * - `plain` bare mono text, no chrome (default).
+ * - `soft` subtle background pill — surfaces the inline code in prose.
+ * - `outline` outline pill — same shape, no fill.
+ * @default 'plain'
+ */
+ variant?: ResponsiveProp;
+ /** Size override. Maps to `var(--font-size-{key})` / matching line-height. */
+ size?: ResponsiveProp;
+ /** Content-role color slot. */
+ color?: ResponsiveProp;
+ /**
+ * Language hint. Reserved for future syntax-highlighting extension —
+ * stamped as `data-language` for downstream tooling.
+ */
+ language?: string;
+ /** Render a different tag. @default 'code' */
+ as?: string;
+ /** Extra inline style. */
+ tagStyle?: string;
+ /** Extra class names. */
+ class?: string;
+ children?: Snippet;
+};
diff --git a/src/uix/eidos/components/display/README.md b/src/uix/eidos/components/display/README.md
new file mode 100644
index 000000000..9f4e9546e
--- /dev/null
+++ b/src/uix/eidos/components/display/README.md
@@ -0,0 +1,89 @@
+# Eidos Display
+
+Hero typography primitive — the page's display title (above an ``
+headline). Always uses the foundation's `--style-hero-*` tokens, which
+scale up at the `md` breakpoint. Smaller scope than Heading: a single
+visual treatment, with per-axis overrides + a deliberate `tracking`
+prop because at hero sizes letter-spacing is a design decision.
+
+## Superficie
+
+```svelte
+Welcome to UIX.
+Big.
+Spread.
+Render display typography on a paragraph element.
+```
+
+## Baseline
+
+Origen: `air/components/typography/display` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Default tag is ``**: air defaulted to ``. Display titles
+ are virtually always the document hero, so the heading semantics
+ match the most common use; the `as` prop switches the tag when
+ needed (e.g. `as="p"` for a marketing pull-quote).
+- **Single style — hero**: air carried `size` (visual paso 1..12).
+ Eidos collapses to the foundation's hero token + per-axis overrides
+ via `--font-size-{key}` aliases.
+- **`tracking` retained**: kept the explicit letter-spacing prop
+ because at 44-60px tracking is a visual decision — but air's mapped
+ to `--air-text-{n}-ls`; eidos maps to fixed em values (-0.02em /
+ 0 / 0.02em) since the foundation's letter-spacing tokens are mostly
+ 0 across the scale.
+- **Color content-role slot**: same contract as Text / Heading.
+
+## Comparativa
+
+| Capacidad | UIX Display | Radix Themes Heading | Chakra Heading | Mantine Title | MUI Typography |
+| --- | --- | --- | --- | --- | --- |
+| Dedicated hero primitive | Sí | No (``) | No (use ``) | No (use ``) | Sí (`variant="h1"`) |
+| Default tag | `` | inherits | `` | `` | `` |
+| `as` polymorphism | Sí | Sí | Sí | Sí | Sí (`component`) |
+| Tracking override | Sí (`tracking`) | No | Sí (`letterSpacing`) | Sí (`ls`) | Sí (`letterSpacing`) |
+| Size scale | xxs..xxxl override | numeric | xs..6xl | xs..xxl | h1..h6 only |
+| Family / weight / color overrides | Sí | Sí | Sí | Sí | Sí |
+| Truncate / clamp | Sí | Sí | Sí | Sí | partial |
+| Responsive defaults | Sí (md breakpoint) | Sí | Sí | partial | Sí |
+
+## Decisiones
+
+- **Always `hero` style**: that's the whole point of Display — a single
+ oversize treatment. If you want `h1`..`h6` scale, use Heading.
+- **Default tag ``**: page hero. Override via `as` when display
+ typography is needed outside heading semantics.
+- **Tracking maps to fixed em values**: the foundation's
+ `--font-letter-spacing-*` tokens are mostly 0 — they exist to
+ reserve the API but don't carry hero-scale tracking. Display
+ shortcuts past them with literal em values so the prop produces
+ visible output today.
+- **No `level` prop**: Display is always semantically a heading; if
+ the consumer needs ``, they pass `as="h3"`.
+
+## Eventos Sema
+
+Display declara 0 eventos. Como Heading/Text, es una primitiva pasiva.
+
+## Gaps
+
+| Gap | Disposición | Detalle |
+| --- | --- | --- |
+| Free hex/rgb `color` | **descartar** | Rompe contrato de contraste; usar `tagStyle`. |
+| Gradient text fill | **diferir** | Composable con `tagStyle`. Si emerge demanda, añadir `` dedicado. |
+| Per-breakpoint custom hero responsive curve | **diferir** | Cubierto por la curva default (`hero@base`, `hero@md`); ajustes per-instance vía `size`. |
+| `tracking` mapped to foundation tokens | **diferir** | Cuando los tokens `--font-letter-spacing-*` emerjan con valores hero-apropiados, reemplazar los em literales por `var(--font-letter-spacing-{key})`. |
+
+## Referencias
+
+- Radix Themes Heading: https://www.radix-ui.com/themes/docs/components/heading
+- Chakra UI Heading: https://chakra-ui.com/docs/components/heading
+- Mantine Title: https://mantine.dev/core/title/
+- MUI Typography (h1 variant): https://mui.com/material-ui/react-typography/
+
+## Passive justification
+
+Visual-only (`scope: ['eidos']`). Una sola part Provider, marker
+`[data-display]`. Sin estados, sin keyboard, sin ARIA propia. La
+recipe consume `--style-hero-*` del foundation + `--_display-{axis}`
+overrides escritas inline.
diff --git a/src/uix/eidos/components/display/display.css b/src/uix/eidos/components/display/display.css
new file mode 100644
index 000000000..303768942
--- /dev/null
+++ b/src/uix/eidos/components/display/display.css
@@ -0,0 +1,55 @@
+/*
+ * Display recipe — hero typography primitive.
+ *
+ * Always consumes the foundation's `--style-hero-*` tokens (responsive:
+ * scales up at the `md` breakpoint to the next size step). Per-axis
+ * overrides via local `--_display-*` variables.
+ *
+ * Order of resolution:
+ * 1. Per-instance override (`--_display-{axis}`) inline by the wrapper.
+ * 2. Hero style token (`--style-hero-{axis}`) from the foundation.
+ */
+
+[data-display] {
+ font-family: var(--_display-font-family, var(--style-hero-font-family));
+ font-size: var(--_display-font-size, var(--style-hero-font-size));
+ font-weight: var(--_display-font-weight, var(--style-hero-font-weight));
+ line-height: var(--_display-line-height, var(--style-hero-line-height));
+ letter-spacing: var(--_display-letter-spacing, var(--style-hero-letter-spacing));
+ color: var(--_display-color, var(--style-hero-color));
+ text-align: var(--_display-align, inherit);
+ font-style: normal;
+ text-decoration: none;
+ margin: 0;
+ display: block;
+}
+
+[data-display][data-italic] {
+ font-style: italic;
+}
+
+[data-display][data-underline] {
+ text-decoration: underline;
+ text-underline-offset: 0.12em;
+}
+
+[data-display][data-truncate] {
+ max-inline-size: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+[data-display][data-clamp] {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: var(--_display-clamp, 1);
+ line-clamp: var(--_display-clamp, 1);
+ overflow: hidden;
+}
+
+[data-display][data-clamp][data-truncate] {
+ display: -webkit-box;
+ white-space: normal;
+ text-overflow: clip;
+}
diff --git a/src/uix/eidos/components/display/display.svelte b/src/uix/eidos/components/display/display.svelte
new file mode 100644
index 000000000..8842633d6
--- /dev/null
+++ b/src/uix/eidos/components/display/display.svelte
@@ -0,0 +1,91 @@
+
+
+ 0 ? '' : undefined}
+ style={inlineStyle}
+>
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/display/index.ts b/src/uix/eidos/components/display/index.ts
new file mode 100644
index 000000000..fb5da997b
--- /dev/null
+++ b/src/uix/eidos/components/display/index.ts
@@ -0,0 +1,27 @@
+// Display — eidos single-component hero typography primitive.
+//
+// import { Display } from '$uix/eidos/components/display';
+//
+// Welcome to UIX.
+// Big.
+//
+// Single-component primitive — `` IS the root. Renders as
+// `` by default; `as` switches the tag. Eidos-native: no soma
+// backing, no semantic events.
+import DisplayComponent from './display.svelte';
+
+const Display = DisplayComponent;
+
+export { Display };
+
+export default Display;
+
+export type {
+ DisplayProps,
+ DisplaySize,
+ DisplayFamily,
+ DisplayWeight,
+ DisplayColor,
+ DisplayAlign,
+ DisplayTracking
+} from './types';
diff --git a/src/uix/eidos/components/display/types.ts b/src/uix/eidos/components/display/types.ts
new file mode 100644
index 000000000..0ee8d7d66
--- /dev/null
+++ b/src/uix/eidos/components/display/types.ts
@@ -0,0 +1,71 @@
+import type { Snippet } from 'svelte';
+import type { HTMLAttributes } from 'svelte/elements';
+import type { ResponsiveProp } from '$uix/eidos/lib/types';
+
+/** Discrete size scale — matches eidos shared `Size`. */
+export type DisplaySize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
+
+/** Font family slot. */
+export type DisplayFamily = 'primary' | 'secondary' | 'mono' | 'display';
+
+/** Weight slot. */
+export type DisplayWeight = 'regular' | 'medium' | 'semibold' | 'bold';
+
+/** Content-role color slot. */
+export type DisplayColor = 'primary' | 'secondary' | 'muted' | 'disabled' | 'on-solid';
+
+/** Horizontal alignment. */
+export type DisplayAlign = 'start' | 'center' | 'end' | 'justify';
+
+/**
+ * Display lets the consumer override letter-spacing because at hero
+ * sizes (44-60px) tracking becomes a design decision. Maps to a fixed
+ * em-based value chosen for hero copy:
+ * - `tight` → -0.02em
+ * - `normal` → 0
+ * - `wide` → 0.02em
+ */
+export type DisplayTracking = 'tight' | 'normal' | 'wide';
+
+export type DisplayProps = Omit, 'style' | 'children'> & {
+ /**
+ * Override the hero style's font-size. Defaults to whatever the
+ * `--style-hero-font-size` token emits (responsive: scales up at
+ * the `md` breakpoint).
+ */
+ size?: ResponsiveProp;
+ /** Override the hero style's font-family. */
+ family?: ResponsiveProp;
+ /** Override the hero style's font-weight. */
+ weight?: ResponsiveProp;
+ /** Override the hero style's color. */
+ color?: ResponsiveProp;
+ /** Text alignment. */
+ align?: ResponsiveProp;
+ /**
+ * Explicit tracking override. At display sizes letter-spacing is a
+ * design choice, not a derived value. Maps to
+ * `var(--font-letter-spacing-{key})`.
+ */
+ tracking?: ResponsiveProp;
+ /** Italic shorthand. @default false */
+ italic?: boolean;
+ /** Underline shorthand. @default false */
+ underline?: boolean;
+ /** Single-line truncation with ellipsis. */
+ truncate?: boolean;
+ /** Multi-line clamp via CSS `-webkit-line-clamp`. Set 0 to disable. */
+ clamp?: number;
+ /**
+ * HTML tag to render. Default `` — Display is most often used
+ * as the page hero title and should slot into the heading hierarchy
+ * accordingly.
+ * @default 'h1'
+ */
+ as?: string;
+ /** Extra inline style — merged after the recipe variables. */
+ tagStyle?: string;
+ /** Extra class names. */
+ class?: string;
+ children?: Snippet;
+};
diff --git a/src/uix/eidos/components/heading/README.md b/src/uix/eidos/components/heading/README.md
new file mode 100644
index 000000000..621537a26
--- /dev/null
+++ b/src/uix/eidos/components/heading/README.md
@@ -0,0 +1,108 @@
+# Eidos Heading
+
+Semantic heading typography primitive. `level` chooses the HTML tag
+(`h1`..`h6`), `style` chooses the visual treatment — by default they
+mirror each other (`` renders `` with the h1
+style), but they can be decoupled (``)
+for the common case where semantics and visual weight diverge.
+
+## Superficie
+
+```svelte
+Page title
+Hero section
+Subhead
+Override scale + color
+Single-line headline that may overflow…
+Two-line clamped title.
+```
+
+## Baseline
+
+Origen: `air/components/typography/heading` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Visual + semantic split**: air carried `size` (visual paso 1..12) +
+ `level` (1..6) independent props. Eidos renames `size` to `style`
+ (`h1..h6`) tying it to the foundation tokens, while keeping `level`
+ for HTML semantics. The two default-mirror each other but can be
+ decoupled.
+- **Sizes**: air `1..12` → eidos `xxs..xxxl` mapping to the foundation
+ `--font-size-{key}` axis. Available as `size` override.
+- **Families**: air `'sans'|'serif'` → eidos
+ `primary/secondary/mono/display` from the foundation contract.
+- **Named foundation styles**: nuevo respecto a air. The recipe
+ consumes `--style-h{n}-*` (family + size + weight + line-height +
+ letter-spacing + color), with responsive overrides at `md` for
+ `h1`/`h2`. Per-prop overrides ganan vía local `--_heading-{axis}`.
+
+## Comparativa
+
+| Capacidad | UIX Heading | Radix Themes Heading | Chakra Heading | Mantine Title | MUI Typography |
+| --- | --- | --- | --- | --- | --- |
+| Semantic level prop | Sí (`level`) | Sí (`as`) | Sí (`as`) | Sí (`order`) | Sí (`component`) |
+| Visual style prop (decoupled) | Sí (`style`) | Numerical `size` | `textStyle` | Numerical `order` only | `variant` |
+| Decouple level vs style | Sí | Sí | Sí | Limited (order ties both) | Sí |
+| Size override | Sí (`size`) | Sí (`size`) | Sí (`fontSize`) | Sí (`size`) | Sí (`fontSize`) |
+| Family override | Sí (`family`) | Sí | Sí (`fontFamily`) | Sí (`ff`) | Sí |
+| Weight override | Sí | Sí (`weight`) | Sí (`fontWeight`) | Sí (`fw`) | Sí |
+| Color override (content-role) | Sí | Sí (intent) | Sí (free) | Sí (free) | Sí (free) |
+| Align | Sí | Sí | Sí | Sí (`ta`) | Sí |
+| Italic shorthand | Sí | No | Sí | Sí | Sí |
+| Underline shorthand | Sí | Sí | Sí | Sí | Sí |
+| Truncate | Sí | Sí | Sí | Sí | Sí |
+| Clamp (multi-line) | Sí (`clamp={N}`) | No | Sí (`noOfLines`) | Sí (`lineClamp`) | No |
+| Responsive props | Sí | Sí | Sí | Sí | Sí |
+
+## Decisiones
+
+- **`level` drives the rendered tag**: rendering `` keeps
+ the heading hierarchy correct for screen readers. The `as` prop
+ is available as an escape hatch when the visual look is needed
+ outside heading semantics.
+- **`style` mirrors `level` by default**: avoids a footgun where
+ consumers forget to set both. Common case is "h2 looks like h2";
+ decoupling is opt-in (`level={3} style="h1"`).
+- **Named foundation styles only**: keeps the visual scale aligned
+ with the design tokens. Per-axis overrides via `size`, `family`,
+ `weight` give granular control without breaking the contract.
+- **No `tracking` axis prop**: Display owns the explicit letter-spacing
+ prop because at hero sizes the value is a design decision; for
+ h1..h6 the foundation token already encodes the right tracking.
+- **`color` restricted to content-role slots**: same contract as Text.
+ Free hex/rgb values go through `tagStyle` escape hatch.
+- **Default tag is ``**: matches the most common in-page heading
+ level — h1 is reserved for the page title (use `level={1}`
+ explicitly). Mirrors air's default.
+
+## Eventos Sema
+
+Heading declara 0 eventos. Como Text/Box/Avatar, es una primitiva
+pasiva. Componentes que animan headings en aparición componen
+Heading dentro de un primitive interactivo (dialog, collapsible,
+toast) que posee los verbos sema relevantes.
+
+## Gaps
+
+| Gap | Disposición | Detalle |
+| --- | --- | --- |
+| Canvas-based line-count measurement (`text` string + bindable `lineCount`) | **diferir** | Pesa y complica SSR. CSS `-webkit-line-clamp` cubre el caso común; añadir cuando emerja la demanda real. |
+| Anchor-link (`#fragment` heading) integration | **diferir** | Patrón docs-specific, no del primitive. Construir un wrapper `` cuando la docs lo necesite. |
+| `tracking` prop como en Display | **descartar** | No-goal: h1..h6 tienen tracking definido por el token; si se necesita ajuste, usar `tagStyle`. |
+| Color libre (hex / CSS custom) en `color` prop | **descartar** | Rompe contrato de contraste. Escape hatch vía `tagStyle`. |
+| Variants tipográficas (small-caps, oldstyle figures) | **diferir** | Bajo prioridad. |
+
+## Referencias
+
+- Radix Themes Heading: https://www.radix-ui.com/themes/docs/components/heading
+- Chakra UI Heading: https://chakra-ui.com/docs/components/heading
+- Mantine Title: https://mantine.dev/core/title/
+- MUI Typography: https://mui.com/material-ui/react-typography/
+
+## Passive justification
+
+Visual-only (`scope: ['eidos']`). Una sola part Provider, marker
+`[data-heading]`. Sin estados, sin keyboard, sin ARIA propia (la
+semántica viene del tag `` que el navegador y AT entienden
+nativamente). La recipe consume `--style-h{n}-*` del foundation +
+`--_heading-{axis}` overrides escritas inline.
diff --git a/src/uix/eidos/components/heading/heading.css b/src/uix/eidos/components/heading/heading.css
new file mode 100644
index 000000000..cd9840fa0
--- /dev/null
+++ b/src/uix/eidos/components/heading/heading.css
@@ -0,0 +1,106 @@
+/*
+ * Heading recipe — semantic heading primitive.
+ *
+ * Applies one of the foundation's `--style-h{n}-*` named typography styles
+ * (h1..h6) per the `data-style` attribute, with per-axis overrides via
+ * local `--_heading-*` variables. The default `data-style` mirrors `level`,
+ * so `` renders the h1 style automatically.
+ *
+ * Order of resolution (per CSS variable cascade):
+ * 1. Per-instance override (`--_heading-{axis}`) set inline by the
+ * wrapper when the consumer passes a prop like `size="lg"`.
+ * 2. Named style token (`--style-h{n}-{axis}`) selected by `data-style`.
+ *
+ * Headings inherit a `margin: 0` reset — flow margins live on the
+ * containing layout primitive (Stack / Section).
+ */
+
+[data-heading] {
+ font-family: var(--_heading-font-family, var(--style-h2-font-family));
+ font-size: var(--_heading-font-size, var(--style-h2-font-size));
+ font-weight: var(--_heading-font-weight, var(--style-h2-font-weight));
+ line-height: var(--_heading-line-height, var(--style-h2-line-height));
+ letter-spacing: var(--_heading-letter-spacing, var(--style-h2-letter-spacing));
+ color: var(--_heading-color, var(--style-h2-color));
+ text-align: var(--_heading-align, inherit);
+ font-style: normal;
+ text-decoration: none;
+ margin: 0;
+}
+
+[data-heading][data-style='h1'] {
+ font-family: var(--_heading-font-family, var(--style-h1-font-family));
+ font-size: var(--_heading-font-size, var(--style-h1-font-size));
+ font-weight: var(--_heading-font-weight, var(--style-h1-font-weight));
+ line-height: var(--_heading-line-height, var(--style-h1-line-height));
+ letter-spacing: var(--_heading-letter-spacing, var(--style-h1-letter-spacing));
+ color: var(--_heading-color, var(--style-h1-color));
+}
+
+[data-heading][data-style='h3'] {
+ font-family: var(--_heading-font-family, var(--style-h3-font-family));
+ font-size: var(--_heading-font-size, var(--style-h3-font-size));
+ font-weight: var(--_heading-font-weight, var(--style-h3-font-weight));
+ line-height: var(--_heading-line-height, var(--style-h3-line-height));
+ letter-spacing: var(--_heading-letter-spacing, var(--style-h3-letter-spacing));
+ color: var(--_heading-color, var(--style-h3-color));
+}
+
+[data-heading][data-style='h4'] {
+ font-family: var(--_heading-font-family, var(--style-h4-font-family));
+ font-size: var(--_heading-font-size, var(--style-h4-font-size));
+ font-weight: var(--_heading-font-weight, var(--style-h4-font-weight));
+ line-height: var(--_heading-line-height, var(--style-h4-line-height));
+ letter-spacing: var(--_heading-letter-spacing, var(--style-h4-letter-spacing));
+ color: var(--_heading-color, var(--style-h4-color));
+}
+
+[data-heading][data-style='h5'] {
+ font-family: var(--_heading-font-family, var(--style-h5-font-family));
+ font-size: var(--_heading-font-size, var(--style-h5-font-size));
+ font-weight: var(--_heading-font-weight, var(--style-h5-font-weight));
+ line-height: var(--_heading-line-height, var(--style-h5-line-height));
+ letter-spacing: var(--_heading-letter-spacing, var(--style-h5-letter-spacing));
+ color: var(--_heading-color, var(--style-h5-color));
+}
+
+[data-heading][data-style='h6'] {
+ font-family: var(--_heading-font-family, var(--style-h6-font-family));
+ font-size: var(--_heading-font-size, var(--style-h6-font-size));
+ font-weight: var(--_heading-font-weight, var(--style-h6-font-weight));
+ line-height: var(--_heading-line-height, var(--style-h6-line-height));
+ letter-spacing: var(--_heading-letter-spacing, var(--style-h6-letter-spacing));
+ color: var(--_heading-color, var(--style-h6-color));
+}
+
+[data-heading][data-italic] {
+ font-style: italic;
+}
+
+[data-heading][data-underline] {
+ text-decoration: underline;
+ text-underline-offset: 0.15em;
+}
+
+[data-heading][data-truncate] {
+ display: inline-block;
+ max-inline-size: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ vertical-align: bottom;
+}
+
+[data-heading][data-clamp] {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: var(--_heading-clamp, 1);
+ line-clamp: var(--_heading-clamp, 1);
+ overflow: hidden;
+}
+
+[data-heading][data-clamp][data-truncate] {
+ display: -webkit-box;
+ white-space: normal;
+ text-overflow: clip;
+}
diff --git a/src/uix/eidos/components/heading/heading.svelte b/src/uix/eidos/components/heading/heading.svelte
new file mode 100644
index 000000000..87704962e
--- /dev/null
+++ b/src/uix/eidos/components/heading/heading.svelte
@@ -0,0 +1,86 @@
+
+
+ 0 ? '' : undefined}
+ style={inlineStyle}
+>
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/heading/index.ts b/src/uix/eidos/components/heading/index.ts
new file mode 100644
index 000000000..1af2cab51
--- /dev/null
+++ b/src/uix/eidos/components/heading/index.ts
@@ -0,0 +1,29 @@
+// Heading — eidos single-component typography primitive.
+//
+// import { Heading } from '$uix/eidos/components/heading';
+//
+// Page title
+// Hero title
+// Section
+//
+// Single-component primitive — `` IS the root, no compound
+// parts. Renders as `` (default ``). Eidos-native: no
+// soma backing, no semantic events.
+import HeadingComponent from './heading.svelte';
+
+const Heading = HeadingComponent;
+
+export { Heading };
+
+export default Heading;
+
+export type {
+ HeadingProps,
+ HeadingStyle,
+ HeadingLevel,
+ HeadingSize,
+ HeadingFamily,
+ HeadingWeight,
+ HeadingColor,
+ HeadingAlign
+} from './types';
diff --git a/src/uix/eidos/components/heading/types.ts b/src/uix/eidos/components/heading/types.ts
new file mode 100644
index 000000000..deeb71cc7
--- /dev/null
+++ b/src/uix/eidos/components/heading/types.ts
@@ -0,0 +1,69 @@
+import type { Snippet } from 'svelte';
+import type { HTMLAttributes } from 'svelte/elements';
+import type { ResponsiveProp } from '$uix/eidos/lib/types';
+
+/** Named heading styles emitted by the foundation as `--style-h{n}-*`. */
+export type HeadingStyle = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
+
+/** Semantic HTML heading level (h1..h6). */
+export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
+
+/** Discrete size scale — matches eidos `Size`. */
+export type HeadingSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
+
+/** Font family slot. Maps to `--font-family-{key}`. */
+export type HeadingFamily = 'primary' | 'secondary' | 'mono' | 'display';
+
+/** Weight slot. Maps to `--font-weight-{key}`. */
+export type HeadingWeight = 'regular' | 'medium' | 'semibold' | 'bold';
+
+/** Content-role color slot. */
+export type HeadingColor = 'primary' | 'secondary' | 'muted' | 'disabled' | 'on-solid';
+
+/** Horizontal alignment. */
+export type HeadingAlign = 'start' | 'center' | 'end' | 'justify';
+
+export type HeadingProps = Omit, 'style' | 'children'> & {
+ /**
+ * Semantic HTML level — renders ``. Default `2`. Independent
+ * of `style`, allowing decoupling (`level={3} style="h1"` for visual h1
+ * with semantic h3).
+ * @default 2
+ */
+ level?: HeadingLevel;
+ /**
+ * Named typography style. When omitted, mirrors `level` (h1..h6).
+ * Applies the full `--style-h{n}-*` token set (family + size + weight +
+ * line-height + letter-spacing + color).
+ */
+ style?: HeadingStyle;
+ /** Override the style's font-size. */
+ size?: ResponsiveProp;
+ /** Override the style's font-family. */
+ family?: ResponsiveProp;
+ /** Override the style's font-weight. */
+ weight?: ResponsiveProp;
+ /** Override the style's color. */
+ color?: ResponsiveProp;
+ /** Text alignment. */
+ align?: ResponsiveProp;
+ /** Italic shorthand. @default false */
+ italic?: boolean;
+ /** Underline shorthand. @default false */
+ underline?: boolean;
+ /** Single-line truncation with ellipsis. @default false */
+ truncate?: boolean;
+ /** Multi-line clamp via CSS `-webkit-line-clamp`. Set 0 to disable. */
+ clamp?: number;
+ /**
+ * Override the rendered tag. By default the tag is derived from `level`
+ * (``). Use this to render a non-heading element while keeping
+ * the heading typography.
+ */
+ as?: string;
+ /** Extra inline style — merged after the recipe variables. */
+ tagStyle?: string;
+ /** Extra class names. */
+ class?: string;
+ children?: Snippet;
+};
diff --git a/src/uix/eidos/components/highlight/README.md b/src/uix/eidos/components/highlight/README.md
new file mode 100644
index 000000000..12955ae3b
--- /dev/null
+++ b/src/uix/eidos/components/highlight/README.md
@@ -0,0 +1,83 @@
+# Eidos Highlight
+
+Substring highlighter primitive. Wraps a `` shell containing the
+input `text` and replaces matches of `query` with `` elements
+that inherit the same recipe as the standalone `` primitive.
+Composes inside any inline-flow primitive (Text, Heading, paragraph).
+
+## Superficie
+
+```svelte
+
+
+
+```
+
+## Baseline
+
+Origen: `air/components/typography/highlight` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Reuses Mark recipe**: air's Highlight imported `../mark/mark.css`
+ directly. Eidos keeps the same idea — the inner `` elements
+ stamp `data-mark` so the Mark recipe applies without duplication.
+- **Palette canonized**: same migration as Mark — `yellow / green /
+ blue / red / purple` → intent vocabulary.
+- **Default `fulfill`**: consistent with Mark.
+- **Pure CSS**: the highlight shell is `` + `font: inherit`.
+ Air did the same.
+
+## Comparativa
+
+| Capacidad | UIX Highlight | Mantine Highlight | Radix Themes | Chakra Highlight |
+| --- | --- | --- | --- | --- |
+| Substring match | Sí | Sí | No | Sí |
+| Multiple terms | Sí (`query` array) | Sí (`highlight` array) | No | Sí (`query` array) |
+| Case-insensitive | Sí (`ignoreCase`) | Sí (`highlightStyles`) | No | Sí (`styles.matchCase`) |
+| Color palette | Intent | free | No | colorScheme |
+| Regex query | No (literal escape) | Yes (with prop) | No | No |
+| Inherits typography | Sí | Sí | No | Sí |
+
+## Decisiones
+
+- **Inner marks reuse Mark recipe**: avoids two parallel highlight
+ surfaces. The Mark recipe is the single visual source.
+- **Outer shell is ``**: keeps Highlight composable inline.
+ Consumers wrap it inside `` / `` for typography.
+- **`query` is literal**: each term is regex-escaped, so consumers
+ can pass user input safely. No `regex` opt-in today — if a power
+ use case emerges, add a `pattern` prop that accepts a `RegExp`.
+- **`ignoreCase` default `true`**: search-result UX expects
+ case-insensitive matching. Opt out via `ignoreCase={false}` when
+ the consumer needs case-sensitive logic (code search).
+- **No empty-query gotcha**: if `query` is empty / whitespace-only,
+ render the text unchanged (no marks).
+
+## Eventos Sema
+
+Highlight declara 0 eventos. Visual-only — el primitive es un
+formatter, no input.
+
+## Gaps
+
+| Gap | Disposición | Detalle |
+| --- | --- | --- |
+| Regex query support | **diferir** | Si emerge, añadir `pattern: RegExp` prop como alternativa a `query`. |
+| Token / word-boundary match | **diferir** | Requiere parser; cubrir cuando llegue un caso real. |
+| Snippet-based render override | **diferir** | Si un consumer necesita custom Mark per term, ya puede componer Mark manualmente sin Highlight. |
+| Per-term different colors | **diferir** | Hoy todos los matches usan el mismo `color`. Si emerge, aceptar un map `{ term: color }`. |
+| Free hex color | **descartar** | Mismo razón que Mark — contrato de contraste. |
+
+## Referencias
+
+- Mantine Highlight: https://mantine.dev/core/highlight/
+- Chakra UI Highlight: https://chakra-ui.com/docs/components/highlight
+- MDN ``: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark
+
+## Passive justification
+
+Visual-only (`scope: ['eidos']`). Una sola part Provider (`` shell). El primitive es un formatter: dado `text` +
+`query` produce un árbol de segmentos, marcando los matches con
+``. Sin estados, sin keyboard, sin ARIA propia.
+La recipe del mark inner es compartida con ``.
diff --git a/src/uix/eidos/components/highlight/highlight.css b/src/uix/eidos/components/highlight/highlight.css
new file mode 100644
index 000000000..2dbb3be1c
--- /dev/null
+++ b/src/uix/eidos/components/highlight/highlight.css
@@ -0,0 +1,20 @@
+/*
+ * Highlight recipe — substring highlighter shell.
+ *
+ * The shell is purely a structural anchor; all visible chrome lives on
+ * the inner `` elements which inherit the Mark recipe
+ * (see ../mark/mark.css). The shell consumes `font: inherit` so it
+ * composes inside any inline-flow primitive without breaking line
+ * height.
+ *
+ * Per-instance color is stamped via `data-color` on the outer shell
+ * for consumer overrides (the inner marks duplicate the value so the
+ * Mark recipe matches without traversing the cascade).
+ */
+
+[data-highlight] {
+ font: inherit;
+ color: inherit;
+ background: transparent;
+ display: inline;
+}
diff --git a/src/uix/eidos/components/highlight/highlight.svelte b/src/uix/eidos/components/highlight/highlight.svelte
new file mode 100644
index 000000000..c92f35fb8
--- /dev/null
+++ b/src/uix/eidos/components/highlight/highlight.svelte
@@ -0,0 +1,72 @@
+
+
+
+ {#each segments as seg}
+ {#if seg.mark}
+ {seg.text}
+ {:else}
+ {seg.text}
+ {/if}
+ {/each}
+
diff --git a/src/uix/eidos/components/highlight/index.ts b/src/uix/eidos/components/highlight/index.ts
new file mode 100644
index 000000000..2d25641aa
--- /dev/null
+++ b/src/uix/eidos/components/highlight/index.ts
@@ -0,0 +1,18 @@
+// Highlight — eidos single-component substring highlighter.
+//
+// import { Highlight } from '$uix/eidos/components/highlight';
+//
+//
+//
+//
+// Single-component primitive — `` IS the root. Renders a
+// `` shell wrapping matches in `` elements.
+import HighlightComponent from './highlight.svelte';
+
+const Highlight = HighlightComponent;
+
+export { Highlight };
+
+export default Highlight;
+
+export type { HighlightProps, HighlightColor } from './types';
diff --git a/src/uix/eidos/components/highlight/types.ts b/src/uix/eidos/components/highlight/types.ts
new file mode 100644
index 000000000..96a0a585f
--- /dev/null
+++ b/src/uix/eidos/components/highlight/types.ts
@@ -0,0 +1,22 @@
+import type { HTMLAttributes } from 'svelte/elements';
+import type { ColorRole, ResponsiveProp } from '$uix/eidos/lib/types';
+
+/** Intent palette for the highlight tint. */
+export type HighlightColor = ColorRole;
+
+export type HighlightProps = Omit, 'style' | 'children'> & {
+ /** Full text to render. Matches of `query` will be wrapped in ``. */
+ text: string;
+ /** Substring(s) to highlight. Pass an array for multiple matches. */
+ query: string | string[];
+ /** Case-insensitive match. @default true */
+ ignoreCase?: boolean;
+ /** Intent palette for the highlight tint. @default 'fulfill' */
+ color?: ResponsiveProp;
+ /** Render a different tag for the outer shell. @default 'span' */
+ as?: string;
+ /** Extra inline style on the outer shell. */
+ tagStyle?: string;
+ /** Extra class names on the outer shell. */
+ class?: string;
+};
diff --git a/src/uix/eidos/components/kbd/README.md b/src/uix/eidos/components/kbd/README.md
new file mode 100644
index 000000000..09aa1233f
--- /dev/null
+++ b/src/uix/eidos/components/kbd/README.md
@@ -0,0 +1,82 @@
+# Eidos Kbd
+
+Keyboard shortcut typography primitive. Renders `` with a small
+chrome (border + background + inner padding) so the key looks like a
+physical cap. Inline within prose — for shortcut hints, key sequences.
+
+## Superficie
+
+```svelte
+Press Ctrl + K to open search.
+Hit Esc to dismiss.
+Type tab to switch focus.
+```
+
+## Baseline
+
+Origen: `air/components/typography/kbd` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Default size aligned with foundation**: air defaulted `size=2`
+ (small). Eidos reads `--font-size-xs` directly so the default
+ pulls from the foundation contract instead of a numeric paso.
+- **Variants added**: `surface` (default, filled with neutral element
+ bg + border), `outline` (transparent + border), `ghost` (no chrome).
+ Air had only one treatment.
+- **Lower border emphasis preserved**: `surface` keeps the
+ `border-bottom-width: 2px` + inset shadow trick to read like a
+ physical cap; `outline` and `ghost` drop it for cleaner inline
+ flow.
+- **Tokens**: `--air-kbd-*` → consume the foundation directly
+ (`--style-code-*`, `--color-neutral-*`, `--font-size-{key}`).
+
+## Comparativa
+
+| Capacidad | UIX Kbd | Radix Themes Kbd | Chakra Kbd | Mantine Kbd | shadcn/ui Kbd |
+| --- | --- | --- | --- | --- | --- |
+| Visual key-cap chrome | Sí | Sí | Sí | Sí | Sí |
+| Variants | surface / outline / ghost | size only | colorScheme | none | none |
+| Size override | Sí | Sí | Sí | Sí | inherits font-size |
+| Mono family | Sí (`--style-code-font-family`) | Sí | Sí | Sí | Sí |
+| Inline semantics (``) | Sí | Sí | Sí | Sí | Sí |
+| `as` polymorphism | Sí | Sí | Sí | Sí | No |
+
+## Decisiones
+
+- **Default `surface` variant**: the chrome is the entire point of the
+ primitive — bare mono text would just be ``.
+- **Mono family from `--style-code-font-family`**: same source as Code
+ / CodeBlock. Keeps the typography contract single-rooted.
+- **`min-inline-size: 1.5em`**: ensures single-char keys ("K", "1")
+ read as square caps and don't collapse to text width.
+- **No platform key mapping**: air did not ship `` to swap
+ `Ctrl` → `⌘` on macOS. Defer until a clear use case emerges; the
+ consumer can branch on `navigator.platform` today.
+
+## Eventos Sema
+
+Kbd declara 0 eventos. Visual-only — el shortcut event lives on
+the consumer (a Toolbar button, a global key handler), no en el
+primitive de label.
+
+## Gaps
+
+| Gap | Disposición | Detalle |
+| --- | --- | --- |
+| Platform-aware key swap (`Ctrl` ↔ `⌘`) | **diferir** | El consumer branches en `navigator.platform`. Si el patrón se repite, añadir un helper `kbdForPlatform()` o un wrapper docs-level. |
+| Sequence helper (``) | **diferir** | Composable inline — `Ctrl + K` cubre el caso sin contratar más superficie. |
+| Sound on key press demo (educational) | **descartar** | El primitive es label, no input. Sound on press pertenece a una demo o componente Sound separado. |
+
+## Referencias
+
+- Radix Themes Kbd: https://www.radix-ui.com/themes/docs/components/kbd
+- Chakra UI Kbd: https://chakra-ui.com/docs/components/kbd
+- Mantine Kbd: https://mantine.dev/core/kbd/
+- MDN ``: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd
+
+## Passive justification
+
+Visual-only (`scope: ['eidos']`). Una sola part Provider, marker
+`[data-kbd]`. Sin estados, sin keyboard, sin ARIA propia. La recipe
+consume `--style-code-*` + `--color-neutral-*` del foundation + el
+override `--_kbd-font-size` inline.
diff --git a/src/uix/eidos/components/kbd/index.ts b/src/uix/eidos/components/kbd/index.ts
new file mode 100644
index 000000000..2133e43f7
--- /dev/null
+++ b/src/uix/eidos/components/kbd/index.ts
@@ -0,0 +1,18 @@
+// Kbd — eidos single-component keyboard shortcut primitive.
+//
+// import { Kbd } from '$uix/eidos/components/kbd';
+//
+// Ctrl + K
+// Esc
+//
+// Single-component primitive — `` IS the root. Renders as
+// `` by default.
+import KbdComponent from './kbd.svelte';
+
+const Kbd = KbdComponent;
+
+export { Kbd };
+
+export default Kbd;
+
+export type { KbdProps, KbdSize, KbdVariant } from './types';
diff --git a/src/uix/eidos/components/kbd/kbd.css b/src/uix/eidos/components/kbd/kbd.css
new file mode 100644
index 000000000..5ceffd4df
--- /dev/null
+++ b/src/uix/eidos/components/kbd/kbd.css
@@ -0,0 +1,49 @@
+/*
+ * Kbd recipe — keyboard shortcut visual.
+ *
+ * Uses the foundation's code typography (mono family + small size) plus
+ * a chrome that mimics a physical key cap. The `data-variant` flag
+ * picks the chrome treatment:
+ * - `surface` filled with neutral element bg + border
+ * - `outline` transparent fill, border only
+ * - `ghost` no chrome, mono font only
+ */
+
+[data-kbd] {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--style-code-font-family);
+ font-size: var(--_kbd-font-size, var(--font-size-xs));
+ font-weight: var(--font-weight-medium);
+ line-height: 1;
+ color: var(--color-content-primary);
+ background: transparent;
+ border: 0;
+ border-radius: var(--radius-sm);
+ padding-inline: 0.4em;
+ padding-block: 0.18em;
+ min-inline-size: 1.5em;
+ vertical-align: baseline;
+ user-select: none;
+ white-space: nowrap;
+ letter-spacing: 0.02em;
+}
+
+[data-kbd][data-variant='surface'] {
+ background: var(--color-neutral-element);
+ border: 1px solid var(--color-neutral-border);
+ border-bottom-width: 2px;
+ box-shadow: 0 1px 0 var(--color-neutral-border) inset;
+}
+
+[data-kbd][data-variant='outline'] {
+ background: transparent;
+ border: 1px solid var(--color-neutral-border);
+}
+
+[data-kbd][data-variant='ghost'] {
+ background: transparent;
+ border: 0;
+ padding-inline: 0;
+}
diff --git a/src/uix/eidos/components/kbd/kbd.svelte b/src/uix/eidos/components/kbd/kbd.svelte
new file mode 100644
index 000000000..f6a47f53b
--- /dev/null
+++ b/src/uix/eidos/components/kbd/kbd.svelte
@@ -0,0 +1,51 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/uix/eidos/components/kbd/types.ts b/src/uix/eidos/components/kbd/types.ts
new file mode 100644
index 000000000..9ab189207
--- /dev/null
+++ b/src/uix/eidos/components/kbd/types.ts
@@ -0,0 +1,23 @@
+import type { Snippet } from 'svelte';
+import type { HTMLAttributes } from 'svelte/elements';
+import type { ResponsiveProp } from '$uix/eidos/lib/types';
+
+/** Discrete size scale. */
+export type KbdSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+/** Visual treatment. */
+export type KbdVariant = 'surface' | 'outline' | 'ghost';
+
+export type KbdProps = Omit, 'style' | 'children'> & {
+ /** Visual treatment. @default 'surface' */
+ variant?: ResponsiveProp;
+ /** Size override. @default inherits foundation `--style-code-font-size` */
+ size?: ResponsiveProp;
+ /** Render a different tag. @default 'kbd' */
+ as?: string;
+ /** Extra inline style. */
+ tagStyle?: string;
+ /** Extra class names. */
+ class?: string;
+ children?: Snippet;
+};
diff --git a/src/uix/eidos/components/link/README.md b/src/uix/eidos/components/link/README.md
new file mode 100644
index 000000000..dd373a778
--- /dev/null
+++ b/src/uix/eidos/components/link/README.md
@@ -0,0 +1,112 @@
+# Eidos Link
+
+Inline text link primitive. Renders `` by default — or
+`` when disabled — so the link composes
+mid-paragraph without breaking the ambient typography. Visual
+variants drive color + underline; `external` adds the standard
+`target="_blank"` + `rel="noopener noreferrer"` + accessibility
+affordances.
+
+## Superficie
+
+```svelte
+Read the documentation.
+GitHub
+Tertiary
+Unavailable
+```
+
+## Baseline
+
+Origen: `air/components/typography/link` (rama `morfo-runtime`).
+Adaptaciones para eidos:
+
+- **Variants**: air shipped `default / subtle / quiet / emphasized`.
+ Eidos consolidates to `default / subtle / plain`:
+ - `default` = colored underline-on-hover (most common)
+ - `subtle` = inherits color, underline-on-hover (sidebar / nav)
+ - `plain` = inherits color, no underline (button-like inline)
+ `emphasized` is replaceable via `weight` / `color` overrides on Text
+ if the user truly needs a bold link.
+- **Underline contract preserved**: `none / hover / always` — air's
+ shape.
+- **`color` opened to full intent palette**: air locked to a single
+ `primary` color. Eidos accepts the canonical `ColorRole` union so
+ destructive-link variants (`color="risk"`) and affirmative
+ (`color="affirm"`) compose with the rest of the system.
+- **`size` reduced to xxs..xl**: air used `sm/md/lg`. Eidos aligns
+ with the foundation `--font-size-{key}` scale; default inherits
+ ambient typography.
+- **External-link icon kept**: tiny inline SVG + SR-only "(opens in
+ new tab)" text. Air's pattern.
+- **`disabled` renders ``**: matches air; drops
+ `href`, disables pointer events.
+
+## Comparativa
+
+| Capacidad | UIX Link | Radix Themes Link | Chakra Link | Mantine Anchor |
+| --- | --- | --- | --- | --- |
+| Inline anchor element | Sí | Sí | Sí | Sí |
+| Variant treatments | default / subtle / plain | size + weight | colorScheme | underline + colors |
+| Underline control | none / hover / always | hover only | hover + always | hover + always |
+| Color (intent palette) | Sí (full ColorRole) | accent | colorScheme | colors |
+| External link affordance | Sí (`external` + SR text + icon) | manual | manual | manual |
+| Disabled state | Sí | No | Sí (custom) | Sí |
+| Inherits typography | Sí | Sí | Sí | Sí |
+| `size` override | Sí | Sí (size scale) | Sí | Sí |
+
+## Decisiones
+
+- **Inherits ambient typography**: Link is mid-paragraph; the font /
+ weight / line-height come from the surrounding `` /
+ `` / ``. Local overrides are limited to size, color,
+ underline.
+- **`color` opens to full ColorRole**: enables `color="risk"` for
+ destructive links (`Delete account`) and `color="affirm"` for
+ positive actions, while keeping the contrast contract via the
+ intent palette.
+- **Default `variant='default'` + `underline='hover'`**: matches
+ Radix / Chakra default. The colored-underline-on-hover is the
+ most familiar inline link affordance.
+- **`disabled` renders ``**: keeps the anchor
+ out of the tab order without removing the visual position. The
+ consumer can swap to a `