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:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user