feat: add Calendar blueprint illustration
Transcribe the calendar blueprint from blueprint-ontology (README + calendar.als): Navigate, Render, Load, Scope functions, its own Idle/Loading/Refreshing/Error lifecycle, and a bespoke month/week specimen. Composition follows the formal model (Async + Pullable).
This commit is contained in:
@@ -6,8 +6,10 @@ import type { Component } from "vue"
|
||||
import type { Blueprint } from "./blueprint"
|
||||
import { list } from "./listBlueprint"
|
||||
import { grid } from "./gridBlueprint"
|
||||
import { calendar } from "./calendarBlueprint"
|
||||
import ListSpecimen from "@/specimens/ListSpecimen.vue"
|
||||
import GridSpecimen from "@/specimens/GridSpecimen.vue"
|
||||
import CalendarSpecimen from "@/specimens/CalendarSpecimen.vue"
|
||||
|
||||
export interface RegistryEntry {
|
||||
blueprint: Blueprint
|
||||
@@ -17,6 +19,7 @@ export interface RegistryEntry {
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
list: { blueprint: list, specimen: ListSpecimen },
|
||||
grid: { blueprint: grid, specimen: GridSpecimen },
|
||||
calendar: { blueprint: calendar, specimen: CalendarSpecimen },
|
||||
}
|
||||
|
||||
export const BLUEPRINTS: Blueprint[] = Object.values(REGISTRY).map((e) => e.blueprint)
|
||||
|
||||
208
src/data/calendarBlueprint.ts
Normal file
208
src/data/calendarBlueprint.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
// The Calendar blueprint, as data. Lifted from the blueprint-ontology `calendar`
|
||||
// README + calendar.als. Calendar has its OWN lifecycle (no LoadingMore; adds
|
||||
// navigate/changeScope transitions), so it defines its own state machine rather
|
||||
// than sharing LOADSTATE_MACHINE with List/Grid. `visible` is a viewport
|
||||
// projection Calendar owns — not Filterable — so only Async + Pullable are
|
||||
// represented as composed capabilities, matching what the formal model models.
|
||||
|
||||
import type { Blueprint, StateMachine } from "./blueprint"
|
||||
|
||||
// Idle ⇄ Loading (navigate/changeScope out, succeed in), plus refresh via
|
||||
// Refreshing and the two failure edges. Node coordinates are hand-placed.
|
||||
const CALENDAR_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: 540, y: 56, w: 130, h: 40, label: "Refreshing" },
|
||||
{ id: "error", x: 770, y: 150, w: 96, h: 40, label: "Error" },
|
||||
],
|
||||
edges: [
|
||||
{ from: "loading", to: "idle", label: "succeed()", kind: "succeed", off: 24 },
|
||||
{ from: "loading", to: "error", label: "fail()", kind: "fail", off: 150 },
|
||||
{ from: "idle", to: "loading", label: "navigate(δ)", kind: "navigate", off: 20 },
|
||||
{ from: "idle", to: "loading", label: "changeScope(s)", kind: "changeScope", off: 56 },
|
||||
{ from: "idle", to: "refreshing", label: "refresh()", kind: "refresh", off: -16 },
|
||||
{ from: "refreshing", to: "idle", label: "succeed()", kind: "succeed", off: -16 },
|
||||
{ from: "refreshing", to: "error", label: "fail()", kind: "fail", off: 14 },
|
||||
{ from: "error", to: "refreshing", label: "refresh()", kind: "refresh", off: 70 },
|
||||
],
|
||||
}
|
||||
|
||||
export const calendar: Blueprint = {
|
||||
slug: "calendar",
|
||||
name: "Calendar<Event>",
|
||||
tagline: "A navigable temporal grid of time-bounded events within a scoped viewport.",
|
||||
role: "feature",
|
||||
extendsName: "Set<Event>",
|
||||
composesCount: 2,
|
||||
|
||||
sig: {
|
||||
header: "Calendar<Event> + Async + Pullable",
|
||||
fields: [
|
||||
{
|
||||
name: "members",
|
||||
type: "set CalendarEvent",
|
||||
note: "loaded events for the current viewport",
|
||||
},
|
||||
{ name: "scope", type: "Scope", note: "viewport granularity" },
|
||||
{ name: "focusDate", type: "Date", note: "anchor date of the viewport" },
|
||||
{ name: "loadState", type: "LoadState", note: "Async · Pullable" },
|
||||
{
|
||||
name: "visible",
|
||||
type: "set CalendarEvent",
|
||||
note: "derived: events anchored in the viewport",
|
||||
},
|
||||
],
|
||||
types: [
|
||||
"Scope = Day | Week | Month",
|
||||
"LoadState = Idle | Loading | Refreshing | Error",
|
||||
"CalendarEvent extends Item { startAt, endAt : DateTime } -- endAt ≥ startAt",
|
||||
"viewportStart = startOf(focusDate, scope) viewportEnd = endOf(focusDate, scope)",
|
||||
"visible = { e ∈ members | e.startAt ∈ [viewportStart, viewportEnd) } (derived)",
|
||||
],
|
||||
},
|
||||
|
||||
functions: [
|
||||
{
|
||||
id: "navigate",
|
||||
name: "Navigate",
|
||||
fig: "01",
|
||||
caps: ["Calendar"],
|
||||
verb: "Advance or retreat focusDate by one scope unit; trigger a fetch for the new viewport range.",
|
||||
state: [
|
||||
["focusDate", "Date"],
|
||||
["loadState", "LoadState"],
|
||||
],
|
||||
behaviors: [
|
||||
{
|
||||
sig: "navigate(delta)",
|
||||
pre: "loadState = Idle",
|
||||
eff: "focusDate ← focusDate ± 1 scope unit, loadState ← Loading",
|
||||
},
|
||||
],
|
||||
invariants: ["scope ∈ {Day, Week, Month} — viewport always has a defined granularity"],
|
||||
failures: [
|
||||
"navigate during load — navigate() called while loadState = Loading; pre-condition fails, the call must be ignored or queued, never double-fetched",
|
||||
],
|
||||
perf: [
|
||||
"new viewport renders within 300 ms — pre-fetch adjacent viewports while loadState = Idle",
|
||||
],
|
||||
notes: [
|
||||
"navigate(+1) advances by one scope unit (e.g. next week when scope = Week); navigate(-1) retreats.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "render",
|
||||
name: "Render",
|
||||
fig: "02",
|
||||
caps: ["Calendar", "Async"],
|
||||
verb: "Position events in their day / time-slot cells; clip multi-day events at the viewport boundary.",
|
||||
state: [
|
||||
["members", "set CalendarEvent"],
|
||||
["visible", "set CalendarEvent"],
|
||||
],
|
||||
behaviors: [],
|
||||
invariants: [
|
||||
"#1 visible ⊆ members — visible never exceeds loaded events",
|
||||
"#2 ∀ e ∈ visible : e.startAt ∈ [viewportStart, viewportEnd) — only events anchored in the viewport are shown",
|
||||
"#4 ∀ e ∈ members : e.endAt ≥ e.startAt — no negative-duration events",
|
||||
],
|
||||
failures: [
|
||||
"multi-day overflow — event.endAt > viewportEnd; the event must be clipped at viewportEnd in the display, the underlying data not truncated",
|
||||
"timezone mismatch — server returns UTC, client displays local time; events land in the wrong day or hour, so every DateTime must carry a timezone",
|
||||
],
|
||||
perf: [
|
||||
"60 fps minimum for time-axis scrolling in Day and Week scopes (hour-slot rows must be virtualized)",
|
||||
],
|
||||
notes: ["visible is always derived — it is never set directly."],
|
||||
},
|
||||
{
|
||||
id: "load",
|
||||
name: "Load",
|
||||
fig: "03",
|
||||
caps: ["Async", "Pullable"],
|
||||
verb: "Fetch events for the current viewport range; reload on every navigate and changeScope; support refresh.",
|
||||
state: [
|
||||
["loadState", "LoadState"],
|
||||
["members", "set CalendarEvent"],
|
||||
],
|
||||
behaviors: [
|
||||
{
|
||||
sig: "refresh()",
|
||||
pre: "loadState = Idle",
|
||||
eff: "loadState ← Refreshing; re-fetch events for the current viewport range",
|
||||
},
|
||||
{
|
||||
sig: "succeed(ev)",
|
||||
pre: "loadState ∈ {Loading, Refreshing}",
|
||||
eff: "members ← ev, loadState ← Idle",
|
||||
},
|
||||
{ sig: "fail()", pre: "loadState ∈ {Loading, Refreshing}", eff: "loadState ← Error" },
|
||||
],
|
||||
invariants: [
|
||||
"#6 members = ∅ is a valid state (empty calendar, not an error)",
|
||||
"#3 events are identity-unique within members",
|
||||
],
|
||||
failures: [
|
||||
"empty vs error conflation — server returns 0 events with a 4xx status; the empty viewport is treated as an error and the user sees the wrong UI",
|
||||
"stale events after reload — succeed(ev) does not replace members atomically; old and new events coexist and identity-uniqueness is violated",
|
||||
],
|
||||
perf: [
|
||||
"pre-fetch adjacent viewports while loadState = Idle so navigate() renders within 300 ms",
|
||||
],
|
||||
notes: [],
|
||||
},
|
||||
{
|
||||
id: "scope",
|
||||
name: "Scope",
|
||||
fig: "04",
|
||||
caps: ["Calendar"],
|
||||
verb: "Switch between Day, Week, and Month granularities without losing focusDate.",
|
||||
state: [
|
||||
["scope", "Scope"],
|
||||
["loadState", "LoadState"],
|
||||
],
|
||||
behaviors: [
|
||||
{
|
||||
sig: "changeScope(s)",
|
||||
pre: "loadState = Idle, s ≠ scope",
|
||||
eff: "scope ← s, loadState ← Loading; fetch events for the new [viewportStart, viewportEnd)",
|
||||
},
|
||||
],
|
||||
invariants: [
|
||||
"#5 scope ∈ {Day, Week, Month} — viewport always has a defined granularity",
|
||||
"changeScope preserves focusDate",
|
||||
],
|
||||
failures: [],
|
||||
perf: ["layout recalculates within one frame (≤ 16 ms) for already-loaded events"],
|
||||
notes: [],
|
||||
},
|
||||
],
|
||||
|
||||
coreInvariants: [
|
||||
"visible ⊆ members — only loaded events can be shown",
|
||||
"∀ e ∈ visible : e.startAt ∈ [viewportStart, viewportEnd) — only viewport-anchored events are shown",
|
||||
"∀ e ∈ members : e.endAt ≥ e.startAt — no negative-duration events",
|
||||
"events are identity-unique within members",
|
||||
"members = ∅ is a valid state (empty calendar, not an error)",
|
||||
],
|
||||
|
||||
composition: {
|
||||
caps: [
|
||||
{ id: "core", label: "Calendar : Set", sub: "members, scope, focusDate", core: true },
|
||||
{ id: "async", label: "Async", sub: "loadState" },
|
||||
{ id: "pullable", label: "Pullable", sub: "refresh" },
|
||||
],
|
||||
funcs: ["Navigate", "Render", "Load", "Scope"],
|
||||
links: [
|
||||
["core", "Navigate"],
|
||||
["core", "Render"],
|
||||
["core", "Scope"],
|
||||
["async", "Render"],
|
||||
["async", "Load"],
|
||||
["pullable", "Load"],
|
||||
],
|
||||
},
|
||||
|
||||
stateMachine: CALENDAR_MACHINE,
|
||||
}
|
||||
Reference in New Issue
Block a user