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
207 lines
7.7 KiB
TypeScript
207 lines
7.7 KiB
TypeScript
// 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,
|
||
}
|