docs(docs): add Carousel component and embed in topological‑sort page, replace static spine image with interactive component

This commit is contained in:
Prad Nukala committed 2026-09-22 14:11:33 -04:00
1 parent fd3b1952b1
commit 1a440e55ce
17 files changed
+635 -556

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
/**
* Project MDX components, available on every page with no import. Static
* `.astro` files ship no framework JS (a custom element carries any
* behaviour); interactive React lives in `islands/` instead.
*/
import { defineComponents } from "blume";
export default defineComponents({
mdx: {
Carousel: "./components/Carousel.astro",
CarouselImage: "./components/CarouselImage.astro",
},
});
+170
View File
@@ -0,0 +1,170 @@
---
/**
* A horizontal carousel of <CarouselImage> slides, built the way Blume's own
* content components are: static markup, a custom element for the behaviour,
* no framework. The track is a CSS scroll-snap strip (so it also swipes on
* touch and the browser does the animation); prev/next sit outside it and
* the play toggle sits below with a "n / N" counter.
*
* `time` is seconds per slide while playing (default 1.25); `autoplay` starts
* playing on load. Play wraps around, pauses whenever the tab is hidden, and
* never starts on its own for a reader who prefers reduced motion — the
* button still works for them. Manual navigation (buttons, arrow keys, swipe)
* does not stop playback; it only restarts the interval so a step is never
* doubled.
*/
import Icon from "blume/components/Icon.astro";
interface Props {
/** Seconds each slide stays while playing. */
time?: number;
autoplay?: boolean;
}
const { time = 1.25, autoplay = false } = Astro.props;
const buttonClass =
"inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border bg-background text-muted-foreground transition-colors hover:border-foreground hover:text-foreground disabled:cursor-default disabled:opacity-40";
---
<blume-carousel
class="not-prose my-6 block"
data-time={String(time)}
data-autoplay={autoplay ? "" : undefined}
role="region"
aria-roledescription="carousel"
>
<div class="flex items-center gap-3">
<button class={buttonClass} data-prev type="button" aria-label="Previous slide">
<Icon class="rtl:-scale-x-100" name="chevron-left" size={18} />
</button>
<div
class="flex min-w-0 flex-1 snap-x snap-mandatory overflow-x-auto scroll-smooth rounded-blume [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
data-track
tabindex="0"
aria-live="polite"
>
<slot />
</div>
<button class={buttonClass} data-next type="button" aria-label="Next slide">
<Icon class="rtl:-scale-x-100" name="chevron-right" size={18} />
</button>
</div>
<div class="mt-3 flex items-center justify-center gap-3">
<button class={buttonClass} data-play type="button" aria-label="Play" aria-pressed="false">
<Icon class="[[aria-pressed=true]_&]:hidden" name="play" size={16} />
<Icon class="[[aria-pressed=false]_&]:hidden" name="pause" size={16} />
</button>
<span class="text-muted-foreground text-xs tabular-nums leading-5" data-count></span>
</div>
</blume-carousel>
<script>
class BlumeCarousel extends HTMLElement {
#track!: HTMLElement;
#play!: HTMLButtonElement;
#count!: HTMLElement;
#timer: number | null = null;
#index = 0;
connectedCallback() {
this.#track = this.querySelector("[data-track]")!;
this.#play = this.querySelector("[data-play]")!;
this.#count = this.querySelector("[data-count]")!;
const prev = this.querySelector<HTMLButtonElement>("[data-prev]")!;
const next = this.querySelector<HTMLButtonElement>("[data-next]")!;
const slides = this.#slides().length;
if (slides < 2) {
for (const b of [prev, next, this.#play]) b.disabled = true;
}
this.#render();
prev.addEventListener("click", () => this.#step(-1));
next.addEventListener("click", () => this.#step(1));
this.#play.addEventListener("click", () => (this.#timer === null ? this.#start() : this.#stop()));
this.addEventListener("keydown", (event) => {
if (event.key === "ArrowLeft") this.#step(-1);
else if (event.key === "ArrowRight") this.#step(1);
else return;
event.preventDefault();
});
// Swipes and trackpad scrolls move the track without #step; follow them
// once the scroll settles. Settle-only, not per event: a long smooth
// scroll from #step passes over intermediate slides and must not be
// read back as the reader's position mid-flight.
let settle: number | undefined;
this.#track.addEventListener(
"scroll",
() => {
window.clearTimeout(settle);
settle = window.setTimeout(() => this.#syncFromScroll(), 150);
},
{ passive: true }
);
document.addEventListener("visibilitychange", this.#onVisibility);
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
if (this.hasAttribute("data-autoplay") && slides > 1 && !reduced) this.#start();
}
disconnectedCallback() {
this.#stop();
document.removeEventListener("visibilitychange", this.#onVisibility);
}
#slides(): HTMLElement[] {
return [...this.#track.querySelectorAll<HTMLElement>(":scope > blume-carousel-image")];
}
#step(delta: number) {
const slides = this.#slides();
this.#index = (this.#index + delta + slides.length) % slides.length;
slides[this.#index]!.scrollIntoView({ block: "nearest", inline: "start", behavior: "smooth" });
this.#render();
if (this.#timer !== null) this.#start(); // restart the interval from this slide
}
#syncFromScroll() {
const at = Math.round(this.#track.scrollLeft / this.#track.clientWidth);
if (at !== this.#index && Number.isFinite(at)) {
this.#index = Math.min(Math.max(at, 0), this.#slides().length - 1);
this.#render();
}
}
#start() {
this.#stop();
const ms = Number(this.dataset.time) * 1000;
this.#timer = window.setInterval(() => this.#step(1), ms > 0 ? ms : 1250);
this.#play.setAttribute("aria-pressed", "true");
this.#play.setAttribute("aria-label", "Pause");
}
#stop() {
if (this.#timer !== null) window.clearInterval(this.#timer);
this.#timer = null;
this.#play.setAttribute("aria-pressed", "false");
this.#play.setAttribute("aria-label", "Play");
}
// A background tab keeps its timers but not its layout; resume on return.
#wasPlaying = false;
#onVisibility = () => {
if (document.hidden) {
this.#wasPlaying = this.#timer !== null;
this.#stop();
} else if (this.#wasPlaying) this.#start();
};
#render() {
const slides = this.#slides();
this.#count.textContent = `${this.#index + 1} / ${slides.length}`;
slides.forEach((s, i) => s.setAttribute("aria-hidden", i === this.#index ? "false" : "true"));
}
}
if (!customElements.get("blume-carousel")) {
customElements.define("blume-carousel", BlumeCarousel);
}
</script>
+45
View File
@@ -0,0 +1,45 @@
---
/**
* One slide of a <Carousel>: Blume's own <Frame> around an <img>, so a slide
* is pixel-identical to a framed image elsewhere on the site — `alt` doubles
* as the frame's hint line and `caption` is Markdown, exactly as in Frame.
*
* `src` accepts what MDX authors already write for images: a URL, a
* root-relative `/file.png` (served from public/, prefixed with the deploy
* base), or the page-relative `../../public/file.png` form. A relative path
* that does not pass through public/ has nothing to serve it, so it is a
* build error rather than a silent 404.
*/
import Frame from "blume/components/content/Frame.astro";
import { withBase } from "blume/components/islands/base-path.ts";
interface Props {
src: string;
alt?: string;
caption?: string;
}
const { src, alt, caption } = Astro.props;
const resolveSrc = (value: string): string => {
if (URL.canParse(value)) return value;
if (value.startsWith("/")) return withBase(value);
const at = value.indexOf("public/");
if (at === -1) {
throw new Error(
`<CarouselImage src="${value}">: a relative src must point into public/ (or use /file.png)`
);
}
return withBase(`/${value.slice(at + "public/".length)}`);
};
---
<blume-carousel-image
class="block w-full shrink-0 snap-center [&_blume-frame]:my-0"
role="group"
aria-roledescription="slide"
>
<Frame caption={caption} hint={alt}>
<img alt={alt ?? ""} src={resolveSrc(src)} loading="lazy" draggable="false" />
</Frame>
</blume-carousel-image>
+1 -1
View File
@@ -3,7 +3,7 @@ title: Overview
description: Live SRS campaign dashboard — phases, ladder, review queue, gates, and recent attempts, fetched from the SRS Worker. description: Live SRS campaign dashboard — phases, ladder, review queue, gates, and recent attempts, fetched from the SRS Worker.
--- ---
![Dependency Map](../public/dependency-spine.svg) <DependencySpine />
Live view of the 56-day campaign. Everything below is fetched client-side Live view of the 56-day campaign. Everything below is fetched client-side
from the SRS Worker, so it is always current — no rebuild required. from the SRS Worker, so it is always current — no rebuild required.
+29 -6
View File
@@ -8,11 +8,11 @@ hidden: true
Before we get started on topological order, let's talk about directed graphs first. A graph is directed when its edges have directions (these edges are also called arcs). Suppose that v, w are vertices in a directed graph. Let's get familiar with a few concepts. Before we get started on topological order, let's talk about directed graphs first. A graph is directed when its edges have directions (these edges are also called arcs). Suppose that v, w are vertices in a directed graph. Let's get familiar with a few concepts.
- in-edge - an edge pointing to or coming into the node - **in-edge** - an edge pointing to or coming into the node
- out-edge - an edge pointing away or going out from the node (we will refer those nodes on the other end of the out-edges of v to v's neighbors) - **out-edge** - an edge pointing away or going out from the node (we will refer those nodes on the other end of the out-edges of v to v's neighbors)
- in-degree - the number of in-edges coming into the node - **in-degree** - the number of in-edges coming into the node
- out-degree - the number of out-edges going out from the node - **out-degree** - the number of out-edges going out from the node
- dependency - "v depends on w" / "v is dependent on w" means that there is a directed path from w to v where v, w are vertices in a directed graph. With these new terms under our belt, it will make more sense when we talk about directed graphs. - **dependency** - "v depends on w" / "v is dependent on w" means that there is a directed path from w to v where v, w are vertices in a directed graph. With these new terms under our belt, it will make more sense when we talk about directed graphs.
## What is a topological order? ## What is a topological order?
@@ -33,10 +33,33 @@ Task 3 is completely independent of task 2 and 4, and it can be anywhere in the
It should be obvious that if a graph with a cycle does not have a topological ordering. In the above example, 2 has to come before 5 which has come before 4 which has to come before 2 which has to come before 5... which is impossible. It should be obvious that if a graph with a cycle does not have a topological ordering. In the above example, 2 has to come before 5 which has come before 4 which has to come before 2 which has to come before 5... which is impossible.
::: :::
## Kahn's Algorithm
To obtain a topological order, we can use Kahn's algorithm which is very similar to Breadth First Search.
In order to understand why we must use this algorithm we first look at the problem it is trying to solve.
Given a directed graph does there exist a way to remove the nodes such that each time we remove a node we guarantee that no other nodes point to that particular node?
For this algorithm, we systematically remove one node at a time, each time removing a node such that no other nodes point to that node (in-degree is 0). If no such node exists, then there must be a cycle, and there is no way to order the nodes such that "every node appears in the ordering before all the nodes it points to" (no solution). After removing the current node, for each neighboring node the current node points to, we check whether any nodes point to this node. If there isn't any, we push this node into the queue. Notice it is important to keep track of the number of nodes pointing to a node (in-degree) in the question as we only push the node into the queue once all nodes the current node depended on have been removed.
Here is a graphic to demonstrate the idea.
<Carousel>
<CarouselImage src='../../public/topo_kahn.001.png' />
<CarouselImage src='../../public/topo_kahn.002.png' />
<CarouselImage src='../../public/topo_kahn.003.png' />
<CarouselImage src='../../public/topo_kahn.004.png' />
<CarouselImage src='../../public/topo_kahn.005.png' />
<CarouselImage src='../../public/topo_kahn.006.png' />
<CarouselImage src='../../public/topo_kahn.007.png' />
<CarouselImage src='../../public/topo_kahn.009.png' />
<CarouselImage src='../../public/topo_kahn.010.png' />
</Carousel>
## Example Code ## Example Code
:::note :::tip
Notice the topological sort algorithm is very similar to BFS. The main difference is that we only push nodes with 0 in-degree into the queue in topological sort whereas in BFS we push all the neighboring nodes into the queue. Notice the topological sort algorithm is very similar to BFS. The main difference is that we only push nodes with 0 in-degree into the queue in topological sort whereas in BFS we push all the neighboring nodes into the queue.
::: :::
+377
View File
@@ -0,0 +1,377 @@
/**
* Dependency spine island — the "how the 24 topics build on each other" map
* from the README, redrawn as data (nodes on a 7×6 grid, edges as SVG path
* strings) so it is themed, zoomable, and clickable.
*
* - Wheel over the figure zooms about the cursor (trackpad pinch arrives as a
* ctrl+wheel and behaves the same); dragging pans; the toolbar and +/-/0
* keys do the same. Zooming OUT at the minimum scale is deliberately left
* to the page, so a reader scrolling past the map is never trapped in it.
* - A node is a link to its topic. Routes come from Blume's own navigation
* endpoint at mount: the topic's landing page when the folder has an
* `index.mdx`, else its first solution page. A topic with no pages yet is
* drawn inert rather than linked to a 404, so the map stays honest as the
* campaign fills in without a rebuild of this file.
*
* Invariants: the first frame is the figure at HOME_ZOOM, centred, with no
* links (server and client render identically); the geometry is the README
* SVG's, to the pixel, so `viewBox` maths below is in that 1472×648 space.
*/
import { useEffect, useRef, useState, type CSSProperties, type PointerEvent } from "react";
import { withBase } from "blume/components/islands/base-path.ts";
export const client = "visible";
// ── figure data (README dependency-spine geometry) ───────────────
const W = 1472;
const H = 648;
const MAX_ZOOM = 6;
const NODE_W = 144;
const NODE_H = 48;
const COL_X = [56, 248, 472, 664, 888, 1080, 1272] as const;
const ROW_Y = [72, 144, 216, 288, 416, 488] as const;
type Kind = "entry" | "topic" | "focal";
interface Topic {
n: number;
name: string;
sub?: string;
/** Sync's group slug for the README topic name — the docs folder. */
slug: string;
kind: Kind;
col: number;
row: number;
}
const TOPICS: Topic[] = [
{ n: 5, name: "Binary Search", slug: "binary-search", kind: "entry", col: 0, row: 0 },
{ n: 3, name: "Two Pointers", slug: "two-pointers", kind: "entry", col: 0, row: 1 },
{ n: 4, name: "Sliding Window", slug: "sliding-window", kind: "topic", col: 1, row: 1 },
{ n: 6, name: "Stack", slug: "stack", kind: "entry", col: 0, row: 2 },
{ n: 7, name: "Monotonic Stack", slug: "monotonic-stack", kind: "topic", col: 1, row: 2 },
{ n: 1, name: "Hash Lookup", slug: "hash-table", kind: "entry", col: 0, row: 3 },
{ n: 2, name: "Prefix Sum", slug: "prefix-sum", kind: "topic", col: 1, row: 3 },
{ n: 11, name: "Tree DFS", slug: "tree-dfs", kind: "focal", col: 2, row: 0 },
{ n: 13, name: "Binary Search Tree", slug: "binary-search-tree", kind: "topic", col: 2, row: 1 },
{ n: 14, name: "Heap", sub: "priority queue", slug: "heap-priority-queue", kind: "topic", col: 3, row: 1 },
{ n: 15, name: "Trie", slug: "trie", kind: "topic", col: 3, row: 2 },
{ n: 12, name: "Tree BFS", slug: "tree-bfs", kind: "entry", col: 2, row: 3 },
{ n: 8, name: "Linked List", slug: "linked-list", kind: "entry", col: 2, row: 4 },
{ n: 9, name: "Hybrid Structures", slug: "hybrid-structures", kind: "topic", col: 3, row: 4 },
{ n: 10, name: "Matrix Index Math", slug: "matrix-index-math", kind: "topic", col: 2, row: 5 },
{ n: 16, name: "Graph DFS", slug: "graph-dfs", kind: "topic", col: 4, row: 0 },
{ n: 18, name: "Topological Sort", slug: "topological-sort", kind: "topic", col: 5, row: 0 },
{ n: 19, name: "Union-Find", slug: "union-find", kind: "topic", col: 4, row: 1 },
{ n: 20, name: "Backtracking", slug: "backtracking", kind: "topic", col: 5, row: 1 },
{ n: 21, name: "1-D DP", sub: "dynamic programming", slug: "1-d-dynamic-programming", kind: "focal", col: 5, row: 2 },
{ n: 22, name: "Multi-D / Grid DP", slug: "multi-d-grid-dp", kind: "topic", col: 6, row: 2 },
{ n: 17, name: "Graph BFS", slug: "graph-bfs", kind: "topic", col: 4, row: 3 },
{ n: 23, name: "Greedy", slug: "greedy", kind: "topic", col: 5, row: 3 },
{ n: 24, name: "Intervals", slug: "intervals", kind: "topic", col: 6, row: 3 },
];
/** "builds on" arrows, source → target, in the figure's coordinate space. */
const EDGES: [string, string][] = [
["3 → 4", "M200,168 H248"],
["6 → 7", "M200,240 H248"],
["1 → 2", "M200,312 H248"],
["8 → 9", "M616,440 H664"],
["16 → 18", "M1032,96 H1080"],
["21 → 22", "M1224,240 H1272"],
["23 → 24", "M1224,312 H1272"],
["11 → 16", "M616,84 H888"],
["12 → 17", "M616,312 H888"],
["11 → 13", "M544,120 V144"],
["16 → 19", "M960,120 V144"],
["18 → 20", "M1152,120 V144"],
["20 → 21", "M1152,192 V216"],
["21 → 23", "M1152,264 V288"],
["5 → 13", "M200,96 H416 Q424,96 424,104 V160 Q424,168 432,168 H472"],
["11 → 14", "M616,96 H640 Q648,96 648,104 V160 Q648,168 656,168 H664"],
["11 → 15", "M616,108 H624 Q632,108 632,116 V232 Q632,240 640,240 H664"],
["4 → 10", "M392,184 H432 Q440,184 440,192 V504 Q440,512 448,512 H472"],
["1 → 9", "M128,336 V560 Q128,568 136,568 H728 Q736,568 736,560 V464"],
];
const ZONES: [string, number, number, number, number, number][] = [
// label, x, y, w, h, label plate width
["PHASE I — LINEAR STRUCTURES", 40, 40, 368, 312, 168],
["PHASE III — HIERARCHICAL", 456, 40, 368, 312, 152],
["PHASE II — NODAL & GRID", 456, 384, 368, 168, 144],
["PHASES IV–V — RELATIONAL & DECISION-SPACE", 872, 40, 560, 312, 264],
];
// ── theme ────────────────────────────────────────────────────────
// Everything is a Blume token so the map follows the site's light/dark
// scheme instead of the README's fixed black; tints go through color-mix.
const fg = "var(--blume-foreground)";
const muted = "var(--blume-muted-foreground)";
const accent = "var(--blume-accent)";
const tint = (color: string, pct: number) => `color-mix(in oklab, ${color} ${pct}%, transparent)`;
const NODE_STYLE: Record<Kind, { fill: string; stroke: string; strokeWidth: number; tag: string }> = {
entry: { fill: tint(muted, 10), stroke: tint(muted, 70), strokeWidth: 1, tag: muted },
topic: { fill: tint(fg, 4), stroke: fg, strokeWidth: 1, tag: muted },
focal: { fill: tint(accent, 12), stroke: accent, strokeWidth: 1.2, tag: accent },
};
const mono: CSSProperties = { fontFamily: "var(--blume-font-mono, ui-monospace, monospace)" };
const sans: CSSProperties = { fontFamily: "var(--blume-font-sans, system-ui, sans-serif)" };
const toolButton: CSSProperties = {
width: "1.75rem",
height: "1.75rem",
border: "1px solid var(--blume-border)",
borderRadius: "9999px",
background: "var(--blume-background)",
color: muted,
font: "inherit",
lineHeight: 1,
cursor: "pointer",
};
// ── navigation lookup ────────────────────────────────────────────
interface NavGroup {
kind: "group";
path: string;
route?: string | null;
children: unknown[];
}
const isGroup = (v: unknown): v is NavGroup =>
typeof v === "object" && v !== null && (v as NavGroup).kind === "group" &&
typeof (v as NavGroup).path === "string" && Array.isArray((v as NavGroup).children);
/** The first page route under a sidebar entry, depth-first. */
const firstRoute = (items: unknown[]): string | null => {
for (const item of items) {
if (typeof item !== "object" || item === null) continue;
const v = item as { route?: unknown; children?: unknown };
if (typeof v.route === "string") return v.route;
if (Array.isArray(v.children)) {
const inner = firstRoute(v.children);
if (inner) return inner;
}
}
return null;
};
/** folder path (`/tree-dfs`) → the route a node should open. */
const topicRoutes = (nav: unknown): Record<string, string> => {
const out: Record<string, string> = {};
const sidebar = (nav as { sidebar?: unknown })?.sidebar;
if (!Array.isArray(sidebar)) return out;
for (const entry of sidebar) {
if (!isGroup(entry)) continue;
const route = entry.route ?? firstRoute(entry.children);
if (route) out[entry.path] = route;
}
return out;
};
// ── component ────────────────────────────────────────────────────
interface ViewBox { x: number; y: number; w: number; h: number }
/** The resting view — 2× on the centre so node labels read at the content width; 1× stays reachable by zooming out. */
const HOME_ZOOM = 2;
const HOME: ViewBox = { x: (W - W / HOME_ZOOM) / 2, y: (H - H / HOME_ZOOM) / 2, w: W / HOME_ZOOM, h: H / HOME_ZOOM };
const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi);
export default function DependencySpine() {
const svgRef = useRef<SVGSVGElement>(null);
const [vb, setVbState] = useState<ViewBox>(HOME);
// The native wheel listener and back-to-back events read the latest view
// from the ref, which is written synchronously — state alone lags a render.
const vbRef = useRef(vb);
const setVb = (next: ViewBox) => {
vbRef.current = next;
setVbState(next);
};
const [routes, setRoutes] = useState<Record<string, string> | null>(null);
const drag = useRef<{ x: number; y: number; moved: boolean } | null>(null);
useEffect(() => {
let live = true;
fetch(withBase("/api/docs/navigation.json"))
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
.then((nav) => live && setRoutes(topicRoutes(nav)))
.catch(() => live && setRoutes({}));
return () => { live = false; };
}, []);
/** Zoom by `factor` (>1 in) about a client-space point; omitted = centre. */
const zoomAt = (factor: number, clientX?: number, clientY?: number) => {
const rect = svgRef.current?.getBoundingClientRect();
const cur = vbRef.current;
if (!rect) return;
const px = clientX === undefined ? cur.x + cur.w / 2 : cur.x + ((clientX - rect.left) / rect.width) * cur.w;
const py = clientY === undefined ? cur.y + cur.h / 2 : cur.y + ((clientY - rect.top) / rect.height) * cur.h;
const w = clamp(cur.w / factor, W / MAX_ZOOM, W);
const f = cur.w / w;
const h = H * (w / W);
setVb({ x: clamp(px - (px - cur.x) / f, 0, W - w), y: clamp(py - (py - cur.y) / f, 0, H - h), w, h });
};
// React registers `wheel` as passive, so preventDefault has to go native.
useEffect(() => {
const svg = svgRef.current;
if (!svg) return;
const onWheel = (e: WheelEvent) => {
if (e.deltaY > 0 && vbRef.current.w >= W) return; // fully zoomed out: let the page scroll
e.preventDefault();
zoomAt(Math.exp(-e.deltaY * 0.0015), e.clientX, e.clientY);
};
svg.addEventListener("wheel", onWheel, { passive: false });
return () => svg.removeEventListener("wheel", onWheel);
}, []);
const onPointerDown = (e: PointerEvent<SVGSVGElement>) => {
if (e.button !== 0) return;
drag.current = { x: e.clientX, y: e.clientY, moved: false };
};
const onPointerMove = (e: PointerEvent<SVGSVGElement>) => {
const d = drag.current;
const rect = svgRef.current?.getBoundingClientRect();
if (!d || !rect) return;
const dx = e.clientX - d.x;
const dy = e.clientY - d.y;
if (!d.moved && Math.hypot(dx, dy) < 4) return; // a click, not a drag
if (!d.moved) {
// Capture only once it is a drag: capturing on pointerdown would retarget
// pointerup (and so the click) to the <svg>, and a node's link would never fire.
d.moved = true;
e.currentTarget.setPointerCapture(e.pointerId);
}
d.x = e.clientX;
d.y = e.clientY;
const cur = vbRef.current;
const scale = cur.w / rect.width;
setVb({ ...cur, x: clamp(cur.x - dx * scale, 0, W - cur.w), y: clamp(cur.y - dy * scale, 0, H - cur.h) });
};
const onPointerUp = (e: PointerEvent<SVGSVGElement>) => {
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
// Cleared on the next tick so a node's click handler can still see `moved`.
setTimeout(() => { drag.current = null; }, 0);
};
const zoom = W / vb.w;
const atHome = vb.x === HOME.x && vb.y === HOME.y && vb.w === HOME.w;
return (
<figure
className="not-prose"
style={{ margin: "1.5rem 0", border: "1px solid var(--blume-border)", borderRadius: "var(--blume-radius)", background: tint("var(--blume-muted)", 30), overflow: "hidden" }}
aria-label="Dependency spine: how the 24 topics build on each other"
onKeyDown={(e) => {
if (e.key === "+" || e.key === "=") zoomAt(1.25);
else if (e.key === "-") zoomAt(0.8);
else if (e.key === "0") setVb(HOME);
else return;
e.preventDefault();
}}
tabIndex={0}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.75rem", padding: "0.5rem 0.75rem", borderBottom: "1px solid var(--blume-border)" }}>
<span style={{ ...mono, fontSize: "0.7rem", letterSpacing: "0.12em", textTransform: "uppercase", color: muted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
scroll to zoom · drag to pan · click a topic
</span>
<span style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexShrink: 0 }}>
<span style={{ ...mono, fontSize: "0.7rem", color: muted, minWidth: "4ch", textAlign: "right", whiteSpace: "nowrap" }}>{zoom.toFixed(1)}×</span>
<button type="button" style={toolButton} aria-label="Zoom out" onClick={() => zoomAt(0.8)} disabled={zoom <= 1.001}>−</button>
<button type="button" style={toolButton} aria-label="Zoom in" onClick={() => zoomAt(1.25)} disabled={zoom >= MAX_ZOOM - 0.001}>+</button>
<button type="button" style={toolButton} aria-label="Reset zoom" onClick={() => setVb(HOME)} disabled={atHome}>⟲</button>
</span>
</div>
<svg
ref={svgRef}
viewBox={`${vb.x} ${vb.y} ${vb.w} ${vb.h}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="dependency-spine-title dependency-spine-desc"
style={{ display: "block", width: "100%", height: "auto", touchAction: "none", cursor: drag.current?.moved ? "grabbing" : zoom > 1.001 ? "grab" : "default", userSelect: "none" }}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
onDoubleClick={() => setVb(HOME)}
>
<title id="dependency-spine-title">LeetCode topic dependency spine</title>
<desc id="dependency-spine-desc">
Dependency graph of 24 LeetCode study topics across five phases, showing which topics build on which, with Tree DFS and 1-D dynamic programming as the two central hubs.
</desc>
<defs>
<marker id="spine-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" style={{ fill: muted }} />
</marker>
</defs>
{ZONES.map(([label, x, y, w, h, plate]) => (
<g key={label}>
<rect x={x} y={y} width={w} height={h} rx={8} style={{ fill: tint(fg, 2), stroke: tint(fg, 10), strokeWidth: 0.8 }} />
<rect x={x + 16} y={y + 4} width={plate} height={12} rx={2} style={{ fill: "var(--blume-background)" }} />
<text x={x + 20} y={y + 13} fontSize={7} letterSpacing="0.14em" style={{ ...mono, fill: tint(fg, 40) }}>{label}</text>
</g>
))}
{EDGES.map(([id, d]) => (
<path key={id} d={d} fill="none" strokeWidth={1} markerEnd="url(#spine-arrow)" style={{ stroke: muted }}>
<title>{id}</title>
</path>
))}
{TOPICS.map((t) => {
const x = COL_X[t.col]!;
const y = ROW_Y[t.row]!;
const s = NODE_STYLE[t.kind];
const route = routes?.[`/${t.slug}`];
const tag = String(t.n).padStart(2, "0");
const body = (
<>
<title>{route ? `${tag} · ${t.name} — open topic` : `${tag} · ${t.name} — no pages yet`}</title>
<rect x={x} y={y} width={NODE_W} height={NODE_H} rx={6} style={{ fill: "var(--blume-background)" }} />
<rect x={x} y={y} width={NODE_W} height={NODE_H} rx={6} style={{ fill: s.fill, stroke: s.stroke, strokeWidth: s.strokeWidth }} />
<rect x={x + 8} y={y + 6} width={24} height={12} rx={2} fill="transparent" style={{ stroke: tint(s.tag, 50), strokeWidth: 0.8 }} />
<text x={x + 20} y={y + 15} fontSize={7} textAnchor="middle" letterSpacing="0.08em" style={{ ...mono, fill: s.tag }}>{tag}</text>
<text x={x + NODE_W / 2} y={y + (t.sub ? 26 : 32)} fontSize={12} fontWeight={600} textAnchor="middle" style={{ ...sans, fill: fg }}>{t.name}</text>
{t.sub && <text x={x + NODE_W / 2} y={y + 40} fontSize={9} textAnchor="middle" style={{ ...mono, fill: muted }}>{t.sub}</text>}
</>
);
return route ? (
<a
key={t.n}
href={withBase(route)}
className="spine-node"
style={{ cursor: "pointer", textDecoration: "none" }}
onClick={(e) => { if (drag.current?.moved) e.preventDefault(); }}
>
{body}
</a>
) : (
<g key={t.n} style={{ opacity: routes ? 0.55 : 1 }} aria-disabled="true">{body}</g>
);
})}
<line x1={40} y1={600} x2={1432} y2={600} strokeWidth={0.8} style={{ stroke: tint(fg, 10) }} />
<text x={40} y={620} fontSize={8} letterSpacing="0.14em" style={{ ...mono, fill: muted }}>LEGEND</text>
<rect x={160} y={608} width={20} height={12} rx={2} style={{ fill: NODE_STYLE.entry.fill, stroke: NODE_STYLE.entry.stroke, strokeWidth: 1 }} />
<text x={188} y={618} fontSize={8} letterSpacing="0.06em" style={{ ...mono, fill: muted }}>ENTRY — NO PREREQS</text>
<rect x={360} y={608} width={20} height={12} rx={2} style={{ fill: NODE_STYLE.topic.fill, stroke: NODE_STYLE.topic.stroke, strokeWidth: 1 }} />
<text x={388} y={618} fontSize={8} letterSpacing="0.06em" style={{ ...mono, fill: muted }}>TOPIC</text>
<rect x={480} y={608} width={20} height={12} rx={2} style={{ fill: NODE_STYLE.focal.fill, stroke: NODE_STYLE.focal.stroke, strokeWidth: 1.2 }} />
<text x={508} y={618} fontSize={8} letterSpacing="0.06em" style={{ ...mono, fill: muted }}>FOCAL HUB</text>
<line x1={624} y1={614} x2={656} y2={614} strokeWidth={1} markerEnd="url(#spine-arrow)" style={{ stroke: muted }} />
<text x={668} y={618} fontSize={8} letterSpacing="0.06em" style={{ ...mono, fill: muted }}>BUILDS ON</text>
<rect x={792} y={608} width={20} height={12} rx={2} style={{ fill: tint(fg, 2), stroke: tint(fg, 10), strokeWidth: 0.8 }} />
<text x={820} y={618} fontSize={8} letterSpacing="0.06em" style={{ ...mono, fill: muted }}>PHASE ZONE</text>
<rect x={1000} y={608} width={20} height={12} rx={2} style={{ fill: NODE_STYLE.topic.fill, stroke: NODE_STYLE.topic.stroke, strokeWidth: 1, opacity: 0.55 }} />
<text x={1028} y={618} fontSize={8} letterSpacing="0.06em" style={{ ...mono, fill: muted }}>NO PAGES YET</text>
</svg>
<style>{`.spine-node:hover rect:nth-of-type(2), .spine-node:focus-visible rect:nth-of-type(2) { stroke: ${accent}; stroke-width: 1.6; } .spine-node:focus { outline: none; }`}</style>
</figure>
);
}
-305
View File
@@ -1,305 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LeetCode Dependency Spine</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #020202;
--color-ink: #f2f2f2;
--color-muted: #989898;
--color-accent: #449df0;
--font-sans: 'Inter', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1360px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">prdlk / leetcode · dependency spine</p>
<h1>How the 24 topics build on each other</h1>
<svg viewBox="0 0 1472 648" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="dependency-spine-title dependency-spine-desc">
<title id="dependency-spine-title">LeetCode topic dependency spine</title>
<desc id="dependency-spine-desc">Dependency graph of 24 LeetCode study topics across five phases, showing which topics build on which, with Tree DFS and 1-D dynamic programming as the two central hubs.</desc>
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#989898"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#449df0"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#6ab0f5"/></marker>
</defs>
<rect width="100%" height="100%" fill="#020202"/>
<!-- ============ zones (painted first) ============ -->
<rect x="40" y="40" width="368" height="312" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="56" y="44" width="168" height="12" rx="2" fill="#020202"/>
<text x="60" y="53" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASE I — LINEAR STRUCTURES</text>
<rect x="456" y="40" width="368" height="312" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="472" y="44" width="152" height="12" rx="2" fill="#020202"/>
<text x="476" y="53" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASE III — HIERARCHICAL</text>
<rect x="456" y="384" width="368" height="168" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="472" y="388" width="144" height="12" rx="2" fill="#020202"/>
<text x="476" y="397" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASE II — NODAL &amp; GRID</text>
<rect x="872" y="40" width="560" height="312" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="888" y="44" width="264" height="12" rx="2" fill="#020202"/>
<text x="892" y="53" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASES IV–V — RELATIONAL &amp; DECISION-SPACE</text>
<!-- ============ arrows (before boxes) ============ -->
<!-- same-row horizontals -->
<line x1="200" y1="168" x2="248" y2="168" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 3 → 4 -->
<line x1="200" y1="240" x2="248" y2="240" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 6 → 7 -->
<line x1="200" y1="312" x2="248" y2="312" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 1 → 2 -->
<line x1="616" y1="440" x2="664" y2="440" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 8 → 9 -->
<line x1="1032" y1="96" x2="1080" y2="96" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 16 → 18 -->
<line x1="1224" y1="240" x2="1272" y2="240" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 21 → 22 -->
<line x1="1224" y1="312" x2="1272" y2="312" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 23 → 24 -->
<line x1="616" y1="84" x2="888" y2="84" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 16 -->
<line x1="616" y1="312" x2="888" y2="312" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 12 → 17 -->
<!-- same-column verticals -->
<line x1="544" y1="120" x2="544" y2="144" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 13 -->
<line x1="960" y1="120" x2="960" y2="144" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 16 → 19 -->
<line x1="1152" y1="120" x2="1152" y2="144" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 18 → 20 -->
<line x1="1152" y1="192" x2="1152" y2="216" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 20 → 21 -->
<line x1="1152" y1="264" x2="1152" y2="288" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 21 → 23 -->
<!-- rounded right-angle elbows -->
<path d="M200,96 H416 Q424,96 424,104 V160 Q424,168 432,168 H472" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 5 → 13 -->
<path d="M616,96 H640 Q648,96 648,104 V160 Q648,168 656,168 H664" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 14 -->
<path d="M616,108 H624 Q632,108 632,116 V232 Q632,240 640,240 H664" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 15 -->
<path d="M392,184 H432 Q440,184 440,192 V504 Q440,512 448,512 H472" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 4 → 10 -->
<path d="M128,336 V560 Q128,568 136,568 H728 Q736,568 736,560 V464" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 1 → 9 -->
<!-- ============ nodes ============ -->
<!-- 05 Binary Search — entry -->
<rect x="56" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="72" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="87" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">05</text>
<text x="128" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Binary Search</text>
<!-- 03 Two Pointers — entry -->
<rect x="56" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="144" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">03</text>
<text x="128" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Two Pointers</text>
<!-- 04 Sliding Window -->
<rect x="248" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="248" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="256" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="268" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">04</text>
<text x="320" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Sliding Window</text>
<!-- 06 Stack — entry -->
<rect x="56" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="216" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">06</text>
<text x="128" y="248" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Stack</text>
<!-- 07 Monotonic Stack -->
<rect x="248" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="248" y="216" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="256" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="268" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">07</text>
<text x="320" y="248" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Monotonic Stack</text>
<!-- 01 Hash-Based Lookup — entry -->
<rect x="56" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="288" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">01</text>
<text x="128" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Hash Lookup</text>
<!-- 02 Prefix Sum -->
<rect x="248" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="248" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="256" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="268" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">02</text>
<text x="320" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Prefix Sum</text>
<!-- 11 Tree DFS — FOCAL -->
<rect x="472" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="72" width="144" height="48" rx="6" fill="rgba(68,157,240,0.12)" stroke="#449df0" stroke-width="1.2"/>
<rect x="480" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(68,157,240,0.50)" stroke-width="0.8"/>
<text x="492" y="87" fill="#449df0" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">11</text>
<text x="544" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Tree DFS</text>
<!-- 13 Binary Search Tree -->
<rect x="472" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="480" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">13</text>
<text x="544" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Binary Search Tree</text>
<!-- 14 Heap -->
<rect x="664" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="664" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="672" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="684" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">14</text>
<text x="736" y="170" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Heap</text>
<text x="736" y="184" fill="#989898" font-size="9" font-family="'IBM Plex Mono', monospace" text-anchor="middle">priority queue</text>
<!-- 15 Trie -->
<rect x="664" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="664" y="216" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="672" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="684" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">15</text>
<text x="736" y="248" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Trie</text>
<!-- 12 Tree BFS — entry -->
<rect x="472" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="288" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="480" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">12</text>
<text x="544" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Tree BFS</text>
<!-- 08 Linked List — entry -->
<rect x="472" y="416" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="416" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="480" y="422" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="431" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">08</text>
<text x="544" y="448" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Linked List</text>
<!-- 09 Hybrid Structures -->
<rect x="664" y="416" width="144" height="48" rx="6" fill="#020202"/>
<rect x="664" y="416" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="672" y="422" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="684" y="431" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">09</text>
<text x="736" y="448" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Hybrid Structures</text>
<!-- 10 Matrix Index Math -->
<rect x="472" y="488" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="488" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="480" y="494" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="503" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">10</text>
<text x="544" y="520" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Matrix Index Math</text>
<!-- 16 Graph DFS -->
<rect x="888" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="888" y="72" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="896" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="908" y="87" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">16</text>
<text x="960" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Graph DFS</text>
<!-- 18 Topological Sort -->
<rect x="1080" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="72" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1088" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1100" y="87" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">18</text>
<text x="1152" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Topological Sort</text>
<!-- 19 Union-Find -->
<rect x="888" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="888" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="896" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="908" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">19</text>
<text x="960" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Union-Find</text>
<!-- 20 Backtracking -->
<rect x="1080" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1088" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1100" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">20</text>
<text x="1152" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Backtracking</text>
<!-- 21 1-D DP — FOCAL -->
<rect x="1080" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="216" width="144" height="48" rx="6" fill="rgba(68,157,240,0.12)" stroke="#449df0" stroke-width="1.2"/>
<rect x="1088" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(68,157,240,0.50)" stroke-width="0.8"/>
<text x="1100" y="231" fill="#449df0" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">21</text>
<text x="1152" y="242" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">1-D DP</text>
<text x="1152" y="256" fill="#989898" font-size="9" font-family="'IBM Plex Mono', monospace" text-anchor="middle">dynamic programming</text>
<!-- 22 Multi-D / Grid DP -->
<rect x="1272" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1272" y="216" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1280" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1292" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">22</text>
<text x="1344" y="242" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Multi-D / Grid DP</text>
<!-- 17 Graph BFS -->
<rect x="888" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="888" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="896" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="908" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">17</text>
<text x="960" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Graph BFS</text>
<!-- 23 Greedy -->
<rect x="1080" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1088" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1100" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">23</text>
<text x="1152" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Greedy</text>
<!-- 24 Intervals -->
<rect x="1272" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1272" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1280" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1292" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">24</text>
<text x="1344" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Intervals</text>
<!-- ============ legend ============ -->
<line x1="40" y1="600" x2="1432" y2="600" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<text x="40" y="620" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">LEGEND</text>
<rect x="160" y="608" width="20" height="12" rx="2" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<text x="188" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">ENTRY — NO PREREQS</text>
<rect x="360" y="608" width="20" height="12" rx="2" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<text x="388" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">TOPIC</text>
<rect x="480" y="608" width="20" height="12" rx="2" fill="rgba(68,157,240,0.12)" stroke="#449df0" stroke-width="1.2"/>
<text x="508" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">FOCAL HUB</text>
<line x1="624" y1="614" x2="656" y2="614" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/>
<text x="668" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">BUILDS ON</text>
<rect x="792" y="608" width="20" height="12" rx="2" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<text x="820" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">PHASE ZONE</text>
</svg>
</div>
</body>
</html>
-244
View File
@@ -1,244 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 1472 648" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="dependency-spine-title dependency-spine-desc">
<title id="dependency-spine-title">LeetCode topic dependency spine</title>
<desc id="dependency-spine-desc">Dependency graph of 24 LeetCode study topics across five phases, showing which topics build on which, with Tree DFS and 1-D dynamic programming as the two central hubs.</desc>
<defs>
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Inter:wght@400;500;600&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;display=swap');</style>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#989898"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#449df0"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#6ab0f5"/></marker>
</defs>
<rect width="100%" height="100%" fill="#020202"/>
<!-- ============ zones (painted first) ============ -->
<rect x="40" y="40" width="368" height="312" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="56" y="44" width="168" height="12" rx="2" fill="#020202"/>
<text x="60" y="53" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASE I — LINEAR STRUCTURES</text>
<rect x="456" y="40" width="368" height="312" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="472" y="44" width="152" height="12" rx="2" fill="#020202"/>
<text x="476" y="53" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASE III — HIERARCHICAL</text>
<rect x="456" y="384" width="368" height="168" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="472" y="388" width="144" height="12" rx="2" fill="#020202"/>
<text x="476" y="397" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASE II — NODAL &amp; GRID</text>
<rect x="872" y="40" width="560" height="312" rx="8" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<rect x="888" y="44" width="264" height="12" rx="2" fill="#020202"/>
<text x="892" y="53" fill="rgba(242,242,242,0.40)" font-size="7" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">PHASES IV–V — RELATIONAL &amp; DECISION-SPACE</text>
<!-- ============ arrows (before boxes) ============ -->
<!-- same-row horizontals -->
<line x1="200" y1="168" x2="248" y2="168" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 3 → 4 -->
<line x1="200" y1="240" x2="248" y2="240" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 6 → 7 -->
<line x1="200" y1="312" x2="248" y2="312" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 1 → 2 -->
<line x1="616" y1="440" x2="664" y2="440" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 8 → 9 -->
<line x1="1032" y1="96" x2="1080" y2="96" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 16 → 18 -->
<line x1="1224" y1="240" x2="1272" y2="240" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 21 → 22 -->
<line x1="1224" y1="312" x2="1272" y2="312" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 23 → 24 -->
<line x1="616" y1="84" x2="888" y2="84" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 16 -->
<line x1="616" y1="312" x2="888" y2="312" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 12 → 17 -->
<!-- same-column verticals -->
<line x1="544" y1="120" x2="544" y2="144" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 13 -->
<line x1="960" y1="120" x2="960" y2="144" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 16 → 19 -->
<line x1="1152" y1="120" x2="1152" y2="144" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 18 → 20 -->
<line x1="1152" y1="192" x2="1152" y2="216" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 20 → 21 -->
<line x1="1152" y1="264" x2="1152" y2="288" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 21 → 23 -->
<!-- rounded right-angle elbows -->
<path d="M200,96 H416 Q424,96 424,104 V160 Q424,168 432,168 H472" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 5 → 13 -->
<path d="M616,96 H640 Q648,96 648,104 V160 Q648,168 656,168 H664" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 14 -->
<path d="M616,108 H624 Q632,108 632,116 V232 Q632,240 640,240 H664" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 11 → 15 -->
<path d="M392,184 H432 Q440,184 440,192 V504 Q440,512 448,512 H472" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 4 → 10 -->
<path d="M128,336 V560 Q128,568 136,568 H728 Q736,568 736,560 V464" fill="none" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/> <!-- 1 → 9 -->
<!-- ============ nodes ============ -->
<!-- 05 Binary Search — entry -->
<rect x="56" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="72" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="87" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">05</text>
<text x="128" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Binary Search</text>
<!-- 03 Two Pointers — entry -->
<rect x="56" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="144" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">03</text>
<text x="128" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Two Pointers</text>
<!-- 04 Sliding Window -->
<rect x="248" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="248" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="256" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="268" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">04</text>
<text x="320" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Sliding Window</text>
<!-- 06 Stack — entry -->
<rect x="56" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="216" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">06</text>
<text x="128" y="248" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Stack</text>
<!-- 07 Monotonic Stack -->
<rect x="248" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="248" y="216" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="256" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="268" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">07</text>
<text x="320" y="248" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Monotonic Stack</text>
<!-- 01 Hash-Based Lookup — entry -->
<rect x="56" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="56" y="288" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="64" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="76" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">01</text>
<text x="128" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Hash Lookup</text>
<!-- 02 Prefix Sum -->
<rect x="248" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="248" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="256" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="268" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">02</text>
<text x="320" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Prefix Sum</text>
<!-- 11 Tree DFS — FOCAL -->
<rect x="472" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="72" width="144" height="48" rx="6" fill="rgba(68,157,240,0.12)" stroke="#449df0" stroke-width="1.2"/>
<rect x="480" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(68,157,240,0.50)" stroke-width="0.8"/>
<text x="492" y="87" fill="#449df0" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">11</text>
<text x="544" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Tree DFS</text>
<!-- 13 Binary Search Tree -->
<rect x="472" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="480" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">13</text>
<text x="544" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Binary Search Tree</text>
<!-- 14 Heap -->
<rect x="664" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="664" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="672" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="684" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">14</text>
<text x="736" y="170" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Heap</text>
<text x="736" y="184" fill="#989898" font-size="9" font-family="'IBM Plex Mono', monospace" text-anchor="middle">priority queue</text>
<!-- 15 Trie -->
<rect x="664" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="664" y="216" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="672" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="684" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">15</text>
<text x="736" y="248" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Trie</text>
<!-- 12 Tree BFS — entry -->
<rect x="472" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="288" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="480" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">12</text>
<text x="544" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Tree BFS</text>
<!-- 08 Linked List — entry -->
<rect x="472" y="416" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="416" width="144" height="48" rx="6" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<rect x="480" y="422" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="431" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">08</text>
<text x="544" y="448" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Linked List</text>
<!-- 09 Hybrid Structures -->
<rect x="664" y="416" width="144" height="48" rx="6" fill="#020202"/>
<rect x="664" y="416" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="672" y="422" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="684" y="431" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">09</text>
<text x="736" y="448" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Hybrid Structures</text>
<!-- 10 Matrix Index Math -->
<rect x="472" y="488" width="144" height="48" rx="6" fill="#020202"/>
<rect x="472" y="488" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="480" y="494" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="492" y="503" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">10</text>
<text x="544" y="520" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Matrix Index Math</text>
<!-- 16 Graph DFS -->
<rect x="888" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="888" y="72" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="896" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="908" y="87" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">16</text>
<text x="960" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Graph DFS</text>
<!-- 18 Topological Sort -->
<rect x="1080" y="72" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="72" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1088" y="78" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1100" y="87" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">18</text>
<text x="1152" y="104" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Topological Sort</text>
<!-- 19 Union-Find -->
<rect x="888" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="888" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="896" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="908" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">19</text>
<text x="960" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Union-Find</text>
<!-- 20 Backtracking -->
<rect x="1080" y="144" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="144" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1088" y="150" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1100" y="159" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">20</text>
<text x="1152" y="176" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Backtracking</text>
<!-- 21 1-D DP — FOCAL -->
<rect x="1080" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="216" width="144" height="48" rx="6" fill="rgba(68,157,240,0.12)" stroke="#449df0" stroke-width="1.2"/>
<rect x="1088" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(68,157,240,0.50)" stroke-width="0.8"/>
<text x="1100" y="231" fill="#449df0" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">21</text>
<text x="1152" y="242" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">1-D DP</text>
<text x="1152" y="256" fill="#989898" font-size="9" font-family="'IBM Plex Mono', monospace" text-anchor="middle">dynamic programming</text>
<!-- 22 Multi-D / Grid DP -->
<rect x="1272" y="216" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1272" y="216" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1280" y="222" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1292" y="231" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">22</text>
<text x="1344" y="242" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Multi-D / Grid DP</text>
<!-- 17 Graph BFS -->
<rect x="888" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="888" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="896" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="908" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">17</text>
<text x="960" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Graph BFS</text>
<!-- 23 Greedy -->
<rect x="1080" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1080" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1088" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1100" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">23</text>
<text x="1152" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Greedy</text>
<!-- 24 Intervals -->
<rect x="1272" y="288" width="144" height="48" rx="6" fill="#020202"/>
<rect x="1272" y="288" width="144" height="48" rx="6" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<rect x="1280" y="294" width="24" height="12" rx="2" fill="transparent" stroke="rgba(242,242,242,0.30)" stroke-width="0.8"/>
<text x="1292" y="303" fill="#989898" font-size="7" font-family="'IBM Plex Mono', monospace" text-anchor="middle" letter-spacing="0.08em">24</text>
<text x="1344" y="320" fill="#f2f2f2" font-size="12" font-weight="600" font-family="'Inter', sans-serif" text-anchor="middle">Intervals</text>
<!-- ============ legend ============ -->
<line x1="40" y1="600" x2="1432" y2="600" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<text x="40" y="620" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.14em">LEGEND</text>
<rect x="160" y="608" width="20" height="12" rx="2" fill="rgba(152,152,152,0.10)" stroke="#6f6f6f" stroke-width="1"/>
<text x="188" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">ENTRY — NO PREREQS</text>
<rect x="360" y="608" width="20" height="12" rx="2" fill="#0d0d0d" stroke="#f2f2f2" stroke-width="1"/>
<text x="388" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">TOPIC</text>
<rect x="480" y="608" width="20" height="12" rx="2" fill="rgba(68,157,240,0.12)" stroke="#449df0" stroke-width="1.2"/>
<text x="508" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">FOCAL HUB</text>
<line x1="624" y1="614" x2="656" y2="614" stroke="#989898" stroke-width="1" marker-end="url(#arrow)"/>
<text x="668" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">BUILDS ON</text>
<rect x="792" y="608" width="20" height="12" rx="2" fill="rgba(242,242,242,0.02)" stroke="rgba(242,242,242,0.10)" stroke-width="0.8"/>
<text x="820" y="618" fill="#989898" font-size="8" font-family="'IBM Plex Mono', monospace" letter-spacing="0.06em">PHASE ZONE</text>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB