|
|
<script lang="ts">
|
|
|
import {
|
|
|
VirtualGrid,
|
|
|
type VirtualGridSize,
|
|
|
type VirtualGridVariant,
|
|
|
type VirtualGridColor,
|
|
|
type VirtualGridProviderSnippetProps
|
|
|
} from '$uix/eidos/components/virtual-grid';
|
|
|
import { Button } from '$uix/eidos/components/button';
|
|
|
import { compileMorfo } from '$uix/morfo';
|
|
|
import { virtualGridMorfo } from '@/uix/morfo/components/virtual-grid';
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
|
let tab = $state<Tab>('live');
|
|
|
|
|
|
let rowCount = $state(1000);
|
|
|
let columnCount = $state(50);
|
|
|
let rowSize = $state(40);
|
|
|
let columnSize = $state(120);
|
|
|
|
|
|
let size = $state<VirtualGridSize>('md');
|
|
|
let variant = $state<VirtualGridVariant>('surface');
|
|
|
let color = $state<VirtualGridColor>('primary');
|
|
|
let block = $state(true);
|
|
|
|
|
|
const compiled = compileMorfo(virtualGridMorfo);
|
|
|
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
|
|
const events = $derived([...compiled.actions.byName.values()]);
|
|
|
|
|
|
let providerSnippetProps: VirtualGridProviderSnippetProps | null = $state(null);
|
|
|
|
|
|
function scrollToRandom() {
|
|
|
const r = Math.floor(Math.random() * rowCount);
|
|
|
const c = Math.floor(Math.random() * columnCount);
|
|
|
providerSnippetProps?.scrollToCell(r, c, { rowAlign: 'center', columnAlign: 'center', behavior: 'smooth' });
|
|
|
}
|
|
|
|
|
|
function scrollToCorner() {
|
|
|
providerSnippetProps?.scrollToCell(rowCount - 1, columnCount - 1, { rowAlign: 'end', columnAlign: 'end' });
|
|
|
}
|
|
|
|
|
|
const eidosSnippet = $derived(
|
|
|
[
|
|
|
"<script lang='ts'>",
|
|
|
" import { VirtualGrid } from '$uix/eidos/components/virtual-grid';",
|
|
|
'</' + 'script>',
|
|
|
'',
|
|
|
'<VirtualGrid',
|
|
|
` rowCount={${rowCount}}`,
|
|
|
` columnCount={${columnCount}}`,
|
|
|
` rowSize={${rowSize}}`,
|
|
|
` columnSize={${columnSize}}`,
|
|
|
size !== 'md' && ` size="${size}"`,
|
|
|
variant !== 'surface' && ` variant="${variant}"`,
|
|
|
color !== 'primary' && ` color="${color}"`,
|
|
|
'>',
|
|
|
' {#snippet children({ virtualCells, totalWidth, totalHeight })}',
|
|
|
' <VirtualGrid.Viewport style="height: 24rem;">',
|
|
|
' <div style="position: relative; width: {totalWidth}px; height: {totalHeight}px;">',
|
|
|
' {#each virtualCells as c (c.key)}',
|
|
|
' <VirtualGrid.Cell {...c}>',
|
|
|
' R{c.rowIndex} · C{c.columnIndex}',
|
|
|
' </VirtualGrid.Cell>',
|
|
|
' {/each}',
|
|
|
' </div>',
|
|
|
' </VirtualGrid.Viewport>',
|
|
|
' {/snippet}',
|
|
|
'</VirtualGrid>'
|
|
|
]
|
|
|
.filter(Boolean)
|
|
|
.join('\n')
|
|
|
);
|
|
|
</script>
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
<header>
|
|
|
<div data-uix-eyebrow>Data · VirtualGrid</div>
|
|
|
<h1 data-uix-page-title>VirtualGrid</h1>
|
|
|
<p data-uix-page-lede>
|
|
|
2D windowed rendering — virtualises rows AND columns. Renders
|
|
|
the cells inside the visible rectangle plus row/column
|
|
|
overscan and keeps DOM size constant regardless of total cell
|
|
|
count. Use for large data tables (without the
|
|
|
<code>Table</code> machinery), thumbnail grids, spreadsheets.
|
|
|
</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>cells</span> {(rowCount * columnCount).toLocaleString()}
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>rows</span> {rowCount.toLocaleString()}
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>cols</span> {columnCount.toLocaleString()}
|
|
|
</span>
|
|
|
<span data-uix-meta-pill>
|
|
|
<span data-uix-meta-key>cellSize</span> {rowSize}×{columnSize}px
|
|
|
</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<div data-uix-stage>
|
|
|
<div data-uix-stage-area style="padding: var(--uix-space-3); display: flex; justify-content: center;">
|
|
|
<div style="inline-size: min(48rem, 100%);">
|
|
|
<VirtualGrid
|
|
|
{rowCount}
|
|
|
{columnCount}
|
|
|
{rowSize}
|
|
|
{columnSize}
|
|
|
{size}
|
|
|
{variant}
|
|
|
{color}
|
|
|
{block}
|
|
|
>
|
|
|
{#snippet children(snippetProps)}
|
|
|
{@const { virtualCells, totalWidth, totalHeight } = snippetProps}
|
|
|
{(providerSnippetProps = snippetProps, '')}
|
|
|
<VirtualGrid.Viewport style="block-size: 24rem;">
|
|
|
<div style="position: relative; inline-size: {totalWidth}px; block-size: {totalHeight}px;">
|
|
|
{#each virtualCells as c (c.key)}
|
|
|
<VirtualGrid.Cell
|
|
|
rowIndex={c.rowIndex}
|
|
|
columnIndex={c.columnIndex}
|
|
|
rowStart={c.rowStart}
|
|
|
columnStart={c.columnStart}
|
|
|
rowSize={c.rowSize}
|
|
|
columnSize={c.columnSize}
|
|
|
>
|
|
|
<div style="display: flex; align-items: center; justify-content: center; block-size: 100%; padding: var(--uix-space-1) var(--uix-space-2); border-block-end: 1px solid var(--color-border-default); border-inline-end: 1px solid var(--color-border-default); font-size: var(--font-size-xs); background: {(c.rowIndex + c.columnIndex) % 2 === 0 ? 'var(--color-surface-default)' : 'var(--color-surface-subtle)'};">
|
|
|
R{c.rowIndex}·C{c.columnIndex}
|
|
|
</div>
|
|
|
</VirtualGrid.Cell>
|
|
|
{/each}
|
|
|
</div>
|
|
|
</VirtualGrid.Viewport>
|
|
|
{/snippet}
|
|
|
</VirtualGrid>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div data-uix-stage-trace>
|
|
|
<span data-uix-stage-trace-key>note</span>
|
|
|
<span>scroll in both axes. <code>scrollToCell</code> in the snippet props lets you jump anywhere.</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>3</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 · {events.length}e</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>v1 is fixed-size only (consistent <code>rowSize</code> + <code>columnSize</code>). Dynamic sizes in 2D require attributing measured deltas to row vs column — a fundamentally ambiguous problem most virtualisers sidestep.</p>
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
<span data-uix-layer-badge="soma">soma</span> data + sizing
|
|
|
</div>
|
|
|
<div data-uix-controls>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>rowCount</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each [100, 1_000, 10_000] as n (n)}
|
|
|
<button data-uix-chip data-active={rowCount === n} onclick={() => (rowCount = n)}>{n.toLocaleString()}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>columnCount</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each [10, 50, 200] as n (n)}
|
|
|
<button data-uix-chip data-active={columnCount === n} onclick={() => (columnCount = n)}>{n}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>rowSize</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each [32, 40, 64] as n (n)}
|
|
|
<button data-uix-chip data-active={rowSize === n} onclick={() => (rowSize = n)}>{n}px</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>columnSize</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each [80, 120, 200] as n (n)}
|
|
|
<button data-uix-chip data-active={columnSize === n} onclick={() => (columnSize = n)}>{n}px</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>imperative</span>
|
|
|
<span style="display: flex; gap: var(--uix-space-1);">
|
|
|
<Button size="sm" variant="ghost" onclick={scrollToRandom}>Scroll to random</Button>
|
|
|
<Button size="sm" variant="ghost" onclick={scrollToCorner}>Scroll to corner</Button>
|
|
|
</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>
|
|
|
<span data-uix-layer-badge="eidos">eidos</span> visual props
|
|
|
</div>
|
|
|
<div data-uix-controls>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>variant</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each ['surface', 'outline', 'ghost'] as v (v)}
|
|
|
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v as VirtualGridVariant)}>{v}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>size</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
|
|
|
<button data-uix-chip data-active={size === s} onclick={() => (size = s as VirtualGridSize)}>{s}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>color</span>
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
{#each ['primary', 'secondary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as c (c)}
|
|
|
<button data-uix-chip data-active={color === c} onclick={() => (color = c as VirtualGridColor)}>{c}</button>
|
|
|
{/each}
|
|
|
</span>
|
|
|
</label>
|
|
|
<label data-uix-control>
|
|
|
<span data-uix-control-label>block</span>
|
|
|
<span data-uix-switch>
|
|
|
<input type="checkbox" bind:checked={block} />
|
|
|
<span data-uix-switch-label>{block ? 'on' : 'off'}</span>
|
|
|
</span>
|
|
|
</label>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-code>
|
|
|
<div data-uix-code-head>
|
|
|
<span data-uix-layer-badge="eidos">eidos</span>
|
|
|
<span>visual · VirtualGrid + Viewport + Cell</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>3 parts: <code>Provider · Viewport · Cell</code>. v1 fixed-size only.</p>
|
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">rowCount</td><td class="type">number</td><td class="default empty">required</td><td>Total rows.</td></tr>
|
|
|
<tr><td class="name">columnCount</td><td class="type">number</td><td class="default empty">required</td><td>Total columns.</td></tr>
|
|
|
<tr><td class="name">rowSize</td><td class="type">number</td><td class="default">40</td><td>Fixed row height (px).</td></tr>
|
|
|
<tr><td class="name">columnSize</td><td class="type">number</td><td class="default">120</td><td>Fixed column width (px).</td></tr>
|
|
|
<tr><td class="name">rowOverscan</td><td class="type">number</td><td class="default">2</td><td>Extra rows rendered above/below.</td></tr>
|
|
|
<tr><td class="name">columnOverscan</td><td class="type">number</td><td class="default">2</td><td>Extra columns rendered left/right.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
<div data-uix-subsection-head>Snippet props (children)</div>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr><td class="name">virtualCells</td><td class="type">VirtualCell[]</td><td>Cells in the visible rect + overscan.</td></tr>
|
|
|
<tr><td class="name">totalWidth</td><td class="type">number</td><td>Sum of column sizes.</td></tr>
|
|
|
<tr><td class="name">totalHeight</td><td class="type">number</td><td>Sum of row sizes.</td></tr>
|
|
|
<tr><td class="name">scrollToCell</td><td class="type">fn</td><td>Imperative scroll to a (row, col) cell.</td></tr>
|
|
|
<tr><td class="name">isEmpty</td><td class="type">boolean</td><td><code>rowCount === 0 || columnCount === 0</code>.</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'morfo'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title><span data-uix-layer-badge="morfo">morfo</span> · declarative contract</h2>
|
|
|
<div data-uix-table-wrap>
|
|
|
<table data-uix-table>
|
|
|
<thead><tr><th>Part</th><th>Marker</th><th>Element</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"><{part.defaultElement}></td>
|
|
|
<td class="type">{part.role ?? '—'}</td>
|
|
|
<td class="default">{part.optional ? 'yes' : 'no'}</td>
|
|
|
</tr>
|
|
|
{/each}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'sema'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title><span data-uix-layer-badge="sema">sema</span> · events</h2>
|
|
|
<p data-uix-section-desc>VirtualGrid is a pure performance primitive — no sema events. Compose with selection-aware grids when needed.</p>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'recipe'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
|
<p data-uix-section-desc>Selectors at <code>src/uix/eidos/components/virtual-grid/virtual-grid.css</code>. Eidos paints wrapper chrome only — Cell positioning comes from soma inline styles.</p>
|
|
|
</section>
|
|
|
{/if}
|
|
|
|
|
|
{#if tab === 'a11y'}
|
|
|
<section data-uix-section>
|
|
|
<h2 data-uix-section-title>Accessibility</h2>
|
|
|
<p data-uix-section-desc>VirtualGrid is a primitive — no implicit role on the container. Use <code>role="grid"</code> + proper aria attrs in the consumer when building tabular UIs, or compose with <code>TreeGrid</code> / <code>Table</code> for ARIA-complete tables.</p>
|
|
|
</section>
|
|
|
{/if}
|
|
|
</div>
|