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.
470 lines
17 KiB
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"><Text></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"
|
|
><Text></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><Text></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><Text clamp={2}></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><SText text=... clamp={2}></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"><Text></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<...></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"><{part.defaultElement}></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"><Text></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>
|