feat(typography): Text — POC consuming new --style-* foundation tokens

First typography primitive consuming the named-style tokens added in
`015bd761`. Sets the canon for the remaining typography ports
(Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link).

API:

- `style: 'body' | 'prose' | 'label' | 'caption'` (default `body`).
  Selects which `--style-{name}-*` token set the recipe consumes.
- Per-axis overrides via `size`, `family`, `weight`, `color`, `align`.
  Each writes a local `--_text-{axis}` CSS variable that wins over
  the named style's default via the cascade.
- `italic`, `underline` shorthand booleans.
- `truncate` (single-line ellipsis) + `clamp={N}` (multi-line via
  CSS `-webkit-line-clamp`).
- `as` prop switches the tag (default `'span'` — inline composable).
- `tagStyle` escape hatch for one-off inline CSS that the prop
  surface doesn't cover (separate from the `style` named-style prop).

Architectural choices documented in README:

- Sizes map to eidos `xxs..xxxl` (not air's `1..12`).
- Families use the eidos slot names (`primary/secondary/mono/display`)
  not air's domain aliases (`ui/prose/code`) — coherence with the
  foundation contract.
- Colors are restricted to the content-role slots
  (`--color-content-*`) to preserve contrast guarantees. Free colors
  via `tagStyle`.
- Canvas line-count measurement (air's `text` + bindable `lineCount`)
  deferred; CSS line-clamp covers the visual-truncate use case.

Sidebar nav gains a `Typography` group with Text as its first entry.

`npm run component:audit`: 82 / 82 PASS, 0 NEEDS-WORK. svelte-check 0
errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 015bd761bd
commit 0353170982

@ -0,0 +1,115 @@
# Eidos Text
Universal inline-flow typography primitive. Aplica uno de los named
styles del foundation (`body` / `prose` / `label` / `caption`) y
permite sobrescribir per-axis (size / family / weight / color / align)
sin tocar el resto. Render por defecto `<span>` — composable dentro de
cualquier contenedor.
## Superficie
```svelte
<Text>Body copy.</Text>
<Text style="prose" size="lg">Longer reading passage.</Text>
<Text style="caption" italic>Image credit.</Text>
<Text truncate>Single-line label that may overflow…</Text>
<Text clamp={3}>Three-line clamped paragraph.</Text>
<Text as="p" align="center">Centered block paragraph.</Text>
```
## Baseline
Origen: `air/components/typography/text` (rama `morfo-runtime`).
Adaptaciones para eidos:
- **Sizes**: air `1..12` → eidos `xxs/xs/sm/md/lg/xl/xxl/xxxl`. Sin escala
separada — usamos el contrato existente `--font-size-{key}` /
`--font-line-height-{key}` / `--font-letter-spacing-{key}` del foundation.
- **Families**: air `'ui'|'prose'|'code'` → eidos `primary/secondary/mono/display`.
El foundation ya emite los aliases `--font-ui` / `--font-prose` / `--font-code`
pero preferimos referencias directas a `--font-family-{key}` para
claridad de origen.
- **Named styles**: nuevo respecto a air. Cuando se pasa `style="body"`
(default), `"prose"`, `"label"` o `"caption"`, el recipe consume los
tokens `--style-{name}-*` que el foundation emite (familia + size +
weight + line-height + letter-spacing + color, con overrides
responsive cuando aplica). Los per-prop overrides ganan vía local
`--_text-{axis}` declaradas inline por el componente.
- **Canvas measurement** del air (clamp con conteo exacto de líneas vía
canvas) se simplifica a CSS `-webkit-line-clamp` en este port —
ver Gaps.
## Comparativa
| Capacidad | UIX Text | Radix Themes Text | Chakra Text | Mantine Text |
| --- | --- | --- | --- | --- |
| Named style prop (body/prose/label/caption) | Sí (`style`) | Sí (`as` + `size`) | Sí (`textStyle`) | Sí (`size`) |
| Size scale | xxs..xxxl (8 steps) | 1..9 | xs..6xl | xs..xxl |
| Family override | Sí (`family`) | Sí | Sí (`fontFamily`) | Sí (`ff`) |
| Weight override | Sí (`weight`) | Sí | Sí (`fontWeight`) | Sí (`fw`) |
| Color override | Sí (`color` → content-role slot) | Sí (intent) | Sí (free) | Sí (free) |
| Align | Sí | Sí | Sí (`textAlign`) | Sí (`ta`) |
| Italic shorthand | Sí (`italic`) | No (style) | Sí (`fontStyle`) | Sí (`fs`) |
| Underline shorthand | Sí (`underline`) | Sí (`underline`) | Sí (`textDecoration`) | Sí (`td`) |
| Truncate (single-line ellipsis) | Sí (`truncate`) | Sí (`truncate`) | Sí (`isTruncated`) | Sí (`truncate`) |
| Line-clamp (multi-line) | Sí (`clamp={N}`, CSS only) | Sí (`truncate` + `weight`) | Sí (`noOfLines`) | Sí (`lineClamp`) |
| Canvas line-count measurement | **No** — gap | No | No | No |
| Bindable `lineCount` output | **No** — gap | No | No | No |
| `as` prop polymorphism | Sí | Sí | Sí | Sí |
| Responsive props | Sí (`ResponsiveProp`) | Sí | Sí | Sí |
## Decisiones
- **`style` prop apunta a named foundation tokens** — no recreamos los
combos family+size+weight. Cuando cambias el design system, los
Text con `style="body"` actualizan sin tocar componentes.
- **Per-prop overrides vía local CSS vars `--_text-*`** — escritas
inline en el span. El recipe usa `var(--_text-x, var(--style-{name}-x))`
para cascade limpio.
- **Default `<span>`, no `<p>`** — Text es inline-flow primitive.
Composable dentro de un `<h1>`, `<button>`, `<label>`, etc. Cuando
necesitas block-level, `as="p"`. Difiere de Mantine/Chakra que
default a `<p>`; alinea con el principio "the dumbest element by
default".
- **`color` siempre desde el content-role slot** — `primary` ↔
`--color-content-primary`. No aceptamos colores libres en este
primitive para mantener el contrato de contraste y la coherencia
con el theme.
- **Canvas measurement diferido** — air tenía un sistema canvas para
contar líneas exactas + bindable output. Pesa y complica el SSR.
CSS `-webkit-line-clamp` cubre el 95% de los casos (clamp visual,
no necesitas el conteo exacto si solo quieres truncar).
- **`tagStyle` separa del `style` prop** — el `style` HTML prop entra en
conflicto semántico con nuestra prop `style` (named style). El escape
hatch inline está expuesto vía `tagStyle`.
## Eventos Sema
Text declara 0 eventos. Como Box/Flex/etc., es una primitiva pasiva.
Componentes que animan/cambian estado en aparición componen Text
dentro de un primitive interactivo (collapsible, dialog, toast) que
posee los verbos sema relevantes.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Canvas line-count measurement (`text` string prop + bindable `lineCount`) | **diferir** | Pesa, complica SSR, y el 95% de casos se cubre con CSS line-clamp. Si llega un caso de uso (e.g. show "+N more lines" badge), añadir como mixin opt-in. |
| Hint visual cuando clamp recorta ("+N líneas más") | **diferir** | Requiere el canvas measurement. Sin él, no podemos saber si hay overflow real. |
| Color libre (hex / CSS custom) en `color` prop | **descartar** | Rompe contrato de contraste con theme. Usar `tagStyle` como escape hatch. |
| Variants tipográficas (small-caps, oldstyle figures, etc.) | **diferir** | Si emerge demanda, añadir feature flags. Bajo prioridad. |
| Text gradient | **diferir** | Composable con `tagStyle` o un componente `<GradientText>` dedicado. |
## Referencias
- Radix Themes Text: https://www.radix-ui.com/themes/docs/components/text
- Chakra UI Text: https://chakra-ui.com/docs/components/text
- Mantine Text: https://mantine.dev/core/text/
- MUI Typography: https://mui.com/material-ui/react-typography/
## Passive justification
Visual-only (`scope: ['eidos']`). Una sola part Provider, marker
`[data-text]`. Sin estados, sin keyboard, sin ARIA, sin eventos.
La recipe consume `--style-{name}-*` tokens del foundation +
`--_text-{axis}` overrides escritas inline.

@ -0,0 +1,29 @@
// Text — eidos compound API, disciplined option C.
//
// import { Text } from '$uix/eidos/components/text';
//
// <Text>Body copy.</Text>
// <Text style="prose">Reading passage.</Text>
// <Text style="caption" italic>Image credit.</Text>
//
// Single-component primitive — `<Text>` IS the root, no compound
// parts. Renders as `<span data-text>` by default; `as="p" | "div" |
// "em" | …` switches the tag. Eidos-native: no soma backing, no
// semantic events.
import TextComponent from './text.svelte';
const Text = TextComponent;
export { Text };
export default Text;
export type {
TextProps,
TextStyle,
TextSize,
TextFamily,
TextWeight,
TextColor,
TextAlign
} from './types';

@ -0,0 +1,93 @@
/*
* Text recipe — applies one of the foundation's `--style-{name}-*`
* named typography styles, with per-axis overrides via local `--_text-*`
* variables. Default style is `body`.
*
* Order of resolution (per CSS variable cascade):
* 1. Per-instance override (`--_text-{axis}`) set inline by the wrapper
* when the consumer passes a prop like `size="lg"`.
* 2. Named style token (`--style-{textStyle}-{axis}`) selected by the
* active `data-style` attribute.
*
* `revert-layer` is not used here — typography has authoritative defaults
* (it's never "unset" — there is always a body/prose/label/caption fallback).
*/
[data-text] {
font-family: var(--_text-font-family, var(--style-body-font-family));
font-size: var(--_text-font-size, var(--style-body-font-size));
font-weight: var(--_text-font-weight, var(--style-body-font-weight));
line-height: var(--_text-line-height, var(--style-body-line-height));
letter-spacing: var(--_text-letter-spacing, var(--style-body-letter-spacing));
color: var(--_text-color, var(--style-body-color));
text-align: var(--_text-align, inherit);
font-style: normal;
text-decoration: none;
margin: 0;
}
[data-text][data-style='prose'] {
font-family: var(--_text-font-family, var(--style-prose-font-family));
font-size: var(--_text-font-size, var(--style-prose-font-size));
font-weight: var(--_text-font-weight, var(--style-prose-font-weight));
line-height: var(--_text-line-height, var(--style-prose-line-height));
letter-spacing: var(--_text-letter-spacing, var(--style-prose-letter-spacing));
color: var(--_text-color, var(--style-prose-color));
}
[data-text][data-style='label'] {
font-family: var(--_text-font-family, var(--style-label-font-family));
font-size: var(--_text-font-size, var(--style-label-font-size));
font-weight: var(--_text-font-weight, var(--style-label-font-weight));
line-height: var(--_text-line-height, var(--style-label-line-height));
letter-spacing: var(--_text-letter-spacing, var(--style-label-letter-spacing));
color: var(--_text-color, var(--style-label-color));
}
[data-text][data-style='caption'] {
font-family: var(--_text-font-family, var(--style-caption-font-family));
font-size: var(--_text-font-size, var(--style-caption-font-size));
font-weight: var(--_text-font-weight, var(--style-caption-font-weight));
line-height: var(--_text-line-height, var(--style-caption-line-height));
letter-spacing: var(--_text-letter-spacing, var(--style-caption-letter-spacing));
color: var(--_text-color, var(--style-caption-color));
}
[data-text][data-italic] {
font-style: italic;
}
[data-text][data-underline] {
text-decoration: underline;
text-underline-offset: 0.15em;
}
/* Single-line truncation with ellipsis. Requires a constrained
* inline-size on the container; this rule sets the necessary primitives
* (white-space + overflow + text-overflow) so the consumer only needs
* to provide the width. */
[data-text][data-truncate] {
display: inline-block;
max-inline-size: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
/* Multi-line clamp via -webkit-line-clamp. Read the line count from
* the per-instance `--_text-clamp` (set inline by the wrapper). */
[data-text][data-clamp] {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--_text-clamp, 1);
line-clamp: var(--_text-clamp, 1);
overflow: hidden;
}
/* When clamp + truncate collide, clamp wins (multi-line). */
[data-text][data-clamp][data-truncate] {
display: -webkit-box;
white-space: normal;
text-overflow: clip;
}

@ -0,0 +1,83 @@
<script lang="ts">
/**
* Eidos `<Text>` — universal inline-flow typography primitive.
*
* Defaults to `style="body"` which pulls every typographic
* declaration (family, size, weight, line-height, letter-spacing,
* color) from the foundation's `--style-body-*` tokens. Individual
* props override the corresponding axis without touching the rest.
*
* <Text>Body copy.</Text>
* <Text style="prose" size="lg">Longer reading passage.</Text>
* <Text style="caption" italic>Image credit.</Text>
* <Text truncate>Single-line label that may overflow…</Text>
* <Text clamp={3}>Three-line clamped paragraph.</Text>
*
* `as` switches the rendered tag (`'span'` default, `'p'` /
* `'div'` / `'em'` / `'strong'` / `'small'` accepted). The
* `[data-text]` marker is the canonical selector for the recipe.
*/
import { ActiveEidos } from '$uix/eidos';
import type { TextProps } from './types';
let {
style: textStyle = 'body',
size,
family,
weight,
color,
align,
italic = false,
underline = false,
truncate = false,
clamp,
as = 'span',
tagStyle,
class: className,
children,
...restProps
}: TextProps = $props();
const eidos = ActiveEidos.require();
function pushVar(decls: string[], name: string, value: string | number | undefined): void {
if (value === undefined || value === null || value === '') return;
decls.push(`${name}: ${value};`);
}
const resolvedStyle = $derived.by(() => {
const decls: string[] = [];
const sizeKey = eidos.resolve(size);
const familyKey = eidos.resolve(family);
const weightKey = eidos.resolve(weight);
const colorKey = eidos.resolve(color);
const alignKey = eidos.resolve(align);
if (sizeKey) pushVar(decls, '--_text-font-size', `var(--font-size-${sizeKey})`);
if (sizeKey) pushVar(decls, '--_text-line-height', `var(--font-line-height-${sizeKey})`);
if (sizeKey) pushVar(decls, '--_text-letter-spacing', `var(--font-letter-spacing-${sizeKey})`);
if (familyKey) pushVar(decls, '--_text-font-family', `var(--font-family-${familyKey})`);
if (weightKey) pushVar(decls, '--_text-font-weight', `var(--font-weight-${weightKey})`);
if (colorKey) pushVar(decls, '--_text-color', `var(--color-content-${colorKey})`);
if (alignKey) pushVar(decls, '--_text-align', alignKey);
if (typeof clamp === 'number' && clamp > 0) pushVar(decls, '--_text-clamp', String(clamp));
const inline = decls.join(' ');
return [inline, tagStyle].filter(Boolean).join(' ');
});
</script>
<svelte:element
this={as}
{...restProps}
class={className}
data-text=""
data-style={textStyle}
data-italic={italic ? '' : undefined}
data-underline={underline ? '' : undefined}
data-truncate={truncate ? '' : undefined}
data-clamp={typeof clamp === 'number' && clamp > 0 ? '' : undefined}
style={resolvedStyle}
>
{@render children?.()}
</svelte:element>

@ -0,0 +1,67 @@
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { ResponsiveProp } from '$uix/eidos/lib/types';
/** Named typography styles emitted by the foundation as `--style-{name}-*`. */
export type TextStyle = 'body' | 'prose' | 'label' | 'caption';
/** Discrete size scale — matches eidos `TextSize`. */
export type TextSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
/** Font family slot. Maps to `--font-family-{key}` tokens. */
export type TextFamily = 'primary' | 'secondary' | 'mono' | 'display';
/** Weight slot. Maps to `--font-weight-{key}`. */
export type TextWeight = 'regular' | 'medium' | 'semibold' | 'bold';
/** Content-role color slot. Maps to `--color-content-{key}`. */
export type TextColor = 'primary' | 'secondary' | 'muted' | 'disabled' | 'on-solid';
/** Horizontal text alignment. */
export type TextAlign = 'start' | 'center' | 'end' | 'justify';
export type TextProps = Omit<HTMLAttributes<HTMLElement>, 'style' | 'children'> & {
/**
* Named typography style. Applies the full set of `--style-{name}-*`
* tokens (family + size + weight + line-height + letter-spacing +
* color) the foundation emits. Per-prop overrides win over the style
* defaults.
* @default 'body'
*/
style?: TextStyle;
/** Override the style's font-size. Maps to `var(--font-size-{key})`. */
size?: ResponsiveProp<TextSize>;
/** Override the style's font-family. Maps to `var(--font-family-{key})`. */
family?: ResponsiveProp<TextFamily>;
/** Override the style's font-weight. Maps to `var(--font-weight-{key})`. */
weight?: ResponsiveProp<TextWeight>;
/** Override the style's color. Maps to `var(--color-content-{key})`. */
color?: ResponsiveProp<TextColor>;
/** Text alignment. */
align?: ResponsiveProp<TextAlign>;
/** Italic shorthand for `font-style: italic`. @default false */
italic?: boolean;
/** Underline shorthand for `text-decoration: underline`. @default false */
underline?: boolean;
/**
* Truncate to a single line with ellipsis. Requires the container
* to have a constrained width. @default false
*/
truncate?: boolean;
/**
* Clamp to N lines via CSS `-webkit-line-clamp`. Takes precedence
* over `truncate`. Set `0` or omit to disable.
*/
clamp?: number;
/**
* HTML tag to render. Default `<span>` keeps Text inline-by-default
* so it composes inside any container.
* @default 'span'
*/
as?: string;
/** Extra inline style — merged after the recipe variables. */
tagStyle?: string;
/** Extra class names. */
class?: string;
children?: Snippet;
};

@ -73,6 +73,7 @@
@import './components/auto-grid/auto-grid.css'; @import './components/auto-grid/auto-grid.css';
@import './components/banner/banner.css'; @import './components/banner/banner.css';
@import './components/float/float.css'; @import './components/float/float.css';
@import './components/text/text.css';
@import './components/icon/icon.css'; @import './components/icon/icon.css';
@import './components/avatar/avatar.css'; @import './components/avatar/avatar.css';
@import './components/breadcrumb/breadcrumb.css'; @import './components/breadcrumb/breadcrumb.css';

@ -0,0 +1,32 @@
import type { Morfo } from '../types';
/**
* Text — universal inline-flow typography primitive.
*
* Eidos-native: a single `<span data-text>` shell that applies one of
* the named typography styles (body, prose, label, caption) via the
* `--style-{name}-*` tokens emitted by the foundation, with per-prop
* overrides for size / family / weight / color / align / truncation.
*
* Passive — no semantic events. Text is a styled inline shell that
* does not commit, emerge, or react to anything. Components that
* compose Text inside an interactive primitive own the relevant
* sema verbs.
*/
export const textMorfo = {
name: 'Text',
kebab: 'text',
scope: ['eidos'],
parts: [
{
name: 'Provider',
kebab: 'provider',
archetype: 'provider',
kind: 'public',
defaultElement: 'span',
optional: false,
data: [],
aria: []
}
]
} as const satisfies Morfo;

@ -197,6 +197,11 @@
{ slug: '/uix/components/tooltip', label: 'Tooltip' } { slug: '/uix/components/tooltip', label: 'Tooltip' }
] ]
}, },
{
kind: 'group',
heading: 'Typography',
items: [{ slug: '/uix/components/text', label: 'Text' }]
},
{ {
kind: 'group', kind: 'group',
heading: 'Layout', heading: 'Layout',

@ -0,0 +1,505 @@
<script lang="ts">
import {
Text,
type TextStyle,
type TextSize,
type TextFamily,
type TextWeight,
type TextColor,
type TextAlign
} from '$uix/eidos/components/text';
import { compileMorfo } from '$uix/morfo';
import { textMorfo } from '@/uix/morfo/components/text';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Live state ────────────────────────────────────────────────────────
let textStyle = $state<TextStyle>('body');
let size = $state<TextSize | ''>('');
let family = $state<TextFamily | ''>('');
let weight = $state<TextWeight | ''>('');
let color = $state<TextColor | ''>('');
let align = $state<TextAlign | ''>('');
let italic = $state(false);
let underline = $state(false);
let truncate = $state(false);
let clamp = $state<number>(0);
let content = $state(
'Eidos Text composes the named foundation typography styles with per-axis overrides. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
);
const styleOptions: TextStyle[] = ['body', 'prose', 'label', 'caption'];
const sizeOptions = ['', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl', 'xxxl'] as const;
const familyOptions = ['', 'primary', 'secondary', 'mono', 'display'] as const;
const weightOptions = ['', 'regular', 'medium', 'semibold', 'bold'] as const;
const colorOptions = ['', 'primary', 'secondary', 'muted', 'disabled'] as const;
const alignOptions = ['', 'start', 'center', 'end', 'justify'] as const;
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(textMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
// ── Code snippets ─────────────────────────────────────────────────────
const somaSnippet = $derived(
[
'<!-- Text is eidos-native — no soma layer. -->',
'<!-- Equivalent semantic markup (not real soma): -->',
'',
'<span',
' data-text',
` data-style="${textStyle}"`,
' style="font-family: var(--style-' + textStyle + '-font-family); …"',
'>',
' …',
'</span>'
].join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Text } from '$uix/eidos/components/text';",
'</' + 'script>',
'',
'<Text',
textStyle !== 'body' && ` style="${textStyle}"`,
size && ` size="${size}"`,
family && ` family="${family}"`,
weight && ` weight="${weight}"`,
color && ` color="${color}"`,
align && ` align="${align}"`,
italic && ' italic',
underline && ' underline',
truncate && ' truncate',
clamp > 0 && ` clamp={${clamp}}`,
'>',
' ' + content.slice(0, 50) + (content.length > 50 ? '…' : ''),
'</Text>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Typography · Text</div>
<h1 data-uix-page-title>Text</h1>
<p data-uix-page-lede>
Universal inline-flow typography primitive. Default renders as <code>&lt;span&gt;</code> applying
the foundation's <code>--style-body-*</code> tokens; the <code>style</code> prop switches to
<code>prose</code> / <code>label</code> / <code>caption</code>, and per-axis props (<code>size</code>,
<code>family</code>, <code>weight</code>, <code>color</code>, <code>align</code>) override
individual fields without touching the rest. Single part — Eidos-native, no soma backing, no
semantic events.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
<span data-uix-meta-key>parts</span>{compiled.parts.order.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>events</span>0
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>styles</span>4
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>sizes</span>8
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>eidos
</span>
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<div style="max-inline-size: 36rem; padding: var(--space-3); border: 1px dashed var(--color-border-default); border-radius: var(--radius-md);">
<Text
style={textStyle}
size={size || undefined}
family={family || undefined}
weight={weight || undefined}
color={color || undefined}
align={align || undefined}
{italic}
{underline}
{truncate}
clamp={clamp > 0 ? clamp : undefined}
as="p"
>
{content}
</Text>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>style</span>
<span>{textStyle}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>size</span> {size || 'inherit'} ·
<span data-uix-stage-trace-key>weight</span> {weight || 'inherit'}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>12</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p · 0e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
Text is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The
<code>style</code> prop selects one of the four named foundation styles
(body / prose / label / caption); the rest of the props override individual axes via
local <code>--_text-*</code> CSS variables. Unset = inherit from the active style.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · named style + axis overrides
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>style <span data-uix-control-hint>named foundation token</span></span>
<span data-uix-chips role="radiogroup">
{#each styleOptions as opt}
<button data-uix-chip data-active={textStyle === opt} onclick={() => (textStyle = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size override</span>
<span data-uix-chips role="radiogroup">
{#each sizeOptions as opt}
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt as TextSize | '')}>{opt || 'inherit'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>family override</span>
<span data-uix-chips role="radiogroup">
{#each familyOptions as opt}
<button data-uix-chip data-active={family === opt} onclick={() => (family = opt as TextFamily | '')}>{opt || 'inherit'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>weight override</span>
<span data-uix-chips role="radiogroup">
{#each weightOptions as opt}
<button data-uix-chip data-active={weight === opt} onclick={() => (weight = opt as TextWeight | '')}>{opt || 'inherit'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color override <span data-uix-control-hint>content-role slot</span></span>
<span data-uix-chips role="radiogroup">
{#each colorOptions as opt}
<button data-uix-chip data-active={color === opt} onclick={() => (color = opt as TextColor | '')}>{opt || 'inherit'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>align</span>
<span data-uix-chips role="radiogroup">
{#each alignOptions as opt}
<button data-uix-chip data-active={align === opt} onclick={() => (align = opt as TextAlign | '')}>{opt || 'inherit'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>italic</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={italic} />
<span data-uix-switch-label>{italic ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>underline</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={underline} />
<span data-uix-switch-label>{underline ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>truncate <span data-uix-control-hint>single-line ellipsis</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={truncate} />
<span data-uix-switch-label>{truncate ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>clamp <span data-uix-control-hint>multi-line cap (0 = off)</span></span>
<input type="number" min="0" max="10" step="1" bind:value={clamp} style="inline-size: 6rem;" />
</label>
<label data-uix-control style="grid-column: span 2;">
<span data-uix-control-label>content</span>
<textarea bind:value={content} rows="3" style="inline-size: 100%; font: inherit;"></textarea>
</label>
</div>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>n/a · text is eidos-native — equivalent markup shown</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · style + per-axis overrides</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
The <code>style</code> prop selects one of four named foundation tokens
(<code>--style-{`{name}`}-{`{axis}`}</code>). Per-axis props (<code>size</code>,
<code>family</code>, <code>weight</code>, <code>color</code>) emit local
<code>--_text-{`{axis}`}</code> CSS variables that override the named-style defaults.
Unset overrides inherit the style's value.
</p>
<div data-uix-subsection-head>Named style</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">style</td><td class="type">'body' | 'prose' | 'label' | 'caption'</td><td class="default">'body'</td><td>Selects which <code>--style-*</code> token set the recipe consumes.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Per-axis overrides</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">size</td><td class="type">'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl'</td><td>Resolves to <code>var(--font-size-{`{key}`})</code> + matching line-height / letter-spacing.</td></tr>
<tr><td class="name">family</td><td class="type">'primary' | 'secondary' | 'mono' | 'display'</td><td>Resolves to <code>var(--font-family-{`{key}`})</code>.</td></tr>
<tr><td class="name">weight</td><td class="type">'regular' | 'medium' | 'semibold' | 'bold'</td><td>Resolves to <code>var(--font-weight-{`{key}`})</code>.</td></tr>
<tr><td class="name">color</td><td class="type">'primary' | 'secondary' | 'muted' | 'disabled' | 'on-solid'</td><td>Resolves to <code>var(--color-content-{`{key}`})</code>. Free colors via <code>tagStyle</code>.</td></tr>
<tr><td class="name">align</td><td class="type">'start' | 'center' | 'end' | 'justify'</td><td>Maps to <code>text-align</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Shorthand booleans</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">italic</td><td class="type">boolean</td><td><code>font-style: italic</code>.</td></tr>
<tr><td class="name">underline</td><td class="type">boolean</td><td><code>text-decoration: underline</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Truncation</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">truncate</td><td class="type">boolean</td><td>Single-line ellipsis. Requires container width.</td></tr>
<tr><td class="name">clamp</td><td class="type">number</td><td>Multi-line cap via CSS <code>-webkit-line-clamp</code>. Takes precedence over <code>truncate</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Element + escape hatch</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">as</td><td class="type">string</td><td>HTML tag. Default <code>'span'</code>. Use <code>'p'</code> / <code>'em'</code> / <code>'small'</code> for semantic blocks.</td></tr>
<tr><td class="name">tagStyle</td><td class="type">string</td><td>Extra inline CSS. Appended after the recipe's vars.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Reference comparison</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Library</th><th>Closest equivalent</th><th>Difference</th></tr></thead>
<tbody>
<tr><td class="name">radix-themes</td><td><code>&lt;Text&gt;</code></td><td>Radix uses numeric <code>size="1..9"</code>; UIX maps to the named scale + per-style defaults.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;Text&gt;</code></td><td>Chakra accepts free <code>color</code>; UIX narrows to content-role slots for contract integrity.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;Text&gt;</code></td><td>Mantine ships a 2-letter prop syntax (<code>fz</code>, <code>fw</code>, <code>ta</code>); UIX uses full names.</td></tr>
<tr><td class="name">mui Typography</td><td><code>&lt;Typography variant="…"&gt;</code></td><td>MUI's <code>variant</code> maps to a named style — same idea as UIX's <code>style</code>.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">name</td><td>{textMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{textMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{textMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
<tr><td class="name">events</td><td>0</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>kebab</th><th>marker</th><th>element</th><th>archetype</th><th>optional</th></tr></thead>
<tbody>
{#each partsList as part}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.archetype}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<p data-uix-section-desc style="margin-top: var(--uix-space-4);">
The Provider part emits the <code>data-text</code> marker plus
<code>data-style</code> for the active named style and boolean attrs
(<code>data-italic</code>, <code>data-underline</code>,
<code>data-truncate</code>, <code>data-clamp</code>) for the visual
modifiers. Per-axis overrides write local CSS variables
(<code>--_text-*</code>) inline; the recipe falls through to the
<code>--style-{`{name}`}-*</code> foundation tokens when an override is
absent.
</p>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
Text declares no semantic events. As a passive typography primitive, it does not
commit, emerge, or react — it just styles its inline content. Components that animate
text (toast appearance, collapsible reveal) compose Text inside an interactive
primitive that owns the relevant sema verbs.
</p>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Recipe lives in <code>src/uix/eidos/components/text/text.css</code>. The selector
<code>[data-text]</code> applies the body-style fallback chain; matching the
<code>[data-style]</code> attribute swaps to the corresponding
<code>--style-{`{name}`}-*</code> tokens. Per-axis overrides come from the local
<code>--_text-{`{axis}`}</code> custom properties the component writes inline.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>[data-text]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Default body-style applied.</td>
</tr>
<tr>
<td class="name"><code>[data-text][data-style='prose' | 'label' | 'caption']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Swap to the named style's <code>--style-*</code> token set.</td>
</tr>
<tr>
<td class="name"><code>[data-text][data-italic|data-underline|data-truncate|data-clamp]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Visual modifier flags emitted by boolean props.</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr><td class="name">Role</td><td>None implicit. Default tag is <code>&lt;span&gt;</code> — semantically neutral. Use <code>as="p"</code> for paragraphs, <code>as="strong"</code> / <code>as="em"</code> for emphasis with semantic weight.</td></tr>
<tr><td class="name">Heading levels</td><td>Out of scope. Use the future <code>&lt;Heading level={`{1..6}`}&gt;</code> primitive for h1–h6 with the matching style.</td></tr>
<tr><td class="name">Truncation + AT</td><td>CSS-only truncation hides text visually but it remains in the DOM and is announced fully by screen readers. Don't rely on truncation to hide meaning.</td></tr>
<tr><td class="name">Color contrast</td><td>All content-role colors meet WCAG AA against the foundation surfaces. Free colors via <code>tagStyle</code> bypass this guarantee — verify manually.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>
Loading…
Cancel
Save

Powered by TurnKey Linux.