fix(demos): rewrite Skeleton + Spinner demos to canonical 6-tab depth

The previous demos violated DEMO_AUTHORING_GUIDE in several places. Both
have been rewritten end-to-end against the aspect-ratio canary template
(canonical reference for 0-events eidos primitives).

Changes per demo:

- Stage always rendered between header and tablist (was inside Live tab).
- MutationObserver wired with stageRef + 6-entry trace strip (was absent —
  required even for 0-event components per §6).
- Trace strip layout: 'no semantic events' badge + shape/variant + size +
  color, matches aspect-ratio shape.
- Live tab adds the soma + eidos code snippets with full data-uix-code
  wrappers and head badges (§7 + §8).
- somaSnippet now shows the equivalent semantic markup with a clear
  "not real soma" note (since these are eidos-native).
- API tab uses data-uix-table with explicit "Skeleton-specific props" +
  "Inherited from Box" + "Reference comparison" subsections (§4).
- Morfo tab matches §10: header field/value table + Parts table with
  marker column + per-part data/aria subsection tables.
- Sema tab keeps the explicit 0-events explanation (was absent for one
  of the demos).
- Recipe tab uses the Selectors table with morfo/eidos owner tags (§11)
  instead of a single code snippet.
- A11y tab uses data-uix-table (concern/contract) instead of a bullet
  list, matching aspect-ratio.

Reference template: web/routes/uix/components/aspect-ratio/+page.svelte.
Per §1, when in doubt, copy from the canary template and adapt.

svelte-check: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 616d8535f7
commit fead097ce1

@ -143,6 +143,12 @@
opacity: var(--words-disabled-opacity);
}
[data-words]:has([data-words-toolbar][data-orientation='vertical']) {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
grid-template-rows: minmax(var(--_words-content-min-block-size), auto) auto;
}
[data-words-toolbar] {
display: flex;
flex-wrap: wrap;
@ -155,6 +161,8 @@
}
[data-words-toolbar][data-orientation='vertical'] {
grid-column: 1;
grid-row: 1;
flex-direction: column;
align-items: stretch;
inline-size: max-content;
@ -238,6 +246,12 @@
pointer-events: none;
}
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-placeholder] {
grid-column: 2;
grid-row: 1;
z-index: 1;
}
[data-words-placeholder][hidden],
[data-words-hidden-input][hidden] {
display: none;
@ -253,6 +267,11 @@
caret-color: var(--_words-accent-solid);
}
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-content] {
grid-column: 2;
grid-row: 1;
}
[data-words-placeholder]:not([hidden]) + [data-words-content] {
padding-block-start: var(--words-content-py-after-placeholder);
}
@ -347,6 +366,11 @@
font-size: var(--words-status-font-size);
}
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-status] {
grid-column: 1 / -1;
grid-row: 2;
}
[data-words-status][data-status='error'] {
color: var(--words-status-error-color);
}

@ -6,12 +6,18 @@
type SkeletonColor
} from '$uix/eidos/components/skeleton';
import { Box } from '$uix/eidos/components/box';
import { Stack } from '$uix/eidos/components/stack';
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');
@ -38,6 +44,51 @@
'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 somaSnippet = $derived(
[
'<!-- Skeleton is eidos-native — no soma layer. -->',
'<!-- Equivalent semantic markup (not real soma): -->',
'',
'<div',
' data-skeleton',
shape !== 'rect' && ` data-shape="${shape}"`,
size !== 'md' && ` data-size="${size}"`,
color !== 'neutral' && ` data-color="${color}"`,
animated && ' data-animated=""',
' role="status"',
' aria-busy="true"',
' aria-label="Loading"',
shape === 'rect' && ` style="inline-size: ${width}; block-size: ${height};"`,
'></div>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
@ -65,21 +116,36 @@
<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. Pure eidos primitive — no semantic events, no soma layer.
Reduced-motion freezes the animation while keeping the visual.
(<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>{partsList.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>scope</span>eidos</span>
<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>
<Box style="padding: var(--uix-space-4); display: flex; justify-content: center;">
<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} />
@ -91,20 +157,31 @@
</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 === '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>0</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button
>
@ -114,9 +191,14 @@
{#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 · shape and bounds
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
@ -170,119 +252,370 @@
{/if}
</div>
<div data-uix-subsection-head>Composition examples</div>
<Stack gap="var(--uix-space-4)">
<div data-uix-section-desc>Avatar + 2 lines</div>
<Box
style="display: flex; gap: var(--uix-space-3); align-items: center; padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
<Skeleton shape="circle" size="lg" />
<Box style="flex: 1;">
<Skeleton shape="text" lines={2} />
</Box>
</Box>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>n/a · skeleton is eidos-native — equivalent markup shown</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-section-desc>Card with image + content</div>
<Box
style="border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md); overflow: hidden;"
>
<Skeleton width="100%" height="160px" />
<Box style="padding: var(--uix-space-4);">
<Skeleton shape="text" lines={3} />
</Box>
</Box>
</Stack>
</section>
{:else if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API</h2>
<table data-uix-api-table>
<thead>
<tr><th>prop</th><th>type</th><th>default</th><th>description</th></tr>
</thead>
<tbody>
<tr><td>shape</td><td>'rect' | 'circle' | 'text'</td><td>'rect'</td><td>Visual shape.</td></tr>
<tr><td>size</td><td>'xs'..'xl'</td><td>'md'</td><td>Intrinsic block-size.</td></tr>
<tr><td>color</td><td>ColorRole</td><td>'neutral'</td><td>Placeholder tint.</td></tr>
<tr><td>lines</td><td>number</td><td>1</td><td>Bar count when shape='text'.</td></tr>
<tr><td>animated</td><td>boolean</td><td>true</td><td>Shimmer wave (CSS keyframes).</td></tr>
<tr><td>aria-label</td><td>string?</td><td>—</td><td>Override the translated 'Loading'.</td></tr>
<tr>
<td>...Box</td><td>BoxProps</td><td>—</td><td>width/height/padding/gridColumn/…</td>
</tr>
</tbody>
</table>
<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 · shape + size drive recipe geometry</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{:else if tab === 'morfo'}
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span> contract
</h2>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
One provider part with <code>role="status"</code> and <code>aria-busy="true"</code>. Carries
<code>data-shape</code> + <code>data-animated</code> attrs that the eidos recipe consumes.
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>
<table data-uix-api-table>
<thead><tr><th>part</th><th>element</th><th>role</th><th>archetype</th></tr></thead>
<tbody>
{#each partsList as p (p.kebab)}
<tr>
<td><code>{p.kebab}</code></td>
<td><code>{p.defaultElement}</code></td>
<td>{p.role ?? '—'}</td>
<td>{p.archetype ?? '—'}</td>
</tr>
{/each}
</tbody>
</table>
<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>
{:else if tab === 'sema'}
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> events
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
Skeleton has no semantic events — it's a pure visual placeholder that the consumer unmounts
when real content is ready. The morfo declares no events, so the perceptual cascade has
nothing to react to. The single Provider part carries <code>aria-busy</code> +
<code>role="status"</code> for assistive tech.
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>
{:else if tab === 'recipe'}
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Recipe</h2>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> · canonical usage
<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>
<pre data-uix-snippet><code>{eidosSnippet}</code></pre>
</section>
{:else if tab === 'a11y'}
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<ul>
<li>
Renders <code>&lt;div role="status" aria-busy="true"&gt;</code>. Screen readers will
announce the configured label ("Loading" in EN, "Cargando" in ES) when the skeleton
enters the DOM.
</li>
<li>
When swapped with real content, the screen reader hears the new content because the
Skeleton element is removed entirely (not just hidden).
</li>
<li>
<code>aria-label</code> can override the default "Loading" string with a more specific
message (e.g. <code>aria-label="Loading user profile"</code>).
</li>
<li>
Animation respects <code>prefers-reduced-motion</code> — under reduced motion the shimmer
stops but the placeholder shape remains visible.
</li>
<li>
Color contrast: the placeholder uses <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.
</li>
</ul>
<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>

@ -9,9 +9,16 @@
import { Stack } from '$uix/eidos/components/stack';
import { compileMorfo } from '$uix/morfo';
import { spinnerMorfo } from '@/uix/morfo/components/spinner';
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 variant = $state<SpinnerVariant>('ring');
let size = $state<SpinnerSize>('md');
@ -35,19 +42,71 @@
'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 somaSnippet = $derived(
[
'<!-- Spinner is eidos-native — no soma layer. -->',
'<!-- Equivalent semantic markup (not real soma): -->',
'',
'<div',
' data-spinner',
variant !== 'ring' && ` data-variant="${variant}"`,
size !== 'md' && ` data-size="${size}"`,
color !== 'primary' && ` data-color="${color}"`,
' role="status"',
' aria-live="polite"',
' aria-label="Loading"',
'>',
variant === 'ring' && ' <span data-spinner-track aria-hidden="true"></span>',
variant === 'dots' &&
' <span data-spinner-track aria-hidden="true">' +
'<span data-spinner-dot></span><span data-spinner-dot></span><span data-spinner-dot></span></span>',
variant === 'bars' &&
' <span data-spinner-track aria-hidden="true">' +
'<span data-spinner-bar></span><span data-spinner-bar></span><span data-spinner-bar></span><span data-spinner-bar></span></span>',
withLabel && ' <span data-spinner-label>Loading…</span>',
'</div>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Spinner } from '$uix/eidos/components/spinner';",
'</' + 'script>',
'',
'<Spinner',
withLabel ? '<Spinner' : '<Spinner',
variant !== 'ring' && ` variant="${variant}"`,
size !== 'md' && ` size="${size}"`,
color !== 'primary' && ` color="${color}"`,
'>',
withLabel ? '>' : '/>',
withLabel && ' Loading…',
'</Spinner>'
withLabel && '</Spinner>'
]
.filter(Boolean)
.join('\n')
@ -59,23 +118,37 @@
<div data-uix-eyebrow>Feedback · Spinner</div>
<h1 data-uix-page-title>Spinner</h1>
<p data-uix-page-lede>
Indeterminate progress indicator. Three visual variants (<code>ring</code>,
<code>dots</code>, <code>bars</code>), five sizes, eight color accents, and an optional
inline label. Pure eidos primitive — no semantic events. Use
<code>&lt;Progress&gt;</code> instead when you have a known percentage.
Indeterminate progress indicator. Three visual variants
(<code>ring</code>, <code>dots</code>, <code>bars</code>), five sizes, eight color accents,
and an optional inline label. Eidos-native: no semantic events, no soma split — the morfo
declares two parts (Provider + decorative Track). Use
<a href="/uix/components/progress">&lt;Progress&gt;</a> instead when you have a known
percentage.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>parts</span>{partsList.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>{variants.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>scope</span>eidos</span>
<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>{variants.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>
<div data-uix-stage-area bind:this={stageRef}>
<Box
style="padding: var(--uix-space-6); display: flex; align-items: center; justify-content: center;"
style="padding: var(--uix-space-6); display: flex; align-items: center; justify-content: center; inline-size: 100%;"
>
{#if withLabel}
<Spinner {variant} {size} {color}>Loading…</Spinner>
@ -84,20 +157,31 @@
{/if}
</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>variant</span>
<span>{variant}</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>5</span></button
>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>5</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>0</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button
>
@ -107,9 +191,15 @@
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
Spinner is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The
<code>variant</code> prop swaps the inner Track DOM (one arc vs three dots vs four bars);
<code>size</code> drives a single <code>--_spinner-size</code> token that all three
geometries share.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
@ -148,114 +238,342 @@
</label>
</div>
<div data-uix-subsection-head>All variants side-by-side</div>
<Stack gap="var(--uix-space-4)">
{#each variants as v (v)}
<Box
style="display: flex; gap: var(--uix-space-6); align-items: center; padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
<code style="min-inline-size: 4rem;">{v}</code>
{#each sizes as s (s)}
<Spinner variant={v} size={s} {color} />
{/each}
</Box>
{/each}
</Stack>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>n/a · spinner is eidos-native — equivalent markup shown</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-subsection-head>Color matrix</div>
<Box style="display: flex; gap: var(--uix-space-5); flex-wrap: wrap; padding: var(--uix-space-3);">
{#each colors as c (c)}
<Box style="display: flex; flex-direction: column; align-items: center; gap: var(--uix-space-2);">
<Spinner {variant} {size} color={c} />
<code>{c}</code>
</Box>
{/each}
</Box>
</section>
{:else if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API</h2>
<table data-uix-api-table>
<thead>
<tr><th>prop</th><th>type</th><th>default</th><th>description</th></tr>
</thead>
<tbody>
<tr><td>variant</td><td>'ring' | 'dots' | 'bars'</td><td>'ring'</td><td>Visual style.</td></tr>
<tr><td>size</td><td>'xs'..'xl'</td><td>'md'</td><td>Track diameter / dot size.</td></tr>
<tr><td>color</td><td>ColorRole</td><td>'primary'</td><td>Accent palette.</td></tr>
<tr><td>aria-label</td><td>string?</td><td>—</td><td>Override the translated 'Loading'.</td></tr>
<tr><td>children</td><td>Snippet?</td><td>—</td><td>Optional inline label.</td></tr>
</tbody>
</table>
<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 · variant swaps Track DOM</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{:else if tab === 'morfo'}
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span> contract
</h2>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
Two parts: <code>provider</code> (role=status, polite live region) and <code>track</code>
(decorative, aria-hidden). The variant prop only swaps the inner DOM of Track.
Spinner exposes variant / size / color and an optional inline label snippet. Inherits
the full Box prop surface (gap, gridColumn, …) so it composes inline next to text.
</p>
<table data-uix-api-table>
<thead><tr><th>part</th><th>element</th><th>role</th><th>archetype</th></tr></thead>
<tbody>
{#each partsList as p (p.kebab)}
<div data-uix-subsection-head>Spinner-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">variant</td>
<td class="type">'ring' | 'dots' | 'bars'</td>
<td>
<code>ring</code> (default) — rotating arc, lightest. <code>dots</code> —
three bouncing dots, medium weight. <code>bars</code> — four stretching bars,
most attention.
</td>
</tr>
<tr>
<td class="name">size</td>
<td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td>
<td>Track diameter / dot size / bar height. Default <code>md</code>.</td>
</tr>
<tr>
<td class="name">color</td>
<td class="type">ColorRole</td>
<td>Accent palette. Default <code>primary</code>.</td>
</tr>
<tr>
<td class="name">aria-label</td>
<td class="type">string?</td>
<td>Override the translated 'Loading' default — use to give context.</td>
</tr>
<tr>
<td class="name">children</td>
<td class="type">Snippet?</td>
<td>Optional inline label rendered next to the spinner.</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">chakra-ui</td>
<td><code>&lt;Spinner&gt;</code></td>
<td>Chakra has one variant (ring). UIX adds <code>dots</code> + <code>bars</code> for higher-weight contexts.</td>
</tr>
<tr>
<td class="name">mantine</td>
<td><code>&lt;Loader&gt;</code></td>
<td>Mantine's Loader exposes three variants by the same names. UIX keeps the API but uses <code>role="status"</code> over Mantine's plain div.</td>
</tr>
<tr>
<td class="name">radix-themes</td>
<td><code>&lt;Spinner&gt;</code></td>
<td>Radix only ships the ring. UIX matches the API surface, adds variants + the inline-label snippet.</td>
</tr>
<tr>
<td class="name">react-aria</td>
<td><code>&lt;ProgressBar isIndeterminate&gt;</code></td>
<td>React Aria reuses ProgressBar in indeterminate mode. UIX separates concerns — <code>&lt;Progress&gt;</code> for determinate, <code>&lt;Spinner&gt;</code> for indeterminate.</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>{spinnerMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{spinnerMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{spinnerMorfo.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 · 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.spinner.label')</td>
<td class="type">recommended</td>
</tr>
<tr>
<td class="name"><code>aria-live</code></td>
<td>"polite"</td>
<td class="type">literal</td>
</tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Track · 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><code>{p.kebab}</code></td>
<td><code>{p.defaultElement}</code></td>
<td>{p.role ?? '—'}</td>
<td>{p.archetype ?? '—'}</td>
<td class="name"><code>aria-hidden</code></td>
<td>"true"</td>
<td class="type">literal</td>
</tr>
{/each}
</tbody>
</table>
</tbody>
</table>
</div>
</section>
{:else if tab === 'sema'}
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> events
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
Spinner has no semantic events — the loading state is announced only through ARIA. There's
no commit/emerge/shift to react to. Mount when work starts; unmount when it ends.
Spinner declares <strong>no semantic events</strong>. The loading state announces only
through ARIA — there is no commit/emerge/shift to react to. Mount the spinner when work
starts; unmount when it ends.
</p>
<p data-uix-section-desc>
If you need a perceptual cue when the work starts or finishes, attach it to the parent
action (the form submission that triggered the fetch, the route navigation that opened
the page) — not to Spinner.
</p>
</section>
{:else if tab === 'recipe'}
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Recipe</h2>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> · canonical usage
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Recipe lives in <code>src/uix/eidos/components/spinner/spinner.css</code>. All three
variants share private tokens (<code>--_spinner-size</code>,
<code>--_spinner-color</code>, <code>--_spinner-track</code>); each variant adds its own
keyframes block.
</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-spinner]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Inline-flex container with label gap.</td>
</tr>
<tr>
<td class="name"><code>[data-spinner][data-size='xs'..'xl']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Sets <code>--_spinner-size</code> + <code>--_spinner-thickness</code>.</td>
</tr>
<tr>
<td class="name"><code>[data-spinner][data-color='primary'..'loss']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Maps to color-mix accent + track tokens.</td>
</tr>
<tr>
<td class="name"><code>[data-spinner-track]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Track part marker (aria-hidden decoration).</td>
</tr>
<tr>
<td class="name"><code>[data-spinner][data-variant='ring'] [data-spinner-track]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Rotating arc via border-top-color contrast + <code>spinner-rotate</code> keyframes.</td>
</tr>
<tr>
<td class="name"><code>[data-spinner][data-variant='dots'] [data-spinner-dot]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Three dots with staggered <code>spinner-bounce</code> delays.</td>
</tr>
<tr>
<td class="name"><code>[data-spinner][data-variant='bars'] [data-spinner-bar]</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Four bars with staggered <code>spinner-stretch</code> delays.</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>Freezes all keyframes; ring loses the top-color contrast for low-distraction.</td>
</tr>
</tbody>
</table>
</div>
<pre data-uix-snippet><code>{eidosSnippet}</code></pre>
<div data-uix-subsection-head>All variants × sizes</div>
<Stack gap="var(--uix-space-4)">
{#each variants as v (v)}
<Box
style="display: flex; gap: var(--uix-space-6); align-items: center; padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
<code style="min-inline-size: 4rem;">{v}</code>
{#each sizes as s (s)}
<Spinner variant={v} size={s} {color} />
{/each}
</Box>
{/each}
</Stack>
</section>
{:else if tab === 'a11y'}
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<ul>
<li>
<code>role="status"</code> + <code>aria-live="polite"</code> on the root — the screen
reader announces the configured label when the spinner enters the DOM (typically when
you start a fetch).
</li>
<li>
Default label is the translated "Loading" / "Cargando". Override per-instance with
<code>aria-label="Loading orders"</code> for more specific context.
</li>
<li>
Visual track parts (<code>data-spinner-track</code>, <code>-dot</code>, <code>-bar</code>)
are <code>aria-hidden="true"</code> — decoration only.
</li>
<li>
Animation respects <code>prefers-reduced-motion</code> — under reduced motion the
rotation/bounce/stretch keyframes stop, but the visual stays so the user still sees the
loading affordance.
</li>
<li>
Use <code>&lt;Spinner&gt;</code> for unknown progress (network requests, async
rendering). Switch to <code>&lt;Progress&gt;</code> when you can report a percentage.
</li>
</ul>
<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> on the Provider — a polite live region. Screen
readers announce the label when the spinner enters the DOM.
</td>
</tr>
<tr>
<td class="name">Live region</td>
<td>
<code>aria-live="polite"</code> — announcements wait for the user to finish
their current speech utterance before being read.
</td>
</tr>
<tr>
<td class="name">Label</td>
<td>
Defaults to the translated 'Loading' / 'Cargando'. Override with
<code>aria-label="Loading orders"</code> for context.
</td>
</tr>
<tr>
<td class="name">Track decoration</td>
<td>
<code>data-spinner-track</code>, <code>-dot</code>, <code>-bar</code> all carry
<code>aria-hidden="true"</code>. Only the Provider's label is announced.
</td>
</tr>
<tr>
<td class="name">Keyboard</td>
<td>Spinner is not focusable. Tab order skips it entirely.</td>
</tr>
<tr>
<td class="name">Focus visible</td>
<td>Spinner does not paint a focus ring — no interactive state.</td>
</tr>
<tr>
<td class="name">Reduced motion</td>
<td>
All three keyframes stop under
<code>prefers-reduced-motion: reduce</code>. The visual stays so the user still
sees the loading affordance.
</td>
</tr>
<tr>
<td class="name">When to use</td>
<td>
Spinner for unknown progress (network requests, async rendering). Switch to
<a href="/uix/components/progress">&lt;Progress&gt;</a> when you can report a
percentage.
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

@ -1492,3 +1492,120 @@
gap: var(--uix-space-2);
}
}
/* ── Responsive docs shell ─────────────────────────────────────────── */
@media (max-width: 900px) {
[data-uix-shell] {
grid-template-columns: minmax(0, 1fr);
}
[data-uix-topbar] {
max-inline-size: 100vw;
overflow: hidden;
padding-inline: var(--uix-space-3);
gap: var(--uix-space-2);
}
[data-uix-brand],
[data-uix-topbar-nav],
[data-uix-topbar-tools] {
min-inline-size: 0;
}
[data-uix-brand-version] {
display: none;
}
[data-uix-topbar-nav] {
margin-inline-start: 0;
}
[data-uix-topbar-nav] a {
padding-inline: var(--uix-space-2);
}
[data-uix-topbar-tools] {
flex: 1 1 auto;
justify-content: flex-end;
overflow-x: auto;
overscroll-behavior-inline: contain;
scrollbar-width: none;
}
[data-uix-topbar-tools]::-webkit-scrollbar {
display: none;
}
[data-uix-rail] {
display: none;
}
[data-uix-canvas] {
grid-column: 1;
padding: var(--uix-space-8) var(--uix-space-4) var(--uix-space-12);
}
[data-uix-canvas-inner],
[data-uix-canvas-inner][data-with-toc='true'] {
display: block;
max-inline-size: none;
}
[data-uix-toc] {
display: none;
}
}
@media (max-width: 560px) {
[data-uix-topbar-nav] a[href='https://github.com/'] {
display: none;
}
[data-uix-canvas] {
padding: var(--uix-space-6) var(--uix-space-3) var(--uix-space-10);
}
[data-uix-page-title] {
font-size: var(--uix-text-4xl);
}
[data-uix-page-lede] {
font-size: var(--uix-text-lg);
}
[data-uix-page-meta],
[data-uix-tabs],
[data-uix-code-head] {
overflow-x: auto;
overscroll-behavior-inline: contain;
}
[data-uix-tabs] {
flex-wrap: nowrap;
}
[data-uix-tab],
[data-uix-meta-pill],
[data-uix-code-head] > * {
flex: 0 0 auto;
}
[data-uix-stage-area] {
place-items: stretch;
}
[data-uix-stage-trace] {
align-items: flex-start;
gap: var(--uix-space-2);
}
[data-uix-controls] {
grid-template-columns: minmax(0, 1fr);
padding: var(--uix-space-4);
}
[data-uix-part] {
grid-template-columns: minmax(0, 1fr);
gap: var(--uix-space-2);
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.