Redirecting…
++ This page moved to {to}. +
+diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 88633b05..b8aebedd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -44,7 +44,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Type check - run: pnpm types:check + run: pnpm typecheck - name: Lint run: pnpm lint @@ -54,11 +54,14 @@ jobs: DOCS_BASE_PATH: /taskito run: pnpm build + - name: SPA fallback for client-side routes + run: cp build/client/__spa-fallback.html build/client/404.html + - name: Upload Pages artifact if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' uses: actions/upload-pages-artifact@v5 with: - path: docs/out + path: docs/build/client deploy: needs: build diff --git a/docs/.gitignore b/docs/.gitignore index 9e429e49..8c074087 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -23,4 +23,7 @@ yarn-error.log* # others .env*.local .vercel -next-env.d.ts \ No newline at end of file +next-env.d.ts +# React Router +.react-router/ +build/ diff --git a/docs/README.md b/docs/README.md index d0aff640..35e28957 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,47 +1,49 @@ -# docs +# Taskito docs -This is a Next.js application generated with -[Create Fumadocs](https://github.com/fuma-nama/fumadocs). +The Taskito documentation site — a polyglot (Python + Node.js) docs site built with +[React Router v7](https://reactrouter.com) in framework mode, statically prerendered +to HTML for GitHub Pages. -It is a Next.js app with [Static Export](https://nextjs.org/docs/app/guides/static-exports) configured. +## Stack -Run development server: +- **React Router v7** (framework mode, `ssr: false` + `prerender`) — static export to + `build/client/`. +- **MDX** via `@mdx-js/rollup` — the content under `content/docs/**/*.mdx` is the source, + compiled at build and rendered through the component map in `app/components/mdx/`. +- **Shiki** for build-time code highlighting; **Mermaid** (client-side) for diagrams. +- **Tailwind v4** + a ported design system in `app/app.css` (dark + warm-light themes). +- **MiniSearch** for the client-side ⌘K search index. + +## Develop ```bash -npm run dev -# or -pnpm dev -# or -yarn dev +pnpm install +pnpm dev # local dev server +pnpm typecheck # react-router typegen + tsc +pnpm lint # biome +pnpm build # static prerender → build/client/ +pnpm start # serve the built output ``` -Open http://localhost:3000 with your browser to see the result. - -## Explore - -In the project, you can see: - -- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content. -- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep. - -| Route | Description | -| ------------------------- | ------------------------------------------------------ | -| `app/(home)` | The route group for your landing page and other pages. | -| `app/docs` | The documentation layout and pages. | -| `app/api/search/route.ts` | The Route Handler for search. | +Set `DOCS_BASE_PATH=/taskito` to build under the GitHub Pages base path (CI does this). -### Fumadocs MDX +## Layout -A `source.config.ts` config file has been included, you can customise different options like frontmatter schema. - -Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details. - -## Learn More - -To learn more about Next.js and Fumadocs, take a look at the following -resources: +``` +content/docs/**/*.mdx the documentation source (+ meta.json nav) +app/ + root.tsx document shell, fonts, no-flash theme + routes.ts route config (landing, llms.txt, docs catch-all) + routes/ home, docs-layout, docs.$ (MDX page), llms[.txt] + components/ + landing/ polyglot landing sections + docs/ sidebar, SDK switch, TOC, prev/next, search modal + mdx/ Callout/Tabs/Card shims + MDX component map + ui/ nav, theme toggle, RawHtml + animated/ diagram components (reused in MDX) + lib/ content glob, nav tree, search index, theme, highlighters +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js - features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs +Adding a page: drop an `.mdx` file under `content/docs/`, add it to the directory's +`meta.json`. It is picked up, prerendered, indexed for search, and slotted into the +sidebar automatically. diff --git a/docs/app/app.css b/docs/app/app.css new file mode 100644 index 00000000..c066aaee --- /dev/null +++ b/docs/app/app.css @@ -0,0 +1,12 @@ +@import "tailwindcss"; + +/* Design system, split by concern. tokens first — every module uses var(--*). */ +@import "./styles/tokens.css"; +@import "./styles/atoms.css"; +@import "./styles/cmdk.css"; +@import "./styles/docs.css"; +@import "./styles/changelog.css"; +@import "./styles/diagrams.css"; +@import "./styles/landing.css"; +@import "./styles/shiki.css"; +@import "./styles/sdk.css"; diff --git a/docs/app/components/diagrams/arch-stack.tsx b/docs/app/components/diagrams/arch-stack.tsx new file mode 100644 index 00000000..8030b5f9 --- /dev/null +++ b/docs/app/components/diagrams/arch-stack.tsx @@ -0,0 +1,166 @@ +import { type CSSProperties, Fragment, type ReactNode } from "react"; + +// Layered architecture stack — exact port of the prototype's `.archstack` HTML +// (semantic layers + PyO3/store boundaries as direct children). + +type Kind = "py" | "rust" | "store" | ""; + +interface Layer { + tag: string; + /** Pill colour: `.ltag t-*`. */ + tagKind?: Kind; + /** `.layer` modifier; defaults to `tagKind`. The prototype sometimes pairs a + * `rust` layer surface with a `py` pill (e.g. the resource-proxy layer). */ + layerKind?: Kind; + style?: CSSProperties; + title: string; + body: ReactNode; + role?: string; +} + +function ArchStack({ + layers, + bounds, + legend, +}: { + layers: Layer[]; + bounds: ReactNode[]; // one fewer than layers + legend?: ReactNode; +}) { + return ( +
Queue, @task, .delay(),
+ results, workflows, resources — the surface you write against.
+ >
+ ),
+ role: "what you write",
+ },
+ {
+ tag: "Rust",
+ tagKind: "rust",
+ title: "Engine",
+ body: "Scheduler, dispatcher, worker pool, rate limiter — Tokio runtime, OS-thread pool, near-zero Python overhead.",
+ role: "where the work runs",
+ },
+ {
+ tag: "Store",
+ tagKind: "store",
+ title: "Storage layer",
+ body: "SQLite (bundled) or Postgres — jobs, results, schedules, rate-limit state, all in one place.",
+ role: "where state lives",
+ },
+ ]}
+ bounds={[
+ <>
+ ↓
+ PyO3 boundary
+ ↑
+ >,
+ <>
+ ↓reads & writes
+ ↑
+ >,
+ ]}
+ />
+ );
+}
+
+/** `architecture/resources.mdx` — the 3-layer resource pipeline. */
+export function ResourcePipeline() {
+ return (
+ ArgumentInterceptor walks every argument before
+ serialization. CONVERT types become JSON-safe markers, REDIRECT
+ types become DI placeholders, PROXY types are deconstructed,
+ REJECT types raise in strict mode.
+ >
+ ),
+ },
+ {
+ tag: "Layer 2",
+ tagKind: "py",
+ title: "Worker resource runtime",
+ body: (
+ <>
+ ResourceRuntime initializes resources at worker
+ startup in topological order, then injects requested ones (via{" "}
+ inject= or Inject["name"]) as kwargs.
+ Task-scoped resources come from a semaphore pool.
+ >
+ ),
+ },
+ {
+ tag: "Layer 3",
+ tagKind: "py",
+ layerKind: "rust",
+ style: {
+ borderColor: "var(--indigo-line)",
+ background:
+ "linear-gradient(100deg,var(--indigo-soft),var(--panel))",
+ },
+ title: "Resource proxies",
+ body: (
+ <>
+ ProxyHandlers deconstruct live objects (file handles,
+ HTTP sessions, cloud clients) into a JSON recipe and reconstruct
+ them on the worker. Recipes are optionally HMAC-signed for tamper
+ detection.
+ >
+ ),
+ },
+ ]}
+ bounds={["enqueue → worker", "before task()"]}
+ />
+ );
+}
diff --git a/docs/app/components/diagrams/failure-timeline.tsx b/docs/app/components/diagrams/failure-timeline.tsx
new file mode 100644
index 00000000..322dd34a
--- /dev/null
+++ b/docs/app/components/diagrams/failure-timeline.tsx
@@ -0,0 +1,54 @@
+import type { ReactNode } from "react";
+
+// Crash-recovery timeline — ports the prototype's `.timeline` (`.tl`/`.tt`/`.tb`).
+// `architecture/failure-model.mdx`.
+const STEPS: { at: string; body: ReactNode }[] = [
+ {
+ at: "t = 0s",
+ body: (
+ <>
+ Worker claims a job — status pending → running,{" "}
+ started_at recorded.
+ >
+ ),
+ },
+ {
+ at: "t = crash",
+ body: (
+ <>
+ Worker dies mid-task. The row is left in running with no result.
+ >
+ ),
+ },
+ {
+ at: "t = timeout_ms",
+ body: (
+ <>
+ The stale reaper (runs ~every 5s) finds the job has exceeded{" "}
+ timeout_ms and marks it failed.
+ >
+ ),
+ },
+ {
+ at: "t + retry",
+ body: (
+ <>
+ If retries remain → re-enqueued as pending with backoff.
+ Otherwise → dead-letter.
+ >
+ ),
+ },
+];
+
+export function CrashRecoveryTimeline() {
+ return (
+ async def functions to the async runtime.
+ std::thread. The GIL is
+ acquired per task via Python::with_gil() —
+ independent across workers.
+ async def tasks are dispatched to an{" "}
+ AsyncTaskExecutor on a Python daemon thread;{" "}
+ PyResultSender bridges results back.
+ {pane.body}
+ github.com/ByteVeda/taskito +
+ A Rust-powered task queue with a first-class{" "}
+ {isNode ? "Node.js" : "Python"} SDK over one core and one store
+ — no broker. Start on SQLite, scale to{" "}
+ Postgres.
+
{lead}
: null} +| + | taskito | +Celery + Redis | +
|---|---|---|
| {d.label} | +
+ The quickstart walks you through defining a task, enqueuing it, and + watching the worker run it — in Python or Node, no Redis, no broker, + no config. +
+` in the design's `.codeblock` panel with a
+ * header bar (language label + copy button). Mapped as the MDX `pre` element. */
+export function CodeBlock(
+ props: ComponentProps<"pre"> & { "data-language"?: string },
+) {
+ const { children, ...preProps } = props;
+ const lang = props["data-language"] || "code";
+ const ref = useRef(null);
+ const [copied, setCopied] = useState(false);
+
+ const copy = () => {
+ const text = ref.current?.textContent ?? "";
+ navigator.clipboard?.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1400);
+ };
+
+ return (
+
+
+ {lang}
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/docs/app/components/mdx/index.tsx b/docs/app/components/mdx/index.tsx
new file mode 100644
index 00000000..4f859a3f
--- /dev/null
+++ b/docs/app/components/mdx/index.tsx
@@ -0,0 +1,49 @@
+import type { MDXComponents } from "mdx/types";
+import type { ComponentProps } from "react";
+import { Link } from "react-router";
+import * as Diagrams from "@/components/diagrams";
+import { Callout } from "./callout";
+import { Card, Cards } from "./card";
+import { CodeBlock } from "./code-block";
+import { CodeTabs, SdkLink, SdkOnly } from "./sdk";
+import { MdxTable } from "./table";
+import { Tab, Tabs } from "./tabs";
+
+// Internal links use the router for client navigation; external/anchor links
+// stay plain. Code blocks are highlighted at build time by rehype-shiki, and
+// headings/paragraphs are styled by element selectors inside `.article`.
+function Anchor({ href, children, ...rest }: ComponentProps<"a">) {
+ if (href?.startsWith("/")) {
+ return (
+
+ {children}
+
+ );
+ }
+ const external = href?.startsWith("http");
+ return (
+
+ {children}
+
+ );
+}
+
+/** Components made available to every MDX page via ``. */
+export const mdxComponents: MDXComponents = {
+ a: Anchor,
+ pre: CodeBlock,
+ table: MdxTable,
+ Callout,
+ Tabs,
+ Tab,
+ CodeTabs,
+ SdkOnly,
+ SdkLink,
+ Card,
+ Cards,
+ ...Diagrams,
+};
diff --git a/docs/app/components/mdx/sdk.tsx b/docs/app/components/mdx/sdk.tsx
new file mode 100644
index 00000000..5caa923e
--- /dev/null
+++ b/docs/app/components/mdx/sdk.tsx
@@ -0,0 +1,109 @@
+import {
+ Children,
+ type ComponentProps,
+ isValidElement,
+ type ReactElement,
+ type ReactNode,
+ useId,
+ useRef,
+} from "react";
+import { Link } from "react-router";
+import { type Sdk, useSdk } from "@/hooks";
+
+const LABELS: Record = { python: "Python", node: "Node.js" };
+
+/** Show children only under one SDK. All variants ship in the HTML; the inactive
+ * one is hidden by CSS off `` — no flash, no hydration mismatch. */
+export function SdkOnly({ sdk, children }: { sdk: Sdk; children: ReactNode }) {
+ return {children};
+}
+
+/** Internal link from a shared page into an SDK-specific tree. `to="guides/x"`
+ * resolves to `/{activeSdk}/guides/x` and re-points when the SDK switches. */
+export function SdkLink({
+ to,
+ children,
+ ...rest
+}: { to: string; children: ReactNode } & Omit<
+ ComponentProps,
+ "to" | "children"
+>) {
+ const { sdk } = useSdk();
+ const path = to.startsWith("/") ? to : `/${to}`;
+ return (
+
+ {children}
+
+ );
+}
+
+type TabChild = ReactElement<{ sdk: Sdk; children: ReactNode }>;
+
+/** Synced code/prose tabs: one panel per ``. Selecting a tab sets
+ * the global SDK, so every CodeTabs on the page and the sidebar switch together.
+ * Visibility is CSS-driven (data-sdk-variant); the tablist is APG-compliant. */
+export function CodeTabs({ children }: { children: ReactNode }) {
+ const { sdk, setSdk } = useSdk();
+ const baseId = useId();
+ const panels = Children.toArray(children).filter(
+ isValidElement,
+ ) as TabChild[];
+ const order = panels.map((p) => p.props.sdk);
+ const tabs = useRef<(HTMLButtonElement | null)[]>([]);
+ const tabId = (variant: Sdk) => `${baseId}-tab-${variant}`;
+ const panelId = (variant: Sdk) => `${baseId}-panel-${variant}`;
+
+ function onKeyDown(event: React.KeyboardEvent, index: number) {
+ const delta =
+ event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
+ if (delta === 0) {
+ return;
+ }
+ event.preventDefault();
+ const next = (index + delta + order.length) % order.length;
+ setSdk(order[next]);
+ tabs.current[next]?.focus();
+ }
+
+ return (
+
+
+ {order.map((variant, index) => {
+ const selected = variant === sdk;
+ return (
+
+ );
+ })}
+
+ {panels.map((panel) => (
+
+ {panel.props.children}
+
+ ))}
+
+ );
+}
diff --git a/docs/app/components/mdx/table.tsx b/docs/app/components/mdx/table.tsx
new file mode 100644
index 00000000..7421ba1b
--- /dev/null
+++ b/docs/app/components/mdx/table.tsx
@@ -0,0 +1,15 @@
+import type { ComponentProps } from "react";
+
+/**
+ * Wraps every Markdown/MDX `` in the prototype's `.md-table` shell so
+ * tables across the docs get the rounded surface, header band, and row rules
+ * from `docs.css`. Mapped onto the `table` element in `mdxComponents`, so it
+ * applies everywhere without touching individual pages.
+ */
+export function MdxTable(props: ComponentProps<"table">) {
+ return (
+
+
+
+ );
+}
diff --git a/docs/app/components/mdx/tabs.tsx b/docs/app/components/mdx/tabs.tsx
new file mode 100644
index 00000000..c5e98ab0
--- /dev/null
+++ b/docs/app/components/mdx/tabs.tsx
@@ -0,0 +1,64 @@
+import {
+ Children,
+ isValidElement,
+ type ReactElement,
+ type ReactNode,
+ useState,
+} from "react";
+
+/** Tab panel for both `` (keyed by `value`) and `` (keyed
+ * by `sdk`); a pass-through whose parent reads the relevant prop. */
+export function Tab({
+ children,
+}: {
+ value?: string;
+ sdk?: string;
+ children?: ReactNode;
+}) {
+ return <>{children}>;
+}
+
+export function Tabs({
+ items,
+ children,
+}: {
+ items: string[];
+ children: ReactNode;
+}) {
+ const [active, setActive] = useState(items[0] ?? "");
+ const panels = Children.toArray(children).filter(
+ isValidElement,
+ ) as ReactElement<{
+ value: string;
+ children: ReactNode;
+ }>[];
+
+ return (
+
+
+ {items.map((item) => (
+
+ ))}
+
+ {panels.map((panel) => (
+
+ {panel.props.children}
+
+ ))}
+
+ );
+}
diff --git a/docs/src/components/mermaid.tsx b/docs/app/components/mermaid.tsx
similarity index 98%
rename from docs/src/components/mermaid.tsx
rename to docs/app/components/mermaid.tsx
index ed173a77..4bb8e8c9 100644
--- a/docs/src/components/mermaid.tsx
+++ b/docs/app/components/mermaid.tsx
@@ -1,7 +1,5 @@
-"use client";
-
-import { useTheme } from "next-themes";
import { useEffect, useId, useRef, useState } from "react";
+import { useThemeMode } from "@/lib/theme";
const DARK_THEME_VARIABLES = {
darkMode: true,
@@ -121,7 +119,7 @@ const LIGHT_THEME_VARIABLES = {
export function Mermaid({ chart }: { chart: string }) {
const id = useId();
const containerRef = useRef(null);
- const { resolvedTheme } = useTheme();
+ const resolvedTheme = useThemeMode();
const [svg, setSvg] = useState("");
useEffect(() => {
diff --git a/docs/app/components/ui/index.ts b/docs/app/components/ui/index.ts
new file mode 100644
index 00000000..a363e6c1
--- /dev/null
+++ b/docs/app/components/ui/index.ts
@@ -0,0 +1,3 @@
+export { RawHtml } from "./raw-html";
+export { SiteNav } from "./site-nav";
+export { ThemeToggle } from "./theme-toggle";
diff --git a/docs/app/components/ui/raw-html.tsx b/docs/app/components/ui/raw-html.tsx
new file mode 100644
index 00000000..8c1084f6
--- /dev/null
+++ b/docs/app/components/ui/raw-html.tsx
@@ -0,0 +1,22 @@
+import type { ElementType } from "react";
+
+/** Render trusted HTML (our own syntax highlighter / static prototype copy).
+ * Single chokepoint for the dangerouslySetInnerHTML suppression. */
+export function RawHtml({
+ as,
+ html,
+ className,
+}: {
+ as?: ElementType;
+ html: string;
+ className?: string;
+}) {
+ const Tag = as ?? "div";
+ return (
+
+ );
+}
diff --git a/docs/app/components/ui/site-nav.tsx b/docs/app/components/ui/site-nav.tsx
new file mode 100644
index 00000000..0f8c4969
--- /dev/null
+++ b/docs/app/components/ui/site-nav.tsx
@@ -0,0 +1,64 @@
+import { Search } from "lucide-react";
+import { Link } from "react-router";
+import { ThemeToggle } from "@/components/ui/theme-toggle";
+
+// lucide dropped brand glyphs, so the GitHub mark is inlined.
+function GithubMark() {
+ return (
+
+ );
+}
+
+const LINKS = [
+ { label: "Getting Started", href: "/python/getting-started/installation" },
+ { label: "Guides", href: "/python/guides" },
+ { label: "Architecture", href: "/python/architecture" },
+ { label: "API", href: "/python/api-reference" },
+ { label: "Changelog", href: "/python/more/changelog" },
+];
+
+/** Sticky top navigation, shared by the landing and docs shells. */
+export function SiteNav({ onSearch }: { onSearch?: () => void }) {
+ return (
+
+ );
+}
diff --git a/docs/app/components/ui/theme-toggle.tsx b/docs/app/components/ui/theme-toggle.tsx
new file mode 100644
index 00000000..4f56ddc7
--- /dev/null
+++ b/docs/app/components/ui/theme-toggle.tsx
@@ -0,0 +1,17 @@
+import { Moon, Sun } from "lucide-react";
+import { toggleTheme } from "@/lib/theme";
+
+/** Nav theme switch. Icons are CSS-gated by `[data-theme]` (see app.css). */
+export function ThemeToggle() {
+ return (
+
+ );
+}
diff --git a/docs/app/hooks/index.ts b/docs/app/hooks/index.ts
new file mode 100644
index 00000000..73471711
--- /dev/null
+++ b/docs/app/hooks/index.ts
@@ -0,0 +1 @@
+export { type Sdk, useActiveSdk, useSdk } from "./use-sdk";
diff --git a/docs/app/hooks/use-sdk.ts b/docs/app/hooks/use-sdk.ts
new file mode 100644
index 00000000..2b9b9315
--- /dev/null
+++ b/docs/app/hooks/use-sdk.ts
@@ -0,0 +1,28 @@
+import { useSyncExternalStore } from "react";
+import { useLocation } from "react-router";
+import { forcedSdkForPath, type Sdk, sdkStore } from "@/lib";
+
+export type { Sdk };
+
+/** Reactive active SDK + setter, backed by the SSG-safe external store. The
+ * server snapshot is the default SDK, so prerendered HTML and the first client
+ * render agree; the boot script has already applied the real value to
+ * `` before paint. */
+export function useSdk(): { sdk: Sdk; setSdk: (sdk: Sdk) => void } {
+ const sdk = useSyncExternalStore(
+ sdkStore.subscribe,
+ sdkStore.getSnapshot,
+ sdkStore.getServerSnapshot,
+ );
+ return { sdk, setSdk: sdkStore.set };
+}
+
+/** The SDK to render for the current page: forced by the URL prefix on
+ * SDK-specific pages, otherwise the global store value (shared pages). Avoids a
+ * first-paint nav flash when a `/node/*` URL loads while the store still holds
+ * the default. */
+export function useActiveSdk(): Sdk {
+ const { sdk } = useSdk();
+ const { pathname } = useLocation();
+ return forcedSdkForPath(pathname.replace(/\/$/, "") || "/") ?? sdk;
+}
diff --git a/docs/src/lib/cn.ts b/docs/app/lib/cn.ts
similarity index 100%
rename from docs/src/lib/cn.ts
rename to docs/app/lib/cn.ts
diff --git a/docs/app/lib/content.ts b/docs/app/lib/content.ts
new file mode 100644
index 00000000..ac4e4529
--- /dev/null
+++ b/docs/app/lib/content.ts
@@ -0,0 +1,35 @@
+import type { ComponentType } from "react";
+
+interface MdxModule {
+ default: ComponentType;
+}
+
+// LAZY glob: each MDX compiles to its own chunk, loaded on demand by the route
+// (React.lazy). Page metadata lives in the build-time manifest (manifest.ts), so
+// nav/search never pull these heavy (shiki-inflated) modules into a shared chunk.
+const LOADERS = import.meta.glob("../../content/docs/**/*.mdx");
+
+function keyToSlug(key: string): string {
+ const rel = key.replace(/^.*\/content\/docs\//, "").replace(/\.mdx$/, "");
+ const parts = rel.split("/");
+ if (parts[parts.length - 1] === "index") {
+ parts.pop();
+ }
+ return `/${parts.join("/")}`.replace(/\/$/, "") || "/";
+}
+
+const BY_SLUG = new Map Promise>();
+for (const [key, loader] of Object.entries(LOADERS)) {
+ BY_SLUG.set(keyToSlug(key), loader);
+}
+
+/** The dynamic import for a doc page's compiled component, or undefined if unknown. */
+export function getDocLoader(
+ path: string,
+): (() => Promise) | undefined {
+ return BY_SLUG.get(path.replace(/\/$/, "") || "/");
+}
+
+export function allDocSlugs(): string[] {
+ return [...BY_SLUG.keys()];
+}
diff --git a/docs/app/lib/doc-paths.ts b/docs/app/lib/doc-paths.ts
new file mode 100644
index 00000000..233dee1f
--- /dev/null
+++ b/docs/app/lib/doc-paths.ts
@@ -0,0 +1,40 @@
+import { readdirSync } from "node:fs";
+import { join, relative, sep } from "node:path";
+import { fileURLToPath } from "node:url";
+
+// Filesystem walk of the MDX content tree, used by react-router.config's
+// `prerender()` to enumerate every static doc URL at build time. The runtime
+// router (app/lib/content.ts) maps the same files via import.meta.glob — both
+// derive slugs identically so the prerendered set matches the served routes.
+
+const CONTENT_DIR = fileURLToPath(
+ new URL("../../content/docs", import.meta.url),
+);
+
+/** `content/docs/a/b.mdx` → `/a/b`; `…/a/index.mdx` → `/a`. */
+export function fileToDocPath(absFile: string): string {
+ const rel = relative(CONTENT_DIR, absFile).replace(/\.mdx$/, "");
+ const parts = rel.split(sep);
+ if (parts[parts.length - 1] === "index") {
+ parts.pop();
+ }
+ return `/${parts.join("/")}`.replace(/\/$/, "") || "/";
+}
+
+function walk(dir: string, out: string[]): void {
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walk(full, out);
+ } else if (entry.name.endsWith(".mdx")) {
+ out.push(full);
+ }
+ }
+}
+
+/** Every doc URL (e.g. `/getting-started/installation`, `/node/workflows/saga`). */
+export function allDocPaths(): string[] {
+ const files: string[] = [];
+ walk(CONTENT_DIR, files);
+ return files.map(fileToDocPath).filter((p) => p !== "/");
+}
diff --git a/docs/app/lib/highlight-lite.ts b/docs/app/lib/highlight-lite.ts
new file mode 100644
index 00000000..35f7dfac
--- /dev/null
+++ b/docs/app/lib/highlight-lite.ts
@@ -0,0 +1,98 @@
+// Lightweight regex syntax highlighter ported from the prototype's landing.js.
+// Deterministic + synchronous, so it runs during prerender (HTML baked in) — no
+// async Shiki on the landing. Emits the same .kw/.str/.fn/.num/.cmt/.def token
+// classes the design system styles. Build-time Shiki still powers MDX code blocks.
+
+function escapeHtml(code: string): string {
+ return code
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+}
+
+const PY_KW = new Set([
+ "from",
+ "import",
+ "def",
+ "return",
+ "class",
+ "async",
+ "await",
+ "try",
+ "except",
+ "raise",
+ "if",
+ "else",
+ "for",
+ "in",
+ "with",
+ "as",
+ "None",
+ "True",
+ "False",
+ "self",
+]);
+
+const TS_KW = new Set([
+ "import",
+ "from",
+ "export",
+ "const",
+ "let",
+ "var",
+ "function",
+ "return",
+ "await",
+ "async",
+ "new",
+ "class",
+ "extends",
+ "if",
+ "else",
+ "for",
+ "of",
+ "in",
+ "try",
+ "catch",
+ "throw",
+ "typeof",
+ "number",
+ "string",
+ "boolean",
+ "void",
+ "null",
+ "undefined",
+ "true",
+ "false",
+]);
+
+function tokenize(code: string, kw: Set, lineComment: RegExp): string {
+ const escaped = escapeHtml(code);
+ const re =
+ /(\/\/[^\n]*|#[^\n]*)|(`(?:[^`\\]|\\.)*`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|(@[A-Za-z_][\w.]*)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_$]\w*)/g;
+ void lineComment;
+ return escaped.replace(
+ re,
+ (m, cmt, str, dec, num, ident, offset: number, full: string) => {
+ if (cmt != null) return `${cmt}`;
+ if (str != null) return `${str}`;
+ if (dec != null) return `${dec}`;
+ if (num != null) return `${num}`;
+ if (ident != null) {
+ if (kw.has(ident)) return `${ident}`;
+ if (/^\s*\(/.test(full.slice(offset + ident.length)))
+ return `${ident}`;
+ return ident;
+ }
+ return m;
+ },
+ );
+}
+
+export function highlightPython(code: string): string {
+ return tokenize(code, PY_KW, /#[^\n]*/);
+}
+
+export function highlightTs(code: string): string {
+ return tokenize(code, TS_KW, /\/\/[^\n]*/);
+}
diff --git a/docs/app/lib/index.ts b/docs/app/lib/index.ts
new file mode 100644
index 00000000..6f6530c7
--- /dev/null
+++ b/docs/app/lib/index.ts
@@ -0,0 +1,2 @@
+export * from "./nav";
+export * from "./sdk-store";
diff --git a/docs/app/lib/landing-content.ts b/docs/app/lib/landing-content.ts
new file mode 100644
index 00000000..d5a15e99
--- /dev/null
+++ b/docs/app/lib/landing-content.ts
@@ -0,0 +1,245 @@
+// Landing-page copy + code, ported verbatim from the prototype (index.html / landing.js).
+
+/** A worker-output line: glyph + text, optionally a result value + timing. */
+export interface OutLine {
+ glyph: string;
+ glyphKind: "p" | "g";
+ text: string;
+ value?: string;
+ timing?: string;
+}
+
+export interface LangPane {
+ id: "py" | "ts";
+ label: string;
+ filename: string;
+ install: string;
+ code: string;
+ output: OutLine[];
+ docHref: string;
+ docLabel: string;
+}
+
+export const HERO_PANES: LangPane[] = [
+ {
+ id: "py",
+ label: "Python",
+ filename: "tasks.py",
+ install: "pip install taskito",
+ code: `from taskito import Queue
+
+queue = Queue(db_path="tasks.db")
+
+@queue.task(max_retries=3)
+def add(a: int, b: int) -> int:
+ return a + b
+
+job = add.delay(2, 3)
+print(job.result()) # → 5`,
+ output: [
+ { glyph: "$", glyphKind: "p", text: "taskito worker --app tasks:queue" },
+ {
+ glyph: "→",
+ glyphKind: "p",
+ text: "scheduler online · 6 workers ready",
+ },
+ {
+ glyph: "✓",
+ glyphKind: "g",
+ text: "add(2, 3) =",
+ value: "5",
+ timing: "12 ms",
+ },
+ ],
+ docHref: "/python/getting-started/quickstart",
+ docLabel: "Read the Python quickstart",
+ },
+ {
+ id: "ts",
+ label: "TypeScript",
+ filename: "tasks.ts",
+ install: "pnpm add taskito",
+ code: `import { Queue } from "taskito";
+
+const queue = new Queue({ dbPath: "taskito.db" });
+
+queue.task("add", (a: number, b: number) => a + b, {
+ maxRetries: 3,
+});
+
+const id = queue.enqueue("add", [2, 3]);
+queue.runWorker();
+
+console.log(await queue.result(id)); // → 5`,
+ output: [
+ { glyph: "$", glyphKind: "p", text: "taskito run ./tasks.js" },
+ { glyph: "→", glyphKind: "p", text: "runWorker() · Rust core attached" },
+ {
+ glyph: "✓",
+ glyphKind: "g",
+ text: "add(2, 3) =",
+ value: "5",
+ timing: "9 ms",
+ },
+ ],
+ docHref: "/node/getting-started/quickstart",
+ docLabel: "Read the TypeScript quickstart",
+ },
+];
+
+/** Roadmap languages: disabled tab + "coming soon" panel (no fabricated SDK). */
+export interface SoonLang {
+ id: "go" | "java";
+ label: string;
+ heading: string;
+ body: string;
+}
+
+export const SOON_PANES: SoonLang[] = [
+ {
+ id: "go",
+ label: "Go",
+ heading: "Go client — coming soon",
+ body: "A native Go client for enqueuing and inspecting taskito jobs is on the roadmap.",
+ },
+ {
+ id: "java",
+ label: "Java",
+ heading: "Java client — coming soon",
+ body: "A JVM client for enqueuing taskito jobs from Java & Kotlin is planned.",
+ },
+];
+
+export interface IconCard {
+ icon: string;
+ rect?: boolean;
+ title: string;
+ body: string;
+}
+
+export const FEATURES: IconCard[] = [
+ {
+ icon: "M13 2L3 14h7l-1 8 10-12h-7l1-8z",
+ title: "Brokerless",
+ body: "No Redis, no RabbitMQ. Everything in a single SQLite file — queue, results, rate limits, schedules. Just pip install or pnpm add and go.",
+ },
+ {
+ icon: "M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3",
+ rect: true,
+ title: "Rust-powered",
+ body: "The scheduler, dispatcher, and storage engine are all Rust. Tokio runtime, OS-thread worker pool; thin PyO3 and napi-rs boundaries keep Python and Node overhead negligible.",
+ },
+ {
+ icon: "M22 12h-4l-3 9L9 3l-3 9H2",
+ title: "One core, two SDKs",
+ body: "First-class Python and Node.js clients are peers over the same Rust core and store — enqueue in one runtime, run workers in the other. Zero cross-language dependency.",
+ },
+ {
+ icon: "M6 3v12M18 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM15 6a9 9 0 0 0-9 9",
+ title: "DAG workflows",
+ body: "Multi-step pipelines as directed acyclic graphs. Fan-out, fan-in, conditions, approval gates, sub-workflows, incremental re-runs, Mermaid viz.",
+ },
+ {
+ icon: "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5",
+ title: "Resource system",
+ body: "Inject database connections, HTTP clients, and cloud SDKs by name. Three-layer pipeline: argument interception, worker DI, transparent proxy.",
+ },
+ {
+ icon: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
+ title: "Production-ready",
+ body: "Retries with exponential backoff, dead letter queue, rate limits, circuit breakers, distributed locks, structured logs, OTel/Sentry/Prometheus.",
+ },
+];
+
+export const USE_CASES: IconCard[] = [
+ {
+ icon: "M12 2C6.48 2 2 4.02 2 6.5v11C2 19.98 6.48 22 12 22s10-2.02 10-4.5v-11C22 4.02 17.52 2 12 2zM2 6.5C2 8.43 6.48 10 12 10s10-1.57 10-3.5M2 12c0 1.93 4.48 3.5 10 3.5s10-1.57 10-3.5",
+ title: "ETL pipelines",
+ body: "Chain extract → transform → load as a DAG. Fan out across workers, fan in to aggregate, restart from any node on failure.",
+ },
+ {
+ icon: "M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6",
+ title: "Email & notifications",
+ body: "Bursty SMTP, push, or webhook delivery. Per-task rate limits keep providers happy; retries with backoff handle transient failures.",
+ },
+ {
+ icon: "M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3",
+ rect: true,
+ title: "ML inference & batch",
+ body: "Long-running model jobs with progress tracking, soft timeouts, and prefork pools for true CPU parallelism without GIL contention.",
+ },
+ {
+ icon: "M12 6v6l4 2M12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20z",
+ title: "Scheduled jobs",
+ body: "Six-field cron syntax down to the second. Periodic tasks live in the scheduler — no separate beat daemon to babysit.",
+ },
+];
+
+export interface DeltaRow {
+ label: string;
+ taskito: string;
+ celery: string;
+}
+
+export const DELTA: DeltaRow[] = [
+ {
+ label: "Install",
+ taskito: "pip install taskito",
+ celery: "pip install celery[redis] + run Redis daemon",
+ },
+ {
+ label: "Background services",
+ taskito: "1 (worker)",
+ celery: "3 (worker, beat, Redis)",
+ },
+ {
+ label: "Default storage",
+ taskito: "SQLite file (built-in)",
+ celery: "Redis (separate daemon)",
+ },
+ {
+ label: "Retry config above",
+ taskito: "max_retries=3 decorator arg",
+ celery: "try/except + self.retry(exc=…)",
+ },
+];
+
+export interface IntegrationGroup {
+ group: string;
+ items: string[];
+}
+
+export const INTEGRATIONS: IntegrationGroup[] = [
+ { group: "Python frameworks", items: ["Django", "FastAPI", "Flask"] },
+ { group: "Node frameworks", items: ["Express", "Fastify", "NestJS"] },
+ { group: "Storage", items: ["Postgres", "SQLite", "Redis"] },
+ { group: "Observability", items: ["OpenTelemetry", "Sentry", "Prometheus"] },
+];
+
+export const CODE_TASKITO = `from taskito import Queue
+
+queue = Queue(db_path="tasks.db")
+
+@queue.task(max_retries=3, rate_limit="100/m")
+def send_email(to, subject, body):
+ smtp.send(to, subject, body)
+
+# Enqueue + run
+send_email.delay("a@x.com", "Hi", "Body")
+# $ taskito worker --app tasks:queue`;
+
+export const CODE_CELERY = `from celery import Celery
+
+app = Celery("myapp",
+ broker="redis://localhost:6379/0",
+ backend="redis://localhost:6379/1")
+app.conf.task_default_rate_limit = "100/m"
+
+@app.task(bind=True, max_retries=3)
+def send_email(self, to, subject, body):
+ try:
+ smtp.send(to, subject, body)
+ except SMTPError as exc:
+ raise self.retry(exc=exc, countdown=60)
+
+# $ celery -A myapp worker (+ Redis)`;
diff --git a/docs/app/lib/llms.ts b/docs/app/lib/llms.ts
new file mode 100644
index 00000000..155bb02e
--- /dev/null
+++ b/docs/app/lib/llms.ts
@@ -0,0 +1,18 @@
+import { SEARCH_DOCS } from "./search";
+
+/** Index of every doc page (title + URL) — the `/llms.txt` body. */
+export function llmsIndex(): string {
+ const lines = ["# Taskito documentation", ""];
+ for (const doc of [...SEARCH_DOCS].sort((a, b) => a.id.localeCompare(b.id))) {
+ lines.push(`- [${doc.title}](${doc.id})`);
+ }
+ return `${lines.join("\n")}\n`;
+}
+
+/** Full corpus (title + stripped body per page) — the `/llms-full.txt` body. */
+export function llmsFull(): string {
+ const blocks = [...SEARCH_DOCS]
+ .sort((a, b) => a.id.localeCompare(b.id))
+ .map((doc) => `## ${doc.title}\nURL: ${doc.id}\n\n${doc.text}\n`);
+ return `# Taskito documentation (full text)\n\n${blocks.join("\n---\n\n")}`;
+}
diff --git a/docs/app/lib/manifest.ts b/docs/app/lib/manifest.ts
new file mode 100644
index 00000000..7752e730
--- /dev/null
+++ b/docs/app/lib/manifest.ts
@@ -0,0 +1,26 @@
+// Lightweight page metadata (slug/title/description) from the build-time manifest
+// (vite-plugin-docs-manifest). No compiled MDX here — keeps nav/search/llms out of
+// the heavy component graph.
+import { DOCS } from "virtual:docs-manifest";
+
+export interface DocMeta {
+ slug: string;
+ title: string;
+ description: string;
+}
+
+export const DOC_METAS: DocMeta[] = DOCS;
+
+const BY_SLUG = new Map(DOC_METAS.map((d) => [d.slug, d]));
+
+export function docMeta(slug: string): DocMeta | undefined {
+ return BY_SLUG.get(slug.replace(/\/$/, "") || "/");
+}
+
+export function docTitle(slug: string): string | undefined {
+ return BY_SLUG.get(slug)?.title;
+}
+
+export function hasDoc(slug: string): boolean {
+ return BY_SLUG.has(slug);
+}
diff --git a/docs/app/lib/nav.ts b/docs/app/lib/nav.ts
new file mode 100644
index 00000000..6485f2e7
--- /dev/null
+++ b/docs/app/lib/nav.ts
@@ -0,0 +1,155 @@
+import { docTitle, hasDoc } from "./manifest";
+import type { Sdk } from "./sdk-store";
+
+interface Meta {
+ title?: string;
+ pages?: string[];
+ root?: boolean;
+}
+
+// meta.json files describe nav order + group titles; reused unchanged as the nav
+// source (same files Fumadocs used), keyed by their content-relative directory.
+const META = import.meta.glob<{ default: Meta }>(
+ "../../content/docs/**/meta.json",
+ {
+ eager: true,
+ },
+);
+
+function metaFor(dir: string): Meta {
+ const suffix = dir ? `${dir}/meta.json` : "meta.json";
+ for (const [key, mod] of Object.entries(META)) {
+ if (key.endsWith(`/content/docs/${suffix}`)) {
+ return mod.default;
+ }
+ }
+ return {};
+}
+
+function humanize(name: string): string {
+ return name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+function titleFor(slug: string, fallback: string): string {
+ return (
+ docTitle(slug) ??
+ metaFor(slug.replace(/^\//, "")).title ??
+ humanize(fallback)
+ );
+}
+
+/** A sidebar entry: either a link (`href`) or a labelled group (`children`). */
+export interface NavNode {
+ title: string;
+ href?: string;
+ children?: NavNode[];
+}
+
+/** Build the nav nodes for a directory's `pages`, recursing into subsections. */
+function nodesForDir(dir: string): NavNode[] {
+ const meta = metaFor(dir);
+ const nodes: NavNode[] = [];
+ for (const name of meta.pages ?? []) {
+ if (name === "index") {
+ continue; // the section's own index is its group header, not a child item
+ }
+ const childDir = `${dir}/${name}`;
+ const childMeta = metaFor(childDir);
+ if (childMeta.pages) {
+ // Subsection → nested group. Link the group title to its index page if any.
+ const indexSlug = `/${childDir}`;
+ nodes.push({
+ title: childMeta.title ?? humanize(name),
+ href: hasDoc(indexSlug) ? indexSlug : undefined,
+ children: nodesForDir(childDir),
+ });
+ } else {
+ const slug = `/${childDir}`;
+ nodes.push({ title: titleFor(slug, name), href: slug });
+ }
+ }
+ return nodes;
+}
+
+/** Top-level sidebar groups for an SDK, one per section directory. */
+function buildTree(sections: string[]): NavNode[] {
+ return sections.map((dir) => {
+ const indexSlug = `/${dir}`;
+ return {
+ title: metaFor(dir).title ?? humanize(dir),
+ href: hasDoc(indexSlug) ? indexSlug : undefined,
+ children: nodesForDir(dir),
+ };
+ });
+}
+
+// `architecture` and `resources` are SDK-neutral (the engine is identical across
+// SDKs); both live at top-level shared URLs and appear in every SDK's nav.
+export const PYTHON_SECTIONS = [
+ "python/getting-started",
+ "python/guides",
+ "architecture",
+ "python/api-reference",
+ "python/more/examples",
+ "resources",
+];
+export const NODE_SECTIONS = [
+ "node/getting-started",
+ "node/guides",
+ "architecture",
+ "node/api-reference",
+ "resources",
+];
+
+export const PYTHON_NAV = buildTree(PYTHON_SECTIONS);
+export const NODE_NAV = buildTree(NODE_SECTIONS);
+
+export type { Sdk };
+
+/** The SDK forced by an explicit `/python`|`/node` URL prefix, or null on a
+ * shared page (where the active SDK comes from the global store instead). */
+export function forcedSdkForPath(path: string): Sdk | null {
+ if (path === "/node" || path.startsWith("/node/")) {
+ return "node";
+ }
+ if (path === "/python" || path.startsWith("/python/")) {
+ return "python";
+ }
+ return null;
+}
+
+/** Where the sidebar SDK switch should go: the same page under the target SDK's
+ * prefix if it exists, else that SDK's install landing. Shared pages stay put
+ * (the caller skips navigation when the current path isn't SDK-prefixed). */
+export function sdkSwitchTarget(path: string, target: Sdk): string {
+ const other: Sdk = target === "node" ? "python" : "node";
+ const prefix = `/${other}`;
+ if (path === prefix || path.startsWith(`${prefix}/`)) {
+ const swapped = `/${target}${path.slice(prefix.length)}`;
+ if (hasDoc(swapped)) {
+ return swapped;
+ }
+ }
+ return `/${target}/getting-started/installation`;
+}
+
+export function navForSdk(sdk: Sdk): NavNode[] {
+ return sdk === "node" ? NODE_NAV : PYTHON_NAV;
+}
+
+/** Depth-first flattened links for the active SDK — drives prev/next. */
+export function flatNav(sdk: Sdk): { title: string; href: string }[] {
+ const out: { title: string; href: string }[] = [];
+ const walk = (nodes: NavNode[]) => {
+ for (const n of nodes) {
+ if (n.href) {
+ out.push({ title: n.title, href: n.href });
+ }
+ if (n.children) {
+ walk(n.children);
+ }
+ }
+ };
+ walk(navForSdk(sdk));
+ return out;
+}
diff --git a/docs/app/lib/redirects.ts b/docs/app/lib/redirects.ts
new file mode 100644
index 00000000..616fc209
--- /dev/null
+++ b/docs/app/lib/redirects.ts
@@ -0,0 +1,40 @@
+// Old → new doc URLs after the shared-content move (architecture + resources
+// dropped their `/python` prefix). GitHub Pages has no server-side redirect
+// rules, so each old path is prerendered as a stub that meta-refreshes (direct
+// hits) and client-navigates (SPA) to its new home. Dependency-free: imported by
+// both the client route and the build-time prerender config.
+
+const ARCH_PAGES = [
+ "",
+ "overview",
+ "job-lifecycle",
+ "worker-pool",
+ "scheduler",
+ "mesh",
+ "storage",
+ "resources",
+ "failure-model",
+ "serialization",
+];
+
+export const REDIRECTS: Record = {
+ ...Object.fromEntries(
+ ARCH_PAGES.map((page) => {
+ const suffix = page ? `/${page}` : "";
+ return [`/python/architecture${suffix}`, `/architecture${suffix}`];
+ }),
+ ),
+ "/python/more/comparison": "/resources/comparison",
+ "/python/more/faq": "/resources/faq",
+ "/python/more/changelog": "/resources/changelog",
+};
+
+/** The destination for a moved path, or undefined if it isn't a redirect. */
+export function redirectFor(path: string): string | undefined {
+ return REDIRECTS[path.replace(/\/$/, "") || "/"];
+}
+
+/** Old paths to prerender as redirect stubs (so direct hits don't 404). */
+export function redirectPaths(): string[] {
+ return Object.keys(REDIRECTS);
+}
diff --git a/docs/app/lib/sdk-store.ts b/docs/app/lib/sdk-store.ts
new file mode 100644
index 00000000..54a4d7f7
--- /dev/null
+++ b/docs/app/lib/sdk-store.ts
@@ -0,0 +1,85 @@
+// Global active-SDK store. Single source of truth shared by the no-flash boot
+// script (root.tsx), the sidebar switcher, inline ``, and ``.
+// Kept as a tiny external store so `useSyncExternalStore` can hand React an
+// explicit server snapshot — the SSG-safe way to read a browser-only value
+// without a hydration mismatch. The live value lives on ``; CSS
+// shows/hides each SDK's variants off that attribute.
+
+export type Sdk = "python" | "node";
+
+const KEY = "taskito-sdk";
+const DEFAULT: Sdk = "python";
+
+const listeners = new Set<() => void>();
+
+function isSdk(value: string | null | undefined): value is Sdk {
+ return value === "python" || value === "node";
+}
+
+/** Resolve the active SDK: `?sdk=` query > localStorage > default. Used by the
+ * no-flash bootstrap in root.tsx and as the first client read. */
+export function readSdk(): Sdk {
+ if (typeof document === "undefined") {
+ return DEFAULT;
+ }
+ try {
+ const param = new URLSearchParams(window.location.search).get("sdk");
+ if (isSdk(param)) {
+ return param;
+ }
+ const stored = localStorage.getItem(KEY);
+ if (isSdk(stored)) {
+ return stored;
+ }
+ } catch {
+ // ignore storage/URL access failures (private mode etc.)
+ }
+ return DEFAULT;
+}
+
+function currentSdk(): Sdk {
+ const attr = document.documentElement.dataset.sdk;
+ return isSdk(attr) ? attr : DEFAULT;
+}
+
+function notify(): void {
+ for (const listener of listeners) {
+ listener();
+ }
+}
+
+// Another tab changed the choice — mirror it onto this document, then notify.
+function onStorage(event: StorageEvent): void {
+ if (event.key !== KEY || !isSdk(event.newValue)) {
+ return;
+ }
+ document.documentElement.dataset.sdk = event.newValue;
+ notify();
+}
+
+export const sdkStore = {
+ subscribe(callback: () => void): () => void {
+ if (listeners.size === 0) {
+ window.addEventListener("storage", onStorage);
+ }
+ listeners.add(callback);
+ return () => {
+ listeners.delete(callback);
+ if (listeners.size === 0) {
+ window.removeEventListener("storage", onStorage);
+ }
+ };
+ },
+ getSnapshot: currentSdk,
+ getServerSnapshot: (): Sdk => DEFAULT,
+ /** Set + persist the active SDK; drives the CSS show/hide via ``. */
+ set(sdk: Sdk): void {
+ document.documentElement.dataset.sdk = sdk;
+ try {
+ localStorage.setItem(KEY, sdk);
+ } catch {
+ // ignore storage failures (private mode etc.)
+ }
+ notify();
+ },
+};
diff --git a/docs/app/lib/search.ts b/docs/app/lib/search.ts
new file mode 100644
index 00000000..969a611e
--- /dev/null
+++ b/docs/app/lib/search.ts
@@ -0,0 +1,111 @@
+import MiniSearch from "minisearch";
+import { DOC_METAS } from "./manifest";
+import { forcedSdkForPath } from "./nav";
+import type { Sdk } from "./sdk-store";
+
+export interface SearchDoc {
+ id: string; // slug
+ title: string;
+ section: string;
+ description: string;
+ text: string;
+}
+
+function sectionOf(slug: string): string {
+ const top = slug.split("/")[1] ?? "";
+ return top
+ ? top.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
+ : "Home";
+}
+
+function humanizeSlug(slug: string): string {
+ return slug.split("/").filter(Boolean).join(" ").replace(/-/g, " ");
+}
+
+// Index built from the build-time manifest (title + description + slug words).
+// No compiled MDX pulled in — keeps the search chunk tiny.
+export const SEARCH_DOCS: SearchDoc[] = DOC_METAS.map((d) => ({
+ id: d.slug,
+ title: d.title,
+ section: sectionOf(d.slug),
+ description: d.description,
+ text: `${d.description} ${humanizeSlug(d.slug)}`.trim(),
+}));
+
+let index: MiniSearch | null = null;
+
+function getIndex(): MiniSearch {
+ if (!index) {
+ index = new MiniSearch({
+ fields: ["title", "text", "section"],
+ storeFields: ["title", "section", "description"],
+ searchOptions: {
+ boost: { title: 3, section: 1.5 },
+ prefix: true,
+ fuzzy: 0.2,
+ },
+ });
+ index.addAll(SEARCH_DOCS);
+ }
+ return index;
+}
+
+export interface SearchHit {
+ id: string;
+ title: string;
+ section: string;
+ description: string;
+}
+
+function toHit(d: SearchDoc): SearchHit {
+ return {
+ id: d.id,
+ title: d.title,
+ section: d.section,
+ description: d.description,
+ };
+}
+
+// Browse-mode section order (mirrors the sidebar); unknown sections sort last.
+const SECTION_ORDER = [
+ "Getting Started",
+ "Guides",
+ "Architecture",
+ "Api Reference",
+ "More",
+ "Node",
+];
+const sectionRank = (s: string) => {
+ const i = SECTION_ORDER.indexOf(s);
+ return i === -1 ? SECTION_ORDER.length : i;
+};
+
+// A page is in scope when it's shared (no SDK prefix) or matches the active SDK.
+function inSdk(slug: string, sdk?: Sdk): boolean {
+ if (!sdk) {
+ return true;
+ }
+ const pageSdk = forcedSdkForPath(slug);
+ return pageSdk === null || pageSdk === sdk;
+}
+
+/** Empty query → the full index (browse mode); otherwise ranked matches. When
+ * `sdk` is given, results are scoped to that SDK's pages plus shared pages. */
+export function searchDocs(query: string, sdk?: Sdk): SearchHit[] {
+ const q = query.trim();
+ if (!q) {
+ return SEARCH_DOCS.filter((d) => inSdk(d.id, sdk))
+ .map(toHit)
+ .sort((a, b) => sectionRank(a.section) - sectionRank(b.section));
+ }
+ return getIndex()
+ .search(q)
+ .filter((r) => inSdk(r.id as string, sdk))
+ .slice(0, 20)
+ .map((r) => ({
+ id: r.id as string,
+ title: r.title,
+ section: r.section,
+ description: r.description,
+ }));
+}
diff --git a/docs/app/lib/theme.ts b/docs/app/lib/theme.ts
new file mode 100644
index 00000000..a76020ae
--- /dev/null
+++ b/docs/app/lib/theme.ts
@@ -0,0 +1,39 @@
+import { useEffect, useState } from "react";
+
+export type ThemeMode = "dark" | "light";
+
+/** Current theme from the `` attribute (dark default / SSR). */
+function readTheme(): ThemeMode {
+ if (typeof document === "undefined") {
+ return "dark";
+ }
+ return document.documentElement.getAttribute("data-theme") === "light"
+ ? "light"
+ : "dark";
+}
+
+/** Reactive current theme, kept in sync with `` changes. */
+export function useThemeMode(): ThemeMode {
+ const [mode, setMode] = useState("dark");
+ useEffect(() => {
+ setMode(readTheme());
+ const observer = new MutationObserver(() => setMode(readTheme()));
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+ return () => observer.disconnect();
+ }, []);
+ return mode;
+}
+
+/** Flip and persist the theme (consumed by the no-flash bootstrap in root.tsx). */
+export function toggleTheme(): void {
+ const next: ThemeMode = readTheme() === "light" ? "dark" : "light";
+ document.documentElement.setAttribute("data-theme", next);
+ try {
+ localStorage.setItem("taskito-theme", next);
+ } catch {
+ // ignore storage failures (private mode etc.)
+ }
+}
diff --git a/docs/app/root.tsx b/docs/app/root.tsx
new file mode 100644
index 00000000..eaa60842
--- /dev/null
+++ b/docs/app/root.tsx
@@ -0,0 +1,73 @@
+import {
+ isRouteErrorResponse,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "react-router";
+import type { Route } from "./+types/root";
+import "./app.css";
+
+export const links: Route.LinksFunction = () => [
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Shantell+Sans:wght@500;600;700&display=swap",
+ },
+];
+
+// Apply the persisted theme before paint to avoid a light/dark flash.
+const THEME_INIT = `(function(){try{var t=localStorage.getItem('taskito-theme')||'dark';document.documentElement.setAttribute('data-theme',t);}catch(e){}})();`;
+
+// Apply the active SDK before paint (query > localStorage > default) so the
+// CSS show/hide picks the right variant with no flash. Mirrors readSdk().
+const SDK_INIT = `(function(){try{var u=new URLSearchParams(location.search).get('sdk');var s=(u==='python'||u==='node')?u:(localStorage.getItem('taskito-sdk')||'python');if(s!=='python'&&s!=='node'){s='python';}document.documentElement.setAttribute('data-sdk',s);}catch(e){document.documentElement.setAttribute('data-sdk','python');}})();`;
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: tiny no-flash theme bootstrap */}
+
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: tiny no-flash sdk bootstrap */}
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ const message = isRouteErrorResponse(error)
+ ? `${error.status} ${error.statusText}`
+ : "Something went wrong";
+ return (
+
+ {message}
+ Back to the docs
+
+ );
+}
diff --git a/docs/app/routes.ts b/docs/app/routes.ts
new file mode 100644
index 00000000..9e8e0ed2
--- /dev/null
+++ b/docs/app/routes.ts
@@ -0,0 +1,14 @@
+import {
+ index,
+ layout,
+ type RouteConfig,
+ route,
+} from "@react-router/dev/routes";
+
+export default [
+ index("routes/home.tsx"),
+ route("llms.txt", "routes/llms.txt.tsx"),
+ route("llms-full.txt", "routes/llms-full.txt.tsx"),
+ // Every docs URL renders inside the shell (nav + sidebar + TOC).
+ layout("routes/docs-layout.tsx", [route("*", "routes/docs.$.tsx")]),
+] satisfies RouteConfig;
diff --git a/docs/app/routes/docs-layout.tsx b/docs/app/routes/docs-layout.tsx
new file mode 100644
index 00000000..9cf853c3
--- /dev/null
+++ b/docs/app/routes/docs-layout.tsx
@@ -0,0 +1,50 @@
+import { useEffect, useState } from "react";
+import { Outlet, useLocation } from "react-router";
+import { SearchModal, Sidebar, Toc } from "@/components/docs";
+import { SiteNav } from "@/components/ui";
+import { useActiveSdk, useSdk } from "@/hooks";
+import { forcedSdkForPath } from "@/lib";
+
+/** Shell for every docs page: top nav + sidebar + article outlet + on-this-page TOC. */
+export default function DocsLayout() {
+ const [searchOpen, setSearchOpen] = useState(false);
+ const { pathname } = useLocation();
+ const { setSdk } = useSdk();
+ const sdk = useActiveSdk();
+
+ // Visiting an SDK-specific page (`/python/*`,`/node/*`) makes that SDK sticky,
+ // so walking onto a shared page keeps the choice. No-op on shared pages.
+ useEffect(() => {
+ const forced = forcedSdkForPath(pathname.replace(/\/$/, "") || "/");
+ if (forced) {
+ setSdk(forced);
+ }
+ }, [pathname, setSdk]);
+
+ // ⌘K / Ctrl-K opens search anywhere in the docs.
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
+ e.preventDefault();
+ setSearchOpen(true);
+ }
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, []);
+ return (
+ <>
+ setSearchOpen(true)} />
+
+ setSearchOpen(true)} />
+
+
+
+ setSearchOpen(false)}
+ sdk={sdk}
+ />
+ >
+ );
+}
diff --git a/docs/app/routes/docs.$.tsx b/docs/app/routes/docs.$.tsx
new file mode 100644
index 00000000..5c457197
--- /dev/null
+++ b/docs/app/routes/docs.$.tsx
@@ -0,0 +1,116 @@
+import { MDXProvider } from "@mdx-js/react";
+import { Fragment, lazy, Suspense, useEffect, useMemo } from "react";
+import { Link, useNavigate } from "react-router";
+import { PrevNext } from "@/components/docs";
+import { mdxComponents } from "@/components/mdx";
+import { getDocLoader } from "@/lib/content";
+import { docMeta } from "@/lib/manifest";
+import { redirectFor } from "@/lib/redirects";
+import type { Route } from "./+types/docs.$";
+
+function pathOf(params: { "*"?: string }): string {
+ return `/${params["*"] ?? ""}`;
+}
+
+/** Breadcrumb from the URL segments; the leading segment links to its section index. */
+function Breadcrumb({ path }: { path: string }) {
+ const segs = path.split("/").filter(Boolean);
+ if (segs.length < 2) {
+ return null;
+ }
+ const crumbs = segs.slice(0, -1).map((seg, i) => {
+ const href = `/${segs.slice(0, i + 1).join("/")}`;
+ const label =
+ docMeta(href)?.title ??
+ seg.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+ return { href, label };
+ });
+ return (
+
+ {crumbs.map((c) => (
+
+ {c.label}
+ /
+
+ ))}
+
+ );
+}
+
+export function meta({ params }: Route.MetaArgs) {
+ const path = pathOf(params);
+ const dest = redirectFor(path);
+ if (dest) {
+ // Prerendered into the old URL's static HTML so a direct hit refreshes to
+ // the new home; canonical points search engines at the destination.
+ return [
+ { title: "Redirecting… | Taskito" },
+ { httpEquiv: "refresh", content: `0; url=${dest}` },
+ { tagName: "link", rel: "canonical", href: dest },
+ ];
+ }
+ const meta = docMeta(path);
+ return [
+ { title: meta?.title ? `${meta.title} | Taskito` : "Taskito" },
+ { name: "description", content: meta?.description ?? "" },
+ ];
+}
+
+/** Stub shown at a moved URL: meta-refresh handles direct hits, this handles
+ * client-side (SPA) navigation by replacing the history entry. */
+function RedirectStub({ to }: { to: string }) {
+ const navigate = useNavigate();
+ useEffect(() => {
+ navigate(to, { replace: true });
+ }, [to, navigate]);
+ return (
+
+ Redirecting…
+
+ This page moved to {to}.
+
+
+ );
+}
+
+export default function DocRoute({ params }: Route.ComponentProps) {
+ const path = pathOf(params);
+ const dest = redirectFor(path);
+ const loader = getDocLoader(path);
+ const meta = docMeta(path);
+
+ // Each page is its own lazily-loaded chunk; React.lazy is resolved during
+ // prerender (Suspense) so the static HTML still carries the full content.
+ const Page = useMemo(() => (loader ? lazy(loader) : null), [loader]);
+
+ if (dest) {
+ return ;
+ }
+
+ if (!Page) {
+ return (
+
+ Page not found
+ No documentation page matches this URL.
+
+ );
+ }
+
+ return (
+
+
+ {meta?.title ? {meta.title}
: null}
+ {meta?.description ? {meta.description}
: null}
+
+ {/* Key by path: client-side navigation between two pages that share this
+ splat route re-suspends the lazy page. Without a fresh boundary per
+ path, React retains the previous committed tree for the whole route
+ during the navigation transition and never swaps in the new page. */}
+
+
+
+
+
+
+ );
+}
diff --git a/docs/app/routes/home.tsx b/docs/app/routes/home.tsx
new file mode 100644
index 00000000..86a91b4d
--- /dev/null
+++ b/docs/app/routes/home.tsx
@@ -0,0 +1,71 @@
+import { useEffect, useState } from "react";
+import { SearchModal } from "@/components/docs";
+import {
+ Comparison,
+ CTA,
+ Features,
+ Footer,
+ Hero,
+ HowItWorks,
+ Integrations,
+ UseCases,
+ useReveal,
+} from "@/components/landing";
+import { SiteNav } from "@/components/ui";
+import { useActiveSdk } from "@/hooks";
+import type { Route } from "./+types/home";
+
+export function meta(_: Route.MetaArgs) {
+ return [
+ { title: "Taskito — one queue, built for Python" },
+ {
+ name: "description",
+ content: "Rust-powered task queue for Python. No broker required.",
+ },
+ ];
+}
+
+export default function Home() {
+ useReveal();
+ const [searchOpen, setSearchOpen] = useState(false);
+ // Scope landing search to the SDK chosen in the hero (store-backed on `/`).
+ const sdk = useActiveSdk();
+
+ // ⌘K / Ctrl-K opens search on the landing page too.
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
+ e.preventDefault();
+ setSearchOpen(true);
+ }
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, []);
+
+ return (
+ <>
+
+ setSearchOpen(true)} />
+
+
+
+
+
+
+
+
+
+
+ setSearchOpen(false)}
+ sdk={sdk}
+ />
+ >
+ );
+}
diff --git a/docs/app/routes/llms-full.txt.tsx b/docs/app/routes/llms-full.txt.tsx
new file mode 100644
index 00000000..3462c00f
--- /dev/null
+++ b/docs/app/routes/llms-full.txt.tsx
@@ -0,0 +1,7 @@
+import { llmsFull } from "@/lib/llms";
+
+export function loader() {
+ return new Response(llmsFull(), {
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ });
+}
diff --git a/docs/app/routes/llms.txt.tsx b/docs/app/routes/llms.txt.tsx
new file mode 100644
index 00000000..e5518e72
--- /dev/null
+++ b/docs/app/routes/llms.txt.tsx
@@ -0,0 +1,9 @@
+import { llmsIndex } from "@/lib/llms";
+
+// Resource route: prerendered to a plain-text file at build (ssr:false runs the
+// loader once during prerender).
+export function loader() {
+ return new Response(llmsIndex(), {
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ });
+}
diff --git a/docs/app/styles/atoms.css b/docs/app/styles/atoms.css
new file mode 100644
index 00000000..9503a6cd
--- /dev/null
+++ b/docs/app/styles/atoms.css
@@ -0,0 +1,366 @@
+/* ---------- shared atoms ---------- */
+.eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 9px;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ padding: 6px 13px;
+ border-radius: 100px;
+}
+.eyebrow .dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--indigo-br);
+ box-shadow: 0 0 0 4px var(--indigo-soft);
+ animation: blip 2.4s ease-in-out infinite;
+}
+@keyframes blip {
+ 0%,
+ 100% {
+ opacity: 0.5;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+.btn {
+ font-family: var(--sans);
+ font-size: 14.5px;
+ font-weight: 600;
+ padding: 12px 20px;
+ border-radius: 12px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition:
+ transform 0.15s,
+ box-shadow 0.2s,
+ background 0.2s,
+ border-color 0.2s,
+ color 0.2s;
+ white-space: nowrap;
+}
+.btn svg {
+ width: 16px;
+ height: 16px;
+}
+.btn.pri {
+ background: linear-gradient(180deg, var(--indigo), var(--indigo-dk));
+ color: #fff;
+ box-shadow:
+ 0 10px 22px -14px var(--indigo),
+ inset 0 1px 0 rgba(255, 255, 255, 0.16);
+}
+.btn.pri:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 14px 30px -16px var(--indigo),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+}
+.btn.sec {
+ background: var(--panel);
+ border-color: var(--line2);
+ color: var(--txt);
+}
+.btn.sec:hover {
+ transform: translateY(-2px);
+ border-color: var(--indigo);
+}
+.btn.gho {
+ color: var(--mut);
+ background: transparent;
+}
+.btn.gho:hover {
+ color: var(--txt);
+}
+
+.kicker {
+ font-family: var(--mono);
+ font-size: 13px;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ color: var(--indigo-br);
+ text-transform: uppercase;
+}
+
+/* section scaffolding */
+.section {
+ padding: 96px 32px;
+ position: relative;
+}
+.wrap {
+ max-width: var(--maxw);
+ margin: 0 auto;
+}
+.section-head {
+ max-width: 640px;
+ margin: 0 auto 56px;
+ text-align: center;
+}
+.section-head h2 {
+ font-size: clamp(30px, 3.6vw, 44px);
+ line-height: 1.05;
+ letter-spacing: -0.03em;
+ font-weight: 600;
+ margin-top: 14px;
+}
+.section-head p {
+ margin-top: 16px;
+ font-size: 18px;
+ line-height: 1.55;
+ color: var(--mut);
+}
+
+/* code tokens */
+.cmt {
+ color: var(--dim);
+}
+.kw {
+ color: #c98bff;
+}
+.def {
+ color: var(--cyan);
+}
+.fn {
+ color: var(--indigo-br);
+}
+.str {
+ color: #9ce08a;
+}
+.num {
+ color: var(--amber);
+}
+.pn {
+ color: var(--mut);
+}
+.var {
+ color: var(--txt);
+}
+[data-theme="light"] .kw {
+ color: #8b3cf0;
+}
+[data-theme="light"] .str {
+ color: #1a8f3a;
+}
+[data-theme="light"] .def {
+ color: #0e9b91;
+}
+
+/* ---------- nav ---------- */
+.nav {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ display: flex;
+ align-items: center;
+ gap: 28px;
+ padding: 16px 32px;
+ background: color-mix(in oklch, var(--bg) 78%, transparent);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ border-bottom: 1px solid var(--line);
+}
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-family: var(--mono);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ font-size: 17px;
+}
+.logo {
+ width: 27px;
+ height: 27px;
+ border-radius: 8px;
+ position: relative;
+ flex: none;
+ background: linear-gradient(135deg, var(--indigo-br), var(--indigo-dk));
+ box-shadow:
+ 0 0 0 1px var(--line2),
+ 0 6px 18px -6px var(--indigo);
+ display: grid;
+ place-items: center;
+}
+/* real logo mark (tile) */
+.logo-mark {
+ display: inline-flex;
+ align-items: center;
+ flex: none;
+}
+.logo-mark svg {
+ height: 26px;
+ width: auto;
+ display: block;
+ border-radius: 7px;
+ box-shadow: 0 4px 12px -4px
+ color-mix(in oklch, var(--indigo) 55%, transparent);
+}
+.logo-mark rect {
+ fill: var(--indigo);
+}
+.brand .bto {
+ color: var(--indigo-br);
+}
+.navlinks {
+ display: flex;
+ gap: 22px;
+ font-size: 14px;
+ color: var(--mut);
+ font-weight: 500;
+}
+.navlinks a {
+ transition: color 0.15s;
+ position: relative;
+}
+.navlinks a:hover {
+ color: var(--txt);
+}
+.navlinks a.active {
+ color: var(--txt);
+}
+.navright {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+.kbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border: 1px solid var(--line2);
+ border-radius: 10px;
+ color: var(--dim);
+ font-size: 12.5px;
+ font-family: var(--mono);
+ background: var(--panel);
+ cursor: pointer;
+ transition: border-color 0.2s;
+}
+.kbar:hover {
+ border-color: var(--line3);
+}
+.kbar kbd {
+ font-family: var(--mono);
+ border: 1px solid var(--line2);
+ border-radius: 4px;
+ padding: 0 5px;
+ font-size: 11px;
+ color: var(--mut);
+}
+.icon-btn {
+ width: 36px;
+ height: 36px;
+ border: 1px solid var(--line2);
+ border-radius: 10px;
+ display: grid;
+ place-items: center;
+ color: var(--mut);
+ background: var(--panel);
+ cursor: pointer;
+ transition:
+ color 0.2s,
+ border-color 0.2s;
+}
+.icon-btn:hover {
+ color: var(--txt);
+ border-color: var(--line3);
+}
+.icon-btn svg {
+ width: 17px;
+ height: 17px;
+}
+.theme-toggle .sun {
+ display: none;
+}
+[data-theme="light"] .theme-toggle .sun {
+ display: block;
+}
+[data-theme="light"] .theme-toggle .moon {
+ display: none;
+}
+
+/* ---------- footer ---------- */
+.foot {
+ border-top: 1px solid var(--line);
+ background: var(--bg-soft);
+ padding: 64px 32px 40px;
+}
+.foot-grid {
+ max-width: var(--maxw);
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: 1.6fr 1fr 1fr 1fr;
+ gap: 40px;
+}
+.foot-brand .brand {
+ margin-bottom: 14px;
+}
+.foot-brand p {
+ color: var(--mut);
+ font-size: 14px;
+ line-height: 1.6;
+ max-width: 280px;
+}
+.foot-col h4 {
+ font-size: 13px;
+ font-family: var(--mono);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--dim);
+ margin-bottom: 16px;
+ font-weight: 600;
+}
+.foot-col a {
+ display: block;
+ color: var(--mut);
+ font-size: 14px;
+ padding: 5px 0;
+ transition: color 0.15s;
+}
+.foot-col a:hover {
+ color: var(--txt);
+}
+.foot-bottom {
+ max-width: var(--maxw);
+ margin: 48px auto 0;
+ padding-top: 24px;
+ border-top: 1px solid var(--line);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: var(--dim);
+ font-size: 13px;
+ font-family: var(--mono);
+}
+
+@media (max-width: 880px) {
+ .navlinks,
+ .kbar {
+ display: none;
+ }
+ .foot-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+ .section {
+ padding: 72px 22px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation: none !important;
+ scroll-behavior: auto;
+ }
+}
diff --git a/docs/app/styles/changelog.css b/docs/app/styles/changelog.css
new file mode 100644
index 00000000..57210657
--- /dev/null
+++ b/docs/app/styles/changelog.css
@@ -0,0 +1,147 @@
+/* ===== release / changelog ===== */
+.changelog {
+ margin-top: 10px;
+}
+.release {
+ padding: 30px 0;
+ border-bottom: 1px solid var(--line);
+}
+.release:first-child {
+ padding-top: 8px;
+}
+.rel-top {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+.rel-v {
+ font-family: var(--mono);
+ font-size: 23px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: var(--txt);
+ scroll-margin-top: 90px;
+}
+.rel-badge {
+ font-family: var(--mono);
+ font-size: 11px;
+ font-weight: 600;
+ padding: 3px 9px;
+ border-radius: 7px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+.rel-badge.feature {
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+}
+.rel-badge.patch {
+ color: var(--mut);
+ background: var(--panel2);
+ border: 1px solid var(--line2);
+}
+.rel-badge.security {
+ color: var(--cyan);
+ background: color-mix(in oklch, var(--cyan) 10%, transparent);
+ border: 1px solid color-mix(in oklch, var(--cyan) 35%, transparent);
+}
+.rel-badge.current {
+ color: var(--grn);
+ background: color-mix(in oklch, var(--grn) 12%, transparent);
+ border: 1px solid color-mix(in oklch, var(--grn) 40%, transparent);
+}
+.rel-tagline {
+ flex-basis: 100%;
+ margin-top: 4px;
+ color: var(--mut);
+ font-size: 15px;
+ line-height: 1.55;
+}
+.rel-section {
+ margin-top: 18px;
+}
+.rel-cat {
+ display: inline-block;
+ font-family: var(--mono);
+ font-size: 10.5px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: 2px 9px;
+ border-radius: 6px;
+ margin-bottom: 9px;
+}
+.cat-added {
+ color: var(--grn);
+ background: color-mix(in oklch, var(--grn) 11%, transparent);
+}
+.cat-fixed {
+ color: var(--amber);
+ background: color-mix(in oklch, var(--amber) 11%, transparent);
+}
+.cat-changed {
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+}
+.cat-breaking {
+ color: var(--red);
+ background: color-mix(in oklch, var(--red) 11%, transparent);
+}
+.cat-security {
+ color: var(--cyan);
+ background: color-mix(in oklch, var(--cyan) 11%, transparent);
+}
+.cat-notes {
+ color: var(--mut);
+ background: var(--panel2);
+}
+.rel-list {
+ margin: 0 0 4px;
+ padding: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.rel-list li {
+ font-size: 14.5px;
+ line-height: 1.6;
+ color: var(--txt2);
+ padding-left: 20px;
+ position: relative;
+}
+.rel-list li::before {
+ content: "";
+ position: absolute;
+ left: 4px;
+ top: 9px;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--faint);
+}
+.rel-list li b {
+ color: var(--txt);
+ font-weight: 600;
+}
+.rel-list li code {
+ font-family: var(--mono);
+ font-size: 0.84em;
+ background: var(--panel2);
+ border: 1px solid var(--line);
+ padding: 1px 5px;
+ border-radius: 5px;
+ color: var(--txt);
+}
+.rel-earlier {
+ margin-top: 6px;
+}
+.rel-earlier .release {
+ padding: 18px 0;
+}
+.rel-earlier .rel-tagline {
+ margin-top: 2px;
+ font-size: 14px;
+}
diff --git a/docs/app/styles/cmdk.css b/docs/app/styles/cmdk.css
new file mode 100644
index 00000000..aba5d910
--- /dev/null
+++ b/docs/app/styles/cmdk.css
@@ -0,0 +1,234 @@
+/* ============================================================
+ ⌘K command palette (shared, all pages)
+ ============================================================ */
+.cmdk {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ display: none;
+ align-items: flex-start;
+ justify-content: center;
+}
+.cmdk.show {
+ display: flex;
+}
+.cmdk-backdrop {
+ position: absolute;
+ inset: 0;
+ background: color-mix(in oklch, var(--bg) 55%, transparent);
+ backdrop-filter: blur(6px);
+ -webkit-backdrop-filter: blur(6px);
+}
+.cmdk-panel {
+ position: relative;
+ margin-top: 11vh;
+ width: min(620px, 92vw);
+ background: var(--panel);
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ box-shadow:
+ 0 40px 100px -30px rgba(0, 0, 0, 0.6),
+ 0 0 0 1px var(--line);
+ overflow: hidden;
+ opacity: 1;
+ animation: cmdkpop 0.18s cubic-bezier(0.2, 0.7, 0.3, 1);
+}
+@keyframes cmdkpop {
+ from {
+ transform: translateY(-8px) scale(0.98);
+ }
+ to {
+ transform: none;
+ }
+}
+.cmdk-inputrow {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ padding: 15px 17px;
+ border-bottom: 1px solid var(--line);
+}
+.cmdk-ic {
+ width: 19px;
+ height: 19px;
+ color: var(--mut);
+ flex: none;
+}
+.cmdk-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--txt);
+ font-family: var(--sans);
+ font-size: 16.5px;
+}
+.cmdk-input::placeholder {
+ color: var(--dim);
+}
+.cmdk-esc {
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--mut);
+ border: 1px solid var(--line2);
+ border-radius: 5px;
+ padding: 2px 7px;
+ cursor: pointer;
+}
+.cmdk-results {
+ max-height: min(420px, 56vh);
+ overflow-y: auto;
+ padding: 8px;
+}
+.cmdk-results::-webkit-scrollbar {
+ width: 8px;
+}
+.cmdk-results::-webkit-scrollbar-thumb {
+ background: var(--line2);
+ border-radius: 8px;
+}
+.cmdk-group {
+ font-family: var(--mono);
+ font-size: 10.5px;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: var(--dim);
+ padding: 12px 12px 6px;
+ font-weight: 600;
+}
+.cmdk-item {
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ padding: 10px 12px;
+ border-radius: 11px;
+ text-decoration: none;
+ cursor: pointer;
+}
+.cmdk-item.active {
+ background: var(--indigo-soft);
+}
+.cmdk-it-ic {
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ flex: none;
+ display: grid;
+ place-items: center;
+ background: var(--panel2);
+ border: 1px solid var(--line2);
+ color: var(--mut);
+}
+.cmdk-item.active .cmdk-it-ic {
+ background: var(--panel);
+ border-color: var(--indigo-line);
+ color: var(--indigo-br);
+}
+.cmdk-it-ic svg {
+ width: 17px;
+ height: 17px;
+}
+.cmdk-it-body {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+ flex: 1;
+}
+.cmdk-it-t {
+ font-size: 14.5px;
+ font-weight: 600;
+ color: var(--txt);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.cmdk-it-t em {
+ font-style: normal;
+ font-family: var(--mono);
+ font-size: 10px;
+ font-weight: 500;
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ border-radius: 5px;
+ padding: 1px 6px;
+}
+.cmdk-it-d {
+ font-size: 12.5px;
+ color: var(--mut);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cmdk-it-go {
+ font-family: var(--mono);
+ font-size: 13px;
+ color: var(--dim);
+ opacity: 0;
+ flex: none;
+}
+.cmdk-item.active .cmdk-it-go {
+ opacity: 1;
+ color: var(--indigo-br);
+}
+.cmdk-empty {
+ padding: 34px 16px;
+ text-align: center;
+ color: var(--mut);
+ font-size: 14px;
+}
+.cmdk-foot {
+ display: flex;
+ gap: 18px;
+ padding: 11px 16px;
+ border-top: 1px solid var(--line);
+ color: var(--dim);
+ font-size: 11.5px;
+ font-family: var(--mono);
+}
+.cmdk-foot kbd {
+ font-family: var(--mono);
+ border: 1px solid var(--line2);
+ border-radius: 4px;
+ padding: 0 5px;
+ margin-right: 3px;
+ color: var(--mut);
+}
+.cmdk-foot span {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+}
+.kbar,
+.side-search {
+ cursor: pointer;
+}
+
+/* ---------- search palette (React renders only when open) ---------- */
+.cmdk {
+ display: flex;
+}
+.cmdk-title {
+ font-size: 14px;
+ color: var(--txt);
+ font-weight: 500;
+}
+.cmdk-section {
+ margin-left: auto;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--dim);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+.cmdk-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ width: 100%;
+ text-align: left;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+}
diff --git a/docs/app/styles/diagrams.css b/docs/app/styles/diagrams.css
new file mode 100644
index 00000000..950f5cdb
--- /dev/null
+++ b/docs/app/styles/diagrams.css
@@ -0,0 +1,1081 @@
+/* ===== diagram components (architecture) ===== */
+/* --- job lifecycle state graph --- */
+.lc {
+ position: relative;
+ margin: 24px 0;
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ padding: 20px 16px;
+ background: linear-gradient(180deg, var(--bg-soft), var(--panel));
+ overflow: hidden;
+}
+.lc svg {
+ width: 100%;
+ height: auto;
+ display: block;
+ overflow: visible;
+}
+.lc-node rect {
+ fill: var(--panel);
+ stroke: var(--line2);
+ stroke-width: 1.5;
+}
+.lc-nt {
+ fill: var(--txt);
+ font-size: 16px;
+ font-weight: 600;
+ font-family: var(--sans);
+}
+.lc-nc {
+ fill: var(--dim);
+ font-size: 11px;
+ font-family: var(--mono);
+}
+.lc-elabel {
+ fill: var(--mut);
+ font-size: 11.5px;
+ font-family: var(--mono);
+}
+.lc-node.s-pending rect {
+ stroke: var(--line3);
+}
+.lc-node.s-pending .lc-sd {
+ fill: var(--dim);
+}
+.lc-node.s-running rect {
+ stroke: var(--indigo-line);
+ fill: var(--indigo-soft);
+}
+.lc-node.s-running .lc-sd {
+ fill: var(--indigo);
+}
+.lc-node.s-complete rect {
+ stroke: color-mix(in oklch, var(--grn) 50%, transparent);
+}
+.lc-node.s-complete .lc-sd {
+ fill: var(--grn);
+}
+.lc-node.s-failed rect {
+ stroke: color-mix(in oklch, var(--amber) 50%, transparent);
+}
+.lc-node.s-failed .lc-sd {
+ fill: var(--amber);
+}
+.lc-node.s-dead rect {
+ stroke: color-mix(in oklch, var(--red) 50%, transparent);
+}
+.lc-node.s-dead .lc-sd {
+ fill: var(--red);
+}
+.lc-node.s-cancel rect {
+ stroke: var(--line3);
+ stroke-dasharray: 4 4;
+}
+.lc-node.s-cancel .lc-sd {
+ fill: var(--mut);
+}
+.lc-token {
+ position: absolute;
+ top: 21.4%;
+ left: 15%;
+ width: 11px;
+ height: 11px;
+ border-radius: 50%;
+ margin: -5.5px 0 0 -5.5px;
+ background: var(--indigo);
+ box-shadow: 0 0 12px var(--indigo);
+ z-index: 2;
+}
+@media (prefers-reduced-motion: no-preference) {
+ .lc-node.s-running .lc-sd {
+ animation: blip 1.4s infinite;
+ }
+ .lc-token {
+ animation: lctravel 4.4s cubic-bezier(0.5, 0, 0.4, 1) infinite;
+ }
+}
+@keyframes lctravel {
+ 0% {
+ left: 15%;
+ opacity: 0;
+ }
+ 6% {
+ opacity: 1;
+ }
+ 34% {
+ left: 50%;
+ opacity: 1;
+ }
+ 46% {
+ left: 50%;
+ opacity: 1;
+ }
+ 74% {
+ left: 84.5%;
+ opacity: 1;
+ }
+ 86% {
+ left: 84.5%;
+ opacity: 1;
+ }
+ 100% {
+ left: 84.5%;
+ opacity: 0;
+ }
+}
+
+/* --- scheduler poll cycle --- */
+.pc-wrap {
+ display: flex;
+ align-items: center;
+ gap: 30px;
+ flex-wrap: wrap;
+ justify-content: center;
+ margin: 24px 0;
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ padding: 26px;
+ background: linear-gradient(180deg, var(--bg-soft), var(--panel));
+}
+.pc-svg {
+ width: 230px;
+ height: 230px;
+ flex: none;
+}
+.pc-center {
+ fill: var(--txt);
+ font-size: 30px;
+ font-weight: 600;
+ font-family: var(--mono);
+ letter-spacing: -0.02em;
+}
+.pc-centersub {
+ fill: var(--dim);
+ font-size: 12px;
+ font-family: var(--mono);
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+}
+.pc-sweep {
+ transform-origin: 180px 180px;
+}
+@media (prefers-reduced-motion: no-preference) {
+ .pc-sweep {
+ animation: pcspin 3.4s linear infinite;
+ }
+}
+@keyframes pcspin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+.pc-legend {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ min-width: 220px;
+}
+.pc-leg {
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ font-size: 14px;
+}
+.pc-leg .pc-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ flex: none;
+}
+.pc-leg .pc-name {
+ font-family: var(--mono);
+ font-size: 13.5px;
+ color: var(--txt);
+ font-weight: 500;
+}
+.pc-leg .pc-cad {
+ margin-left: auto;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--dim);
+}
+
+/* --- mesh live demo --- */
+.mesh-demo {
+ position: relative;
+ margin: 24px 0;
+ border: 1px solid var(--line2);
+ border-radius: 18px;
+ overflow: hidden;
+ background: radial-gradient(
+ 120% 90% at 50% 40%,
+ var(--bg-soft),
+ var(--panel)
+ );
+}
+.mesh-demo .mesh-toolbar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 5;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 13px 16px;
+ flex-wrap: wrap;
+}
+.mesh-stats {
+ display: flex;
+ gap: 18px;
+ font-size: 12.5px;
+ color: var(--mut);
+ font-family: var(--mono);
+}
+.mesh-stats .mstat b {
+ color: var(--indigo-br);
+ font-weight: 600;
+ font-size: 14px;
+}
+.mesh-btns {
+ display: flex;
+ gap: 8px;
+}
+.mesh-btn {
+ font-family: var(--mono);
+ font-size: 12px;
+ font-weight: 500;
+ padding: 7px 13px;
+ border-radius: 9px;
+ cursor: pointer;
+ white-space: nowrap;
+ background: var(--panel);
+ border: 1px solid var(--line2);
+ color: var(--txt);
+ transition:
+ border-color 0.15s,
+ background 0.15s;
+}
+.mesh-btn:hover {
+ border-color: var(--indigo);
+ color: var(--indigo-br);
+}
+.mesh-stepper {
+ display: inline-flex;
+ align-items: center;
+ gap: 0;
+ border: 1px solid var(--line2);
+ border-radius: 9px;
+ background: var(--panel);
+ overflow: hidden;
+}
+.mesh-stepper button {
+ width: 30px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ color: var(--txt);
+ font-size: 17px;
+ cursor: pointer;
+ display: grid;
+ place-items: center;
+ transition:
+ background 0.15s,
+ color 0.15s;
+ font-family: var(--mono);
+ line-height: 1;
+}
+.mesh-stepper button:hover:not(:disabled) {
+ background: var(--indigo-soft);
+ color: var(--indigo-br);
+}
+.mesh-stepper button:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+}
+.mesh-count {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--mut);
+ padding: 0 10px;
+ border-left: 1px solid var(--line2);
+ border-right: 1px solid var(--line2);
+ white-space: nowrap;
+}
+.mesh-count b {
+ color: var(--txt);
+ font-weight: 600;
+}
+/* world-map backdrop */
+.mesh-map {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+.mesh-map .grat {
+ stroke: var(--line);
+ stroke-width: 1;
+ fill: none;
+}
+.mesh-map .land {
+ fill: var(--line);
+ opacity: 0.55;
+}
+[data-theme="light"] .mesh-map .land {
+ opacity: 0.7;
+}
+.mesh-map .link {
+ stroke: var(--line2);
+ stroke-width: 1.2;
+ fill: none;
+ stroke-dasharray: 2 6;
+ stroke-linecap: round;
+ opacity: 0.6;
+ transition:
+ opacity 0.2s,
+ stroke 0.2s;
+}
+.mesh-map .link.on {
+ stroke: var(--indigo-line);
+ opacity: 1;
+ stroke-dasharray: none;
+}
+.mesh-map.focus .link {
+ opacity: 0.12;
+}
+.mesh-map.focus .link.lit {
+ opacity: 1;
+ stroke: var(--indigo);
+ stroke-dasharray: none;
+}
+.mesh-map .ping {
+ stroke: var(--cyan);
+ stroke-width: 2;
+ fill: none;
+ opacity: 0;
+}
+
+/* latency labels on links (shown on focus) */
+.mlat {
+ position: absolute;
+ z-index: 3;
+ transform: translate(-50%, -50%);
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--cyan);
+ background: color-mix(in oklch, var(--panel) 88%, transparent);
+ border: 1px solid color-mix(in oklch, var(--cyan) 32%, transparent);
+ border-radius: 6px;
+ padding: 1px 6px;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.18s;
+}
+.mlat.show {
+ opacity: 1;
+}
+
+/* region node (callout card offset from its geo pin) */
+.mw {
+ position: absolute;
+ transform: translate(-50%, -50%);
+ z-index: 2;
+ width: 140px;
+ cursor: pointer;
+ background: var(--panel);
+ border: 1px solid var(--line2);
+ border-radius: 12px;
+ padding: 9px 11px;
+ box-shadow: var(--shadow-md);
+ transition:
+ border-color 0.25s,
+ box-shadow 0.25s;
+}
+.mw:hover {
+ z-index: 6;
+ border-color: var(--indigo-line);
+}
+.mw.idle {
+ opacity: 0.85;
+}
+.mw.hot {
+ border-color: color-mix(in oklch, var(--amber) 55%, transparent);
+ box-shadow:
+ 0 0 0 1px color-mix(in oklch, var(--amber) 30%, transparent),
+ var(--shadow-md);
+}
+.mw.thief {
+ border-color: var(--indigo-line);
+ box-shadow:
+ 0 0 0 2px var(--indigo-line),
+ var(--shadow-md);
+}
+.mw-head {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ margin-bottom: 6px;
+}
+.mw-id {
+ font-family: var(--mono);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--txt);
+ letter-spacing: -0.01em;
+ white-space: nowrap;
+}
+.mw-city {
+ font-family: var(--mono);
+ font-size: 9.5px;
+ color: var(--dim);
+ white-space: nowrap;
+ margin-left: auto;
+}
+.mw-pulse {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--dim);
+ flex: none;
+}
+.mw-pulse.go {
+ background: var(--grn);
+ box-shadow: 0 0 10px var(--grn);
+ animation: mwpulse 0.6s ease-out;
+}
+@keyframes mwpulse {
+ 0% {
+ transform: scale(0.6);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(1.4);
+ opacity: 0.4;
+ }
+}
+.mw-q {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ min-height: 24px;
+ align-content: flex-start;
+}
+.mw-q i {
+ width: 10px;
+ height: 10px;
+ border-radius: 3px;
+ background: linear-gradient(180deg, var(--indigo-br), var(--indigo));
+ display: block;
+ animation: mwjob 0.3s ease-out;
+}
+.mw.hot .mw-q i {
+ background: linear-gradient(
+ 180deg,
+ var(--amber),
+ color-mix(in oklch, var(--amber) 70%, #000)
+ );
+}
+@keyframes mwjob {
+ from {
+ transform: scale(0);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+.mw-q u {
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--mut);
+ font-style: normal;
+ align-self: center;
+ margin-left: 2px;
+}
+.mw-load {
+ margin-top: 6px;
+ font-family: var(--mono);
+ font-size: 10px;
+ color: var(--dim);
+}
+.mw-load .mw-n {
+ color: var(--txt2);
+ font-weight: 600;
+}
+
+/* geo pin + callout connector */
+.mnode {
+ position: absolute;
+ z-index: 1;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ background: var(--indigo);
+ box-shadow:
+ 0 0 0 4px var(--indigo-soft),
+ 0 0 10px var(--indigo);
+ transition:
+ background 0.25s,
+ box-shadow 0.25s;
+}
+.mnode.hot {
+ background: var(--amber);
+ box-shadow:
+ 0 0 0 4px color-mix(in oklch, var(--amber) 16%, transparent),
+ 0 0 10px var(--amber);
+}
+.mesh-map .callout {
+ stroke: var(--line3);
+ stroke-width: 1;
+ stroke-dasharray: 2 4;
+ fill: none;
+ opacity: 0.7;
+}
+
+/* traveling packets */
+.mjob {
+ position: absolute;
+ z-index: 4;
+ width: 11px;
+ height: 11px;
+ border-radius: 3px;
+ left: 0;
+ top: 0;
+ background: linear-gradient(180deg, var(--indigo-br), var(--indigo));
+ box-shadow: 0 0 10px var(--indigo);
+ pointer-events: none;
+}
+.mjob.steal {
+ background: linear-gradient(
+ 180deg,
+ var(--cyan),
+ color-mix(in oklch, var(--cyan) 60%, #000)
+ );
+ box-shadow: 0 0 12px var(--cyan);
+ border-radius: 50%;
+}
+.mjob.gos {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--indigo-br);
+ box-shadow: 0 0 9px var(--indigo);
+}
+
+/* legend */
+.mesh-legend {
+ position: absolute;
+ left: 16px;
+ bottom: 13px;
+ z-index: 5;
+ display: flex;
+ gap: 15px;
+ flex-wrap: wrap;
+ font-family: var(--mono);
+ font-size: 10.5px;
+ color: var(--mut);
+}
+.mesh-legend span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.mesh-legend i {
+ width: 16px;
+ height: 0;
+ border-top: 2px dashed var(--line3);
+ display: inline-block;
+}
+.mesh-legend i.http {
+ border-top: 2px solid var(--indigo);
+}
+.mesh-legend i.steal {
+ border-top: 2px solid var(--cyan);
+}
+.mesh-legend i.dot {
+ width: 8px;
+ height: 8px;
+ border: 0;
+ border-radius: 50%;
+ background: var(--indigo);
+ box-shadow: 0 0 8px var(--indigo);
+}
+@media (max-width: 640px) {
+ .mw {
+ width: 118px;
+ }
+ .mesh-stats {
+ gap: 11px;
+ font-size: 11px;
+ }
+ .mw-city {
+ display: none;
+ }
+ .mesh-legend {
+ display: none;
+ }
+}
+
+/* --- layered stack (overview/worker/resources/mesh) --- */
+.archstack {
+ margin: 24px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ padding: 22px;
+ background: linear-gradient(180deg, var(--bg-soft), var(--panel));
+ position: relative;
+ overflow: hidden;
+}
+
+/* self-explanatory annotations (overview stack) */
+.arch-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 18px;
+ justify-content: center;
+ margin-bottom: 16px;
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--mut);
+}
+.al-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+.al-arrow {
+ display: inline-grid;
+ place-items: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 700;
+ line-height: 1;
+}
+.al-arrow.down {
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ color: var(--indigo-br);
+}
+.al-arrow.up {
+ background: color-mix(in oklch, var(--cyan) 12%, transparent);
+ border: 1px solid color-mix(in oklch, var(--cyan) 38%, transparent);
+ color: var(--cyan);
+}
+.archstack .lrole {
+ margin-left: auto;
+ flex: none;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--dim);
+ border: 1px dashed var(--line3);
+ border-radius: 100px;
+ padding: 3px 11px;
+ white-space: nowrap;
+}
+.archstack .bound .bdir {
+ color: var(--indigo-br);
+ font-weight: 700;
+ margin: 0 2px;
+}
+@media (max-width: 620px) {
+ .archstack .lrole {
+ display: none;
+ }
+}
+
+/* --- branching fork: scheduler routes to parallel pools --- */
+.archfork {
+ position: relative;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+ margin-top: 30px;
+}
+.archfork::after {
+ content: "";
+ position: absolute;
+ top: -22px;
+ left: 50%;
+ width: 2px;
+ height: 8px;
+ background: var(--line3);
+ transform: translateX(-50%);
+}
+.archfork::before {
+ content: "";
+ position: absolute;
+ top: -14px;
+ left: 25%;
+ right: 25%;
+ height: 14px;
+ border: 2px solid var(--line3);
+ border-bottom: none;
+ border-radius: 9px 9px 0 0;
+}
+.fork-route {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 9px;
+ margin-top: 14px;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--dim);
+ position: relative;
+}
+.fork-route::before,
+.fork-route::after {
+ content: "";
+ height: 1px;
+ flex: 1;
+ max-width: 120px;
+ background: linear-gradient(90deg, transparent, var(--line2), transparent);
+}
+.forkcol {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+.forkcol .forktag {
+ display: flex;
+ justify-content: center;
+ font-family: var(--mono);
+ font-size: 10.5px;
+ color: var(--indigo-br);
+ border: 1px dashed var(--indigo-line);
+ border-radius: 100px;
+ padding: 3px 11px;
+ background: var(--indigo-soft);
+ align-self: center;
+}
+.forkcol .layer {
+ flex: 1;
+}
+@media (max-width: 620px) {
+ .archfork {
+ grid-template-columns: 1fr;
+ gap: 22px;
+ margin-top: 22px;
+ }
+ .archfork::before,
+ .archfork::after {
+ display: none;
+ }
+}
+
+/* flow steps (dispatch / ordered) */
+.flowsteps {
+ margin: 20px 0;
+ list-style: none;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+.flowsteps li {
+ position: relative;
+ padding: 0 0 20px 44px;
+ font-size: 15px;
+ line-height: 1.6;
+ color: var(--txt2);
+}
+.flowsteps li::before {
+ content: counter(fs);
+ counter-increment: fs;
+ position: absolute;
+ left: 0;
+ top: -2px;
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ color: var(--indigo-br);
+ font-family: var(--mono);
+ font-weight: 600;
+ font-size: 13px;
+ display: grid;
+ place-items: center;
+}
+.flowsteps li::after {
+ content: "";
+ position: absolute;
+ left: 13.5px;
+ top: 28px;
+ bottom: 2px;
+ width: 1px;
+ background: var(--line2);
+}
+.flowsteps li:last-child {
+ padding-bottom: 0;
+}
+.flowsteps li:last-child::after {
+ display: none;
+}
+.flowsteps {
+ counter-reset: fs;
+}
+.flowsteps li code {
+ font-family: var(--mono);
+ font-size: 0.85em;
+ background: var(--panel2);
+ border: 1px solid var(--line);
+ padding: 1.5px 6px;
+ border-radius: 5px;
+ color: var(--txt);
+}
+.flowsteps li b {
+ color: var(--txt);
+ font-weight: 600;
+}
+
+/* sequence diagram */
+.seq {
+ margin: 22px 0;
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ padding: 22px;
+ background: var(--panel);
+ overflow-x: auto;
+}
+.seq-heads {
+ display: flex;
+ justify-content: space-around;
+ gap: 12px;
+ margin-bottom: 6px;
+ min-width: 420px;
+}
+.seq-head {
+ font-family: var(--mono);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--txt);
+ background: var(--panel2);
+ border: 1px solid var(--line2);
+ padding: 7px 14px;
+ border-radius: 9px;
+ flex: 1;
+ text-align: center;
+ max-width: 160px;
+}
+.seq-msgs {
+ display: flex;
+ flex-direction: column;
+ gap: 9px;
+ margin-top: 14px;
+ min-width: 420px;
+}
+.seq-msg {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 13px;
+ color: var(--txt2);
+}
+.seq-msg .ln {
+ flex: 1;
+ height: 2px;
+ border-radius: 2px;
+ background: linear-gradient(
+ 90deg,
+ color-mix(in oklch, var(--indigo) 35%, transparent),
+ var(--indigo)
+ );
+ position: relative;
+}
+.seq-msg .ln::after {
+ content: "";
+ position: absolute;
+ right: -1px;
+ top: -4px;
+ border-left: 10px solid var(--indigo);
+ border-top: 5px solid transparent;
+ border-bottom: 5px solid transparent;
+}
+.seq-msg.rev .ln {
+ background: linear-gradient(
+ 90deg,
+ var(--cyan),
+ color-mix(in oklch, var(--cyan) 35%, transparent)
+ );
+}
+.seq-msg.rev .ln::after {
+ right: auto;
+ left: -1px;
+ border-left: none;
+ border-right: 10px solid var(--cyan);
+}
+.seq-msg .lbl {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--txt2);
+ white-space: nowrap;
+}
+.seq-msg .self {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--dim);
+ font-style: italic;
+ padding-left: 8px;
+}
+
+/* schema entity grid */
+.schema-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 14px;
+ margin: 22px 0;
+}
+.entity {
+ border: 1px solid var(--line2);
+ border-radius: 13px;
+ overflow: hidden;
+ background: var(--panel);
+}
+.entity .ehead {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 11px 15px;
+ background: var(--panel2);
+ border-bottom: 1px solid var(--line);
+ font-family: var(--mono);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--txt);
+}
+.entity .ehead .ekey {
+ margin-left: auto;
+ font-size: 10px;
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ padding: 1px 7px;
+ border-radius: 5px;
+}
+.entity .erow {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 7px 15px;
+ font-family: var(--mono);
+ font-size: 12px;
+ border-top: 1px solid var(--line);
+}
+.entity .erow:first-of-type {
+ border-top: none;
+}
+.entity .erow .fn {
+ color: var(--txt2);
+}
+.entity .erow .ft {
+ margin-left: auto;
+ color: var(--dim);
+ font-size: 11px;
+}
+.entity .erow .pk {
+ color: var(--amber);
+ font-size: 10px;
+ font-weight: 600;
+}
+.entity .erow .fk {
+ color: var(--cyan);
+ font-size: 10px;
+ font-weight: 600;
+}
+@media (max-width: 860px) {
+ .schema-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* recovery timeline */
+.timeline {
+ margin: 22px 0;
+ border-left: 2px solid var(--line2);
+ padding-left: 0;
+ margin-left: 8px;
+}
+.timeline .tl {
+ position: relative;
+ padding: 0 0 22px 28px;
+}
+.timeline .tl::before {
+ content: "";
+ position: absolute;
+ left: -7px;
+ top: 2px;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--panel);
+ border: 2px solid var(--indigo);
+ box-shadow: 0 0 0 4px var(--bg);
+}
+.timeline .tl:last-child {
+ padding-bottom: 0;
+}
+.timeline .tt {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--indigo-br);
+ font-weight: 600;
+}
+.timeline .tb {
+ font-size: 14.5px;
+ color: var(--txt2);
+ margin-top: 3px;
+ line-height: 1.55;
+}
+.timeline .tb b {
+ color: var(--txt);
+ font-weight: 600;
+}
+
+/* page nav (prev/next) */
+.pagenav {
+ display: flex;
+ justify-content: space-between;
+ gap: 14px;
+ margin-top: 56px;
+ padding-top: 28px;
+ border-top: 1px solid var(--line);
+}
+.pagenav a {
+ flex: 1;
+ padding: 16px 18px;
+ border: 1px solid var(--line2);
+ border-radius: 13px;
+ transition: border-color 0.2s;
+ text-decoration: none;
+}
+.pagenav a:hover {
+ border-color: var(--indigo-line);
+}
+.pagenav .dir {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--dim);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+.pagenav .ttl {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--txt);
+ margin-top: 4px;
+}
+.pagenav .nx {
+ text-align: right;
+}
diff --git a/docs/app/styles/docs.css b/docs/app/styles/docs.css
new file mode 100644
index 00000000..3b150dd9
--- /dev/null
+++ b/docs/app/styles/docs.css
@@ -0,0 +1,1158 @@
+/* ============================================================
+ taskito docs — shared page styles (sidebar / article / toc)
+ Loaded after theme.css on every docs page.
+ ============================================================ */
+body {
+ overflow-x: hidden;
+}
+.docs-shell {
+ display: grid;
+ grid-template-columns: 282px minmax(0, 1fr) 240px;
+ max-width: 1500px;
+ margin: 0 auto;
+ align-items: start;
+}
+
+/* ---------- sidebar ---------- */
+.sidebar {
+ position: sticky;
+ top: 65px;
+ height: calc(100vh - 65px);
+ overflow-y: auto;
+ padding: 28px 18px 40px 32px;
+ border-right: 1px solid var(--line);
+}
+.sidebar::-webkit-scrollbar {
+ width: 7px;
+}
+.sidebar::-webkit-scrollbar-thumb {
+ background: var(--line2);
+ border-radius: 8px;
+}
+.side-search {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ width: 100%;
+ padding: 9px 12px;
+ border: 1px solid var(--line2);
+ border-radius: 10px;
+ background: var(--panel);
+ color: var(--dim);
+ font-size: 13px;
+ font-family: var(--mono);
+ cursor: pointer;
+ margin-bottom: 22px;
+ transition: border-color 0.2s;
+}
+.side-search:hover {
+ border-color: var(--line3);
+}
+.side-search .sk {
+ margin-left: auto;
+ display: flex;
+ gap: 4px;
+}
+.side-search kbd {
+ font-family: var(--mono);
+ border: 1px solid var(--line2);
+ border-radius: 4px;
+ padding: 0 5px;
+ font-size: 11px;
+}
+/* SDK switcher (Python | Node) — sits above the nav tree */
+.sdk-switch {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4px;
+ padding: 4px;
+ margin-bottom: 20px;
+ background: var(--panel2);
+ border: 1px solid var(--line2);
+ border-radius: 11px;
+}
+.sdk-opt {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ width: 100%;
+ padding: 8px 6px;
+ border: 0;
+ border-radius: 8px;
+ background: transparent;
+ font-family: var(--sans);
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--mut);
+ text-decoration: none;
+ cursor: pointer;
+ transition:
+ background 0.15s,
+ color 0.15s,
+ box-shadow 0.15s;
+}
+.sdk-opt svg {
+ width: 15px;
+ height: 15px;
+ flex: none;
+}
+.sdk-opt:hover {
+ color: var(--txt);
+}
+.sdk-opt.active {
+ color: var(--indigo-br);
+ background: var(--panel);
+ box-shadow:
+ 0 1px 3px rgba(0, 0, 0, 0.12),
+ 0 0 0 1px var(--indigo-line);
+}
+.nav-group {
+ margin-bottom: 22px;
+}
+.nav-group > .gt {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-family: var(--mono);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ color: var(--dim);
+ margin-bottom: 9px;
+ padding: 0 4px;
+}
+.nav-group > .gt svg {
+ width: 14px;
+ height: 14px;
+ color: var(--indigo-br);
+}
+.nav-item {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 7px 11px;
+ border-radius: 9px;
+ color: var(--mut);
+ font-size: 14px;
+ cursor: pointer;
+ transition:
+ background 0.15s,
+ color 0.15s;
+ position: relative;
+ text-decoration: none;
+}
+.nav-item:hover {
+ background: var(--panel2);
+ color: var(--txt);
+}
+.nav-item.active {
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+ font-weight: 500;
+}
+.nav-item.active::before {
+ content: "";
+ position: absolute;
+ left: -32px;
+ top: 6px;
+ bottom: 6px;
+ width: 2px;
+ background: var(--indigo);
+ border-radius: 2px;
+}
+.nav-sub {
+ margin-left: 6px;
+ border-left: 1px solid var(--line2);
+ padding-left: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+/* Collapsible nav: caret toggles on groups + subsections, auto-open on active. */
+.nav-caret {
+ flex: none;
+ margin-left: auto;
+ display: grid;
+ place-items: center;
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--dim);
+ border-radius: 6px;
+ cursor: pointer;
+ transition:
+ background 0.15s,
+ color 0.15s;
+}
+.nav-caret:hover {
+ background: var(--panel2);
+ color: var(--txt);
+}
+.nav-caret svg {
+ width: 13px;
+ height: 13px;
+ transition: transform 0.18s ease;
+}
+.nav-caret[data-open] svg {
+ transform: rotate(90deg);
+}
+.nav-sub-head {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}
+.nav-sub-head > .nav-item {
+ flex: 1;
+ min-width: 0;
+}
+
+/* ---------- article ---------- */
+.article {
+ padding: 40px 56px 96px;
+ min-width: 0;
+ max-width: 840px;
+}
+.crumb {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--dim);
+ font-family: var(--mono);
+ margin-bottom: 22px;
+}
+.crumb a:hover {
+ color: var(--mut);
+}
+.crumb .sep {
+ opacity: 0.5;
+}
+.article h1 {
+ font-size: 40px;
+ line-height: 1.08;
+ letter-spacing: -0.03em;
+ font-weight: 600;
+ margin-bottom: 12px;
+}
+.lead {
+ font-size: 18px;
+ line-height: 1.55;
+ color: var(--mut);
+ margin-bottom: 14px;
+}
+.article h2 {
+ font-size: 24px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ margin: 48px 0 14px;
+ scroll-margin-top: 90px;
+ display: flex;
+ align-items: center;
+ gap: 11px;
+}
+.article h2 .nbadge {
+ font-family: var(--mono);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--indigo-br);
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ display: grid;
+ place-items: center;
+ flex: none;
+}
+.article h3 {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 32px 0 10px;
+ scroll-margin-top: 90px;
+}
+.article p {
+ font-size: 15.5px;
+ line-height: 1.7;
+ color: var(--txt2);
+ margin: 12px 0;
+}
+.article p code,
+.article li code,
+.article td code {
+ font-family: var(--mono);
+ font-size: 0.85em;
+ background: var(--panel2);
+ border: 1px solid var(--line);
+ padding: 1.5px 6px;
+ border-radius: 5px;
+ color: var(--txt);
+}
+.article a {
+ color: var(--indigo-br);
+ text-decoration: none;
+ border-bottom: 1px solid var(--indigo-line);
+ transition: border-color 0.15s;
+}
+.article a:hover {
+ border-color: var(--indigo-br);
+}
+.article ul {
+ margin: 14px 0;
+ padding-left: 4px;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 9px;
+}
+.article ul li {
+ font-size: 15px;
+ line-height: 1.6;
+ color: var(--txt2);
+ padding-left: 22px;
+ position: relative;
+}
+.article ul li::before {
+ content: "";
+ position: absolute;
+ left: 4px;
+ top: 9px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--indigo);
+ opacity: 0.6;
+}
+
+/* code block */
+.codeblock {
+ margin: 18px 0;
+ border: 1px solid var(--line2);
+ border-radius: 13px;
+ overflow: hidden;
+ background: var(--panel);
+ box-shadow: var(--shadow-md);
+}
+.cb-bar {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--line);
+ background: color-mix(in oklch, var(--panel) 60%, #000);
+}
+[data-theme="light"] .cb-bar {
+ background: var(--panel2);
+}
+.cb-bar .lang {
+ font-family: var(--mono);
+ font-size: 11.5px;
+ color: var(--mut);
+ text-transform: lowercase;
+}
+.cb-bar .fname {
+ font-family: var(--mono);
+ font-size: 12px;
+ color: var(--txt2);
+}
+.cb-copy {
+ margin-left: auto;
+ background: transparent;
+ border: none;
+ color: var(--dim);
+ cursor: pointer;
+ display: grid;
+ place-items: center;
+ padding: 5px;
+ border-radius: 6px;
+ transition:
+ color 0.15s,
+ background 0.15s;
+}
+.cb-copy:hover {
+ color: var(--txt);
+ background: var(--panel3);
+}
+.cb-copy svg {
+ width: 15px;
+ height: 15px;
+}
+.codeblock pre {
+ margin: 0;
+ padding: 16px 18px;
+ overflow-x: auto;
+ font-family: var(--mono);
+ font-size: 13.5px;
+ line-height: 1.7;
+ color: var(--txt);
+}
+.codeblock pre::-webkit-scrollbar {
+ height: 8px;
+}
+.codeblock pre::-webkit-scrollbar-thumb {
+ background: var(--line3);
+ border-radius: 8px;
+}
+.tok-flag {
+ color: var(--cyan);
+}
+.tok-cmd {
+ color: var(--indigo-br);
+ font-weight: 600;
+}
+.tok-comment {
+ color: var(--dim);
+}
+.tok-str {
+ color: #9ce08a;
+}
+[data-theme="light"] .tok-str {
+ color: #1a8f3a;
+}
+
+/* tabs */
+.tabs {
+ margin: 18px 0;
+}
+.tablist {
+ display: flex;
+ gap: 4px;
+ border-bottom: 1px solid var(--line);
+ margin-bottom: 0;
+}
+.tab {
+ padding: 9px 15px;
+ font-size: 13.5px;
+ font-weight: 500;
+ color: var(--mut);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition:
+ color 0.15s,
+ border-color 0.15s;
+ font-family: var(--sans);
+}
+.tab:hover {
+ color: var(--txt);
+}
+.tab.active {
+ color: var(--indigo-br);
+ border-bottom-color: var(--indigo);
+}
+.tabpanel {
+ display: none;
+}
+.tabpanel.active {
+ display: block;
+}
+.tabpanel .codeblock {
+ margin-top: 14px;
+}
+
+/* callout (+ variants) */
+.callout {
+ display: flex;
+ gap: 13px;
+ margin: 20px 0;
+ padding: 15px 17px;
+ border-radius: 12px;
+ border: 1px solid var(--indigo-line);
+ background: var(--indigo-soft);
+}
+.callout .ci {
+ flex: none;
+ width: 22px;
+ height: 22px;
+ color: var(--indigo-br);
+}
+.callout .cc {
+ font-size: 14.5px;
+ line-height: 1.6;
+ color: var(--txt2);
+}
+.callout .cc strong {
+ color: var(--txt);
+ font-weight: 600;
+}
+.callout .ctitle {
+ display: block;
+ font-weight: 600;
+ color: var(--txt);
+ margin-bottom: 3px;
+ font-size: 14.5px;
+}
+.callout.info {
+ border-color: color-mix(in oklch, var(--cyan) 40%, transparent);
+ background: color-mix(in oklch, var(--cyan) 9%, transparent);
+}
+.callout.info .ci {
+ color: var(--cyan);
+}
+.callout.warn {
+ border-color: color-mix(in oklch, var(--amber) 45%, transparent);
+ background: color-mix(in oklch, var(--amber) 10%, transparent);
+}
+.callout.warn .ci {
+ color: var(--amber);
+}
+
+/* doc cards (concept / next-steps) */
+.next-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+/* icon card grid (index pages) */
+.icard-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 14px;
+ margin: 24px 0;
+}
+.icard {
+ display: block;
+ padding: 20px;
+ border: 1px solid var(--line2);
+ border-radius: 15px;
+ background: var(--panel);
+ transition:
+ border-color 0.2s,
+ transform 0.2s,
+ box-shadow 0.2s;
+ position: relative;
+ overflow: hidden;
+}
+.icard:hover {
+ border-color: var(--indigo-line);
+ transform: translateY(-3px);
+ box-shadow: var(--shadow-md);
+}
+.icard .ic {
+ width: 40px;
+ height: 40px;
+ border-radius: 11px;
+ display: grid;
+ place-items: center;
+ margin-bottom: 14px;
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ color: var(--indigo-br);
+}
+.icard .ic svg {
+ width: 20px;
+ height: 20px;
+}
+.icard .it {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--txt);
+ display: flex;
+ align-items: center;
+ gap: 7px;
+}
+.icard .it .arr {
+ color: var(--dim);
+ transition:
+ transform 0.2s,
+ color 0.2s;
+ font-size: 14px;
+}
+.icard:hover .it .arr {
+ transform: translateX(3px);
+ color: var(--indigo-br);
+}
+.icard .id {
+ font-size: 13.5px;
+ color: var(--mut);
+ margin-top: 6px;
+ line-height: 1.55;
+}
+@media (max-width: 860px) {
+ .icard-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.next-card {
+ display: block;
+ padding: 16px 18px;
+ border: 1px solid var(--line2);
+ border-radius: 13px;
+ background: var(--panel);
+ transition:
+ border-color 0.2s,
+ transform 0.2s;
+}
+.next-card:hover {
+ border-color: var(--indigo-line);
+ transform: translateY(-2px);
+}
+.next-card .nt {
+ font-size: 14.5px;
+ font-weight: 600;
+ color: var(--txt);
+ display: flex;
+ align-items: center;
+ gap: 7px;
+}
+.next-card .nt .arr {
+ color: var(--dim);
+ transition: transform 0.2s;
+}
+.next-card:hover .nt .arr {
+ transform: translateX(3px);
+ color: var(--indigo-br);
+}
+.next-card .nd {
+ font-size: 13px;
+ color: var(--mut);
+ margin-top: 5px;
+ line-height: 1.5;
+}
+
+/* markdown table */
+.md-table {
+ margin: 20px 0;
+ border: 1px solid var(--line2);
+ border-radius: 13px;
+ overflow-x: auto;
+ background: var(--panel);
+}
+.md-table table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 14.5px;
+}
+.md-table th {
+ text-align: left;
+ font-family: var(--mono);
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--dim);
+ font-weight: 600;
+ padding: 13px 18px;
+ background: var(--panel2);
+ border-bottom: 1px solid var(--line);
+ white-space: nowrap;
+}
+.md-table td {
+ padding: 13px 18px;
+ border-top: 1px solid var(--line);
+ color: var(--txt2);
+ vertical-align: top;
+}
+.md-table tr td:first-child {
+ white-space: nowrap;
+}
+.md-table a {
+ color: var(--indigo-br);
+ border-bottom: 1px solid var(--indigo-line);
+}
+.md-table td b,
+.md-table .yes {
+ color: var(--grn);
+ font-weight: 600;
+}
+.md-table .no {
+ color: var(--mut);
+}
+/* feature-matrix taskito column highlight */
+.md-table.matrix th:nth-child(2),
+.md-table.matrix td:nth-child(2) {
+ background: var(--indigo-soft);
+}
+.md-table.matrix th:nth-child(2) {
+ color: var(--indigo-br);
+}
+
+/* when-to-use split */
+.use-split {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+ margin: 22px 0;
+}
+.use-card {
+ border: 1px solid var(--line2);
+ border-radius: 14px;
+ padding: 20px;
+ background: var(--panel);
+}
+.use-card.yes {
+ border-color: color-mix(in oklch, var(--grn) 35%, transparent);
+}
+.use-card.no {
+ border-color: color-mix(in oklch, var(--amber) 30%, transparent);
+}
+.use-card h3 {
+ font-size: 15px;
+ font-weight: 600;
+ margin: 0 0 12px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.use-card.yes h3 {
+ color: var(--grn);
+}
+.use-card.no h3 {
+ color: var(--amber);
+}
+.use-card ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.use-card li {
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--txt2);
+ padding-left: 20px;
+ position: relative;
+}
+.use-card li::before {
+ content: "";
+ position: absolute;
+ left: 3px;
+ top: 8px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+}
+.use-card.yes li::before {
+ background: var(--grn);
+}
+.use-card.no li::before {
+ background: var(--amber);
+}
+.use-card li b {
+ color: var(--txt);
+ font-weight: 600;
+}
+@media (max-width: 760px) {
+ .use-split {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* FAQ accordion */
+.faq-list {
+ margin: 16px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.faq-item {
+ border: 1px solid var(--line2);
+ border-radius: 13px;
+ background: var(--panel);
+ overflow: hidden;
+ transition: border-color 0.2s;
+}
+.faq-item.open {
+ border-color: var(--indigo-line);
+}
+.faq-q {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ padding: 16px 18px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ font-family: var(--sans);
+ font-size: 15.5px;
+ font-weight: 600;
+ color: var(--txt);
+}
+.faq-q .faq-chevron {
+ margin-left: auto;
+ flex: none;
+ color: var(--mut);
+ transition: transform 0.22s;
+}
+.faq-item.open .faq-q .faq-chevron {
+ transform: rotate(180deg);
+ color: var(--indigo-br);
+}
+.faq-q .faq-qmark {
+ flex: none;
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ display: grid;
+ place-items: center;
+ font-family: var(--mono);
+ font-size: 13px;
+ font-weight: 600;
+ background: var(--indigo-soft);
+ border: 1px solid var(--indigo-line);
+ color: var(--indigo-br);
+}
+.faq-a {
+ display: none;
+ padding: 0 18px 18px 57px;
+}
+.faq-item.open .faq-a {
+ display: block;
+}
+.faq-a p {
+ margin: 6px 0;
+ font-size: 14.5px;
+ line-height: 1.65;
+ color: var(--txt2);
+}
+.faq-a p code,
+.faq-a li code {
+ font-family: var(--mono);
+ font-size: 0.85em;
+ background: var(--panel2);
+ border: 1px solid var(--line);
+ padding: 1.5px 6px;
+ border-radius: 5px;
+ color: var(--txt);
+}
+.faq-a .codeblock {
+ margin: 12px 0;
+}
+.faq-a ul {
+ margin: 10px 0;
+ padding: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.faq-a ul li {
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--txt2);
+ padding-left: 20px;
+ position: relative;
+}
+.faq-a ul li::before {
+ content: "";
+ position: absolute;
+ left: 4px;
+ top: 9px;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--indigo);
+ opacity: 0.6;
+}
+@media (max-width: 760px) {
+ .faq-a {
+ padding-left: 18px;
+ }
+}
+
+/* when-to / general */
+.use-split + h2 {
+ margin-top: 40px;
+}
+
+/* architecture stack diagram */
+.archstack {
+ margin: 26px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ padding: 22px;
+ background: linear-gradient(180deg, var(--bg-soft), var(--panel));
+ position: relative;
+ overflow: hidden;
+}
+.archstack .layer {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 18px;
+ border-radius: 13px;
+ border: 1px solid var(--line2);
+ background: var(--panel);
+ position: relative;
+ z-index: 1;
+}
+.archstack .layer.py {
+ border-color: var(--indigo-line);
+ background: linear-gradient(100deg, var(--indigo-soft), var(--panel));
+}
+.archstack .layer.rust {
+ border-color: color-mix(in oklch, var(--amber) 35%, transparent);
+ background: linear-gradient(
+ 100deg,
+ color-mix(in oklch, var(--amber) 8%, transparent),
+ var(--panel)
+ );
+}
+.archstack .ltag {
+ flex: none;
+ font-family: var(--mono);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ padding: 5px 10px;
+ border-radius: 7px;
+ color: #fff;
+}
+.archstack .ltag.t-py {
+ background: var(--indigo);
+}
+.archstack .ltag.t-rust {
+ background: var(--amber);
+ color: #1a1206;
+}
+.archstack .ltag.t-store {
+ background: var(--mut);
+}
+.archstack .lbody {
+ min-width: 0;
+}
+.archstack .lt {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--txt);
+}
+.archstack .ld {
+ font-size: 13px;
+ color: var(--mut);
+ margin-top: 2px;
+ line-height: 1.5;
+}
+.archstack .bound {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 9px;
+ padding: 8px 0;
+ font-family: var(--mono);
+ font-size: 11px;
+ color: var(--dim);
+ position: relative;
+}
+.archstack .bound::before,
+.archstack .bound::after {
+ content: "";
+ height: 1px;
+ flex: 1;
+ max-width: 120px;
+ background: linear-gradient(90deg, transparent, var(--line2), transparent);
+}
+.archstack .pyo3 {
+ color: var(--indigo-br);
+ border: 1px solid var(--indigo-line);
+ border-radius: 100px;
+ padding: 2px 10px;
+ background: var(--indigo-soft);
+}
+.archstack .pulse {
+ position: absolute;
+ left: 50%;
+ top: 0;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--indigo);
+ box-shadow: 0 0 12px var(--indigo);
+ transform: translateX(-50%);
+ opacity: 0;
+}
+@media (prefers-reduced-motion: no-preference) {
+ .archstack .pulse {
+ animation: archflow 3.2s ease-in-out infinite;
+ }
+}
+@keyframes archflow {
+ 0% {
+ top: 8%;
+ opacity: 0;
+ }
+ 10% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 90% {
+ opacity: 1;
+ }
+ 100% {
+ top: 92%;
+ opacity: 0;
+ }
+}
+
+/* ---------- TOC ---------- */
+.toc {
+ position: sticky;
+ top: 65px;
+ height: calc(100vh - 65px);
+ overflow-y: auto;
+ padding: 40px 24px 40px 8px;
+}
+.toc h4 {
+ font-family: var(--mono);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ color: var(--dim);
+ margin-bottom: 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.toc h4 svg {
+ width: 14px;
+ height: 14px;
+}
+.toc-link {
+ display: block;
+ font-size: 13px;
+ color: var(--mut);
+ padding: 5px 0 5px 14px;
+ border-left: 2px solid var(--line2);
+ transition:
+ color 0.15s,
+ border-color 0.15s;
+ cursor: pointer;
+}
+.toc-link:hover {
+ color: var(--txt);
+}
+.toc-link.sub {
+ padding-left: 26px;
+ font-size: 12.5px;
+}
+.toc-link.active {
+ color: var(--indigo-br);
+ border-left-color: var(--indigo);
+}
+
+/* mobile */
+.menu-btn {
+ display: none;
+}
+@media (max-width: 1180px) {
+ .docs-shell {
+ grid-template-columns: 262px minmax(0, 1fr);
+ }
+ .toc {
+ display: none;
+ }
+}
+@media (max-width: 860px) {
+ .docs-shell {
+ grid-template-columns: 1fr;
+ }
+ .sidebar {
+ display: none;
+ }
+ .article {
+ padding: 32px 22px 72px;
+ }
+ .menu-btn {
+ display: grid;
+ }
+ .next-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Error boundary fallback */
+.error-page {
+ max-width: 680px;
+ margin: 0 auto;
+ padding: 120px 32px;
+ text-align: center;
+}
+.error-page h1 {
+ font-size: 32px;
+ letter-spacing: -0.02em;
+ margin-bottom: 16px;
+}
+.error-page a {
+ color: var(--indigo-br);
+ border-bottom: 1px solid var(--indigo-line);
+}
+
+/* ---------- React port additions (nested nav, link-group headers, prev/next blocks) ---------- */
+.gt.gt-link {
+ color: var(--dim);
+ transition: color 0.15s;
+}
+.gt.gt-link:hover {
+ color: var(--txt);
+}
+/* Group header with no index page: the label toggles the section (clickable). */
+.gt-toggle {
+ flex: 1;
+ min-width: 0;
+ text-align: left;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ font: inherit;
+ letter-spacing: inherit;
+ text-transform: inherit;
+ color: inherit;
+ cursor: pointer;
+ transition: color 0.15s;
+}
+.gt-toggle:hover {
+ color: var(--txt);
+}
+.nav-subsection {
+ margin: 2px 0;
+}
+.nav-subsection .nav-sub {
+ margin-left: 11px;
+ padding-left: 9px;
+ border-left: 1px solid var(--line);
+}
+.nav-item-label {
+ color: var(--dim);
+ font-weight: 600;
+ cursor: default;
+}
+.nav-item-label:hover {
+ background: transparent;
+ color: var(--dim);
+}
+.pagenav .dir,
+.pagenav .ttl {
+ display: block;
+}
+.next-card .nt svg {
+ width: 16px;
+ height: 16px;
+ color: var(--indigo-br);
+}
+/* search modal (Phase 6 swaps the body in) */
+.cmdk-panel {
+ width: min(620px, 92vw);
+ margin: 11vh auto 0;
+ padding: 22px 20px;
+ border: 1px solid var(--line2);
+ border-radius: 16px;
+ background: var(--panel);
+ color: var(--mut);
+ font-family: var(--mono);
+ font-size: 13px;
+}
diff --git a/docs/app/styles/landing.css b/docs/app/styles/landing.css
new file mode 100644
index 00000000..b7e55e07
--- /dev/null
+++ b/docs/app/styles/landing.css
@@ -0,0 +1,1358 @@
+/* Landing page — authentic CSS, verbatim from the prototype index.html
- );
-}
diff --git a/docs/src/components/animated/architecture-stack.tsx b/docs/src/components/animated/architecture-stack.tsx
deleted file mode 100644
index a55c1c13..00000000
--- a/docs/src/components/animated/architecture-stack.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import {
- Arrow,
- DiagramFrame,
- MotionStyles,
- NodeBox,
- Panel,
-} from "./_primitives";
-
-const VIEW_W = 640;
-const VIEW_H = 296;
-
-const PANELS = [
- { id: "python", label: "python layer", y: 24, h: 76, cy: 62 },
- { id: "rust", label: "rust core", y: 110, h: 76, cy: 148 },
- { id: "storage", label: "storage", y: 196, h: 76, cy: 234 },
-] as const;
-
-type NodeData = {
- id: string;
- label: string;
- hint?: string;
- cx: number;
- cy: number;
- variant?: "primary";
-};
-
-const NODES: NodeData[] = [
- { id: "queue", label: "Queue", hint: ".delay()", cx: 150, cy: 62 },
- { id: "task", label: "@task fn", hint: "Python", cx: 490, cy: 62 },
- { id: "pyqueue", label: "PyQueue", hint: "PyO3", cx: 150, cy: 148 },
- {
- id: "scheduler",
- label: "Scheduler",
- hint: "Tokio · 50ms",
- cx: 320,
- cy: 148,
- variant: "primary",
- },
- {
- id: "workerpool",
- label: "Worker Pool",
- hint: "Rust threads",
- cx: 490,
- cy: 148,
- },
- { id: "sqlite", label: "SQLite", hint: "default", cx: 260, cy: 234 },
- { id: "postgres", label: "PostgreSQL", hint: "scale-out", cx: 420, cy: 234 },
-];
-
-const ARROWS = [
- { id: "enqueue", d: "M 150 85 L 150 125" },
- { id: "insert", d: "M 205 148 L 265 148" },
- { id: "dispatch", d: "M 375 148 L 435 148" },
- { id: "gil", d: "M 490 125 L 490 85" },
- {
- id: "persist",
- d: "M 320 172 Q 320 188 340 200",
- variant: "muted" as const,
- },
-];
-
-export function ArchitectureStack() {
- return (
-
- {PANELS.map((p) => (
-
- ))}
-
- {ARROWS.map((a) => (
-
- ))}
-
- {NODES.map((n) => (
-
- ))}
-
-
- acquire GIL
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/components/animated/crash-recovery-timeline.tsx b/docs/src/components/animated/crash-recovery-timeline.tsx
deleted file mode 100644
index d33f2045..00000000
--- a/docs/src/components/animated/crash-recovery-timeline.tsx
+++ /dev/null
@@ -1,287 +0,0 @@
-import {
- Arrow,
- Caption,
- DiagramFrame,
- MotionStyles,
- NodeBox,
- SKETCH_FILTER,
-} from "./_primitives";
-
-const VIEW_W = 760;
-const VIEW_H = 260;
-const TRACK_Y = 150;
-const TRACK_LEFT = 50;
-const TRACK_RIGHT = 730;
-const WORKER_Y = 60;
-
-type Marker = {
- id: string;
- x: number;
- time: string;
- label: string;
- variant?: "primary" | "danger" | "success";
-};
-
-const MARKERS: Marker[] = [
- { id: "enqueue", x: 80, time: "T=0", label: "enqueue", variant: "primary" },
- {
- id: "dispatch",
- x: 170,
- time: "T=1s",
- label: "dispatch",
- variant: "primary",
- },
- { id: "crash", x: 250, time: "T=5s", label: "crash", variant: "danger" },
- {
- id: "reap",
- x: 510,
- time: "T=300s",
- label: "reap_stale",
- variant: "primary",
- },
- { id: "retry", x: 600, time: "T=301s", label: "retry", variant: "primary" },
- { id: "done", x: 690, time: "T=302s", label: "complete", variant: "success" },
-];
-
-const POLLING_LEFT = 280;
-const POLLING_RIGHT = 480;
-
-export function CrashRecoveryTimeline() {
- return (
-
- {/* Worker boxes (above timeline) */}
-
-
-
- {/* dispatch arrows from timeline up to workers */}
-
-
-
- {/* horizontal timeline */}
-
-
-
- {/* polling-ticks region between crash and reap */}
-
-
- {/* polling annotation above the ticks (between crash and reap markers) */}
-
-
- {MARKERS.map((m) => (
-
- ))}
-
- {/* crash burst at worker A location */}
-
-
- {/* reap glow ring at T=300s marker */}
-
-
- {/* job dot animating across the timeline */}
-
-
-
-
- );
-}
-
-function MarkerPin({ marker }: { marker: Marker }) {
- const color =
- marker.variant === "danger"
- ? "var(--color-fd-muted-foreground)"
- : marker.variant === "success" || marker.variant === "primary"
- ? "var(--color-fd-primary)"
- : "var(--color-fd-muted-foreground)";
- return (
-
-
-
-
- {marker.time}
-
-
- {marker.label}
-
-
- );
-}
-
-function CrashBurst({ cx, cy }: { cx: number; cy: number }) {
- return (
-
-
-
-
-
- );
-}
-
-function PollingRegion({ x1, x2, y }: { x1: number; x2: number; y: number }) {
- const count = Math.floor((x2 - x1) / 14);
- return (
-
- {Array.from({ length: count }, (_, i) => (
-
- ))}
-
- );
-}
diff --git a/docs/src/components/animated/dispatch-sequence.tsx b/docs/src/components/animated/dispatch-sequence.tsx
deleted file mode 100644
index 1434d2eb..00000000
--- a/docs/src/components/animated/dispatch-sequence.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import {
- Arrow,
- Caption,
- DiagramFrame,
- Lifeline,
- MotionStyles,
- NodeBox,
-} from "./_primitives";
-
-const VIEW_W = 760;
-const VIEW_H = 380;
-
-type Lane = { id: string; label: string; cx: number };
-
-const LANES: Lane[] = [
- { id: "scheduler", label: "Scheduler", cx: 90 },
- { id: "db", label: "Storage", cx: 240 },
- { id: "rl", label: "RateLimiter", cx: 380 },
- { id: "wp", label: "WorkerPool", cx: 520 },
- { id: "worker", label: "Worker", cx: 660 },
-];
-
-const HEADER_Y = 24;
-const HEADER_H = 36;
-const LIFELINE_TOP = HEADER_Y + HEADER_H + 4;
-const LIFELINE_BOTTOM = 358;
-
-type Msg = {
- num: number;
- fromLane: string;
- toLane: string;
- label: string;
- y: number;
- reply?: boolean;
-};
-
-const MSGS: Msg[] = [
- {
- num: 1,
- fromLane: "scheduler",
- toLane: "db",
- label: "dequeue_from()",
- y: 88,
- },
- {
- num: 2,
- fromLane: "db",
- toLane: "scheduler",
- label: "pending job",
- y: 124,
- reply: true,
- },
- { num: 3, fromLane: "scheduler", toLane: "rl", label: "check(task)", y: 160 },
- {
- num: 4,
- fromLane: "rl",
- toLane: "scheduler",
- label: "ok",
- y: 196,
- reply: true,
- },
- {
- num: 5,
- fromLane: "scheduler",
- toLane: "wp",
- label: "mpsc.send(job)",
- y: 232,
- },
- { num: 6, fromLane: "wp", toLane: "worker", label: "execute()", y: 268 },
- {
- num: 7,
- fromLane: "worker",
- toLane: "scheduler",
- label: "result",
- y: 304,
- reply: true,
- },
- {
- num: 8,
- fromLane: "scheduler",
- toLane: "db",
- label: "handle_result()",
- y: 340,
- },
-];
-
-const TOTAL_DURATION = 14;
-const SLOT_PERCENT = 8;
-const TRAVEL_PERCENT = 6.5;
-const FADE_IN_PERCENT = 0.5;
-const FADE_OUT_PERCENT = TRAVEL_PERCENT + 0.5;
-
-function laneCx(id: string): number {
- const lane = LANES.find((l) => l.id === id);
- if (!lane) throw new Error(`unknown lane: ${id}`);
- return lane.cx;
-}
-
-export function DispatchSequence() {
- return (
-
- {LANES.map((lane) => (
-
-
-
-
- ))}
-
- {MSGS.map((msg) => {
- const x1 = laneCx(msg.fromLane);
- const x2 = laneCx(msg.toLane);
- const dir = x2 > x1 ? 1 : -1;
- const startX = x1 + dir * 6;
- const endX = x2 - dir * 6;
- return (
-
-
-
-
- {`${msg.num} `}
-
- {msg.label}
-
-
- );
- })}
-
-
-
- {/* animated tokens — one per message, staggered to fire in order */}
- {MSGS.map((msg, i) => (
-
- ))}
-
- keyframeFor(m)).join("\n")}
-`}
- animations={`
-${MSGS.map(
- (m) => `.taskito-seq .tok-${m.num} {
- animation: taskito-seq-msg-${m.num} ${TOTAL_DURATION}s linear infinite;
- filter: drop-shadow(0 0 6px var(--color-fd-primary));
-}`,
-).join("\n")}
-`}
- />
-
- );
-}
-
-function keyframeFor(msg: Msg): string {
- const startX = laneCx(msg.fromLane);
- const endX = laneCx(msg.toLane);
- return `
-@keyframes taskito-seq-msg-${msg.num} {
- 0% { cx: ${startX}px; cy: ${msg.y}px; opacity: 0; }
- ${FADE_IN_PERCENT}% { cx: ${startX}px; cy: ${msg.y}px; opacity: 1; }
- ${TRAVEL_PERCENT}% { cx: ${endX}px; cy: ${msg.y}px; opacity: 1; }
- ${FADE_OUT_PERCENT}% { cx: ${endX}px; cy: ${msg.y}px; opacity: 0; }
- ${SLOT_PERCENT}% { cx: ${startX}px; cy: ${msg.y}px; opacity: 0; }
- 100% { cx: ${startX}px; cy: ${msg.y}px; opacity: 0; }
-}`;
-}
diff --git a/docs/src/components/animated/entity-schema.tsx b/docs/src/components/animated/entity-schema.tsx
deleted file mode 100644
index e7d7d8e3..00000000
--- a/docs/src/components/animated/entity-schema.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-import { DiagramFrame, SKETCH_FILTER } from "./_primitives";
-
-const FONT_MONO =
- 'var(--font-mono), "IBM Plex Mono", ui-monospace, SFMono-Regular, monospace';
-
-type Field = {
- name: string;
- type: string;
- key?: "PK" | "FK";
-};
-
-type EntitySchemaProps = {
- name: string;
- fields: Field[];
-};
-
-const ROW_H = 22;
-const HEADER_H = 36;
-const PAD_X = 18;
-const SCHEMA_W = 360;
-
-export function EntitySchema({ name, fields }: EntitySchemaProps) {
- const h = HEADER_H + fields.length * ROW_H + 14;
- return (
-
-
-
-
- {name}
-
-
- table
-
-
- {fields.map((field, i) => (
-
- ))}
-
- );
-}
-
-function FieldRow({ field, y }: { field: Field; y: number }) {
- const tagX = SCHEMA_W - PAD_X - 30;
- const tagColor =
- field.key === "PK"
- ? "var(--color-fd-primary)"
- : "var(--color-fd-muted-foreground)";
- return (
-
-
- {field.name}
-
-
- {field.type}
-
- {field.key ? (
-
-
-
- {field.key}
-
-
- ) : null}
-
- );
-}
-
-type EntityRelationsProps = {
- caption?: string;
- relations: Array<{
- from: string;
- to: string;
- cardinality: string;
- label?: string;
- }>;
-};
-
-export function EntityRelations({ caption, relations }: EntityRelationsProps) {
- const names = Array.from(new Set(relations.flatMap((r) => [r.from, r.to])));
- const w = 600;
- const slotW = w / Math.max(names.length, 1);
- const boxW = Math.min(slotW - 24, 160);
- const boxH = 56;
- const topY = 40;
- const linkBaseY = topY + boxH + 50;
- const linkSpacing = 60;
- const h = topY + boxH + 60 + linkSpacing * relations.length;
-
- const xFor = (entity: string) => {
- const i = names.indexOf(entity);
- return slotW * i + slotW / 2;
- };
-
- return (
-
-
- {names.map((name) => {
- const cx = xFor(name);
- return (
-
-
-
- {name}
-
-
- );
- })}
-
- {relations.map((rel, i) => {
- const x1 = xFor(rel.from);
- const x2 = xFor(rel.to);
- const y = linkBaseY + i * linkSpacing;
- const midX = (x1 + x2) / 2;
- return (
- // biome-ignore lint/suspicious/noArrayIndexKey: relations are statically authored; index disambiguates same-pair duplicates
-
-
-
- {rel.cardinality}
- {rel.label ? ` · ${rel.label}` : ""}
-
-
- );
- })}
-
- {caption ? (
-
- {caption}
-
- ) : null}
-
- );
-}
diff --git a/docs/src/components/animated/how-it-works.tsx b/docs/src/components/animated/how-it-works.tsx
deleted file mode 100644
index a5b7e47b..00000000
--- a/docs/src/components/animated/how-it-works.tsx
+++ /dev/null
@@ -1,317 +0,0 @@
-import type { ReactNode } from "react";
-
-type Station = {
- id: string;
- label: string;
- hint: string;
- cx: number;
- glyph: ReactNode;
-};
-
-const VIEW_W = 460;
-const VIEW_H = 130;
-const TRACK_Y = 50;
-const STATION_W = 56;
-const STATION_H = 44;
-
-const STATIONS: Station[] = [
- {
- id: "enqueue",
- label: "enqueue",
- hint: ".delay()",
- cx: 56,
- glyph: ,
- },
- {
- id: "queue",
- label: "queue",
- hint: "SQLite · Postgres",
- cx: 184,
- glyph: ,
- },
- {
- id: "worker",
- label: "worker",
- hint: "Rust pool",
- cx: 304,
- glyph: ,
- },
- {
- id: "result",
- label: "result",
- hint: ".result()",
- cx: 412,
- glyph: ,
- },
-];
-
-const JOB_DELAYS = ["0s", "-1.7s", "-3.4s"] as const;
-
-export function HowItWorks() {
- return (
-
-
-
- how it works
-
-
- Your code calls{" "}
- .delay() · the
- job durably queues · a Rust scheduler routes it · a worker returns the
- result.
-
-
-
-
-
-
-
- );
-}
-
-function StationNode({ station }: { station: Station }) {
- const isWorker = station.id === "worker";
- const x = station.cx - STATION_W / 2;
- const y = TRACK_Y - STATION_H / 2;
- return (
-
-
-
- {station.glyph}
-
- {isWorker ? (
-
- ) : null}
-
- {station.label}
-
-
- {station.hint}
-
-
- );
-}
-
-function CodeGlyph() {
- return (
-
- );
-}
-
-function DatabaseGlyph() {
- return (
-
- );
-}
-
-function WorkerGlyph() {
- return (
-
- );
-}
-
-function CheckGlyph() {
- return (
-
- );
-}
-
-function FlowStyles() {
- return (
-
- );
-}
diff --git a/docs/src/components/animated/index.ts b/docs/src/components/animated/index.ts
deleted file mode 100644
index b369b7bc..00000000
--- a/docs/src/components/animated/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export { ArchitectureStack } from "./architecture-stack";
-export { CrashRecoveryTimeline } from "./crash-recovery-timeline";
-export { DispatchSequence } from "./dispatch-sequence";
-export { EntityRelations, EntitySchema } from "./entity-schema";
-export { HowItWorks } from "./how-it-works";
-export { JobStateMachine } from "./job-state-machine";
-export { ResourcePipeline } from "./resource-pipeline";
-export { SchedulerPollLoop } from "./scheduler-poll-loop";
-export { WorkerDispatch } from "./worker-dispatch";
-export { WorkerPool } from "./worker-pool";
diff --git a/docs/src/components/animated/job-state-machine.tsx b/docs/src/components/animated/job-state-machine.tsx
deleted file mode 100644
index a3cdfc7f..00000000
--- a/docs/src/components/animated/job-state-machine.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import { Arrow, DiagramFrame, MotionStyles, NodeBox } from "./_primitives";
-
-const VIEW_W = 580;
-const VIEW_H = 280;
-const NODE_W = 116;
-const NODE_H = 46;
-const ROW_LIVE = 90;
-const ROW_FAULT = 200;
-
-type StateData = {
- id: string;
- label: string;
- cx: number;
- cy: number;
- variant?: "primary" | "terminal";
-};
-
-const STATES: StateData[] = [
- { id: "pending", label: "Pending", cx: 100, cy: ROW_LIVE },
- {
- id: "running",
- label: "Running",
- cx: 290,
- cy: ROW_LIVE,
- variant: "primary",
- },
- {
- id: "complete",
- label: "Complete",
- cx: 480,
- cy: ROW_LIVE,
- variant: "terminal",
- },
- {
- id: "cancelled",
- label: "Cancelled",
- cx: 100,
- cy: ROW_FAULT,
- variant: "terminal",
- },
- { id: "failed", label: "Failed", cx: 290, cy: ROW_FAULT },
- { id: "dead", label: "Dead", cx: 480, cy: ROW_FAULT, variant: "terminal" },
-];
-
-type EdgeData = {
- id: string;
- d: string;
- label?: string;
- labelX?: number;
- labelY?: number;
- anchor?: "start" | "middle" | "end";
- variant?: "muted";
-};
-
-const EDGES: EdgeData[] = [
- {
- id: "dequeue",
- d: `M 158 ${ROW_LIVE} L 232 ${ROW_LIVE}`,
- label: "dequeue",
- labelX: 195,
- labelY: ROW_LIVE - 10,
- },
- {
- id: "success",
- d: `M 348 ${ROW_LIVE} L 422 ${ROW_LIVE}`,
- label: "success",
- labelX: 385,
- labelY: ROW_LIVE - 10,
- },
- {
- id: "raise",
- d: `M 290 ${ROW_LIVE + NODE_H / 2} L 290 ${ROW_FAULT - NODE_H / 2}`,
- label: "raise",
- labelX: 302,
- labelY: 145,
- anchor: "start",
- },
- {
- id: "cancel",
- d: `M 100 ${ROW_LIVE + NODE_H / 2} L 100 ${ROW_FAULT - NODE_H / 2}`,
- label: "cancel",
- labelX: 92,
- labelY: 158,
- anchor: "end",
- variant: "muted",
- },
- {
- id: "exhausted",
- d: `M 348 ${ROW_FAULT} L 422 ${ROW_FAULT}`,
- label: "exhausted",
- labelX: 385,
- labelY: ROW_FAULT - 10,
- },
- {
- id: "retry",
- d: `M 232 ${ROW_FAULT - 6} Q 175 ${ROW_FAULT - 4} 158 ${ROW_LIVE + NODE_H / 2 - 2}`,
- label: "retry · backoff",
- labelX: 222,
- labelY: 142,
- anchor: "end",
- },
- {
- id: "retry-dead",
- d: `M 480 ${ROW_FAULT + NODE_H / 2} Q 290 ${VIEW_H - 10} 100 ${ROW_LIVE + NODE_H / 2 + 14}`,
- label: "retry_dead()",
- labelX: 290,
- labelY: VIEW_H - 14,
- variant: "muted",
- },
-];
-
-// Composite trajectories — each token traces a coherent story along the arrows.
-const PATH_HAPPY = "M 100 90 L 290 90 L 480 90";
-const PATH_RETRY =
- "M 100 90 L 290 90 L 290 200 Q 175 196 158 113 L 290 90 L 480 90";
-const PATH_DEATH = "M 100 90 L 290 90 L 290 200 L 480 200";
-
-export function JobStateMachine() {
- return (
-
- {EDGES.map((e) => (
-
- ))}
-
- {STATES.map((s) => (
-
- ))}
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/components/animated/resource-pipeline.tsx b/docs/src/components/animated/resource-pipeline.tsx
deleted file mode 100644
index a52fc371..00000000
--- a/docs/src/components/animated/resource-pipeline.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-import {
- Arrow,
- Caption,
- Cylinder,
- DiagramFrame,
- MotionStyles,
- NodeBox,
- Panel,
-} from "./_primitives";
-
-const VIEW_W = 820;
-const VIEW_H = 380;
-
-const ENQ_X = 20;
-const ENQ_Y = 24;
-const ENQ_W = 340;
-const ENQ_H = 332;
-const ENQ_CX = ENQ_X + ENQ_W / 2; // 190
-
-const WRK_X = 400;
-const WRK_Y = 24;
-const WRK_W = 400;
-const WRK_H = 332;
-const WRK_CX = WRK_X + WRK_W / 2; // 600
-
-const QUEUE_CX = 380;
-const QUEUE_CY = 196;
-const QUEUE_RX = 24;
-const QUEUE_RY = 16;
-
-const ROW_TOP = 78;
-const ROW_MID = 152;
-const ROW_PROXY = 232;
-const ROW_FN = 304;
-
-type NodeData = {
- id: string;
- label: string;
- hint?: string;
- cx: number;
- cy: number;
- w: number;
- h: number;
- variant?: "primary" | "muted";
-};
-
-const ENQ_NODES: NodeData[] = [
- {
- id: "args",
- label: "Task arguments",
- cx: ENQ_CX,
- cy: ROW_TOP,
- w: 200,
- h: 40,
- },
- {
- id: "ic",
- label: "ArgumentInterceptor",
- hint: "PASS · CONVERT · REDIRECT · PROXY",
- cx: ENQ_CX,
- cy: ROW_MID,
- w: 260,
- h: 50,
- variant: "primary",
- },
- {
- id: "px",
- label: "ProxyHandler",
- hint: ".deconstruct()",
- cx: ENQ_CX,
- cy: ROW_PROXY,
- w: 200,
- h: 42,
- },
- {
- id: "ser",
- label: "Serializer",
- hint: "bytes",
- cx: ENQ_CX,
- cy: ROW_FN,
- w: 170,
- h: 42,
- },
-];
-
-const WORKER_NODES: NodeData[] = [
- { id: "de", label: "Deserialize", cx: WRK_CX, cy: ROW_TOP, w: 170, h: 40 },
- {
- id: "rc",
- label: "reconstruct_args()",
- cx: WRK_CX,
- cy: ROW_MID,
- w: 220,
- h: 46,
- },
- {
- id: "pxr",
- label: "ProxyHandler",
- hint: ".reconstruct()",
- cx: 510,
- cy: ROW_PROXY,
- w: 140,
- h: 42,
- variant: "muted",
- },
- {
- id: "rr",
- label: "ResourceRuntime",
- hint: "inject()",
- cx: 690,
- cy: ROW_PROXY,
- w: 140,
- h: 42,
- variant: "muted",
- },
- {
- id: "fn",
- label: "Task function",
- hint: "f(**args)",
- cx: WRK_CX,
- cy: ROW_FN,
- w: 180,
- h: 50,
- variant: "primary",
- },
-];
-
-export function ResourcePipeline() {
- return (
-
-
-
-
- {/* ENQ vertical chain: Args → IC → PX → SER */}
-
-
-
-
- {/* PASS bypass */}
-
-
- {/* SER → Queue → DE */}
-
-
-
- {/* WRK chain: DE → RC → FN */}
-
-
-
- {/* PXR → FN (diagonal down-right) */}
-
- {/* RR → FN (diagonal down-left) */}
-
-
-
-
-
- {ENQ_NODES.map((n) => (
-
- ))}
- {WORKER_NODES.map((n) => (
-
- ))}
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/components/animated/scheduler-poll-loop.tsx b/docs/src/components/animated/scheduler-poll-loop.tsx
deleted file mode 100644
index 2cb477a6..00000000
--- a/docs/src/components/animated/scheduler-poll-loop.tsx
+++ /dev/null
@@ -1,291 +0,0 @@
-import {
- Arrow,
- Caption,
- DiagramFrame,
- MotionStyles,
- NodeBox,
- SKETCH_FILTER,
-} from "./_primitives";
-
-const VIEW_W = 760;
-const VIEW_H = 380;
-const CENTER_X = 510;
-const CENTER_Y = 200;
-const ORBIT_R = 120;
-
-type LoopNode = {
- id: string;
- label: string;
- hint?: string;
- cx: number;
- cy: number;
- shape: "rect" | "diamond" | "pill";
- variant?: "primary";
-};
-
-const LOOP_NODES: LoopNode[] = [
- {
- id: "tick",
- label: "loop tick",
- cx: CENTER_X,
- cy: CENTER_Y - ORBIT_R,
- shape: "pill",
- },
- {
- id: "sleep",
- label: "sleep",
- hint: "50ms",
- cx: CENTER_X + ORBIT_R,
- cy: CENTER_Y,
- shape: "rect",
- },
- {
- id: "dispatch",
- label: "try_dispatch()",
- cx: CENTER_X,
- cy: CENTER_Y + ORBIT_R,
- shape: "rect",
- variant: "primary",
- },
- {
- id: "periodic",
- label: "iteration % N",
- cx: CENTER_X - ORBIT_R,
- cy: CENTER_Y,
- shape: "diamond",
- },
-];
-
-type Branch = {
- id: string;
- label: string;
- every: string;
- cx: number;
- cy: number;
- /** Where the arrow exits the diamond. */
- startX: number;
- startY: number;
- pulseClass: string;
-};
-
-// Diamond geometry: center at (CENTER_X - ORBIT_R, CENTER_Y) = (390, 200).
-// halfX=64, halfY=40 → vertices: top (390,160), right (454,200), bottom
-// (390,240), left (326,200). Each branch arrow exits from a distinct point
-// on the diamond's left half so arrows are visibly anchored to the shape.
-const DIAMOND_CX = CENTER_X - ORBIT_R;
-const DIAMOND_TOP_LEFT = { x: DIAMOND_CX - 32, y: CENTER_Y - 20 };
-const DIAMOND_LEFT = { x: DIAMOND_CX - 64, y: CENTER_Y };
-const DIAMOND_BOTTOM_LEFT = { x: DIAMOND_CX - 32, y: CENTER_Y + 20 };
-
-const BRANCHES: Branch[] = [
- {
- id: "checkp",
- label: "check_periodic()",
- every: "every ~60",
- cx: 160,
- cy: 100,
- startX: DIAMOND_TOP_LEFT.x,
- startY: DIAMOND_TOP_LEFT.y,
- pulseClass: "pulse-fast",
- },
- {
- id: "reap",
- label: "reap_stale()",
- every: "every ~100",
- cx: 130,
- cy: 200,
- startX: DIAMOND_LEFT.x,
- startY: DIAMOND_LEFT.y,
- pulseClass: "pulse-mid",
- },
- {
- id: "cleanup",
- label: "auto_cleanup()",
- every: "every ~1200",
- cx: 160,
- cy: 300,
- startX: DIAMOND_BOTTOM_LEFT.x,
- startY: DIAMOND_BOTTOM_LEFT.y,
- pulseClass: "pulse-slow",
- },
-];
-
-export function SchedulerPollLoop() {
- return (
-
-
-
- {[45, 135, 225, 315].map((angle) => (
-
- ))}
-
- {LOOP_NODES.map((node) => (
-
- ))}
-
- {BRANCHES.map((b) => {
- const endX = b.cx + 80;
- const endY = b.cy;
- const midX = (b.startX + endX) / 2;
- const midY = (b.startY + endY) / 2;
- return (
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
- );
-}
-
-function LoopNodeShape({ node }: { node: LoopNode }) {
- if (node.shape === "diamond") {
- const halfX = 64;
- const halfY = 40;
- return (
-
-
-
- {node.label}
-
-
- );
- }
-
- if (node.shape === "pill") {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
-function OrbitTick({ angle }: { angle: number }) {
- const rad = (angle * Math.PI) / 180;
- const x = CENTER_X + ORBIT_R * Math.cos(rad);
- const y = CENTER_Y + ORBIT_R * Math.sin(rad);
- const tangent = angle + 90;
- return (
-
-
-
- );
-}
diff --git a/docs/src/components/animated/worker-dispatch.tsx b/docs/src/components/animated/worker-dispatch.tsx
deleted file mode 100644
index ad447ae0..00000000
--- a/docs/src/components/animated/worker-dispatch.tsx
+++ /dev/null
@@ -1,336 +0,0 @@
-import type { CSSProperties } from "react";
-import {
- Arrow,
- Caption,
- Cylinder,
- DiagramFrame,
- MotionStyles,
- NodeBox,
- SKETCH_FILTER,
-} from "./_primitives";
-
-const VIEW_W = 720;
-const VIEW_H = 320;
-
-const SCHED_CX = 80;
-const SCHED_CY = 160;
-const SCHED_W = 120;
-const SCHED_H = 60;
-
-const SYNC_LANE_Y = 110;
-const ASYNC_LANE_Y = 230;
-
-const WORKER_SIZE = 36;
-const WORKER_GAP = 10;
-const POOL_LEFT = 232;
-const POOL_TOP = SYNC_LANE_Y - WORKER_SIZE - WORKER_GAP / 2;
-const POOL_W = 3 * WORKER_SIZE + 2 * WORKER_GAP;
-const POOL_RIGHT = POOL_LEFT + POOL_W;
-
-const ASYNC_LEFT = 232;
-const ASYNC_W = POOL_W;
-const ASYNC_H = 44;
-const ASYNC_RIGHT = ASYNC_LEFT + ASYNC_W;
-
-const RESULT_CX = 460;
-const RESULT_CY = SCHED_CY;
-const RESULT_W = 116;
-const RESULT_H = 60;
-const RESULT_LEFT = RESULT_CX - RESULT_W / 2;
-const RESULT_RIGHT = RESULT_CX + RESULT_W / 2;
-
-const SQLITE_CX = 620;
-const SQLITE_CY = SCHED_CY;
-const SQLITE_RX = 50;
-const SQLITE_RY = 30;
-
-type Worker = {
- id: number;
- cx: number;
- cy: number;
- delay: string;
- duration: string;
-};
-
-const WORKERS: Worker[] = [
- {
- id: 0,
- cx: POOL_LEFT + WORKER_SIZE / 2,
- cy: POOL_TOP + WORKER_SIZE / 2,
- delay: "0s",
- duration: "2.4s",
- },
- {
- id: 1,
- cx: POOL_LEFT + WORKER_SIZE * 1.5 + WORKER_GAP,
- cy: POOL_TOP + WORKER_SIZE / 2,
- delay: "-0.4s",
- duration: "2.8s",
- },
- {
- id: 2,
- cx: POOL_LEFT + WORKER_SIZE * 2.5 + WORKER_GAP * 2,
- cy: POOL_TOP + WORKER_SIZE / 2,
- delay: "-1.1s",
- duration: "2.6s",
- },
- {
- id: 3,
- cx: POOL_LEFT + WORKER_SIZE / 2,
- cy: POOL_TOP + WORKER_SIZE * 1.5 + WORKER_GAP,
- delay: "-0.7s",
- duration: "3.1s",
- },
- {
- id: 4,
- cx: POOL_LEFT + WORKER_SIZE * 1.5 + WORKER_GAP,
- cy: POOL_TOP + WORKER_SIZE * 1.5 + WORKER_GAP,
- delay: "-1.6s",
- duration: "2.5s",
- },
- {
- id: 5,
- cx: POOL_LEFT + WORKER_SIZE * 2.5 + WORKER_GAP * 2,
- cy: POOL_TOP + WORKER_SIZE * 1.5 + WORKER_GAP,
- delay: "-0.2s",
- duration: "2.9s",
- },
-];
-
-const SYNC_DOTS = ["0s", "-0.6s", "-1.2s", "-1.8s"];
-const ASYNC_DOTS = ["-0.3s", "-1.5s"];
-const RESULT_DOTS = ["-0.2s", "-1.0s", "-1.7s"];
-
-export function WorkerDispatch() {
- return (
-
-
-
-
-
-
- {/* Scheduler -> sync entry */}
-
- {/* Scheduler -> async entry */}
-
- {/* sync -> result */}
-
- {/* async -> result */}
-
- {/* result -> SQLite */}
-
-
- {WORKERS.map((w) => (
-
- ))}
-
-
-
-
-
-
-
- {SYNC_DOTS.map((d) => (
-
- ))}
-
- {ASYNC_DOTS.map((d) => (
-
- ))}
-
- {RESULT_DOTS.map((d) => (
-
- ))}
-
-
-
- );
-}
-
-function WorkerCell({ worker }: { worker: Worker }) {
- const half = WORKER_SIZE / 2;
- return (
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/components/animated/worker-pool.tsx b/docs/src/components/animated/worker-pool.tsx
deleted file mode 100644
index ab3d7c62..00000000
--- a/docs/src/components/animated/worker-pool.tsx
+++ /dev/null
@@ -1,312 +0,0 @@
-const WORKERS = [
- { id: 0, x: 0, y: 0, delay: "0s", duration: "2.4s" },
- { id: 1, x: 1, y: 0, delay: "-0.4s", duration: "2.8s" },
- { id: 2, x: 2, y: 0, delay: "-1.1s", duration: "2.6s" },
- { id: 3, x: 0, y: 1, delay: "-0.7s", duration: "3.1s" },
- { id: 4, x: 1, y: 1, delay: "-1.6s", duration: "2.5s" },
- { id: 5, x: 2, y: 1, delay: "-0.2s", duration: "2.9s" },
-] as const;
-
-const INCOMING_JOBS = [
- { delay: "0s" },
- { delay: "-0.6s" },
- { delay: "-1.2s" },
- { delay: "-1.8s" },
-] as const;
-
-const RESULTS = [
- { delay: "-0.3s" },
- { delay: "-0.9s" },
- { delay: "-1.5s" },
-] as const;
-
-const VIEW_W = 580;
-const VIEW_H = 170;
-const POOL_LEFT = 210;
-const POOL_TOP = 50;
-const WORKER_SIZE = 38;
-const WORKER_GAP = 12;
-const POOL_WIDTH = 3 * WORKER_SIZE + 2 * WORKER_GAP;
-const POOL_HEIGHT = 2 * WORKER_SIZE + 1 * WORKER_GAP;
-const POOL_RIGHT = POOL_LEFT + POOL_WIDTH;
-const TRACK_Y = POOL_TOP + POOL_HEIGHT / 2;
-const INCOMING_LEFT = 30;
-const RESULT_LEFT = POOL_RIGHT + 30;
-const RESULT_END = VIEW_W - 30;
-
-export function WorkerPool() {
- return (
-
-
-
- six workers, constant flow
-
-
- Jobs stream in, workers pick them up, results stream out. No broker
- between them.
-
-
-
-
-
-
-
- );
-}
-
-function LaneLabels() {
- const labels = [
- { x: INCOMING_LEFT, text: "incoming" },
- { x: POOL_LEFT, text: "workers" },
- { x: RESULT_LEFT, text: "results" },
- ];
- return (
- <>
- {labels.map((label) => (
-
- {label.text}
-
- ))}
- >
- );
-}
-
-function Lanes() {
- return (
- <>
-
-
- >
- );
-}
-
-function Worker({ worker }: { worker: (typeof WORKERS)[number] }) {
- const x = POOL_LEFT + worker.x * (WORKER_SIZE + WORKER_GAP);
- const y = POOL_TOP + worker.y * (WORKER_SIZE + WORKER_GAP);
- const cx = x + WORKER_SIZE / 2;
- const cy = y + WORKER_SIZE / 2;
- return (
-
-
-
-
-
-
- );
-}
-
-function PoolStyles() {
- return (
-
- );
-}
diff --git a/docs/src/components/diagram-carousel.tsx b/docs/src/components/diagram-carousel.tsx
deleted file mode 100644
index dbe690fa..00000000
--- a/docs/src/components/diagram-carousel.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-"use client";
-
-import { ChevronLeft, ChevronRight } from "lucide-react";
-import {
- Children,
- isValidElement,
- type ReactElement,
- type ReactNode,
- useCallback,
- useEffect,
- useRef,
- useState,
-} from "react";
-import { cn } from "@/lib/cn";
-
-type DiagramSlideProps = {
- title?: string;
- children: ReactNode;
-};
-
-export function DiagramSlide({ children }: DiagramSlideProps) {
- return <>{children}>;
-}
-
-type SlideElement = ReactElement;
-
-export function DiagramCarousel({
- children,
- title,
-}: {
- children: ReactNode;
- title?: string;
-}) {
- const slides = Children.toArray(children).filter(
- (child): child is SlideElement => isValidElement(child),
- );
- const total = slides.length;
- const containerRef = useRef(null);
- const [index, setIndex] = useState(0);
-
- const prev = useCallback(
- () => setIndex((i) => (i - 1 + total) % total),
- [total],
- );
- const next = useCallback(() => setIndex((i) => (i + 1) % total), [total]);
-
- useEffect(() => {
- const node = containerRef.current;
- if (!node) return;
- function onKey(e: KeyboardEvent) {
- if (e.key === "ArrowLeft") {
- e.preventDefault();
- prev();
- } else if (e.key === "ArrowRight") {
- e.preventDefault();
- next();
- }
- }
- node.addEventListener("keydown", onKey);
- return () => node.removeEventListener("keydown", onKey);
- }, [prev, next]);
-
- if (total === 0) return null;
-
- const activeTitle = slides[index]?.props.title;
-
- return (
- }
- className="taskito-carousel my-6 rounded-lg border border-fd-border bg-fd-card overflow-hidden focus:outline-none focus:ring-2 focus:ring-fd-primary/40"
- // biome-ignore lint/a11y/noNoninteractiveTabindex: container hosts arrow-key navigation per WAI-ARIA carousel pattern
- tabIndex={0}
- aria-roledescription="carousel"
- aria-label={title ?? "Diagram carousel"}
- >
-
-
- {title && (
-
- {title}
-
- )}
- {activeTitle && (
-
- {title ? "·" : ""} {activeTitle}
-
- )}
-
-
-
- {index + 1} / {total}
-
-
-
-
-
-
-
-
- {slides.map((slide, i) => (
-
- {slide}
-
- ))}
-
-
- {slides.map((slide, i) => {
- const slideTitle = slide.props.title;
- return (
-
-
- );
-}
diff --git a/docs/src/components/mdx.tsx b/docs/src/components/mdx.tsx
deleted file mode 100644
index fbd873a5..00000000
--- a/docs/src/components/mdx.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import defaultMdxComponents from "fumadocs-ui/mdx";
-import type { MDXComponents } from "mdx/types";
-import {
- ArchitectureStack,
- CrashRecoveryTimeline,
- DispatchSequence,
- EntityRelations,
- EntitySchema,
- HowItWorks,
- JobStateMachine,
- ResourcePipeline,
- SchedulerPollLoop,
- WorkerDispatch,
- WorkerPool,
-} from "./animated";
-import { DiagramCarousel, DiagramSlide } from "./diagram-carousel";
-import { Mermaid } from "./mermaid";
-
-export function getMDXComponents(components?: MDXComponents) {
- return {
- ...defaultMdxComponents,
- Mermaid,
- DiagramCarousel,
- DiagramSlide,
- ArchitectureStack,
- CrashRecoveryTimeline,
- DispatchSequence,
- EntityRelations,
- EntitySchema,
- HowItWorks,
- JobStateMachine,
- ResourcePipeline,
- SchedulerPollLoop,
- WorkerDispatch,
- WorkerPool,
- ...components,
- } satisfies MDXComponents;
-}
-
-export const useMDXComponents = getMDXComponents;
-
-declare global {
- type MDXProvidedComponents = ReturnType;
-}
diff --git a/docs/src/components/provider.tsx b/docs/src/components/provider.tsx
deleted file mode 100644
index de8ad125..00000000
--- a/docs/src/components/provider.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-"use client";
-import { RootProvider } from "fumadocs-ui/provider/next";
-import type { ReactNode } from "react";
-import SearchDialog from "@/components/search";
-
-export function Provider({ children }: { children: ReactNode }) {
- return {children} ;
-}
diff --git a/docs/src/components/search.tsx b/docs/src/components/search.tsx
deleted file mode 100644
index 3b2dc8a5..00000000
--- a/docs/src/components/search.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-"use client";
-import { create } from "@orama/orama";
-import { useDocsSearch } from "fumadocs-core/search/client";
-import {
- SearchDialog,
- SearchDialogClose,
- SearchDialogContent,
- SearchDialogHeader,
- SearchDialogIcon,
- SearchDialogInput,
- SearchDialogList,
- SearchDialogOverlay,
- type SharedProps,
-} from "fumadocs-ui/components/dialog/search";
-import { useI18n } from "fumadocs-ui/contexts/i18n";
-
-function initOrama() {
- return create({
- schema: { _: "string" },
- // https://docs.orama.com/docs/orama-js/supported-languages
- language: "english",
- });
-}
-
-const basePath = process.env.NEXT_PUBLIC_DOCS_BASE_PATH ?? "";
-
-export default function DefaultSearchDialog(props: SharedProps) {
- const { locale } = useI18n(); // (optional) for i18n
- const { search, setSearch, query } = useDocsSearch({
- type: "static",
- initOrama,
- locale,
- from: `${basePath}/api/search`,
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/components/ui/button.tsx b/docs/src/components/ui/button.tsx
deleted file mode 100644
index 343e8175..00000000
--- a/docs/src/components/ui/button.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import Link from "next/link";
-import type { ComponentProps, ReactNode } from "react";
-import { cn } from "@/lib/cn";
-
-export type ButtonVariant = "primary" | "secondary" | "ghost";
-
-const VARIANT_CLASSES: Record = {
- primary:
- "bg-fd-primary text-fd-primary-foreground hover:opacity-90 transition-opacity",
- secondary:
- "border border-fd-border bg-fd-card hover:bg-fd-accent transition-colors",
- ghost: "text-fd-muted-foreground hover:text-fd-foreground transition-colors",
-};
-
-const BASE_CLASSES =
- "inline-flex items-center gap-2 rounded-md px-5 py-2.5 text-sm font-medium";
-
-type CommonProps = {
- variant?: ButtonVariant;
- icon?: ReactNode;
- children: ReactNode;
- className?: string;
-};
-
-type AsLink = CommonProps & {
- href: string;
-} & Omit, "href" | "className" | "children">;
-
-type AsButton = CommonProps & {
- href?: undefined;
-} & Omit<
- ComponentProps<"button">,
- "className" | "children" | keyof CommonProps
- >;
-
-export function Button(props: AsLink | AsButton) {
- const { variant = "primary", icon, children, className, ...rest } = props;
- const classes = cn(BASE_CLASSES, VARIANT_CLASSES[variant], className);
-
- if ("href" in rest && rest.href !== undefined) {
- const { href, ...linkRest } = rest;
- return (
-
- {children}
- {icon}
-
- );
- }
-
- return (
-
- );
-}
diff --git a/docs/src/components/ui/code-panel.tsx b/docs/src/components/ui/code-panel.tsx
deleted file mode 100644
index 1072eef9..00000000
--- a/docs/src/components/ui/code-panel.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import type { ReactNode } from "react";
-import { cn } from "@/lib/cn";
-
-export type CodePanelTone = "primary" | "muted" | "default";
-
-const TONE_CLASSES: Record = {
- primary: "border-t-2 border-t-fd-primary",
- muted: "border-t-2 border-t-fd-border",
- default: "",
-};
-
-const LABEL_TONE_CLASSES: Record = {
- primary: "text-fd-primary",
- muted: "text-fd-foreground",
- default: "text-fd-foreground",
-};
-
-type LabelHeader = {
- label: string;
- caption?: string;
- header?: undefined;
-};
-
-type CustomHeader = {
- label?: undefined;
- caption?: undefined;
- header: ReactNode;
-};
-
-type NoHeader = {
- label?: undefined;
- caption?: undefined;
- header?: undefined;
-};
-
-export type CodePanelProps = {
- tone?: CodePanelTone;
- className?: string;
- children: ReactNode;
-} & (LabelHeader | CustomHeader | NoHeader);
-
-export function CodePanel(props: CodePanelProps) {
- const { tone = "default", className, children } = props;
-
- return (
-
- {renderHeader(props)}
- {children}
-
- );
-}
-
-function renderHeader(props: CodePanelProps) {
- if (props.header !== undefined) {
- return (
-
- {props.header}
-
- );
- }
- if (props.label !== undefined) {
- const tone = props.tone ?? "default";
- return (
-
-
- {props.label}
-
- {props.caption ? (
-
- {props.caption}
-
- ) : null}
-
- );
- }
- return null;
-}
diff --git a/docs/src/components/ui/index.ts b/docs/src/components/ui/index.ts
deleted file mode 100644
index 3a4bccd9..00000000
--- a/docs/src/components/ui/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export { Button, type ButtonVariant } from "./button";
-export {
- CodePanel,
- type CodePanelProps,
- type CodePanelTone,
-} from "./code-panel";
-export {
- SectionHeader,
- type SectionHeaderAlign,
-} from "./section-header";
diff --git a/docs/src/components/ui/section-header.tsx b/docs/src/components/ui/section-header.tsx
deleted file mode 100644
index d8196d9a..00000000
--- a/docs/src/components/ui/section-header.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { cn } from "@/lib/cn";
-
-export type SectionHeaderAlign = "center" | "left";
-
-export function SectionHeader({
- title,
- description,
- align = "center",
- className,
-}: {
- title: string;
- description?: string;
- align?: SectionHeaderAlign;
- className?: string;
-}) {
- return (
-
-
- {title}
-
- {description ? (
- {description}
- ) : null}
-
- );
-}
diff --git a/docs/src/lib/highlight.tsx b/docs/src/lib/highlight.tsx
deleted file mode 100644
index b5b862a9..00000000
--- a/docs/src/lib/highlight.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { highlight as fumaHighlight } from "fumadocs-core/highlight";
-import type { ReactNode } from "react";
-import { cn } from "@/lib/cn";
-
-export type HighlightLang = "python" | "bash" | "tsx" | "ts" | "json" | "yaml";
-
-const DEFAULT_PRE_CLASSES =
- "p-5 text-sm leading-relaxed overflow-x-auto bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)";
-
-export async function highlight(
- code: string,
- lang: HighlightLang,
- preClassName?: string,
-): Promise {
- return fumaHighlight(code, {
- lang,
- defaultColor: false,
- themes: {
- light: "github-light",
- dark: "github-dark",
- },
- components: {
- pre: ({ children, className, ...props }) => (
-
- {children}
-
- ),
- },
- });
-}
diff --git a/docs/src/lib/landing-content.tsx b/docs/src/lib/landing-content.tsx
deleted file mode 100644
index 0229467a..00000000
--- a/docs/src/lib/landing-content.tsx
+++ /dev/null
@@ -1,325 +0,0 @@
-import {
- Activity,
- BookOpen,
- Clock,
- Code2,
- Cpu,
- Database,
- Layers,
- type LucideIcon,
- Mail,
- Shield,
- Sparkles,
- Workflow,
- Zap,
-} from "lucide-react";
-import { RELEASE_BADGE } from "./release";
-
-export type CtaTarget = {
- href: string;
- label: string;
-};
-
-export type LandingHero = {
- badge: string;
- headline: string[];
- description: string;
- primaryCta: CtaTarget;
- secondaryCta: CtaTarget;
- ghostCta: CtaTarget;
- preview: {
- filename: string;
- code: string;
- };
-};
-
-export type LandingFeature = {
- icon: LucideIcon;
- title: string;
- body: string;
-};
-
-export type DocSectionCard = {
- icon: LucideIcon;
- title: string;
- description: string;
- href: string;
-};
-
-export const DOC_SECTIONS: DocSectionCard[] = [
- {
- icon: BookOpen,
- title: "Guides",
- description:
- "Recipes for tasks, queues, workers, retries, workflows, observability, and integrations.",
- href: "/guides",
- },
- {
- icon: Layers,
- title: "Architecture",
- description:
- "How the Rust core, scheduler, worker pool, storage, and resource graph fit together.",
- href: "/architecture/overview",
- },
- {
- icon: Code2,
- title: "API Reference",
- description:
- "Queues, tasks, results, contexts, the canvas, workflows, testing helpers, and the CLI.",
- href: "/api-reference/overview",
- },
- {
- icon: Sparkles,
- title: "More",
- description:
- "Worked examples, the Celery comparison, FAQ, and the per-release changelog.",
- href: "/more/examples",
- },
-];
-
-export type ComparisonRow = {
- label: string;
- taskito: string;
- celery: string;
-};
-
-export type ComparisonStack = {
- label: string;
- caption: string;
- code: string;
-};
-
-export type LandingComparison = {
- title: string;
- description: string;
- taskito: ComparisonStack;
- celery: ComparisonStack;
- rows: ComparisonRow[];
-};
-
-export type LandingCta = {
- title: string;
- description: string;
- primary: CtaTarget;
- secondary: CtaTarget;
-};
-
-export const HERO: LandingHero = {
- badge: RELEASE_BADGE,
- headline: ["Task queue", "without the broker."],
- description:
- "Rust-powered task queue for Python. Replace Celery without Redis or RabbitMQ. Start with SQLite, scale to Postgres.",
- primaryCta: {
- href: "/getting-started/quickstart",
- label: "Quickstart",
- },
- secondaryCta: {
- href: "/getting-started/installation",
- label: "Install",
- },
- ghostCta: {
- href: "https://github.com/ByteVeda/taskito",
- label: "GitHub →",
- },
- preview: {
- filename: "tasks.py",
- code: `# pip install taskito
-from taskito import Queue
-
-queue = Queue(db_path="tasks.db")
-
-@queue.task()
-def add(a, b):
- return a + b
-
-job = add.delay(2, 3)
-print(job.result()) # 5`,
- },
-};
-
-export const FEATURES_TITLE = "What you get";
-export const FEATURES_DESCRIPTION =
- "The convenience of Celery, the performance of Rust, the simplicity of SQLite.";
-
-export const FEATURES: LandingFeature[] = [
- {
- icon: Zap,
- title: "Brokerless",
- body: "No Redis, no RabbitMQ. Everything in a single SQLite file — queue, results, rate limits, schedules. Just `pip install` and go.",
- },
- {
- icon: Cpu,
- title: "Rust-powered",
- body: "The scheduler, dispatcher, and storage engine are all Rust. Tokio runtime, OS-thread worker pool, thin PyO3 boundary keeps the Python overhead negligible.",
- },
- {
- icon: Activity,
- title: "Async-first",
- body: "`async def` tasks dispatch onto a dedicated event loop — no `asyncio.run()` wrapping, no thread-pool bridging. Sync and async tasks coexist transparently.",
- },
- {
- icon: Workflow,
- title: "DAG workflows",
- body: "Multi-step pipelines as directed acyclic graphs. Fan-out, fan-in, conditions, approval gates, sub-workflows, incremental re-runs, Mermaid visualization.",
- },
- {
- icon: Layers,
- title: "Resource system",
- body: "Inject database connections, HTTP clients, and cloud SDKs by name. Three-layer pipeline: argument interception, worker DI, transparent proxy reconstruction.",
- },
- {
- icon: Shield,
- title: "Production-ready",
- body: "Retries with exponential backoff, dead letter queue, rate limits, circuit breakers, distributed locks, structured logs, OTel/Sentry/Prometheus middleware.",
- },
-];
-
-export const COMPARISON: LandingComparison = {
- title: "Less to operate",
- description:
- "The same task, two stacks. Side by side, with the operational delta.",
- taskito: {
- label: "taskito",
- caption: "Brokerless · single process",
- code: `from taskito import Queue
-
-queue = Queue(db_path="tasks.db")
-
-@queue.task(max_retries=3, rate_limit="100/m")
-def send_email(to, subject, body):
- smtp.send(to, subject, body)
-
-# Enqueue
-send_email.delay("alice@example.com", "Hi", "Body")
-
-# Run the worker
-# $ taskito worker --app tasks:queue`,
- },
- celery: {
- label: "Celery + Redis",
- caption: "Requires Redis · 3 processes",
- code: `from celery import Celery
-
-app = Celery(
- "myapp",
- broker="redis://localhost:6379/0",
- backend="redis://localhost:6379/1",
-)
-app.conf.task_default_rate_limit = "100/m"
-
-@app.task(bind=True, max_retries=3)
-def send_email(self, to, subject, body):
- try:
- smtp.send(to, subject, body)
- except SMTPError as exc:
- raise self.retry(exc=exc, countdown=60)
-
-# Enqueue
-send_email.delay("alice@example.com", "Hi", "Body")
-
-# Run the worker (in a separate terminal, plus Redis)
-# $ celery -A myapp worker --loglevel=info`,
- },
- rows: [
- {
- label: "Install",
- taskito: "pip install taskito",
- celery: "pip install celery[redis] + run Redis daemon",
- },
- {
- label: "Background services",
- taskito: "1 (worker)",
- celery: "3 (worker, beat, Redis)",
- },
- {
- label: "Default storage",
- taskito: "SQLite file (built-in)",
- celery: "Redis (separate daemon)",
- },
- {
- label: "Retry config in the example above",
- taskito: "max_retries=3 decorator arg",
- celery: "try/except + self.retry(exc=…)",
- },
- ],
-};
-
-export type UseCase = {
- icon: LucideIcon;
- title: string;
- body: string;
- href: string;
-};
-
-export const USE_CASES_TITLE = "Built for the jobs you actually have";
-export const USE_CASES_DESCRIPTION =
- "Pick the workload — taskito ships the primitives.";
-
-export const USE_CASES: UseCase[] = [
- {
- icon: Database,
- title: "ETL pipelines",
- body: "Chain extract → transform → load as a DAG. Fan out across workers, fan in to aggregate, restart from any node on failure.",
- href: "/guides/workflows",
- },
- {
- icon: Mail,
- title: "Email & notifications",
- body: "Bursty SMTP, push, or webhook delivery. Per-task rate limits keep providers happy; retries with backoff handle transient failures.",
- href: "/guides/reliability/retries",
- },
- {
- icon: Cpu,
- title: "ML inference & batch",
- body: "Long-running model jobs with progress tracking, soft timeouts, and prefork pools for true CPU parallelism without GIL contention.",
- href: "/guides/advanced-execution/prefork",
- },
- {
- icon: Clock,
- title: "Scheduled jobs",
- body: "Six-field cron syntax down to the second. Periodic tasks live in the scheduler — no separate beat daemon to babysit.",
- href: "/guides/core/scheduling",
- },
-];
-
-export const INTEGRATIONS_TITLE = "Slots into your stack";
-export const INTEGRATIONS_DESCRIPTION =
- "First-class support for the tools you already run.";
-
-export type IntegrationGroup = {
- group: string;
- items: string[];
-};
-
-export const INTEGRATIONS: IntegrationGroup[] = [
- {
- group: "Frameworks",
- items: ["Django", "FastAPI", "Flask"],
- },
- {
- group: "Storage",
- items: ["Postgres", "SQLite", "Redis"],
- },
- {
- group: "Observability",
- items: ["OpenTelemetry", "Sentry", "Prometheus"],
- },
- {
- group: "Serialization",
- items: ["JSON", "MsgPack", "Cloudpickle"],
- },
-];
-
-export const CTA: LandingCta = {
- title: "Five minutes from `pip install` to your first job.",
- description:
- "The quickstart walks you through defining a task, enqueuing it, and watching the worker run it — no Redis, no broker, no config.",
- primary: {
- href: "/getting-started/quickstart",
- label: "Start the quickstart",
- },
- secondary: {
- href: "/more/comparison",
- label: "See the full comparison",
- },
-};
diff --git a/docs/src/lib/layout.shared.tsx b/docs/src/lib/layout.shared.tsx
deleted file mode 100644
index 7d4402fc..00000000
--- a/docs/src/lib/layout.shared.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
-import { appName, gitConfig } from "./shared";
-
-const PRIMARY_NAV_LINKS = [
- {
- text: "Getting Started",
- url: "/getting-started/installation",
- },
- {
- text: "Guides",
- url: "/guides",
- },
- {
- text: "Architecture",
- url: "/architecture/overview",
- },
- {
- text: "API",
- url: "/api-reference/overview",
- },
- {
- text: "Changelog",
- url: "/more/changelog",
- },
-];
-
-export function baseOptions(): BaseLayoutProps {
- return {
- nav: {
- title: appName,
- },
- githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`,
- };
-}
-
-export function homeOptions(): BaseLayoutProps {
- return {
- ...baseOptions(),
- links: PRIMARY_NAV_LINKS,
- };
-}
diff --git a/docs/src/lib/release.ts b/docs/src/lib/release.ts
deleted file mode 100644
index ce20ecf6..00000000
--- a/docs/src/lib/release.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const RELEASE_VERSION = "0.16.2";
-export const RELEASE_TAGLINE =
- "mesh scheduling, DLQ policies, dashboard redesign";
-export const RELEASE_BADGE = `v${RELEASE_VERSION.replace(/\.\d+$/, "")} — ${RELEASE_TAGLINE}`;
diff --git a/docs/src/lib/shared.ts b/docs/src/lib/shared.ts
deleted file mode 100644
index fe5750f6..00000000
--- a/docs/src/lib/shared.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export const appName = "Taskito";
-export const docsRoute = "/";
-export const docsImageRoute = "/og";
-export const docsContentRoute = "/llms.mdx";
-
-// Raw href strings (markdown/OG routes) are not run through Next's router,
-// so basePath is not auto-applied — prepend it ourselves when deployed under
-// a sub-path (e.g. /taskito on GitHub Pages).
-export const basePath = process.env.NEXT_PUBLIC_DOCS_BASE_PATH ?? "";
-
-export const gitConfig = {
- user: "ByteVeda",
- repo: "taskito",
- branch: "master",
-};
diff --git a/docs/src/lib/source.ts b/docs/src/lib/source.ts
deleted file mode 100644
index bbd1cee7..00000000
--- a/docs/src/lib/source.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { docs } from "collections/server";
-import { loader } from "fumadocs-core/source";
-import {
- basePath,
- docsContentRoute,
- docsImageRoute,
- docsRoute,
-} from "./shared";
-
-// See https://fumadocs.dev/docs/headless/source-api for more info
-export const source = loader({
- baseUrl: docsRoute,
- source: docs.toFumadocsSource(),
- plugins: [],
-});
-
-export function getPageImage(page: (typeof source)["$inferPage"]) {
- const segments = [...page.slugs, "image.png"];
-
- return {
- segments,
- url: `${basePath}${docsImageRoute}/${segments.join("/")}`,
- };
-}
-
-export function getPageMarkdownUrl(page: (typeof source)["$inferPage"]) {
- const segments = [...page.slugs, "content.md"];
-
- return {
- segments,
- url: `${basePath}${docsContentRoute}/${segments.join("/")}`,
- };
-}
-
-export async function getLLMText(page: (typeof source)["$inferPage"]) {
- const processed = await page.data.getText("processed");
-
- return `# ${page.data.title} (${page.url})
-
-${processed}`;
-}
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
index f43f8735..d3533f57 100644
--- a/docs/tsconfig.json
+++ b/docs/tsconfig.json
@@ -1,35 +1,30 @@
{
+ "include": [
+ "app/**/*.ts",
+ "app/**/*.tsx",
+ "*.ts",
+ "*.tsx",
+ ".react-router/types/**/*"
+ ],
"compilerOptions": {
- "target": "ESNext",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "forceConsistentCasingInFileNames": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@react-router/node", "vite/client"],
+ "target": "ES2022",
+ "module": "ESNext",
"moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "skipLibCheck": true,
"resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "allowJs": true,
"isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
+ "verbatimModuleSyntax": true,
+ "rootDirs": [".", "./.react-router/types"],
"paths": {
- "@/*": ["./src/*"],
- "collections/*": ["./.source/*"]
- },
- "plugins": [
- {
- "name": "next"
- }
- ]
+ "@/*": ["./app/*"]
+ }
},
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ".next/dev/types/**/*.ts"
- ],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "build"]
}
diff --git a/docs/vite-plugin-docs-manifest.ts b/docs/vite-plugin-docs-manifest.ts
new file mode 100644
index 00000000..b5d766bd
--- /dev/null
+++ b/docs/vite-plugin-docs-manifest.ts
@@ -0,0 +1,76 @@
+import { readdirSync, readFileSync } from "node:fs";
+import { join, relative, sep } from "node:path";
+import { fileURLToPath } from "node:url";
+import type { Plugin } from "vite";
+
+// Build-time manifest of doc pages: { slug, title, description } only — NO compiled
+// components. nav/search/llms import this (via `virtual:docs-manifest`) instead of the
+// component glob, so they never pull the 128 shiki-inflated MDX modules into a chunk.
+
+const CONTENT_DIR = fileURLToPath(new URL("./content/docs", import.meta.url));
+const VIRTUAL_ID = "virtual:docs-manifest";
+const RESOLVED_ID = `\0${VIRTUAL_ID}`;
+
+export interface DocMeta {
+ slug: string;
+ title: string;
+ description: string;
+}
+
+function slugOf(absFile: string): string {
+ const rel = relative(CONTENT_DIR, absFile).replace(/\.mdx$/, "");
+ const parts = rel.split(sep);
+ if (parts[parts.length - 1] === "index") {
+ parts.pop();
+ }
+ return `/${parts.join("/")}`.replace(/\/$/, "") || "/";
+}
+
+function parseFrontmatter(raw: string): { title: string; description: string } {
+ const fm = raw.match(/^---\n([\s\S]*?)\n---/);
+ const block = fm?.[1] ?? "";
+ const field = (name: string) =>
+ block.match(new RegExp(`^${name}:\\s*"?(.+?)"?\\s*$`, "m"))?.[1] ?? "";
+ return { title: field("title"), description: field("description") };
+}
+
+function walk(dir: string, out: string[]): void {
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walk(full, out);
+ } else if (entry.name.endsWith(".mdx")) {
+ out.push(full);
+ }
+ }
+}
+
+function buildManifest(): DocMeta[] {
+ const files: string[] = [];
+ walk(CONTENT_DIR, files);
+ return files
+ .map((file) => {
+ const { title, description } = parseFrontmatter(
+ readFileSync(file, "utf8"),
+ );
+ const slug = slugOf(file);
+ return { slug, title: title || slug, description };
+ })
+ .sort((a, b) => a.slug.localeCompare(b.slug));
+}
+
+export function docsManifest(): Plugin {
+ return {
+ name: "docs-manifest",
+ resolveId(id) {
+ if (id === VIRTUAL_ID) {
+ return RESOLVED_ID;
+ }
+ },
+ load(id) {
+ if (id === RESOLVED_ID) {
+ return `export const DOCS = ${JSON.stringify(buildManifest())};`;
+ }
+ },
+ };
+}
diff --git a/docs/vite.config.ts b/docs/vite.config.ts
new file mode 100644
index 00000000..45ff004c
--- /dev/null
+++ b/docs/vite.config.ts
@@ -0,0 +1,78 @@
+import { fileURLToPath } from "node:url";
+import mdx from "@mdx-js/rollup";
+import { reactRouter } from "@react-router/dev/vite";
+import rehypeShiki from "@shikijs/rehype";
+import tailwindcss from "@tailwindcss/vite";
+import rehypeAutolinkHeadings from "rehype-autolink-headings";
+import rehypeSlug from "rehype-slug";
+import remarkFrontmatter from "remark-frontmatter";
+import remarkGfm from "remark-gfm";
+import remarkMdxFrontmatter from "remark-mdx-frontmatter";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+import { docsManifest } from "./vite-plugin-docs-manifest";
+
+// Deploy under /taskito on GitHub Pages; serve from root locally.
+const base = process.env.DOCS_BASE_PATH
+ ? `${process.env.DOCS_BASE_PATH}/`
+ : "/";
+
+const mdxComponentDir = (name: string) =>
+ fileURLToPath(new URL(`./app/components/mdx/${name}.tsx`, import.meta.url));
+
+export default defineConfig({
+ base,
+ resolve: {
+ alias: {
+ // The reused content MDX imports Fumadocs components; map those paths to
+ // our own design-matched shims so the content compiles unchanged.
+ "fumadocs-ui/components/callout": mdxComponentDir("callout"),
+ "fumadocs-ui/components/tabs": mdxComponentDir("tabs"),
+ "fumadocs-ui/components/card": mdxComponentDir("card"),
+ },
+ },
+ plugins: [
+ tailwindcss(),
+ docsManifest(),
+ // MDX must transform `.mdx` before React Router's plugin processes routes.
+ {
+ enforce: "pre",
+ ...mdx({
+ remarkPlugins: [
+ remarkGfm,
+ remarkFrontmatter,
+ [remarkMdxFrontmatter, { name: "frontmatter" }],
+ ],
+ rehypePlugins: [
+ rehypeSlug,
+ [
+ rehypeShiki,
+ {
+ themes: { light: "github-light", dark: "github-dark" },
+ // Emit only --shiki-light/--shiki-dark CSS vars (no inline color/bg),
+ // so app.css can switch them on our [data-theme] selector.
+ defaultColor: false,
+ // Tag each with its language so the CodeBlock wrapper can
+ // render the design's header bar.
+ transformers: [
+ {
+ name: "code-language",
+ pre(
+ this: { options: { lang: string } },
+ node: { properties: Record },
+ ) {
+ node.properties["data-language"] = this.options.lang;
+ },
+ },
+ ],
+ },
+ ],
+ [rehypeAutolinkHeadings, { behavior: "wrap" }],
+ ],
+ providerImportSource: "@mdx-js/react",
+ }),
+ },
+ reactRouter(),
+ tsconfigPaths(),
+ ],
+});