Skip to content

Commit 2ac5387

Browse files
committed
✨ Redesign home page around the structured-concurrency arc
Rebuild the Effection home page from the new mockup: a problem→proof→ solution narrative for library authors. Replaces the logo + feature-grid landing page with beats covering the await-boundary leak, the proven model, Effection's leak-proof rewrite, an interactive operation tree, three superpowers, the GC analogy, an async/await rosetta, and a closing CTA. Ported to the site's own framework (revolution SSR + Tailwind), with dark variants throughout and syntax highlighting reusing the site's Prism theme. New pieces: - components/code-window.tsx — macOS-window code block, highlighted via refractor (the tokenizer rehype-prism-plus already uses) so colours match the rest of the site and follow the OS light/dark preference. - components/operation-tree.tsx + assets/home-islands.js — static SVG driven by a small vanilla-JS island that animates the halt↓/teardown↑ cascade. - components/install-copy.tsx — copy-to-clipboard install pill (same island). Brand colours use arbitrary hex values because the custom Tailwind color tokens (blue-primary/blue-secondary/pink-secondary) are not emitted by the current v4 build.
1 parent 4f400d0 commit 2ac5387

6 files changed

Lines changed: 907 additions & 200 deletions

File tree

www/assets/home-islands.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Home page interactivity — vanilla JS, no framework.
2+
// Drives (1) the OperationTree halt↓/teardown↑ cascade and (2) the install
3+
// command copy button. Both no-op gracefully when their DOM isn't present.
4+
5+
const NAVY = "#14315d";
6+
const GREEN = "#16a34a";
7+
const AMBER = "#b45309";
8+
const IDLE_EDGE = "#cbd5e1";
9+
10+
function initOperationTree() {
11+
let root = document.getElementById("operation-tree");
12+
let haltBtn = document.getElementById("operation-tree-halt");
13+
let resetBtn = document.getElementById("operation-tree-reset");
14+
if (!root || !haltBtn || !resetBtn) return;
15+
16+
let nodes = Array.from(root.querySelectorAll("[data-node]"));
17+
let edges = Array.from(root.querySelectorAll("[data-edge]"));
18+
let state = {}; // id -> "halting" | "done"
19+
let timers = [];
20+
let running = false;
21+
22+
function nodeFill(st) {
23+
if (st === "done") return "#fffbeb";
24+
if (st === "halting") return "#f0fdf4";
25+
return "#fff";
26+
}
27+
function nodeStroke(st) {
28+
if (st === "done") return AMBER;
29+
if (st === "halting") return GREEN;
30+
return NAVY;
31+
}
32+
function edgeColor(st) {
33+
if (st === "done") return AMBER;
34+
if (st === "halting") return GREEN;
35+
return IDLE_EDGE;
36+
}
37+
38+
function render() {
39+
for (let g of nodes) {
40+
let st = state[g.getAttribute("data-node")];
41+
let rect = g.querySelector("[data-rect]");
42+
let label = g.querySelector("[data-label]");
43+
let done = g.querySelector("[data-done-label]");
44+
if (rect) {
45+
rect.setAttribute("fill", nodeFill(st));
46+
rect.setAttribute("stroke", nodeStroke(st));
47+
}
48+
if (label) label.setAttribute("fill", st === "done" ? AMBER : NAVY);
49+
if (done) done.setAttribute("opacity", st === "done" ? "1" : "0");
50+
g.setAttribute("opacity", st === "done" ? "0.7" : "1");
51+
}
52+
for (let line of edges) {
53+
line.setAttribute(
54+
"stroke",
55+
edgeColor(state[line.getAttribute("data-child")]),
56+
);
57+
}
58+
}
59+
60+
function reset() {
61+
timers.forEach(clearTimeout);
62+
timers = [];
63+
state = {};
64+
running = false;
65+
haltBtn.disabled = false;
66+
render();
67+
}
68+
69+
function halt() {
70+
if (running) return;
71+
running = true;
72+
haltBtn.disabled = true;
73+
74+
// 1) halt signal propagates DOWN the tree, fast, reaching every operation
75+
let haltAt = {
76+
main: 450,
77+
server: 850,
78+
watch: 850,
79+
sock: 1250,
80+
db: 1250,
81+
fs: 1250,
82+
};
83+
// 2) teardown COMPLETES bottom-up, independently per branch — a parent
84+
// only completes once ALL of its own children have
85+
let doneAt = {
86+
sock: 2150,
87+
db: 2650,
88+
fs: 2350,
89+
server: 3500,
90+
watch: 3150,
91+
main: 4300,
92+
};
93+
94+
for (let [id, t] of Object.entries(haltAt)) {
95+
timers.push(setTimeout(() => {
96+
if (state[id] !== "done") state[id] = "halting";
97+
render();
98+
}, t));
99+
}
100+
for (let [id, t] of Object.entries(doneAt)) {
101+
timers.push(setTimeout(() => {
102+
state[id] = "done";
103+
render();
104+
}, t));
105+
}
106+
timers.push(setTimeout(() => {
107+
running = false;
108+
haltBtn.disabled = false;
109+
}, 4700));
110+
}
111+
112+
haltBtn.addEventListener("click", halt);
113+
resetBtn.addEventListener("click", reset);
114+
}
115+
116+
function initCopyButtons() {
117+
for (let btn of document.querySelectorAll("[data-copy]")) {
118+
btn.addEventListener("click", async () => {
119+
let text = btn.getAttribute("data-copy");
120+
try {
121+
await navigator.clipboard.writeText(text);
122+
} catch (_) { /* clipboard unavailable */ }
123+
let label = btn.querySelector("[data-copy-label]") || btn;
124+
let prev = label.textContent;
125+
label.textContent = "copied";
126+
btn.setAttribute("data-copied", "1");
127+
setTimeout(() => {
128+
label.textContent = prev;
129+
btn.removeAttribute("data-copied");
130+
}, 1400);
131+
});
132+
}
133+
}
134+
135+
initOperationTree();
136+
initCopyButtons();

www/components/code-window.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { refractor } from "refractor/all";
2+
import type { JSXElement } from "revolution/jsx-runtime";
3+
4+
const LANGS: Record<string, string> = {
5+
ts: "typescript",
6+
tsx: "tsx",
7+
js: "javascript",
8+
jsx: "jsx",
9+
};
10+
11+
export interface CodeWindowProps {
12+
code: string;
13+
filename?: string;
14+
language?: string;
15+
}
16+
17+
/**
18+
* CodeWindow — a "macOS window" code block: a chrome bar with three
19+
* traffic-light dots and an optional filename, above a syntax-highlighted
20+
* `<pre>`. Highlighting is produced at render time with `refractor` (the same
21+
* tokenizer `rehype-prism-plus` uses across the site), so colours come from
22+
* `/assets/prism-atom-one-dark.css` and follow the OS light/dark preference.
23+
*/
24+
export function CodeWindow(
25+
{ code, filename, language = "ts" }: CodeWindowProps,
26+
): JSXElement {
27+
let grammar = LANGS[language] ?? "typescript";
28+
// refractor returns a hast tree of <span class="token …"> nodes, which is the
29+
// same shape revolution's JSX runtime renders — embed its children directly.
30+
let tokens = refractor.highlight(code.replace(/\n$/, ""), grammar)
31+
.children as unknown as JSXElement[];
32+
33+
return (
34+
<div class="overflow-hidden rounded-lg border border-gray-200 shadow-sm dark:border-gray-700">
35+
<div class="flex items-center gap-2 border-b border-gray-200 bg-gray-100 px-4 py-2.5 dark:border-gray-700 dark:bg-gray-800">
36+
<span class="flex gap-1.5">
37+
<span class="h-3 w-3 rounded-full bg-[#ff5f57]"></span>
38+
<span class="h-3 w-3 rounded-full bg-[#febc2e]"></span>
39+
<span class="h-3 w-3 rounded-full bg-[#28c840]"></span>
40+
</span>
41+
{filename
42+
? (
43+
<span class="ml-1 font-mono text-xs text-gray-500 dark:text-gray-400">
44+
{filename}
45+
</span>
46+
)
47+
: <></>}
48+
</div>
49+
<pre
50+
class={`language-${language} !m-0 !rounded-none overflow-x-auto p-4 text-[13.5px] leading-relaxed`}
51+
>
52+
<code class={`language-${language} code-highlight`}>{tokens}</code>
53+
</pre>
54+
</div>
55+
);
56+
}

www/components/install-copy.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// @ts-nocheck hastx's button `type` union omits "button"
2+
import type { JSXElement } from "revolution/jsx-runtime";
3+
4+
export interface InstallCopyProps {
5+
command?: string;
6+
}
7+
8+
/**
9+
* InstallCopy — a terminal-style install pill with a copy-to-clipboard button.
10+
* The button carries the command in `data-copy`; `/assets/home-islands.js`
11+
* wires the clipboard write and the "copied" feedback. Kept dark in both
12+
* themes since it reads as a terminal.
13+
*/
14+
export function InstallCopy(
15+
{ command = "npm install effection" }: InstallCopyProps,
16+
): JSXElement {
17+
return (
18+
<div class="inline-flex items-center gap-3.5 rounded-lg border border-white/10 bg-[#0f1b2d] px-4 py-3 pl-[18px] font-mono text-[15px]">
19+
<span class="select-none text-[#6b7c93]">$</span>
20+
<span class="text-[#e6edf6]">{command}</span>
21+
<button
22+
type="button"
23+
data-copy={command}
24+
aria-label="Copy install command"
25+
class="ml-1 inline-flex items-center gap-1.5 rounded border border-white/10 bg-white/[0.06] px-2.5 py-1 font-mono text-[12.5px] font-semibold text-[#8aa0b8] transition-colors data-[copied]:text-[#28c840]"
26+
>
27+
<span data-copy-label>copy</span>
28+
</button>
29+
</div>
30+
);
31+
}

www/components/operation-tree.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// @ts-nocheck hastx does not typecheck raw <svg> children
2+
import type { JSXElement } from "revolution/jsx-runtime";
3+
4+
interface Node {
5+
id: string;
6+
x: number;
7+
y: number;
8+
label: string;
9+
}
10+
11+
const NODES: Node[] = [
12+
{ id: "main", x: 300, y: 34, label: "entrypoint()" },
13+
{ id: "server", x: 150, y: 138, label: "startServer()" },
14+
{ id: "watch", x: 450, y: 138, label: "watchFiles()" },
15+
{ id: "sock", x: 70, y: 246, label: "socket" },
16+
{ id: "db", x: 230, y: 246, label: "db pool" },
17+
{ id: "fs", x: 450, y: 246, label: "fs handle" },
18+
];
19+
20+
const EDGES: [string, string][] = [
21+
["main", "server"],
22+
["main", "watch"],
23+
["server", "sock"],
24+
["server", "db"],
25+
["watch", "fs"],
26+
];
27+
28+
const NAVY = "#14315d";
29+
const IDLE_EDGE = "#cbd5e1";
30+
const byId = Object.fromEntries(NODES.map((n) => [n.id, n]));
31+
32+
/**
33+
* OperationTree — the interactive proof that halting forces control back and
34+
* teardown always runs. Rendered as static SVG server-side with stable
35+
* `data-*` hooks; `/assets/operation-tree.js` drives the halt↓ / teardown↑
36+
* cascade on click (no client framework).
37+
*/
38+
export function OperationTree(): JSXElement {
39+
return (
40+
<div id="operation-tree" class="mx-auto max-w-[620px]">
41+
<div class="rounded-lg border border-gray-200 bg-white px-2 pt-2 shadow-sm dark:border-gray-700">
42+
<svg
43+
viewBox="0 0 600 300"
44+
class="block h-auto w-full"
45+
font-family='"Fira Code", "Fira Mono", Menlo, Consolas, monospace'
46+
>
47+
<>
48+
{EDGES.map(([a, b]) => {
49+
let na = byId[a];
50+
let nb = byId[b];
51+
return (
52+
<line
53+
data-edge
54+
data-child={b}
55+
x1={na.x}
56+
y1={na.y + 16}
57+
x2={nb.x}
58+
y2={nb.y - 16}
59+
stroke={IDLE_EDGE}
60+
stroke-width={2}
61+
style="transition: stroke .3s"
62+
/>
63+
);
64+
})}
65+
</>
66+
<>
67+
{NODES.map((n) => {
68+
let w = Math.round(n.label.length * 8.2 + 34);
69+
return (
70+
<g
71+
data-node={n.id}
72+
style="transition: opacity .3s"
73+
>
74+
<rect
75+
data-rect
76+
x={n.x - w / 2}
77+
y={n.y - 16}
78+
width={w}
79+
height={32}
80+
rx={6}
81+
fill="#fff"
82+
stroke={NAVY}
83+
stroke-width={2}
84+
style="transition: fill .3s, stroke .3s"
85+
/>
86+
<text
87+
data-label
88+
x={n.x}
89+
y={n.y + 5}
90+
text-anchor="middle"
91+
font-size={13.5}
92+
fill={NAVY}
93+
style="transition: fill .3s"
94+
>
95+
{n.label}
96+
</text>
97+
<text
98+
data-done-label
99+
x={n.x + w / 2 + 6}
100+
y={n.y + 4}
101+
font-size={11}
102+
fill="#b45309"
103+
opacity={0}
104+
>
105+
✓ torn down
106+
</text>
107+
</g>
108+
);
109+
})}
110+
</>
111+
</svg>
112+
</div>
113+
114+
<div class="mt-3.5 flex items-center justify-center gap-5 text-xs text-gray-500 dark:text-gray-400">
115+
<span class="inline-flex items-center gap-2">
116+
<span class="h-2.5 w-2.5 rounded-[3px] border-2 border-[#16a34a] bg-[#f0fdf4]">
117+
</span>
118+
halt signal ↓
119+
</span>
120+
<span class="inline-flex items-center gap-2">
121+
<span class="h-2.5 w-2.5 rounded-[3px] border-2 border-[#b45309] bg-[#fffbeb]">
122+
</span>
123+
teardown ↑
124+
</span>
125+
</div>
126+
127+
<div class="mt-4 flex items-center justify-center gap-3">
128+
<button
129+
id="operation-tree-halt"
130+
type="button"
131+
class="rounded font-mono text-[13px] font-semibold text-white bg-blue-900 px-4 py-2.5 transition-colors hover:bg-blue-800 disabled:cursor-default disabled:bg-gray-400"
132+
>
133+
entrypoint.halt()
134+
</button>
135+
<button
136+
id="operation-tree-reset"
137+
type="button"
138+
class="rounded border-2 border-[#14315D] font-mono text-[13px] font-semibold text-[#14315D] px-3.5 py-1.5 transition-colors hover:bg-blue-50 dark:border-[#26ABE8] dark:text-[#26ABE8]"
139+
>
140+
reset
141+
</button>
142+
</div>
143+
144+
<p class="mt-3 text-center text-[13px] text-gray-500 dark:text-gray-400">
145+
The halt signal travels down the tree (green); teardown then completes
146+
bottom-up, rolling up each branch independently (amber) — a parent
147+
finishes only once all its children have, each <code>ensure</code>{" "}
148+
running as its operation exits.
149+
</p>
150+
</div>
151+
);
152+
}

www/deno.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
"octokit": "npm:octokit@4.0.3",
6262
"pagefind": "npm:pagefind@1.3.0",
6363
"path-to-regexp": "npm:path-to-regexp@8.2.0",
64+
"refractor": "npm:refractor@4.8.1",
65+
"refractor/all": "npm:refractor@4.8.1/lib/all.js",
6466
"rehype-add-classes": "npm:rehype-add-classes@1.0.0",
6567
"rehype-autolink-headings": "npm:rehype-autolink-headings@7.1.0",
6668
"rehype-infer-description-meta": "npm:rehype-infer-description-meta@2.0.0",

0 commit comments

Comments
 (0)