Appearance
Lists
A headless, framework-agnostic data-grid (list) engine for Lukeflow — a typed core plus a React renderer and a column designer. It is the spreadsheet-shaped sibling of the Forms library, built on the same monorepo conventions.
Library (vendored) · headless
Repository:
luke-lists· Type: Headless library (monorepo) · Packages: list-core / list-react / list-builder
Overview
luke-lists models a list as a standalone data grid: a ListSchema (an ordered set of strongly-typed ColumnDefs) plus a set of Rows of typed cells. It mirrors luke-forms — a pure headless core owns the model, validation, and behavior, while thin React packages provide the runtime renderer and the design-time builder. No DOM or framework concerns leak into the core.
The engine deliberately owns only type-specific cell behavior (parsing, formatting, validation, sort comparison) through a pluggable column-type registry; rendering (selection, keyboard nav, clipboard, fill-handle) is delegated to a swappable grid renderer. This keeps the intellectual property — the schema and column-type system — independent of any particular UI grid.
Where the data lives
The library is headless and stateless — it holds rows in memory and can serialize/restore a snapshot, but it does not persist anything. Tenant-scoped persistence (ListDefinition / ListVersion / ListRow) is planned as a LISTS capability in luke-core-engine, mirroring the FORMS capability. That backend is not started (see Status & gaps).
Architecture
Three npm-workspace packages build in strict dependency order (list-core → list-react → list-builder):
| Package | Public surface | Responsibility |
|---|---|---|
@lukeflow/list-core | createListEngine, ColumnTypeRegistry / createDefaultColumnTypeRegistry / defaultColumnTypeRegistry, validateSchema, plus the model types | Headless engine: ListSchema model, extensible column-type registry, row store, typed cell mutation, filter/sort view, validation, serialize/restore, change subscriptions. No DOM, no React. |
@lukeflow/list-react | useListEngine, ListGrid (ListGridProps) | React adapter + grid renderer. Wraps react-data-grid (MIT) for the Excel-style interaction layer. Renderer is swappable. |
@lukeflow/list-builder | useListBuilder (ListBuilderController, UseListBuilderOptions), ListBuilder (ListBuilderProps) | React column/schema designer built on the engine. |
The ListEngine
createListEngine(schema, options) returns a ListEngine that owns the row store and exposes a filtered-and-sorted view over the rows — with no rendering concerns. A renderer drives it through setCell/addRow/removeRow/moveRow, re-reads getView(), and subscribes for change notifications.
ts
import { createListEngine } from "@lukeflow/list-core";
const engine = createListEngine({
name: "Invoices",
columns: [
{ key: "vendor", label: "Vendor", type: "text", required: true },
{ key: "amount", label: "Amount", type: "currency" },
{ key: "paid", label: "Paid", type: "boolean" },
],
});
const row = engine.addRow({ vendor: "Acme" });
engine.setCell(row.id, "amount", "$1,250.00"); // parsed → 1250
engine.setSort({ columnKey: "amount", direction: "desc" });
const report = engine.validate(); // { ok, errors: CellError[] }
const snapshot = engine.serialize(); // { schema, rows } — persist & restore laterThe column-type registry
Each column type is a ColumnTypeHandler owning empty / parse / format / validate / compare. The built-in registry ships text, number, currency, boolean, date, and select. The ColumnType union is intentionally open (string & {}), so custom types can be registered without touching the core. parse is total — an unparseable input resolves to the empty value rather than throwing, so the engine never crashes on bad data; validate (not parse) is what reports the problem.
ts
import { createDefaultColumnTypeRegistry } from "@lukeflow/list-core";
const registry = createDefaultColumnTypeRegistry();
registry.register({
type: "email",
empty: () => null,
parse: (input) => (input == null || input === "" ? null : String(input)),
format: (v) => (v === null ? "" : String(v)),
validate: (v, col) =>
v && !String(v).includes("@") ? `${col.label} must be an email` : null,
compare: (a, b) => String(a ?? "").localeCompare(String(b ?? "")),
});
const engine = createListEngine(schema, { registry });Key features
- Typed cell mutation —
setCellroutes every raw input through the column type'sparse, so stored values are always canonical (e.g."$1,250"→1250). - Filtered + sorted view —
setFilters/setSortmaintain a derivedgetView(); filter operators includeeq,neq,contains,gt/gte/lt/lte, andempty/notEmpty. Empty cells always sort last. - Validation — per-cell
validate()returns a whole-listValidationReportofCellErrors;validateSchema()separately checks the design-time schema (non-empty name, ≥1 column, unique non-empty keys, known types,selectoptions present). - Serialize / restore —
serialize()produces a{ schema, rows }snapshot;createListEnginere-canonicalizesinitialRowsthrough their column types on restore. - Change subscriptions —
subscribe(listener)returns an unsubscribe function; every mutation emits. - Extensible column types — register custom
ColumnTypeHandlers or clone the default registry. - Defensive hardening —
setCell/getCell/removeRow/moveRoware no-ops on unknown row or column ids;readOnlycolumns reject writes;moveRowclamps its target index; restoredrow-Nids never collide with the engine's monotonic id counter; imported cells are canonicalized.
Renderer is swappable
list-react wraps react-data-grid (a free MIT grid) purely for the interaction layer. Because all type behavior lives in list-core, the grid renderer can be replaced without changing the engine or schema.
Technology
| Concern | Choice |
|---|---|
| Language | TypeScript (ESM, type: module) |
| Monorepo | npm workspaces (packages/*) |
| Build | tsup — dual ESM + CJS output with .d.ts |
| Tests | Vitest (jsdom for React packages) |
| UI runtime | React 19 (peer >=18) |
| Grid | react-data-grid 7.0.0-beta.59 (peer ^7.0.0-beta.50) |
| CI | GitHub Actions: build → typecheck → test, plus a blocking supply-chain audit gate |
| Node | >=18 (CI runs Node 20) |
| Versioning | 0.1.0-alpha.0, UNLICENSED (vendored, private) |
The CI build-test job builds first on purpose: typecheck and cross-package tests resolve siblings through their built dist/*.d.ts, which is gitignored. A separate supply-chain job runs npm audit --omit=dev --audit-level=high and fails the build on any high/critical advisory in runtime dependencies.
Local development
bash
npm install
npm run build # builds list-core → list-react → list-builder (dependency order)
npm run typecheck
npm testRebuild the core before testing adapters
Cross-package imports resolve via dist, so list-core must be built before running list-react / list-builder tests. npm run build handles the ordering; run it after changing the core.
Consumption follows the same vendored-dist model as luke-forms: each package's built dist (plus styles.css) is copied into luke-consumer-ui/vendor/@lukeflow/<pkg>/dist rather than published to a registry.
Status & gaps
Alpha, vendored / local-only. The library builds green and is consumed by luke-consumer-ui via vendored dist, but luke-lists main is not pushed to a remote. As of July 2026:
LISTSbackend capability is not started. There is noluke-core-enginepersistence layer (ListDefinition/ListVersion/ListRow, tenant-scoped RBAC) yet — it is a planned capability mirroring FORMS, to be built after the library stabilizes. The engine is in-memory only.- No CSV import/export and no pagination or virtualization — the grid renders the full view, which will not scale to large lists yet.
- Thin React/builder tests. The
list-reactandlist-buildertest scripts run with--passWithNoTests; near-all coverage is inlist-core. Total suite is ~27 tests across 6 files, concentrated in the engine and column-registry specs.
See the Completeness Scorecard for how Lists ranks against the other Lukeflow libraries and capabilities.