feat(typography): 8 primitives — Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link
Full canon for the rest of the typography family, following Text
(commit `03531709`) as template. Each primitive ships morfo (passive,
scope eidos, 0 events) + full eidos set (svelte/css/types/index +
README with required sections) + canon 6-tab demo (≥400 lines each).
**Heading** — semantic h1..h6 via `level` prop, visual style decoupled
via `style` prop (defaults to match level but `<Heading level={3}
style="h1">` allowed). Demo 594 lines.
**Display** — always-h1 page-title primitive. Always uses
`style="hero"`. Tracking via literal em values until foundation emits
hero-scale tracking tokens. Demo 546 lines.
**Code** — inline code via `<code>`. `variant: 'plain' | 'soft' |
'outline'` for subtle background chrome. Demo 423 lines.
**CodeBlock** — multi-line `<pre><code>` with optional title +
copy button (button utility, NOT a sema verb — kept out of morfo per
2-of-3 rule). Demo 402 lines.
**Kbd** — keyboard shortcut chrome over `style="code"`. Demo 418 lines.
**Mark** — text highlight via `<mark>`. Palette uses canonical
`ColorRole` intents (default `fulfill` ≈ legacy yellow). Demo 402 lines.
**Highlight** — substring highlighter inside Text. Scans children for
`query` and wraps matches. Same intent palette as Mark. Demo 407 lines.
**Link** — text link via `<a>`. `variant: 'default' | 'subtle' |
'plain'` for underline style, `intent: ColorRole` for color. Passive
on the morfo — hover/active is CSS-only, navigation belongs to the
consumer (router). Demo 448 lines.
**Architectural deviations from air documented per README's
Decisiones section:**
- Heading: semantic level vs visual style decoupling
- Code split into inline `<Code>` + block `<CodeBlock>` (was one
component with `variant='block'` in air)
- Mark/Highlight palette swapped from literal hue names to canonical
ColorRole intents (theme contract)
- Link palette opened to full ColorRole; `quiet`/`emphasized` air
variants dropped in favour of composable patterns
`npm run component:audit`: **90 / 90 PASS**, 0 NEEDS-WORK.
svelte-check: 0 errors, 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< script lang = "ts" >
import {
Code,
type CodeVariant,
type CodeSize,
type CodeColor
} from '$uix/eidos/components/code';
import { compileMorfo } from '$uix/morfo';
import { codeMorfo } from '@/uix/morfo/components/code';
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);
let variant = $state< CodeVariant > ('plain');
let size = $state< CodeSize | ' ' > ('');
let color = $state< CodeColor | ' ' > ('');
let language = $state< string > ('');
let content = $state< string > ('npm run dev');
const variants: CodeVariant[] = ['plain', 'soft', 'outline'];
const sizes: CodeSize[] = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'];
const colors: CodeColor[] = ['primary', 'secondary', 'muted', 'disabled', 'on-solid'];
const compiled = compileMorfo(codeMorfo);
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();
});
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { Code } from '$uix/eidos/components/code';",
'< /' + 'script>',
'',
'< Code ' ,
variant !== 'plain' && ` variant="${ variant } "`,
size && ` size="${ size } "`,
color && ` color="${ color } "`,
language && ` language="${ language } "`,
'>',
' ' + content,
'< / Code > '
]
.filter(Boolean)
.join('\n')
);
< / script >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Typography · Code< / div >
< h1 data-uix-page-title > Code< / h1 >
< p data-uix-page-lede >
Inline monospace text primitive. Renders < code > { `<code>` } </ code > by default applying the
foundation's < code > --style-code-*< / code > tokens. Sits inline within prose — for keywords,
identifiers, file names. Multi-line code blocks use the separate
< code > { `<CodeBlock>` } </ code >
primitive. 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 > variants< / span > 3
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > sizes< / span > 6
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > scope< / span > eidos
< / span >
< / div >
< / header >
< div data-uix-stage >
< div data-uix-stage-area bind:this = { stageRef } >
< p
style="max-inline-size: 38rem; padding: var(--space-3); border: 1px dashed var(--color-neutral-border); border-radius: var(--radius-md); font: inherit;"
>
Run the command
< Code
{ variant }
size={ size || undefined }
color={ color || undefined }
language={ language || undefined }
>
{ content }
< / Code >
to start the dev server.
< / p >
< / 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 > variant< / span >
< span > { variant } </ span >
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > size</ span > { size || 'inherit' } ·
< span data-uix-stage-trace-key > language</ span > { language || 'none' }
< / 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 > 7< / 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 >
Code is eidos-native — no < span data-uix-layer-badge = "soma" > soma< / span > split. The
< code > variant< / code >
picks chrome treatment; size + color override the foundation defaults.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > props · inline mono
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > variant< / span >
< span data-uix-chips role = "radiogroup" >
{ #each variants as opt }
< button data-uix-chip data-active = { variant === opt } onclick= {() => ( variant = opt )}
>{ opt } < /button
>
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > size< / span >
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { size === '' } onclick= {() => ( size = '' )}
>inherit< /button
>
{ #each sizes as opt }
< button data-uix-chip data-active = { size === opt } onclick= {() => ( size = opt )}
>{ opt } < /button
>
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color< / span >
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { color === '' } onclick= {() => ( color = '' )}
>inherit< /button
>
{ #each colors as opt }
< button data-uix-chip data-active = { color === opt } onclick= {() => ( color = opt )}
>{ opt } < /button
>
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label
>language < span data-uix-control-hint > reserved hint (data-language)< / span > < /span
>
< input
type="text"
bind:value={ language }
placeholder="ts, bash, json..."
style="inline-size: 12rem;"
/>
< / label >
< label data-uix-control style = "grid-column: span 2;" >
< span data-uix-control-label > content< / span >
< input type = "text" bind:value = { content } style="inline-size: 100 %; font: inherit ;" />
< / label >
< / div >
<!-- ── Code snippet (eidos - native — no soma layer) ──────────────── -->
feat(typography): 8 primitives — Heading, Display, Code, CodeBlock, Kbd, Mark, Highlight, Link
Full canon for the rest of the typography family, following Text
(commit `03531709`) as template. Each primitive ships morfo (passive,
scope eidos, 0 events) + full eidos set (svelte/css/types/index +
README with required sections) + canon 6-tab demo (≥400 lines each).
**Heading** — semantic h1..h6 via `level` prop, visual style decoupled
via `style` prop (defaults to match level but `<Heading level={3}
style="h1">` allowed). Demo 594 lines.
**Display** — always-h1 page-title primitive. Always uses
`style="hero"`. Tracking via literal em values until foundation emits
hero-scale tracking tokens. Demo 546 lines.
**Code** — inline code via `<code>`. `variant: 'plain' | 'soft' |
'outline'` for subtle background chrome. Demo 423 lines.
**CodeBlock** — multi-line `<pre><code>` with optional title +
copy button (button utility, NOT a sema verb — kept out of morfo per
2-of-3 rule). Demo 402 lines.
**Kbd** — keyboard shortcut chrome over `style="code"`. Demo 418 lines.
**Mark** — text highlight via `<mark>`. Palette uses canonical
`ColorRole` intents (default `fulfill` ≈ legacy yellow). Demo 402 lines.
**Highlight** — substring highlighter inside Text. Scans children for
`query` and wraps matches. Same intent palette as Mark. Demo 407 lines.
**Link** — text link via `<a>`. `variant: 'default' | 'subtle' |
'plain'` for underline style, `intent: ColorRole` for color. Passive
on the morfo — hover/active is CSS-only, navigation belongs to the
consumer (router). Demo 448 lines.
**Architectural deviations from air documented per README's
Decisiones section:**
- Heading: semantic level vs visual style decoupling
- Code split into inline `<Code>` + block `<CodeBlock>` (was one
component with `variant='block'` in air)
- Mark/Highlight palette swapped from literal hue names to canonical
ColorRole intents (theme contract)
- Link palette opened to full ColorRole; `quiet`/`emphasized` air
variants dropped in favour of composable patterns
`npm run component:audit`: **90 / 90 PASS**, 0 NEEDS-WORK.
svelte-check: 0 errors, 0 new warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · variant + 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 >
Code wraps inline monospace text. < code > variant='plain'< / code > is the bare default;
< code > 'soft'< / code > and < code > 'outline'< / code > add chrome for surfacing from surrounding
prose.
< / p >
< div data-uix-subsection-head > Props< / 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" > variant< / td > < td class = "type" > 'plain' | 'soft' | 'outline'< / td > < td class = "default" > 'plain'< / td > < td > Visual chrome.< / td > < / tr >
< tr >< td class = "name" > size</ td >< td class = "type" > 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'</ td >< td class = "default" > inherit</ td >< td > Maps to < code > { `var(--font-size-{key})` } </ code > .</ td ></ tr >
< tr > < td class = "name" > color< / td > < td class = "type" > 'primary' | 'secondary' | 'muted' | 'disabled' | 'on-solid'< / td > < td class = "default" > inherit< / td > < td > Content-role color slot.< / td > < / tr >
< tr > < td class = "name" > language< / td > < td class = "type" > string< / td > < td class = "default" > —< / td > < td > Reserved hint for future syntax-highlighting. Stamped as < code > data-language< / code > .< / td > < / tr >
< tr > < td class = "name" > as< / td > < td class = "type" > string< / td > < td class = "default" > 'code'< / td > < td > Override rendered tag.< / td > < / tr >
< tr > < td class = "name" > tagStyle< / td > < td class = "type" > string< / td > < td class = "default" > —< / td > < td > Extra inline CSS.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Composability examples< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Use case< / th > < th > Snippet< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > Inline command< / td >
< td >< code > { `<p>Run <Code>npm run dev</Code></p>` } </ code ></ td >
< / tr >
< tr >
< td class = "name" > Soft-pill identifier< / td >
< td >< code > { `<Code variant="soft">USER_ID</Code>` } </ code ></ td >
< / tr >
< tr >
< td class = "name" > Outline shortcut hint< / td >
< td >< code > { `<Code variant="outline" size="sm">Ctrl+K</Code>` } </ code ></ td >
< / tr >
< tr >
< td class = "name" > Inside a heading< / td >
< td >
< code > { `<Heading>How <Code variant="soft">useState</Code> works</Heading>` } </ code >
< / td >
< / tr >
< tr >
< td class = "name" > With language hint (future highlighting)< / td >
< td >< code > { `<Code language="ts">readonly id: string</Code>` } </ code ></ 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 > < Code> < / code > < / td > < td > Radix has size + variant (solid/soft/outline/surface); UIX consolidates to 3.< / td > < / tr >
< tr > < td class = "name" > chakra-ui< / td > < td > < code > < Code> < / code > < / td > < td > Chakra uses < code > colorScheme< / code > ; UIX restricts to content-role.< / td > < / tr >
< tr > < td class = "name" > mantine< / td > < td > < code > < Code> < / code > < / td > < td > Mantine has < code > block< / code > boolean; UIX splits into Code + CodeBlock.< / td > < / tr >
< tr > < td class = "name" > MDN < code > < code> < / code > < / td > < td > HTML primitive< / td > < td > UIX wraps the native element with the foundation tokens.< / 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 > { codeMorfo . name } </ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td >< code > { codeMorfo . kebab } </ code ></ td ></ tr >
< tr >< td class = "name" > scope</ td >< td > { codeMorfo . 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" > < { part . defaultElement } > </ 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 stamps < code > data-code< / code > on the rendered tag (default
< code > { `<code>` } </ code > ), plus < code > data-variant</ code > and (when set) < code > data-language</ code > .
< / 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 >
Code declares no semantic events. The primitive is passive — there is no commit, no
emerge. If the consumer needs click-to-copy on inline code, they typically use the
multi-line CodeBlock primitive instead.
< / 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/code/code.css< / code > . Base typography
comes from < code > --style-code-*< / code > ; variants apply chrome through the
< code > data-variant< / code > selector.
< / 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-code]< / code > < / td > < td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td > < td > Provider marker. Base mono typography.< / td > < / tr >
< tr >< td class = "name" >< code > { `[data-code][data-variant='soft']` } </ code ></ td >< td >< span data-uix-tag data-kind = "eidos" > eidos</ span ></ td >< td > Subtle background pill.</ td ></ tr >
< tr >< td class = "name" >< code > { `[data-code][data-variant='outline']` } </ code ></ td >< td >< span data-uix-tag data-kind = "eidos" > eidos</ span ></ td >< td > Outlined pill.</ 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 > Native HTML < code > { `<code>` } </ code > semantics — AT announce as "code".</ td ></ tr >
< tr > < td class = "name" > Inside prose< / td > < td > Composes inline. AT read the surrounding text as a single utterance with the code embedded.< / td > < / tr >
< tr > < td class = "name" > Color contrast< / td > < td > Soft / outline variants use neutral palette tokens that meet WCAG AA on the foundation surfaces.< / td > < / tr >
< tr > < td class = "name" > Language hint< / td > < td > < code > data-language< / code > is not announced by AT; it's a styling / tooling hint.< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >