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
This commit is contained in:
Julien Calixte
2026-07-02 18:34:49 +02:00
parent 4405497658
commit 03804b10dc
21 changed files with 2304 additions and 765 deletions

99
src/data/blueprint.ts Normal file
View File

@@ -0,0 +1,99 @@
// Generic types shared by every blueprint's data module. A concrete blueprint
// (list, grid, …) is a `Blueprint` object; the generic Viewer renders it, and a
// bespoke Specimen component is paired with it in the registry (src/data/blueprints.ts).
export interface SigField {
name: string
type: string
note: string
}
export interface ComposedSig {
header: string
fields: SigField[]
types: string[]
}
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[]
}
// ── Composition map ────────────────────────────────────────────────────────
export interface CapNode {
id: string
label: string
sub: string
core?: boolean
}
export interface Composition {
caps: CapNode[]
funcs: string[]
links: [string, string][]
}
// ── State machine ──────────────────────────────────────────────────────────
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 interface StateMachine {
nodes: SmNode[]
edges: SmEdge[]
}
export const SM_COLORS: Record<SmKind | "init", string> = {
succeed: "#7fdca0",
fail: "#ff8f8f",
refresh: "#ffb84d",
loadMore: "#8fd0ff",
init: "#4f7099",
}
// ── Blueprint ──────────────────────────────────────────────────────────────
export type BlueprintRole = "feature" | "capability"
export interface Blueprint {
slug: string
name: string
tagline: string
role: BlueprintRole
extendsName?: string
composesCount?: number
sig: ComposedSig
functions: BlueprintFunction[]
coreInvariants: string[]
composition?: Composition
stateMachine?: StateMachine
}

26
src/data/blueprints.ts Normal file
View File

@@ -0,0 +1,26 @@
// Registry: the single source the router and Gallery both read from. A blueprint
// appears in the app precisely when it is registered here — pairing its typed data
// with its bespoke Specimen component.
import type { Component } from "vue"
import type { Blueprint } from "./blueprint"
import { list } from "./listBlueprint"
import { grid } from "./gridBlueprint"
import ListSpecimen from "@/specimens/ListSpecimen.vue"
import GridSpecimen from "@/specimens/GridSpecimen.vue"
export interface RegistryEntry {
blueprint: Blueprint
specimen: Component
}
export const REGISTRY: Record<string, RegistryEntry> = {
list: { blueprint: list, specimen: ListSpecimen },
grid: { blueprint: grid, specimen: GridSpecimen },
}
export const BLUEPRINTS: Blueprint[] = Object.values(REGISTRY).map((e) => e.blueprint)
export function entryFor(slug: string): RegistryEntry | undefined {
return REGISTRY[slug]
}

206
src/data/gridBlueprint.ts Normal file
View File

@@ -0,0 +1,206 @@
// 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,
}

View File

@@ -1,318 +1,230 @@
// 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.
// The generic Viewer renders this; ListSpecimen supplies the bespoke visual.
export interface SampleItem {
n: string
s: string
}
import type { Blueprint } from "./blueprint"
import { LOADSTATE_MACHINE } from "./loadStateMachine"
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 const list: Blueprint = {
slug: "list",
name: "List<Item>",
tagline: "A scrollable, ordered set of items.",
role: "feature",
extendsName: "Set<Item>",
composesCount: 5,
export interface SigField {
name: string
type: string
note: string
}
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",
],
},
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" },
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?"],
},
],
types: [
"LoadState = Idle | Loading | Refreshing | LoadingMore | Error",
"SelectionMode = None | Single | Multi",
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",
],
}
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"],
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" },
],
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.",
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"],
],
},
{
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",
stateMachine: LOADSTATE_MACHINE,
}

View File

@@ -0,0 +1,25 @@
import type { StateMachine } from "./blueprint"
// The composed LoadState lifecycle: Idle / Loading / Refreshing / LoadingMore / Error.
// Shared verbatim by every blueprint that composes Async + Paginated + Pullable
// (List, Grid, Feed, Table, …). Node coordinates are hand-placed for a clean layout.
export const LOADSTATE_MACHINE: StateMachine = {
nodes: [
{ 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" },
],
edges: [
{ 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 },
],
}