Skip to content

Commit c3e45d2

Browse files
Merge pull request #26 from New1Direction/feat/canvas-engine
feat: Interactive Canvas — Blueprint, Guided Tour, Corkboard & Stack Studio
2 parents 13b4630 + 4e1b631 commit c3e45d2

52 files changed

Lines changed: 6175 additions & 61 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ jobs:
99
test:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v4
13-
- uses: actions/setup-node@v4
12+
- uses: actions/checkout@v5
13+
- uses: actions/setup-node@v5
1414
with:
1515
node-version: '20'
1616
cache: 'npm'

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ website
44
package-lock.json
55
*.min.js
66
icons
7+
vendor

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ This project follows [Semantic Versioning](https://semver.org/) and groups chang
77
by theme. Dates are when the release landed on `main` — 1.1.0 through 1.6.0 shipped
88
the same day, as a rapid burst of improvements, so they share a date.
99

10+
## [3.1.0] — 2026-06-16 · _Interactive Canvas (Blueprint · Guided Tour · Corkboard · Stack Studio)_
11+
12+
### Added
13+
14+
- **Canvas tab (Blueprint).** A new **Canvas** tab in the Lenses group turns any repo's Deep Dive into an interactive, zoomable, pannable map of the repo's atoms — modules, subsystems, and their lineage — colour-coded by kind with a live node legend. Drag nodes to rearrange; positions persist across sessions.
15+
- **Guided Tour.** Spotlights the architecture node-by-node in dependency order with plain-English narration drawn from the Deep Dive. Navigate with **Back / Next**, auto-play, or keyboard **← → Esc**; fully reduced-motion safe.
16+
- **Export to `.excalidraw` and SVG.** Download the canvas as an `.excalidraw` file (opens hand-drawn in excalidraw.com, Obsidian, or VS Code) or as a clean SVG for docs and slides.
17+
- **Persistent arrangements.** Node positions and canvas state are stored in a new `scenes` IndexedDB store and round-trip through the library backup/export envelope, so layouts travel with your library.
18+
- **Corkboard (Library-wide canvas).** A toggle in the Library page switches your whole collection into a red-string board: every scanned repo is a draggable manila card, and related repos are joined by colored string keyed to relationship type (alternatives, synergies, head-to-heads, combined ideas) and shaded by fit score. Filter by Collection to focus a board, and the arrangement is saved so it's exactly where you left it next session. Reuses the same canvas engine as Blueprint — zero new dependencies, theme-aware, reduced-motion safe.
19+
- **Stack Studio (canvas view of a tech-stack).** The Tech-Stack Builder result gains a **View on canvas** toggle: the repos you wired together render as layer-coloured cards in adoption order, joined by their integrations, with any gaps shown as dashed cards — the same engine, turning "how these fit together" into a living diagram.
20+
- **Zero-build, zero dependencies.** Plain ES modules only — no bundler, no new npm packages. Theme-aware across all 13 themes and reduced-motion safe throughout.
21+
1022
## [3.0.1] — 2026-06-15 · _Audit hardening_
1123

1224
A focused correctness, security, and tooling pass from a full code audit — no

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ A scan opens to a **verdict landing** and fans out into focused tabs:
3737
| 🔍 | **Discover** | Search GitHub from inside the extension, or get **recommendations** from the repos you've already adopted. |
3838
| 🕸️ | **Connections** | A walkable map centred on the current repo, showing how it relates to the others you've scanned. |
3939
| 🤝 | **Synergies** · **Versus** · **Combinator** | Complements, head-to-heads, and fused project ideas — grounded in *your* library. |
40+
| 🗺️ | **Canvas** | Turn a repo's Deep Dive into an interactive, draggable **Blueprint** — pan/zoom the architecture map, take a narrated **Guided Tour** in dependency order (keyboard-navigable, reduced-motion safe), and export to **.excalidraw** or SVG. Switch the Library into a **Corkboard** to map your whole collection at once: every scanned repo a draggable card, related repos joined by colored string (alternatives, synergies, head-to-heads, combined ideas), colored by fit, filterable by Collection, arrangement saved. And the Tech-Stack Builder renders its wiring on the same canvas as a **Stack Studio**. |
4041

4142
Plus **SKTPG** (a one-tap State / Known-pitfalls / Trajectory / Proof / Growth read), framework lenses, and capability re-tagging.
4243

backup.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const BACKUP_VERSION = 2;
1313
// Upper bounds on how much a single import may write, so a hostile or corrupt
1414
// file can't pin the IndexedDB write lock or blow the storage quota. Anything
1515
// past these is dropped with a surfaced warning (never silently).
16-
export const MAX_ROWS = { repos: 5000, nodes: 20000, edges: 50000, cache: 5000, collections: 2000, decisions: 5000, snapshots: 5000 };
16+
export const MAX_ROWS = { repos: 5000, nodes: 20000, edges: 50000, cache: 5000, collections: 2000, decisions: 5000, snapshots: 5000, scenes: 2000 };
1717

1818
// Per-repo snapshot ring-buffer cap (mirrors SNAPSHOT_CAP in snapshots.js); each
1919
// imported snapshots row is trimmed to its most recent SNAP_CAP entries.
@@ -27,10 +27,11 @@ const cacheOk = (c) => !!(c && c.repoId && c.platform);
2727
const collectionOk = (c) => !!(c && c.id != null && c.payload && typeof c.payload.name === 'string');
2828
const decisionOk = (d) => !!(d && d.id != null && d.payload && d.payload.repoId && d.payload.decision);
2929
const snapshotOk = (r) => !!(r && r.id != null && r.repoId && Array.isArray(r.snaps));
30+
const sceneOk = (s) => !!(s && s.id && s.scope && Array.isArray(s.nodes) && Array.isArray(s.edges));
3031

3132
/** Empty normalized shape — the safe fallback when a file can't be parsed. */
3233
function emptyValue() {
33-
return { repos: [], nodes: [], edges: [], cache: [], collections: [], decisions: [], snapshots: [] };
34+
return { repos: [], nodes: [], edges: [], cache: [], collections: [], decisions: [], snapshots: [], scenes: [] };
3435
}
3536

3637
/**
@@ -39,14 +40,14 @@ function emptyValue() {
3940
* @param {{ repos?: object[], nodes?: object[], edges?: object[], cache?: object[], exportedAt?: string }} [parts]
4041
* @returns {object}
4142
*/
42-
export function buildBackup({ repos, nodes, edges, cache, collections, decisions, snapshots, exportedAt } = {}) {
43-
const r = arr(repos), n = arr(nodes), e = arr(edges), c = arr(cache), col = arr(collections), dec = arr(decisions), snap = arr(snapshots);
43+
export function buildBackup({ repos, nodes, edges, cache, collections, decisions, snapshots, scenes, exportedAt } = {}) {
44+
const r = arr(repos), n = arr(nodes), e = arr(edges), c = arr(cache), col = arr(collections), dec = arr(decisions), snap = arr(snapshots), sc = arr(scenes);
4445
return {
4546
format: BACKUP_FORMAT,
4647
version: BACKUP_VERSION,
4748
exportedAt: exportedAt || new Date().toISOString(),
48-
counts: { repos: r.length, nodes: n.length, edges: e.length, cache: c.length, collections: col.length, decisions: dec.length, snapshots: snap.length },
49-
repos: r, nodes: n, edges: e, cache: c, collections: col, decisions: dec, snapshots: snap,
49+
counts: { repos: r.length, nodes: n.length, edges: e.length, cache: c.length, collections: col.length, decisions: dec.length, snapshots: snap.length, scenes: sc.length },
50+
repos: r, nodes: n, edges: e, cache: c, collections: col, decisions: dec, snapshots: snap, scenes: sc,
5051
};
5152
}
5253

@@ -88,6 +89,7 @@ export function validateBackup(obj) {
8889
collections: clamp('collections', arr(obj.collections).filter(collectionOk)),
8990
decisions: clamp('decisions', arr(obj.decisions).filter(decisionOk)),
9091
snapshots: clamp('snapshots', arr(obj.snapshots).filter(snapshotOk).map((r) => ({ ...r, snaps: arr(r.snaps).slice(-SNAP_CAP) }))),
92+
scenes: clamp('scenes', arr(obj.scenes).filter(sceneOk)),
9193
};
9294
return { ok: errors.length === 0, errors, warnings, value };
9395
}
@@ -100,7 +102,7 @@ export function validateBackup(obj) {
100102
*/
101103
export function summarizeBackup(obj) {
102104
const { value } = validateBackup(obj);
103-
return { repos: value.repos.length, nodes: value.nodes.length, edges: value.edges.length, cache: value.cache.length, collections: value.collections.length, decisions: value.decisions.length, snapshots: value.snapshots.length };
105+
return { repos: value.repos.length, nodes: value.nodes.length, edges: value.edges.length, cache: value.cache.length, collections: value.collections.length, decisions: value.decisions.length, snapshots: value.snapshots.length, scenes: value.scenes.length };
104106
}
105107

106108
/**

blueprint-adapter.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// blueprint-adapter.js
2+
// Deep Dive atoms/lineage → a laid-out Blueprint scene.
3+
4+
import { createScene } from './scene.js';
5+
import { repairGraph } from './repair-graph.js';
6+
import { layoutBlueprint } from './canvas-layout.js';
7+
8+
/**
9+
* @param {object} args
10+
* @param {{atoms:any[], lineage:{links:any[], roots?:string[], leaves?:string[]}}} args.deepDive
11+
* @param {string} args.repoId
12+
* @param {string} args.title
13+
* @param {string|null} [args.scanAt]
14+
* @param {(atom:object)=>string} [args.layerOf] defaults to atom.kind
15+
* @param {boolean} [args.withIssues] when true, returns { scene, issues }
16+
* @returns {object|{scene:object, issues:object[]}}
17+
*/
18+
export function buildBlueprintScene({ deepDive, repoId, title, scanAt = null, layerOf = (a) => a.kind, withIssues = false }) {
19+
const atoms = (deepDive && deepDive.atoms) || [];
20+
const links = (deepDive && deepDive.lineage && deepDive.lineage.links) || [];
21+
const roots = new Set((deepDive && deepDive.lineage && deepDive.lineage.roots) || []);
22+
23+
const layerByAtomId = Object.fromEntries(atoms.map((a) => [a.id, layerOf(a) ?? null]));
24+
const { nodes, edges, issues } = repairGraph({
25+
nodes: atoms.map((a) => ({ ...a, layer: layerByAtomId[a.id] })),
26+
edges: links,
27+
});
28+
29+
// mark lineage roots (load-bearing) so the engine can highlight them (immutably)
30+
const marked = nodes.map((n) => ({ ...n, ref: { ...(n.ref || {}), root: roots.has(n.id) } }));
31+
32+
const placed = layoutBlueprint(marked, edges);
33+
34+
const scene = createScene({ scope: 'blueprint', repoId, title });
35+
scene.nodes = placed;
36+
scene.edges = edges;
37+
scene.source.scanAt = scanAt;
38+
39+
return withIssues ? { scene, issues } : scene;
40+
}

canvas-demo.html

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Canvas demo — RepoLens Blueprint</title>
6+
<link rel="stylesheet" href="./themes.css" />
7+
<style>
8+
/* Case File fallbacks so the demo renders standalone (themes.css canvas rules use these tokens). */
9+
:root { --bg:#fbf6ea; --surface:#fffdf6; --text:#211c14; --text-sub:#5a4f3e; --accent:#c2691c; --border:#b9a273; --dur:.2s; --ease-out:cubic-bezier(.16,1,.3,1); }
10+
body { margin:0; font-family:ui-monospace,monospace; background:#efe4c9; color:#211c14; padding:24px; }
11+
h1 { font-size:18px; margin:0 0 4px; }
12+
.sub { font-size:12px; color:#6b5a36; margin:0 0 16px; }
13+
.frame { max-width:900px; border:1px solid #b9a273; border-radius:14px; overflow:hidden; background:#fff; box-shadow:0 10px 30px rgba(33,28,20,.14); }
14+
.bar { display:flex; gap:8px; align-items:center; padding:9px 12px; background:#f6eed7; border-bottom:1px solid #b9a273; font-size:12px; }
15+
.bar button { padding:5px 11px; border-radius:7px; font-size:12px; cursor:pointer; border:1px solid #211c14; background:#fffdf6; }
16+
.host { position:relative; height:520px; }
17+
</style>
18+
</head>
19+
<body>
20+
<h1>🔭 RepoLens — Blueprint canvas (live demo)</h1>
21+
<p class="sub">Real pipeline: <code>buildBlueprintScene</code><code>layoutBlueprint</code><code>mountCanvas</code><code>buildTour</code>/<code>startTour</code>. No extension, no API.</p>
22+
<div class="frame">
23+
<div class="bar">
24+
<strong>output-tab · evanw/esbuild</strong>
25+
<span style="flex:1"></span>
26+
<button id="tour">▶ Guided Tour</button>
27+
</div>
28+
<div class="host" id="host"></div>
29+
</div>
30+
31+
<script type="module">
32+
import { buildBlueprintScene } from './blueprint-adapter.js';
33+
import { mountCanvas } from './canvas-engine.js';
34+
import { buildTour } from './tour.js';
35+
import { startTour } from './tour-runner.js';
36+
37+
const deepDive = {
38+
atoms: [
39+
{ id: 'cli', name: 'CLI', kind: 'entrypoint', purpose: 'Parses argv and dispatches a build.' },
40+
{ id: 'config', name: 'config', kind: 'data', purpose: 'Resolved build options.' },
41+
{ id: 'core', name: 'core', kind: 'subsystem', purpose: 'The build engine — everything routes through here.' },
42+
{ id: 'bundler', name: 'bundler', kind: 'module', purpose: 'Walks imports and concatenates modules.' },
43+
{ id: 'plugins', name: 'plugins', kind: 'module', purpose: 'User hooks into resolve/load.' },
44+
{ id: 'output', name: 'output', kind: 'module', purpose: 'Writes the final files to disk.' },
45+
],
46+
lineage: {
47+
links: [
48+
{ from: 'cli', to: 'core', relation: 'depends-on' },
49+
{ from: 'config', to: 'core', relation: 'depends-on' },
50+
{ from: 'core', to: 'bundler', relation: 'triggers' },
51+
{ from: 'bundler', to: 'plugins', relation: 'enables' },
52+
{ from: 'bundler', to: 'output', relation: 'triggers' },
53+
],
54+
roots: ['cli', 'core'],
55+
leaves: ['output'],
56+
},
57+
};
58+
59+
const host = document.getElementById('host');
60+
const scene = buildBlueprintScene({ deepDive, repoId: 'evanw/esbuild', title: 'esbuild' });
61+
const api = mountCanvas(host, scene, {});
62+
let tour = null;
63+
document.getElementById('tour').onclick = () => {
64+
if (tour) tour.exit();
65+
tour = startTour({ host, engine: api, steps: buildTour(scene, { roots: deepDive.lineage.roots }), autoplay: false });
66+
};
67+
window.__startTour = () => document.getElementById('tour').click(); // for the screenshot harness
68+
</script>
69+
</body>
70+
</html>

0 commit comments

Comments
 (0)