Codename: Modulation Routing v1.1 Core. Bundles the Modulation v1.1 Core overnight wave (PRs #57-#65) with the Saber GIF Sprint 1 infrastructure (PR #67), the vertical Saber Card layout polish (PR #36), the v0.14.0 left-rail overhaul recap (PR #68 docs), the launch-prep SEO infrastructure (PR #69), the post-launch backlog index (PR #70), and the blade render rewrite + left-rail overhaul + Saber Wizard hardware step that landed across 2026-04-22 → 2026-04-27.
Hardware-validated on 89sabers Proffieboard V3.9 / macOS / Brave (Chromium WebUSB). Cross-OS + cross-board sweeps remain post-launch per docs/POST_LAUNCH_BACKLOG.md.
Saber GIF Sprint 1 — animated share-card export (2026-04-27)
First cut of animated saber GIF infrastructure per docs/SABER_GIF_ROADMAP.md. Sprint 1 ships two GIF variants from the My Crystal panel: Idle (1 s steady-state shimmer loop) and Ignition cycle (2.5 s PREON → IGNITING → ON-hold → RETRACTING → OFF arc). Output uses the v0.14 workbench renderer (capsule rasterizer + 3-mip bloom), not the simplified gradient that the static Saber Card uses, so the GIF visually matches the live editor preview. Open as PR #67.
Added
captureSequence engine helper at packages/engine/src/captureSequence.ts — pure-data multi-frame capture returning Uint8Array[] of LED buffers across N frames. Two modes for Sprint 1: 'idle-loop' (settle to ON, then capture a 1 s window of the steady-state shimmer) and 'ignition-cycle' (full PREON → IGNITING → ON-hold → RETRACTING → OFF, default 2.5 s). Built on the public ignite() / retract() / update() API — no private state poking — so the engine package stays headless. Sibling captureSequenceWithStates returns frames + per-frame BladeState for callers that want to surface state transitions.gif.js Promise wrapper at apps/web/lib/sharePack/gifEncoder.ts — wraps the callback-based gif.js encoder in an async API. Exports encodeGif(canvases, options) (array → Blob), encodeGifStreamed(options, callback) (caller pushes frames to reuse one canvas across the sequence), and setGifEncoderFactory() test seam. Default factory dynamic-imports gif.js so node tests don't try to load the browser-only module.- Workbench renderer headless port at
apps/web/lib/sharePack/bladeRenderHeadless.ts — 1:1 port of the v0.14 capsule rasterizer + 3-mip bloom chain that lives inline in BladeCanvas.tsx. Exports drawWorkbenchBlade() + getGlowProfile() + ledBufferFrom(). Same plateau / α-feather curve, same mip alphas / blur radii, same per-color glow tuning. Per-color getGlowProfile (red / blue / green / amber / amethyst / cyan / orange / pink / fallback). Leaves BladeCanvas.tsx untouched — workbench risk = zero. Paired-but-parallel until the deferred Phase 4 module-extraction collapses them. renderCardGif(options) orchestrator in apps/web/lib/sharePack/cardSnapshot.ts (additive — renderCardSnapshot for static PNGs is unchanged). Per-frame: clear canvas → repaint chrome (backdrop / header / hilt / metadata / qr / footer) → call drawWorkbenchBlade reading the engine LED buffer directly → gif.addFrame(canvas, copy: true). Reuses one <canvas> across the sequence for bounded memory. Test seams: __setCardFrameRendererForTesting + __setCreateQrSurfaceForTesting.- "Save share GIF" button + variant select on
CrystalPanel.tsx next to the existing "Save share card" button. Filename pattern: kyberstation-card-<variant>-<presetSlug>-<timestamp>.gif. Disabled state during the 1.5–4 s encode; status line reports final size in KB. gif.worker.js committed at apps/web/public/gif.worker.js (16 KB, copied from node_modules/gif.js/dist/) so the encoder's web worker resolves at /gif.worker.js under Next.js's static asset path.
Defaults
DEFAULT_GIF_OUTPUT_WIDTH = 640 (down from layout default 1200) — the workbench renderer produces far more colors per frame than the static gradient (bloom + plateau core + per-LED interpolation), so gif.js's 256-color palette quantization fights harder; the smaller default lands both variants under cap.- 18 fps for both variants. Tuning lands idle ≈1.7 MB / ignition ≈4.3 MB at default settings — under the brief's
< 2 MB idle / < 5 MB ignition targets. Callers can override width / fps / quality for hosted-use higher-fidelity renders.
Dependencies
- Adds
gif.js@^0.2.0 + @types/gif.js to apps/web/package.json.
Tests
- engine: 722 → 740 (+18 new
captureSequence.test.ts covering frame-count math, deterministic buffers across calls, idle-loop steady-state lit-ness, ignition-cycle state-machine arc with PREON enabled / skipped, and full validation surface). - web: 1,041 → 1,069 (+28 across
gifEncoder.test.ts (15) and renderCardGif.test.ts (13) — Promise contract, frame count = fps × duration_seconds, abort rejection, console-clean orchestration, QR texture disposal on encoder abort).
Hard constraints honoured
apps/web/lib/sharePack/card/* — untouched (PR #36 turf).apps/web/components/editor/BladeCanvas.tsx — untouched (workbench risk zero).apps/web/components/editor/routing/*, BoardPicker, useBoardProfile, useClickToRoute — untouched (modulation turf).packages/engine/src/modulation/* — untouched (locked types).- Engine package stays headless —
captureSequence is pure data, no DOM. - Worker-encoded GIFs —
gif.js spawns 2 web workers; main thread doesn't block during encode.
Open / pending
- Sprint 2 — per-variant ignition / retraction picker GIFs (19 + 13 thumbnails) + build script +
MiniGalleryPicker wiring. - Sprint 3 — Tier 2 marketing showcases (style grid, colour cycle, lockup loop).
- Sprint 4 — Tier 3 effect-specific + hilt-only + UI walkthrough GIFs.
- Phase 4 module extraction (separate, lower-risk PR): collapse
bladeRenderHeadless.ts ↔ BladeCanvas.tsx inline pipeline into one shared module under lib/blade/*; migrate MiniSaber, card/drawBlade.ts, etc. to call it. - The roadmap doc itself (
docs/SABER_GIF_ROADMAP.md) lives on the still-open PR #56 — Sprint 1 status flip should land there once it merges, or via a follow-up doc commit on main.
Modulation Routing v1.1 Core — overnight wave (2026-04-27)
Eight PRs landed overnight on top of v0.14.0, completing the v1.1 Core scope from docs/MODULATION_ROUTING_ROADMAP.md. Three parallel-agent phases (Phase 1: 4 worktree agents, Phase 2a: 3 worktree agents, Phase 2b: Wave 5 salvaged after agent stalled). All CI-green, all merged. No tag cut yet — pending hardware validation. Candidate v0.15.0.
Added
- All 11 modulators surface as plates (was 5 in v1.0). PR #57 unblocks
twist / battery / lockup / preon / ignition / retraction with bespoke CSS-keyframe live-viz glyphs. Plate grid lg:grid-cols-4 xl:grid-cols-6 for 11-plate readability. - Reciprocal hover highlight — hovering any parameter row in ParameterBank now lights up every modulator plate that drives it (PR #61). New
uiStore.hoveredParameterPath field. Multi-driver case lights multiple plates simultaneously. Replaces the 2026-04-20 1:1 placeholder mapping. - Per-binding expression editing — magenta
fx button on every expression-bound row in BindingList opens ExpressionEditor preloaded with the existing source for in-place iteration (PR #63). Bare-source rows leave the slot empty so column alignment stays consistent. - True drag-to-route — HTML5 drag-and-drop layered on click-to-route as the primary mouse/trackpad gesture, Vital / Bitwig style (PR #64). Plates are draggable; slider rows are drop targets with identity-color visual cue. New
useClickToRoute.dragBind(modulatorId, targetPath) action. MIME type application/x-kyberstation-modulator exported as a constant. Click-to-route preserved as keyboard / a11y fallback. - AST-level template injection in codegen (PR #60) —
composeBindings(ast, mappable) walks the style AST and grafts each mapped binding's astPatch into the slot identified by targetPath. v1.1 Core ships the shimmer-Mix slot pattern (Mix<Int<N>, X, Y>) used by every base-style. generateStyleCode rewired: mapBindings → applyModulationSnapshot baseline → buildAST → composeBindings → emitCode + v1.1 comment block distinguishing live / snapshotted / deferred / skipped bindings. Snapshot path retained for unmappable bindings as the always-flashable fallback. - 5 new starter recipes (PR #58):
heartbeat-pulse (abs(sin(time*0.002))), battery-saver (clamp(1-battery, 0, 0.5)), idle-hue-drift, sound-driven-hue, twist-driven-saturation. Recipe library 6 → 11. V1_0_RECIPES 8; V1_1_EXPRESSION_RECIPES 3. - 7 new user-guide pages (PR #59) —
recipes.md / combinators.md / modulators.md / expressions.md / canon-patterns.md / troubleshooting.md / sharing.md. ~5,400 new words, voice-matched to the existing quick-start. Honest-scope-tagged (v1.0 / v1.1+ / v1.2+). MODULATOR_DRAG_MIME_TYPE constant exported from useClickToRoute (PR #64).MODULATOR_DRAG_MIME_TYPE constant + dragBind action in useClickToRoute (PR #64).applyModulationSnapshot.ts — new onlyBindingIds filter + CommentBlockExtras shape with mappedBindings / deferredFromMapping for the v1.1 comment block (PR #60).
Changed
generateStyleCode flow rewired to a 6-step pipeline (PR #60): mapBindings → applyModulationSnapshot baseline → buildAST → composeBindings (overwrites mappable slots with live drivers) → emitCode → v1.1 comment block. Backward-compatible — configs without modulation payload short-circuit to byte-identical v0.14.0 output.- BindingList row grid picked up a 28-px fx-slot column (PR #63) — bare-source rows render an empty placeholder so the columns stay aligned across mixed lists.
- Modulator plate grid bumped from
lg:grid-cols-5 to lg:grid-cols-4 xl:grid-cols-6 to fit 11 plates readably (PR #57).
Tests
- codegen: 1,842 → 1,859 (+17 new in
composeBindings.test.ts, PR #62). - web: 1,025 → 1,041+ (+9 dragBind in
useClickToRoute.test.ts from PR #64; +7 in bindingListEditExpression.test.tsx from PR #63; reciprocal-hover regression coverage from PR #61). - presets: 29 → 47 (+18 in
recipes.test.ts from PR #58).
Backfill
- PR #62 — composeBindings test backfill. PR #60 (Wave 6) shipped the AST composer without test coverage due to a worktree-environment file-revert issue mid-agent. PR #62 adds 17 tests across 9 groups (pure-function invariants, single binding, breathing heuristic, multi-binding, deferred fall-through, purity / idempotency / structural sharing, result shape,
generateStyleCode integration, snapshot/live boundary).
Salvage
- PR #64 — Wave 5 (drag-to-route) post-stall recovery. Background agent stalled post-implementation but pre-commit. Parent session re-ran typecheck + tests against the worktree (clean, 1041 web tests passing), committed the agent's exact code, pushed, and opened the PR. Code is the agent's; commit message + PR shape are the parent session's.
Open / pending
- Wave 7 — Kyber Glyph v2 modulation round-trip (encoder body)
- Wave 8 — Button routing sub-tab + aux/gesture-as-modulator plates (L scope, separate session)
- Wave 10 — Hardware validation + V2.2 modulation flash + cut
v0.15.0 tag (hardware-gated) - Wave 6 follow-on — composer slot expansion to per-channel RGB + timing scalars (v1.2 candidates per PR #60 body)
- Manual visual verification of all 8 PRs in the live editor
Blade render rewrite — capsule rasterizer + additive composite (2026-04-27)
Major rework of the blade preview pipeline, landed on feat/blade-layers-debug. Collapses the prior body + separate tip cap + parallel offscreen mirror into a single per-pixel capsule rasterizer that's the source of truth for blade geometry. Pipeline goes from 14 passes → 13, ~140 lines of body/cap matching code removed.
- Capsule rasterizer —
rasterizeCapsuleToOffscreen walks the capsule's bounding box per-pixel, samples LED color via axial position with linear interpolation between adjacent LEDs, applies a feathered Gaussian-α radial profile, and lerps toward a luma-driven white-shifted core. Capsule is fixed at the full configured blade length — retraction is rendered via per-LED brightness (engine-driven), not geometry truncation, so the tube doesn't physically shrink during retract. Replaces 4 parallel render paths (offscreen body + offscreen cap + visible body + visible cap) with one. - Sharp + soft offscreen split — sharp offscreen feeds the visible body composite; soft offscreen (sharp + diffusion blur) feeds the bloom mips. Decouples "crisp body silhouette" from "diffuse halo source" so cranking diffusion doesn't shorten or soften the visible blade.
- Pass 12 additive blend — body capsule now ADDS to the bloom underneath via
lighter blend instead of occluding it via normal alpha. Eliminates the visible "body sitting on top of halo" seam that the prior normal-blend produced at the body's α-decay zone. Physically correct compositing for emissive surfaces — light adds, doesn't replace. - Feathered Gaussian α profile — rasterizer's α curve is now a smooth bell shape (1.0 at center, 0 at rim) with no plateau. Combined with the additive composite, the body and surrounding bloom are continuous expressions of the same emission. No detectable boundary between body silhouette and halo.
- Tip axial extension — geometric capsule rim sits 0.15 × radius past the LED endpoint so the feathered α can decay to 0 at the rim while visible-bright pixels reach EXACTLY the configured blade length (verified to the pixel for a 40" blade — body terminates at the 40" grid mark, bloom continues past as natural surrounding halo).
- Body height rebalanced (
BLADE_CORE_H 26 → 32) — paired with the feathered α curve to give a clearly-defined bright tube proportional to the BLADE itself (~1" real neopixel diameter), not the hilt graphic. Hilt is treated as a visual placeholder; blade width stays consistent regardless of hilt sizing. - Hilt-tuck + clipped body — capsule extends
coreH left of the hilt edge so its rounded LEFT cap sits behind the hilt (invisible to the user, but bloom mips still see it and produce halo into the hilt area). Visible body composite is clipped to x ≥ bladeStartPx so the body itself doesn't paint over the hilt's metallic surface. Hilt drawn before bloom so additive bloom mips spill onto the metal naturally — preserves the "lit blade illuminating the metal" look. - Per-LED axial linear interpolation — eliminates hard vertical seams between bright and dim adjacent LEDs (prior hard quantization showed ~17 luma steps every 6 px at a Stripes-style transition; max per-pixel delta now ~3 luma). Implements axial polycarbonate diffusion: light from each LED bleeds into its neighbors along the tube length.
- +25% white-core exposure boost —
ledCoreWhiteAmount multiplied 1.25× (clamped to 1.0). White plateau now reaches pure white (luma 250+) instead of stopping at the per-color asymptote (0.82–0.95). Iconic blown-out tube look for any LED color, not just white blades. - Drops
tests/blade/endpointSeeds.test.ts — sentinel for the v0.14.0 Phase 1 endpoint-seed widening (glowCapRadius, emGlowR), constants the capsule unified out of existence.
Performance: 120 FPS at 1600×1000 viewport / DPR 2. Per-pixel rasterization adds modest CPU cost but stays well within budget.
Status: shipped on feat/blade-layers-debug, NOT yet merged to main. Branch is in a clean checkpoint state; further tweaks to white-shift/bloom layering deferred.
Left-rail overhaul (2026-04-25)
Replaced the multi-tab + multi-column workbench with a unified Sidebar + MainContent shell driven by a single uiStore.activeSection slot. Page-tabs nav, the DesignPanel pill bar, the macro-knob PerformanceBar, and the swipe-driven tablet tab UI all retired together. Seven PRs (#47-#53) shipped in one day via parallel-agent dispatch.
Final desktop shape. The header keeps its utility chrome (logo, Share, FPS, Sound, Docs, ⌘K, Wizard, Settings) but loses the Gallery/Design/Audio/Output tabs — the sidebar absorbs them. Inspector stays left of the canvas as the always-visible "Quick Controls" surface; RightRail (STATE + ANALYSIS) stays right; new Sidebar (~280px) + MainContent split fills the panel area below the canvas. Tablet (600–1023px) uses the same shell at 240px sidebar width. Mobile is intentionally unchanged in this sprint — a small-screen drawer/bottom-sheet UX pass is owned for a follow-up.
Sidebar groups (collapsible, localStorage-persisted): GALLERY → /gallery · APPEARANCE (Blade Style · Color) · BEHAVIOR (Ignition & Retraction · Combat Effects · Gesture Controls) · ADVANCED (Layer Compositor · Motion Simulation · Hardware · My Crystal) · ROUTING BETA (board-gated) · AUDIO · OUTPUT.
Quick Controls (Inspector left rail). GALLERY tab retired. Single-surface stack: Surprise Me + Undo · 8 canonical color chips (Blue / Red / Green / Yellow / Purple / Orange / White / Cyan) + Custom (jumps to deep Color section) · compact ignition + retraction MGP pickers with inline ms field · the existing 7 ParameterBank sliders. Every fast-access section is a thin view over the same store as the deep sidebar section, so changes propagate both directions.
Three panel merges (#47): - Colors + Gradient Builder → unified ColorPanel (channel selector at top; gradient region only renders for the Base channel) - BladeHardwarePanel + PowerDrawPanel → HardwarePanel (config inputs on top, live power readout below the divider with <StatusSignal> headroom indicator) - ModulatorPlateBar + BindingList → RoutingPanel (plate bar + binding count divider + active bindings list)
SettingsModal reorganized to 3 tabs (#47): Appearance (Aurebesh Mode · Display · Row density) · Behavior (UI Sounds · Effect auto-release · Keyboard Shortcuts · Feedback) · Advanced (Performance Tier · Layout). The "Performance Bar" toggle was deleted alongside the bar itself.
Other shipped pieces. Motion Simulation restored under sidebar Advanced (#49) after PR 1 dropped its mount point. ⌘1–⌘4 digit nav rewired from setActiveTab → setActiveSection; ⌘5 still toggles the All States takeover. KeyboardShortcutsModal now surfaces ⌘1–⌘5, ⌘K, ⌘Z, ⌘⇧Z as first-class Editor rows (#49). The 19-ignition + 13-retraction style tables that were copy-pasted across three sites moved to a shared lib/transitionCatalogs.ts (#50). The previously-inert Custom color chip now jumps to the deep Color sidebar section (#51). Tablet shell migrated to Sidebar + MainContent (#52). PerformanceBar.tsx + MacroKnob.tsx + QuickMacroPreview.tsx finally deleted (#53) — the surviving shiftLedColor helper moved to a tiny lib/shiftLight.ts for ShiftLightRail's exclusive use.
Test count: 1030 / 1030 passing across 58 files (was 1044 across 59 pre-overhaul; net change reflects the deletion of the MacroKnob test suite + the addition of QuickColorChips + QuickTransitionPicker + Inspector regression tests). Typecheck clean across all 10 workspace packages. Verified end-to-end at desktop (1600×1000) and tablet (900×1024) viewports.
Deferred (post-overhaul follow-ups, not blocking a v0.15 tag): mobile shell migration (needs UX call on drawer vs bottom-sheet), inline custom-color popover, and compactThumbnail field on transition catalog entries to author crisp 24×24 MGP triggers instead of scaled-down 100×60 SVGs.
Saber Wizard — hardware step (2026-04-22)
Added a new first step to the Saber Wizard so newcomers tell the app about the saber they actually own (blade length + board) before picking aesthetic. The 3-step archetype/colour/vibe flow shifts to steps 2/3/4 and is otherwise unchanged.
- Blade length picker — 6 tiles (20"/24"/28"/32"/36"/40"). LED counts mirror
BLADE_LENGTH_PRESETS in the engine package; selection writes BladeConfig.ledCount so every per-LED surface in the editor (BladeCanvas + PixelStripPanel + RGBGraphPanel + state-grid takeover) renders the chosen length 1:1. - Board picker — 5 tiles (Proffie V3 / V2 / CFX / GH V4 / GH V3) with 3-tier compatibility chips built on the existing
<StatusSignal> primitive (paired colored glyph + label, colorblind-safe): - VERIFIED (green ✓) — Proffie V3, the only board hardware-validated end-to-end (per the 2026-04-20 Phase A/B/C entry above).
- UNTESTED (amber ▲) — Proffie V2. Code path identical to V3, hardware testing pending. Community hardware reports welcome.
- REFERENCE (red ✕) — CFX / GH V4 / GH V3. Different firmware ecosystems entirely; the editor + visualizer work but the generated config.h won't run on these boards.
- A mini legend strip next to the "Board" heading shows what each color means.
- Selection writes
boardType to the active SaberProfile (or auto-creates a "My Saber" profile if none exists). - "Skip for now" advances to the archetype step without writing anything to the blade config or profile — for users who want to dive straight into design and configure hardware later via the Profile + Code panels.
- Initial focus lands on the currently-selected length tile (matches selection rather than always-first), so keyboard users start where the visual selection is.
10 new contract tests in apps/web/tests/saberWizardOptions.test.ts guard the LED-count ↔ inferBladeInches mapping, the V3-only-verified tier assignment (will fail loudly when V2 gets hardware-validated, prompting a tier bump), and the storeValue strings that CodeOutput.tsx maps back to proffieboard_v{2,3}. 637 web tests pass.
WebUSB flash — hardware validation (2026-04-20)
Phases A + B + C all green on Proffieboard V3.9 (89sabers) + macOS 15 + Brave. Connect → dry-run → real flash → post-write verify → recovery re-flash — full clean pass. Blade ignites blue on the first power press after replug; USB serial enumerates as /dev/tty.usbmodem*; audio DAC active (ProffieOS voice pack announces "SD card not found" / "font not found").
Three real DFU protocol bugs fixed that 576 passing mock tests had missed. Real STM32 DfuSe bootloader correctly returned STALL where the mock was too permissive:
DfuSeFlasher.verifyFlash: setAddressPointer leaves the device in dfuDNLOAD_IDLE, but UPLOAD requires dfuIDLE. Added abort() between the two.DfuSeFlasher.flash (manifest step): after UPLOAD verify the device sits in dfuUPLOAD_IDLE, but the manifest's zero-length DNLOAD requires dfuIDLE. Added abort() before the manifest download.DfuSeFlasher.waitForManifestComplete: STM32 resets the USB bus as part of manifest (bitManifestationTolerant=0); the resulting controlTransferIn failure surfaces as a raw DOMException, not our DfuError. The old catch only swallowed DfuError, so successful flashes showed a red error banner. Now any error during the post-manifest poll is treated as success.
Plus two supporting fixes uncovered while building firmware to validate against:
firmware-configs/v3-standard.h: legacy InOutTrL<TrWipe<300>, TrWipeIn<500>, Blue> no longer compiles against current ProffieOS master — bare Blue returns RGBA_nod which doesn't convert to OverDriveColor. Replaced with the modern StyleNormalPtr<Blue, WHITE, 300, 500> factory (same visual result)..github/workflows/firmware-build.yml: Linux runners are case-sensitive, so checking out ProffieOS into proffieos/ broke the arduino-cli compile <sketch-dir> contract (ProffieOS ships ProffieOS.ino). Renamed the checkout path to ProffieOS/.
Validated hardware scope: Proffieboard V3.9 on macOS + Chromium. Brave is Chromium-based, and Chrome/Edge/Arc share Chromium's WebUSB implementation so they should behave identically. Windows, Linux, Proffieboard V2, and V3+OLED are untested; community hardware reports welcome via the hardware_report issue template.
Followups:
- ~~Tighten
MockUsbDevice to enforce the three DFU state-machine rules~~ — done (same session). strictState + resetAfterManifest options added; three regression tests added (one per bug), each verified to fail if the corresponding fix is reverted. 579 tests pass. - Cross-OS sweep: Windows + Linux hardware smoke-tests before promoting the feature to "validated on all supported configurations".
- Cross-board sweep: Proffieboard V2.2 and V3+OLED hardware smoke-tests.
Full details in docs/HARDWARE_VALIDATION_TODO.md § Phase C.
- v0.11.1 — Design Review Polish Pass (shipped): alert-color discipline, skeleton + error-state coverage, color-glyph pairing for accessibility, CHANGELOG + README assets, housekeeping - v0.11.2 — Color Naming Math (shipped): three-tier algorithmic naming (landmark + modifier + coordinate-mood) expanding ~120 curated names into 1,500+ HSL coverage - v0.11.3 — Modular Hilt Library (shipped): 33 reusable line-art SVG parts composed into 8 canonical hilt assemblies (Graflex, MPP, Negotiator, Count, Shoto Sage, Vented Crossguard, Staff, Fulcrum), authored across 3 parallel artist-agents on top of a strict-typed composer + HiltRenderer with horizontal / vertical orientation. 8 new SVG hilt options added to the editor's Hilt picker (marked with ✦) - v0.12.0 — Kyber Crystal Three.js renderer (shipped): full 3D crystal component with PBR materials, 5 procedural Forms, bleed + heal + first-discovery animations, scannable QR embedded, card snapshot pipeline - v0.15.0 — Preset Cartography (planned): multi-agent preset expansion across deep-cut lanes (Prequel/OT/Sequel, Legends/KOTOR, Clone Wars, Mando/Ahsoka, cross-franchise) - v0.16.0 — Multi-Blade Workbench (planned): channel-strip UI for editing dual-blade / saberstaff / crossguard sabers (glyph format already supports multi-blade from v1)
(v0.13.0 — Launch Readiness — shipped via PR #31; v0.14.0 slot open for reassignment after deprecating the former "Kyber Forge" ultra-wide layout concept, which is redundant now that OV11's drag-to-resize handles cover the ultra-wide use case.)
Branch protection — server-side active
After the KyberStation owner upgraded to GitHub Pro (2026-04-17 afternoon), pnpm run branch-protection:setup applied the main-protection ruleset (id 15217927) on refs/heads/main:
- non_fast_forward blocks force-push to main - deletion blocks main-branch deletion - pull_request (0 approvals required) blocks direct pushes — all changes must go through a PR - required_status_checks: build-and-test requires CI green before merge
Client-side .githooks/pre-push remains active as defense-in-depth.
Deferred items (documented, awaiting dedicated pickup)
- Hardware validation of WebUSB flash against real Proffieboard V2.2 and V3.9 — see docs/HARDWARE_VALIDATION_TODO.md - Real ESLint enforcement across packages (stub lint scripts currently) - CANONICAL_DEFAULT_CONFIG drift-sentinel test pattern - Shared <HiltMesh> extraction between BladeCanvas3D.tsx and CrystalRevealScene.tsx - Crystal Vault panel (scanned-crystal collection) - Re-attunement UI for visual-version upgrades - Favicon replacement from crystal snapshot pipeline - SHARE_PACK.md §4 size-estimate table refresh (current doc understates max glyph size; real measurements from PR #20 hit ~490 chars at max)
See ~/.claude/plans/declarative-strolling-dragonfly.md for the orchestration plan that scopes the current sprints, and docs/SESSION_2026-04-17.md Part 2 for the full session summary.
Modulation polish + a11y clean (2026-04-23 late — untagged; candidate v0.14.1 or v0.15.0 once hardware-validated)
Two PRs shipped on top of v0.14.0 (PR #41 + PR #42). Items ordered by PR.
PR #41 — modulation follow-up (merge commit bd9bb7b):
- Generated code now reflects modulation. generateStyleCode threads a new applyModulationSnapshot helper that walks config.modulation.bindings, snapshots each to its current value via computeSnapshotValue, bakes the results into the config at each binding's target path (shallow-clone-on-write, no mutation), and prepends a BETA-labeled comment block listing every applied + skipped binding. Opt out via { comments: false } on the preset-array code path to avoid one-comment-per-preset clutter in the full config.h. Expression bindings currently snapshot; v1.1 Core adds AST-level template injection for Scale<SwingSpeed<>, ...> + friends. - Hover wire-highlighting + bound-param stripe. Three priority- stacked visual states on every slider label in ParameterBank: ARMED (click-to-wire) > HOVERED (this plate drives this param) > BOUND (some binding targets this param — persistent left-edge identity-color stripe). Identity colors propagate via BUILT_IN_MODULATORS descriptors. - Inline BoardPicker chip in StatusBar between Profile and Conn. BOARD · PROFFIE V3.9 · FULL; click opens the modal picker. Reactive across capability-sensitive UI. - ExpressionEditor — v1.1 Core math-formula UI. fx button on every SliderControl opens a 380-px popover with auto-focused textarea, live peggy parse status (✓ Valid / ✕ with error location + message), 5 starter-idiom chips (Breathing / Heartbeat / Battery dim / Swing doubled / Loud OR fast), ⌘+Enter shortcut, Escape/outside-click dismiss. Apply creates a binding with source: null, expression: { source, ast }, combinator: 'replace', amount: 1.0. BindingList distinguishes expression bindings from bare-source with an fx label in status-magenta + full-source hover tooltip. - Color-contrast fix across 9 canvas themes + the root default. --text-muted bumped +40 each channel (106 110 120 → 146 150 160 for Deep Space, equivalent deltas for the other 8 themes). Fixes 82 axe-core color-contrast violations concentrated on muted-text surfaces in the modulation UI.
PR #42 — a11y clean + first expression recipe (merge commit c0a92c4):
- Zero axe-core WCAG 2 AA violations at desktop (1600×1000, 30 passes) AND mobile (375×812) viewports on /editor. Closes the P29 launch blocker carried from v0.13.0 readiness. - MobileTabBar: dropped role="tablist" / role="tab" — semantically this is route navigation, not a tab interface; aria-current="page" handles the active state. - AppShell mobile tablist: scoped role="tablist" to an inner wrapper so the collapse toggle + dot indicators become siblings of the tablist, not children (fixes aria-required-children). - AppShell tab aria-controls: replaced per-tab panel IDs with a single stable id="mobile-panel"; dropped when showPanel is false and paired with aria-expanded. - MiniGalleryPicker: role="listbox" → role="group" (children use role="button", not role="option"). - Cleared the final 5 contrast issues: DesignPanel BETA chip opacity-70, ColorPanel preset subtitle text-accent/70, PerformanceBar page tabs + SaberProfileManager source badge rgb(var(--text-muted) / 0.65). - Breathing Blade — first expression-based starter recipe. sin(time * 0.001) * 0.5 + 0.5 → shimmer · replace · 100%. AST hand-built inline (can't import parseExpression across the .npmrc hoisted boundary per CLAUDE.md decision #1). Test split: V1_0_RECIPES (5) vs V1_1_EXPRESSION_RECIPES (1). Presets test count 29 → 40. The ProffieOS emitter's existing matchSinBreathingEnvelope heuristic recognizes this exact shape so the flashed blade will breathe live on hardware via Sin<Int<period>>.
Tests delta since v0.14.0: codegen +15, presets +11, web unchanged (new UI work covered by visual QA + axe audit, which itself ran clean on both viewports).
PR #43 — docs catch-up (merge commit b98af51): CLAUDE.md + CHANGELOG updates for PR #41 + PR #42. No code touched.
PR #44 — quick-start illustrations (merge commit bfcdbf6): Three hand-authored animated SVGs replace the [gif-placeholder] markers in docs/user-guide/modulation/your-first-wire.md:
- first-wire-step-1.svg (7.0 KB) — five-plate bar with per-plate live viz (swing needle, angle tilt, sound VU, time sweep, clash flash). - first-wire-step-2.svg (3.4 KB) — SWING plate armed, siblings dimmed, "◎ swing armed" banner below. - first-wire-step-3.svg (7.1 KB) — two-column frame with an animated dashed wire connecting plate → shimmer slider, plus the binding row "SWING → Shimmer · add · 60%".
~17.5 KB combined. Native SVG animation — no JS. Each has <title> + <desc> for screen readers. Matches editor color tokens exactly. An inline HTML comment at the bottom of the markdown documents the replacement pass for future screen-recorded GIFs.