Data display
Recipe: filter and page a local table
Complete page component beneath Root. The small page size makes pagination visible without a backend.
<script lang="ts">
import { Card, EmptyState, Field, Input, Pagination, Table, type ColumnDef } from '@atom-forge/ui';
import { Search } from 'lucide-svelte';
type Person = { id: number; name: string; role: string };
const searchId = $props.id();
let people = $state<Person[]>([
{ id: 1, name: 'Alice', role: 'Admin' },
{ id: 2, name: 'Bob', role: 'Editor' },
{ id: 3, name: 'Carol', role: 'Reader' },
]);
const columns: ColumnDef<Person>[] = [
{ key: 'name', label: 'Name', grow: true },
{ key: 'role', label: 'Role', width: '10rem' },
];
let query = $state('');
let page = $state(1);
const pageSize = 2;
const filtered = $derived(people.filter(person => person.name.toLowerCase().includes(query.toLowerCase())));
const total = $derived(Math.max(1, Math.ceil(filtered.length / pageSize)));
const currentPage = $derived(Math.min(Math.max(1, page), total));
const visible = $derived(filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize));
</script>
<Card class="p-4 flex flex-col gap-4">
<h2>People</h2>
<Field label="Search by name" for={searchId}>
<Input id={searchId} bind:value={query} oninput={() => page = 1}/>
</Field>
{#if filtered.length === 0}
<EmptyState icon={Search} title="No matching people" description="Try another name."/>
{:else}
<Table data={visible} {columns}/>
{#if total > 1}
<Pagination page={currentPage} {total} onchange={next => page = next}/>
{/if}
{/if}
<p role="status">{filtered.length} matching people</p>
</Card>
Filter before computing page count and slicing. Pagination.total is a page count, not a record count; pages start at 1 and onchange updates caller state. Reset on search input and clamp the effective page if the collection shrinks.
Application responsibilities
- Fetch data, distinguish loading from errors and empty results, implement retry, and cancel/ignore stale requests. The local array above deliberately has no network lifecycle.
- Implement sorting, selection, editing, filtering, pagination, and virtualization as needed. Table renders the supplied rows in their supplied order; it is not a data-grid engine.
- Own URL synchronization, server-side paging, permissions, formatting, and accessible alternatives for visualizations.
- Treat columns as initialization configuration: Table captures the initial array internally and fills missing
visible flags on its column objects. Later replacements require remounting; bind:columns currently does not report visibility-menu changes back to the caller.
- Do not combine
formatter with snippet, or grow with shrink, in a column. fixed protects visibility and affects no-scroll weighting; it does not create a sticky column.
- For custom row actions, use explicit accessible buttons/links. Table's
rowClick ignores interactive descendants, but caller-owned selection and navigation still need deliberate behavior.
Data display
Recipe: filter and page a local table
Complete page component beneath Root. The small page size makes pagination visible without a backend.
<script lang="ts"> import { Card, EmptyState, Field, Input, Pagination, Table, type ColumnDef } from '@atom-forge/ui'; import { Search } from 'lucide-svelte'; type Person = { id: number; name: string; role: string }; const searchId = $props.id(); let people = $state<Person[]>([ { id: 1, name: 'Alice', role: 'Admin' }, { id: 2, name: 'Bob', role: 'Editor' }, { id: 3, name: 'Carol', role: 'Reader' }, ]); const columns: ColumnDef<Person>[] = [ { key: 'name', label: 'Name', grow: true }, { key: 'role', label: 'Role', width: '10rem' }, ]; let query = $state(''); let page = $state(1); const pageSize = 2; const filtered = $derived(people.filter(person => person.name.toLowerCase().includes(query.toLowerCase()))); const total = $derived(Math.max(1, Math.ceil(filtered.length / pageSize))); const currentPage = $derived(Math.min(Math.max(1, page), total)); const visible = $derived(filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)); </script> <Card class="p-4 flex flex-col gap-4"> <h2>People</h2> <Field label="Search by name" for={searchId}> <Input id={searchId} bind:value={query} oninput={() => page = 1}/> </Field> {#if filtered.length === 0} <EmptyState icon={Search} title="No matching people" description="Try another name."/> {:else} <Table data={visible} {columns}/> {#if total > 1} <Pagination page={currentPage} {total} onchange={next => page = next}/> {/if} {/if} <p role="status">{filtered.length} matching people</p> </Card>Filter before computing page count and slicing.
Pagination.totalis a page count, not a record count; pages start at 1 andonchangeupdates caller state. Reset on search input and clamp the effective page if the collection shrinks.Application responsibilities
visibleflags on its column objects. Later replacements require remounting;bind:columnscurrently does not report visibility-menu changes back to the caller.formatterwithsnippet, orgrowwithshrink, in a column.fixedprotects visibility and affects no-scroll weighting; it does not create a sticky column.rowClickignores interactive descendants, but caller-owned selection and navigation still need deliberate behavior.