diff --git a/apps/website/public/whitepaper-preview.html b/apps/website/public/whitepaper-preview.html index b748dc34..9e15c3b2 100644 --- a/apps/website/public/whitepaper-preview.html +++ b/apps/website/public/whitepaper-preview.html @@ -19,38 +19,38 @@ -
+
@ngaf/langgraph · Production Readiness Guide

From
Prototype
to
Production

The Angular Agent Readiness Guide

-
cacheplane.ai · 2026
+
cacheplane.ai · 2026

Contents

-
+
01 Streaming State Management
-
+
02 Thread Persistence
-
+
03 Tool-Call Rendering
-
+
04 Human Approval Flows
-
+
05 Generative UI
-
+
06 Deterministic Testing
diff --git a/apps/website/public/whitepaper.pdf b/apps/website/public/whitepaper.pdf index 75ccfd75..1295bdff 100644 Binary files a/apps/website/public/whitepaper.pdf and b/apps/website/public/whitepaper.pdf differ diff --git a/apps/website/public/whitepapers/angular-preview.html b/apps/website/public/whitepapers/angular-preview.html index 39548d3c..665efb33 100644 --- a/apps/website/public/whitepapers/angular-preview.html +++ b/apps/website/public/whitepapers/angular-preview.html @@ -19,38 +19,38 @@ -
+
@ngaf/langgraph · Enterprise Guide

The
Enterprise
Guide
to
Agent
Streaming
in
Angular

Ship LangGraph agents in Angular — without building the plumbing

-
cacheplane.ai · 2026
+
cacheplane.ai · 2026

Contents

-
+
01 The Last-Mile Problem
-
+
02 The agent() API
-
+
03 Thread Persistence & Memory
-
+
04 Interrupt & Approval Flows
-
+
05 Full LangGraph Feature Coverage
-
+
06 Deterministic Testing
diff --git a/apps/website/public/whitepapers/angular.pdf b/apps/website/public/whitepapers/angular.pdf index 463d227c..a439dc90 100644 Binary files a/apps/website/public/whitepapers/angular.pdf and b/apps/website/public/whitepapers/angular.pdf differ diff --git a/apps/website/public/whitepapers/chat-preview.html b/apps/website/public/whitepapers/chat-preview.html index 3dcfa93f..1753b138 100644 --- a/apps/website/public/whitepapers/chat-preview.html +++ b/apps/website/public/whitepapers/chat-preview.html @@ -19,34 +19,34 @@ -
+
@ngaf/chat · Enterprise Guide

The
Enterprise
Guide
to
Agent
Chat
Interfaces
in
Angular

Production agent chat UI in days, not sprints

-
cacheplane.ai · 2026
+
cacheplane.ai · 2026

Contents

-
+
01 The Sprint Tax
-
+
02 Batteries-Included Components
-
+
03 Theming & Design System Integration
-
+
04 Generative UI in Chat
-
+
05 Debug Tooling
diff --git a/apps/website/public/whitepapers/chat.pdf b/apps/website/public/whitepapers/chat.pdf index fd83e4ec..7edb5528 100644 Binary files a/apps/website/public/whitepapers/chat.pdf and b/apps/website/public/whitepapers/chat.pdf differ diff --git a/apps/website/public/whitepapers/render-preview.html b/apps/website/public/whitepapers/render-preview.html index 6258a0c3..4317523b 100644 --- a/apps/website/public/whitepapers/render-preview.html +++ b/apps/website/public/whitepapers/render-preview.html @@ -19,34 +19,34 @@ -
+
@ngaf/render · Enterprise Guide

The
Enterprise
Guide
to
Generative
UI
in
Angular

Agents that render UI — without coupling to your frontend

-
cacheplane.ai · 2026
+
cacheplane.ai · 2026

Contents

-
+
01 The Coupling Problem
-
+
02 Declarative UI Specs & the json-render Standard
-
+
03 The Component Registry
-
+
04 Streaming JSON Patches
-
+
05 State Management & Computed Functions
diff --git a/apps/website/public/whitepapers/render.pdf b/apps/website/public/whitepapers/render.pdf index 0f0cacb2..3559958d 100644 Binary files a/apps/website/public/whitepapers/render.pdf and b/apps/website/public/whitepapers/render.pdf differ diff --git a/apps/website/scripts/instance-garamond.py b/apps/website/scripts/instance-garamond.py new file mode 100644 index 00000000..53e70f4f --- /dev/null +++ b/apps/website/scripts/instance-garamond.py @@ -0,0 +1,66 @@ +""" +Generate apps/website/src/app/EBGaramond-Bold.ttf from the upstream EB +Garamond variable font. + +Why this script exists: +- Satori (the engine behind Next.js ImageResponse) crashes on variable-weight + TTFs with "Cannot read properties of undefined (reading '256')". +- Google Fonts only serves Garamond as woff2, which Satori also can't decode. +- So we instance the upstream variable font to a single weight (Bold, 700) + and strip the now-unused variable-font tables, producing a static TTF + Satori parses cleanly. + +The output is committed to the repo and consumed by +apps/website/src/app/opengraph-image.tsx at request time. + +Usage: + pip install --user fonttools + python3 apps/website/scripts/instance-garamond.py + +Re-run if the upstream font is updated. +""" +import os +import tempfile +import urllib.request + +from fontTools.ttLib import TTFont +from fontTools.varLib.instancer import instantiateVariableFont + +UPSTREAM_URL = "https://github.com/google/fonts/raw/main/ofl/ebgaramond/EBGaramond%5Bwght%5D.ttf" +TARGET_WEIGHT = 700 +OUTPUT_PATH = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "src", + "app", + "EBGaramond-Bold.ttf", +) + + +def main() -> None: + print(f"Downloading variable TTF from {UPSTREAM_URL}") + with tempfile.NamedTemporaryFile(suffix=".ttf", delete=False) as tmp: + with urllib.request.urlopen(UPSTREAM_URL) as response: + tmp.write(response.read()) + src_path = tmp.name + + try: + print(f"Instancing to wght={TARGET_WEIGHT}") + font = TTFont(src_path) + static = instantiateVariableFont(font, {"wght": TARGET_WEIGHT}) + + # Drop variable-font tables that no longer serve a purpose and that + # Satori doesn't need. Shaves ~300KB off the file. + for tag in ("STAT", "fvar", "MVAR", "HVAR"): + if tag in static: + del static[tag] + + print(f"Writing {OUTPUT_PATH}") + static.save(OUTPUT_PATH) + size_kb = os.path.getsize(OUTPUT_PATH) // 1024 + print(f"Done — {size_kb} KB") + finally: + os.unlink(src_path) + + +if __name__ == "__main__": + main() diff --git a/apps/website/scripts/refresh-whitepaper-covers.ts b/apps/website/scripts/refresh-whitepaper-covers.ts new file mode 100644 index 00000000..4abbccf4 --- /dev/null +++ b/apps/website/scripts/refresh-whitepaper-covers.ts @@ -0,0 +1,118 @@ +/** + * Refresh whitepaper PDF covers without regenerating chapter content. + * + * The chapter prose in apps/website/public/whitepaper-preview.html and + * apps/website/public/whitepapers/*-preview.html is LLM-generated and + * acceptable as-is. Only the cover styling needs to match the new + * Statusbrew aesthetic (PR #277 updated the source script; this refreshes + * the rendered artifacts surgically without needing ANTHROPIC_API_KEY). + * + * What it does: + * 1. For each preview HTML, find/replace the legacy 4-stop pastel cover + * gradient with a per-paper subtle 2-stop tint matching the new tokens. + * 2. Update the cover footer color (#888 → #8b8fa3 / textMuted). + * 3. Update TOC row colors (rgba(0,0,0,.06) → #e6e8ee / border; + * #444 → #555770 / textSecondary). + * 4. Write the updated HTML back. + * 5. Render to PDF using Puppeteer. + * + * Usage: + * pnpm tsx apps/website/scripts/refresh-whitepaper-covers.ts + */ +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import puppeteer from 'puppeteer'; + +interface Paper { + /** Per-paper cover gradient (matches the source updated in PR #277). */ + newGradient: string; + htmlPath: string; + pdfPath: string; +} + +const ROOT = join(process.cwd(), 'apps', 'website', 'public'); + +const PAPERS: Paper[] = [ + { + newGradient: 'linear-gradient(135deg, #fafbfc 0%, #eaf3ff 100%)', + htmlPath: join(ROOT, 'whitepaper-preview.html'), + pdfPath: join(ROOT, 'whitepaper.pdf'), + }, + { + newGradient: 'linear-gradient(135deg, #fafbfc 0%, #eaf3ff 100%)', + htmlPath: join(ROOT, 'whitepapers', 'angular-preview.html'), + pdfPath: join(ROOT, 'whitepapers', 'angular.pdf'), + }, + { + newGradient: 'linear-gradient(135deg, #fafbfc 0%, #e8f5e9 100%)', + htmlPath: join(ROOT, 'whitepapers', 'render-preview.html'), + pdfPath: join(ROOT, 'whitepapers', 'render.pdf'), + }, + { + newGradient: 'linear-gradient(135deg, #fafbfc 0%, #f3e8ff 100%)', + htmlPath: join(ROOT, 'whitepapers', 'chat-preview.html'), + pdfPath: join(ROOT, 'whitepapers', 'chat.pdf'), + }, +]; + +const LEGACY_GRADIENT_RE = + /background:linear-gradient\(135deg,\s*#[0-9a-fA-F]+\s+0%,\s*#[0-9a-fA-F]+\s+45%,\s*#[0-9a-fA-F]+\s+70%,\s*#[0-9a-fA-F]+\s+100%\)/; + +function refreshHtml(html: string, paper: Paper): string { + let out = html; + + if (!LEGACY_GRADIENT_RE.test(out)) { + throw new Error(`No legacy gradient found in ${paper.htmlPath}`); + } + out = out.replace(LEGACY_GRADIENT_RE, `background:${paper.newGradient}`); + + // Cover footer: #888 → #8b8fa3 (textMuted) + out = out.replace(/font-size:13px;color:#888/g, 'font-size:13px;color:#8b8fa3'); + + // TOC row: rgba(0,0,0,.06) border → #e6e8ee (border token); #444 text → #555770 (textSecondary) + out = out.replace( + /border-bottom:1px solid rgba\(0,0,0,\.06\);font-size:15px;color:#444/g, + 'border-bottom:1px solid #e6e8ee;font-size:15px;color:#555770', + ); + + return out; +} + +async function renderPdf(htmlPath: string, pdfPath: string): Promise { + const html = await readFile(htmlPath, 'utf8'); + const browser = await puppeteer.launch({ headless: true }); + try { + const page = await browser.newPage(); + await page.setContent(html, { waitUntil: 'networkidle0' }); + await page.pdf({ + path: pdfPath, + format: 'A4', + printBackground: true, + margin: { top: '0', right: '0', bottom: '0', left: '0' }, + }); + } finally { + await browser.close(); + } +} + +async function main(): Promise { + for (const paper of PAPERS) { + console.log(`\n— ${paper.htmlPath}`); + const original = await readFile(paper.htmlPath, 'utf8'); + const refreshed = refreshHtml(original, paper); + if (refreshed === original) { + console.log(' (no changes — already on new aesthetic)'); + } else { + await writeFile(paper.htmlPath, refreshed, 'utf8'); + console.log(' HTML refreshed'); + } + await renderPdf(paper.htmlPath, paper.pdfPath); + console.log(` PDF rendered → ${paper.pdfPath}`); + } + console.log('\n✓ All 4 whitepaper covers refreshed.'); +} + +main().catch((err) => { + console.error('Refresh failed:', err); + process.exit(1); +}); diff --git a/apps/website/src/app/EBGaramond-Bold.ttf b/apps/website/src/app/EBGaramond-Bold.ttf new file mode 100644 index 00000000..5106c29b Binary files /dev/null and b/apps/website/src/app/EBGaramond-Bold.ttf differ diff --git a/apps/website/src/app/opengraph-image.tsx b/apps/website/src/app/opengraph-image.tsx index 3e88f28d..1c24eb27 100644 --- a/apps/website/src/app/opengraph-image.tsx +++ b/apps/website/src/app/opengraph-image.tsx @@ -7,7 +7,8 @@ */ import { ImageResponse } from 'next/og'; -export const runtime = 'edge'; +// Node runtime (not edge) so we can read the bundled Garamond TTF off disk. +export const runtime = 'nodejs'; export const alt = 'Angular Agent Framework — Signal-native streaming for Angular + LangGraph'; export const size = { width: 1200, height: 630 }; export const contentType = 'image/png'; @@ -31,9 +32,35 @@ async function loadFont(family: string, weight: number): Promise { + try { + const { fileURLToPath } = await import('node:url'); + const { readFile } = await import('node:fs/promises'); + const { dirname, join } = await import('node:path'); + const here = dirname(fileURLToPath(import.meta.url)); + const buf = await readFile(join(here, 'EBGaramond-Bold.ttf')); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; + } catch (err) { + console.warn('opengraph-image: failed to load local Garamond TTF', err); + return null; + } +} + export default async function OpenGraphImage() { const [garamondBold, interRegular, interBold, monoBold] = await Promise.all([ - loadFont('EB+Garamond', 700), + loadLocalGaramond(), loadFont('Inter', 400), loadFont('Inter', 600), loadFont('JetBrains+Mono', 700),