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/s-text/+page.svelte

470 lines
17 KiB

<script lang="ts">
import { SText } from '$uix/eidos/components/s-text';
import { Text } from '$uix/eidos/components/text';
import { compileMorfo } from '$uix/morfo';
import { sTextMorfo } from '@/uix/morfo/components/s-text';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
// ── Live state ────────────────────────────────────────────────────────
const LOREM =
'Vicen UIX está diseñada como un sistema de cuatro capas: morfo declara el contrato, soma posee el comportamiento, sema gobierna la percepción y eidos pinta. SText pertenece a eidos: usa Intl.Segmenter y canvas measureText para conocer el número exacto de líneas en que rompe un texto al renderizar, sin necesidad de reflows del DOM. La medición permite mostrar un hint "+N líneas más" cuando el contenido se trunca, algo imposible con CSS line-clamp puro. La penalización es ~3900 líneas de motor de tipografía Unicode-aware (UAX #14 line-break, UAX #9 bidi, segmenter de grafemas) que se cargan en el bundle sólo cuando importas <SText>. Para body copy normal, sigue usando <Text> y obtendrás cero overhead.';
let text = $state(LOREM);
let clamp = $state(3);
let hideClampHint = $state(false);
let containerWidth = $state(420);
let lineCount = $state(0);
const compiled = compileMorfo(sTextMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { SText } from '$uix/eidos/components/s-text';",
'</' + 'script>',
'',
'<SText',
` text={text}`,
clamp > 0 && ` clamp={${clamp}}`,
hideClampHint && ' hideClampHint',
` bind:lineCount`,
'/>',
'',
`<!-- canvas-reported line count: ${lineCount} -->`
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Typography · SText</div>
<h1 data-uix-page-title>SText</h1>
<p data-uix-page-lede>
Canvas-augmented sibling of <a href="/uix/components/text">&lt;Text&gt;</a>. "S" stands for
<strong>Segmented</strong>: SText uses <code>Intl.Segmenter</code> + canvas
<code>measureText</code> to compute the exact line count of a paragraph at the current
container width, then renders a <code>+N more lines</code> hint when overflow trims the
content. Cost: importing SText pulls the entire canvas-text engine (~3900 lines of
Unicode line-break + bidi + segmentation logic). Use plain <a href="/uix/components/text"
>&lt;Text&gt;</a
> for pages that don't need exact counts — that path stays at zero canvas overhead.
</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>{events.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>engine</span>canvas-text · 3900 LOC
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>eidos
</span>
</div>
</header>
<!-- Live preview -->
<div data-uix-stage>
<div data-uix-stage-area>
<div style="inline-size: {containerWidth}px; max-inline-size: 100%; resize: horizontal; overflow: auto; padding: var(--uix-space-3); border: 1px dashed var(--uix-border); border-radius: var(--uix-radius-md);">
<SText {text} {clamp} {hideClampHint} bind:lineCount />
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>lineCount</span>
<span data-uix-status-chip>{lineCount}</span>
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>clamp</span>
<span>{clamp}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>width</span> {containerWidth}px
(arrastra el borde del marco)
</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>13</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>
The container above has <code>resize: horizontal</code> — drag its right edge to watch
the canvas re-layout the text and update the line count.
</p>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>clamp</span>
<input
type="range"
bind:value={clamp}
min="1"
max="10"
step="1"
style="inline-size: 14rem;"
/>
<span data-uix-control-hint>{clamp} lines</span>
</label>
<label data-uix-control>
<span data-uix-control-label>container width</span>
<input
type="range"
bind:value={containerWidth}
min="180"
max="720"
step="10"
style="inline-size: 14rem;"
/>
<span data-uix-control-hint>{containerWidth}px</span>
</label>
<label data-uix-control>
<span data-uix-control-label>hide clamp hint</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={hideClampHint} />
<span data-uix-switch-label>{hideClampHint ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>text</span>
<textarea
bind:value={text}
rows="4"
style="inline-size: 32rem; max-inline-size: 100%; font-family: var(--uix-font-mono); font-size: var(--uix-font-size-xs);"
></textarea>
</label>
</div>
<div data-uix-code style="margin-top: var(--uix-space-4);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>composition · canvas activates with `text` prop</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
<div data-uix-subsection-head>Patterns · common shapes</div>
<div data-uix-pattern-stack>
<div data-uix-pattern>
<h3 data-uix-pattern-title>Comment preview — clamp=3 + hint</h3>
<div data-uix-pattern-row>
<div style="inline-size: 24rem;">
<SText
text="Probé el nuevo SText con un párrafo de longitud media para ver cómo se comporta el conteo de líneas exacto. La verdad es que el hint '+N líneas más' es enormemente útil cuando quieres mostrar previews de comentarios sin abrir un modal. Antes lo hacíamos con CSS line-clamp y dejábamos al usuario adivinar si había más contenido; ahora podemos decírselo de forma explícita."
clamp={3}
/>
</div>
</div>
</div>
<div data-uix-pattern>
<h3 data-uix-pattern-title>Card title — single line, no clamp needed</h3>
<div data-uix-pattern-row>
<div style="inline-size: 18rem;">
<SText
text="Untruncated short title without canvas overhead"
clamp={2}
weight="semibold"
/>
</div>
</div>
</div>
<div data-uix-pattern>
<h3 data-uix-pattern-title>
Side-by-side with <code>&lt;Text&gt;</code> · same clamp value, only SText shows the
exact-count hint
</h3>
<div data-uix-pattern-row>
<div style="inline-size: 22rem;">
<div style="font-size: var(--uix-font-size-xs); color: var(--uix-text-faint); margin-bottom: var(--uix-space-1);">
<code>&lt;Text clamp={2}&gt;</code>
</div>
<Text clamp={2}>
{text}
</Text>
</div>
<div style="inline-size: 22rem;">
<div style="font-size: var(--uix-font-size-xs); color: var(--uix-text-faint); margin-bottom: var(--uix-space-1);">
<code>&lt;SText text=... clamp={2}&gt;</code>
</div>
<SText {text} clamp={2} />
</div>
</div>
</div>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Same visual axes as <a href="/uix/components/text">&lt;Text&gt;</a> plus the canvas
measurement props (<code>text</code>, <code>clamp:number</code>,
<code>lineCount</code>, <code>hideClampHint</code>).
</p>
<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">text</td>
<td class="type">string</td>
<td class="default empty">—</td>
<td>
<strong>Activates canvas measurement.</strong> Plain string content (canvas
needs literal characters; rich snippets aren't measurable). When omitted,
SText degrades to CSS-only clamp.
</td>
</tr>
<tr>
<td class="name">clamp</td>
<td class="type">number</td>
<td class="default empty">—</td>
<td>
Maximum visible lines. Pair with <code>text</code> for exact-count clamp +
hint; without <code>text</code>, falls back to CSS line-clamp (no exact
count).
</td>
</tr>
<tr>
<td class="name">lineCount</td>
<td class="type">number</td>
<td class="default">0</td>
<td>
Bindable. Number of lines canvas measured at the current width. Updates on
resize. Always 0 when <code>text</code> is omitted.
</td>
</tr>
<tr>
<td class="name">hideClampHint</td>
<td class="type">boolean</td>
<td class="default">false</td>
<td>
Suppress the "+N more lines" hint even on overflow. Flip on when the
surrounding UI already has a "show more" affordance.
</td>
</tr>
<tr>
<td class="name">style</td>
<td class="type">'body' | 'prose' | 'label' | 'caption'</td>
<td class="default">'body'</td>
<td>Named typography style — same set as Text.</td>
</tr>
<tr>
<td class="name">size, family, weight, color, align</td>
<td class="type">ResponsiveProp&lt;...&gt;</td>
<td class="default empty">— from style</td>
<td>Per-axis overrides — same as Text.</td>
</tr>
<tr>
<td class="name">italic, underline, truncate</td>
<td class="type">boolean</td>
<td class="default">false</td>
<td>Shorthand styling toggles.</td>
</tr>
<tr>
<td class="name">as</td>
<td class="type">string</td>
<td class="default">'p'</td>
<td>
HTML tag for the Provider. Defaults to <code>'p'</code> (vs Text's
<code>'span'</code>) because SText is typically used for paragraph-shaped
content where clamp matters.
</td>
</tr>
<tr>
<td class="name">tagStyle</td>
<td class="type">string</td>
<td class="default empty">—</td>
<td>Extra inline style merged after the recipe variables.</td>
</tr>
<tr>
<td class="name">children</td>
<td class="type">Snippet</td>
<td class="default empty">—</td>
<td>
Ignored when <code>text</code> is set (canvas can't measure arbitrary
markup). Used only as the fallback content when SText runs in non-canvas
mode.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span> · declarative 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>{sTextMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{sTextMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{sTextMorfo.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 (part.kebab)}
<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>
</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>
SText declares <strong>no semantic events</strong>. Line measurement is passive
observation, not an evaluative act. If you need a perceptual cue when overflow
becomes visible (e.g. a chime to indicate "more content"), attach it to the
surrounding interaction (a click on the hint that expands the preview).
</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/s-text/s-text.css</code>. The visual
layer is identical to Text — same <code>--style-{'{name}'}-*</code> token bridge —
plus the <code>[data-s-text-clamp-hint]</code> sibling for the "+N more lines"
hint. The canvas engine lives in <code
>src/uix/eidos/lib/canvas-text/</code
> and is shared with <a href="/uix/components/s-text-virtual-list">STextVirtualList</a>.
</p>
</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">Visible vs hidden text</td>
<td>
The clamped Provider hides overflow visually but the full <code>text</code>
string is still in the DOM — screen readers can navigate the complete
content. The "+N more lines" hint is <code>aria-hidden="true"</code> so it
doesn't pollute the announcement.
</td>
</tr>
<tr>
<td class="name">Resize behaviour</td>
<td>
Canvas re-layouts on container resize via <code>ResizeObserver</code>. The
line count updates without forcing DOM reflow (the engine works on cached
word widths).
</td>
</tr>
<tr>
<td class="name">Reduced motion</td>
<td>
No animation in SText itself. The recipe inherits Text's static styling.
</td>
</tr>
<tr>
<td class="name">When to use SText vs Text</td>
<td>
Reach for SText only when you need exact line counts (comment previews,
truncated reviews, "+N more" UI). For body copy, headings, labels, and
captions, use <a href="/uix/components/text">&lt;Text&gt;</a> — same visual
result with zero canvas overhead.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>
<style>
[data-uix-pattern-stack] {
display: flex;
flex-direction: column;
gap: var(--uix-space-5);
}
[data-uix-pattern-row] {
display: flex;
flex-wrap: wrap;
gap: var(--uix-space-4);
padding: var(--uix-space-4);
border: 1px solid var(--uix-border);
border-radius: var(--uix-radius-md);
align-items: flex-start;
}
h3[data-uix-pattern-title] {
margin: 0 0 var(--uix-space-3);
font-family: var(--uix-font-ui);
font-size: var(--uix-font-size-sm);
font-weight: 600;
color: var(--uix-text-faint);
text-transform: uppercase;
letter-spacing: 0.04em;
}
[data-uix-status-chip] {
font-family: var(--uix-font-mono);
font-size: var(--uix-font-size-xs);
padding: 0 var(--uix-space-1);
border-radius: var(--uix-radius-sm);
background: var(--uix-bg-subtle, var(--color-surface-raised));
}
</style>

Powered by TurnKey Linux.