mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
A design system imported from a SwiftUI repo could never be published. The import's file scorer was web-only, so every .swift file scored 0 while the repo's config dotfiles (.zenflow, .vscode, .zed) scored on the generic text bonus and won the top-N selection. Even when a source file was selected, the snapshot writer's text-file allowlist didn't include .swift, so it was dropped. The result: snapshots were all config dotfiles, which the project files API hides, so the publish gate saw zero evidence snapshots and stayed blocked no matter how many times the intake ran. Three layers fixed in tools-connectors-cli.ts: - scoreDesignFile now scores native/design-token source (Swift, Kotlin, Go, Rust, etc.), with a high boost for token files like ColorSystem.swift, Typography.swift, Spacing.swift. - shouldSkipRepoPath skips editor/CI/agent tooling dirs (.vscode, .zed, .idea, .zenflow, .github, .husky, ...) so their files stop crowding out real source. - isTextSnapshotPath recognizes native source extensions so selected files are actually written. Also teach the design-system swatch extraction to read SwiftUI colors: swift-colors.ts parses Color(red:green:blue:), Color(hue:saturation:brightness:) (HSB), and Color(white:), evaluating decimal, hex-byte, and division component expressions (0xF4 / 255, 220 / 360), and design-systems.ts uses it as a new swatch form. scoreDesignFile, shouldSkipRepoPath, and isTextSnapshotPath are exported for unit tests. Verified against a real SwiftUI repo: the intake now captures ColorSystem, TypographySystem, SpacingSystem and the view/model source instead of config dotfiles.
281 lines
8.2 KiB
TypeScript
281 lines
8.2 KiB
TypeScript
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { listDesignSystems } from '../src/design-systems.js';
|
|
|
|
function fresh(): string {
|
|
return mkdtempSync(path.join(tmpdir(), 'od-design-systems-frontmatter-'));
|
|
}
|
|
|
|
function brandDir(root: string, id: string): string {
|
|
const dir = path.join(root, id);
|
|
mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function writeDesignMd(root: string, id: string, body: string): void {
|
|
const dir = brandDir(root, id);
|
|
writeFileSync(path.join(dir, 'DESIGN.md'), body);
|
|
}
|
|
|
|
describe('listDesignSystems frontmatter parsing (issue #1857)', () => {
|
|
it('uses frontmatter name/description/category/surface when no Markdown equivalents are present', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'google-only',
|
|
[
|
|
'---',
|
|
'name: Google Material',
|
|
'description: A clean material-inspired system.',
|
|
'category: Productivity',
|
|
'surface: web',
|
|
'---',
|
|
'',
|
|
'Some rationale text without an H1 or Category blockquote.',
|
|
].join('\n'),
|
|
);
|
|
|
|
const out = await listDesignSystems(root);
|
|
expect(out).toHaveLength(1);
|
|
const ds = out[0]!;
|
|
expect(ds.id).toBe('google-only');
|
|
expect(ds.title).toBe('Google Material');
|
|
expect(ds.category).toBe('Productivity');
|
|
expect(ds.summary).toBe('A clean material-inspired system.');
|
|
expect(ds.surface).toBe('web');
|
|
});
|
|
|
|
it('extracts swatches from frontmatter colors when the Markdown body has no hex swatches', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'colors-only',
|
|
[
|
|
'---',
|
|
'name: Colors Only',
|
|
'description: Frontmatter-driven palette.',
|
|
'colors:',
|
|
' background: "#fafafa"',
|
|
' text: "#111111"',
|
|
' accent: "#ff3366"',
|
|
' border: "#dddddd"',
|
|
'---',
|
|
'',
|
|
'Body prose with no hex codes at all.',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.swatches).toEqual(['#fafafa', '#dddddd', '#111111', '#ff3366']);
|
|
});
|
|
|
|
it('extracts swatches from a markdown color table (issue #2813)', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'table-colors',
|
|
[
|
|
'# Table Colors',
|
|
'',
|
|
'## Color',
|
|
'',
|
|
'| Role | Token | Hex | Notes |',
|
|
'| --- | --- | --- | --- |',
|
|
'| Window canvas | `--window-background` | `#1a1a1d` | base surface |',
|
|
'| Border | `--border` | `#2a2a2e` | rules |',
|
|
'| Primary text | `--fg` | `#f5f5f7` | |',
|
|
'| Accent | `--accent` | `#0a84ff` | brand |',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
// [background, border/support, text, accent] picked from the table rows.
|
|
expect(ds?.swatches).toEqual(['#1a1a1d', '#2a2a2e', '#f5f5f7', '#0a84ff']);
|
|
});
|
|
|
|
it('returns identical summary shape for legacy Markdown-only DESIGN.md (no frontmatter, regression guard)', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'legacy',
|
|
[
|
|
'# Design System Inspired by Legacy',
|
|
'',
|
|
'> Category: Productivity',
|
|
'',
|
|
'A productivity-oriented system used by the picker today.',
|
|
'',
|
|
'- **Background:** `#ffffff`',
|
|
'- **Text:** `#222222`',
|
|
'- **Accent:** `#ff3366`',
|
|
'- **Border:** `#dddddd`',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.title).toBe('Legacy');
|
|
expect(ds?.category).toBe('Productivity');
|
|
expect(ds?.summary).toBe('A productivity-oriented system used by the picker today.');
|
|
expect(ds?.swatches).toEqual(['#ffffff', '#dddddd', '#222222', '#ff3366']);
|
|
expect(ds?.surface).toBe('web');
|
|
});
|
|
|
|
it('prefers frontmatter colors over Markdown swatches when both are present', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'hybrid-colors',
|
|
[
|
|
'---',
|
|
'colors:',
|
|
' background: "#fafafa"',
|
|
' text: "#111111"',
|
|
' accent: "#ff3366"',
|
|
' border: "#dddddd"',
|
|
'---',
|
|
'',
|
|
'# Hybrid Colors',
|
|
'',
|
|
'> Category: Productivity',
|
|
'',
|
|
'Body with explicit swatches.',
|
|
'',
|
|
'- **Background:** `#000000`',
|
|
'- **Text:** `#ffffff`',
|
|
'- **Accent:** `#abcdef`',
|
|
'- **Border:** `#999999`',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.swatches).toEqual(['#fafafa', '#dddddd', '#111111', '#ff3366']);
|
|
});
|
|
|
|
it('falls back to Markdown swatches when frontmatter colors use unrecognized token names (totality-festival regression)', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'totality-style',
|
|
[
|
|
'---',
|
|
'colors:',
|
|
' surface: "#121318"',
|
|
' on-surface: "#e3e1e9"',
|
|
' on-surface-variant: "#d0c6ab"',
|
|
' outline: "#999077"',
|
|
' primary: "#fff6df"',
|
|
' secondary: "#bdf4ff"',
|
|
' background: "#121318"',
|
|
'---',
|
|
'',
|
|
'# Design System Inspired by Totality',
|
|
'',
|
|
'> Category: Themed & Unique',
|
|
'',
|
|
'Cosmic-premium dark system.',
|
|
'',
|
|
'- **Surface:** `#121318`',
|
|
'- **Text:** `#e3e1e9`',
|
|
'- **Text Muted:** `#d0c6ab`',
|
|
'- **Primary:** `#fff6df`',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.swatches).toEqual(['#121318', '#d0c6ab', '#e3e1e9', '#fff6df']);
|
|
});
|
|
|
|
it('prefers Markdown H1 and Markdown Category over frontmatter when a hybrid file has both', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'hybrid',
|
|
[
|
|
'---',
|
|
'name: Frontmatter Title',
|
|
'description: Frontmatter summary that loses to Markdown.',
|
|
'category: Frontmatter Category',
|
|
'---',
|
|
'',
|
|
'# Markdown H1 Title',
|
|
'',
|
|
'> Category: Markdown Category',
|
|
'',
|
|
'Markdown summary paragraph wins.',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.title).toBe('Markdown H1 Title');
|
|
expect(ds?.category).toBe('Markdown Category');
|
|
expect(ds?.summary).toBe('Markdown summary paragraph wins.');
|
|
});
|
|
|
|
it('does not throw on malformed or empty frontmatter and still surfaces the brand from body heuristics', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'malformed',
|
|
[
|
|
'---',
|
|
'---',
|
|
'',
|
|
'# Body Only',
|
|
'',
|
|
'> Category: From Body',
|
|
'',
|
|
'Body summary line.',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.title).toBe('Body Only');
|
|
expect(ds?.category).toBe('From Body');
|
|
expect(ds?.summary).toBe('Body summary line.');
|
|
});
|
|
|
|
it('keeps the body field as the verbatim file content including the frontmatter delimiters', async () => {
|
|
const root = fresh();
|
|
const raw = [
|
|
'---',
|
|
'name: Verbatim Body',
|
|
'description: Body must include the frontmatter.',
|
|
'---',
|
|
'',
|
|
'Trailing prose.',
|
|
'',
|
|
].join('\n');
|
|
writeDesignMd(root, 'verbatim', raw);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.body).toBe(raw);
|
|
});
|
|
|
|
it('extracts SwiftUI Color tokens as swatches when the palette is Swift source', async () => {
|
|
const root = fresh();
|
|
writeDesignMd(
|
|
root,
|
|
'swift-brand',
|
|
[
|
|
'# Swift Brand',
|
|
'',
|
|
'> Category: Custom',
|
|
'',
|
|
'Palette from ColorSystem.swift:',
|
|
'',
|
|
'- appShellLight: `Color(red: 0xF4 / 255, green: 0xF4 / 255, blue: 0xF4 / 255)`',
|
|
'- ink: `Color(red: 0x11 / 255, green: 0x11 / 255, blue: 0x11 / 255)`',
|
|
'- grey50: `Color(hue: 220 / 360, saturation: 0.02, brightness: 0.99)`',
|
|
'- accent: `Color(hue: 220 / 360, saturation: 0.82, brightness: 0.9)`',
|
|
].join('\n'),
|
|
);
|
|
|
|
const [ds] = await listDesignSystems(root);
|
|
expect(ds?.swatches.length).toBeGreaterThan(0);
|
|
expect(ds?.swatches).toContain('#f4f4f4');
|
|
});
|
|
});
|