779 lines
43 KiB
Markdown
779 lines
43 KiB
Markdown
# Phase 41: Diagrams, Icons & Theme Engine — Research
|
|
|
|
**Researched:** 2026-04-04
|
|
**Domain:** Server-side SVG generation, OKLCH color engine, LLM-driven content rendering, React UI panels
|
|
**Confidence:** HIGH
|
|
|
|
---
|
|
|
|
<user_constraints>
|
|
## User Constraints (from CONTEXT.md)
|
|
|
|
### Locked Decisions
|
|
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
|
|
|
|
### Claude's Discretion
|
|
Full discretion on: Mermaid server-side rendering approach, icon SVG generation strategy, OKLCH palette algorithm, multi-asset job output pattern, UI panel structure, export format implementations.
|
|
|
|
### Deferred Ideas (OUT OF SCOPE)
|
|
None — discuss phase skipped.
|
|
</user_constraints>
|
|
|
|
---
|
|
|
|
<phase_requirements>
|
|
## Phase Requirements
|
|
|
|
| ID | Description | Research Support |
|
|
|----|-------------|------------------|
|
|
| DIAG-01 | User can generate diagrams from natural language description | LLM generates Mermaid syntax from prompt; job routed via jobType="diagram" |
|
|
| DIAG-02 | System renders Mermaid syntax to SVG and PNG formats | Server-side: Playwright headless Chromium loads mermaid.js; SVG output sanitized with DOMPurify; PNG via @resvg/resvg-js or sharp following org-chart-svg.ts pattern |
|
|
| DIAG-03 | User can view and edit the Mermaid source for refinement | DiagramSourcePanel collapsible with editable Textarea; re-render sends new POST job |
|
|
| DIAG-04 | System supports architecture, flowchart, ERD, sequence, and mind map diagram types | Mermaid supports all five; diagram type selector maps to preamble hints in LLM prompt |
|
|
| DIAG-05 | Mermaid rendering enforces strict security level to prevent XSS | Strip %%{init}%% and click directives server-side (regex) before render; DOMPurify already in server deps (v3.3.2) sanitizes SVG output |
|
|
| ICON-01 | User can generate SVG icons from a text description | LLM (Claude/Ollama) generates SVG path markup via structured prompt; job routed jobType="icon-set" |
|
|
| ICON-02 | System produces icon sets with consistent visual style | Prompt enforces style (outline/filled/rounded), viewBox="0 0 24 24", stroke-width=1.5 convention; SVGO cleans output |
|
|
| ICON-03 | User can export icons in multiple sizes and formats (SVG, PNG) | SVG stored in bundle asset; PNG variants (16, 32, 64) generated server-side via sharp(svgBuffer, {density:96}).resize(N).png() |
|
|
| THEME-01 | User can pick a seed color and receive a complete palette | ThemeSeedInput sends hex to server; server computes palette via culori 4.0.2 OKLCH math |
|
|
| THEME-02 | System generates palette in OKLCH color space with Catppuccin-style naming | culori: parse hex, convert to oklch, derive palette roles (bg, surface, overlay, text, accent-1/2/3) by L/C adjustments; output as OKLCH + hex |
|
|
| THEME-03 | System validates WCAG AA contrast for all foreground/background pairs | wcag-contrast 3.0.0: score(fg, bg) >= 4.5 = AA pass; computed per swatch pair |
|
|
| THEME-04 | User can preview Nexus UI with the generated palette live | ThemePreviewPanel scoped to .nexus-theme-preview; inject CSS vars via JS into that container only; no global bleed |
|
|
| THEME-05 | User can export palette as CSS custom properties, Tailwind config, VS Code theme, or JSON | Four pure-TypeScript formatters; no external deps needed |
|
|
| THEME-06 | System generates dark and light variants from single seed color | Two palette passes: dark (Catppuccin Mocha L ranges) and light (Catppuccin Latte L ranges), both from same hue |
|
|
| THEME-07 | User can apply generated theme to their Nexus instance in one click | PATCH /api/nexus/settings with new theme tokens; ThemeContext extended to accept custom token map; persist in nexus-settings.json |
|
|
</phase_requirements>
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Phase 41 builds three content generators on top of the Phase 40 job infrastructure: Mermaid diagram rendering, LLM-driven SVG icon generation, and an OKLCH theme engine. All three follow the same async pattern — POST /content-jobs returns 202 + jobId, SSE stream delivers progress, result assets land in the `generated` namespace.
|
|
|
|
**Mermaid rendering** on the server cannot use the browser mermaid library directly via jsdom (mermaid 11 requires real browser SVG APIs that jsdom does not implement). The solution is to use Playwright's already-installed Chromium binary (`~/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`) to run a headless page that calls `mermaid.render()`. This avoids adding new binary dependencies — the Chromium is already downloaded for e2e tests. SVG output is sanitized with DOMPurify (already in server deps). PNG is produced by `sharp(svgBuffer, { density: 144 })` following the exact same pattern as `renderOrgChartPng` in `org-chart-svg.ts`.
|
|
|
|
**Icon generation** uses the LLM to produce SVG path markup from a text description, then SVGO to clean and optimize the output. The job produces a JSON bundle stored as the primary asset, containing N SVG strings plus metadata. PNG variants are generated server-side for the bundle. This sidesteps the "N icons = N separate assets" problem since `content_jobs.resultAssetId` is a single UUID.
|
|
|
|
**The theme engine** uses `culori` (needs to be added as a server dependency — not yet installed there) for all OKLCH math, with `wcag-contrast` for AA validation. "Apply theme" extends the existing `nexus-settings.json` persistence and `ThemeContext` to support a custom token map injected into `document.documentElement` CSS variables.
|
|
|
|
**Primary recommendation:** Add `culori`, `@resvg/resvg-js`, `svgo`, and `wcag-contrast` to server deps. Add `playwright-core` to server deps for headless Mermaid rendering using the already-installed Chromium. Keep icon generation LLM-only with SVGO cleanup — no external image API required.
|
|
|
|
---
|
|
|
|
## Standard Stack
|
|
|
|
### Core (Server — new additions needed)
|
|
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| culori | 4.0.2 | OKLCH color math (parse, convert, derive palette) | Mandated by STATE.md: "OKLCH via culori — HSL is forbidden as an intermediate"; perceptually uniform; ships ESM + CJS bundle |
|
|
| @resvg/resvg-js | 2.6.2 | SVG to PNG rasterization | Pure Rust via NAPI, no librsvg dependency, cross-platform; linux-x64-gnu confirmed available |
|
|
| wcag-contrast | 3.0.0 | WCAG AA contrast ratio validation | Small, correct; uses WCAG 2.x relative luminance formula |
|
|
| svgo | 4.0.1 | SVG optimization and cleanup for LLM-generated icon SVGs | Cleans up LLM output, reduces file size 30-60%, fixes degenerate paths |
|
|
| playwright-core | 1.58.2 | Headless Chromium for server-side Mermaid rendering | `playwright-core` has no bundled browser; uses already-installed Chromium from devDep; keeps server dep minimal |
|
|
|
|
### Core (Server — already installed)
|
|
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| sharp | 0.34.5 | PNG output for diagrams and icons (SVG buffer to PNG) | Already in server deps; established pattern in org-chart-svg.ts |
|
|
| dompurify | 3.3.2 | Sanitize SVG output from Mermaid render | Already in server deps; required by DIAG-05 |
|
|
| jsdom | 28.1.0 | DOM environment for DOMPurify in Node | Already in server deps; DOMPurify requires a DOM |
|
|
| mermaid | 11.14.0 | Client-side Mermaid rendering (UI only, loaded in headless page) | Already in ui/package.json; also loaded via CDN in headless page |
|
|
|
|
### Core (UI — new additions needed)
|
|
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| shadcn progress | — | Job progress bar | Specified in UI-SPEC.md |
|
|
| shadcn toggle | — | Dark/light variant switcher in theme preview | Specified in UI-SPEC.md |
|
|
|
|
### Alternatives Considered
|
|
|
|
| Instead of | Could Use | Tradeoff |
|
|
|------------|-----------|----------|
|
|
| playwright-core for Mermaid | @mermaid-js/mermaid-cli (mmdc) | mmdc ships its own Chromium (~300MB extra); STATE.md blocker explicitly asks to check whether playwright and mmdc can share a binary; playwright-core avoids the duplicate download |
|
|
| playwright-core for Mermaid | mermaid + jsdom on server | Mermaid 11 requires real browser DOM (SVG getBBox, foreignObject); jsdom does not implement these — rendering will fail silently |
|
|
| @resvg/resvg-js for SVG to PNG | sharp(svgBuffer) | sharp depends on librsvg for SVG input support; not guaranteed in all libvips builds; @resvg/resvg-js is self-contained Rust; use @resvg/resvg-js as primary, sharp as fallback |
|
|
| LLM-generated SVG for icons | Stable Diffusion / raster then trace | Explicitly out of scope per REQUIREMENTS.md; no external API; LLM SVG is instant and vector |
|
|
| culori 4.0.2 | chroma-js 3.2.0 | chroma-js lacks OKLCH support; STATE.md mandates culori |
|
|
|
|
**Installation (server):**
|
|
```bash
|
|
pnpm --filter server add culori @resvg/resvg-js wcag-contrast svgo playwright-core
|
|
```
|
|
|
|
**Installation (UI — shadcn components):**
|
|
```bash
|
|
pnpm --filter ui exec shadcn add progress
|
|
pnpm --filter ui exec shadcn add toggle
|
|
```
|
|
|
|
**Version verification (confirmed 2026-04-04):**
|
|
- `@resvg/resvg-js`: 2.6.2 (`npm info @resvg/resvg-js version`)
|
|
- `culori`: 4.0.2 (`npm info culori version`)
|
|
- `wcag-contrast`: 3.0.0 (`npm info wcag-contrast version`)
|
|
- `svgo`: 4.0.1 (`npm info svgo version`)
|
|
- `playwright-core`: 1.58.2 (same version as `@playwright/test` already installed; must match exactly)
|
|
|
|
---
|
|
|
|
## Architecture Patterns
|
|
|
|
### Recommended Project Structure
|
|
|
|
```
|
|
server/src/
|
|
├── services/
|
|
│ ├── content-job-runner.ts # MODIFY: extend renderContent() switch with new jobTypes
|
|
│ ├── renderers/
|
|
│ │ ├── diagram-renderer.ts # NEW: Mermaid headless rendering + DOMPurify + PNG
|
|
│ │ ├── icon-renderer.ts # NEW: LLM SVG prompt + SVGO + PNG variants
|
|
│ │ └── theme-renderer.ts # NEW: OKLCH palette engine + WCAG validation + exporters
|
|
│ └── ...
|
|
ui/src/
|
|
├── pages/
|
|
│ └── ContentStudio.tsx # NEW: tabbed page hosting all three generators
|
|
├── components/
|
|
│ ├── DiagramGeneratePanel.tsx # NEW
|
|
│ ├── DiagramPreview.tsx # NEW (reuses .paperclip-mermaid CSS class from index.css)
|
|
│ ├── DiagramSourcePanel.tsx # NEW
|
|
│ ├── IconGeneratePanel.tsx # NEW
|
|
│ ├── IconResultGrid.tsx # NEW
|
|
│ ├── IconDownloadBar.tsx # NEW
|
|
│ ├── ThemeSeedInput.tsx # NEW
|
|
│ ├── ThemePaletteGrid.tsx # NEW
|
|
│ ├── ThemePreviewPanel.tsx # NEW
|
|
│ ├── ThemeExportTabs.tsx # NEW
|
|
│ └── ThemeApplyConfirmDialog.tsx # NEW
|
|
├── api/
|
|
│ └── contentJobs.ts # NEW: POST/GET /content-jobs + SSE EventSource helper
|
|
└── hooks/
|
|
└── useContentJob.ts # NEW: submit job + subscribe to SSE progress
|
|
```
|
|
|
|
### Pattern 1: renderContent() Switch Extension (Server)
|
|
|
|
Phase 40 established the stub. Phase 41 fills in real renderers:
|
|
|
|
```typescript
|
|
// server/src/services/content-job-runner.ts — modified
|
|
import { renderDiagram } from "./renderers/diagram-renderer.js";
|
|
import { renderIconSet } from "./renderers/icon-renderer.js";
|
|
import { renderThemePalette } from "./renderers/theme-renderer.js";
|
|
|
|
export async function renderContent(
|
|
jobType: string,
|
|
input: Record<string, unknown>,
|
|
): Promise<{ filename: string; contentType: string; buffer: Buffer }> {
|
|
switch (jobType) {
|
|
case "diagram": return renderDiagram(input);
|
|
case "icon-set": return renderIconSet(input);
|
|
case "theme-palette": return renderThemePalette(input);
|
|
default:
|
|
throw new Error(`Unknown jobType: ${jobType}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Headless Mermaid Rendering (Server)
|
|
|
|
Playwright's Chromium is already installed at `~/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`. Use `playwright-core` (no bundled browsers) for server-side mermaid rendering. The playwright version in `package.json` must match the installed Chromium version (1.58.2).
|
|
|
|
```typescript
|
|
// server/src/services/renderers/diagram-renderer.ts
|
|
import { chromium } from "playwright-core";
|
|
import { JSDOM } from "jsdom";
|
|
import DOMPurify from "dompurify";
|
|
import { Resvg } from "@resvg/resvg-js";
|
|
|
|
const INIT_BLOCK_RE = /%%\{[\s\S]*?\}%%/g;
|
|
const CLICK_LINE_RE = /^\s*click\s+.*/gim;
|
|
|
|
function stripUnsafeDirectives(source: string): { cleaned: string; stripped: boolean } {
|
|
const withoutInit = source.replace(INIT_BLOCK_RE, "");
|
|
const withoutClick = withoutInit.replace(CLICK_LINE_RE, "");
|
|
const cleaned = withoutClick.trim();
|
|
return { cleaned, stripped: cleaned !== source.trim() };
|
|
}
|
|
|
|
export async function renderDiagram(
|
|
input: Record<string, unknown>,
|
|
): Promise<{ filename: string; contentType: string; buffer: Buffer }> {
|
|
const source = String(input.source ?? "");
|
|
const darkMode = Boolean(input.darkMode ?? false);
|
|
const { cleaned, stripped } = stripUnsafeDirectives(source);
|
|
|
|
const browser = await chromium.launch({
|
|
executablePath: resolveBrowserPath(), // resolves ~/.cache/ms-playwright/...
|
|
headless: true,
|
|
});
|
|
try {
|
|
const page = await browser.newPage();
|
|
// Inline mermaid rendering page — mermaid loaded from ui/node_modules via relative import
|
|
// or CDN for simplicity in headless context
|
|
await page.setContent(buildMermaidHtml(cleaned, darkMode));
|
|
await page.waitForSelector("#render svg", { timeout: 15_000 });
|
|
const svgRaw = await page.$eval("#render", (el: Element) => el.innerHTML);
|
|
|
|
// Sanitize SVG output
|
|
const { window } = new JSDOM("");
|
|
const purify = DOMPurify(window as unknown as Window);
|
|
const svgClean = purify.sanitize(svgRaw, { USE_PROFILES: { svg: true } });
|
|
const svgBuffer = Buffer.from(svgClean);
|
|
|
|
// Rasterize to PNG via @resvg/resvg-js (no librsvg dependency)
|
|
const resvg = new Resvg(svgClean, { dpi: 144 });
|
|
const pngBuffer = resvg.render().asPng();
|
|
|
|
return buildDiagramBundle(svgBuffer, pngBuffer, stripped);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 3: Multi-Asset Job Output (JSON Bundle)
|
|
|
|
The `content_jobs.resultAssetId` is a single UUID. For jobs producing multiple files (SVG + PNG for diagrams, N SVGs for icon sets), store all output in a **JSON bundle asset** (`application/json`). The UI parses the bundle to offer individual downloads.
|
|
|
|
```typescript
|
|
// Bundle schema (discriminated union by type field)
|
|
type DiagramBundle = {
|
|
type: "diagram-bundle";
|
|
svgBase64: string;
|
|
pngBase64: string;
|
|
mermaidSource: string;
|
|
stripped: boolean;
|
|
};
|
|
|
|
type IconSetBundle = {
|
|
type: "icon-set-bundle";
|
|
icons: Array<{
|
|
name: string;
|
|
style: "outline" | "filled" | "rounded";
|
|
svgSource: string;
|
|
svgBase64: string;
|
|
}>;
|
|
};
|
|
|
|
type ThemePaletteBundle = {
|
|
type: "theme-palette-bundle";
|
|
seedHex: string;
|
|
palette: PaletteRole[];
|
|
exports: { css: string; tailwind: string; vscode: string; json: string };
|
|
};
|
|
```
|
|
|
|
### Pattern 4: OKLCH Palette Engine (Server)
|
|
|
|
culori 4.0.2 ships a CJS bundle at `./bundled/culori.cjs` and ESM at `./src/index.js`. In the server (tsx/ESM context), import from culori directly. The STATE.md constraint "HSL is forbidden as an intermediate" means all color derivation must operate in OKLCH directly.
|
|
|
|
```typescript
|
|
// server/src/services/renderers/theme-renderer.ts
|
|
import { oklch, formatHex, converter } from "culori";
|
|
import wcagContrast from "wcag-contrast";
|
|
|
|
const toOklch = converter("oklch");
|
|
|
|
// L/C values approximate Catppuccin Mocha (dark) and Latte (light) ranges
|
|
const DARK_ROLES = [
|
|
{ name: "background", l: 0.14, c: 0.010 },
|
|
{ name: "surface", l: 0.17, c: 0.012 },
|
|
{ name: "overlay", l: 0.22, c: 0.015 },
|
|
{ name: "text", l: 0.93, c: 0.008 },
|
|
{ name: "accent-1", l: 0.72, c: 0.15 },
|
|
{ name: "accent-2", l: 0.65, c: 0.13 },
|
|
{ name: "accent-3", l: 0.58, c: 0.10 },
|
|
];
|
|
|
|
const LIGHT_ROLES = [
|
|
{ name: "background", l: 0.94, c: 0.005 },
|
|
{ name: "surface", l: 0.91, c: 0.008 },
|
|
{ name: "overlay", l: 0.85, c: 0.012 },
|
|
{ name: "text", l: 0.28, c: 0.008 },
|
|
{ name: "accent-1", l: 0.55, c: 0.16 },
|
|
{ name: "accent-2", l: 0.48, c: 0.14 },
|
|
{ name: "accent-3", l: 0.40, c: 0.11 },
|
|
];
|
|
|
|
export function buildPalette(seedHex: string): PaletteRole[] {
|
|
const seed = toOklch(seedHex);
|
|
if (!seed) throw new Error(`Invalid seed color: ${seedHex}`);
|
|
const hue = seed.h ?? 0;
|
|
|
|
return DARK_ROLES.map((darkRole, i) => {
|
|
const lightRole = LIGHT_ROLES[i]!;
|
|
const darkHex = formatHex({ mode: "oklch", l: darkRole.l, c: darkRole.c, h: hue });
|
|
const lightHex = formatHex({ mode: "oklch", l: lightRole.l, c: lightRole.c, h: hue });
|
|
// Text roles for contrast computation
|
|
const darkTextHex = formatHex({ mode: "oklch", l: 0.93, c: 0.008, h: hue });
|
|
const lightTextHex = formatHex({ mode: "oklch", l: 0.28, c: 0.008, h: hue });
|
|
return {
|
|
name: darkRole.name,
|
|
dark: {
|
|
oklch: `oklch(${darkRole.l} ${darkRole.c} ${hue.toFixed(1)})`,
|
|
hex: darkHex,
|
|
wcagAA: darkRole.name === "text" ? true : wcagContrast.hex(darkHex, darkTextHex) >= 4.5,
|
|
},
|
|
light: {
|
|
oklch: `oklch(${lightRole.l} ${lightRole.c} ${hue.toFixed(1)})`,
|
|
hex: lightHex,
|
|
wcagAA: lightRole.name === "text" ? true : wcagContrast.hex(lightHex, lightTextHex) >= 4.5,
|
|
},
|
|
};
|
|
});
|
|
}
|
|
```
|
|
|
|
### Pattern 5: Theme Preview CSS Injection (UI)
|
|
|
|
The `ThemePreviewPanel` must inject generated CSS variables into `.nexus-theme-preview` only — not into `document.documentElement`. This prevents the preview from bleeding into the nav/sidebar.
|
|
|
|
```typescript
|
|
// ui/src/components/ThemePreviewPanel.tsx
|
|
const ROLE_TO_TOKEN: Record<string, string> = {
|
|
"background": "--background",
|
|
"surface": "--card",
|
|
"overlay": "--secondary",
|
|
"text": "--foreground",
|
|
"accent-1": "--primary",
|
|
"accent-2": "--accent",
|
|
"accent-3": "--muted",
|
|
};
|
|
|
|
function injectPreviewTokens(
|
|
container: HTMLElement,
|
|
palette: PaletteRole[],
|
|
variant: "dark" | "light",
|
|
): void {
|
|
// Imperative DOM update — not React state — to avoid re-render loop
|
|
palette.forEach((role) => {
|
|
const tokenName = ROLE_TO_TOKEN[role.name];
|
|
if (!tokenName) return;
|
|
const value = variant === "dark" ? role.dark.hex : role.light.hex;
|
|
container.style.setProperty(tokenName, value);
|
|
});
|
|
}
|
|
```
|
|
|
|
### Pattern 6: useContentJob Hook (UI)
|
|
|
|
```typescript
|
|
// ui/src/hooks/useContentJob.ts
|
|
import { useState, useRef, useCallback } from "react";
|
|
import { api } from "../api/client";
|
|
|
|
interface JobState {
|
|
jobId: string | null;
|
|
status: "idle" | "queued" | "running" | "done" | "failed";
|
|
progress: number; // 0-100
|
|
resultAssetId: string | null;
|
|
errorMessage: string | null;
|
|
}
|
|
|
|
export function useContentJob(companyId: string) {
|
|
const [state, setState] = useState<JobState>({
|
|
jobId: null, status: "idle", progress: 0, resultAssetId: null, errorMessage: null,
|
|
});
|
|
const esRef = useRef<EventSource | null>(null);
|
|
|
|
const submit = useCallback(async (
|
|
jobType: string,
|
|
input: Record<string, unknown>,
|
|
) => {
|
|
const { jobId } = await api.post<{ jobId: string; status: string }>(
|
|
`/companies/${companyId}/content-jobs`,
|
|
{ jobType, input },
|
|
);
|
|
setState({ jobId, status: "queued", progress: 5, resultAssetId: null, errorMessage: null });
|
|
|
|
esRef.current?.close();
|
|
const es = new EventSource(
|
|
`/api/companies/${companyId}/content-jobs/${jobId}/events`,
|
|
{ withCredentials: true },
|
|
);
|
|
esRef.current = es;
|
|
|
|
es.addEventListener("status", (e) => {
|
|
const data = JSON.parse((e as MessageEvent).data) as {
|
|
status: string; resultAssetId?: string; errorMessage?: string;
|
|
};
|
|
setState((prev) => ({
|
|
...prev,
|
|
status: data.status as JobState["status"],
|
|
progress: data.status === "running" ? 50 : data.status === "done" ? 100 : prev.progress,
|
|
resultAssetId: data.resultAssetId ?? prev.resultAssetId,
|
|
errorMessage: data.errorMessage ?? null,
|
|
}));
|
|
if (data.status === "done" || data.status === "failed") es.close();
|
|
});
|
|
// EventSource reconnects automatically on error — no explicit handler needed
|
|
}, [companyId]);
|
|
|
|
return { state, submit };
|
|
}
|
|
```
|
|
|
|
### Pattern 7: "Apply Theme" to Nexus Instance
|
|
|
|
The existing `nexusSettingsService` persists JSON to `data/nexus-settings.json`. Extend its Zod schema with an optional `customTheme` field. When applied, the server stores the palette and the UI's `ThemeContext` reads it on startup.
|
|
|
|
```typescript
|
|
// Extend nexusSettingsSchema in nexus-settings.ts
|
|
const paletteRoleSchema = z.object({
|
|
name: z.string(),
|
|
dark: z.object({ oklch: z.string(), hex: z.string(), wcagAA: z.boolean() }),
|
|
light: z.object({ oklch: z.string(), hex: z.string(), wcagAA: z.boolean() }),
|
|
});
|
|
|
|
export const nexusSettingsSchema = z.object({
|
|
mode: z.enum(NEXUS_MODES).default("both"),
|
|
voiceEnabled: z.boolean().default(false),
|
|
voiceMode: z.enum(VOICE_MODES).default("text"),
|
|
telegramToken: z.string().optional(),
|
|
piperBinaryPath: z.string().optional(),
|
|
whisperBinaryPath: z.string().optional(),
|
|
customTheme: z.object({ // NEW
|
|
seedHex: z.string(),
|
|
palette: z.array(paletteRoleSchema),
|
|
}).optional(),
|
|
});
|
|
```
|
|
|
|
The `ThemeContext` already calls `applyTheme(theme)` which sets CSS variables on `document.documentElement`. Add `applyCustomTheme(palette, variant)` that injects the palette tokens into the root. On initial load, check if `nexusSettings.customTheme` is set and apply it. Expose "custom" as a Theme option in the THEME_META map.
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **Running mermaid directly in jsdom server-side:** Mermaid 11 requires `getBBox()`, `SVGMatrix`, and `foreignObject` support. jsdom does not implement these — rendering will silently produce empty or malformed SVG.
|
|
- **Storing N icons as N separate content_jobs:** Creates orphan accumulation. Store one JSON bundle asset per icon-set job containing all SVG strings.
|
|
- **HSL as intermediate in palette generation:** STATE.md explicitly forbids this. Use OKLCH throughout. No `hsl()` conversions anywhere in the palette pipeline.
|
|
- **Injecting theme CSS into `document.documentElement` during preview:** Bleeds into nav/sidebar. Use `.nexus-theme-preview` container scope via `container.style.setProperty()`.
|
|
- **Blocking HTTP on render:** All three renderers must go through the async job queue. No synchronous render endpoints.
|
|
- **Launching a new browser instance per request without cleanup:** Playwright `browser.close()` must be called in a `finally` block. Browser leaks will exhaust system memory.
|
|
|
|
---
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| OKLCH color math | Custom polar-to-cartesian | culori 4.0.2 | Correct gamut clamping, perceptual uniformity, all color space conversions; mandated by STATE.md |
|
|
| WCAG contrast ratio | Manual luminance formula | wcag-contrast 3.0.0 | WCAG 2.x formula requires exact sRGB linearization; easy to get wrong; library is small and correct |
|
|
| SVG optimization | String manipulation | svgo 4.0.1 | Handles degenerate paths, redundant attributes, precision issues in LLM output |
|
|
| SVG to PNG rasterization | Custom renderer | @resvg/resvg-js 2.6.2 | Production-quality Rust renderer; no librsvg dependency risk |
|
|
| Mermaid server-side | Custom graph layout | Playwright + mermaid.js | Mermaid's layout algorithms (dagre, elk) are non-trivial; existing browser implementation is correct and already maintained |
|
|
|
|
**Key insight:** The LLM-generated SVG for icons will contain verbose, non-optimized markup. SVGO with `preset-default` reduces file size by 30-60% and fixes common path errors from LLM output.
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: Playwright Browser Path in Production
|
|
|
|
**What goes wrong:** `chromium.launch()` without `executablePath` triggers a fresh Chromium download attempt. In the Nexus monorepo dev context, `@playwright/test` is in root devDependencies; `playwright-core` has no bundled browsers. Without the explicit path, launch fails.
|
|
|
|
**Why it happens:** The Playwright Chromium cache is at `~/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome` — installed by `@playwright/test` for e2e tests. `playwright-core` alone does not install any browsers.
|
|
|
|
**How to avoid:** Write a `resolveBrowserPath()` helper that reads `PLAYWRIGHT_BROWSERS_PATH` env var first, falls back to the default cache path `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome`, and throws a clear error if not found. Document the required env var in server startup.
|
|
|
|
**Warning signs:** `Error: browserType.launch: Failed to launch chromium because executable doesn't exist` in server logs.
|
|
|
|
### Pitfall 2: Mermaid `click` Directive Strip — Incomplete Regex
|
|
|
|
**What goes wrong:** A simple regex like `click\s+\w+` only strips `click NodeId` but Mermaid supports `click NodeId "url"` and `click NodeId call functionName()`. Incomplete strip leaves partial directives that break rendering.
|
|
|
|
**How to avoid:** Use a line-oriented strip — remove any line starting with `click` after normalization:
|
|
```typescript
|
|
const CLICK_LINE_RE = /^\s*click\s+.*/gim;
|
|
const INIT_BLOCK_RE = /%%\{[\s\S]*?\}%%/g;
|
|
const cleaned = source.replace(INIT_BLOCK_RE, "").replace(CLICK_LINE_RE, "");
|
|
```
|
|
|
|
**Warning signs:** Mermaid render errors for diagrams containing click interactions.
|
|
|
|
### Pitfall 3: culori ESM-only Import in CommonJS Context
|
|
|
|
**What goes wrong:** culori 4.0.2 ships `"."` pointing to `"./src/index.js"` (ESM) as primary export. If the server's TypeScript `moduleResolution` is CommonJS-style, `import { oklch } from "culori"` may fail at runtime.
|
|
|
|
**Why it happens:** The server uses `tsx` which is ESM-compatible, but if `server/tsconfig.json` uses `"moduleResolution": "node"` (pre-Node16), the ESM conditional export may not be honored.
|
|
|
|
**How to avoid:** Verify `server/tsconfig.json` `moduleResolution` setting. If it's `"node"` (not `"node16"` or `"bundler"`), use the explicit CJS path: `import culori from "culori/require"`. If it's `"node16"` or `"nodenext"`, standard `import { oklch } from "culori"` works. Check during Wave 0.
|
|
|
|
**Warning signs:** `ERR_REQUIRE_ESM` or module resolution errors at `tsx` startup.
|
|
|
|
### Pitfall 4: LLM SVG Output — Invalid viewBox
|
|
|
|
**What goes wrong:** LLMs generating SVG icons sometimes produce `viewBox` values inconsistent with path coordinates, or omit the `xmlns` attribute. Icons appear clipped or invisible.
|
|
|
|
**How to avoid:** After SVGO optimization, validate the output: (1) `viewBox` attribute is present — normalize to `"0 0 24 24"` if absent; (2) `xmlns="http://www.w3.org/2000/svg"` is present for standalone SVG files; (3) at least one `<path>`, `<circle>`, or `<rect>` element exists. Return an error to the user with copy "Render failed — {detail}. Try again." if validation fails.
|
|
|
|
**Warning signs:** Empty white squares in the icon grid; zero byte SVG files.
|
|
|
|
### Pitfall 5: ThemePreviewPanel Re-render Loop
|
|
|
|
**What goes wrong:** The theme preview updates CSS properties, which triggers a React re-render, which reads the CSS properties, which triggers another update.
|
|
|
|
**How to avoid:** Use `useRef` for the container element and set CSS properties imperatively (`container.style.setProperty()`), not via React state. The debounce fires outside the React render cycle. Only the palette data (from server) is stored in React state; CSS injection is a side effect in `useEffect`.
|
|
|
|
### Pitfall 6: content_jobs.resultAssetId is Single — Multi-file Output
|
|
|
|
**What goes wrong:** Diagram jobs produce SVG + PNG. Icon sets produce N SVG files. Storing each as a separate asset and pointing `resultAssetId` to only one breaks the download flow.
|
|
|
|
**How to avoid:** Store all output as a single JSON bundle asset (`application/json`). The UI parses the bundle to offer individual file downloads. The bundle schema is discriminated by `type` field (`"diagram-bundle"`, `"icon-set-bundle"`, `"theme-palette-bundle"`).
|
|
|
|
### Pitfall 7: Browser Instance Leak in diagram-renderer
|
|
|
|
**What goes wrong:** If the mermaid render page throws or times out, `browser.close()` never runs and the Chromium process is orphaned.
|
|
|
|
**How to avoid:** Always wrap the `page`/`browser` lifecycle in `try { ... } finally { await browser.close(); }`. Set a render timeout (15s) so the process does not hang indefinitely. Add a startup-time browser pool or singleton if latency becomes a concern (single-user context, so one browser at a time is acceptable).
|
|
|
|
---
|
|
|
|
## Code Examples
|
|
|
|
### Culori OKLCH Parse and Derive (culori 4.0.2)
|
|
|
|
```typescript
|
|
import { oklch, formatHex, converter } from "culori";
|
|
|
|
const toOklch = converter("oklch");
|
|
const seed = toOklch("#1e66f5");
|
|
// seed = { mode: "oklch", l: ~0.45, c: ~0.22, h: ~262 }
|
|
|
|
// Derive a background role:
|
|
const bg = formatHex({ mode: "oklch", l: 0.14, c: 0.010, h: seed.h ?? 0 });
|
|
// bg = "#191727" (approximately)
|
|
```
|
|
|
|
### WCAG Contrast Check (wcag-contrast 3.0.0)
|
|
|
|
```typescript
|
|
import wcagContrast from "wcag-contrast";
|
|
|
|
const ratio = wcagContrast.hex("#89b4fa", "#1e1e2e"); // ~8.5
|
|
const passesAA = ratio >= 4.5; // true
|
|
const passesAAA = ratio >= 7.0; // true
|
|
```
|
|
|
|
### DOMPurify in Node (jsdom + dompurify — existing server deps)
|
|
|
|
```typescript
|
|
import { JSDOM } from "jsdom";
|
|
import DOMPurify from "dompurify";
|
|
|
|
const { window } = new JSDOM("");
|
|
const purify = DOMPurify(window as unknown as Window);
|
|
const cleanSvg = purify.sanitize(rawSvg, { USE_PROFILES: { svg: true } });
|
|
```
|
|
|
|
### Sharp SVG to PNG (Established pattern from org-chart-svg.ts)
|
|
|
|
```typescript
|
|
import sharp from "sharp";
|
|
|
|
const pngBuffer = await sharp(Buffer.from(svgString), { density: 144 })
|
|
.resize(targetWidth, targetHeight)
|
|
.png()
|
|
.toBuffer();
|
|
```
|
|
|
|
### @resvg/resvg-js SVG to PNG (Primary alternative — no librsvg)
|
|
|
|
```typescript
|
|
import { Resvg } from "@resvg/resvg-js";
|
|
|
|
const resvg = new Resvg(svgString, { dpi: 144, fitTo: { mode: "width", value: 1200 } });
|
|
const pngBuffer = resvg.render().asPng();
|
|
```
|
|
|
|
### SVGO SVG Optimization (svgo 4.0.1)
|
|
|
|
```typescript
|
|
import { optimize } from "svgo";
|
|
|
|
const result = optimize(rawSvgString, { plugins: ["preset-default"] });
|
|
const optimizedSvg = result.data;
|
|
```
|
|
|
|
### Mermaid Security Strip
|
|
|
|
```typescript
|
|
const INIT_BLOCK_RE = /%%\{[\s\S]*?\}%%/g;
|
|
const CLICK_LINE_RE = /^\s*click\s+.*/gim;
|
|
|
|
function stripUnsafeDirectives(source: string): { cleaned: string; stripped: boolean } {
|
|
const withoutInit = source.replace(INIT_BLOCK_RE, "");
|
|
const withoutClick = withoutInit.replace(CLICK_LINE_RE, "");
|
|
const cleaned = withoutClick.trim();
|
|
return { cleaned, stripped: cleaned !== source.trim() };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| HSL color math for UI themes | OKLCH via culori | CSS Color 4 spec (2022+) | Perceptually uniform; gradients do not go gray in the middle |
|
|
| Mermaid CLI (mmdc) for server rendering | Playwright headless + mermaid.js | mermaid 10+ dropped jsdom compat | No new Chromium download; reuses already-installed browser |
|
|
| Manual WCAG contrast formula | wcag-contrast library | WCAG 2.1 (2018) | Correct sRGB linearization handling |
|
|
| SVG to PNG via sharp(svgBuffer) | @resvg/resvg-js (Rust) | librsvg dependency unreliable across platforms | Self-contained; consistent output quality |
|
|
|
|
**Deprecated/outdated:**
|
|
- `mermaid` in jsdom: mermaid 8/9 could work with jsdom+dagre, but mermaid 10+ requires real browser SVG APIs. Do not attempt this path.
|
|
- `HSL` as intermediate for OKLCH math: Perceptually non-uniform; explicitly forbidden in STATE.md.
|
|
- Storing multiple output files as separate `content_jobs` records: Schema has one `resultAssetId` per job; multi-file output requires a bundle approach.
|
|
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
1. **playwright-core vs @playwright/test for server-side browser launch**
|
|
- What we know: `@playwright/test` is a root devDependency; server has no playwright dep; Chromium binary is at `~/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome`; `playwright-core` is the correct server dep (no bundled browsers)
|
|
- What's unclear: Nexus is a local desktop app, so devDeps are always available during `pnpm dev`. Should `playwright-core` be added to `server/package.json` `dependencies` or is the monorepo context sufficient?
|
|
- Recommendation: Add `playwright-core@1.58.2` to `server/package.json` `dependencies` for explicitness and correctness. Use `executablePath` from env var `PLAYWRIGHT_BROWSERS_PATH` or the known cache location.
|
|
|
|
2. **Icon LLM prompt quality vs style coherence**
|
|
- What we know: LLMs can generate SVG paths; consistency across a set (ICON-02: "cohesive visual style") requires a strong system prompt with explicit constraints
|
|
- What's unclear: Which local model is active via Ollama? SVG generation quality varies significantly by model size and training.
|
|
- Recommendation: Use a structured system prompt with explicit rules (24x24 viewBox, stroke-width=1.5, currentColor fill, no text elements); include existing icon names from lucide-react as style reference; validate output passes SVGO before storing; surface model quality issues to user via the generic error message.
|
|
|
|
3. **Theme "Apply to Nexus" — in-memory vs server persistence**
|
|
- What we know: `ThemeContext` reads from localStorage; `nexus-settings.json` persists on server. The UI-SPEC toast says "Reload to see full effect."
|
|
- Recommendation: Do both — write to server settings (PATCH /nexus/settings with customTheme), AND update ThemeContext in-memory immediately so the change is visible without reload. Toast message "Reload to see full effect" covers components that don't observe ThemeContext.
|
|
|
|
---
|
|
|
|
## Environment Availability
|
|
|
|
| Dependency | Required By | Available | Version | Fallback |
|
|
|------------|------------|-----------|---------|----------|
|
|
| Node.js | All | ✓ | v20.20.2 | — |
|
|
| Playwright Chromium binary | Mermaid server rendering | ✓ | Chromium 1217 (playwright 1.58.2) at ~/.cache/ms-playwright | Cannot render server-side Mermaid; fall back to client-only |
|
|
| sharp | SVG to PNG (fallback path) | ✓ | 0.34.5 in server node_modules | @resvg/resvg-js is primary |
|
|
| @resvg/resvg-js | SVG to PNG (primary) | ✗ not installed yet | 2.6.2 on npm; linux-x64-gnu confirmed | sharp(svgBuffer, {density:144}) as fallback |
|
|
| culori | OKLCH palette engine | ✗ not installed yet | 4.0.2 on npm | No fallback — mandated by STATE.md |
|
|
| wcag-contrast | WCAG AA validation | ✗ not installed yet | 3.0.0 on npm | Manual formula (fragile; not recommended) |
|
|
| svgo | LLM SVG cleanup | ✗ not installed yet | 4.0.1 on npm | Deliver unoptimized SVG (larger, possible LLM artifacts) |
|
|
| playwright-core | Server headless Chromium | ✗ not in server package.json | 1.58.2 on npm (must match installed Chromium) | Cannot render Mermaid server-side |
|
|
| dompurify | Mermaid SVG sanitization | ✓ | 3.3.2 in server node_modules | — |
|
|
| jsdom | DOMPurify DOM provider | ✓ | 28.1.0 in server node_modules | — |
|
|
|
|
**Missing dependencies with no fallback:**
|
|
- `culori` — OKLCH theme engine is blocked without it; mandated by STATE.md
|
|
- `playwright-core` — Mermaid server-side rendering requires a headless browser
|
|
|
|
**Missing dependencies with fallback:**
|
|
- `@resvg/resvg-js` — `sharp` is available as fallback for SVG to PNG (established org-chart pattern)
|
|
- `svgo` — icons can be delivered unoptimized as fallback (not ideal but functional)
|
|
- `wcag-contrast` — can be computed manually as fallback (fragile)
|
|
|
|
---
|
|
|
|
## Validation Architecture
|
|
|
|
### Test Framework
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Framework | vitest 3.x (monorepo root config at `/opt/nexus/vitest.config.ts`; server and UI included) |
|
|
| Config file | `/opt/nexus/vitest.config.ts` |
|
|
| Quick run command | `pnpm --filter server exec vitest run` |
|
|
| Full suite command | `pnpm test:run` (from `/opt/nexus`) |
|
|
|
|
### Phase Requirements to Test Map
|
|
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|
|--------|----------|-----------|-------------------|-------------|
|
|
| DIAG-01 | POST /content-jobs jobType=diagram returns 202 | integration | `pnpm --filter server exec vitest run src/__tests__/diagram-renderer.test.ts` | ❌ Wave 0 |
|
|
| DIAG-02 | renderDiagram() returns bundle with svgBase64 + pngBase64 | unit | same file | ❌ Wave 0 |
|
|
| DIAG-03 | DiagramSourcePanel renders editable textarea | unit (renderToStaticMarkup) | `pnpm --filter ui exec vitest run src/components/DiagramSourcePanel.test.tsx` | ❌ Wave 0 |
|
|
| DIAG-04 | stripUnsafeDirectives removes %%{init}%% blocks | unit | `pnpm --filter server exec vitest run src/__tests__/diagram-renderer.test.ts` | ❌ Wave 0 |
|
|
| DIAG-05 | stripped flag set when click or init directives found | unit | same file | ❌ Wave 0 |
|
|
| ICON-01 | POST /content-jobs jobType=icon-set returns 202 | integration | `pnpm --filter server exec vitest run src/__tests__/content-jobs-routes.test.ts` | ✅ (existing test extended) |
|
|
| ICON-02 | renderIconSet() output bundle contains N icons with valid SVG | unit | `pnpm --filter server exec vitest run src/__tests__/icon-renderer.test.ts` | ❌ Wave 0 |
|
|
| ICON-03 | icon bundle JSON has svgBase64 entries for each icon | unit | same file | ❌ Wave 0 |
|
|
| THEME-01 | buildPalette() returns 7 roles from seed hex | unit | `pnpm --filter server exec vitest run src/__tests__/theme-renderer.test.ts` | ❌ Wave 0 |
|
|
| THEME-02 | All palette roles have oklch string values | unit | same file | ❌ Wave 0 |
|
|
| THEME-03 | wcagAA field is computed per swatch pair | unit | same file | ❌ Wave 0 |
|
|
| THEME-04 | ThemePreviewPanel injects CSS only to .nexus-theme-preview | unit (DOM) | `pnpm --filter ui exec vitest run src/components/ThemePreviewPanel.test.tsx` | ❌ Wave 0 |
|
|
| THEME-05 | exportToCss / exportToTailwind / exportToVSCode / exportToJson return strings | unit | same as THEME-01 | ❌ Wave 0 |
|
|
| THEME-06 | buildPalette() returns dark and light variants for each role | unit | same as THEME-01 | ❌ Wave 0 |
|
|
| THEME-07 | PATCH /nexus/settings with customTheme persists to nexusSettingsService | integration | `pnpm --filter server exec vitest run src/__tests__/nexus-settings-custom-theme.test.ts` | ❌ Wave 0 |
|
|
|
|
### Sampling Rate
|
|
|
|
- **Per task commit:** `pnpm --filter server exec vitest run src/__tests__/<relevant>.test.ts`
|
|
- **Per wave merge:** `pnpm test:run --filter server && pnpm test:run --filter ui`
|
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
|
|
|
### Wave 0 Gaps
|
|
|
|
- [ ] `server/src/__tests__/diagram-renderer.test.ts` — covers DIAG-01, DIAG-02, DIAG-04, DIAG-05 (mock `playwright-core` chromium launch; test stripUnsafeDirectives and bundle structure)
|
|
- [ ] `server/src/__tests__/icon-renderer.test.ts` — covers ICON-02, ICON-03 (mock LLM call; validate SVG bundle structure with SVGO output)
|
|
- [ ] `server/src/__tests__/theme-renderer.test.ts` — covers THEME-01, THEME-02, THEME-03, THEME-05, THEME-06 (pure function tests; no mocking needed; culori and wcag-contrast are real)
|
|
- [ ] `server/src/__tests__/nexus-settings-custom-theme.test.ts` — covers THEME-07 (temp settings file; extend existing settings tests)
|
|
- [ ] `ui/src/components/DiagramSourcePanel.test.tsx` — covers DIAG-03
|
|
- [ ] `ui/src/components/ThemePreviewPanel.test.tsx` — covers THEME-04 (check that CSS variable set calls are scoped to container ref)
|
|
|
|
---
|
|
|
|
## Project Constraints (from CLAUDE.md and STATE.md)
|
|
|
|
No `CLAUDE.md` exists at `/opt/nexus/CLAUDE.md`. Constraints are sourced from `STATE.md` Key Decisions and the `design-guide` skill:
|
|
|
|
| Constraint | Source | Impact on Phase 41 |
|
|
|------------|--------|-------------------|
|
|
| OKLCH via culori — HSL is forbidden as an intermediate | STATE.md | Theme engine uses culori throughout; no HSL conversions anywhere in palette pipeline |
|
|
| Mermaid securityLevel must be "strict" — strip %%{init}%% and click directives before render, DOMPurify on SVG output | STATE.md | stripUnsafeDirectives + DOMPurify both mandatory for DIAG-05 |
|
|
| Async job pattern mandatory — all render requests return 202 + job ID | STATE.md | All three generators go through /content-jobs; no synchronous render routes |
|
|
| MAX_GENERATED_ASSET_BYTES constant — generated namespace, not upload namespace | STATE.md | All diagram/icon/theme assets stored in `generated` namespace |
|
|
| renderContent is a stub in Phase 40 — phases 41-45 add real renderers keyed by jobType | STATE.md | Extend the switch in content-job-runner.ts |
|
|
| shadcn new-york style, neutral base, cssVariables, lucide icons | design-guide SKILL.md | All new UI components follow this preset |
|
|
| Use semantic CSS variable tokens; never raw hex/rgb values | design-guide SKILL.md | ThemePreviewPanel CSS injection uses --background, --card, etc. token names |
|
|
| New reusable components must be added to /design-guide page | design-guide SKILL.md | DiagramPreview, ThemePaletteGrid, IconResultGrid etc. need design-guide entries |
|
|
| sourceTaskId is required on every generated asset | STATE.md | All content-job-runner asset creations pass sourceTaskId from the job |
|
|
| content_jobs uses no FK for resultAssetId | STATE.md | Schema unchanged; single UUID per job; multi-file output requires bundle asset pattern |
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
|
|
- Codebase: `server/src/services/content-job-runner.ts` — renderContent stub confirmed; dispatch pattern confirmed
|
|
- Codebase: `server/src/routes/org-chart-svg.ts` — established SVG to PNG via `sharp(svgBuffer, {density:144})` pattern
|
|
- Codebase: `server/src/services/nexus-settings.ts` — theme persistence approach confirmed (Zod schema + JSON file)
|
|
- Codebase: `ui/src/components/MarkdownBody.tsx` — existing browser Mermaid rendering with `securityLevel: "strict"` confirmed
|
|
- Codebase: `ui/src/context/ThemeContext.tsx` — theme injection via `document.documentElement.style` confirmed
|
|
- Codebase: `server/src/routes/content-jobs.ts` — SSE pattern confirmed (EventSource subscription)
|
|
- Filesystem: `~/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome` — Playwright Chromium binary confirmed present
|
|
- npm registry: `@resvg/resvg-js@2.6.2` — linux-x64-gnu confirmed available
|
|
- npm registry: `culori@4.0.2` — OKLCH exports and CJS bundle confirmed
|
|
- npm registry: `wcag-contrast@3.0.0` — confirmed
|
|
- npm registry: `svgo@4.0.1` — confirmed
|
|
- `.planning/STATE.md` Key Decisions — OKLCH/culori mandate, Mermaid securityLevel, async job pattern, multi-asset approach
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
|
|
- culori package exports inspection — CJS bundle at `./bundled/culori.cjs`; ESM at `./src/index.js`; both confirmed via `npm info`
|
|
- `@resvg/resvg-js` optionalDependencies — linux-x64-gnu at 2.6.2 confirmed
|
|
- server node_modules inspection — dompurify 3.3.2, jsdom 28.1.0, sharp 0.34.5 confirmed in server deps via `pnpm list`
|
|
|
|
### Tertiary (LOW confidence)
|
|
|
|
- Assessment that Mermaid 11 requires a real browser DOM (no jsdom) — based on known mermaid breaking changes in v10+; jsdom-based mermaid solutions are absent from recent ecosystem; this is HIGH-confidence but not directly tested against mermaid 11.14 here. Validate during diagram-renderer implementation.
|
|
|
|
---
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH — all package versions confirmed via npm info; all existing deps confirmed via pnpm list; Playwright Chromium binary confirmed on filesystem
|
|
- Architecture patterns: HIGH — based directly on Phase 40 established patterns (content-job-runner stub, org-chart-svg.ts sharp pattern, existing SSE test structure)
|
|
- Pitfalls: HIGH (Playwright path, culori ESM, browser cleanup) / MEDIUM (LLM SVG quality — model-dependent)
|
|
|
|
**Research date:** 2026-04-04
|
|
**Valid until:** 2026-05-04 (stable libraries; mermaid 11.x API is stable; culori 4.x API is stable)
|