Files
blueprints/src/data/gridBlueprint.ts
Julien Calixte 03804b10dc feat: gallery + generic blueprint viewer, add Grid blueprint
Refactor the List-specific screen into a data-driven, reusable viewer:
- generic Blueprint type + shared LoadState machine + registry
- vue-router: / (gallery) and /b/:slug (illustration)
- BlueprintViewer renders any blueprint; specimens stay bespoke
- add Grid blueprint (6 functions incl. Position) + GridSpecimen
2026-07-02 18:34:49 +02:00

207 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The Grid blueprint, as data. Lifted verbatim from the blueprint-ontology
// `grid` README. Note: Grid has 6 functions (incl. the Grid-specific `Position`,
// no Sort/Act) and shares the composed LoadState machine with List.
import type { Blueprint } from "./blueprint"
import { LOADSTATE_MACHINE } from "./loadStateMachine"
export const grid: Blueprint = {
slug: "grid",
name: "Grid<Item>",
tagline: "A parameterized, scrollable set of items in a 2D cell layout.",
role: "feature",
extendsName: "Set<Item>",
composesCount: 6,
sig: {
header: "Grid<Item> + Async + Paginated + Pullable + Selectable + Filterable",
fields: [
{ name: "items", type: "seq Item", note: "row-major; loaded subset of total" },
{ name: "columns", type: "Int", note: "number of columns (≥ 1)" },
{ name: "totalCount", type: "Int", note: "Paginated" },
{ name: "loadState", type: "LoadState", note: "Async · Paginated · Pullable" },
{ name: "selectionMode", type: "SelectionMode", note: "Selectable" },
{ name: "selected", type: "set Item", note: "Selectable" },
{ name: "visible", type: "set Item", note: "Filterable" },
],
types: [
"LoadState = Idle | Loading | Refreshing | LoadingMore | Error",
"SelectionMode = None | Single | Multi",
"row(i) = i / columns col(i) = i mod columns (derived, not stored)",
],
},
functions: [
{
id: "display",
name: "Display",
fig: "01",
caps: ["Grid", "Async"],
verb: "Render items in a 2D cell layout; handle empty, loading, and error states distinctly.",
state: [
["items", "seq Item"],
["loadState", "LoadState"],
],
behaviors: [],
invariants: [
"#4 items = ∅ is a valid state (empty grid, not an error)",
"#8 all cells have equal dimensions",
],
failures: [
"empty vs error conflation — server returns 0 items with 4xx; empty state treated as error, user sees wrong UI",
"incomplete trailing row — #items mod columns ≠ 0; trailing cells must render as empty placeholders, not crash",
"variable-height cell break — items with differing heights; row alignment lost, grid degenerates to a non-uniform layout",
],
perf: ["visible rows appear within 300 ms of navigation"],
notes: [],
},
{
id: "scroll",
name: "Scroll",
fig: "02",
caps: ["Grid"],
verb: "Allow the user to navigate the full item sequence along one axis; virtualize off-screen rows.",
state: [["items", "seq Item"]],
behaviors: [],
invariants: [],
failures: [],
perf: [
"60 fps minimum — off-screen rows must not be rendered (row virtualization required)",
"rows outside the render window must release their cell view references (memory)",
],
notes: [
"Grid scrolls one axis; the row (not the individual cell) is the unit of virtualization.",
],
},
{
id: "load",
name: "Load",
fig: "03",
caps: ["Async", "Paginated", "Pullable"],
verb: "Fetch the initial page; support refresh (restart) and pagination (append).",
state: [
["loadState", "LoadState"],
["totalCount", "Int"],
],
behaviors: [
{
sig: "refresh()",
pre: "—",
eff: "loadState ← refreshing, reload items from page 1, selected ← ∅",
},
{
sig: "loadMore()",
pre: "#items < totalCount, loadState = idle",
eff: "loadState ← loadingMore, append next page to items",
},
],
invariants: ["#6 #items ≤ totalCount — loaded count never exceeds the server total"],
failures: [
"duplicate load — loadMore() called while loadState = loadingMore; duplicate items appended, identity-uniqueness lost",
],
perf: ["loadMore appends new rows without layout shift or scroll-position jump"],
notes: [],
},
{
id: "position",
name: "Position",
fig: "04",
caps: ["Grid"],
verb: "Derive each item's (row, col) from its index and columns; recompute on resize.",
state: [["columns", "Int"]],
behaviors: [
{
sig: "resize(n)",
pre: "n ≥ 1",
eff: "columns ← n, reflow layout; scroll position preserved by item",
},
],
invariants: [
"#1 columns ≥ 1 — at least one column at all times",
"#7 row(i) = i / columns, col(i) = i mod columns — position deterministically derived from index",
],
failures: [
"layout thrash on resize — columns changes while items are mid-scroll; visible rows reflow, scroll position jumps to a different item",
],
perf: ["column reflow completes within one frame (≤ 16 ms) for in-memory data"],
notes: [
"Derived: row(i) = i / columns (integer division), col(i) = i mod columns, rows = ⌈#items / columns⌉.",
"Open question: adaptive column count (floor(width / minCellWidth)) is treated as a consumer concern; the model uses a fixed integer.",
],
},
{
id: "filter",
name: "Filter",
fig: "05",
caps: ["Filterable"],
verb: "Restrict the visible set without mutating the underlying sequence.",
state: [["visible", "set Item"]],
behaviors: [{ sig: "filter(pred)", pre: "—", eff: "visible ← { x ∈ items | pred(x) }" }],
invariants: ["#5 visible ⊆ items — filter is purely restrictive"],
failures: [],
perf: [],
notes: ["Ephemeral: derives visible from items without mutating the sequence."],
},
{
id: "select",
name: "Select",
fig: "06",
caps: ["Selectable"],
verb: "Track user intent over a subset of items (none / single / multi).",
state: [
["selectionMode", "SelectionMode"],
["selected", "set Item"],
],
behaviors: [
{
sig: "select(x)",
pre: "x ∈ visible, selectionMode ≠ none",
eff: "selected ← selected {x}",
},
{ sig: "deselect(x)", pre: "x ∈ selected", eff: "selected ← selected \\ {x}" },
{ sig: "selectAll()", pre: "selectionMode = multi", eff: "selected ← visible" },
{ sig: "clearSelection()", pre: "—", eff: "selected ← ∅" },
],
invariants: ["selected ⊆ items — selection is always over loaded items"],
failures: [
"stale selection after refresh — refresh() clears items but not selected; selected ⊄ items, invariant violated",
],
perf: [],
notes: [],
},
],
coreInvariants: [
"columns ≥ 1 — at least one column at all times",
"members = elems(items) — set and sequence are always in sync",
"items are identity-unique within items",
"items = ∅ is a valid state (empty grid, not an error)",
"all cells have equal dimensions",
],
composition: {
caps: [
{ id: "core", label: "Grid : Set", sub: "items, columns", core: true },
{ id: "async", label: "Async", sub: "loadState" },
{ id: "paginated", label: "Paginated", sub: "totalCount" },
{ id: "pullable", label: "Pullable", sub: "refresh" },
{ id: "filterable", label: "Filterable", sub: "visible" },
{ id: "selectable", label: "Selectable", sub: "selected" },
],
funcs: ["Display", "Scroll", "Load", "Position", "Filter", "Select"],
links: [
["core", "Display"],
["core", "Scroll"],
["core", "Position"],
["async", "Display"],
["async", "Load"],
["paginated", "Load"],
["pullable", "Load"],
["filterable", "Filter"],
["selectable", "Select"],
],
},
stateMachine: LOADSTATE_MACHINE,
}