vplan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/index.ts +140 -0
- package/dist/index.js +474 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/runtime/Layout.tsx +17 -0
- package/runtime/components/Callout.tsx +38 -0
- package/runtime/components/Chart.tsx +112 -0
- package/runtime/components/Checklist.tsx +34 -0
- package/runtime/components/Compare.tsx +51 -0
- package/runtime/components/ExpandButton.tsx +20 -0
- package/runtime/components/FileTree.tsx +101 -0
- package/runtime/components/Mermaid.tsx +52 -0
- package/runtime/components/Phase.tsx +34 -0
- package/runtime/components/Questions.tsx +30 -0
- package/runtime/components/validate.ts +21 -0
- package/runtime/css.d.ts +1 -0
- package/runtime/fullscreen.ts +218 -0
- package/runtime/index.html +12 -0
- package/runtime/index.tsx +42 -0
- package/runtime/main.tsx +4 -0
- package/runtime/theme.css +789 -0
- package/runtime/virtual-plan.d.ts +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../package.json","../src/commands/check.ts","../src/build/check.ts","../../core/src/index.ts","../src/commands/components.ts","../src/commands/render.ts","../src/build/compile.ts","../src/build/remark-mermaid.ts"],"sourcesContent":["import { Command } from 'commander'\nimport packageJson from '../package.json' with { type: 'json' }\nimport { runCheck } from './commands/check.js'\nimport { runComponents } from './commands/components.js'\nimport { runRender, type RenderOptions } from './commands/render.js'\n\nconst program = new Command('vplan')\n .description('Render Claude plans as visual MDX pages instead of walls of text')\n .version(packageJson.version)\n\nprogram\n .command('render', { isDefault: true })\n .description('Compile a plan .mdx to a self-contained HTML page (default command)')\n .argument('<file>', 'the plan .mdx file to render')\n .option('--watch', 'start a hot-reloading dev server instead of writing a file')\n .option('--out <path>', 'output HTML path (defaults to <file>.plan.html)')\n .option('--no-open', 'do not open the result in a browser')\n .action((file: string, options: RenderOptions) => runRender(file, options))\n\nprogram\n .command('check')\n .description('Validate a plan .mdx (compile + component checks) without rendering')\n .argument('<file>', 'the plan .mdx file to validate')\n .action((file: string) => runCheck(file))\n\nprogram\n .command('components')\n .description('Print the available plan components and their props')\n .action(() => runComponents())\n\nprogram.parseAsync(process.argv).catch((error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n})\n","{\n \"name\": \"vplan\",\n \"version\": \"0.1.0\",\n \"description\": \"Render Claude's plans as visual MDX pages instead of walls of text\",\n \"author\": \"Brandon Burrus <brandon@burrus.io>\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=20.0.0\"\n },\n \"bin\": {\n \"vplan\": \"./dist/index.js\"\n },\n \"files\": [\n \"dist\",\n \"runtime\",\n \"core\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsx src/index.ts\",\n \"typecheck\": \"tsc --noEmit\",\n \"vendor\": \"node scripts/vendor.mjs\",\n \"prepack\": \"node scripts/vendor.mjs && tsup\"\n },\n \"dependencies\": {\n \"@mdx-js/mdx\": \"^3.1.1\",\n \"@mdx-js/react\": \"^3.1.1\",\n \"@mdx-js/rollup\": \"^3.1.1\",\n \"@tabler/icons-react\": \"^3.44.0\",\n \"beautiful-mermaid\": \"^1.1.3\",\n \"commander\": \"^15.0.0\",\n \"open\": \"^11.0.0\",\n \"react\": \"^19.2.7\",\n \"react-dom\": \"^19.2.7\",\n \"recharts\": \"^3.8.1\",\n \"rehype-expressive-code\": \"^0.43.1\",\n \"remark-frontmatter\": \"^5.0.0\",\n \"remark-gfm\": \"^4.0.1\",\n \"remark-mdx\": \"^3.1.1\",\n \"remark-mdx-frontmatter\": \"^5.2.0\",\n \"remark-parse\": \"^11.0.0\",\n \"unified\": \"^11.0.5\",\n \"unist-util-visit\": \"^5.1.0\",\n \"vite\": \"^8.0.16\",\n \"vite-plugin-singlefile\": \"^2.3.3\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@visualplan/core\": \"workspace:*\",\n \"@visualplan/runtime\": \"workspace:*\"\n }\n}\n","import { resolve } from 'node:path'\nimport { type CheckIssue, checkPlan } from '../build/check.js'\n\n/** Print check issues in an editor-clickable `file:line:column message` format. */\nexport function printIssues(file: string, issues: CheckIssue[]): void {\n for (const issue of issues) {\n process.stderr.write(`${file}:${issue.line}:${issue.column} ${issue.message}\\n`)\n }\n}\n\n/** `vplan check <file>` — validate a plan's MDX without rendering it. */\nexport async function runCheck(file: string): Promise<void> {\n const issues = await checkPlan(resolve(file))\n if (issues.length === 0) {\n process.stdout.write(`${file} is valid\\n`)\n return\n }\n printIssues(file, issues)\n process.stdout.write(`\\n${issues.length} issue(s) found\\n`)\n process.exitCode = 1\n}\n","import { readFile } from 'node:fs/promises'\nimport { compile } from '@mdx-js/mdx'\nimport remarkFrontmatter from 'remark-frontmatter'\nimport remarkGfm from 'remark-gfm'\nimport remarkMdxFrontmatter from 'remark-mdx-frontmatter'\nimport remarkMdx from 'remark-mdx'\nimport remarkParse from 'remark-parse'\nimport { unified } from 'unified'\nimport { visit } from 'unist-util-visit'\nimport { CATALOG } from '@visualplan/core'\n\nexport interface CheckIssue {\n line: number\n column: number\n message: string\n}\n\ninterface JsxAttribute {\n type: string\n name?: string\n value?: unknown\n}\n\ninterface JsxNode {\n type: string\n name?: string | null\n attributes?: JsxAttribute[]\n position?: { start: { line: number; column: number } }\n}\n\nconst COMPONENT_NAMES = CATALOG.filter(entry => /^[A-Z][A-Za-z0-9]*$/.test(entry.name)).map(\n entry => entry.name,\n)\n\nconst ENUMS_BY_COMPONENT = new Map(CATALOG.map(entry => [entry.name, entry.staticEnums]))\n\n/** Validate a plan's MDX: real compile errors plus static enum / unknown-component checks. */\nexport async function checkPlan(mdxPath: string): Promise<CheckIssue[]> {\n const source = await readFile(mdxPath, 'utf8')\n const issues: CheckIssue[] = []\n\n try {\n await compile(source, {\n remarkPlugins: [\n remarkFrontmatter,\n [remarkMdxFrontmatter, { name: 'frontmatter' }],\n remarkGfm,\n ],\n })\n } catch (error) {\n const vfileError = error as {\n line?: number\n column?: number\n reason?: string\n message?: string\n }\n return [\n {\n line: vfileError.line ?? 1,\n column: vfileError.column ?? 1,\n message: vfileError.reason ?? vfileError.message ?? 'MDX failed to compile',\n },\n ]\n }\n\n const tree = unified().use(remarkParse).use(remarkFrontmatter).use(remarkMdx).parse(source)\n\n visit(tree, node => {\n const element = node as unknown as JsxNode\n if (element.type !== 'mdxJsxFlowElement' && element.type !== 'mdxJsxTextElement') return\n const name = element.name\n if (!name) return\n const at = element.position?.start ?? { line: 1, column: 1 }\n\n if (!COMPONENT_NAMES.includes(name)) {\n issues.push({\n line: at.line,\n column: at.column,\n message: `Unknown component <${name}>. Valid components: ${COMPONENT_NAMES.join(', ')}.`,\n })\n return\n }\n\n const enums = ENUMS_BY_COMPONENT.get(name) ?? {}\n for (const [prop, allowed] of Object.entries(enums)) {\n const attribute = element.attributes?.find(\n candidate => candidate.type === 'mdxJsxAttribute' && candidate.name === prop,\n )\n if (attribute && typeof attribute.value === 'string' && !allowed.includes(attribute.value)) {\n issues.push({\n line: at.line,\n column: at.column,\n message: `<${name}> prop ${prop}=\"${attribute.value}\" is invalid. Valid: ${allowed.join(', ')}.`,\n })\n }\n }\n })\n\n return issues\n}\n","import { z } from 'zod'\n\n/**\n * Single source of truth for the VisualPlan component vocabulary.\n *\n * This module is imported by BOTH the browser runtime (for render-time zod\n * validation) and the Node CLI (for static `check` and the `components`\n * catalog printer), so it must stay free of any React, recharts, or mermaid\n * imports. Keep it isomorphic.\n */\n\nexport const STATUS_VALUES = ['planned', 'active', 'done'] as const\nexport const CHANGE_VALUES = ['add', 'modify', 'delete', 'move'] as const\nexport const CHART_TYPE_VALUES = ['bar', 'line', 'pie'] as const\nexport const CALLOUT_TYPE_VALUES = ['note', 'risk', 'decision', 'warn'] as const\n\nexport const phaseSchema = z.object({\n title: z.string().min(1, 'title is required'),\n status: z.enum(STATUS_VALUES).default('planned'),\n})\n\nexport const fileTreeSchema = z.object({\n files: z\n .array(\n z.object({\n path: z.string().min(1, 'each file needs a path'),\n change: z.enum(CHANGE_VALUES),\n }),\n )\n .min(1, 'files must list at least one entry'),\n})\n\nexport const chartSchema = z.object({\n type: z.enum(CHART_TYPE_VALUES),\n title: z.string().optional(),\n data: z\n .array(z.object({ label: z.string(), value: z.number() }))\n .min(1, 'data must have at least one point'),\n})\n\nexport const compareSchema = z.object({\n options: z\n .array(\n z.object({\n name: z.string().min(1, 'each option needs a name'),\n pros: z.array(z.string()).default([]),\n cons: z.array(z.string()).default([]),\n pick: z.boolean().optional(),\n }),\n )\n .min(2, 'compare needs at least two options'),\n})\n\nexport const calloutSchema = z.object({\n type: z.enum(CALLOUT_TYPE_VALUES).default('note'),\n})\n\nexport const questionsSchema = z.object({\n items: z.array(z.string().min(1)).min(1, 'questions needs at least one item'),\n})\n\nexport const checklistSchema = z.object({\n title: z.string().optional(),\n items: z\n .array(\n z.object({\n text: z.string().min(1, 'each item needs text'),\n done: z.boolean().default(false),\n }),\n )\n .min(1, 'checklist needs at least one item'),\n})\n\n/** Describes a component for the `components` printer and the static checker. */\nexport interface CatalogEntry {\n name: string\n summary: string\n /** Props the CLI can statically validate from MDX source (string-literal enums). */\n staticEnums: Record<string, readonly string[]>\n example: string\n}\n\nexport const CATALOG: readonly CatalogEntry[] = [\n {\n name: 'Phase',\n summary: 'A collapsible plan stage with a status badge. Wraps markdown.',\n staticEnums: { status: STATUS_VALUES },\n example: '<Phase title=\"Build the API\" status=\"active\">\\n 1. Define routes\\n</Phase>',\n },\n {\n name: 'FileTree',\n summary:\n 'A nested directory tree of file changes, built from the paths, with add/modify/delete/move markers.',\n staticEnums: {},\n example:\n '<FileTree files={[{ path: \"src/api/routes.ts\", change: \"add\" }, { path: \"src/api/db.ts\", change: \"modify\" }, { path: \"src/legacy.ts\", change: \"delete\" }]} />',\n },\n {\n name: 'Chart',\n summary: 'A bar/line/pie chart for estimates or metrics.',\n staticEnums: { type: CHART_TYPE_VALUES },\n example:\n '<Chart type=\"bar\" title=\"Effort (days)\" data={[{ label: \"API\", value: 3 }, { label: \"UI\", value: 2 }]} />',\n },\n {\n name: 'Compare',\n summary: 'Side-by-side option cards for weighing approaches.',\n staticEnums: {},\n example:\n '<Compare options={[{ name: \"Postgres\", pros: [\"ACID\"], cons: [\"ops\"], pick: true }, { name: \"SQLite\", pros: [\"simple\"], cons: [\"scale\"] }]} />',\n },\n {\n name: 'Callout',\n summary: 'A highlighted note/risk/decision/warning block. Wraps markdown.',\n staticEnums: { type: CALLOUT_TYPE_VALUES },\n example: '<Callout type=\"risk\">\\n Migration locks the table for ~2s.\\n</Callout>',\n },\n {\n name: 'Questions',\n summary:\n 'Open questions you (Claude) want the reader to weigh in on before building, as a highlighted panel.',\n staticEnums: {},\n example:\n '<Questions items={[\"Should refresh tokens rotate on every use?\", \"Is a 15-minute access-token TTL acceptable?\"]} />',\n },\n {\n name: 'Checklist',\n summary: 'Acceptance criteria / definition of done, with done and todo states.',\n staticEnums: {},\n example:\n '<Checklist title=\"Done when\" items={[{ text: \"Returns 429 over the limit\", done: true }, { text: \"Dashboards live\" }]} />',\n },\n {\n name: 'mermaid (code fence)',\n summary:\n 'A flowchart, sequence, state, class, ER, or XY-chart diagram. Write a ```mermaid fenced block. (gantt/pie are not supported by the renderer.)',\n staticEnums: {},\n example: '```mermaid\\nflowchart LR\\n A[Client] --> B[API] --> C[(DB)]\\n```',\n },\n]\n","import { CATALOG } from '@visualplan/core'\n\n/** `vplan components` — print the component vocabulary cheat-sheet. */\nexport function runComponents(): void {\n const lines: string[] = [\n 'VisualPlan components — use these directly in a plan .mdx (no imports):',\n '',\n ]\n for (const entry of CATALOG) {\n lines.push(`${entry.name}`)\n lines.push(` ${entry.summary}`)\n const enums = Object.entries(entry.staticEnums)\n for (const [prop, values] of enums) {\n lines.push(` ${prop}: ${values.join(' | ')}`)\n }\n lines.push(' example:')\n for (const exampleLine of entry.example.split('\\n')) {\n lines.push(` ${exampleLine}`)\n }\n lines.push('')\n }\n lines.push('Start the plan with a `# Title` heading. Do not use YAML frontmatter.')\n process.stdout.write(`${lines.join('\\n')}\\n`)\n}\n","import { basename, dirname, extname, join, resolve } from 'node:path'\nimport open from 'open'\nimport { checkPlan } from '../build/check.js'\nimport { renderToFile, startDevServer } from '../build/compile.js'\nimport { printIssues } from './check.js'\n\nexport interface RenderOptions {\n watch?: boolean\n out?: string\n open?: boolean\n}\n\nfunction defaultOutPath(absMdx: string): string {\n const stem = basename(absMdx, extname(absMdx))\n return join(dirname(absMdx), `${stem}.plan.html`)\n}\n\n/** `vplan render <file>` — validate, then build a static page or start a watch server. */\nexport async function runRender(file: string, options: RenderOptions): Promise<void> {\n const absMdx = resolve(file)\n\n const issues = await checkPlan(absMdx)\n if (issues.length > 0) {\n printIssues(file, issues)\n process.exitCode = 1\n return\n }\n\n if (options.watch) {\n const server = await startDevServer(absMdx)\n process.stdout.write(\n `VisualPlan watching ${file}\\n ${server.url}\\n (edit the file to hot-reload; Ctrl+C to stop)\\n`,\n )\n if (options.open !== false) await open(server.url)\n return\n }\n\n const out = options.out ? resolve(options.out) : defaultOutPath(absMdx)\n await renderToFile(absMdx, out)\n process.stdout.write(`Rendered ${out}\\n`)\n if (options.open !== false) await open(out)\n}\n","import { cp, mkdtemp, rm } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { tmpdir } from 'node:os'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport mdx from '@mdx-js/rollup'\nimport rehypeExpressiveCode, { type RehypeExpressiveCodeOptions } from 'rehype-expressive-code'\nimport remarkFrontmatter from 'remark-frontmatter'\nimport remarkGfm from 'remark-gfm'\nimport remarkMdxFrontmatter from 'remark-mdx-frontmatter'\nimport { build, createServer, type InlineConfig, type Plugin } from 'vite'\nimport { viteSingleFile } from 'vite-plugin-singlefile'\nimport { remarkMermaid } from './remark-mermaid.js'\n\nconst expressiveCodeOptions: RehypeExpressiveCodeOptions = {\n themes: ['github-dark', 'github-light'],\n useDarkModeMediaQuery: true,\n // The copy-button script does not execute reliably in our client-rendered SPA;\n // frames (titles) are CSS-only, so keep those and drop the interactive button.\n frames: { showCopyToClipboardButton: false },\n // Match the flat ink design: our borders/radius/surfaces/fonts, no shadow, no\n // colored tab accent. Values are CSS vars so the frame chrome tracks light/dark too.\n styleOverrides: {\n borderRadius: '10px',\n borderColor: 'var(--vp-border)',\n codeBackground: 'var(--vp-surface)',\n codeFontFamily: 'var(--vp-mono)',\n codeFontSize: '0.8rem',\n codeLineHeight: '1.6',\n codePaddingBlock: '0.9rem',\n codePaddingInline: '1rem',\n uiFontFamily: 'var(--vp-font)',\n uiFontSize: '0.78rem',\n frames: {\n frameBoxShadowCssValue: 'none',\n // A flat filename header on the same surface as the code, separated by one\n // border. No editor-tab metaphor, no colored indicator line.\n editorBackground: 'var(--vp-surface)',\n editorTabBarBackground: 'var(--vp-surface)',\n editorTabBarBorderBottomColor: 'var(--vp-border)',\n editorActiveTabBackground: 'var(--vp-surface)',\n editorActiveTabForeground: 'var(--vp-muted)',\n editorActiveTabBorderColor: 'transparent',\n editorActiveTabIndicatorTopColor: 'transparent',\n editorActiveTabIndicatorBottomColor: 'transparent',\n editorTabsMarginInlineStart: '0',\n terminalBackground: 'var(--vp-surface)',\n terminalTitlebarBackground: 'var(--vp-surface)',\n terminalTitlebarForeground: 'var(--vp-muted)',\n terminalTitlebarBorderBottomColor: 'var(--vp-border)',\n },\n },\n}\n\nconst require = createRequire(import.meta.url)\n\ninterface RuntimePaths {\n /** Directory Vite roots at: holds index.html plus the runtime source. */\n runtimeDir: string\n /** Core catalog source, aliased to `@visualplan/core` so the runtime import resolves. */\n coreEntry: string\n}\n\n/**\n * Locate the runtime source and the core catalog in both layouts:\n * - Published: both are vendored next to dist/ (`<pkg>/runtime`, `<pkg>/core`).\n * - Dev (workspace): they are sibling packages, resolved via node module resolution.\n * The core path is aliased to `@visualplan/core` in the Vite build, so the runtime's\n * import resolves identically whether the CLI is installed or run from the monorepo.\n */\nfunction findRuntimePaths(): RuntimePaths {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (let depth = 0; depth < 6; depth++) {\n const runtimeDir = join(dir, 'runtime')\n const coreEntry = join(dir, 'core', 'index.ts')\n // Require BOTH vendored siblings: the monorepo also has a `runtime/index.html`\n // (under packages/), but its core lives at core/src/index.ts, so only the true\n // vendored layout has core/index.ts next to runtime/.\n if (existsSync(join(runtimeDir, 'index.html')) && existsSync(coreEntry)) {\n return { runtimeDir, coreEntry }\n }\n dir = dirname(dir)\n }\n const runtimeDir = dirname(require.resolve('@visualplan/runtime/package.json'))\n const coreDir = dirname(require.resolve('@visualplan/core/package.json'))\n return { runtimeDir, coreEntry: join(coreDir, 'src', 'index.ts') }\n}\n\nfunction mdxPlugin(): Plugin {\n return {\n enforce: 'pre',\n ...mdx({\n providerImportSource: '@mdx-js/react',\n remarkPlugins: [\n remarkFrontmatter,\n [remarkMdxFrontmatter, { name: 'frontmatter' }],\n remarkGfm,\n // Must run before rehype-expressive-code so mermaid never reaches the highlighter.\n remarkMermaid,\n ],\n rehypePlugins: [[rehypeExpressiveCode, expressiveCodeOptions]],\n }),\n }\n}\n\nfunction baseConfig(paths: RuntimePaths, mdxPath: string): InlineConfig {\n return {\n root: paths.runtimeDir,\n configFile: false,\n logLevel: 'silent',\n resolve: { alias: { 'virtual:plan': mdxPath, '@visualplan/core': paths.coreEntry } },\n esbuild: { jsx: 'automatic', jsxImportSource: 'react' },\n plugins: [mdxPlugin()],\n // The runtime, core, and the user's plan span sibling dirs (and a hoisted\n // node_modules) in the monorepo, so the dev server cannot use a single allow\n // root. This is a local tool rendering the user's own file, so fs is unrestricted.\n server: { fs: { strict: false }, open: false },\n }\n}\n\n/** Compile an MDX plan to a single self-contained HTML file at `outPath`. */\nexport async function renderToFile(mdxPath: string, outPath: string): Promise<void> {\n const paths = findRuntimePaths()\n const absMdx = resolve(mdxPath)\n const outDir = await mkdtemp(join(tmpdir(), 'visualplan-build-'))\n try {\n const config = baseConfig(paths, absMdx)\n await build({\n ...config,\n plugins: [...(config.plugins ?? []), viteSingleFile()],\n build: {\n outDir,\n emptyOutDir: true,\n rollupOptions: { input: join(paths.runtimeDir, 'index.html') },\n },\n })\n await cp(join(outDir, 'index.html'), resolve(outPath))\n } finally {\n await rm(outDir, { recursive: true, force: true })\n }\n}\n\nexport interface DevServer {\n url: string\n close: () => Promise<void>\n}\n\n/** Start a hot-reloading dev server for an MDX plan and return its local URL. */\nexport async function startDevServer(mdxPath: string): Promise<DevServer> {\n const paths = findRuntimePaths()\n const absMdx = resolve(mdxPath)\n const server = await createServer(baseConfig(paths, absMdx))\n await server.listen()\n const url =\n server.resolvedUrls?.local[0] ?? `http://localhost:${server.config.server.port ?? 5173}`\n return {\n url,\n close: () => server.close(),\n }\n}\n","import { visit } from 'unist-util-visit'\n\ninterface MdastCode {\n type: 'code'\n lang?: string | null\n value: string\n}\n\ninterface MdastParent {\n children: unknown[]\n}\n\n/**\n * Convert ```mermaid fenced code blocks into `<Mermaid chart=\"...\" />` MDX JSX\n * elements. This runs in the remark (mdast) stage, BEFORE rehype-expressive-code,\n * so the code highlighter never sees mermaid blocks and the diagram renders via the\n * Mermaid component instead.\n */\nexport function remarkMermaid() {\n return (tree: unknown) => {\n visit(\n tree as never,\n 'code',\n (node: MdastCode, index: number | undefined, parent: MdastParent | undefined) => {\n if (node.lang !== 'mermaid' || !parent || index === undefined) return\n parent.children[index] = {\n type: 'mdxJsxFlowElement',\n name: 'Mermaid',\n attributes: [{ type: 'mdxJsxAttribute', name: 'chart', value: node.value }],\n children: [],\n }\n },\n )\n }\n}\n"],"mappings":"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","names":["dirname","join","resolve","resolve","remarkFrontmatter","remarkGfm","remarkMdxFrontmatter","visit","require","runtimeDir","remarkFrontmatter","remarkMdxFrontmatter","remarkGfm","resolve","join","dirname","resolve"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vplan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Render Claude's plans as visual MDX pages instead of walls of text",
|
|
5
|
+
"author": "Brandon Burrus <brandon@burrus.io>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.0.0"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"vplan": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"runtime",
|
|
17
|
+
"core"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@mdx-js/mdx": "^3.1.1",
|
|
21
|
+
"@mdx-js/react": "^3.1.1",
|
|
22
|
+
"@mdx-js/rollup": "^3.1.1",
|
|
23
|
+
"@tabler/icons-react": "^3.44.0",
|
|
24
|
+
"beautiful-mermaid": "^1.1.3",
|
|
25
|
+
"commander": "^15.0.0",
|
|
26
|
+
"open": "^11.0.0",
|
|
27
|
+
"react": "^19.2.7",
|
|
28
|
+
"react-dom": "^19.2.7",
|
|
29
|
+
"recharts": "^3.8.1",
|
|
30
|
+
"rehype-expressive-code": "^0.43.1",
|
|
31
|
+
"remark-frontmatter": "^5.0.0",
|
|
32
|
+
"remark-gfm": "^4.0.1",
|
|
33
|
+
"remark-mdx": "^3.1.1",
|
|
34
|
+
"remark-mdx-frontmatter": "^5.2.0",
|
|
35
|
+
"remark-parse": "^11.0.0",
|
|
36
|
+
"unified": "^11.0.5",
|
|
37
|
+
"unist-util-visit": "^5.1.0",
|
|
38
|
+
"vite": "^8.0.16",
|
|
39
|
+
"vite-plugin-singlefile": "^2.3.3",
|
|
40
|
+
"zod": "^4.4.3"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@visualplan/core": "0.1.0",
|
|
44
|
+
"@visualplan/runtime": "0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"dev": "tsx src/index.ts",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"vendor": "node scripts/vendor.mjs"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode, useEffect } from 'react'
|
|
2
|
+
import { initFullscreenControls } from './fullscreen.js'
|
|
3
|
+
|
|
4
|
+
/** Page shell: a single centered content column. The plan supplies its own
|
|
5
|
+
* `# Title` heading; there is no frontmatter-driven header or sidebar. */
|
|
6
|
+
export function Layout({ children }: { children: ReactNode }) {
|
|
7
|
+
// Fullscreen (diagrams + charts only) is wired up once the tree is committed.
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
initFullscreenControls()
|
|
10
|
+
}, [])
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className='vp-shell'>
|
|
14
|
+
<main className='vp-main'>{children}</main>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IconAlertTriangle, IconBulb, IconInfoCircle, type IconProps } from '@tabler/icons-react'
|
|
2
|
+
import type { FC, ReactNode } from 'react'
|
|
3
|
+
import { calloutSchema } from '@visualplan/core'
|
|
4
|
+
import { validateProps } from './validate.js'
|
|
5
|
+
|
|
6
|
+
interface CalloutProps {
|
|
7
|
+
type?: string
|
|
8
|
+
children?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LABEL: Record<string, string> = {
|
|
12
|
+
note: 'Note',
|
|
13
|
+
risk: 'Risk',
|
|
14
|
+
decision: 'Decision',
|
|
15
|
+
warn: 'Warning',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ICON: Record<string, FC<IconProps>> = {
|
|
19
|
+
note: IconInfoCircle,
|
|
20
|
+
decision: IconBulb,
|
|
21
|
+
risk: IconAlertTriangle,
|
|
22
|
+
warn: IconAlertTriangle,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A highlighted note/risk/decision/warning block. Wraps markdown children. */
|
|
26
|
+
export function Callout(props: CalloutProps) {
|
|
27
|
+
const { type } = validateProps('Callout', calloutSchema, props)
|
|
28
|
+
const Icon = ICON[type] ?? IconInfoCircle
|
|
29
|
+
return (
|
|
30
|
+
<aside className='vp-callout' data-type={type}>
|
|
31
|
+
<div className='vp-callout__label'>
|
|
32
|
+
<Icon size={14} stroke={2} aria-hidden='true' />
|
|
33
|
+
{LABEL[type]}
|
|
34
|
+
</div>
|
|
35
|
+
<div className='vp-callout__body'>{props.children}</div>
|
|
36
|
+
</aside>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bar,
|
|
3
|
+
BarChart,
|
|
4
|
+
CartesianGrid,
|
|
5
|
+
Cell,
|
|
6
|
+
Line,
|
|
7
|
+
LineChart,
|
|
8
|
+
Pie,
|
|
9
|
+
PieChart,
|
|
10
|
+
ResponsiveContainer,
|
|
11
|
+
Tooltip,
|
|
12
|
+
XAxis,
|
|
13
|
+
YAxis,
|
|
14
|
+
} from 'recharts'
|
|
15
|
+
import { chartSchema } from '@visualplan/core'
|
|
16
|
+
import { ExpandButton } from './ExpandButton.js'
|
|
17
|
+
import { validateProps } from './validate.js'
|
|
18
|
+
|
|
19
|
+
interface ChartProps {
|
|
20
|
+
type: string
|
|
21
|
+
title?: string
|
|
22
|
+
data: Array<{ label: string; value: number }>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Vibrant but balanced palette, readable on both the light and dark card surface.
|
|
26
|
+
const COLORS = [
|
|
27
|
+
'#6366f1',
|
|
28
|
+
'#10b981',
|
|
29
|
+
'#f59e0b',
|
|
30
|
+
'#ef4444',
|
|
31
|
+
'#06b6d4',
|
|
32
|
+
'#a855f7',
|
|
33
|
+
'#ec4899',
|
|
34
|
+
'#84cc16',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const AXIS_TICK = { fill: 'var(--vp-muted)', fontSize: 12 }
|
|
38
|
+
const TOOLTIP_STYLE = {
|
|
39
|
+
background: 'var(--vp-surface)',
|
|
40
|
+
border: '1px solid var(--vp-border)',
|
|
41
|
+
borderRadius: 8,
|
|
42
|
+
color: 'var(--vp-text)',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A bar/line/pie chart for estimates or metrics, backed by recharts. */
|
|
46
|
+
export function Chart(props: ChartProps) {
|
|
47
|
+
const { type, title, data } = validateProps('Chart', chartSchema, props)
|
|
48
|
+
const total = data.reduce((sum, point) => sum + point.value, 0)
|
|
49
|
+
return (
|
|
50
|
+
<figure className='vp-chart vp-expandable'>
|
|
51
|
+
{title ? <figcaption className='vp-chart__title'>{title}</figcaption> : null}
|
|
52
|
+
<div className='vp-chart__canvas' data-type={type}>
|
|
53
|
+
<ResponsiveContainer width='100%' height='100%'>
|
|
54
|
+
{type === 'bar' ? (
|
|
55
|
+
<BarChart data={data} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
|
|
56
|
+
<CartesianGrid strokeDasharray='3 3' stroke='var(--vp-border)' vertical={false} />
|
|
57
|
+
<XAxis dataKey='label' tick={AXIS_TICK} stroke='var(--vp-border)' />
|
|
58
|
+
<YAxis allowDecimals tick={AXIS_TICK} stroke='var(--vp-border)' />
|
|
59
|
+
<Tooltip cursor={{ fill: 'var(--vp-surface-2)' }} contentStyle={TOOLTIP_STYLE} />
|
|
60
|
+
<Bar dataKey='value' radius={[4, 4, 0, 0]} maxBarSize={64}>
|
|
61
|
+
{data.map((point, index) => (
|
|
62
|
+
<Cell key={point.label} fill={COLORS[index % COLORS.length]} />
|
|
63
|
+
))}
|
|
64
|
+
</Bar>
|
|
65
|
+
</BarChart>
|
|
66
|
+
) : type === 'line' ? (
|
|
67
|
+
<LineChart data={data} margin={{ top: 4, right: 8, bottom: 0, left: -16 }}>
|
|
68
|
+
<CartesianGrid strokeDasharray='3 3' stroke='var(--vp-border)' vertical={false} />
|
|
69
|
+
<XAxis dataKey='label' tick={AXIS_TICK} stroke='var(--vp-border)' />
|
|
70
|
+
<YAxis allowDecimals tick={AXIS_TICK} stroke='var(--vp-border)' />
|
|
71
|
+
<Tooltip contentStyle={TOOLTIP_STYLE} />
|
|
72
|
+
<Line
|
|
73
|
+
type='monotone'
|
|
74
|
+
dataKey='value'
|
|
75
|
+
stroke={COLORS[0]}
|
|
76
|
+
strokeWidth={2.5}
|
|
77
|
+
dot={{ fill: COLORS[0], r: 3 }}
|
|
78
|
+
/>
|
|
79
|
+
</LineChart>
|
|
80
|
+
) : (
|
|
81
|
+
<PieChart>
|
|
82
|
+
<Tooltip contentStyle={TOOLTIP_STYLE} />
|
|
83
|
+
<Pie data={data} dataKey='value' nameKey='label' outerRadius={88} stroke='none'>
|
|
84
|
+
{data.map((point, index) => (
|
|
85
|
+
<Cell key={point.label} fill={COLORS[index % COLORS.length]} />
|
|
86
|
+
))}
|
|
87
|
+
</Pie>
|
|
88
|
+
</PieChart>
|
|
89
|
+
)}
|
|
90
|
+
</ResponsiveContainer>
|
|
91
|
+
</div>
|
|
92
|
+
{type === 'pie' ? (
|
|
93
|
+
<ul className='vp-chart__legend'>
|
|
94
|
+
{data.map((point, index) => (
|
|
95
|
+
<li key={point.label} className='vp-chart__legend-item'>
|
|
96
|
+
<span
|
|
97
|
+
className='vp-chart__swatch'
|
|
98
|
+
style={{ background: COLORS[index % COLORS.length] }}
|
|
99
|
+
aria-hidden='true'
|
|
100
|
+
/>
|
|
101
|
+
<span className='vp-chart__legend-label'>{point.label}</span>
|
|
102
|
+
<span className='vp-chart__legend-value'>
|
|
103
|
+
{total > 0 ? `${Math.round((point.value / total) * 100)}%` : point.value}
|
|
104
|
+
</span>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
) : null}
|
|
109
|
+
<ExpandButton />
|
|
110
|
+
</figure>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { IconSquare, IconSquareCheckFilled } from '@tabler/icons-react'
|
|
2
|
+
import { checklistSchema } from '@visualplan/core'
|
|
3
|
+
import { validateProps } from './validate.js'
|
|
4
|
+
|
|
5
|
+
interface ChecklistProps {
|
|
6
|
+
title?: string
|
|
7
|
+
items: Array<{ text: string; done?: boolean }>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Acceptance criteria / definition of done, with done and todo states. */
|
|
11
|
+
export function Checklist(props: ChecklistProps) {
|
|
12
|
+
const { title, items } = validateProps('Checklist', checklistSchema, props)
|
|
13
|
+
return (
|
|
14
|
+
<section className='vp-checklist'>
|
|
15
|
+
{title ? <div className='vp-checklist__title'>{title}</div> : null}
|
|
16
|
+
<ul className='vp-checklist__list'>
|
|
17
|
+
{items.map(item => (
|
|
18
|
+
<li
|
|
19
|
+
key={item.text}
|
|
20
|
+
className='vp-checklist__item'
|
|
21
|
+
data-done={item.done ? 'true' : 'false'}
|
|
22
|
+
>
|
|
23
|
+
{item.done ? (
|
|
24
|
+
<IconSquareCheckFilled size={17} className='vp-checklist__check' aria-hidden='true' />
|
|
25
|
+
) : (
|
|
26
|
+
<IconSquare size={17} stroke={2} className='vp-checklist__box' aria-hidden='true' />
|
|
27
|
+
)}
|
|
28
|
+
<span className='vp-checklist__text'>{item.text}</span>
|
|
29
|
+
</li>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
</section>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IconCheck, IconX } from '@tabler/icons-react'
|
|
2
|
+
import { compareSchema } from '@visualplan/core'
|
|
3
|
+
import { validateProps } from './validate.js'
|
|
4
|
+
|
|
5
|
+
interface CompareProps {
|
|
6
|
+
options: Array<{ name: string; pros?: string[]; cons?: string[]; pick?: boolean }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Side-by-side option cards for weighing approaches. */
|
|
10
|
+
export function Compare(props: CompareProps) {
|
|
11
|
+
const { options } = validateProps('Compare', compareSchema, props)
|
|
12
|
+
return (
|
|
13
|
+
<div className='vp-compare'>
|
|
14
|
+
{options.map(option => (
|
|
15
|
+
<div
|
|
16
|
+
key={option.name}
|
|
17
|
+
className='vp-compare__card'
|
|
18
|
+
data-pick={option.pick ? 'true' : 'false'}
|
|
19
|
+
>
|
|
20
|
+
<div className='vp-compare__name'>
|
|
21
|
+
{option.name}
|
|
22
|
+
{option.pick ? <span className='vp-compare__pick'>recommended</span> : null}
|
|
23
|
+
</div>
|
|
24
|
+
<div className='vp-compare__lists'>
|
|
25
|
+
<ul className='vp-compare__pros'>
|
|
26
|
+
{option.pros.map(pro => (
|
|
27
|
+
<li key={pro}>
|
|
28
|
+
<IconCheck
|
|
29
|
+
size={15}
|
|
30
|
+
stroke={2.5}
|
|
31
|
+
className='vp-compare__pro'
|
|
32
|
+
aria-hidden='true'
|
|
33
|
+
/>
|
|
34
|
+
<span>{pro}</span>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
<ul className='vp-compare__cons'>
|
|
39
|
+
{option.cons.map(con => (
|
|
40
|
+
<li key={con}>
|
|
41
|
+
<IconX size={15} stroke={2.5} className='vp-compare__con' aria-hidden='true' />
|
|
42
|
+
<span>{con}</span>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IconMaximize } from '@tabler/icons-react'
|
|
2
|
+
import { toggleFullscreen } from '../fullscreen.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A hover-revealed fullscreen toggle for a React-rendered surface (diagram, chart).
|
|
6
|
+
* It fullscreens its nearest `.vp-expandable` ancestor, so the host just needs that
|
|
7
|
+
* class and `position: relative` (both in theme.css).
|
|
8
|
+
*/
|
|
9
|
+
export function ExpandButton() {
|
|
10
|
+
return (
|
|
11
|
+
<button
|
|
12
|
+
type='button'
|
|
13
|
+
className='vp-expand-btn'
|
|
14
|
+
aria-label='Toggle fullscreen'
|
|
15
|
+
onClick={event => toggleFullscreen(event.currentTarget.closest('.vp-expandable'))}
|
|
16
|
+
>
|
|
17
|
+
<IconMaximize size={16} stroke={2} />
|
|
18
|
+
</button>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { IconArrowRight, IconFolder, IconMinus, IconPencil, IconPlus } from '@tabler/icons-react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { fileTreeSchema } from '@visualplan/core'
|
|
4
|
+
import { validateProps } from './validate.js'
|
|
5
|
+
|
|
6
|
+
interface FileTreeProps {
|
|
7
|
+
files: Array<{ path: string; change: string }>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MARKER: Record<string, ReactNode> = {
|
|
11
|
+
add: <IconPlus size={15} stroke={2.5} />,
|
|
12
|
+
modify: <IconPencil size={14} stroke={2} />,
|
|
13
|
+
delete: <IconMinus size={15} stroke={2.5} />,
|
|
14
|
+
move: <IconArrowRight size={15} stroke={2} />,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DirNode {
|
|
18
|
+
dirs: Map<string, DirNode>
|
|
19
|
+
files: Array<{ name: string; change: string }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function emptyDir(): DirNode {
|
|
23
|
+
return { dirs: new Map(), files: [] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Group flat `{path}` entries into a nested directory tree. */
|
|
27
|
+
function buildTree(files: Array<{ path: string; change: string }>): DirNode {
|
|
28
|
+
const root = emptyDir()
|
|
29
|
+
for (const { path, change } of files) {
|
|
30
|
+
const segments = path.split('/').filter(Boolean)
|
|
31
|
+
let node = root
|
|
32
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
33
|
+
const segment = segments[i]
|
|
34
|
+
if (segment === undefined) continue
|
|
35
|
+
let next = node.dirs.get(segment)
|
|
36
|
+
if (!next) {
|
|
37
|
+
next = emptyDir()
|
|
38
|
+
node.dirs.set(segment, next)
|
|
39
|
+
}
|
|
40
|
+
node = next
|
|
41
|
+
}
|
|
42
|
+
node.files.push({ name: segments[segments.length - 1] ?? path, change })
|
|
43
|
+
}
|
|
44
|
+
return root
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Collapse a chain of single-child directories (src -> src/api -> ...) into one label. */
|
|
48
|
+
function collapse(name: string, node: DirNode): { label: string; node: DirNode } {
|
|
49
|
+
let label = name
|
|
50
|
+
let current = node
|
|
51
|
+
while (current.dirs.size === 1 && current.files.length === 0) {
|
|
52
|
+
const entry = current.dirs.entries().next().value
|
|
53
|
+
if (!entry) break
|
|
54
|
+
label = `${label}/${entry[0]}`
|
|
55
|
+
current = entry[1]
|
|
56
|
+
}
|
|
57
|
+
return { label, node: current }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderDir(node: DirNode, depth: number): ReactNode[] {
|
|
61
|
+
const rows: ReactNode[] = []
|
|
62
|
+
for (const name of [...node.dirs.keys()].sort()) {
|
|
63
|
+
const child = node.dirs.get(name)
|
|
64
|
+
if (!child) continue
|
|
65
|
+
const collapsed = collapse(name, child)
|
|
66
|
+
rows.push(
|
|
67
|
+
<li
|
|
68
|
+
key={`dir:${depth}:${collapsed.label}`}
|
|
69
|
+
className='vp-filetree__row vp-filetree__row--dir'
|
|
70
|
+
style={{ paddingLeft: `${depth * 1.15}rem` }}
|
|
71
|
+
>
|
|
72
|
+
<IconFolder size={15} stroke={2} className='vp-filetree__folder' aria-hidden='true' />
|
|
73
|
+
<span className='vp-filetree__dir'>{collapsed.label}/</span>
|
|
74
|
+
</li>,
|
|
75
|
+
)
|
|
76
|
+
rows.push(...renderDir(collapsed.node, depth + 1))
|
|
77
|
+
}
|
|
78
|
+
for (const file of node.files) {
|
|
79
|
+
rows.push(
|
|
80
|
+
<li
|
|
81
|
+
key={`file:${depth}:${file.name}`}
|
|
82
|
+
className='vp-filetree__row'
|
|
83
|
+
data-change={file.change}
|
|
84
|
+
style={{ paddingLeft: `${depth * 1.15}rem` }}
|
|
85
|
+
>
|
|
86
|
+
<span className='vp-filetree__marker' aria-hidden='true'>
|
|
87
|
+
{MARKER[file.change]}
|
|
88
|
+
</span>
|
|
89
|
+
<span className='vp-filetree__name'>{file.name}</span>
|
|
90
|
+
<span className='vp-filetree__change'>{file.change}</span>
|
|
91
|
+
</li>,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return rows
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A nested directory tree of file changes with add/modify/delete/move markers. */
|
|
98
|
+
export function FileTree(props: FileTreeProps) {
|
|
99
|
+
const { files } = validateProps('FileTree', fileTreeSchema, props)
|
|
100
|
+
return <ul className='vp-filetree'>{renderDir(buildTree(files), 0)}</ul>
|
|
101
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { renderMermaidSVG, type RenderOptions } from 'beautiful-mermaid'
|
|
2
|
+
import { ExpandButton } from './ExpandButton.js'
|
|
3
|
+
|
|
4
|
+
interface MermaidProps {
|
|
5
|
+
chart: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Theme mapped onto our CSS custom properties. Because beautiful-mermaid emits the
|
|
10
|
+
* colors as CSS variables, one synchronously-rendered SVG adapts to light and dark
|
|
11
|
+
* automatically (the vars change with `prefers-color-scheme`), so there is no theme
|
|
12
|
+
* detection and no re-render on scheme change.
|
|
13
|
+
*/
|
|
14
|
+
const THEME: RenderOptions = {
|
|
15
|
+
bg: 'var(--vp-bg)',
|
|
16
|
+
fg: 'var(--vp-text)',
|
|
17
|
+
line: 'var(--vp-muted)',
|
|
18
|
+
accent: 'var(--vp-accent)',
|
|
19
|
+
muted: 'var(--vp-muted)',
|
|
20
|
+
surface: 'var(--vp-surface)',
|
|
21
|
+
border: 'var(--vp-border-strong)',
|
|
22
|
+
font: 'var(--vp-font)',
|
|
23
|
+
transparent: true,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders a mermaid diagram from the text of a ```mermaid code fence. Rendering is
|
|
28
|
+
* synchronous and DOM-free, so the SVG is present in the static HTML output (not
|
|
29
|
+
* just after a client-side effect).
|
|
30
|
+
*/
|
|
31
|
+
export function Mermaid({ chart }: MermaidProps) {
|
|
32
|
+
let svg: string
|
|
33
|
+
try {
|
|
34
|
+
svg = renderMermaidSVG(chart, THEME)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
37
|
+
return (
|
|
38
|
+
<pre className='vp-mermaid vp-mermaid--error'>
|
|
39
|
+
Mermaid error: {message}
|
|
40
|
+
{'\n\n'}
|
|
41
|
+
{chart}
|
|
42
|
+
</pre>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return (
|
|
46
|
+
<div className='vp-mermaid vp-expandable'>
|
|
47
|
+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: trusted SVG from our own synchronous renderer over author-provided diagram text, not untrusted HTML */}
|
|
48
|
+
<div className='vp-mermaid__svg' dangerouslySetInnerHTML={{ __html: svg }} />
|
|
49
|
+
<ExpandButton />
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { phaseSchema } from '@visualplan/core'
|
|
3
|
+
import { validateProps } from './validate.js'
|
|
4
|
+
|
|
5
|
+
interface PhaseProps {
|
|
6
|
+
title: string
|
|
7
|
+
status?: string
|
|
8
|
+
children?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One step in the plan's vertical numbered timeline. The step number is supplied
|
|
13
|
+
* by a CSS counter on the timeline container (see `.vp-phase__node` in theme.css),
|
|
14
|
+
* so phases self-number in document order with no index prop.
|
|
15
|
+
*/
|
|
16
|
+
export function Phase(props: PhaseProps) {
|
|
17
|
+
const { title, status } = validateProps('Phase', phaseSchema, props)
|
|
18
|
+
return (
|
|
19
|
+
<section className='vp-phase' data-status={status}>
|
|
20
|
+
<div className='vp-phase__rail'>
|
|
21
|
+
<div className='vp-phase__node' />
|
|
22
|
+
</div>
|
|
23
|
+
<div className='vp-phase__content'>
|
|
24
|
+
<div className='vp-phase__head'>
|
|
25
|
+
<h3 className='vp-phase__title'>{title}</h3>
|
|
26
|
+
<span className='vp-phase__badge' data-status={status}>
|
|
27
|
+
{status}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div className='vp-phase__body'>{props.children}</div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IconHelpCircle } from '@tabler/icons-react'
|
|
2
|
+
import { questionsSchema } from '@visualplan/core'
|
|
3
|
+
import { validateProps } from './validate.js'
|
|
4
|
+
|
|
5
|
+
interface QuestionsProps {
|
|
6
|
+
items: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Open questions for the reader to resolve before building, as a highlighted panel. */
|
|
10
|
+
export function Questions(props: QuestionsProps) {
|
|
11
|
+
const { items } = validateProps('Questions', questionsSchema, props)
|
|
12
|
+
return (
|
|
13
|
+
<section className='vp-questions'>
|
|
14
|
+
<div className='vp-questions__head'>
|
|
15
|
+
<IconHelpCircle size={16} stroke={2} className='vp-questions__icon' aria-hidden='true' />
|
|
16
|
+
<span className='vp-questions__title'>Open questions</span>
|
|
17
|
+
</div>
|
|
18
|
+
<ol className='vp-questions__list'>
|
|
19
|
+
{items.map((item, index) => (
|
|
20
|
+
<li key={item} className='vp-questions__item'>
|
|
21
|
+
<span className='vp-questions__num' aria-hidden='true'>
|
|
22
|
+
{index + 1}
|
|
23
|
+
</span>
|
|
24
|
+
<span className='vp-questions__text'>{item}</span>
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ol>
|
|
28
|
+
</section>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse component props against a zod schema, throwing a readable,
|
|
5
|
+
* component-named error when they are invalid. The error surfaces in the
|
|
6
|
+
* rendered page (and during `check`) so the author can self-correct.
|
|
7
|
+
*/
|
|
8
|
+
export function validateProps<T extends z.ZodType>(
|
|
9
|
+
component: string,
|
|
10
|
+
schema: T,
|
|
11
|
+
props: unknown,
|
|
12
|
+
): z.infer<T> {
|
|
13
|
+
const result = schema.safeParse(props)
|
|
14
|
+
if (!result.success) {
|
|
15
|
+
const detail = result.error.issues
|
|
16
|
+
.map(issue => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
17
|
+
.join('; ')
|
|
18
|
+
throw new Error(`<${component}> received invalid props — ${detail}`)
|
|
19
|
+
}
|
|
20
|
+
return result.data
|
|
21
|
+
}
|
package/runtime/css.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.css'
|