diff --git a/docs/roadmap.md b/docs/roadmap.md index 6a485c2..15b4b0e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,26 +1,40 @@ # roadmap -zat started as a small set of string primitives for AT Protocol - the types everyone reimplements (`Tid`, `Did`, `Handle`, `Nsid`, `Rkey`, `AtUri`). the scope grew based on real usage. +zat started as a small set of string primitives for AT Protocol — the types everyone reimplements (`Tid`, `Did`, `Handle`, `Nsid`, `Rkey`, `AtUri`). the scope grew based on real usage. -## history +## current status + +**v0.3.0-alpha** — zig 0.16, `std.Io` throughout. + +the v0.3.0 migration replaced all networking and concurrency primitives with zig 0.16's [`std.Io`](https://ziglang.org/documentation/master/std/#std.Io) interface. the API change is mechanical: every networking type takes `io: std.Io` as its first parameter. streaming clients moved from `connect()` + `next()` loops to `subscribe(handler)` with automatic reconnection, backoff, and host rotation. -**initial scope** - string primitives with parsing and validation. the philosophy: primitives not frameworks, layered design, zig idioms, minimal scope. +the library is stable and tested. the alpha tag reflects that downstream consumers (zlay, labelz, pollz) are still validating in production. + +## history **what grew from usage:** -- DID/handle resolution — real projects needed it, so `DidResolver`, `DidDocument`, `HandleResolver` got added -- XRPC client and JSON helpers — same story +- string primitives with parsing and validation — the initial scope +- DID/handle resolution — `DidResolver`, `DidDocument`, `HandleResolver` +- XRPC client and JSON helpers - JWT verification for service auth -- jetstream client — typed JSON event stream with reconnection (0.1.3) -- firehose client — raw CBOR event stream, DAG-CBOR codec, CAR codec, CID creation (0.1.4) -- MST, ECDSA signing, `did:key` construction, multibase encoding (0.1.9) -- full repo verification — end-to-end trust chain from handle to MST root CID match (0.2.0) -- CID hash verification in CAR parser (0.2.1), size limits (0.2.2) +- jetstream client — typed JSON event stream with reconnection (v0.1.3) +- firehose client — raw CBOR event stream, DAG-CBOR codec, CAR codec, CID creation (v0.1.4) +- MST, ECDSA signing, `did:key` construction, multibase encoding (v0.1.9) +- full repo verification — end-to-end trust chain from handle to MST root CID match (v0.2.0) +- CID hash verification in CAR parser (v0.2.1), size limits (v0.2.2) +- OAuth 2.1 DPoP client (v0.2.14) +- configurable keep-alive, transport options (v0.2.12–v0.2.18) +- `std.Io` migration, `subscribe(handler)` streaming API (v0.3.0-alpha) + +this pattern — start minimal, expand based on real pain — continues. -this pattern - start minimal, expand based on real pain - continues. +## what's next -## now +the library covers the full AT Protocol verification pipeline: identity resolution, repo parsing, signature verification, and MST validation. benchmarked against Go (indigo) and Rust (rsky) in [atproto-bench](https://tangled.org/zzstoatzz.io/atproto-bench). -the library covers the full AT Protocol verification pipeline: identity resolution, repo parsing, signature verification, and MST validation. benchmarked against Go (indigo) and Rust (rsky) in [atproto-bench](https://tangled.sh/@zzstoatzz.io/atproto-bench). +near-term: +- promote to v0.3.0-beta once production consumers stabilize +- cut v0.3.0 when the API surface is confirmed what's missing will show up when people build things. until then, no speculative features. @@ -28,10 +42,10 @@ what's missing will show up when people build things. until then, no speculative these stay out of scope unless real demand emerges: -- lexicon codegen - probably a separate project -- higher-level clients/frameworks - too opinionated -- token refresh/session management - app-specific -- feed generator scaffolding - each feed is unique +- lexicon codegen — probably a separate project +- higher-level clients/frameworks — too opinionated +- token refresh/session management — app-specific +- feed generator scaffolding — each feed is unique ## non-goals diff --git a/site/app.js b/site/app.js index 87cd89f..9345ef8 100644 --- a/site/app.js +++ b/site/app.js @@ -1,23 +1,24 @@ const navEl = document.getElementById("nav"); const contentEl = document.getElementById("content"); -const menuToggle = document.querySelector(".menu-toggle"); -const sidebar = document.querySelector(".sidebar"); -const overlay = document.querySelector(".overlay"); - -function toggleMenu(open) { - const isOpen = open ?? !sidebar.classList.contains("open"); - sidebar.classList.toggle("open", isOpen); - overlay?.classList.toggle("open", isOpen); - menuToggle?.setAttribute("aria-expanded", isOpen); - document.body.style.overflow = isOpen ? "hidden" : ""; +const themeToggle = document.querySelector(".theme-toggle"); + +// Theme toggle +function getPreferredTheme() { + const stored = localStorage.getItem("theme"); + if (stored) return stored; + return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; +} + +function applyTheme(theme) { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("theme", theme); } -menuToggle?.addEventListener("click", () => toggleMenu()); -overlay?.addEventListener("click", () => toggleMenu(false)); +applyTheme(getPreferredTheme()); -// Close menu when nav link clicked (mobile) -navEl?.addEventListener("click", (e) => { - if (e.target.closest("a")) toggleMenu(false); +themeToggle?.addEventListener("click", () => { + const current = document.documentElement.getAttribute("data-theme") || getPreferredTheme(); + applyTheme(current === "dark" ? "light" : "dark"); }); const buildId = new URL(import.meta.url).searchParams.get("v") || ""; @@ -110,7 +111,6 @@ function installContentLinkHandler() { async function main() { // SPA fallback: convert pathname to hash route so deep links work - // e.g. /roadmap -> #roadmap.md, /devlog/001 -> #devlog/001.md if (location.pathname !== "/" && !location.hash) { const path = location.pathname.replace(/^\/+/, ""); if (path) { @@ -144,7 +144,7 @@ async function main() { renderNav(pages, activePath); if (!activePath) { - contentEl.innerHTML = `

No docs yet. Add markdown files under zat/docs/ and push to main.

`; + contentEl.innerHTML = `

No docs yet.

`; return; } @@ -153,7 +153,6 @@ async function main() { const html = globalThis.marked.parse(md); contentEl.innerHTML = html; - // Update current marker after navigation re-render. for (const a of navEl.querySelectorAll("a")) { const href = decodeURIComponent((a.getAttribute("href") || "").slice(1)); a.toggleAttribute("aria-current", normalizeDocPath(href) === activePath); diff --git a/site/index.html b/site/index.html index 6d52dec..478ddbf 100644 --- a/site/index.html +++ b/site/index.html @@ -5,43 +5,33 @@ zat.dev - +
- zat.dev -
-
-
- - -
-
- -
-
-
+
+
+ +
+
diff --git a/site/style.css b/site/style.css index 4bf241e..6731c04 100644 --- a/site/style.css +++ b/site/style.css @@ -1,35 +1,50 @@ :root { - color-scheme: light dark; + color-scheme: dark; --bg: #0b0b0f; - --panel: #10101a; - --text: #f3f4f6; - --muted: #a1a1aa; - --border: rgba(255, 255, 255, 0.08); - --link: #93c5fd; - --codebg: rgba(255, 255, 255, 0.06); - --shadow: rgba(0, 0, 0, 0.35); - --max: 900px; - --radius: 12px; + --panel: #12121e; + --text: #e4e4eb; + --muted: #8b8b9e; + --border: rgba(255, 255, 255, 0.07); + --link: #7db4f5; + --codebg: rgba(255, 255, 255, 0.05); + --shadow: rgba(0, 0, 0, 0.4); + --accent: rgba(125, 180, 245, 0.08); + --radius: 10px; --gutter: 16px; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, - Arial, "Apple Color Emoji", "Segoe UI Emoji"; + --sans: -apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; } @media (prefers-color-scheme: light) { - :root { - --bg: #fafafa; + :root:not([data-theme="dark"]) { + color-scheme: light; + --bg: #f8f8fa; --panel: #ffffff; - --text: #0b0b0f; - --muted: #52525b; + --text: #1a1a2e; + --muted: #64648a; --border: rgba(0, 0, 0, 0.08); --link: #2563eb; --codebg: rgba(0, 0, 0, 0.04); - --shadow: rgba(0, 0, 0, 0.08); + --shadow: rgba(0, 0, 0, 0.06); + --accent: rgba(37, 99, 235, 0.06); } } +:root[data-theme="light"] { + color-scheme: light; + --bg: #f8f8fa; + --panel: #ffffff; + --text: #1a1a2e; + --muted: #64648a; + --border: rgba(0, 0, 0, 0.08); + --link: #2563eb; + --codebg: rgba(0, 0, 0, 0.04); + --shadow: rgba(0, 0, 0, 0.06); + --accent: rgba(37, 99, 235, 0.06); +} + * { box-sizing: border-box; } @@ -68,160 +83,132 @@ a:hover { z-index: 20; display: flex; align-items: center; - gap: 12px; - padding: 12px var(--gutter); + gap: 8px; + padding: 0 var(--gutter); + height: 48px; border-bottom: 1px solid var(--border); - background: color-mix(in srgb, var(--panel) 92%, transparent); - backdrop-filter: blur(10px); -} - -.menu-toggle { - display: none; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - padding: 0; - background: transparent; - border: 1px solid var(--border); - border-radius: 8px; - cursor: pointer; - flex-shrink: 0; -} -.menu-toggle span { - display: block; - width: 16px; - height: 2px; - background: var(--text); - border-radius: 1px; - position: relative; -} -.menu-toggle span::before, -.menu-toggle span::after { - content: ""; - position: absolute; - left: 0; - width: 16px; - height: 2px; - background: var(--text); - border-radius: 1px; - transition: transform 0.2s; -} -.menu-toggle span::before { - top: -5px; -} -.menu-toggle span::after { - top: 5px; -} -.menu-toggle[aria-expanded="true"] span { - background: transparent; -} -.menu-toggle[aria-expanded="true"] span::before { - transform: translateY(5px) rotate(45deg); -} -.menu-toggle[aria-expanded="true"] span::after { - transform: translateY(-5px) rotate(-45deg); + background: color-mix(in srgb, var(--bg) 85%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + max-width: 100%; } .brand { + font-family: var(--mono); font-weight: 700; - font-size: 15px; + font-size: 14px; color: var(--text); - padding: 6px 0; + letter-spacing: -0.02em; + white-space: nowrap; + flex-shrink: 0; } .brand:hover { text-decoration: none; opacity: 0.8; } -.header-links { +/* Nav — inline in header */ +.nav { display: flex; - gap: 8px; - margin-left: auto; + gap: 2px; + margin-left: 12px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.nav::-webkit-scrollbar { + display: none; } -.header-link { - padding: 6px 12px; - font-size: 14px; - border-radius: 8px; - border: 1px solid var(--border); - color: var(--text); +.nav a { + padding: 6px 10px; + border-radius: 6px; + color: var(--muted); + font-size: 13px; + white-space: nowrap; + transition: color 0.1s, background 0.1s; } -.header-link:hover { +.nav a:hover { + color: var(--text); background: var(--codebg); text-decoration: none; } - -/* Overlay */ -.overlay { - display: none; - position: fixed; - inset: 0; - z-index: 15; - background: rgba(0, 0, 0, 0.5); -} -.overlay.open { - display: block; +.nav a[aria-current="page"] { + color: var(--text); + background: var(--accent); } -/* Layout */ -.layout { +/* Header right */ +.header-right { display: flex; - gap: var(--gutter); - padding: var(--gutter); - flex: 1; - max-width: 1200px; - margin: 0 auto; - width: 100%; + align-items: center; + gap: 6px; + margin-left: auto; + flex-shrink: 0; } -/* Sidebar */ -.sidebar { - width: 240px; - flex-shrink: 0; - position: sticky; - top: 72px; - align-self: flex-start; - max-height: calc(100vh - 88px); - overflow-y: auto; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--panel); +.header-link { + padding: 5px 10px; + font-size: 13px; + border-radius: 6px; + color: var(--muted); + white-space: nowrap; +} +.header-link:hover { + color: var(--text); + background: var(--codebg); + text-decoration: none; } -.nav { - padding: 8px; +/* Theme toggle */ +.theme-toggle { display: flex; - flex-direction: column; - gap: 2px; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + background: transparent; + border: 1px solid var(--border); + border-radius: 6px; + cursor: pointer; + color: var(--muted); + transition: color 0.15s, border-color 0.15s; } - -.nav a { - display: block; - padding: 10px 12px; - border-radius: 8px; +.theme-toggle:hover { color: var(--text); - font-size: 14px; + border-color: var(--muted); } -.nav a:hover { - background: var(--codebg); - text-decoration: none; +.theme-toggle .icon-moon { + display: none; } -.nav a[aria-current="page"] { - background: color-mix(in srgb, var(--link) 15%, transparent); +:root[data-theme="dark"] .theme-toggle .icon-sun { + display: none; +} +:root[data-theme="dark"] .theme-toggle .icon-moon { + display: block; +} +:root[data-theme="light"] .theme-toggle .icon-sun { + display: block; +} +:root[data-theme="light"] .theme-toggle .icon-moon { + display: none; } /* Main content */ .main { flex: 1; - min-width: 0; + width: 100%; + max-width: 720px; + margin: 0 auto; + padding: var(--gutter); } .content { border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); - padding: 24px; + padding: 28px 32px; } /* Footer */ @@ -232,7 +219,7 @@ a:hover { } .footer-link { - font-size: 13px; + font-size: 12px; color: var(--muted); } .footer-link:hover { @@ -244,59 +231,126 @@ a:hover { .content h1, .content h2, .content h3 { - scroll-margin-top: 80px; + scroll-margin-top: 64px; + letter-spacing: -0.01em; } .content h1 { margin-top: 0; - font-size: 28px; + font-size: 24px; + font-weight: 700; } .content h2 { - font-size: 20px; - margin-top: 32px; + font-size: 17px; + font-weight: 600; + margin-top: 36px; + padding-top: 16px; + border-top: 1px solid var(--border); +} + +.content h2:first-of-type { + border-top: none; + padding-top: 0; } .content h3 { - font-size: 16px; + font-size: 14px; + font-weight: 600; margin-top: 24px; + color: var(--muted); } .content p, .content li { - line-height: 1.65; + line-height: 1.7; + font-size: 14px; } .content code { font-family: var(--mono); - font-size: 0.9em; + font-size: 0.85em; background: var(--codebg); - padding: 2px 6px; - border-radius: 6px; + padding: 2px 5px; + border-radius: 4px; + border: 1px solid var(--border); } .content pre { overflow-x: auto; - padding: 16px; - border-radius: 10px; + padding: 14px 16px; + border-radius: 8px; background: var(--codebg); border: 1px solid var(--border); - font-size: 14px; - line-height: 1.5; + font-size: 13px; + line-height: 1.6; } .content pre code { background: transparent; padding: 0; + border: none; + font-size: inherit; } .content details { - margin: 16px 0; + margin: 12px 0; + border: 1px solid var(--border); + border-radius: 8px; } .content details summary { cursor: pointer; - padding: 8px 0; + padding: 10px 14px; + font-size: 14px; + border-radius: 8px; +} + +.content details summary:hover { + background: var(--accent); +} + +.content details[open] summary { + border-bottom: 1px solid var(--border); + border-radius: 8px 8px 0 0; +} + +.content details > :not(summary) { + padding: 0 14px; +} + +.content hr { + border: none; + border-top: 1px solid var(--border); + margin: 28px 0; +} + +.content blockquote { + border-left: 3px solid var(--link); + margin: 16px 0; + padding: 4px 16px; + color: var(--muted); + background: var(--accent); + border-radius: 0 6px 6px 0; +} + +.content table { + width: 100%; + border-collapse: collapse; + font-size: 13px; + margin: 16px 0; +} + +.content th, +.content td { + padding: 8px 12px; + border: 1px solid var(--border); + text-align: left; +} + +.content th { + background: var(--codebg); + font-weight: 600; } .empty { @@ -304,64 +358,46 @@ a:hover { } /* Mobile */ -@media (max-width: 768px) { - :root { - --gutter: 16px; +@media (max-width: 640px) { + .header { + gap: 4px; + padding: 0 12px; } - .menu-toggle { - display: flex; - } - - .layout { - flex-direction: column; + .nav { + margin-left: 8px; + gap: 0; } - .sidebar { - position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 280px; - max-width: 80vw; - z-index: 16; - border: none; - border-radius: 0; - border-right: 1px solid var(--border); - max-height: none; - padding-top: 60px; - transform: translateX(-100%); - transition: transform 0.2s ease-out; + .nav a { + padding: 6px 8px; + font-size: 12px; } - .sidebar.open { - transform: translateX(0); + .header-link { + font-size: 12px; + padding: 5px 8px; } - .nav { + .main { padding: 12px; } - .nav a { - padding: 12px 14px; - font-size: 15px; - } - .content { - padding: 20px; - border-radius: 10px; + padding: 20px 16px; + border-radius: 8px; } .content h1 { - font-size: 24px; + font-size: 20px; } .content h2 { - font-size: 18px; + font-size: 16px; } .content pre { - font-size: 13px; - padding: 14px; + font-size: 12px; + padding: 12px; } }