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

529 lines
23 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 { Grid, type GridProps } from '$uix/eidos/components/grid';
import { Box } from '$uix/eidos/components/box';
import { compileMorfo } from '$uix/morfo';
import { gridMorfo } from '@/uix/morfo/components/grid';
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 — Grid container props ────────────────────────────────
let templateColumns = $state<string>('repeat(5, 1fr)');
let autoFlow = $state<'row' | 'column' | 'dense' | 'row dense' | 'column dense'>('row');
let align = $state<'stretch' | 'start' | 'center' | 'end' | 'baseline'>('stretch');
let justify = $state<
'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch'
>('start');
let gap = $state<number>(3);
let padding = $state<number>(4);
// `autoRows` gives implicit rows a height. Without it, rows are sized by
// content, so a row-spanning cell collapses to its own content height when
// no sibling cell shares those rows (e.g. when colSpan covers every
// column). A fixed row height makes rowSpan visible regardless of colSpan —
// the same reason Chakra's example pins `templateRows`.
let autoRows = $state<string>('4rem');
// ── Live state — item placement on the featured cell (child Box) ──────
// Grid item span lives on the child `<Box>`, not the container — the
// UIX equivalent of Chakra's `<GridItem rowSpan colSpan>`. `span N`
// maps to `grid-row` / `grid-column` on the Box.
let rowSpan = $state<number>(2);
let colSpan = $state<number>(2);
// A cell can only span as many columns as the grid actually has, so the
// colSpan ceiling tracks the live column count read off the rendered grid
// (handles any `templateColumns` the user types — repeat(), lists, …).
let columnCount = $state<number>(5);
$effect(() => {
templateColumns; // re-measure whenever the track list changes
const grid = stageRef?.querySelector('[data-grid]');
if (!grid) return;
const cols = getComputedStyle(grid).gridTemplateColumns;
const n = cols && cols !== 'none' ? cols.split(/\s+/).filter(Boolean).length : 1;
columnCount = Math.max(1, n);
});
// Keep colSpan within the available columns when the grid shrinks.
$effect(() => {
if (colSpan > columnCount) colSpan = columnCount;
});
const featuredGridRow = $derived(rowSpan > 1 ? `span ${rowSpan}` : undefined);
const featuredGridColumn = $derived(colSpan > 1 ? `span ${colSpan}` : undefined);
const autoFlowOptions = ['row', 'column', 'dense', 'row dense', 'column dense'] as const;
const alignOptions = ['stretch', 'start', 'center', 'end', 'baseline'] as const;
const justifyOptions = [
'start',
'center',
'end',
'space-between',
'space-around',
'space-evenly',
'stretch'
] as const;
// Live preview cells — index 0 is the featured (spanning) cell; the rest
// cycle palettes so the grid reads as a real layout.
const cellPalettes = ['primary', 'affirm', 'risk', 'fulfill', 'secondary'] as const;
const cellCount = 10;
const gridProps = $derived<Partial<GridProps>>({
templateColumns: templateColumns || undefined,
autoRows: autoRows || undefined,
autoFlow,
align,
justify,
gap,
padding
});
// ── Compiled morfo ────────────────────────────────────────────────────
const compiled = compileMorfo(gridMorfo);
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 { Grid } from '$uix/eidos/components/grid';",
" import { Box } from '$uix/eidos/components/box';",
'</' + 'script>',
'',
'<Grid',
templateColumns !== 'repeat(5, 1fr)' && ` templateColumns="${templateColumns}"`,
autoRows && ` autoRows="${autoRows}"`,
autoFlow !== 'row' && ` autoFlow="${autoFlow}"`,
align !== 'stretch' && ` align="${align}"`,
justify !== 'start' && ` justify="${justify}"`,
gap > 0 && ` gap={${gap}}`,
padding > 0 && ` padding={${padding}}`,
'>',
` <Box${featuredGridRow ? ` gridRow="${featuredGridRow}"` : ''}${featuredGridColumn ? ` gridColumn="${featuredGridColumn}"` : ''}>1</Box>`,
' <Box>2</Box>',
' …',
'</Grid>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Layout · Grid</div>
<h1 data-uix-page-title>Grid</h1>
<p data-uix-page-lede>
<code>display:grid</code> container. Owns track templates
(<code>templateColumns</code>, <code>templateRows</code>,
<code>autoColumns</code>, <code>autoRows</code>, <code>autoFlow</code>), container alignment
(<code>align</code>, <code>justify</code>, <code>placeItems</code>,
<code>placeContent</code>) and the row/column gap. <strong>Item placement</strong> props
(<code>gridColumn</code>, <code>gridRow</code>, <code>gridArea</code>,
<code>placeSelf</code>) live on the child <a href="/uix/components/box">&lt;Box&gt;</a>.
For flex layouts use <a href="/uix/components/flex">&lt;Flex&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>extends</span>Box
</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}>
<Grid
{...gridProps}
width="100%"
style="border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); min-block-size: 10rem;"
>
{#each Array.from({ length: cellCount }) as _, i}
{@const palette = cellPalettes[i % cellPalettes.length]}
<Box
gridRow={i === 0 ? featuredGridRow : undefined}
gridColumn={i === 0 ? featuredGridColumn : undefined}
padding={2}
style={`border: 1px solid var(--color-${palette}-border); border-radius: var(--radius-sm); background: var(--color-${palette}-track); color: var(--color-${palette}-text); text-align: center; display: grid; place-items: center;${i === 0 ? ' font-weight: 600;' : ''}`}
>{i + 1}</Box>
{/each}
</Grid>
</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>templateColumns</span>
<span>{templateColumns}</span>
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>gap</span> {gap} ·
<span data-uix-stage-trace-key>autoFlow</span> {autoFlow}
</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>12+</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>
Grid is eidos-native — no <span data-uix-layer-badge="soma">soma</span> split. The
container props below map to <code>--grid-*</code> CSS variables. Grid also inherits
every <a href="/uix/components/box">Box</a> prop (padding, margin, size, position) by
composition. Item-side placement (<code>gridColumn</code>, <code>gridRow</code>,
<code>gridArea</code>, <code>placeSelf</code>) is set on the child Box.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · container behaviour
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>templateColumns <span data-uix-control-hint>repeat(5, 1fr) · try repeat(auto-fit, minmax(4rem, 1fr)) for reflow</span></span>
<input
type="text"
placeholder="(none)"
bind:value={templateColumns}
style="inline-size: 14rem;"
/>
</label>
<label data-uix-control>
<span data-uix-control-label>autoRows <span data-uix-control-hint>row height — makes rowSpan visible</span></span>
<input
type="text"
placeholder="(auto — content height)"
bind:value={autoRows}
style="inline-size: 14rem;"
/>
</label>
<label data-uix-control>
<span data-uix-control-label>autoFlow</span>
<span data-uix-chips role="radiogroup">
{#each autoFlowOptions as opt}
<button data-uix-chip data-active={autoFlow === opt} onclick={() => (autoFlow = opt)}
>{opt}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>align <span data-uix-control-hint>align-items</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>justify <span data-uix-control-hint>justify-content</span></span>
<span data-uix-chips role="radiogroup">
{#each justifyOptions as opt}
<button data-uix-chip data-active={justify === opt} onclick={() => (justify = opt)}
>{opt}</button
>
{/each}
</span>
</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>padding <span data-uix-control-hint>inherited from Box</span></span>
<input
type="number"
min="0"
max="8"
step="1"
bind:value={padding}
style="inline-size: 6rem;"
/>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · item placement · cell 1 (child Box)
</div>
<p data-uix-section-desc>
Span lives on the <strong>child Box</strong>, not the Grid — the UIX equivalent of
Chakra's <code>&lt;GridItem rowSpan colSpan&gt;</code>. Cell 1 maps to
<code>gridRow="span {rowSpan}"</code> / <code>gridColumn="span {colSpan}"</code>.
A cell can only span as many columns as the grid has, so <strong>colSpan</strong> is
capped at the <code>templateColumns</code> count (currently <strong>{columnCount}</strong>) —
raise <code>templateColumns</code> to span wider.
</p>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>rowSpan <span data-uix-control-hint>gridRow → span N rows</span></span>
<input
type="number"
min="1"
max="4"
step="1"
bind:value={rowSpan}
style="inline-size: 6rem;"
/>
</label>
<label data-uix-control>
<span data-uix-control-label>colSpan <span data-uix-control-hint>gridColumn → span N cols</span></span>
<input
type="number"
min="1"
max={columnCount}
step="1"
bind:value={colSpan}
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 · container props mapped to --grid-* CSS variables</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>
Grid adds container-side grid props on top of every <code>BoxProps</code> field. Track
templates accept any CSS <code>grid-template-*</code> string. <strong>Item placement</strong>
props (<code>gridColumn</code>, <code>gridRow</code>, <code>gridArea</code>,
<code>placeSelf</code>) live on the child <a href="/uix/components/box"><code>&lt;Box&gt;</code></a>.
</p>
<div data-uix-subsection-head>Track templates</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">templateColumns</td><td class="type">string</td><td class="default empty">—</td><td><code>grid-template-columns</code>. e.g. <code>"repeat(3, 1fr)"</code>.</td></tr>
<tr><td class="name">templateRows</td><td class="type">string</td><td class="default empty">—</td><td><code>grid-template-rows</code>.</td></tr>
<tr><td class="name">autoColumns</td><td class="type">string</td><td class="default">'auto'</td><td><code>grid-auto-columns</code>.</td></tr>
<tr><td class="name">autoRows</td><td class="type">string</td><td class="default">'auto'</td><td><code>grid-auto-rows</code>.</td></tr>
<tr><td class="name">autoFlow</td><td class="type">row | column | dense | row dense | column dense</td><td class="default">'row'</td><td><code>grid-auto-flow</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>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">align</td><td class="type">stretch | start | center | end | baseline | flex-start | flex-end</td><td class="default">'stretch'</td><td><code>align-items</code>.</td></tr>
<tr><td class="name">justify</td><td class="type">start | center | end | stretch | space-between | space-around | space-evenly | flex-start | flex-end</td><td class="default">'start'</td><td><code>justify-content</code>.</td></tr>
<tr><td class="name">placeItems</td><td class="type">string</td><td class="default empty">—</td><td>Shorthand for <code>align-items</code> + <code>justify-items</code>.</td></tr>
<tr><td class="name">placeContent</td><td class="type">string</td><td class="default empty">—</td><td>Shorthand for <code>align-content</code> + <code>justify-content</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Gap</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">gap</td><td class="type">number | string</td><td class="default empty">—</td><td>Shorthand for both axes. Number → <code>var(--space-N)</code>.</td></tr>
<tr><td class="name">rowGap</td><td class="type">number | string</td><td class="default empty">—</td><td>Per-axis override; takes precedence over <code>gap</code>.</td></tr>
<tr><td class="name">columnGap</td><td class="type">number | string</td><td class="default empty">—</td><td>Per-axis override; takes precedence over <code>gap</code>.</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 (except
<code>display</code>, which Grid locks to <code>grid</code>):
<code>padding*</code>, <code>margin*</code>, <code>width</code>, <code>height</code>,
<code>position</code>, <code>top/right/bottom/left</code>, <code>overflow*</code>.
</p>
<div data-uix-subsection-head>Item placement (on child Box)</div>
<p data-uix-section-desc>
Item-side props are not on Grid. Set them on the child Box:
<code>gridColumn</code>, <code>gridRow</code>, <code>gridArea</code>,
<code>placeSelf</code>, <code>alignSelf</code>, <code>justifySelf</code>, <code>order</code>.
See <a href="/uix/components/box">&lt;Box&gt;</a> → "Flex / grid item".
</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;Grid&gt;</code></td><td>Same shape: container props on Grid, item props on Box. Radix accepts <code>columns="3"</code> shorthand; UIX uses raw <code>templateColumns="repeat(3, 1fr)"</code>.</td></tr>
<tr><td class="name">chakra-ui</td><td><code>&lt;Grid&gt;</code> / <code>&lt;GridItem&gt;</code></td><td>Chakra ships a separate <code>&lt;GridItem&gt;</code> for placement; UIX uses Box on the child instead — one primitive, less indirection.</td></tr>
<tr><td class="name">mantine</td><td><code>&lt;SimpleGrid&gt;</code> / <code>&lt;Grid&gt;</code></td><td>Mantine splits into SimpleGrid (auto-fit responsive) and Grid (12-col span); UIX keeps a single Grid that accepts arbitrary CSS Grid templates.</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>{gridMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{gridMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{gridMorfo.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);">
Grid composes through <code>&lt;Box&gt;</code>, so the rendered DOM is a single
<code>div[data-box][data-grid]</code> shell. Recipe lives at
<code>src/uix/eidos/components/grid/grid.css</code> and layers on top of the Box recipe.
</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>
Grid declares no semantic events. As a passive layout primitive, it does not commit,
emerge, or react to anything — it just arranges its children. Components that animate
on appearance should compose Grid 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/grid/grid.css</code> and layers on top of
the Box recipe. The provider element carries both <code>data-box</code> and
<code>data-grid</code> markers because Grid renders through Box.
</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-grid]</code></td>
<td><span data-uix-tag data-kind="morfo">morfo</span></td>
<td>Provider marker.</td>
</tr>
<tr>
<td class="name"><code>[data-box][data-grid] {`{ grid-template-columns: var(--grid-template-columns, none); … }`}</code></td>
<td><span data-uix-tag data-kind="eidos">eidos</span></td>
<td>Layer container-side grid properties on the Box recipe.</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. Wrap Grid in a landmark (<code>&lt;main&gt;</code>, <code>&lt;nav&gt;</code>, <code>&lt;section&gt;</code>) when content needs one.</td></tr>
<tr><td class="name">Label</td><td>Not applicable — Grid has no content semantics. Labels belong to the interactive children.</td></tr>
<tr><td class="name">Keyboard</td><td>Grid is not focusable. Tab order follows DOM order even when CSS Grid reflows visual order (e.g. <code>order</code>, <code>grid-area</code>, <code>autoFlow="dense"</code>). Verify visual order matches focus order for keyboard users.</td></tr>
<tr><td class="name">Reduced motion</td><td>Grid does not animate. Layout shifts on resize follow CSS defaults.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.