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

390 lines
16 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 {
Container,
type ContainerProps,
type ContainerSize,
type ContainerAlign
} from '$uix/eidos/components/container';
import { Box } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { containerMorfo } from '@/uix/morfo/components/container';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Live state — Container props ──────────────────────────────────────
let size = $state<ContainerSize>('md');
let align = $state<ContainerAlign>('center');
let paddingX = $state<string>('');
let paddingY = $state<number>(4);
const sizeOptions: readonly ContainerSize[] = ['sm', 'md', 'lg', 'xl', 'xxl', 'full'];
const alignOptions: readonly ContainerAlign[] = ['left', 'center', 'right'];
const containerProps = $derived<Partial<ContainerProps>>({
size,
align,
paddingX: paddingX || undefined,
paddingY
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(containerMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
// ── Code snippet ──────────────────────────────────────────────────────
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Container } from '$uix/eidos/components/container';",
'</' + 'script>',
'',
'<Container',
size !== 'xl' && ` size="${size}"`,
align !== 'center' && ` align="${align}"`,
paddingX && ` paddingX="${paddingX}"`,
paddingY > 0 && ` paddingY={${paddingY}}`,
'>',
' …',
'</Container>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Container</div>
<h1 data-uix-page-title>Container</h1>
<p data-uix-page-lede>
Max-width centred content shell. Caps content at one of six tokens
(<code>sm · md · lg · xl · xxl · full</code>), applies a default horizontal padding via
<code>--container-padding-inline</code>, and centres via auto inline margins (or
<code>align="left"</code> / <code>align="right"</code>). For arbitrary
<code>max-width</code> values use <a href="/uix/components/box">&lt;Box&gt;</a> directly;
for semantic block padding use <a href="/uix/components/section">&lt;Section&gt;</a>.
</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>sizes</span>{sizeOptions.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>scope</span>eidos
</span>
</div>
</header>
<!-- Live preview always rendered -->
<div data-uix-stage>
<div
data-uix-stage-area
bind:this={stageRef}
style="background: repeating-linear-gradient(45deg, transparent 0 8px, var(--color-surface-2) 8px 9px); inline-size: 100%;"
>
<Container
{...containerProps}
style="border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-1);"
>
<Box
padding={3}
style="border: 1px solid var(--color-primary-border); border-radius: var(--radius-sm); background: var(--color-primary-track); color: var(--color-primary-text); text-align: center;"
>
Content (max-width: var(--container-width-{size}))
</Box>
</Container>
</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>size</span>
<span>{size}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>align</span> {align} ·
<span data-uix-stage-trace-key>paddingY</span> {paddingY}
</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>4</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>
Container is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split.
The hatched background of the stage shows the parent's width; the bordered region is
what Container caps. <code>full</code> removes the cap entirely. Container also
inherits every <a href="/uix/components/box">Box</a> prop (padding, margin) via
composition.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · width &amp; alignment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size <span data-uix-control-hint>→ var(--container-width-N)</span></span>
<span data-uix-chips role="radiogroup">
{#each sizeOptions as opt}
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>align <span data-uix-control-hint>horizontal alignment within parent</span></span>
<span data-uix-chips role="radiogroup">
{#each alignOptions as opt}
<button data-uix-chip data-active={align === opt} onclick={() => (align = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>paddingX <span data-uix-control-hint>override default inline padding</span></span>
<input
type="text"
placeholder="(token default)"
bind:value={paddingX}
style="inline-size: 12rem;"
/>
</label>
<label data-uix-control>
<span data-uix-control-label>paddingY <span data-uix-control-hint>0–8 → var(--space-N)</span></span>
<input
type="number"
min="0"
max="8"
step="1"
bind:value={paddingY}
style="inline-size: 6rem;"
/>
</label>
</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 · size token + auto inline margins</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>
Container layers <code>max-inline-size</code>, <code>padding-inline</code> and
<code>margin-inline</code> rules on top of <a href="/uix/components/box"
><code>&lt;Box&gt;</code></a
>. Sizes are tokens defined in the eidos foundation; <code>full</code> removes the cap
(<code>max-width: none</code>). For arbitrary <code>max-width</code> values pass
<code>maxWidth</code> on Box directly.
</p>
<div data-uix-subsection-head>Width &amp; alignment</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">size</td><td class="type">sm | md | lg | xl | xxl | full</td><td class="default">'xl'</td><td>Maps to <code>--container-width-{`{size}`}</code> token. <code>full</code> uncaps.</td></tr>
<tr><td class="name">align</td><td class="type">left | center | right</td><td class="default">'center'</td><td>Drives <code>margin-inline</code>: <code>auto</code>/<code>0</code> combinations.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Padding</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">paddingX</td><td class="type">number | string</td><td class="default">'var(--container-padding-inline)'</td><td>Default comes from the foundation token; override per-instance.</td></tr>
<tr><td class="name">paddingY</td><td class="type">number | string</td><td class="default empty">—</td><td>Inherited from Box; default is 0. Use <a href="/uix/components/section">&lt;Section&gt;</a> if you want token-keyed block padding.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Inherited from Box</div>
<p data-uix-section-desc>
Every <a href="/uix/components/box">BoxProps</a> field is forwarded:
<code>padding*</code>, <code>margin*</code>, <code>width</code> (locked to 100% by
Container), <code>height</code>, <code>position</code>, <code>overflow*</code>. Note
that Container sets <code>marginLeft</code> / <code>marginRight</code> from
<code>align</code>, so manual <code>marginX</code> overrides override the alignment.
</p>
<div data-uix-subsection-head>Reference comparison</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Library</th><th>Closest equivalent</th><th>Difference</th></tr></thead>
<tbody>
<tr><td class="name">radix-themes</td><td><code>&lt;Container&gt;</code></td><td>Radix exposes 4 sizes (<code>1..4</code>); UIX uses 6 named tokens (<code>sm..xxl</code> + <code>full</code>). Same centred-by-default shape.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;Container&gt;</code></td><td>Chakra defaults to <code>maxW="60ch"</code> regardless of theme; UIX wires through to foundation tokens for project-wide consistency.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;Container&gt;</code></td><td>Mantine takes <code>size="xs|sm|md|lg|xl"</code> or a raw number; UIX matches the named scale and adds <code>full</code> to opt out.</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>{containerMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{containerMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{containerMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts</td><td>{partsList.length}</td></tr>
<tr><td class="name">events</td><td>0</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>kebab</th><th>marker</th><th>element</th><th>archetype</th><th>optional</th></tr></thead>
<tbody>
{#each partsList as part}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.archetype}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<p data-uix-section-desc style="margin-top: var(--uix-space-4);">
Container composes through <code>&lt;Box&gt;</code>, so the rendered DOM is a single
<code>div[data-box][data-container]</code> shell with <code>data-size</code> and
<code>data-align</code> attributes for selector targeting.
</p>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · events
</h2>
<p data-uix-section-desc>
Container declares no semantic events. As a passive layout primitive, it does not
commit, emerge, or react to anything — it just constrains its children's width.
Content inside the container owns its own sema.
</p>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Container reads <code>--container-width-{`{size}`}</code> and
<code>--container-padding-inline</code> from the eidos foundation. The Svelte
component computes the resolved width and margins inline; the recipe at
<code>src/uix/eidos/components/container/container.css</code> mostly carries the
marker.
</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-container]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker.</td>
</tr>
<tr>
<td class="name"><code>[data-container][data-size='{`{size}`}']</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Per-size hooks (currently used by the component's inline style, available for consumer overrides).</td>
</tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr><td class="name">Role</td><td>None implicit. Default element is <code>&lt;div&gt;</code> — semantically neutral. Container is a layout wrapper; landmarks (<code>&lt;main&gt;</code>, <code>&lt;article&gt;</code>) belong on a separate element.</td></tr>
<tr><td class="name">Label</td><td>Not applicable.</td></tr>
<tr><td class="name">Keyboard</td><td>Container is not focusable. Tab order follows DOM order.</td></tr>
<tr><td class="name">Reduced motion</td><td>Container does not animate.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.