diff --git a/DESIGN.md b/DESIGN.md index b079cf2..b48d344 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -247,6 +247,48 @@ reinforce the afrofuturist identity without overwhelming the layout: - Cards animate in on scroll-reveal (fade-in-up) and on hover (gold border + glow), respecting `prefers-reduced-motion`. +### Loop states + +The boucle label state machine rendered as a progression between "How boucle +works" and "Why boucle" (background `--surface-base`). It makes the +deterministic loop's states — the product's most concrete differentiator — +visible instead of stated as prose. + +- **Content** — driven by the `loopStates` content collection (one entry per + state: `order`, `label`, `description`, `type`, `typeLabel`) plus a + `loopStatesHeader` single-file collection for the title/subtitle/aria-label. + All copy is editable from the Sveltia CMS, like every other section. +- **Nine states, in order** — `boucle:triage`, `boucle:spec-review`, + `boucle:todo`, `boucle:working`, `boucle:review`, `boucle:approval`, + `boucle:merging`, `boucle:done`, `boucle:human`. +- **Colour coding** (existing tokens only — no new colours): autonomous states + use a cyan chip on `--surface-raised` (`--accent-cyan`), human-decision states + use a gold chip (`--accent`), terminal states are muted (`--text-secondary` + chip, struck-through label, dashed border) so they read as exits from the + loop rather than steps in it. A legend with `aria-hidden` swatches reinforces + the colour-meaning mapping; the colour is never the only signal because each + chip also carries a visible `typeLabel` text ("Autonomous" / "Human decides" / + "Terminal" / "Escalation"). +- **Each state** renders as a raised `--surface-raised` node with a 6px radius, + a number badge (mono font, type-coloured), a mono label chip, the visible + type label, and a one-line `--text-secondary` description. +- **Layout** — mobile-first single column. Below 768px the states stack + vertically with a neutral connector rail running down the left edge through + the number badges; the escalation state (`boucle:human`) is pulled out of + the numbered sequence into its own branch block beneath the list, with a + visible join notch and an "Escalation branch" tag connecting it back to the + main flow. At ≥768px the sequence becomes a 2-column grid; at ≥1024px a + 4-column grid, reading left-to-right, top-to-bottom, with the escalation + branch centred beneath the sequence. The branch is conveyed purely by + layout (join notch + tag), not by a new colour. No horizontal page scroll + at 390px or 1440px. +- **Motion** — none. No animation or scroll-triggered reveal on the states + (non-goal); `prefers-reduced-motion` is naturally satisfied. +- **Accessibility** — a semantic section element with an aria-label, an + ordered list for the sequence, each chip's label + `typeLabel` as readable + text, and the legend `aria-hidden` so screen readers get the sequence from + the ordered list rather than duplicated swatch text. + ### Quick start - Minimal: a single `curl -fsSL https://boucle.dev/install.sh | bash` oneliner. diff --git a/public/admin/config.yml b/public/admin/config.yml index 94283a0..4d9a08a 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -175,6 +175,45 @@ collections: widget: image hint: Pick the card icon SVG from the Assets library, e.g. /icons/card-1.svg. + # ---------- Loop states header (single file) ---------- + - name: loopStatesHeader + label: Loop states — header + label_singular: Loop states + description: '"Loop states" section title, subtitle, and aria-label.' + files: + - name: loop-states-header + label: Loop states header + file: src/content/loop-states-header/loop-states-header.md + fields: + - { name: title, label: Title, widget: string } + - { name: sub, label: Subtitle, widget: text } + - { name: ariaLabel, label: Section aria-label, widget: string } + + # ---------- Loop states (label state machine) ---------- + - name: loopStates + label: Loop states + label_singular: Loop state + description: The boucle label state machine. Each state has a label chip, + a one-line description, and a type that drives its colour treatment + (autonomous = cyan, human = gold, terminal = muted exit). + folder: src/content/loop-states + extension: md + format: frontmatter + create: true + slug: "{{slug}}" + fields: + - { name: order, label: Order, widget: number, value_type: int } + - { name: label, label: Label chip, widget: string } + - { name: description, label: Description, widget: text } + - name: type + label: Type + widget: select + options: + - { label: Autonomous, value: autonomous } + - { label: Human decides, value: human } + - { label: Terminal, value: terminal } + - { name: typeLabel, label: Type label (visible text), widget: string } + # ---------- Quick start (single file) ---------- - name: quickStart label: Quick start diff --git a/src/content.config.ts b/src/content.config.ts index e1d4405..450552d 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -106,6 +106,29 @@ const cards = defineCollection({ }), }); +// Loop states section header: title, subtitle, and aria-label. +const loopStatesHeader = defineCollection({ + loader: glob({ pattern: '**/*.md', base: './src/content/loop-states-header' }), + schema: z.object({ + title: z.string(), + sub: z.string(), + ariaLabel: z.string(), + }), +}); + +// The label state machine: one entry per state. `type` drives the visual +// treatment (autonomous = cyan, human = gold, terminal = muted exit). +const loopStates = defineCollection({ + loader: glob({ pattern: '**/*.md', base: './src/content/loop-states' }), + schema: z.object({ + order: z.number().int().positive(), + label: z.string(), + description: z.string(), + type: z.enum(['autonomous', 'human', 'terminal']), + typeLabel: z.string(), + }), +}); + // Quick start section: toggle, install/prompt code, transition, docs link. const quickStart = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/quick-start' }), @@ -157,6 +180,8 @@ export const collections = { steps, whyBoucle, cards, + loopStatesHeader, + loopStates, quickStart, footer, }; diff --git a/src/content/loop-states-header/loop-states-header.md b/src/content/loop-states-header/loop-states-header.md new file mode 100644 index 0000000..663d9be --- /dev/null +++ b/src/content/loop-states-header/loop-states-header.md @@ -0,0 +1,5 @@ +--- +title: The loop, state by state +sub: A label on your issue tells you exactly where the loop is — and when it's your turn to decide. +ariaLabel: The boucle label state machine — autonomous states in cyan, human-decision states in gold, terminal states marked as exits. +--- diff --git a/src/content/loop-states/state-1-triage.md b/src/content/loop-states/state-1-triage.md new file mode 100644 index 0000000..bbb1e4f --- /dev/null +++ b/src/content/loop-states/state-1-triage.md @@ -0,0 +1,7 @@ +--- +order: 1 +label: boucle:triage +description: boucle reads your issue and classifies it before starting any work. +type: autonomous +typeLabel: Autonomous +--- diff --git a/src/content/loop-states/state-2-spec-review.md b/src/content/loop-states/state-2-spec-review.md new file mode 100644 index 0000000..ef42c42 --- /dev/null +++ b/src/content/loop-states/state-2-spec-review.md @@ -0,0 +1,7 @@ +--- +order: 2 +label: boucle:spec-review +description: boucle drafts a spec and preview. You decide — approve or amend. +type: human +typeLabel: Human decides +--- diff --git a/src/content/loop-states/state-3-todo.md b/src/content/loop-states/state-3-todo.md new file mode 100644 index 0000000..0659e04 --- /dev/null +++ b/src/content/loop-states/state-3-todo.md @@ -0,0 +1,7 @@ +--- +order: 3 +label: boucle:todo +description: The approved spec is queued and ready for the worker to pick up. +type: autonomous +typeLabel: Autonomous +--- diff --git a/src/content/loop-states/state-4-working.md b/src/content/loop-states/state-4-working.md new file mode 100644 index 0000000..60f642a --- /dev/null +++ b/src/content/loop-states/state-4-working.md @@ -0,0 +1,7 @@ +--- +order: 4 +label: boucle:working +description: The worker implements the issue, committing as it goes. +type: autonomous +typeLabel: Autonomous +--- diff --git a/src/content/loop-states/state-5-review.md b/src/content/loop-states/state-5-review.md new file mode 100644 index 0000000..7d8030c --- /dev/null +++ b/src/content/loop-states/state-5-review.md @@ -0,0 +1,7 @@ +--- +order: 5 +label: boucle:review +description: The reviewer checks the change against the spec and your amendments. +type: autonomous +typeLabel: Autonomous +--- diff --git a/src/content/loop-states/state-6-approval.md b/src/content/loop-states/state-6-approval.md new file mode 100644 index 0000000..2793ce1 --- /dev/null +++ b/src/content/loop-states/state-6-approval.md @@ -0,0 +1,7 @@ +--- +order: 6 +label: boucle:approval +description: boucle opens a PR for you to review. You decide — approve or send it back. +type: human +typeLabel: Human decides +--- diff --git a/src/content/loop-states/state-7-merging.md b/src/content/loop-states/state-7-merging.md new file mode 100644 index 0000000..9605809 --- /dev/null +++ b/src/content/loop-states/state-7-merging.md @@ -0,0 +1,7 @@ +--- +order: 7 +label: boucle:merging +description: On approval, boucle rebases and merges your feature into main. +type: autonomous +typeLabel: Autonomous +--- diff --git a/src/content/loop-states/state-8-done.md b/src/content/loop-states/state-8-done.md new file mode 100644 index 0000000..3c432a9 --- /dev/null +++ b/src/content/loop-states/state-8-done.md @@ -0,0 +1,7 @@ +--- +order: 8 +label: boucle:done +description: Shipped. Your feature is live and the loop moves on. +type: terminal +typeLabel: Terminal +--- diff --git a/src/content/loop-states/state-9-human.md b/src/content/loop-states/state-9-human.md new file mode 100644 index 0000000..d88e101 --- /dev/null +++ b/src/content/loop-states/state-9-human.md @@ -0,0 +1,7 @@ +--- +order: 9 +label: boucle:human +description: An escalation hands the issue back to you. The loop stops here. +type: terminal +typeLabel: Escalation +--- diff --git a/src/pages/index.astro b/src/pages/index.astro index be8dfc9..cb92040 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -16,6 +16,8 @@ const steps = (await getCollection('steps')).sort((a, b) => a.data.order - b.dat const whyBoucle = (await getCollection('whyBoucle'))[0]; const cards = (await getCollection('cards')).sort((a, b) => a.data.order - b.data.order); const quickStart = (await getCollection('quickStart'))[0]; +const loopStatesHeader = (await getCollection('loopStatesHeader'))[0]; +const loopStates = (await getCollection('loopStates')).sort((a, b) => a.data.order - b.data.order); const footer = (await getCollection('footer'))[0]; const title = hero.data.metaTitle; @@ -985,6 +987,232 @@ const description = hero.data.metaDescription; color: var(--text-gold); } + /* ============ LOOP STATES ============ */ + /* The label state machine rendered as a progression. Autonomous states + read cyan, human-decision states gold, terminal states muted exits. + Only existing DESIGN.md §2 tokens are used — no new colours. No + animation or scroll-triggered reveal (non-goal). */ + .loop { + background: var(--surface-base); + } + .loop-legend { + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + margin: 0 0 2.5rem; + padding: 0; + list-style: none; + color: var(--text-secondary); + font-size: 0.85rem; + } + .loop-legend li { + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + .loop-legend .legend-swatch { + width: 14px; + height: 14px; + flex: none; + border-radius: var(--radius); + } + .legend-swatch--autonomous { + background: var(--accent-cyan); + } + .legend-swatch--human { + background: var(--accent); + } + .legend-swatch--terminal { + background: var(--text-secondary); + } + + /* Main sequence — a wrapping numbered flow so reading order stays + left-to-right, top-to-bottom. On wide screens the states form rows; + on narrow screens a connector runs down the left edge through the + number badges (see the <768px rail). */ + .loop-flow { + position: relative; + display: grid; + grid-template-columns: 1fr; + gap: 1.1rem; + margin: 0 auto; + max-width: 720px; + } + /* Mobile connector rail — runs down the left edge through the number + badges, showing the sequence is connected top-to-bottom. Centred on + the badge column (1.1rem padding + half the 2.25rem badge). */ + .loop-flow::before { + content: ''; + position: absolute; + left: 2.2rem; + top: 0.6rem; + bottom: 0.6rem; + width: 1px; + background: var(--border-neutral); + } + .loop-node { + position: relative; + display: flex; + gap: 1rem; + align-items: flex-start; + background: var(--surface-raised); + border: 1px solid var(--border-neutral); + border-radius: var(--radius); + padding: 1rem 1.1rem; + min-width: 0; + } + .loop-node__num { + flex: none; + width: 2.25rem; + height: 2.25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius); + background: var(--bg-secondary); + border: 1px solid var(--border-neutral); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.85rem; + font-weight: 700; + position: relative; + z-index: 1; + } + .loop-node__body { + min-width: 0; + } + .loop-node__chip { + display: inline-block; + font-family: var(--font-mono); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.02em; + padding: 0.2rem 0.7rem; + border-radius: var(--radius); + margin-bottom: 0.4rem; + } + .loop-node__type { + display: inline-block; + margin-left: 0.5rem; + font-family: var(--font-body); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-secondary); + } + .loop-node__desc { + margin: 0; + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.5; + } + + /* Autonomous — cyan chip on raised surface (DESIGN §2 cyan token). */ + .loop-node--autonomous .loop-node__chip { + color: var(--surface-base); + background: var(--accent-cyan); + } + .loop-node--autonomous .loop-node__num { + color: var(--accent-cyan); + border-color: var(--accent-cyan); + } + /* Human decides — gold chip (DESIGN §2 gold token). */ + .loop-node--human .loop-node__chip { + color: var(--surface-base); + background: var(--accent); + } + .loop-node--human .loop-node__num { + color: var(--accent); + border-color: var(--accent); + } + /* Terminal exits — muted, struck-through label so they read as exits + from the loop, not steps in it. */ + .loop-node--terminal { + opacity: 0.82; + border-style: dashed; + border-color: var(--border-neutral); + } + .loop-node--terminal .loop-node__chip { + color: var(--text-secondary); + background: var(--bg-secondary); + text-decoration: line-through; + text-decoration-color: var(--text-secondary); + } + .loop-node--terminal .loop-node__num { + color: var(--text-secondary); + } + + /* Escalation branch — the human escalation state (boucle:human) is a + terminal exit that branches off the main path rather than sitting in + sequence. It is pulled out of the numbered ordered list into its own + branch block with a visible join notch connecting it back to the main + flow. It keeps the terminal treatment (muted, struck-through label) + so it reads as an exit. */ + .loop-branch { + position: relative; + margin: 1.1rem 0 0; + padding-left: 2.4rem; + color: var(--text-secondary); + font-size: 0.8rem; + } + .loop-branch__join { + position: absolute; + left: 1.15rem; + top: 0; + width: 1.1rem; + height: 1px; + background: var(--border-neutral); + } + .loop-branch__join::before { + content: ''; + position: absolute; + left: 0; + top: -1.1rem; + width: 1px; + height: 1.6rem; + background: var(--border-neutral); + } + .loop-branch__tag { + margin: 0 0 0.5rem; + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-secondary); + } + .loop-branch .loop-node--escalation .loop-node__num { + color: var(--text-secondary); + } + + @media (min-width: 768px) { + .loop-flow::before { + display: none; + } + .loop-flow { + grid-template-columns: repeat(2, 1fr); + max-width: 820px; + } + .loop-branch__join { + display: none; + } + .loop-branch { + max-width: 820px; + text-align: center; + } + } + @media (min-width: 1024px) { + .loop-flow { + grid-template-columns: repeat(4, 1fr); + max-width: 1040px; + } + .loop-branch { + max-width: 1040px; + } + } + /* ============ QUICK START ============ */ .quick { background: var(--surface-base); @@ -1523,6 +1751,58 @@ const description = hero.data.metaDescription; + /* ============ LOOP STATES ============ */ + + + {loopStatesHeader.data.title} + + {loopStatesHeader.data.sub} + + + + Autonomous + You decide + Terminal + + + + { + loopStates + .filter((s) => s.data.order !== 9) + .map((s) => ( + + {s.data.order} + + + {s.data.label} + {s.data.typeLabel} + + {s.data.description} + + + )) + } + + + {/* Escalation branch — pulled out of the sequence so it clearly + branches off the main path rather than sitting in it. */} + s.data.order === 9).data.label}> + + Escalation branch — exits the loop + + {loopStates.find((s) => s.data.order === 9).data.order} + + + {loopStates.find((s) => s.data.order === 9).data.label} + {loopStates.find((s) => s.data.order === 9).data.typeLabel} + + {loopStates.find((s) => s.data.order === 9).data.description} + + + + + +
+ {loopStatesHeader.data.sub} +
{s.data.description}
Escalation branch — exits the loop
{loopStates.find((s) => s.data.order === 9).data.description}