From 32ab38b3024dc96133effd29d64be68c16119636 Mon Sep 17 00:00:00 2001
From: enixCode <58286681+enixCode@users.noreply.github.com>
Date: Tue, 2 Jun 2026 23:45:59 +0200
Subject: [PATCH] docs(website): add animated architecture diagrams
Custom SVG/React figures in the light-* design (icy-blue on near-black,
Fraunces + JetBrains Mono), framed as diagram panels so they read the same
in light and dark Fumadocs themes. Pure CSS animation, no client JS, so they
survive static export, and honour prefers-reduced-motion.
- Architecture: the 3-tier delegation chain (light-process -> light-run ->
light-runner -> Docker) with a request token down and a result token up.
Embedded in the Overview and Execution pages.
- Dag: the flagship workflow graph, fan-out plus a conditional link. Embedded
in Workflows.
- NetworkServices: the proxy/secret pattern, the key stays in the proxy and
the workload reaches it by hostname. Embedded in the Services section.
build with cc
---
website/content/docs/docker.mdx | 2 +
website/content/docs/index.mdx | 2 +
website/content/docs/workflows.mdx | 4 +
.../src/components/diagrams/Architecture.tsx | 109 +++++++
website/src/components/diagrams/Dag.tsx | 106 +++++++
.../components/diagrams/NetworkServices.tsx | 104 +++++++
.../components/diagrams/diagrams.module.css | 266 ++++++++++++++++++
website/src/components/diagrams/index.ts | 3 +
website/src/components/mdx.tsx | 4 +
9 files changed, 600 insertions(+)
create mode 100644 website/src/components/diagrams/Architecture.tsx
create mode 100644 website/src/components/diagrams/Dag.tsx
create mode 100644 website/src/components/diagrams/NetworkServices.tsx
create mode 100644 website/src/components/diagrams/diagrams.module.css
create mode 100644 website/src/components/diagrams/index.ts
diff --git a/website/content/docs/docker.mdx b/website/content/docs/docker.mdx
index 0d31fd5..193a0f8 100644
--- a/website/content/docs/docker.mdx
+++ b/website/content/docs/docker.mdx
@@ -4,6 +4,8 @@ title: "Execution (light-run)"
light-process no longer talks to Docker directly. Every node runs inside a container spawned by the [light-run](https://github.com/enixCode/light-run) HTTP service.
+
+
## Configuration
```bash
diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx
index 13f22ef..5dc3b87 100644
--- a/website/content/docs/index.mdx
+++ b/website/content/docs/index.mdx
@@ -7,6 +7,8 @@ description: A DAG workflow engine that orchestrates code in Docker containers v
Execution itself is delegated: each node runs on a [light-run](https://github.com/enixCode/light-run) instance over HTTP, which in turn drives [light-runner](https://github.com/enixCode/light-runner) and Docker. light-process never touches Docker directly; it owns orchestration, identity, and structured results.
+
+
## Where to start
- **[Getting started](/docs/getting-started)** - install, point at a light-run instance, run the example.
diff --git a/website/content/docs/workflows.mdx b/website/content/docs/workflows.mdx
index f02975c..07856d2 100644
--- a/website/content/docs/workflows.mdx
+++ b/website/content/docs/workflows.mdx
@@ -4,6 +4,8 @@ title: "Workflows"
A workflow is a directed acyclic graph (DAG) of nodes connected by links. Each node runs code in a Docker container.
+
+
## Folder structure
```
@@ -168,6 +170,8 @@ Isolation is topological: put each group of nodes on its own `networkDef` and th
A `service` is a long-running container started **before** the DAG and stopped **after** it, living outside the DAG flow (it is never a node and never an entry node). The canonical use is a network proxy that holds a provider API key so a workload node can reach another provider by hostname **without the secret ever entering the workload container**.
+
+
```json
{
"id": "claude-wf",
diff --git a/website/src/components/diagrams/Architecture.tsx b/website/src/components/diagrams/Architecture.tsx
new file mode 100644
index 0000000..b91ddf3
--- /dev/null
+++ b/website/src/components/diagrams/Architecture.tsx
@@ -0,0 +1,109 @@
+import styles from './diagrams.module.css';
+
+/*
+ * The strict top-down delegation chain:
+ * light-process (graph) -> light-run (HTTP) -> light-runner (Docker SDK) -> Docker
+ * A request token travels down the left rail, a result token back up the right.
+ * Pure CSS animation, no client JS - safe in static export.
+ */
+
+const LAYERS = [
+ { name: 'light-process', sub: 'DAG orchestrator - evaluates the graph', current: true },
+ { name: 'light-run', sub: 'HTTP wrapper - POST /run, serves artifacts' },
+ { name: 'light-runner', sub: 'Docker SDK - dockerode, extract, state' },
+ { name: 'Docker', sub: 'isolation boundary - the container runs here' },
+];
+
+const ROW_H = 52;
+const GAP = 18;
+const TOP = 16;
+const BOX_X = 150;
+const BOX_W = 372;
+
+export function Architecture() {
+ const height = TOP * 2 + LAYERS.length * ROW_H + (LAYERS.length - 1) * GAP;
+ const railTop = TOP + ROW_H / 2;
+ const railBottom = TOP + (LAYERS.length - 1) * (ROW_H + GAP) + ROW_H / 2;
+ const travel = railBottom - railTop;
+
+ return (
+
+
+
+ Fig. - one request, three layers, one isolation boundary.
+ no upward coupling
+
+
+ );
+}
diff --git a/website/src/components/diagrams/Dag.tsx b/website/src/components/diagrams/Dag.tsx
new file mode 100644
index 0000000..36efef1
--- /dev/null
+++ b/website/src/components/diagrams/Dag.tsx
@@ -0,0 +1,106 @@
+import styles from './diagrams.module.css';
+
+/*
+ * The DAG model: entry node fans out into a parallel batch; links carry output
+ * to the next node, optionally gated by a `when` condition. Live edges animate
+ * a flowing dash; the conditional edge is labelled with its predicate. Pure CSS,
+ * no client JS - safe in static export. Reused (scaled) as the landing hero.
+ */
+
+type NodeDef = {
+ id: string;
+ x: number;
+ y: number;
+ label: string;
+ sub: string;
+ cls?: string;
+ pulse?: string;
+};
+
+const W = 124;
+const H = 46;
+
+const NODES: NodeDef[] = [
+ { id: 'fetch', x: 40, y: 110, label: 'fetch', sub: 'node:24', pulse: 'pulse' },
+ { id: 'parse', x: 286, y: 40, label: 'parse', sub: 'python', pulse: 'pulseB' },
+ { id: 'store', x: 286, y: 180, label: 'store', sub: 'node:24', pulse: 'pulseC' },
+ { id: 'report', x: 516, y: 40, label: 'report', sub: 'python', pulse: 'pulseB' },
+];
+
+function center(n: NodeDef) {
+ return { cx: n.x + W / 2, cy: n.y + H / 2 };
+}
+
+export function Dag() {
+ const byId = Object.fromEntries(NODES.map((n) => [n.id, n]));
+ const edge = (from: string, to: string) => {
+ const a = center(byId[from]);
+ const b = center(byId[to]);
+ const x1 = byId[from].x + W;
+ const y1 = a.cy;
+ const x2 = byId[to].x;
+ const y2 = b.cy;
+ const mx = (x1 + x2) / 2;
+ return `M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}`;
+ };
+
+ return (
+
+
+
+ Fig. - nodes fan out; a link fires only when its condition holds.
+ data + conditions
+
+
+ );
+}
diff --git a/website/src/components/diagrams/NetworkServices.tsx b/website/src/components/diagrams/NetworkServices.tsx
new file mode 100644
index 0000000..8903a0a
--- /dev/null
+++ b/website/src/components/diagrams/NetworkServices.tsx
@@ -0,0 +1,104 @@
+import styles from './diagrams.module.css';
+
+/*
+ * The networks/services proxy pattern. A run-scoped Docker network
+ * (`lp--svc-net`) holds the workload and a sidecar proxy service. The
+ * secret (API key) lives only in the proxy: the workload reaches it by
+ * hostname, no credential in the workload's env. The proxy is the single hop
+ * that attaches the key on the way out to the external API. Pure CSS animation,
+ * no client JS - safe in static export.
+ */
+
+const BOX = { x: 20, y: 52, w: 432, h: 140 };
+
+const WORKLOAD = { x: 44, y: 100, w: 150, h: 54 };
+const PROXY = { x: 258, y: 100, w: 150, h: 54 };
+const EXTERNAL = { x: 512, y: 100, w: 148, h: 54 };
+
+function rightMid(n: { x: number; y: number; w: number; h: number }) {
+ return { x: n.x + n.w, y: n.y + n.h / 2 };
+}
+function leftMid(n: { x: number; y: number; w: number; h: number }) {
+ return { x: n.x, y: n.y + n.h / 2 };
+}
+
+export function NetworkServices() {
+ const a = rightMid(WORKLOAD);
+ const b = leftMid(PROXY);
+ const c = rightMid(PROXY);
+ const d = leftMid(EXTERNAL);
+
+ return (
+
+
+
+ Fig. - the workload reaches the API by hostname; the key never leaves the proxy.
+ secret isolation
+
+
+ );
+}
diff --git a/website/src/components/diagrams/diagrams.module.css b/website/src/components/diagrams/diagrams.module.css
new file mode 100644
index 0000000..0101819
--- /dev/null
+++ b/website/src/components/diagrams/diagrams.module.css
@@ -0,0 +1,266 @@
+/*
+ * Self-contained design tokens + animations for the light-process docs
+ * diagrams. The docs site (Fumadocs) runs a neutral/blue theme; these figures
+ * deliberately carry the light-* landing aesthetic (near-black canvas, ice-blue
+ * accent, Fraunces/JetBrains Mono voice) as a framed "diagram panel", so they
+ * read the same in light and dark docs. Tokens are hardcoded here on purpose:
+ * the landing CSS variables are not in scope inside the docs.
+ */
+
+.figure {
+ --d-ink: #050608;
+ --d-ink-2: #0a0c10;
+ --d-line: #1a1d24;
+ --d-line-hot: #242933;
+ --d-paper: #edeef0;
+ --d-paper-dim: #8a8f9a;
+ --d-paper-dimmer: #4d525c;
+ --d-halo: #c7e8ff;
+ --d-halo-soft: #7fddff;
+ --d-amber: #ffb855;
+ --d-ember: #ff6b35;
+
+ position: relative;
+ margin: 1.75rem 0;
+ padding: 1.4rem 1.4rem 1rem;
+ background: radial-gradient(120% 140% at 50% 0%, var(--d-ink-2), var(--d-ink) 70%);
+ border: 1px solid var(--d-line);
+ border-radius: 3px;
+ overflow: hidden;
+ color: var(--d-paper);
+ font-family:
+ 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
+}
+
+/* Corner brackets, the landing's signature motif. */
+.figure::before,
+.figure::after {
+ content: '';
+ position: absolute;
+ width: 14px;
+ height: 14px;
+ border: 1px solid var(--d-halo-soft);
+ opacity: 0.5;
+ pointer-events: none;
+}
+.figure::before {
+ top: 8px;
+ left: 8px;
+ border-right: 0;
+ border-bottom: 0;
+}
+.figure::after {
+ bottom: 8px;
+ right: 8px;
+ border-left: 0;
+ border-top: 0;
+}
+
+.svg {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.caption {
+ margin-top: 0.9rem;
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ font-size: 10.5px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--d-paper-dimmer);
+}
+.captionRight {
+ color: var(--d-halo-soft);
+ opacity: 0.7;
+}
+
+/* --- SVG primitives (applied via className on SVG elements) --- */
+
+.panel {
+ fill: var(--d-ink-2);
+ stroke: var(--d-line-hot);
+ stroke-width: 1;
+}
+.panelDashed {
+ fill: rgba(199, 232, 255, 0.03);
+ stroke: var(--d-halo-soft);
+ stroke-width: 1;
+ stroke-dasharray: 5 4;
+ opacity: 0.8;
+}
+.node {
+ fill: var(--d-ink-2);
+ stroke: var(--d-halo);
+ stroke-width: 1.25;
+}
+.nodeMuted {
+ fill: var(--d-ink-2);
+ stroke: var(--d-line-hot);
+ stroke-width: 1.25;
+}
+.nodeWarm {
+ fill: var(--d-ink-2);
+ stroke: var(--d-amber);
+ stroke-width: 1.25;
+}
+
+.label {
+ fill: var(--d-paper);
+ font-family: 'Fraunces', Georgia, serif;
+ font-style: italic;
+ font-size: 15px;
+}
+.labelMono {
+ fill: var(--d-paper);
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ font-size: 12px;
+}
+.sub {
+ fill: var(--d-paper-dim);
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ font-size: 10px;
+ letter-spacing: 0.04em;
+}
+.tag {
+ fill: var(--d-halo-soft);
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ font-size: 9.5px;
+ letter-spacing: 0.06em;
+}
+.tagWarm {
+ fill: var(--d-amber);
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ font-size: 9.5px;
+ letter-spacing: 0.06em;
+}
+
+.arrowLive {
+ fill: var(--d-halo);
+}
+.arrowMuted {
+ fill: var(--d-paper-dimmer);
+}
+.arrowWarm {
+ fill: var(--d-amber);
+}
+
+.edge {
+ fill: none;
+ stroke: var(--d-line-hot);
+ stroke-width: 1.5;
+}
+.edgeLive {
+ fill: none;
+ stroke: var(--d-halo);
+ stroke-width: 1.5;
+ stroke-dasharray: 4 5;
+ animation: flow 1.1s linear infinite;
+}
+.edgeWarm {
+ fill: none;
+ stroke: var(--d-amber);
+ stroke-width: 1.5;
+ stroke-dasharray: 4 5;
+ animation: flow 1.4s linear infinite;
+}
+
+/* A glowing pulse on a node outline. */
+.pulse {
+ animation: pulse 2.4s ease-in-out infinite;
+ transform-box: fill-box;
+ transform-origin: center;
+}
+.pulseB {
+ animation: pulse 2.4s ease-in-out infinite 0.8s;
+ transform-box: fill-box;
+ transform-origin: center;
+}
+.pulseC {
+ animation: pulse 2.4s ease-in-out infinite 1.6s;
+ transform-box: fill-box;
+ transform-origin: center;
+}
+
+/* A token that travels along a path (architecture rails / proxy hop). */
+.travelDown {
+ fill: var(--d-halo);
+ animation: travelDown 2.6s ease-in-out infinite;
+}
+.travelUp {
+ fill: var(--d-halo-soft);
+ animation: travelUp 2.6s ease-in-out infinite 1.3s;
+}
+.glowDot {
+ fill: var(--d-halo);
+ filter: drop-shadow(0 0 4px var(--d-halo-soft));
+}
+
+@keyframes flow {
+ to {
+ stroke-dashoffset: -18;
+ }
+}
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 0.55;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+@keyframes travelDown {
+ 0% {
+ opacity: 0;
+ transform: translateY(0);
+ }
+ 10% {
+ opacity: 1;
+ }
+ 90% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(var(--travel, 210px));
+ }
+}
+@keyframes travelUp {
+ 0% {
+ opacity: 0;
+ transform: translateY(0);
+ }
+ 10% {
+ opacity: 1;
+ }
+ 90% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(calc(-1 * var(--travel, 210px)));
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .edgeLive,
+ .edgeWarm,
+ .pulse,
+ .pulseB,
+ .pulseC,
+ .travelDown,
+ .travelUp {
+ animation: none;
+ }
+ .edgeLive,
+ .edgeWarm {
+ stroke-dasharray: 4 5;
+ }
+ .travelDown,
+ .travelUp {
+ opacity: 0;
+ }
+}
diff --git a/website/src/components/diagrams/index.ts b/website/src/components/diagrams/index.ts
new file mode 100644
index 0000000..a6ad7d3
--- /dev/null
+++ b/website/src/components/diagrams/index.ts
@@ -0,0 +1,3 @@
+export { Architecture } from './Architecture';
+export { Dag } from './Dag';
+export { NetworkServices } from './NetworkServices';
diff --git a/website/src/components/mdx.tsx b/website/src/components/mdx.tsx
index a640575..dc68724 100644
--- a/website/src/components/mdx.tsx
+++ b/website/src/components/mdx.tsx
@@ -1,9 +1,13 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
+import { Architecture, Dag, NetworkServices } from './diagrams';
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
+ Architecture,
+ Dag,
+ NetworkServices,
...components,
} satisfies MDXComponents;
}