/** * Build a showcase HTML page from a DESIGN.md so the user can see what each * design system looks like *before* generating anything. We don't try to * render a unique product mockup — we extract the palette, typography, and * a couple of component conventions, then drop them into one fixed * template. The full DESIGN.md is rendered below as prose for reference. * * Parsing is deliberately permissive: imported systems vary in section * naming and bullet style, so we use loose regexes and fall back to sane * defaults when a token isn't found. */ export function renderDesignSystemPreview(id, raw) { const titleMatch = /^#\s+(.+?)\s*$/m.exec(raw); const title = cleanTitle(titleMatch?.[1] ?? id); const subtitle = extractSubtitle(raw); const colors = extractColors(raw); const fonts = extractFonts(raw); const bg = pickColor(colors, ['page background', 'background', 'canvas', 'paper', 'bg ', 'page bg']) ?? pickColor(colors, ['white']) ?? '#ffffff'; const fg = pickColor(colors, ['heading', 'foreground', 'ink', 'fg', 'text', 'navy', 'graphite']) ?? '#111111'; // Accent: brand/primary names first, then fall back to the first color // that doesn't look like a neutral white/black/grey so we always show // something punchy in the showcase header. const accent = pickColor(colors, ['primary brand', 'brand primary', 'primary', 'brand', 'accent']) ?? firstNonNeutral(colors) ?? '#2f6feb'; const muted = pickColor(colors, ['muted', 'secondary', 'neutral', 'subtle', 'caption']) ?? '#777777'; const border = pickColor(colors, ['border', 'divider', 'rule', 'stroke']) ?? '#e5e5e5'; const surface = pickColor(colors, ['surface', 'card', 'background-secondary', 'panel', 'elevated']) ?? '#ffffff'; const display = fonts.display ?? fonts.heading ?? "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"; const body = fonts.body ?? display; const mono = fonts.mono ?? "ui-monospace, 'JetBrains Mono', monospace"; const renderedMarkdown = renderMarkdownLite(raw); return ` ${escapeHtml(title)} — design system preview
Design system preview · ${escapeHtml(id)}

${escapeHtml(title)}

${subtitle ? `

${escapeHtml(subtitle)}

` : ''}

Palette

${colors .slice(0, 12) .map( (c) => `
${escapeHtml(c.name)} ${escapeHtml(c.value)}
`, ) .join('')}

Typography

Display
The grid carries weight; the line carries pace.
Body
Body copy reads at sixteen pixels with a 1.55 leading. Restraint and rhythm matter more than novelty — pick a stack that earns the page.
Mono
/* monospace · ${escapeHtml(mono.split(',')[0]?.replace(/['"]/g, '').trim() ?? 'mono')} */

Components

Card

Production-quality artifact

Sample card showing how surfaces, borders, and accent text behave in this system.

Buttons

Three weights, one accent

${renderedMarkdown}
`; } function extractSubtitle(raw) { const lines = raw.split(/\r?\n/); const h1 = lines.findIndex((l) => /^#\s+/.test(l)); if (h1 === -1) return ''; const after = lines.slice(h1 + 1); const nextHeading = after.findIndex((l) => /^#{1,6}\s+/.test(l)); const window = (nextHeading === -1 ? after : after.slice(0, nextHeading)) .join('\n') .replace(/^>\s*Category:.*$/gim, '') .replace(/^>\s*/gm, '') .trim(); return window.split(/\n\n/)[0]?.slice(0, 240) ?? ''; } function extractColors(raw) { const colors = []; const seen = new Set(); function push(name, value) { const cleanName = name.replace(/[*_`]+/g, '').replace(/\s+/g, ' ').trim(); if (!cleanName || cleanName.length > 60) return; const v = normalizeHex(value); const key = `${cleanName.toLowerCase()}|${v}`; if (seen.has(key)) return; seen.add(key); colors.push({ name: cleanName, value: v }); } // Form A: "- **Background:** `#FAFAFA`" / "- Background: #FAFAFA" const reA = /^[\s>*-]*\**\s*([A-Za-z][A-Za-z0-9 /&()+_-]{1,40}?)\s*\**\s*[::]\s*`?(#[0-9a-fA-F]{3,8})/gm; let m; while ((m = reA.exec(raw)) !== null) push(m[1], m[2]); // Form B: "**Stripe Purple** (`#533afd`)" — common in awesome-design-md. // Token name is whatever's bolded; the hex follows in parens/backticks. const reB = /\*\*([A-Za-z][A-Za-z0-9 /&()+_-]{1,40}?)\*\*\s*\(?\s*`?(#[0-9a-fA-F]{3,8})/g; while ((m = reB.exec(raw)) !== null) push(m[1], m[2]); return colors; } function extractFonts(raw) { const out = {}; // "- **Display / headings:** `'GT Sectra', ...`" // We want the backticked stack OR the rest of the line. const re = /^[\s>*-]*\**\s*([A-Za-z][A-Za-z /]{1,30}?)\s*\**\s*[::]\s*`?([^`\n]+?)`?$/gm; let m; while ((m = re.exec(raw)) !== null) { const label = m[1].toLowerCase(); const value = m[2].trim().replace(/[*_`]+$/g, '').trim(); if (!/[a-zA-Z]/.test(value)) continue; if (value.startsWith('#')) continue; if (/display|heading|h1|title/.test(label) && !out.display) out.display = value; else if (/body|text|paragraph|copy/.test(label) && !out.body) out.body = value; else if (/mono|code/.test(label) && !out.mono) out.mono = value; } return out; } function pickColor(colors, hints) { for (const hint of hints) { const needle = hint.toLowerCase(); const found = colors.find((c) => c.name.toLowerCase().includes(needle)); if (found) return found.value; } return null; } function firstNonNeutral(colors) { for (const c of colors) { const v = c.value.replace('#', '').toLowerCase(); if (v.length !== 6) continue; const r = parseInt(v.slice(0, 2), 16); const g = parseInt(v.slice(2, 4), 16); const b = parseInt(v.slice(4, 6), 16); const max = Math.max(r, g, b); const min = Math.min(r, g, b); const sat = max === 0 ? 0 : (max - min) / max; if (sat > 0.25) return c.value; } return null; } function pickReadableForeground(hex) { const n = normalizeHex(hex); if (n.length !== 7) return '#ffffff'; const r = parseInt(n.slice(1, 3), 16); const g = parseInt(n.slice(3, 5), 16); const b = parseInt(n.slice(5, 7), 16); // Standard luminance check. const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return lum > 0.6 ? '#0a0a0a' : '#ffffff'; } function normalizeHex(hex) { let h = hex.toLowerCase(); if (h.length === 4) { h = '#' + h.slice(1).split('').map((c) => c + c).join(''); } return h; } function cleanTitle(raw) { return String(raw).replace(/^Design System (Inspired by|for)\s+/i, '').trim(); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''', ); } // Tiny markdown renderer — enough for our DESIGN.md prose: H1–H4, paragraphs, // bullet/ordered lists, blockquotes, fenced code, GFM pipe tables, horizontal // rules, inline `code` / **bold** / *italic* / [link](url). Not a full markdown // implementation but covers everything the DESIGN.md files actually use. function renderMarkdownLite(src) { const lines = src.split(/\r?\n/); const out = []; let inList = null; let inBlockquote = false; let inCode = false; let i = 0; function closeList() { if (inList) { out.push(``); inList = null; } } function closeBlockquote() { if (inBlockquote) { out.push(''); inBlockquote = false; } } while (i < lines.length) { const raw = lines[i] ?? ''; const line = raw.trimEnd(); if (line.startsWith('```')) { closeList(); closeBlockquote(); if (!inCode) { out.push('
');
        inCode = true;
      } else {
        out.push('
'); inCode = false; } i++; continue; } if (inCode) { out.push(escapeHtml(raw)); i++; continue; } if (!line.trim()) { closeList(); closeBlockquote(); i++; continue; } // GFM pipe table — at least a header row, a separator row of dashes, // and one body row. Look ahead from `i` so we can consume the whole // block in one step. if (looksLikeTableHeader(line) && i + 1 < lines.length && isTableSeparator(lines[i + 1] ?? '')) { closeList(); closeBlockquote(); const headerCells = splitTableRow(line); const aligns = parseAlignments(lines[i + 1] ?? '', headerCells.length); const bodyRows = []; let j = i + 2; while (j < lines.length) { const next = (lines[j] ?? '').trimEnd(); if (!next.trim() || !next.includes('|')) break; bodyRows.push(splitTableRow(next)); j++; } out.push(renderTable(headerCells, bodyRows, aligns)); i = j; continue; } // ATX headings #..#### const h = /^(#{1,4})\s+(.+)$/.exec(line); if (h) { closeList(); closeBlockquote(); const level = h[1].length; out.push(`${inline(h[2])}`); i++; continue; } // Horizontal rule. if (/^([-*_])\1{2,}\s*$/.test(line)) { closeList(); closeBlockquote(); out.push('
'); i++; continue; } const bq = /^>\s?(.*)$/.exec(line); if (bq) { closeList(); if (!inBlockquote) { out.push('
'); inBlockquote = true; } out.push(`

${inline(bq[1] || '')}

`); i++; continue; } closeBlockquote(); const li = /^([-*])\s+(.+)$/.exec(line); if (li) { if (inList !== 'ul') { closeList(); out.push('