Add initial project structure with essential files
- Created .gitignore to exclude build artifacts and dependencies. - Added index.html as the main entry point for the application. - Included LICENSE file with Apache 2.0 terms. - Initialized package.json and package-lock.json for project dependencies. - Added pnpm-lock.yaml for package management. - Created QUICKSTART.md for setup instructions. - Added README.md and README.zh-CN.md for project documentation in English and Chinese.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// Design-system registry. Scans <projectRoot>/design-systems/* for DESIGN.md
|
||||
// files. Title comes from the first H1. Category comes from a
|
||||
// `> Category: <name>` blockquote line beneath the H1. Summary is the first
|
||||
// paragraph between the H1 and the next heading (Category line stripped).
|
||||
|
||||
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export async function listDesignSystems(root) {
|
||||
const out = [];
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await readdir(root, { withFileTypes: true });
|
||||
} catch {
|
||||
return out;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const designPath = path.join(root, entry.name, 'DESIGN.md');
|
||||
try {
|
||||
const stats = await stat(designPath);
|
||||
if (!stats.isFile()) continue;
|
||||
const raw = await readFile(designPath, 'utf8');
|
||||
const titleMatch = /^#\s+(.+?)\s*$/m.exec(raw);
|
||||
const title = cleanTitle(titleMatch?.[1] ?? entry.name);
|
||||
out.push({
|
||||
id: entry.name,
|
||||
title,
|
||||
category: extractCategory(raw) ?? 'Uncategorized',
|
||||
summary: summarize(raw),
|
||||
swatches: extractSwatches(raw),
|
||||
body: raw,
|
||||
});
|
||||
} catch {
|
||||
// Skip.
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function readDesignSystem(root, id) {
|
||||
const file = path.join(root, id, 'DESIGN.md');
|
||||
try {
|
||||
return await readFile(file, 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function summarize(raw) {
|
||||
const lines = raw.split(/\r?\n/);
|
||||
const firstH1 = lines.findIndex((l) => /^#\s+/.test(l));
|
||||
if (firstH1 === -1) return '';
|
||||
const afterH1 = lines.slice(firstH1 + 1);
|
||||
const nextHeading = afterH1.findIndex((l) => /^#{1,6}\s+/.test(l));
|
||||
const window = (nextHeading === -1 ? afterH1 : afterH1.slice(0, nextHeading))
|
||||
.join('\n')
|
||||
// Drop the Category metadata line — it's surfaced separately.
|
||||
.replace(/^>\s*Category:.*$/gim, '')
|
||||
.replace(/^>\s*/gm, '')
|
||||
.trim();
|
||||
return window.split(/\n\n/)[0]?.slice(0, 240) ?? '';
|
||||
}
|
||||
|
||||
function extractCategory(raw) {
|
||||
const m = /^>\s*Category:\s*(.+?)\s*$/im.exec(raw);
|
||||
return m?.[1];
|
||||
}
|
||||
|
||||
// Strip boilerplate like "Design System Inspired by Cohere" → "Cohere" so
|
||||
// the picker dropdown reads cleanly. Hand-authored titles that don't match
|
||||
// the pattern (e.g. "Neutral Modern") pass through unchanged.
|
||||
function cleanTitle(raw) {
|
||||
return raw
|
||||
.replace(/^Design System (Inspired by|for)\s+/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull 4 representative colors from a DESIGN.md so the picker can render
|
||||
* a tiny swatch row next to each system. Order: [bg, support, fg, accent].
|
||||
*
|
||||
* The shape is deliberately compact — one accent + one background + one
|
||||
* fg + one supporting tone — so the row reads like a brand mark even at
|
||||
* thumbnail scale. Picked greedily by token-name hints (matches the
|
||||
* heuristics in design-system-preview.js so the strip and the showcase
|
||||
* agree on which colors the system "is").
|
||||
*
|
||||
* @param {string} raw Markdown body of DESIGN.md
|
||||
* @returns {string[]} Up to 4 hex strings; [] if extraction fails.
|
||||
*/
|
||||
function extractSwatches(raw) {
|
||||
const colors = [];
|
||||
const seen = new Set();
|
||||
function push(name, value) {
|
||||
const cleanName = name.replace(/[*_`]+/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
const v = normalizeHex(value);
|
||||
if (!v || cleanName.length > 60) return;
|
||||
const key = `${cleanName}|${v}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
colors.push({ name: cleanName, value: v });
|
||||
}
|
||||
// Form A: "- **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`)"
|
||||
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]);
|
||||
if (colors.length === 0) return [];
|
||||
|
||||
function pick(hints) {
|
||||
for (const h of hints) {
|
||||
const found = colors.find((c) => c.name.includes(h));
|
||||
if (found) return found.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function isNeutral(hex) {
|
||||
if (!/^#[0-9a-f]{6}$/.test(hex)) return false;
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return Math.max(r, g, b) - Math.min(r, g, b) < 10;
|
||||
}
|
||||
|
||||
const bg =
|
||||
pick(['page background', 'background', 'canvas', 'paper', 'surface'])
|
||||
?? '#ffffff';
|
||||
const fg =
|
||||
pick(['heading', 'foreground', 'ink', 'fg', 'text', 'navy', 'graphite'])
|
||||
?? '#111111';
|
||||
const accent =
|
||||
pick(['primary brand', 'brand primary', 'accent', 'brand', 'primary'])
|
||||
?? colors.find((c) => !isNeutral(c.value))?.value
|
||||
?? colors[0]?.value
|
||||
?? '#888888';
|
||||
const support =
|
||||
pick(['border', 'divider', 'rule', 'muted', 'secondary', 'subtle'])
|
||||
?? colors.find(
|
||||
(c) => isNeutral(c.value) && c.value !== bg && c.value !== fg,
|
||||
)?.value
|
||||
?? '#cccccc';
|
||||
|
||||
return [bg, support, fg, accent];
|
||||
}
|
||||
|
||||
function normalizeHex(raw) {
|
||||
if (typeof raw !== 'string') return null;
|
||||
const m = /^#([0-9a-fA-F]{3,8})$/.exec(raw.trim());
|
||||
if (!m) return null;
|
||||
let hex = m[1];
|
||||
if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');
|
||||
if (hex.length === 4) hex = hex.split('').map((c) => c + c).join('').slice(0, 8);
|
||||
return '#' + hex.toLowerCase();
|
||||
}
|
||||
Reference in New Issue
Block a user