feat: scaffold blueprints SPA with List functional-blueprint illustration

This commit is contained in:
Julien Calixte
2026-07-02 18:12:20 +02:00
commit 4405497658
27 changed files with 5108 additions and 0 deletions

318
src/data/listBlueprint.ts Normal file
View File

@@ -0,0 +1,318 @@
// The List blueprint, as data. Lifted verbatim from the blueprint-ontology
// `list` README: signature, behaviors, invariants, failure modes, performance.
// Rendered by the components under src/components as an interactive schematic.
export interface SampleItem {
n: string
s: string
}
export const SAMPLE: SampleItem[] = [
{ n: "Item A", s: "#a1f3 · ready" },
{ n: "Item B", s: "#b2e8 · ready" },
{ n: "Item C", s: "#c7d1 · ready" },
{ n: "Item D", s: "#d0a4 · ready" },
{ n: "Item E", s: "#e5b9 · ready" },
{ n: "Item F", s: "#f3c2 · ready" },
{ n: "Item G", s: "#01d7 · ready" },
{ n: "Item H", s: "#12e0 · ready" },
]
export interface SigField {
name: string
type: string
note: string
}
export const COMPOSED_SIG: {
header: string
fields: SigField[]
types: string[]
} = {
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",
],
}
export interface Behavior {
sig: string
pre: string
eff: string
}
export interface BlueprintFunction {
id: string
name: string
fig: string
caps: string[]
verb: string
state: [string, string][]
behaviors: Behavior[]
invariants: string[]
failures: string[]
perf: string[]
notes: string[]
}
export const FUNCTIONS: BlueprintFunction[] = [
{
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?"],
},
]
export const CORE_INVARIANTS: string[] = [
"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 map model ──────────────────────────────────────────────────
export interface CapNode {
id: string
label: string
sub: string
core?: boolean
}
export const COMP_CAPS: CapNode[] = [
{ 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" },
]
export const COMP_FUNCS: string[] = ["Display", "Scroll", "Load", "Filter", "Sort", "Select", "Act"]
export const COMP_LINKS: [string, string][] = [
["core", "Display"],
["core", "Scroll"],
["async", "Display"],
["async", "Load"],
["paginated", "Load"],
["pullable", "Load"],
["filterable", "Filter"],
["selectable", "Select"],
["sortable", "Sort"],
["swipeable", "Act"],
]
// ── LoadState machine model ────────────────────────────────────────────────
export interface SmNode {
id: string
x: number
y: number
w: number
h: number
label: string
}
export type SmKind = "succeed" | "fail" | "refresh" | "loadMore"
export interface SmEdge {
from: string
to: string
label: string
kind: SmKind
off: number
}
export const SM_NODES: SmNode[] = [
{ id: "loading", x: 70, y: 150, w: 110, h: 40, label: "Loading" },
{ id: "idle", x: 300, y: 150, w: 96, h: 40, label: "Idle" },
{ id: "refreshing", x: 520, y: 56, w: 130, h: 40, label: "Refreshing" },
{ id: "loadingmore", x: 520, y: 250, w: 140, h: 40, label: "LoadingMore" },
{ id: "error", x: 770, y: 150, w: 96, h: 40, label: "Error" },
]
export const SM_EDGES: SmEdge[] = [
{ from: "loading", to: "idle", label: "succeed()", kind: "succeed", off: 0 },
{ from: "loading", to: "error", label: "fail()", kind: "fail", off: 150 },
{ from: "idle", to: "refreshing", label: "refresh()", kind: "refresh", off: -16 },
{ from: "refreshing", to: "idle", label: "succeed()", kind: "succeed", off: -16 },
{ from: "idle", to: "loadingmore", label: "loadMore()", kind: "loadMore", off: 16 },
{ from: "loadingmore", to: "idle", label: "succeed()", kind: "succeed", off: 16 },
{ from: "refreshing", to: "error", label: "fail()", kind: "fail", off: 14 },
{ from: "loadingmore", to: "error", label: "fail()", kind: "fail", off: -14 },
{ from: "error", to: "refreshing", label: "refresh()", kind: "refresh", off: 70 },
]
export const SM_COLORS: Record<SmKind | "init", string> = {
succeed: "#7fdca0",
fail: "#ff8f8f",
refresh: "#ffb84d",
loadMore: "#8fd0ff",
init: "#4f7099",
}