Files
blueprints/src/data/listBlueprint.ts
Julien Calixte 903e5465d5 refactor(state-machine): support multiple lifecycles per blueprint
Blueprint.stateMachine becomes stateMachines[], the viewer renders each
machine in its own panel, and StateMachine draws self-loops (a === b) for
recurring transitions that don't change state. Existing blueprints move
to the array form unchanged.
2026-07-02 23:52:03 +02:00

231 lines
8.4 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 List blueprint, as data. Lifted verbatim from the blueprint-ontology
// `list` README: signature, behaviors, invariants, failure modes, performance.
// The generic Viewer renders this; ListSpecimen supplies the bespoke visual.
import type { Blueprint } from "./blueprint"
import { LOADSTATE_MACHINE } from "./loadStateMachine"
export const list: Blueprint = {
slug: "list",
name: "List<Item>",
tagline: "A scrollable, ordered set of items.",
role: "feature",
extendsName: "Set<Item>",
composesCount: 5,
sig: {
header: "List<Item> + Async + Paginated + Pullable + Selectable + Filterable",
fields: [
{ name: "items", type: "seq Item", note: "List: ordered, loaded subset" },
{ name: "loadState", type: "LoadState", note: "Async · Paginated · Pullable" },
{ name: "totalCount", type: "Int", note: "Paginated" },
{ 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",
],
},
functions: [
{
id: "display",
name: "Display",
fig: "01",
caps: ["List", "Async"],
verb: "Render items in order; render skeleton placeholders during initial load; handle empty, loading, and error states distinctly.",
state: [
["items", "seq Item"],
["loadState", "LoadState"],
],
behaviors: [],
invariants: ["#1 items = ∅ is a valid state (empty list, not an error)"],
failures: [
"empty vs error conflation — network returns 0 items with 4xx; empty state treated as error, user sees wrong UI",
"skeleton / content mismatch — skeleton row dimensions differ from real item; layout shift on Loading → Idle",
],
perf: [
"skeleton placeholders render within 100 ms of navigation",
"real items replace the skeleton within 300 ms",
"skeleton dimensions must match real items to prevent layout shift",
],
notes: [],
},
{
id: "scroll",
name: "Scroll",
fig: "02",
caps: ["List"],
verb: "Allow the user to navigate the full item sequence; virtualize off-screen items.",
state: [["items", "seq Item"]],
behaviors: [],
invariants: ["items form a well-formed sequence (no duplicate indices) — l.items.isSeq"],
failures: [],
perf: [
"60 fps minimum — off-screen items must not be rendered (virtualization required)",
"items outside the render window must release their view references (memory)",
],
notes: [
"Scroll is navigation, not a mutating behavior — it moves the render window over items, it does not change them.",
],
},
{
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: "loadState = idle",
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", "#7 loadState = loadingMore → #items < totalCount"],
failures: [
"duplicate load — loadMore() called while loadState = loadingMore; duplicate items appended, items loses uniqueness",
"unknown total — streaming source, totalCount never known; use CursorPaginated with a hasMore sentinel instead",
],
perf: ["loadMore appends new items without layout shift or scroll-position jump"],
notes: [],
},
{
id: "filter",
name: "Filter",
fig: "04",
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: ["#8 visible ⊆ items — filter never introduces new items"],
failures: [
"filter after reload — predicate references old item shape; visible silently empties without error",
],
perf: ["result updates within one frame (≤ 16 ms) for in-memory data"],
notes: [
"Ephemeral: derives visible from items but does not mutate the sequence. Persisting a filter is a consumer concern.",
],
},
{
id: "sort",
name: "Sort",
fig: "05",
caps: ["Sortable"],
verb: "Reorder the sequence by a comparator without changing identity.",
state: [["items", "seq Item"]],
behaviors: [{ sig: "sort(cmp)", pre: "—", eff: "reorder items by comparator" }],
invariants: ["identity is preserved — sorting changes order, never membership"],
failures: [
"sort invalidates cursor (CursorPaginated) — cursor is stale; must reset to NoCursor and reload from page 1",
],
perf: [],
notes: [
"Ephemeral alongside filter: reorders the displayed sequence without persisting the change.",
],
},
{
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: [
"#3 selected ⊆ items",
"#4 selectionMode = none → selected = ∅",
"#5 selectionMode = single → #selected ≤ 1",
],
failures: [
"stale selection — refresh() clears items but not selected; selected ⊄ items, invariant #3 violated",
],
perf: [],
notes: [],
},
{
id: "act",
name: "Act",
fig: "07",
caps: ["Swipeable", "Reorderable"],
verb: "Expose per-item contextual actions (swipe, long-press) to the consumer.",
state: [],
behaviors: [
{
sig: "swipeAction(x, dir)",
pre: "x ∈ visible",
eff: "trigger consumer-defined contextual action",
},
{
sig: "reorder(from, to)",
pre: "capability: reorderable = true",
eff: "swap positions in items",
},
],
invariants: [],
failures: [
"reorder on filtered view — reorder operates on visible, not items; position mismatch between displayed and stored order",
],
perf: [],
notes: ["Open question: swipe direction — left | right only, or also up | down?"],
},
],
coreInvariants: [
"items = ∅ is a valid state (empty list, not an error)",
"items are identity-unique — no duplicates by key",
"elems[items] = members — ordered form stays in sync with the set",
],
composition: {
caps: [
{ id: "core", label: "List : Set", sub: "items : seq Item", 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" },
{ id: "sortable", label: "Sortable", sub: "cmp" },
{ id: "swipeable", label: "Swipeable", sub: "+ Reorderable" },
],
funcs: ["Display", "Scroll", "Load", "Filter", "Sort", "Select", "Act"],
links: [
["core", "Display"],
["core", "Scroll"],
["async", "Display"],
["async", "Load"],
["paginated", "Load"],
["pullable", "Load"],
["filterable", "Filter"],
["selectable", "Select"],
["sortable", "Sort"],
["swipeable", "Act"],
],
},
stateMachines: [LOADSTATE_MACHINE],
}