From e43e912fc1fa2dc607cbc33d3ca32fa9145920e5 Mon Sep 17 00:00:00 2001 From: tototofu123 Date: Thu, 23 Apr 2026 17:45:02 +0800 Subject: [PATCH] feat: add visual dashboard generator via show command --- packages/cli/src/commands/html.ts | 276 ++++++++++++++++++++++++++++++ packages/cli/src/commands/show.ts | 38 ++++ packages/cli/src/index.ts | 2 + 3 files changed, 316 insertions(+) create mode 100644 packages/cli/src/commands/html.ts create mode 100644 packages/cli/src/commands/show.ts diff --git a/packages/cli/src/commands/html.ts b/packages/cli/src/commands/html.ts new file mode 100644 index 0000000..baf8124 --- /dev/null +++ b/packages/cli/src/commands/html.ts @@ -0,0 +1,276 @@ +import type { DesignSystemState, ResolvedValue, ResolvedDimension, ResolvedTypography, ResolvedColor } from '../linter/index.js'; + +export function generateHtml(state: DesignSystemState): string { + const { name, colors, typography, rounded, spacing, components } = state; + + const colorObj = Object.fromEntries(colors || new Map()); + const typoObj = Object.fromEntries(typography || new Map()); + const roundedObj = Object.fromEntries(rounded || new Map()); + const spacingObj = Object.fromEntries(spacing || new Map()); + const compObj = Object.fromEntries(components || new Map()); + + const toCssValue = (val: any): string => { + if (!val) return ''; + if (typeof val === 'string') return val; + if (val.type === 'color') return val.hex; + if (val.type === 'dimension') return `${val.value}${val.unit || ''}`; + if (val.type === 'typography') return `${val.fontSize?.value}${val.fontSize?.unit || ''} ${val.fontFamily}`; + return String(val); + }; + + const fonts = new Set(); + Object.values(typoObj).forEach((t: any) => { + if (t.fontFamily) fonts.add(t.fontFamily); + }); + fonts.add('Manrope'); + + const fontImport = Array.from(fonts).length > 0 + ? `@import url('https://fonts.googleapis.com/css2?family=${Array.from(fonts).map(f => f.replace(/ /g, '+')).join(':wght@300;400;500;600;700&family=')}:wght@300;400;500;600;700&display=swap');` + : ''; + + const primary = toCssValue(colorObj.primary) || '#2E7D32'; + + return ` + + + + +${name || 'Design System'} — Dashboard + + + + +
+
+
+ ${name || 'Design System'} +
+
+ SYSTEM DASHBOARD +
+
+ +
+ + +
+ + +
+
Typography
+
+ ${Object.entries(typoObj).map(([key, t]: [string, any]) => ` +
+
+ ${key} + ${t.fontFamily} +
+
The quick brown fox
+
+
Size: ${toCssValue(t.fontSize)} / Weight: ${t.fontWeight || 400} / Lead: ${toCssValue(t.lineHeight)}
+
+
+ `).join('')} +
+
+ + +
+
Components
+
+ ${Object.entries(compObj).map(([key, c]: [string, any]) => { + const props = Object.fromEntries(c.properties); + const typo = props.typography as any; + const typoStyles = typo?.type === 'typography' ? ` + font-family: '${typo.fontFamily || 'inherit'}'; + font-size: ${toCssValue(typo.fontSize) || 'inherit'}; + font-weight: ${typo.fontWeight || 'inherit'}; + ` : ''; + + return ` +
+
${key}
+
+
+ Preview +
+
+
+ ${Object.entries(props).map(([pk, pv]) => ` + ${pk} + ${toCssValue(pv)} + `).join('')} +
+
+ `; + }).join('')} +
+
+ + +
+
+
Spacing
+
+ ${Object.entries(spacingObj).map(([key, val]: [string, any]) => ` +
+
${key}
+
+
${toCssValue(val)}
+
+ `).join('')} +
+
+
+
Rounding
+
+ ${Object.entries(roundedObj).map(([key, val]: [string, any]) => ` +
+
+
${key}
+
${toCssValue(val)}
+
+ `).join('')} +
+
+
+ +
+
+ + +`; +} + +function generateScaleHtml(hex: string, steps: number): string { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + const rgb = result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; + if (!rgb) return Array(steps).fill('').join(''); + + let html = ''; + for (let i = 0; i < steps; i++) { + const lum = 0.2 + (i * (1.6 / (steps - 1))); + const r = Math.min(255, Math.max(0, Math.round(rgb.r * lum))); + const g = Math.min(255, Math.max(0, Math.round(rgb.g * lum))); + const b = Math.min(255, Math.max(0, Math.round(rgb.b * lum))); + html += ``; + } + return html; +} diff --git a/packages/cli/src/commands/show.ts b/packages/cli/src/commands/show.ts new file mode 100644 index 0000000..76c927a --- /dev/null +++ b/packages/cli/src/commands/show.ts @@ -0,0 +1,38 @@ +import { defineCommand } from 'citty'; +import { lint } from '../linter/index.js'; +import { readInput } from '../utils.js'; +import { generateHtml } from './html.js'; +import { writeFile } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; + +export default defineCommand({ + meta: { + name: 'show', + description: 'Generate a visual dashboard for a DESIGN.md file.', + }, + args: { + file: { + type: 'positional', + description: 'Path to DESIGN.md', + required: true, + }, + }, + async run({ args }) { + const content = await readInput(args.file); + const report = lint(content); + + if (report.summary.errors > 0) { + console.error('Cannot generate dashboard: DESIGN.md has errors.'); + report.findings + .filter(f => f.severity === 'error') + .forEach(f => console.error(`- ${f.message}`)); + process.exit(1); + } + + const html = generateHtml(report.designSystem); + const outPath = join(dirname(args.file), 'design.html'); + + await writeFile(outPath, html, 'utf8'); + console.log(`Dashboard generated: ${outPath}`); + }, +}); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4c1ff67..d27b478 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -19,6 +19,7 @@ import lintCommand from './commands/lint.js'; import diffCommand from './commands/diff.js'; import exportCommand from './commands/export.js'; import specCommand from './commands/spec.js'; +import showCommand from './commands/show.js'; const main = defineCommand({ meta: { @@ -31,6 +32,7 @@ const main = defineCommand({ diff: diffCommand, export: exportCommand, spec: specCommand, + show: showCommand, }, });