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

442 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 { Box, type BoxProps } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { boxMorfo } from '@/uix/morfo/components/box';
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 ────────────────────────────────────────────────────────
// Box has 40+ props; we expose the most-used ones as live controls
// and document the rest in the API table.
let display = $state<'block' | 'flex' | 'inline-flex' | 'grid' | 'inline-grid' | 'inline' | 'inline-block' | 'contents' | 'none'>('flex');
let padding = $state<number>(4);
let gap = $state<number>(3);
let width = $state<string>('');
let height = $state<string>('');
let overflow = $state<'visible' | 'hidden' | 'auto' | 'scroll' | 'clip' | ''>('');
let position = $state<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' | ''>('');
const displayOptions = ['block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'inline', 'inline-block', 'contents', 'none'] as const;
const overflowOptions = ['', 'visible', 'hidden', 'auto', 'scroll', 'clip'] as const;
const positionOptions = ['', 'static', 'relative', 'absolute', 'fixed', 'sticky'] as const;
const boxProps = $derived<Partial<BoxProps>>({
display,
padding,
gap: gap || undefined,
width: width || undefined,
height: height || undefined,
overflow: overflow || undefined,
position: position || undefined
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(boxMorfo);
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 snippets ─────────────────────────────────────────────────────
const somaSnippet = $derived(
[
'<!-- Box is eidos-native — no soma layer. -->',
'<!-- Equivalent semantic markup (not real soma): -->',
'',
'<div',
' data-box',
' style="display: var(--box-display); padding: var(--box-padding); gap: var(--box-gap);"',
'>',
' …',
'</div>'
].join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Box } from '$uix/eidos/components/box';",
'</' + 'script>',
'',
'<Box',
display !== 'flex' && ` display="${display}"`,
padding !== 0 && ` padding={${padding}}`,
gap > 0 && ` gap={${gap}}`,
width && ` width="${width}"`,
height && ` height="${height}"`,
overflow && ` overflow="${overflow}"`,
position && ` position="${position}"`,
'>',
' …',
'</Box>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Box</div>
<h1 data-uix-page-title>Box</h1>
<p data-uix-page-lede>
Universal box-model utility — size, padding/margin, position, overflow, plus flex/grid
<em>item</em> props (alignment, placement, order). Container-side flex/grid props (direction,
align, justify, wrap, templateColumns, …) live in
<a href="/uix/components/flex">&lt;Flex&gt;</a> and
<a href="/uix/components/grid">&lt;Grid&gt;</a> — UIX follows the Radix Themes split, not the
Chakra/MUI everything-on-Box model. Every prop maps to a <code>--box-*</code> CSS custom
property emitted inline; unset props fall through via <code>revert-layer</code>.
Eidos-native: no soma backing, no semantic events.
</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>props</span>40+
</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}>
<Box {...boxProps} style="border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); min-width: 16rem;">
<Box padding={2} style="border: 1px solid var(--color-primary-border); border-radius: var(--radius-sm); background: var(--color-primary-track); color: var(--color-primary-text);">Cell A</Box>
<Box padding={2} style="border: 1px solid var(--color-affirm-border); border-radius: var(--radius-sm); background: var(--color-affirm-track); color: var(--color-affirm-text);">Cell B</Box>
<Box padding={2} style="border: 1px solid var(--color-risk-border); border-radius: var(--radius-sm); background: var(--color-risk-track); color: var(--color-risk-text);">Cell C</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>display</span>
<span>{display}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>padding</span>
{padding} · <span data-uix-stage-trace-key>gap</span> {gap}
</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>40+</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>
Box is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. Each prop
maps to a <code>--box-*</code> custom property; numeric space values resolve to
<code>var(--space-N)</code>, numeric length values to <code>{`{N}px`}</code>, strings pass
through. Unset props fall back to the cascade via <code>revert-layer</code>.
</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>display</span>
<span data-uix-chips role="radiogroup">
{#each displayOptions as opt}
<button data-uix-chip data-active={display === opt} onclick={() => (display = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>padding <span data-uix-control-hint>0–8 → var(--space-N)</span></span>
<input type="number" min="0" max="8" step="1" bind:value={padding} style="inline-size: 6rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>gap <span data-uix-control-hint>0–8 → var(--space-N)</span></span>
<input type="number" min="0" max="8" step="1" bind:value={gap} style="inline-size: 6rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>width <span data-uix-control-hint>e.g. 320px, 50%, auto</span></span>
<input type="text" placeholder="(auto)" bind:value={width} style="inline-size: 8rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>height <span data-uix-control-hint>e.g. 200px, auto</span></span>
<input type="text" placeholder="(auto)" bind:value={height} style="inline-size: 8rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>overflow</span>
<span data-uix-chips role="radiogroup">
{#each overflowOptions as opt}
<button data-uix-chip data-active={overflow === opt} onclick={() => (overflow = opt)}>{opt || '(unset)'}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>position</span>
<span data-uix-chips role="radiogroup">
{#each positionOptions as opt}
<button data-uix-chip data-active={position === opt} onclick={() => (position = opt)}>{opt || '(unset)'}</button>
{/each}
</span>
</label>
</div>
<!-- ── Code snippets per layer ─────────────────────────────────── -->
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>n/a · box is eidos-native — equivalent markup shown</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<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 · every prop maps to a --box-* CSS variable</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>
Numeric values for space props (<code>padding*</code>, <code>margin*</code>,
<code>gap</code>) resolve to <code>var(--space-N)</code>. Numeric values for length props
(<code>width</code>, <code>height</code>, insets) resolve to <code>{`{N}px`}</code>. String
values pass through unchanged. <code>revert-layer</code> is the fallback for any unset prop.
</p>
<div data-uix-subsection-head>Sizing</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 / minWidth / maxWidth</td><td class="type">number | string</td><td>Number → px, string passes through.</td></tr>
<tr><td class="name">height / minHeight / maxHeight</td><td class="type">number | string</td><td>Same as width.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Spacing</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">padding / paddingX / paddingY</td><td class="type">number | string</td><td>Number → <code>var(--space-N)</code>.</td></tr>
<tr><td class="name">paddingTop / paddingRight / paddingBottom / paddingLeft</td><td class="type">number | string</td><td>Per-side overrides; cascade <code>paddingX/Y</code> → <code>padding</code>.</td></tr>
<tr><td class="name">margin / marginX / marginY</td><td class="type">number | string</td><td>Same as padding mapping.</td></tr>
<tr><td class="name">marginTop / marginRight / marginBottom / marginLeft</td><td class="type">number | string</td><td>Per-side overrides.</td></tr>
<tr><td class="name">gap</td><td class="type">number | string</td><td>Useful with display flex/grid.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Display + position</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">display</td><td class="type">block | inline | inline-block | flex | inline-flex | grid | inline-grid | contents | none</td><td>—</td></tr>
<tr><td class="name">position</td><td class="type">static | relative | absolute | fixed | sticky</td><td>—</td></tr>
<tr><td class="name">top / right / bottom / left</td><td class="type">number | string</td><td>Number → px.</td></tr>
<tr><td class="name">inset / insetX / insetY</td><td class="type">number | string</td><td>Shorthand insets.</td></tr>
<tr><td class="name">overflow / overflowX / overflowY</td><td class="type">visible | hidden | clip | scroll | auto</td><td>—</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Flex / grid item</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">flex / grow / shrink</td><td class="type">number | string</td><td>Item-side flex props.</td></tr>
<tr><td class="name">basis</td><td class="type">number | string</td><td>Number → px.</td></tr>
<tr><td class="name">order</td><td class="type">number</td><td>—</td></tr>
<tr><td class="name">alignSelf</td><td class="type">auto | start | end | center | stretch | baseline</td><td>—</td></tr>
<tr><td class="name">justifySelf</td><td class="type">auto | start | end | center | stretch</td><td>—</td></tr>
<tr><td class="name">placeSelf</td><td class="type">string</td><td>Shorthand for align-self + justify-self.</td></tr>
<tr><td class="name">gridColumn / gridRow</td><td class="type">string</td><td>Grid item placement. e.g. <code>"1 / 3"</code>, <code>"span 2"</code>.</td></tr>
<tr><td class="name">gridArea</td><td class="type">string</td><td>Named grid area, e.g. <code>"header"</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Escape hatch</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">style</td><td class="type">string</td><td>Extra inline CSS appended after Box's variable declarations. Use for one-offs the prop surface doesn't cover.</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">radix-themes</td><td><code>&lt;Box&gt;</code></td><td>Radix exposes spacing props as <code>p</code>/<code>px</code> shorthands; UIX uses full names. Both compile to CSS vars.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;Box&gt;</code></td><td>Chakra mixes the style-props system with theme aliases (<code>bg</code>, <code>color</code>); UIX Box is layout-only — color / typography live elsewhere.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;Box&gt;</code></td><td>Mantine compiles styles to classnames; UIX writes inline CSS variables for transparent debugging.</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>{boxMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{boxMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{boxMorfo.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);">
The Provider part emits only the <code>data-box</code> marker — no states, no data
properties, no aria attributes, no keyboard. The recipe consumes the
<code>--box-*</code> CSS variables the component writes inline.
</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>
Box declares no semantic events. As a passive layout primitive, it does not commit, emerge,
or react to anything — it just styles its children. Components that animate or change state
on appearance should compose Box with an interactive primitive (popover, drawer,
collapsible) that owns the relevant sema verbs.
</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/box/box.css</code>. Every property reads
its own <code>--box-*</code> custom property and falls back via
<code>revert-layer</code> so consumers can mix Box with any other styling.
</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-box]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker. Single root element emitted by the component.</td>
</tr>
<tr>
<td class="name"><code>[data-box] {`{ display: var(--box-display, revert-layer); … }`}</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Read every <code>--box-*</code> custom property, fall back to the cascade when unset.</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. Use a semantic wrapper (<code>&lt;main&gt;</code>, <code>&lt;section&gt;</code>, <code>&lt;article&gt;</code>) around Box when content needs a landmark.</td></tr>
<tr><td class="name">Label</td><td>Not applicable — Box has no content semantics. Labels belong to the interactive child the Box wraps.</td></tr>
<tr><td class="name">Keyboard</td><td>Box is not focusable. Tab order follows children.</td></tr>
<tr><td class="name">Focus visible</td><td>Box does not paint a focus ring. Focus styling belongs to the interactive children.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.