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

592 lines
20 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 {
Skeleton,
type SkeletonShape,
type SkeletonSize,
type SkeletonColor
} from '$uix/eidos/components/skeleton';
import { Box } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { skeletonMorfo } from '@/uix/morfo/components/skeleton';
import { getActiveUix } from '$active-uix';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
const uix = getActiveUix();
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
let shape = $state<SkeletonShape>('rect');
let size = $state<SkeletonSize>('md');
let color = $state<SkeletonColor>('neutral');
let animated = $state(true);
let lines = $state(3);
let width = $state<string>('100%');
let height = $state<string>('40px');
const compiled = compileMorfo(skeletonMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const shapes: SkeletonShape[] = ['rect', 'circle', 'text'];
const sizes: SkeletonSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const colors: SkeletonColor[] = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
];
$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 { Skeleton } from '$uix/eidos/components/skeleton';",
'</' + 'script>',
'',
'<Skeleton',
shape !== 'rect' && ` shape="${shape}"`,
size !== 'md' && ` size="${size}"`,
color !== 'neutral' && ` color="${color}"`,
!animated && ' animated={false}',
shape === 'text' && lines !== 1 && ` lines={${lines}}`,
shape === 'rect' && width !== '100%' && ` width="${width}"`,
shape === 'rect' && height !== '40px' && ` height="${height}"`,
'/>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Feedback · Skeleton</div>
<h1 data-uix-page-title>Skeleton</h1>
<p data-uix-page-lede>
Content placeholder shown while data loads. Three shapes
(<code>rect</code>, <code>circle</code>, <code>text</code>), five sizes, eight color
accents, and an optional shimmer animation. Eidos-native: no semantic events, no soma
split — the morfo declares one Provider part with <code>role="status"</code> and
<code>aria-busy="true"</code>.
</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>shapes</span>{shapes.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>sizes</span>{sizes.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>eidos
</span>
</div>
</header>
<!-- Live preview always rendered (between header and tabs). -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<Box
style="padding: var(--uix-space-4); display: flex; justify-content: center; inline-size: 100%;"
>
<Box style="inline-size: min(28rem, 100%);">
{#if shape === 'rect'}
<Skeleton {shape} {size} {color} {animated} {width} {height} />
{:else if shape === 'circle'}
<Skeleton {shape} {size} {color} {animated} />
{:else}
<Skeleton {shape} {size} {color} {animated} {lines} />
{/if}
</Box>
</Box>
</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>shape</span>
<span>{shape}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>size</span>
{size} · <span data-uix-stage-trace-key>color</span> {color}
</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>
Skeleton is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The
<code>shape</code> prop drives the recipe geometry; <code>size</code> sets intrinsic
block-size; <code>color</code> tints the placeholder background.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>shape</span>
<span data-uix-chips role="radiogroup">
{#each shapes as s (s)}
<button data-uix-chip data-active={shape === s} onclick={() => (shape = s)}>{s}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each sizes as s (s)}
<button data-uix-chip data-active={size === s} onclick={() => (size = s)}>{s}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each colors as c (c)}
<button data-uix-chip data-active={color === c} onclick={() => (color = c)}>{c}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>animated</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={animated} />
<span data-uix-switch-label>{animated ? 'on' : 'off'}</span>
</span>
</label>
{#if shape === 'rect'}
<label data-uix-control>
<span data-uix-control-label>width</span>
<input type="text" bind:value={width} />
</label>
<label data-uix-control>
<span data-uix-control-label>height</span>
<input type="text" bind:value={height} />
</label>
{/if}
{#if shape === 'text'}
<label data-uix-control>
<span data-uix-control-label>lines</span>
<input type="number" min="1" max="10" bind:value={lines} />
</label>
{/if}
</div>
<!-- ── Code snippet (eidos-native — no soma layer) ──────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · shape + size drive recipe geometry</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>
Skeleton accepts shape / size / color / lines / animated and inherits the full Box prop
surface (width, height, padding, gridColumn, …) so it composes inside any layout.
</p>
<div data-uix-subsection-head>Skeleton-specific props</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">shape</td>
<td class="type">'rect' | 'circle' | 'text'</td>
<td>
<code>rect</code> (default) → rounded rectangle. <code>circle</code> → 1:1 ratio,
fully rounded. <code>text</code> → stacked thin bars at staggered widths.
</td>
</tr>
<tr>
<td class="name">size</td>
<td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td>
<td>Intrinsic block-size when no <code>height</code> is set. Default <code>md</code>.</td>
</tr>
<tr>
<td class="name">color</td>
<td class="type">ColorRole</td>
<td>
Placeholder tint. <code>neutral</code> (default) for content-agnostic
placeholders; intent colors for failure states.
</td>
</tr>
<tr>
<td class="name">lines</td>
<td class="type">number</td>
<td>
Bar count when <code>shape='text'</code>. Each bar's width staggers so the block
reads as a paragraph. Default <code>1</code>.
</td>
</tr>
<tr>
<td class="name">animated</td>
<td class="type">boolean</td>
<td>
Shimmer wave via CSS background-position keyframes. Frozen under
<code>prefers-reduced-motion: reduce</code>. Default <code>true</code>.
</td>
</tr>
<tr>
<td class="name">aria-label</td>
<td class="type">string?</td>
<td>Override the translated 'Loading' default — useful for context-specific messages.</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Inherited from Box</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">width / height</td>
<td class="type">number | string</td>
<td>Override intrinsic size — pass any CSS length.</td>
</tr>
<tr>
<td class="name">padding / margin (and per-side)</td>
<td class="type">number | string</td>
<td>Numbers map to <code>var(--space-N)</code>.</td>
</tr>
<tr>
<td class="name">gridColumn / gridRow / placeSelf</td>
<td class="type">string</td>
<td>Use Skeleton as a grid item — placement props live on Box.</td>
</tr>
<tr>
<td class="name">…</td>
<td class="type">BoxProps</td>
<td>See <a href="/uix/components/box">&lt;Box&gt;</a> for the full surface.</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">shadcn/ui</td>
<td><code>&lt;Skeleton&gt;</code></td>
<td>shadcn ships a single rectangular class. UIX adds shape variants + the text-paragraph composition out of the box.</td>
</tr>
<tr>
<td class="name">chakra-ui</td>
<td><code>&lt;Skeleton&gt;</code> + <code>&lt;SkeletonText&gt;</code></td>
<td>Chakra splits into two components. UIX folds both into one with <code>shape='text'</code>.</td>
</tr>
<tr>
<td class="name">mantine</td>
<td><code>&lt;Skeleton&gt;</code> + <code>&lt;SkeletonText&gt;</code></td>
<td>Same split as Chakra; UIX collapses to one component with <code>lines</code> for paragraphs.</td>
</tr>
<tr>
<td class="name">radix-themes</td>
<td><code>&lt;Skeleton&gt;</code></td>
<td>Radix uses an inert `data-skeleton` over arbitrary children. UIX exposes structured shape variants.</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>{skeletonMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{skeletonMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{skeletonMorfo.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>role</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="type">{part.role ?? '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div data-uix-subsection-head>Provider · data attrs</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>attr</th><th>values</th><th>source</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>data-shape</code></td>
<td>rect | circle | text</td>
<td class="type">propRef('shape')</td>
</tr>
<tr>
<td class="name"><code>data-animated</code></td>
<td>'' (when on)</td>
<td class="type">propRef('animated')</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Provider · aria</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>attr</th><th>value</th><th>severity</th></tr></thead>
<tbody>
<tr>
<td class="name"><code>role</code></td>
<td>status</td>
<td class="type">part default</td>
</tr>
<tr>
<td class="name"><code>aria-label</code></td>
<td>translationRef('components.skeleton.label')</td>
<td class="type">recommended</td>
</tr>
<tr>
<td class="name"><code>aria-busy</code></td>
<td>"true"</td>
<td class="type">literal</td>
</tr>
</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>
Skeleton declares <strong>no semantic events</strong>. It is a passive visual placeholder
the consumer unmounts when real content is ready — there is no commit/emerge/shift to
react to. Screen readers learn about its presence through the morfo's
<code>role="status"</code> + <code>aria-busy="true"</code> contract, not through sema.
</p>
<p data-uix-section-desc>
If you need a perceptual cue when loading <em>starts</em> or <em>completes</em>, attach
the cue to the parent action (commit-submit on the form that triggered the fetch, or
commit-set on the slot that received the data) — not to Skeleton.
</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/skeleton/skeleton.css</code>. Tokens are
private (<code>--_skeleton-*</code>). The shimmer is a single keyframes block reused by
rect/circle/text variants.
</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-skeleton]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Emitted by the component on the root div.</td>
</tr>
<tr>
<td class="name"><code>[data-skeleton][data-shape='rect'|'circle'|'text']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Toggles border-radius + intrinsic aspect-ratio + layout.</td>
</tr>
<tr>
<td class="name"><code>[data-skeleton][data-size='xs'..'xl']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Sets <code>--_skeleton-size</code>. Default <code>md = 1rem</code>.</td>
</tr>
<tr>
<td class="name"><code>[data-skeleton][data-color='primary'..'loss']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Color-mixes accent tracks into <code>--_skeleton-bg</code>.</td>
</tr>
<tr>
<td class="name"><code>[data-skeleton][data-animated]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Adds the linear-gradient + <code>skeleton-shimmer</code> keyframes.</td>
</tr>
<tr>
<td class="name"><code>[data-skeleton-line]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>One bar inside <code>shape='text'</code>. Stacks via flex column.</td>
</tr>
<tr>
<td class="name"><code>@media (prefers-reduced-motion: reduce)</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Stops the shimmer; placeholder shape stays visible.</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>
<code>role="status"</code> — a polite live region. Screen readers announce the
element's label when it enters the DOM and stay silent otherwise.
</td>
</tr>
<tr>
<td class="name">Busy state</td>
<td>
<code>aria-busy="true"</code> — signals that the region's content is loading and
the user should expect a change soon.
</td>
</tr>
<tr>
<td class="name">Label</td>
<td>
Defaults to the translated 'Loading' / 'Cargando'. Override with
<code>aria-label="Loading user profile"</code> when you can give specific
context.
</td>
</tr>
<tr>
<td class="name">Keyboard</td>
<td>Skeleton is not focusable. Tab order skips it entirely.</td>
</tr>
<tr>
<td class="name">Focus visible</td>
<td>Skeleton does not paint a focus ring — no interactive state.</td>
</tr>
<tr>
<td class="name">Reduced motion</td>
<td>
Shimmer animation freezes under
<code>prefers-reduced-motion: reduce</code>. The placeholder shape stays visible
to preserve the loading affordance.
</td>
</tr>
<tr>
<td class="name">Color contrast</td>
<td>
<code>color-mix</code> at 50–60% opacity over the surface — low enough to read
as "non-content" but high enough to register against most backgrounds.
</td>
</tr>
<tr>
<td class="name">Replacement</td>
<td>
When swapped with real content, the skeleton element is removed entirely (not
just hidden). Screen readers then announce the new content via its own ARIA.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.