Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 34 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
SHELL := /bin/bash

.PHONY: hooks
# Default docs port; override with: make docs PORT=5180
PORT ?= 5173

.PHONY: hooks docs docs-build docs-ci

hooks:
@git config core.hooksPath .githooks
@chmod +x .githooks/* 2>/dev/null || true
@echo "[hooks] Installed git hooks from .githooks (core.hooksPath)"

# Start VitePress dev server and open the browser automatically.
docs:
@echo "[docs] Ensuring deps..."
@npm install --silent
@echo "[docs] Starting VitePress on http://localhost:$(PORT) ..."
# Start server in background and record PID
@ (npm run --silent docs:dev -- --port $(PORT) &) ; \
server_pid=$$! ; \
echo "[docs] Waiting for server to become ready..." ; \
for i in {1..80}; do \
if curl -sSf "http://localhost:$(PORT)/" >/dev/null ; then \
echo "[docs] Server is up at http://localhost:$(PORT)/" ; \
scripts/docs-open.sh "http://localhost:$(PORT)/" ; \
wait $$server_pid ; \
exit 0 ; \
fi ; \
sleep 0.25 ; \
done ; \
echo "[docs] Timed out waiting for VitePress." ; \
exit 1
Comment on lines +13 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

From: Linus Torvalds torvalds@linux-foundation.org
To: flyingrobots
Cc: code-rabbit@vger.kernel.org

Subject: Re: [PATCH] Makefile: add docs target - BROKEN PID CAPTURE

What the hell is this subshell nonsense on line 19?

@ (npm run --silent docs:dev -- --port $(PORT) &) ; \
  server_pid=$$! ;

You're wrapping the backgrounded npm command in a subshell that runs synchronously and exits immediately. $$! will NOT capture the npm process PID—it'll either be empty or capture something completely unrelated. Then on line 26 you wait $$server_pid on a bogus PID, which will either fail immediately or wait forever on nothing.

This is broken. Just do:

@ npm run --silent docs:dev -- --port $(PORT) & \
  server_pid=$$! ;

No subshell. No nonsense. $$! captures the actual npm background job PID.

Second issue: If your polling loop times out (line 31), you print an error and exit 1, but you never kill the orphaned VitePress server that's still running in the background. Every time someone hits the timeout, they leak a server process. You need to trap the timeout and kill $$server_pid before exiting.

Third issue: Your static analysis is crying about an 18-line function body when it wants 5. I don't care about that warning—Make targets can be as long as they need to be—but I do care about the PID capture bug and the resource leak.

Fix the PID capture or this target is useless.

BunBun

Apply this diff:

 docs:
 	@echo "[docs] Ensuring deps..."
 	@npm install --silent
 	@echo "[docs] Starting VitePress on http://localhost:$(PORT) ..."
-	# Start server in background and record PID
-	@ (npm run --silent docs:dev -- --port $(PORT) &) ; \
+	@ npm run --silent docs:dev -- --port $(PORT) & \
 	  server_pid=$$! ; \
 	  echo "[docs] Waiting for server to become ready..." ; \
 	  for i in {1..80}; do \
 	    if curl -sSf "http://localhost:$(PORT)/" >/dev/null ; then \
 	      echo "[docs] Server is up at http://localhost:$(PORT)/" ; \
 	      scripts/docs-open.sh "http://localhost:$(PORT)/" ; \
 	      wait $$server_pid ; \
 	      exit 0 ; \
 	    fi ; \
 	    sleep 0.25 ; \
 	  done ; \
 	  echo "[docs] Timed out waiting for VitePress." ; \
+	  kill $$server_pid 2>/dev/null ; \
 	  exit 1
🧰 Tools
🪛 checkmake (0.2.2)

[warning] 14-14: Target body for "docs" exceeds allowed length of 5 (18).

(maxbodylength)

🤖 Prompt for AI Agents
Makefile lines 13–32: the current recipe launches npm in a subshell so $$! does
not capture the real VitePress PID and on timeout the server is left running;
remove the subshell so the backgrounded command and PID capture run in the same
shell (e.g. run the npm command with & then immediately set server_pid=$$!), add
a cleanup that kills that captured PID on timeout (and/or trap EXIT or ERR to
kill $$server_pid before exiting), and ensure all recipe lines that must share
state are joined with backslashes so $$server_pid is available to wait/kill
later.


# Build static site
docs-build:
@npm run --silent docs:build

# Build docs without installing dependencies (for CI caches)
docs-ci:
@echo "[docs] CI build (no npm install)"
@npm run --silent docs:build
24 changes: 24 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineConfig } from 'vitepress'

export default defineConfig({
title: 'Echo',
description: 'Deterministic, multiverse-aware ECS',
cleanUrls: true,
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'Collision Tour', link: '/guide/collision-tour' },
],
sidebar: {
'/guide/': [
{
text: 'Guide',
items: [
{ text: 'Collision Tour', link: '/guide/collision-tour' },
]
}
]
}
}
})

33 changes: 33 additions & 0 deletions docs/assets/collision/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Collision/CCD DPO Diagrams

- Base CSS: `diagrams.css`
- Static SVGs:
- `dpo_build_temporal_proxy.svg`
- `dpo_broad_phase_pairing.svg`
- `dpo_narrow_phase_discrete.svg`
- `dpo_narrow_phase_ccd.svg`
- `dpo_contact_events.svg`
- `dpo_gc_ephemeral.svg`
- `scheduler_phase_mapping.svg`
- `legend.svg`

Each SVG uses semantic classes:
- Nodes: `node`, variants: `interfaceK`, `added`, `removed`
- Edges: `edge`, variants: `added`, `removed`
- Other: `title`, `label`, `caption`, `scope`
- Optional animations: `pulse-add`, `pulse-remove`

## Animations
- Extend `diagrams.css` with your own `@keyframes` and apply classes to elements.
- For step-by-step tutorials, use the `_step1.svg`, `_step2.svg`, `_step3.svg` variants or overlay multiple SVGs in the docs site.
Comment on lines +20 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add blank line before heading.

You've got a heading smashed right up against the previous content. Markdown isn't assembly—we use whitespace for readability here.

Apply this diff:

 - Optional animations: `pulse-add`, `pulse-remove`
+
 ## Animations

BunBun

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
In docs/assets/collision/README.md around lines 20 to 22, the "## Animations"
heading is immediately adjacent to the previous paragraph with no blank line;
insert a single blank line before the heading so there is an empty line
separating the prior content and the "## Animations" heading to ensure proper
Markdown rendering and readability.


## Mermaid Sources
- Source `.mmd` files live alongside these SVGs. You can generate SVGs locally:

```
# Requires Node.js
npm i -g @mermaid-js/mermaid-cli
mmdc -i build_temporal_proxy.mmd -o build_temporal_proxy.svg
```

CI also compiles `.mmd` files to SVG artifacts (see `.github/workflows/ci.yml` job `diagrams`).
Comment on lines +24 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix heading spacing and code fence language.

Two issues here:

  1. Missing blank line before the heading (line 24)
  2. Fenced code block (line 27) needs a language identifier for syntax highlighting

The static analysis whining about "github" casing is a false positive—.github is a directory path, not the platform name.

Apply this diff:

 - Optional animations: `pulse-add`, `pulse-remove`
 
 ## Animations
 - Extend `diagrams.css` with your own `@keyframes` and apply classes to elements.
 - For step-by-step tutorials, use the `_step1.svg`, `_step2.svg`, `_step3.svg` variants or overlay multiple SVGs in the docs site.
+
 ## Mermaid Sources
 - Source `.mmd` files live alongside these SVGs. You can generate SVGs locally:
 
-```
+```bash
 # Requires Node.js
 npm i -g @mermaid-js/mermaid-cli
 mmdc -i build_temporal_proxy.mmd -o build_temporal_proxy.svg

BunBun

🧰 Tools
🪛 LanguageTool

[uncategorized] ~33-~33: The official name of this software platform is spelled with a capital “H”.
Context: ...iles .mmd files to SVG artifacts (see .github/workflows/ci.yml job diagrams).

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)

24-24: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In docs/assets/collision/README.md around lines 24 to 33, add a blank line
immediately before the "## Mermaid Sources" heading, change the fenced code
block to include a language identifier by making the opening fence ```bash (keep
the closing fence), and remove the stray "BunBun" text so only the intended bash
snippet remains.

154 changes: 154 additions & 0 deletions docs/assets/collision/animate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
(function(){
const els = Array.from(document.querySelectorAll('.fade-seed'));
if (!('IntersectionObserver' in window)) {
els.forEach(el => el.classList.add('fade-in'));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
io.unobserve(entry.target);
}
});
}, { rootMargin: '0px 0px -10% 0px', threshold: 0.1 });
els.forEach(el => io.observe(el));
})();

// Carousel pager for each rule's step-grid
(function(){
function buildPager(rule) {
const grid = rule.querySelector('.step-grid');
if (!grid) return;
const slides = Array.from(grid.querySelectorAll('figure'));
if (slides.length < 2) return;

// Build overlay captions with step counts from figcaptions
slides.forEach((fig, i) => {
const cap = fig.querySelector('figcaption');
const text = cap ? cap.textContent.trim() : '';
const ov = document.createElement('div');
ov.className = 'overlay';
ov.innerHTML = `<strong>Step ${i + 1} of ${slides.length}</strong><div class="ov-t">${text}</div>`;
fig.appendChild(ov);
fig.classList.add('has-overlay');
});
Comment on lines +30 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid innerHTML; keep it DOM-safe.

No reason to template HTML here. Build nodes; assign textContent. Safer and cheaper.

-      const ov = document.createElement('div');
-      ov.className = 'overlay';
-      ov.innerHTML = `<strong>Step ${i + 1} of ${slides.length}</strong><div class="ov-t">${text}</div>`;
-      fig.appendChild(ov);
+      const ov = document.createElement('div');
+      ov.className = 'overlay';
+      const strong = document.createElement('strong');
+      strong.textContent = `Step ${i + 1} of ${slides.length}`;
+      const ovT = document.createElement('div');
+      ovT.className = 'ov-t';
+      ovT.textContent = text;
+      ov.append(strong, ovT);
+      fig.appendChild(ov);
       fig.classList.add('has-overlay');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ov = document.createElement('div');
ov.className = 'overlay';
ov.innerHTML = `<strong>Step ${i + 1} of ${slides.length}</strong><div class="ov-t">${text}</div>`;
fig.appendChild(ov);
fig.classList.add('has-overlay');
});
const ov = document.createElement('div');
ov.className = 'overlay';
const strong = document.createElement('strong');
strong.textContent = `Step ${i + 1} of ${slides.length}`;
const ovT = document.createElement('div');
ovT.className = 'ov-t';
ovT.textContent = text;
ov.append(strong, ovT);
fig.appendChild(ov);
fig.classList.add('has-overlay');
});
🤖 Prompt for AI Agents
In docs/assets/collision/animate.js around lines 30 to 35, avoid using innerHTML
to inject the overlay markup; instead create the child elements via DOM APIs and
assign textContent for any text. Replace the template string with created
elements: make a strong element for the "Step X of Y" label, a div with class
"ov-t" for the text, set their textContent, append them to the ov div, then
append ov and add the 'has-overlay' class to fig. Ensure no raw HTML is passed
to innerHTML.


const nav = document.createElement('div');
nav.className = 'pager';
const prev = document.createElement('button');
prev.className = 'btn'; prev.textContent = '◀ Prev';
const next = document.createElement('button');
next.className = 'btn'; next.textContent = 'Next ▶';
const toggle = document.createElement('button');
toggle.className = 'btn'; toggle.textContent = 'Show all';
const world = document.createElement('button');
world.className = 'btn'; world.textContent = 'World view: On';
nav.append(prev, next, toggle, world);
grid.after(nav);

let mode = 'all'; // default to all frames visible
let idx = 0;

// Helpers to find neighbor rule blocks
const prevRule = (el) => { let r = el.previousElementSibling; while (r && !r.classList.contains('rule')) r = r.previousElementSibling; return r; };
const nextRule = (el) => { let r = el.nextElementSibling; while (r && !r.classList.contains('rule')) r = r.nextElementSibling; return r; };

function render() {
if (mode === 'all') {
slides.forEach(el => el.classList.remove('hidden'));
toggle.textContent = 'Carousel mode';
} else {
slides.forEach((el, i) => {
el.classList.toggle('hidden', i !== idx);
// ensure visible slide is faded in
if (!el.classList.contains('fade-in')) el.classList.add('fade-in');
});
toggle.textContent = 'Show all';
}
// Enable/disable edges. If at first slide and no previous rule, disable Prev.
// If at last slide and no next rule, disable Next.
if (mode === 'all') {
// Keep navigation enabled in 'all' mode so users/tests can enter carousel via Prev/Next.
prev.disabled = false; next.disabled = false;
} else {
Comment thread
flyingrobots marked this conversation as resolved.
const atFirst = idx === 0;
const atLast = idx === slides.length - 1;
prev.disabled = atFirst && !prevRule(rule);
next.disabled = atLast && !nextRule(rule);
}
}

prev.addEventListener('click', () => {
// If in all mode, enter carousel at first slide
if (mode === 'all') { mode = 'one'; idx = 0; render(); return; }
if (idx > 0) { idx -= 1; render(); return; }
// At first slide: navigate to previous rule, show its first slide
const pr = prevRule(rule);
if (pr && pr._pager) {
pr._pager.setIndex(0);
pr.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
render();
});
next.addEventListener('click', () => {
if (mode === 'all') { mode = 'one'; idx = 0; render(); return; }
if (idx < slides.length - 1) { idx += 1; render(); return; }
// At last slide: navigate to next rule, show its first slide
const nr = nextRule(rule);
if (nr && nr._pager) {
nr._pager.setIndex(0);
nr.scrollIntoView({ behavior: 'smooth', block: 'start' });
Comment on lines +87 to +101
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Prev should go to previous rule’s last slide, not its first.

Current behavior is jarring. Jumping to the previous rule should land on its last slide; mirror “Next → next rule first slide”.

-      if (pr && pr._pager) {
-        pr._pager.setIndex(0);
+      if (pr && pr._pager) {
+        pr._pager.setIndex(pr._pager.length - 1);
         pr.scrollIntoView({ behavior: 'smooth', block: 'start' });
       }

Expose length in the pager API:

   rule._pager = {
     setIndex: (i) => { mode = 'one'; idx = Math.max(0, Math.min(slides.length - 1, i)); render(); },
     setMode: (m) => { mode = m; render(); },
+    length: slides.length,
   };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In docs/assets/collision/animate.js around lines 87 to 101, the "prev" click
handler incorrectly sets the previous rule's pager index to 0; change it to set
the index to the last slide (use pr._pager.length - 1) so it mirrors the "next"
behavior. Expose a length property on the pager API (or a getter method) so
callers can read the number of slides; update pager creation to populate length
and ensure setIndex accepts that value. Replace pr._pager.setIndex(0) with
pr._pager.setIndex(pr._pager.length - 1) and add the length field to the pager
implementation.

}
Comment on lines +99 to +102
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Respect prefers-reduced-motion (optional).

Use instant scroll if users prefer reduced motion.

-        nr._pager.setIndex(0);
-        nr.scrollIntoView({ behavior: 'smooth', block: 'start' });
+        nr._pager.setIndex(0);
+        const smooth = !window.matchMedia || !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+        nr.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (nr && nr._pager) {
nr._pager.setIndex(0);
nr.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
if (nr && nr._pager) {
nr._pager.setIndex(0);
const smooth = !window.matchMedia || !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
nr.scrollIntoView({ behavior: smooth ? 'smooth' : 'auto', block: 'start' });
}
🤖 Prompt for AI Agents
In docs/assets/collision/animate.js around lines 99 to 102, the scrollIntoView
call always uses smooth scrolling; update it to respect users'
prefers-reduced-motion setting by detecting
window.matchMedia('(prefers-reduced-motion: reduce)').matches and, when true,
call scrollIntoView with behavior: 'auto' (or no behavior) so the scroll is
instant; keep smooth behavior only when the media query is false and ensure the
code gracefully falls back if matchMedia is unavailable.

render();
});
toggle.addEventListener('click', () => {
mode = (mode === 'all') ? 'one' : 'all';
render();
});

// Picture-in-picture container with tabs (World / Graph)
slides.forEach((fig) => {
const srcWorld = fig.getAttribute('data-pip');
const srcGraph = fig.getAttribute('data-graph');
if (!srcWorld && !srcGraph) return;
const wrap = document.createElement('div');
wrap.className = 'pip';
const tabs = document.createElement('div');
tabs.className = 'pip-tabs';
const tabWorld = document.createElement('div'); tabWorld.className = 'tab active'; tabWorld.textContent = 'World';
const tabGraph = document.createElement('div'); tabGraph.className = 'tab'; tabGraph.textContent = 'Graph';
tabs.append(tabWorld, tabGraph);
const imgWorld = document.createElement('img'); imgWorld.alt = 'World view'; if (srcWorld) imgWorld.src = srcWorld; else imgWorld.style.display='none';
const imgGraph = document.createElement('img'); imgGraph.alt = 'Graph view'; if (srcGraph) imgGraph.src = srcGraph; else imgGraph.style.display='none'; imgGraph.classList.add('hidden');
wrap.append(tabs, imgWorld, imgGraph);
fig.appendChild(wrap);
function show(which){
if (which==='world') { tabWorld.classList.add('active'); tabGraph.classList.remove('active'); imgWorld.classList.remove('hidden'); imgGraph.classList.add('hidden'); }
else { tabGraph.classList.add('active'); tabWorld.classList.remove('active'); imgGraph.classList.remove('hidden'); imgWorld.classList.add('hidden'); }
}
tabWorld.addEventListener('click', ()=>show('world'));
tabGraph.addEventListener('click', ()=>show('graph'));
});

let worldOn = true;
world.addEventListener('click', () => {
worldOn = !worldOn;
world.textContent = worldOn ? 'World view: On' : 'World view: Off';
slides.forEach(fig => {
const pip = fig.querySelector('.pip');
if (pip) pip.classList.toggle('hidden', !worldOn);
});
});

// Expose simple API for cross-rule navigation
rule._pager = {
setIndex: (i) => { mode = 'one'; idx = Math.max(0, Math.min(slides.length - 1, i)); render(); },
setMode: (m) => { mode = m; render(); },
};

render();
}

document.querySelectorAll('.rule').forEach(buildPager);
})();
17 changes: 17 additions & 0 deletions docs/assets/collision/broad_phase_pairing.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
flowchart LR
subgraph LHS
PA((TemporalProxy(a,n))):::KClass
PB((TemporalProxy(b,n))):::KClass
PA -- overlaps --> PB
end

subgraph RHS
PA2((TemporalProxy(a,n))):::KClass
PB2((TemporalProxy(b,n))):::KClass
PP[[PotentialPair(a,b,n)]]:::Add
PA2 -- pair_of --> PP
PB2 -- pair_of --> PP
end

classDef KClass stroke:#ffd166,stroke-width:2;
classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff;
19 changes: 19 additions & 0 deletions docs/assets/collision/build_temporal_proxy.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
flowchart LR
subgraph LHS
E[Collider(e)] --- T[Transform(e,n)]
V[Velocity(e)]
K[Tick(n)]
T -- at --> K
E -- produced_in --> K
end

subgraph RHS
E2[Collider(e)]:::KClass --- T2[Transform(e,n)]:::KClass
K2[Tick(n)]:::KClass
P((TemporalProxy(e,n))):::Add
E2 -- has_proxy --> P
P -- produced_in --> K2
end

classDef KClass stroke:#ffd166,stroke-width:2;
classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff;
12 changes: 12 additions & 0 deletions docs/assets/collision/contact_events.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
flowchart LR
subgraph LHS
C1((Contact(pair,n-1))):::KClass
C2((Contact(pair,n))):::KClass
end
subgraph RHS
E((ContactEvent(kind,pair,n))):::Add
E -- event_of --> C2
end

classDef KClass stroke:#ffd166,stroke-width:2;
classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff;
63 changes: 63 additions & 0 deletions docs/assets/collision/diagrams.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* Echo collision/CCD DPO diagrams — base styles and optional animations */
:root {
--bg: #0b1020;
--panel: #121a33;
--text: #dfe7ff;
--muted: #9fb0d6;
--edge: #6aa0ff;
--k: #ffd166;
--added: #00c853;
--removed: #ff5252;
--scope: #ffa600;
}

svg { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans', sans-serif; }
.panel { fill: var(--panel); rx: 8px; }
.title { fill: var(--text); font-weight: 600; font-size: 16px; }
.caption { fill: var(--muted); font-size: 12px; }
.node rect { fill: #182347; stroke: #3356a6; stroke-width: 1.2; rx: 6px; }
.node.interfaceK rect { fill: #2b2746; stroke: var(--k); stroke-width: 2; }
.node.added rect { fill: #0d2a1a; stroke: var(--added); stroke-width: 2; }
.node.removed rect { fill: #2a0d0d; stroke: var(--removed); stroke-width: 2; }
.node text { fill: var(--text); font-size: 12px; }
.edge line, .edge path { stroke: var(--edge); stroke-width: 1.6; fill: none; }
.edge.added line, .edge.added path { stroke: var(--added); stroke-width: 2; }
.edge.removed line, .edge.removed path { stroke: var(--removed); stroke-width: 2; }
.edge text { fill: var(--muted); font-size: 11px; }
.scope rect { fill: none; stroke: var(--scope); stroke-width: 2; stroke-dasharray: 6 4; rx: 8px; }
.label { fill: var(--text); font-size: 13px; }

/* Optional animation hooks */
.pulse-add { animation: pulseAdd 1.6s ease-in-out infinite; }
.pulse-remove { animation: pulseRemove 1.6s ease-in-out infinite; }
@keyframes pulseAdd { 0%{opacity:0.5} 50%{opacity:1} 100%{opacity:0.5} }
@keyframes pulseRemove { 0%{opacity:1} 50%{opacity:0.6} 100%{opacity:1} }

/* Fade-in on scroll (tour page) */
.fade-seed { opacity: 0; transform: translateY(12px); transition: opacity 400ms ease, transform 400ms ease; }
.fade-in { opacity: 1; transform: translateY(0); }

/* Carousel pager */
.pager { display: flex; gap: 8px; margin-top: 8px; }
.btn { background: #1b2446; color: var(--text); border: 1px solid #3356a6; border-radius: 6px; padding: 6px 10px; cursor: pointer; font-size: 12px; }
.btn:hover { background: #223060; }
.btn:disabled { opacity: 0.5; cursor: default; }
.hidden { display: none; }

/* Overlay caption inside figures */
.has-overlay figcaption { display: none; }
.overlay { position: absolute; left: 12px; right: 12px; bottom: 12px; background: rgba(18,26,51,0.85); border: 1px solid #3356a6; border-radius: 6px; padding: 8px 10px; backdrop-filter: blur(2px); }
.overlay strong { color: var(--text); font-size: 12px; letter-spacing: 0.2px; }
.overlay .ov-t { color: var(--muted); font-size: 12px; margin-top: 2px; }
.has-overlay { padding-bottom: 68px; }
.slide-explain { margin-top: 10px; font-size: 13px; color: var(--text); }
.slide-explain p { margin: 0 0 6px; }
.slide-explain ul { margin: 6px 0 0 18px; }
.slide-explain li { margin: 3px 0; color: var(--muted); }

/* Picture-in-picture world view */
.pip { position: absolute; right: 12px; top: 12px; width: 32%; max-width: 240px; border: 1px solid #3356a6; background: #0b1020; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.35); overflow: hidden; }
.pip img { width: 100%; display: block; }
.pip-tabs { display: flex; gap: 0; border-bottom: 1px solid #2a3c72; }
.pip-tabs .tab { flex: 1; text-align: center; font-size: 11px; color: var(--muted); padding: 6px 4px; cursor: pointer; background: #121a33; }
.pip-tabs .tab.active { color: var(--text); background: #1b2446; }
Comment on lines +1 to +63
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

From: Linus Torvalds torvalds@linux-foundation.org
To: flyingrobots
Cc: code-rabbit@vger.kernel.org

Subject: Re: [PATCH] docs: add collision diagram styles

The CSS is competent. You have a consistent color scheme, your animations aren't obnoxious, and you're not doing anything insane with z-indexes or position:absolute soup.

Minor gripe: Your .btn and .pip-tabs .tab have hover states (lines 43, 63) but no :focus states for keyboard navigation. If someone tabs through your UI with a keyboard, they get no visual feedback. Add outline or box-shadow on :focus or :focus-visible to fix this.

Not blocking the PR, but it's sloppy accessibility hygiene.

BunBun

Add focus states:

.btn:focus-visible {
  outline: 2px solid #6aa0ff;
  outline-offset: 2px;
}

.pip-tabs .tab:focus-visible {
  outline: 2px solid #6aa0ff;
  outline-offset: -2px;
}
🤖 Prompt for AI Agents
In docs/assets/collision/diagrams.css lines 1-63, the review notes missing
keyboard focus styles for interactive controls; add :focus-visible rules for
.btn and .pip-tabs .tab that provide a visible outline (use the same accent
color as edges, e.g. #6aa0ff or var(--edge)) and appropriate outline-offsets so
keyboard users receive clear visual feedback; ensure the focus styles match
hover/active aesthetics and do not change layout (prefer outline or box-shadow
rather than border changes).

Loading