Files
blueprints/archive/list-illustration.html

1501 lines
53 KiB
HTML
Raw Permalink 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.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>List&lt;Item&gt; — Functional Blueprint</title>
<style>
:root {
--paper: #0a2645; /* blueprint blue */
--paper-2: #0d2f54; /* panel */
--paper-3: #103763; /* raised */
--ink: #dbe9f7; /* primary ink */
--ink-dim: #7ea6cd; /* secondary ink */
--ink-faint: #4f7099; /* tertiary */
--line: rgba(200, 226, 255, 0.14);
--line-strong: rgba(200, 226, 255, 0.3);
--amber: #ffb84d; /* highlight / annotation */
--amber-dim: rgba(255, 184, 77, 0.14);
--cyan: #8fd0ff;
--green: #7fdca0;
--red: #ff8f8f;
--mono:
ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
background: var(--paper);
color: var(--ink);
font-family: var(--mono);
font-size: 13px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
/* blueprint grid */
background-image:
linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px),
linear-gradient(rgba(200, 226, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(200, 226, 255, 0.05) 1px, transparent 1px);
background-size:
120px 120px,
120px 120px,
24px 24px,
24px 24px;
background-position:
-1px -1px,
-1px -1px,
-1px -1px,
-1px -1px;
}
.sheet {
max-width: 1180px;
margin: 0 auto;
padding: 22px 20px 60px;
}
a {
color: var(--cyan);
text-decoration: none;
border-bottom: 1px dotted var(--ink-faint);
}
/* ── title block ─────────────────────────────────────────── */
.titleblock {
border: 1.5px solid var(--line-strong);
display: grid;
grid-template-columns: 1fr 130px 90px 110px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
}
.titleblock .cell {
border-left: 1px solid var(--line);
padding: 8px 12px;
}
.titleblock .cell:first-child {
border-left: none;
}
.tb-k {
color: var(--ink-faint);
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.tb-v {
color: var(--ink);
font-size: 13px;
margin-top: 2px;
}
.title-main {
padding: 12px 14px 10px;
}
.title-main h1 {
margin: 0;
font-size: 20px;
letter-spacing: 0.02em;
font-weight: 600;
}
.title-main h1 .brk {
color: var(--amber);
}
.title-main .sub {
color: var(--ink-dim);
font-size: 12px;
margin-top: 4px;
}
/* ── legend ──────────────────────────────────────────────── */
.legend {
display: flex;
flex-wrap: wrap;
gap: 6px 20px;
margin: 12px 0 18px;
padding: 8px 12px;
border: 1px solid var(--line);
color: var(--ink-dim);
font-size: 11px;
}
.legend b {
color: var(--ink);
}
.legend .sw {
color: var(--amber);
}
h2.section {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-dim);
font-weight: 600;
margin: 28px 0 10px;
display: flex;
align-items: center;
gap: 10px;
}
h2.section::after {
content: "";
flex: 1;
height: 1px;
background: var(--line);
}
/* ── tabs ────────────────────────────────────────────────── */
.tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 12px;
}
.tab {
font-family: var(--mono);
font-size: 12px;
color: var(--ink-dim);
background: transparent;
border: 1px solid var(--line);
padding: 5px 12px;
cursor: pointer;
letter-spacing: 0.06em;
transition: all 0.12s;
}
.tab:hover {
color: var(--ink);
border-color: var(--line-strong);
}
.tab.active {
color: var(--paper);
background: var(--amber);
border-color: var(--amber);
font-weight: 700;
}
.tab.full {
color: var(--cyan);
}
.tab.full.active {
color: var(--paper);
background: var(--cyan);
border-color: var(--cyan);
}
/* ── viewer layout ───────────────────────────────────────── */
.viewer {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: start;
}
@media (max-width: 820px) {
.viewer {
grid-template-columns: 1fr;
}
}
.panel {
border: 1.5px solid var(--line-strong);
background: var(--paper-2);
position: relative;
}
.panel .cap {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 12px;
border-bottom: 1px solid var(--line);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-dim);
background: rgba(255, 255, 255, 0.02);
}
.panel .cap .id {
color: var(--ink-faint);
}
.panel .body {
padding: 16px;
}
/* corner ticks (drawing crop marks) */
.panel::before,
.panel::after {
content: "";
position: absolute;
width: 8px;
height: 8px;
pointer-events: none;
}
.panel::before {
top: -1px;
left: -1px;
border-top: 1.5px solid var(--amber);
border-left: 1.5px solid var(--amber);
}
.panel::after {
bottom: -1px;
right: -1px;
border-bottom: 1.5px solid var(--amber);
border-right: 1.5px solid var(--amber);
}
/* ── specimen list ───────────────────────────────────────── */
.spec-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: var(--ink-dim);
font-size: 12px;
}
.search {
flex: 1;
border: 1px solid var(--line);
background: var(--paper);
padding: 5px 9px;
color: var(--ink);
position: relative;
}
.search .ph {
color: var(--ink-faint);
}
.search .q {
color: var(--amber);
}
.ctl {
border: 1px solid var(--line);
padding: 5px 9px;
white-space: nowrap;
position: relative;
}
.badge {
display: inline-block;
border: 1px solid var(--line-strong);
color: var(--cyan);
padding: 1px 7px;
font-size: 10px;
letter-spacing: 0.08em;
}
.listbox {
border: 1px solid var(--line);
position: relative;
background: var(--paper);
}
.listwrap {
display: flex;
}
.rows {
flex: 1;
min-width: 0;
}
.row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 11px;
border-bottom: 1px dashed var(--line);
position: relative;
}
.row:last-child {
border-bottom: none;
}
.row .chk {
width: 14px;
height: 14px;
border: 1px solid var(--ink-faint);
flex: none;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: var(--amber);
}
.row .chk.on {
border-color: var(--amber);
background: var(--amber-dim);
}
.row .txt {
flex: 1;
min-width: 0;
}
.row .txt .n {
color: var(--ink);
}
.row .txt .s {
color: var(--ink-faint);
font-size: 11px;
}
.row .chev {
color: var(--ink-dim);
}
.row.dim {
opacity: 0.32;
}
.row.dim .n {
text-decoration: line-through;
}
.row.sel {
background: var(--amber-dim);
}
.row.win {
background: rgba(143, 208, 255, 0.07);
}
.row.ghost .txt .n,
.row.ghost .txt .s {
color: transparent;
}
.row.ghost {
opacity: 0.4;
}
.skel {
height: 10px;
background: repeating-linear-gradient(
90deg,
var(--ink-faint),
var(--ink-faint) 6px,
transparent 6px,
transparent 12px
);
opacity: 0.5;
border-radius: 2px;
}
.skel.w1 {
width: 60%;
}
.skel.w2 {
width: 40%;
margin-top: 5px;
}
.row .actions {
display: flex;
gap: 0;
flex: none;
}
.row .actions .act {
padding: 9px 12px;
font-size: 11px;
border-left: 1px solid var(--line);
}
.row .actions .archive {
background: rgba(143, 208, 255, 0.12);
color: var(--cyan);
}
.row .actions .delete {
background: rgba(255, 143, 143, 0.14);
color: var(--red);
}
.row.swiped {
padding-right: 0;
}
.swipehint {
color: var(--ink-faint);
font-size: 10px;
margin-left: auto;
padding-right: 8px;
}
.scrollbar {
width: 8px;
flex: none;
background: rgba(255, 255, 255, 0.03);
border-left: 1px solid var(--line);
position: relative;
}
.scrollbar .thumb {
position: absolute;
left: 1px;
right: 1px;
background: var(--ink-faint);
opacity: 0.6;
}
.footer-load {
padding: 8px 11px;
border-top: 1px solid var(--line);
text-align: center;
color: var(--cyan);
font-size: 11px;
letter-spacing: 0.05em;
}
.empty-state {
padding: 26px 12px;
text-align: center;
color: var(--ink-faint);
}
.empty-state .big {
font-size: 22px;
color: var(--ink-dim);
}
.error-state {
padding: 22px 12px;
text-align: center;
color: var(--red);
}
.display-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
@media (max-width: 520px) {
.display-grid {
grid-template-columns: 1fr;
}
}
.mini {
border: 1px solid var(--line);
background: var(--paper);
}
.mini .mh {
padding: 4px 8px;
border-bottom: 1px solid var(--line);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--ink-dim);
text-transform: uppercase;
}
.mini .mb {
padding: 8px;
}
.mini .mrow {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 4px;
border-bottom: 1px dashed var(--line);
font-size: 11px;
}
.mini .mrow:last-child {
border-bottom: none;
}
/* highlight callouts */
.hl {
outline: 1.5px dashed var(--amber);
outline-offset: 3px;
position: relative;
z-index: 2;
border-radius: 1px;
}
.hl::after {
content: attr(data-tag);
position: absolute;
top: -10px;
left: 8px;
background: var(--paper);
color: var(--amber);
font-size: 9px;
padding: 0 5px;
border: 1px solid var(--amber);
white-space: nowrap;
letter-spacing: 0.1em;
line-height: 1.5;
z-index: 3;
}
.hl.tag-r::after {
left: auto;
right: 8px;
}
.hl.tag-b::after {
top: auto;
bottom: -10px;
}
/* ── readout ─────────────────────────────────────────────── */
.readout .rhead {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px;
margin-bottom: 2px;
}
.readout .rhead .fn {
font-size: 16px;
color: var(--amber);
font-weight: 700;
letter-spacing: 0.02em;
}
.capbadge {
border: 1px solid var(--cyan);
color: var(--cyan);
font-size: 10px;
padding: 1px 6px;
letter-spacing: 0.06em;
}
.capbadge.core {
border-color: var(--ink-faint);
color: var(--ink-dim);
}
.rverb {
color: var(--ink-faint);
font-size: 11px;
margin-bottom: 14px;
}
.block {
margin: 0 0 14px;
}
.block .k {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink-dim);
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
.block .k::after {
content: "";
flex: 1;
height: 1px;
background: var(--line);
}
.block .v {
color: var(--ink);
}
.code {
color: var(--cyan);
white-space: pre-wrap;
}
.field {
display: flex;
gap: 8px;
}
.field .fk {
color: var(--ink);
}
.field .ft {
color: var(--ink-faint);
}
.field .fc {
color: var(--cyan);
}
.beh {
border-left: 2px solid var(--line-strong);
padding-left: 10px;
margin-bottom: 8px;
}
.beh .sig {
color: var(--amber);
}
.beh .pe {
display: flex;
gap: 8px;
color: var(--ink-dim);
font-size: 12px;
}
.beh .pe .lbl {
color: var(--ink-faint);
width: 26px;
flex: none;
}
.beh .pe .ex {
color: var(--ink);
}
.warn {
color: var(--red);
}
.perf {
color: var(--amber);
}
ul.tight {
margin: 0;
padding-left: 16px;
}
ul.tight li {
margin: 2px 0;
}
.note {
color: var(--ink-faint);
font-style: italic;
font-size: 11px;
}
.sig-block {
background: var(--paper);
border: 1px solid var(--line);
padding: 10px 12px;
white-space: pre;
overflow-x: auto;
color: var(--ink);
font-size: 12px;
line-height: 1.6;
}
.sig-block .cm {
color: var(--ink-faint);
}
.sig-block .ty {
color: var(--cyan);
}
.fn-index {
display: grid;
grid-template-columns: auto 1fr;
gap: 2px 12px;
font-size: 12px;
}
.fn-index .fi-n {
color: var(--amber);
}
.fn-index .fi-d {
color: var(--ink-dim);
}
/* ── companions ──────────────────────────────────────────── */
.svg-wrap {
overflow-x: auto;
}
svg {
display: block;
max-width: 100%;
height: auto;
margin: 0 auto;
}
svg text {
font-family: var(--mono);
fill: var(--ink);
}
.node rect {
fill: var(--paper-3);
stroke: var(--line-strong);
stroke-width: 1.5;
}
.node text {
font-size: 12px;
}
.node .sub {
fill: var(--ink-faint);
font-size: 9.5px;
}
.edge {
fill: none;
stroke-width: 1.4;
}
.elabel {
font-size: 10px;
paint-order: stroke;
stroke: var(--paper);
stroke-width: 3.5px;
stroke-linejoin: round;
}
footer.credit {
margin-top: 34px;
padding-top: 14px;
border-top: 1px solid var(--line);
color: var(--ink-faint);
font-size: 11px;
line-height: 1.7;
}
footer.credit .k {
color: var(--ink-dim);
}
</style>
</head>
<body>
<div class="sheet">
<!-- ── TITLE BLOCK ─────────────────────────────────────── -->
<div class="titleblock">
<div class="title-main">
<h1>
List<span class="brk">&lt;</span>Item<span class="brk">&gt;</span> — Functional
Blueprint
</h1>
<div class="sub">
A scrollable, ordered set of items · rendered as an interactive schematic of its 7
functions
</div>
</div>
<div class="cell">
<div class="tb-k">Extends</div>
<div class="tb-v">Set&lt;Item&gt;</div>
</div>
<div class="cell">
<div class="tb-k">Sheet</div>
<div class="tb-v">1 / 1</div>
</div>
<div class="cell">
<div class="tb-k">Composition</div>
<div class="tb-v">+5 caps</div>
</div>
</div>
<!-- ── LEGEND ──────────────────────────────────────────── -->
<div class="legend">
<span
><span class="sw"></span> <b>highlighted region</b> — the part of the list this function
owns</span
>
<span><b></b> skeleton placeholder</span>
<span><b></b> subset</span>
<span><b></b> assignment / effect</span>
<span><span class="warn"></span> failure mode</span>
<span><span class="perf"></span> critical-performance threshold</span>
</div>
<!-- ── MAIN VIEWER ─────────────────────────────────────── -->
<h2 class="section">Functional viewer — select a function</h2>
<div class="tabs" id="tabs"></div>
<div class="viewer">
<div class="panel">
<div class="cap">
<span>Specimen · List&lt;Item&gt;</span><span class="id" id="specId">FIG.00</span>
</div>
<div class="body" id="specimen"></div>
</div>
<div class="panel readout">
<div class="cap"><span>Readout · Contract</span><span class="id">§ list.als</span></div>
<div class="body" id="readout"></div>
</div>
</div>
<!-- ── COMPANION 1: COMPOSITION ────────────────────────── -->
<h2 class="section">Anatomy — where the 7 functions come from</h2>
<div class="panel">
<div class="cap">
<span>Composition map</span><span class="id">Set ◁ List ◁ capabilities</span>
</div>
<div class="body svg-wrap" id="compmap"></div>
</div>
<!-- ── COMPANION 2: STATE MACHINE ──────────────────────── -->
<h2 class="section">Lifecycle — LoadState machine</h2>
<div class="panel">
<div class="cap">
<span>Composed state machine · List + Async + Paginated + Pullable</span
><span class="id">loadState : LoadState</span>
</div>
<div class="body svg-wrap" id="statemachine"></div>
</div>
<footer class="credit">
<div>
<span class="k">SOURCE</span> &nbsp;blueprint-ontology / blueprints / list &nbsp;·&nbsp;
formal model: <code>list.als</code>, <code>list.test.als</code>
</div>
<div>
<span class="k">READS AS</span> &nbsp;a <i>concept</i> (Jackson) / <i>contract</i>
declarative, categorical, abstract. See BLUEPRINT-METAPHOR.md
</div>
<div>
<span class="k">SCOPE</span> &nbsp;standard composition:
<code>List + Async + Paginated + Pullable + Selectable + Filterable</code>
</div>
</footer>
</div>
<script>
/* ══════════════════════════════════════════════════════════════
MODEL — the List blueprint, function by function.
Static content lifted verbatim from the blueprint README.
══════════════════════════════════════════════════════════════ */
const COMPOSED_SIG = `List<Item> + Async + Paginated + Pullable + Selectable + Filterable
items : seq Item ⟨comment⟩-- List: ordered, loaded subset
loadState : LoadState ⟨comment⟩-- Async · Paginated · Pullable
totalCount : Int ⟨comment⟩-- Paginated
selectionMode : SelectionMode ⟨comment⟩-- Selectable
selected : set Item ⟨comment⟩-- Selectable
visible : set Item ⟨comment⟩-- Filterable
⟨type⟩LoadState⟨/type⟩ = Idle | Loading | Refreshing | LoadingMore | Error
⟨type⟩SelectionMode⟨/type⟩ = None | Single | Multi`
const 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?"],
},
]
const SAMPLE = [
{ 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" },
]
/* ══════════════════════════════════════════════════════════════
SPECIMEN RENDERERS — one static frame per function.
══════════════════════════════════════════════════════════════ */
function esc(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
}
function rowHTML(it, opts = {}) {
const cls = ["row"]
if (opts.dim) cls.push("dim")
if (opts.sel) cls.push("sel")
if (opts.win) cls.push("win")
if (opts.ghost) cls.push("ghost")
if (opts.swiped) cls.push("swiped")
let hl = opts.hlTxt ? ` hl` : ""
let chk = ""
if (opts.check !== undefined) {
chk = `<span class="chk${opts.check ? " on" : ""}">${opts.check ? "✓" : ""}</span>`
}
let tail
if (opts.swiped) {
tail = `<span class="swipehint">◂ swiped</span><div class="actions"><span class="act archive">Archive</span><span class="act delete">Delete</span></div>`
} else {
tail = `<span class="chev${opts.hlChev ? " hl tag-r" : ""}"${opts.hlChev ? ' data-tag="◈ ACT"' : ""}>&gt;</span>`
}
return `<div class="${cls.join(" ")}">
${chk}
<div class="txt${hl}"${opts.hlTxt ? ` data-tag="${opts.hlTxt}"` : ""}><div class="n">${esc(it.n)}</div><div class="s">${esc(it.s)}</div></div>
${tail}
</div>`
}
function skelRow() {
return `<div class="row"><div class="txt"><div class="skel w1"></div><div class="skel w2"></div></div></div>`
}
function toolbar(cfg) {
// cfg: {search, searchQ, searchHl, sortHl, selectMeta, selectHl, badge}
let inner = ""
if (cfg.search !== false) {
const q = cfg.searchQ
? `<span class="q">${esc(cfg.searchQ)}</span>`
: `<span class="ph">search…</span>`
inner += `<div class="search${cfg.searchHl ? " hl" : ""}"${cfg.searchHl ? ' data-tag="◈ FILTER"' : ""}>⌕ ${q}</div>`
}
if (cfg.sort !== false) {
inner += `<div class="ctl${cfg.sortHl ? " hl tag-r" : ""}"${cfg.sortHl ? ' data-tag="◈ SORT"' : ""}>sort ▾ ${cfg.sortLabel || "name ↑"}</div>`
}
if (cfg.selectCtl) {
inner += `<div class="ctl${cfg.selectHl ? " hl tag-r" : ""}"${cfg.selectHl ? ' data-tag="◈ SELECT"' : ""}>☰ ${cfg.selectMeta || "select"}</div>`
}
return inner ? `<div class="spec-toolbar">${inner}</div>` : ""
}
function listbox(rowsHTML, cfg = {}) {
const scrollbar = cfg.scrollbar
? `<div class="scrollbar${cfg.scrollHl ? " hl tag-r" : ""}"${cfg.scrollHl ? ' data-tag="◈ SCROLL"' : ""}><div class="thumb" style="top:${cfg.thumbTop || 30}%;height:${cfg.thumbH || 36}%"></div></div>`
: ""
const footer = cfg.footer
? `<div class="footer-load${cfg.footerHl ? " hl tag-b" : ""}"${cfg.footerHl ? ' data-tag="◈ LOAD"' : ""}>${cfg.footer}</div>`
: ""
return `<div class="listbox"><div class="listwrap"><div class="rows">${rowsHTML}</div>${scrollbar}</div>${footer}</div>`
}
const RENDER = {
full() {
let rows = ""
rows += rowHTML(SAMPLE[0], { check: false, hlTxt: "◈ DISPLAY" })
rows += rowHTML(SAMPLE[1], { check: true, sel: true })
rows += rowHTML(SAMPLE[2], { check: false, hlChev: true })
rows += rowHTML(SAMPLE[3], { check: false })
rows += rowHTML(SAMPLE[4], { check: false })
const bar = `<div class="spec-toolbar">
<div class="search hl" data-tag="◈ FILTER">⌕ <span class="ph">search…</span></div>
<div class="ctl hl tag-r" data-tag="◈ SORT">sort ▾ name ↑</div>
<div class="ctl">☰ multi</div>
</div>`
const gutter = `<div class="hl" style="position:absolute;top:44px;left:6px;width:22px;height:150px" data-tag="◈ SELECT"></div>`
return (
bar +
`<div style="position:relative">` +
listbox(rows, {
scrollbar: true,
scrollHl: true,
thumbTop: 12,
thumbH: 44,
footer: "↻ loadMore ▾ · items 5 / 42",
footerHl: true,
}) +
gutter +
`</div>`
)
},
display() {
const empty = `<div class="mini"><div class="mh">Empty · items = ∅</div><div class="mb"><div class="empty-state"><div class="big">∅</div>no items — valid state,<br>not an error</div></div></div>`
const loading = `<div class="mini"><div class="mh">Loading</div><div class="mb">${skelRow()}${skelRow()}${skelRow()}</div></div>`
const error = `<div class="mini"><div class="mh">Error</div><div class="mb"><div class="error-state">⚠ failed to load<br><span style="color:var(--cyan)">↻ retry</span></div></div></div>`
const idle = `<div class="mini"><div class="mh">Idle</div><div class="mb">
<div class="mrow"><span style="color:var(--ink)">${SAMPLE[0].n}</span><span class="chev" style="margin-left:auto;color:var(--ink-dim)">&gt;</span></div>
<div class="mrow"><span style="color:var(--ink)">${SAMPLE[1].n}</span><span class="chev" style="margin-left:auto;color:var(--ink-dim)">&gt;</span></div>
<div class="mrow"><span style="color:var(--ink)">${SAMPLE[2].n}</span><span class="chev" style="margin-left:auto;color:var(--ink-dim)">&gt;</span></div>
</div></div>`
return `<div class="note" style="margin-bottom:10px">Four states, rendered distinctly. The skeleton mirrors item shape so the transition to Idle causes no layout shift.</div>
<div class="display-grid hl" data-tag="◈ DISPLAY">${empty}${loading}${error}${idle}</div>`
},
scroll() {
let rows = ""
rows += rowHTML(SAMPLE[0], { ghost: true })
rows += rowHTML(SAMPLE[1], { ghost: true })
rows += rowHTML(SAMPLE[2], { win: true })
rows += rowHTML(SAMPLE[3], { win: true })
rows += rowHTML(SAMPLE[4], { win: true })
rows += rowHTML(SAMPLE[5], { win: true })
rows += rowHTML(SAMPLE[6], { ghost: true })
rows += rowHTML(SAMPLE[7], { ghost: true })
return (
`<div class="note" style="margin-bottom:10px"><span style="color:var(--cyan)">▓ render window</span> — only these rows exist in the DOM. <span style="opacity:.5">Faded rows</span> are virtualized: not rendered, view references released.</div>` +
listbox(rows, { scrollbar: true, scrollHl: true, thumbTop: 30, thumbH: 40 })
)
},
load() {
let rows = ""
rows += rowHTML(SAMPLE[0], {})
rows += rowHTML(SAMPLE[1], {})
rows += rowHTML(SAMPLE[2], {})
rows += skelRow()
rows += skelRow()
return (
`<div class="spec-toolbar"><span class="badge">loadState = LoadingMore</span><span style="color:var(--ink-faint)">appending page 2…</span></div>` +
listbox(rows, { footer: "↻ loadMore ▾ · items 3 / 42", footerHl: true })
)
},
filter() {
let rows = ""
rows += rowHTML(SAMPLE[0], { dim: true })
rows += rowHTML(SAMPLE[1], { hlTxt: "◈ visible" }) // Item B matches "b"
rows += rowHTML(SAMPLE[2], { dim: true })
rows += rowHTML(SAMPLE[3], { dim: true })
rows += rowHTML(SAMPLE[4], { dim: true })
return (
toolbar({ searchQ: '"b"', searchHl: true, sort: false, badge: false }) +
`<div class="note" style="margin:2px 0 10px">visible = { x ∈ items | name matches "b" } · underlying items sequence is untouched.</div>` +
listbox(rows, {})
)
},
sort() {
// reordered view (descending)
let rows = ""
rows += rowHTML(SAMPLE[7], {})
rows += rowHTML(SAMPLE[6], {})
rows += rowHTML(SAMPLE[5], {})
rows += rowHTML(SAMPLE[4], {})
rows += rowHTML(SAMPLE[3], {})
return (
toolbar({ search: false, sortHl: true, sortLabel: "name ↓" }) +
`<div class="note" style="margin:2px 0 10px">Same members, new order (name ↓). Identity is unchanged — only the sequence is reordered.</div>` +
listbox(rows, {})
)
},
select() {
let rows = ""
rows += rowHTML(SAMPLE[0], { check: false })
rows += rowHTML(SAMPLE[1], { check: true, sel: true })
rows += rowHTML(SAMPLE[2], { check: false })
rows += rowHTML(SAMPLE[3], { check: true, sel: true })
rows += rowHTML(SAMPLE[4], { check: false })
return (
toolbar({
search: false,
sort: false,
selectCtl: true,
selectMeta: "MULTI · 2 selected",
selectHl: true,
}) +
`<div class="note" style="margin:2px 0 10px">selectionMode = Multi · selected = { Item B, Item D } ⊆ visible</div>` +
listbox(rows, {})
)
},
act() {
let rows = ""
rows += rowHTML(SAMPLE[0], {})
rows += rowHTML(SAMPLE[1], {})
rows += rowHTML(SAMPLE[2], { swiped: true })
rows += rowHTML(SAMPLE[3], {})
return (
`<div class="note" style="margin-bottom:10px">swipeAction(Item C, left) → reveals consumer-defined contextual actions. The list does not define them; it exposes the hook.</div>` +
`<div class="hl tag-b" data-tag="◈ ACT">` +
listbox(rows, {}) +
`</div>`
)
},
}
/* ══════════════════════════════════════════════════════════════
READOUT RENDERER
══════════════════════════════════════════════════════════════ */
function sigBlock() {
let h = esc(COMPOSED_SIG)
.replace(/⟨comment⟩(.*)/g, (m, p1) => `<span class="cm">${p1}</span>`)
.replace(/⟨type⟩(.*?)⟨\/type⟩/g, (m, p1) => `<span class="ty">${p1}</span>`)
return `<div class="sig-block">${h}</div>`
}
function readoutFull() {
const idx = FUNCTIONS.map(
(f) => `<div class="fi-n">${f.name}</div><div class="fi-d">${esc(f.verb)}</div>`,
).join("")
return `<div class="rhead"><span class="fn" style="color:var(--cyan)">FULL VIEW</span></div>
<div class="rverb">The standard composition and its seven functions. Select a function above to focus it.</div>
<div class="block"><div class="k">Composed signature</div>${sigBlock()}</div>
<div class="block"><div class="k">Core invariants</div>
<ul class="tight">
<li>items = ∅ is a valid state (empty list, not an error)</li>
<li>items are identity-unique — no duplicates by key</li>
<li>elems[items] = members — ordered form stays in sync with the set</li>
</ul>
</div>
<div class="block"><div class="k">Function index</div><div class="fn-index">${idx}</div></div>`
}
function readoutFn(f) {
let h =
`<div class="rhead"><span class="fn">${f.name}</span>` +
f.caps
.map((c) => `<span class="capbadge${c === "List" ? " core" : ""}">${c}</span>`)
.join("") +
`</div>`
h += `<div class="rverb">${esc(f.verb)}</div>`
if (f.state.length) {
h +=
`<div class="block"><div class="k">State touched</div>` +
f.state
.map(
([k, t]) =>
`<div class="field"><span class="fk">${k}</span><span class="ft">:</span><span class="fc">${esc(t)}</span></div>`,
)
.join("") +
`</div>`
}
if (f.behaviors.length) {
h +=
`<div class="block"><div class="k">Behavior${f.behaviors.length > 1 ? "s" : ""}</div>` +
f.behaviors
.map(
(b) => `<div class="beh"><div class="sig">${esc(b.sig)}</div>
<div class="pe"><span class="lbl">pre</span><span class="ex">${esc(b.pre)}</span></div>
<div class="pe"><span class="lbl">eff</span><span class="ex">${esc(b.eff)}</span></div></div>`,
)
.join("") +
`</div>`
}
if (f.invariants.length) {
h +=
`<div class="block"><div class="k">Invariant${f.invariants.length > 1 ? "s" : ""}</div><ul class="tight">` +
f.invariants.map((i) => `<li>${esc(i)}</li>`).join("") +
`</ul></div>`
}
if (f.failures.length) {
h +=
`<div class="block"><div class="k warn">⚠ Failure mode${f.failures.length > 1 ? "s" : ""}</div><ul class="tight warn">` +
f.failures.map((x) => `<li>${esc(x)}</li>`).join("") +
`</ul></div>`
}
if (f.perf.length) {
h +=
`<div class="block"><div class="k perf">⚡ Critical performance</div><ul class="tight perf">` +
f.perf.map((x) => `<li>${esc(x)}</li>`).join("") +
`</ul></div>`
}
if (f.notes.length) {
h += f.notes.map((n) => `<div class="note">— ${esc(n)}</div>`).join("")
}
return h
}
/* ══════════════════════════════════════════════════════════════
TABS + WIRING
══════════════════════════════════════════════════════════════ */
const tabsEl = document.getElementById("tabs")
const specEl = document.getElementById("specimen")
const readEl = document.getElementById("readout")
const specId = document.getElementById("specId")
const TABS = [{ id: "full", name: "Full view", fig: "00" }].concat(FUNCTIONS)
function select(id) {
document
.querySelectorAll(".tab")
.forEach((t) => t.classList.toggle("active", t.dataset.id === id))
if (id === "full") {
specEl.innerHTML = RENDER.full()
readEl.innerHTML = readoutFull()
specId.textContent = "FIG.00"
} else {
const f = FUNCTIONS.find((x) => x.id === id)
specEl.innerHTML = RENDER[id]()
readEl.innerHTML = readoutFn(f)
specId.textContent = "FIG." + f.fig
}
}
TABS.forEach((t) => {
const b = document.createElement("button")
b.className = "tab" + (t.id === "full" ? " full" : "")
b.dataset.id = t.id
b.textContent = t.id === "full" ? "▣ FULL VIEW" : t.name
b.onclick = () => select(t.id)
tabsEl.appendChild(b)
})
/* ══════════════════════════════════════════════════════════════
COMPANION 1 — COMPOSITION MAP (static SVG)
══════════════════════════════════════════════════════════════ */
function buildCompMap() {
const caps = [
{ id: "core", label: "List : Set", sub: "items : seq Item", color: "var(--ink-dim)" },
{ id: "async", label: "Async", sub: "loadState", color: "var(--cyan)" },
{ id: "paginated", label: "Paginated", sub: "totalCount", color: "var(--cyan)" },
{ id: "pullable", label: "Pullable", sub: "refresh", color: "var(--cyan)" },
{ id: "filterable", label: "Filterable", sub: "visible", color: "var(--cyan)" },
{ id: "selectable", label: "Selectable", sub: "selected", color: "var(--cyan)" },
{ id: "sortable", label: "Sortable", sub: "cmp", color: "var(--cyan)" },
{ id: "swipeable", label: "Swipeable", sub: "+ Reorderable", color: "var(--cyan)" },
]
const funcs = ["Display", "Scroll", "Load", "Filter", "Sort", "Select", "Act"]
const links = [
["core", "Display"],
["core", "Scroll"],
["async", "Display"],
["async", "Load"],
["paginated", "Load"],
["pullable", "Load"],
["filterable", "Filter"],
["selectable", "Select"],
["sortable", "Sort"],
["swipeable", "Act"],
]
const W = 640,
lx = 24,
lw = 170,
rx = 446,
rw = 170,
bh = 34,
gap = 6,
top = 16
const lstep = bh + gap,
rstep = 52
const H = top + caps.length * lstep + 6
const capY = {},
funcY = {}
caps.forEach((c, i) => (capY[c.id] = top + i * lstep + bh / 2))
funcs.forEach((f, i) => (funcY[f] = top + i * rstep + bh / 2))
let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Composition map">`
// edges first
links.forEach(([a, b]) => {
const y1 = capY[a],
y2 = funcY[b]
const x1 = lx + lw,
x2 = rx
const mx = (x1 + x2) / 2
const col = a === "core" ? "var(--ink-faint)" : "var(--cyan)"
s += `<path class="edge" d="M${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}" stroke="${col}" opacity=".55"/>`
s += `<circle cx="${x2}" cy="${y2}" r="2.4" fill="${col}"/>`
})
// left boxes (capabilities)
caps.forEach((c) => {
const y = capY[c.id] - bh / 2
s +=
`<g class="node"><rect x="${lx}" y="${y}" width="${lw}" height="${bh}" rx="2" ${c.id === "core" ? 'stroke="var(--amber)"' : ""}/>` +
`<text x="${lx + 12}" y="${y + 15}">${c.label}</text>` +
`<text class="sub" x="${lx + 12}" y="${y + 27}">${c.sub}</text></g>`
})
// right boxes (functions)
funcs.forEach((f) => {
const y = funcY[f] - bh / 2
s +=
`<g class="node"><rect x="${rx}" y="${y}" width="${rw}" height="${bh}" rx="2" stroke="var(--amber)"/>` +
`<text x="${rx + 12}" y="${y + 21}" fill="var(--amber)">${f}</text></g>`
})
// column labels
s += `<text x="${lx}" y="${H - 2}" fill="var(--ink-faint)" font-size="10" letter-spacing="1.5">CAPABILITIES (compose onto host)</text>`
s += `<text x="${rx}" y="${H - 2}" fill="var(--ink-faint)" font-size="10" letter-spacing="1.5">FUNCTIONS</text>`
s += `</svg>`
return s
}
document.getElementById("compmap").innerHTML = buildCompMap()
/* ══════════════════════════════════════════════════════════════
COMPANION 2 — LOADSTATE MACHINE (static SVG)
══════════════════════════════════════════════════════════════ */
function buildStateMachine() {
const nodes = {
loading: { x: 70, y: 150, w: 110, h: 40, label: "Loading" },
idle: { x: 300, y: 150, w: 96, h: 40, label: "Idle" },
refreshing: { x: 520, y: 56, w: 130, h: 40, label: "Refreshing" },
loadingmore: { x: 520, y: 250, w: 140, h: 40, label: "LoadingMore" },
error: { x: 770, y: 150, w: 96, h: 40, label: "Error" },
}
const COL = {
succeed: "var(--green)",
fail: "var(--red)",
refresh: "var(--amber)",
loadMore: "var(--cyan)",
init: "var(--ink-faint)",
}
const 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 },
]
const W = 900,
H = 330,
vbTop = -6
function center(n) {
return [n.x + n.w / 2, n.y + n.h / 2]
}
function edgePoint(n, tx, ty) {
const cx = n.x + n.w / 2,
cy = n.y + n.h / 2
let dx = tx - cx,
dy = ty - cy
if (dx === 0 && dy === 0) return [cx, cy]
const sx = dx !== 0 ? n.w / 2 / Math.abs(dx) : Infinity
const sy = dy !== 0 ? n.h / 2 / Math.abs(dy) : Infinity
const s = Math.min(sx, sy)
return [cx + dx * s, cy + dy * s]
}
let markers = ""
Object.entries(COL).forEach(([k, c]) => {
markers += `<marker id="ar-${k}" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="${c}"/></marker>`
})
let s = `<svg viewBox="0 ${vbTop} ${W} ${H}" role="img" aria-label="LoadState machine"><defs>${markers}</defs>`
// initial marker → loading
{
const l = nodes.loading
s += `<circle cx="26" cy="${l.y + l.h / 2}" r="5" fill="var(--ink-faint)"/>`
const [ex, ey] = edgePoint(l, 26, l.y + l.h / 2)
s += `<path class="edge" d="M31 ${l.y + l.h / 2} L ${ex} ${ey}" stroke="${COL.init}" marker-end="url(#ar-init)"/>`
}
// edges
edges.forEach((e) => {
const A = nodes[e.from],
B = nodes[e.to]
const [acx, acy] = center(A),
[bcx, bcy] = center(B)
const [sx, sy] = edgePoint(A, bcx, bcy)
const [tx, ty] = edgePoint(B, acx, acy)
const mx = (sx + tx) / 2,
my = (sy + ty) / 2
let dx = tx - sx,
dy = ty - sy
const len = Math.hypot(dx, dy) || 1
const nx = -dy / len,
ny = dx / len // unit normal
const cx = mx + nx * e.off,
cy = my + ny * e.off // control point
const col = COL[e.kind]
s += `<path class="edge" d="M${sx} ${sy} Q ${cx} ${cy} ${tx} ${ty}" stroke="${col}" marker-end="url(#ar-${e.kind})"/>`
// label at quadratic midpoint t=0.5
const lx = 0.25 * sx + 0.5 * cx + 0.25 * tx,
ly = 0.25 * sy + 0.5 * cy + 0.25 * ty
s += `<text class="elabel" x="${lx}" y="${ly - 3}" fill="${col}" text-anchor="middle">${e.label}</text>`
})
// nodes
Object.values(nodes).forEach((n) => {
s +=
`<g class="node"><rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="3"/>` +
`<text x="${n.x + n.w / 2}" y="${n.y + n.h / 2 + 4}" text-anchor="middle">${n.label}</text></g>`
})
// legend
const leg = [
["succeed()", "succeed"],
["fail()", "fail"],
["refresh()", "refresh"],
["loadMore()", "loadMore"],
]
let lxp = 70
leg.forEach(([t, k]) => {
s +=
`<line x1="${lxp}" y1="${vbTop + 14}" x2="${lxp + 18}" y2="${vbTop + 14}" stroke="${COL[k]}" stroke-width="2"/>` +
`<text x="${lxp + 24}" y="${vbTop + 18}" fill="var(--ink-dim)" font-size="10">${t}</text>`
lxp += 24 + t.length * 6.6 + 26
})
s += `</svg>`
return s
}
document.getElementById("statemachine").innerHTML = buildStateMachine()
/* init */
select("full")
</script>
</body>
</html>