You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/text/+page.svelte

506 lines
21 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>

Powered by TurnKey Linux.