Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a1b3fc8
docs: interactive canvas design spec (engine + Blueprint + Guided Tour)
New1Direction Jun 16, 2026
6a82e92
docs: interactive canvas Phase 1 implementation plan
New1Direction Jun 16, 2026
2625dc2
feat(canvas): pure scene model (hashId, createScene, withNodePos, val…
New1Direction Jun 16, 2026
7f404b8
feat(canvas): repairGraph — tame messy LLM node/edge data
New1Direction Jun 16, 2026
2d97030
feat(canvas): layoutBlueprint — cycle-safe layered DAG positions
New1Direction Jun 16, 2026
2761b86
feat(canvas): blueprint-adapter — Deep Dive atoms/lineage → scene
New1Direction Jun 16, 2026
00f8e29
feat(canvas): buildTour — dependency-ordered guided tour steps
New1Direction Jun 16, 2026
360bb93
feat(canvas): toCanvasSvg + toExcalidraw exporters
New1Direction Jun 16, 2026
6608d0a
fix(canvas): coerce coordinates to numbers in exporters (SVG attr-inj…
New1Direction Jun 16, 2026
4a3cd45
feat(canvas): add 'scenes' object store (idb v4->v5, additive)
New1Direction Jun 16, 2026
826c3c0
feat(canvas): saveScene/getScene/listScenes/deleteScene + bulk export…
New1Direction Jun 16, 2026
41cf7aa
feat(canvas): round-trip scenes through the backup envelope
New1Direction Jun 16, 2026
681a045
feat(canvas): allowlist canvasEnabled/canvasTourAutoplay in settings …
New1Direction Jun 16, 2026
124cd88
feat(canvas): vanilla SVG engine — pan/zoom/drag + overlay spotlight
New1Direction Jun 16, 2026
9754320
refactor(canvas): drop DOM-shim test infra; unit-test engine geometry…
New1Direction Jun 16, 2026
db95fe7
feat(canvas): tour-runner — narrated spotlight walkthrough
New1Direction Jun 16, 2026
f5a6d91
feat(canvas): canvas + tour styles (theme-aware, reduced-motion safe)
New1Direction Jun 16, 2026
48f2c0a
feat(canvas): wire the Canvas tab — Blueprint render, Guided Tour, ex…
New1Direction Jun 16, 2026
4e895ff
docs: changelog + README for the interactive Canvas
New1Direction Jun 16, 2026
38b3cb9
fix(canvas): dispatch on deep-link/restore + catch async; getScene/li…
New1Direction Jun 16, 2026
935dc99
fix(canvas): tour listener cleanup + re-launch guard, slugify export …
New1Direction Jun 16, 2026
54361e6
docs: Corkboard (Canvas Phase 2) implementation plan
New1Direction Jun 16, 2026
60815b7
feat(corkboard): getLibraryGraph — full-library nodes + edges reader
New1Direction Jun 16, 2026
df896ca
feat(corkboard): layoutCorkboard — component-clustered grid seed
New1Direction Jun 16, 2026
5ccde7c
feat(corkboard): buildLibraryScene — library graph → corkboard scene
New1Direction Jun 16, 2026
e0ce11c
feat(corkboard): nodeClass helper — carry fit on cards (engine)
New1Direction Jun 16, 2026
c24c3cc
fix(corkboard): exclude unsourced idea nodes under a collection filte…
New1Direction Jun 16, 2026
36c490e
feat(corkboard): cork texture, manila cards, red-string + fit styles
New1Direction Jun 16, 2026
a5b0d0c
feat(corkboard): Library Corkboard view — mount, filter by collection…
New1Direction Jun 16, 2026
b68c93a
docs: Corkboard (Canvas Phase 2) in changelog + README
New1Direction Jun 16, 2026
76b1cab
fix(corkboard): join edges via hashed node-id → repoId (red string wa…
New1Direction Jun 16, 2026
f00e872
docs: Stack Studio (Canvas Phase 3) implementation plan
New1Direction Jun 16, 2026
2bad2de
feat(stack-studio): buildStackScene — stack result → canvas scene
New1Direction Jun 16, 2026
bfcfa1f
feat(stack-studio): layoutStack — adoption-order rows
New1Direction Jun 16, 2026
711582d
feat(stack-studio): layer-colored cards + gap/integrates styles (+ no…
New1Direction Jun 16, 2026
b5ca903
feat(stack-studio): View-on-canvas toggle in the stack result page
New1Direction Jun 16, 2026
fbdebf9
docs: Stack Studio (Canvas Phase 3) in changelog + README
New1Direction Jun 16, 2026
4a64f8f
chore(canvas): standalone demo previews (Blueprint / Corkboard / Stac…
New1Direction Jun 16, 2026
eab15cb
feat(library): clean & sexy UI polish — grouped toolbar, card micro-i…
New1Direction Jun 16, 2026
a4d016e
fix(canvas): auto-size node cards to their label (clamp + ellipsis + …
New1Direction Jun 16, 2026
4e1b631
ci: exclude vendored libs from eslint, drop unused esc import, bump d…
New1Direction Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ website
package-lock.json
*.min.js
icons
vendor
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ This project follows [Semantic Versioning](https://semver.org/) and groups chang
by theme. Dates are when the release landed on `main` — 1.1.0 through 1.6.0 shipped
the same day, as a rapid burst of improvements, so they share a date.

## [3.1.0] — 2026-06-16 · _Interactive Canvas (Blueprint · Guided Tour · Corkboard · Stack Studio)_

### Added

- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.

## [3.0.1] — 2026-06-15 · _Audit hardening_

A focused correctness, security, and tooling pass from a full code audit — no
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ A scan opens to a **verdict landing** and fans out into focused tabs:
| 🔍 | **Discover** | Search GitHub from inside the extension, or get **recommendations** from the repos you've already adopted. |
| 🕸️ | **Connections** | A walkable map centred on the current repo, showing how it relates to the others you've scanned. |
| 🤝 | **Synergies** · **Versus** · **Combinator** | Complements, head-to-heads, and fused project ideas — grounded in *your* library. |
| 🗺️ | **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**. |

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

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

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

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

/**
Expand All @@ -39,14 +40,14 @@ function emptyValue() {
* @param {{ repos?: object[], nodes?: object[], edges?: object[], cache?: object[], exportedAt?: string }} [parts]
* @returns {object}
*/
export function buildBackup({ repos, nodes, edges, cache, collections, decisions, snapshots, exportedAt } = {}) {
const r = arr(repos), n = arr(nodes), e = arr(edges), c = arr(cache), col = arr(collections), dec = arr(decisions), snap = arr(snapshots);
export function buildBackup({ repos, nodes, edges, cache, collections, decisions, snapshots, scenes, exportedAt } = {}) {
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);
return {
format: BACKUP_FORMAT,
version: BACKUP_VERSION,
exportedAt: exportedAt || new Date().toISOString(),
counts: { repos: r.length, nodes: n.length, edges: e.length, cache: c.length, collections: col.length, decisions: dec.length, snapshots: snap.length },
repos: r, nodes: n, edges: e, cache: c, collections: col, decisions: dec, snapshots: snap,
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 },
repos: r, nodes: n, edges: e, cache: c, collections: col, decisions: dec, snapshots: snap, scenes: sc,
};
}

Expand Down Expand Up @@ -88,6 +89,7 @@ export function validateBackup(obj) {
collections: clamp('collections', arr(obj.collections).filter(collectionOk)),
decisions: clamp('decisions', arr(obj.decisions).filter(decisionOk)),
snapshots: clamp('snapshots', arr(obj.snapshots).filter(snapshotOk).map((r) => ({ ...r, snaps: arr(r.snaps).slice(-SNAP_CAP) }))),
scenes: clamp('scenes', arr(obj.scenes).filter(sceneOk)),
};
return { ok: errors.length === 0, errors, warnings, value };
}
Expand All @@ -100,7 +102,7 @@ export function validateBackup(obj) {
*/
export function summarizeBackup(obj) {
const { value } = validateBackup(obj);
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 };
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 };
}

/**
Expand Down
40 changes: 40 additions & 0 deletions blueprint-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// blueprint-adapter.js
// Deep Dive atoms/lineage → a laid-out Blueprint scene.

import { createScene } from './scene.js';
import { repairGraph } from './repair-graph.js';
import { layoutBlueprint } from './canvas-layout.js';

/**
* @param {object} args
* @param {{atoms:any[], lineage:{links:any[], roots?:string[], leaves?:string[]}}} args.deepDive
* @param {string} args.repoId
* @param {string} args.title
* @param {string|null} [args.scanAt]
* @param {(atom:object)=>string} [args.layerOf] defaults to atom.kind
* @param {boolean} [args.withIssues] when true, returns { scene, issues }
* @returns {object|{scene:object, issues:object[]}}
*/
export function buildBlueprintScene({ deepDive, repoId, title, scanAt = null, layerOf = (a) => a.kind, withIssues = false }) {
const atoms = (deepDive && deepDive.atoms) || [];
const links = (deepDive && deepDive.lineage && deepDive.lineage.links) || [];
const roots = new Set((deepDive && deepDive.lineage && deepDive.lineage.roots) || []);

const layerByAtomId = Object.fromEntries(atoms.map((a) => [a.id, layerOf(a) ?? null]));
const { nodes, edges, issues } = repairGraph({
nodes: atoms.map((a) => ({ ...a, layer: layerByAtomId[a.id] })),
edges: links,
});

// mark lineage roots (load-bearing) so the engine can highlight them (immutably)
const marked = nodes.map((n) => ({ ...n, ref: { ...(n.ref || {}), root: roots.has(n.id) } }));

const placed = layoutBlueprint(marked, edges);

const scene = createScene({ scope: 'blueprint', repoId, title });
scene.nodes = placed;
scene.edges = edges;
scene.source.scanAt = scanAt;

return withIssues ? { scene, issues } : scene;
}
70 changes: 70 additions & 0 deletions canvas-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Canvas demo — RepoLens Blueprint</title>
<link rel="stylesheet" href="./themes.css" />
<style>
/* Case File fallbacks so the demo renders standalone (themes.css canvas rules use these tokens). */
:root { --bg:#fbf6ea; --surface:#fffdf6; --text:#211c14; --text-sub:#5a4f3e; --accent:#c2691c; --border:#b9a273; --dur:.2s; --ease-out:cubic-bezier(.16,1,.3,1); }
body { margin:0; font-family:ui-monospace,monospace; background:#efe4c9; color:#211c14; padding:24px; }
h1 { font-size:18px; margin:0 0 4px; }
.sub { font-size:12px; color:#6b5a36; margin:0 0 16px; }
.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); }
.bar { display:flex; gap:8px; align-items:center; padding:9px 12px; background:#f6eed7; border-bottom:1px solid #b9a273; font-size:12px; }
.bar button { padding:5px 11px; border-radius:7px; font-size:12px; cursor:pointer; border:1px solid #211c14; background:#fffdf6; }
.host { position:relative; height:520px; }
</style>
</head>
<body>
<h1>🔭 RepoLens — Blueprint canvas (live demo)</h1>
<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>
<div class="frame">
<div class="bar">
<strong>output-tab · evanw/esbuild</strong>
<span style="flex:1"></span>
<button id="tour">▶ Guided Tour</button>
</div>
<div class="host" id="host"></div>
</div>

<script type="module">
import { buildBlueprintScene } from './blueprint-adapter.js';
import { mountCanvas } from './canvas-engine.js';
import { buildTour } from './tour.js';
import { startTour } from './tour-runner.js';

const deepDive = {
atoms: [
{ id: 'cli', name: 'CLI', kind: 'entrypoint', purpose: 'Parses argv and dispatches a build.' },
{ id: 'config', name: 'config', kind: 'data', purpose: 'Resolved build options.' },
{ id: 'core', name: 'core', kind: 'subsystem', purpose: 'The build engine — everything routes through here.' },
{ id: 'bundler', name: 'bundler', kind: 'module', purpose: 'Walks imports and concatenates modules.' },
{ id: 'plugins', name: 'plugins', kind: 'module', purpose: 'User hooks into resolve/load.' },
{ id: 'output', name: 'output', kind: 'module', purpose: 'Writes the final files to disk.' },
],
lineage: {
links: [
{ from: 'cli', to: 'core', relation: 'depends-on' },
{ from: 'config', to: 'core', relation: 'depends-on' },
{ from: 'core', to: 'bundler', relation: 'triggers' },
{ from: 'bundler', to: 'plugins', relation: 'enables' },
{ from: 'bundler', to: 'output', relation: 'triggers' },
],
roots: ['cli', 'core'],
leaves: ['output'],
},
};

const host = document.getElementById('host');
const scene = buildBlueprintScene({ deepDive, repoId: 'evanw/esbuild', title: 'esbuild' });
const api = mountCanvas(host, scene, {});
let tour = null;
document.getElementById('tour').onclick = () => {
if (tour) tour.exit();
tour = startTour({ host, engine: api, steps: buildTour(scene, { roots: deepDive.lineage.roots }), autoplay: false });
};
window.__startTour = () => document.getElementById('tour').click(); // for the screenshot harness
</script>
</body>
</html>
Loading
Loading