@webdesignhot/design-md 0.4.0 → 0.5.1

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/README.md CHANGED
@@ -64,12 +64,9 @@ export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailw
64
64
  Pass --tailwind-version v3 for the legacy
65
65
  module.exports = { theme: { extend: { ... } } } shape.
66
66
  css-tailwind — v4 @theme {} CSS block (alias of `--to tailwind`,
67
- matches @google/design.md's `--format css-tailwind`
68
- naming from PR #64 so commands transfer between
69
- the two CLIs without re-learning flag conventions).
67
+ version-explicit naming for clarity in scripts).
70
68
  json-tailwind — v3 module.exports JSON (alias of
71
- `--to tailwind --tailwind-version v3`, same
72
- Google CLI compat reason as above).
69
+ `--to tailwind --tailwind-version v3`).
73
70
  css — :root { --color-bg, --radius-card, … }
74
71
  dtcg — W3C Design Tokens Community Group JSON
75
72
  figma — Figma Variables import format
@@ -79,8 +76,8 @@ extract <url> [-o <path>] [--token-only]
79
76
  (Requires a webdesignhot session — opens the browser flow.)
80
77
 
81
78
  import <url>
82
- Alias of `extract`. Naming aligned with Google Labs DESIGN.md tooling
83
- so the same verb works whichever CLI you've internalised.
79
+ Alias of `extract`. Common `import` verb so the muscle memory carries
80
+ over from other dev tools.
84
81
 
85
82
  theme <slug> [--dark|--light]
86
83
  Compute a dark/light variant of any design.
package/package.json CHANGED
@@ -1,14 +1,23 @@
1
1
  {
2
2
  "name": "@webdesignhot/design-md",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Drop any DESIGN.md into your repo in one command. Browse, install, lint, diff, and export design systems extracted from real production sites.",
5
- "keywords": ["design.md", "design-system", "design-tokens", "ai-agents", "cli", "tailwind", "figma", "dtcg"],
5
+ "keywords": [
6
+ "design.md",
7
+ "design-system",
8
+ "design-tokens",
9
+ "ai-agents",
10
+ "cli",
11
+ "tailwind",
12
+ "figma",
13
+ "dtcg"
14
+ ],
6
15
  "author": "webdesignhot",
7
16
  "license": "MIT",
8
17
  "homepage": "https://www.webdesignhot.com/design.md/",
9
18
  "repository": {
10
19
  "type": "git",
11
- "url": "https://github.com/WebDesignHot/design-md"
20
+ "url": "git+https://github.com/WebDesignHot/design-md.git"
12
21
  },
13
22
  "bin": {
14
23
  "design-md": "bin/design-md.mjs"
@@ -23,6 +32,9 @@
23
32
  "engines": {
24
33
  "node": ">=18"
25
34
  },
35
+ "scripts": {
36
+ "prepublishOnly": "node -e \"const p=require('./package.json');const e=require('child_process').execSync;let r='0.0.0';try{r=e('npm view '+p.name+' version 2>/dev/null').toString().trim()||'0.0.0'}catch(_){};if(p.version===r){console.error('\\n\\x1b[31m✗ '+p.name+'@'+p.version+' already on npm. Bump version before publishing.\\x1b[0m\\n');process.exit(1)}console.log('→ '+p.name+': '+r+' → '+p.version)\""
37
+ },
26
38
  "dependencies": {
27
39
  "@clack/prompts": "^0.7.0",
28
40
  "kleur": "^4.1.5",
@@ -8,8 +8,15 @@ function parseFlags(args) {
8
8
  const positional = []
9
9
  for (let i = 0; i < args.length; i++) {
10
10
  const a = args[i]
11
- if (a === '-o' || a === '--out') flags.out = args[++i]
12
- else if (a === '-f' || a === '--force') flags.force = true
11
+ if (a === '-o' || a === '--out') {
12
+ const value = args[++i]
13
+ if (value === undefined || value.startsWith('-')) {
14
+ console.error(`Usage: design-md add <slug> [-o <path>] [-f]`)
15
+ console.error(`Error: ${a} requires a <path> value.`)
16
+ process.exit(2)
17
+ }
18
+ flags.out = value
19
+ } else if (a === '-f' || a === '--force') flags.force = true
13
20
  else positional.push(a)
14
21
  }
15
22
  return { flags, positional }
@@ -14,6 +14,19 @@ function parseFlags(args) {
14
14
  return { flags, positional }
15
15
  }
16
16
 
17
+ function flatten(obj, prefix = '') {
18
+ const out = {}
19
+ if (!obj || typeof obj !== 'object') return out
20
+ for (const [k, v] of Object.entries(obj)) {
21
+ const key = prefix ? `${prefix}.${k}` : k
22
+ if (v == null) continue
23
+ if (Array.isArray(v)) out[key] = `[${v.join(', ')}]`
24
+ else if (typeof v === 'object') Object.assign(out, flatten(v, key))
25
+ else out[key] = String(v)
26
+ }
27
+ return out
28
+ }
29
+
17
30
  function diffMaps(left, right) {
18
31
  const result = { added: {}, removed: {}, modified: {} }
19
32
  const keys = new Set([...Object.keys(left), ...Object.keys(right)])
@@ -39,10 +52,13 @@ export const diff = {
39
52
  const A = await readDesignMd(resolve(process.cwd(), a))
40
53
  const B = await readDesignMd(resolve(process.cwd(), b))
41
54
  const colors = diffMaps(flattenColors(A.data), flattenColors(B.data))
42
- const radii = diffMaps(A.data.radius ?? {}, B.data.radius ?? {})
55
+ const radii = diffMaps(flatten(A.data.radius ?? {}), flatten(B.data.radius ?? {}))
56
+ const typography = diffMaps(flatten(A.data.typography ?? {}), flatten(B.data.typography ?? {}))
57
+ const spacing = diffMaps(flatten(A.data.spacing ?? {}), flatten(B.data.spacing ?? {}))
58
+ const components = diffMaps(flatten(A.data.components ?? {}), flatten(B.data.components ?? {}))
43
59
 
44
60
  if (flags.format === 'json') {
45
- console.log(JSON.stringify({ colors, radii }, null, 2))
61
+ console.log(JSON.stringify({ colors, radii, typography, spacing, components }, null, 2))
46
62
  return
47
63
  }
48
64
 
@@ -61,5 +77,8 @@ export const diff = {
61
77
 
62
78
  printSection('Colors', colors)
63
79
  printSection('Radii', radii)
80
+ printSection('Typography', typography)
81
+ printSection('Spacing', spacing)
82
+ printSection('Components', components)
64
83
  },
65
84
  }
@@ -1,15 +1,14 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { readDesignMd } from '../lib/parse.mjs'
3
3
 
4
- // Format aliases align our CLI with @google/design.md's PR #64 naming
5
- // (`--format css-tailwind` / `--format json-tailwind`) so users can move
6
- // between the two CLIs without re-learning flag conventions. Both shapes
7
- // are accepted; the underlying renderer is the same.
4
+ // Format flag accepts `tailwind` (our default v4 CSS) plus the explicit
5
+ // `css-tailwind` / `json-tailwind` aliases so callers can be version-
6
+ // explicit without re-learning conventions. Underlying renderer is the
7
+ // same only the output shape changes.
8
8
  //
9
- // `--to tailwind` → defaults to v4 CSS (was v3 JSON pre-0.2;
10
- // same default we set in 0.2 still applies)
11
- // `--to css-tailwind` v4 CSS @theme block (Google compat)
12
- // `--to json-tailwind` → v3 JSON theme.extend (Google compat)
9
+ // `--to tailwind` → v4 CSS @theme block (default)
10
+ // `--to css-tailwind` → v4 CSS @theme block (explicit)
11
+ // `--to json-tailwind` v3 JSON theme.extend
13
12
  // `--to tailwind --tailwind-version v3` → v3 JSON (legacy flag, kept)
14
13
  const FORMATS = new Set(['tailwind', 'css-tailwind', 'json-tailwind', 'css', 'dtcg', 'figma'])
15
14
  const TAILWIND_VERSIONS = new Set(['v3', 'v4'])
@@ -29,10 +28,12 @@ function parseFlags(args) {
29
28
  }
30
29
 
31
30
  function toCss(data) {
32
- const c = data.colors ?? {}
31
+ const c = flattenColors(data.colors ?? {})
33
32
  const r = data.radius ?? {}
34
33
  const lines = [':root {']
35
- for (const [k, v] of Object.entries(c)) lines.push(` --color-${k}: ${v};`)
34
+ for (const [k, v] of Object.entries(c)) {
35
+ if (typeof v === 'string') lines.push(` --color-${k}: ${v};`)
36
+ }
36
37
  for (const [k, v] of Object.entries(r)) lines.push(` --radius-${k}: ${typeof v === 'number' ? `${v}px` : v};`)
37
38
  lines.push('}')
38
39
  return lines.join('\n')
@@ -90,11 +91,15 @@ function toTailwind(data, version = 'v4') {
90
91
  }
91
92
 
92
93
  function toDtcg(data) {
93
- const c = data.colors ?? {}
94
+ const c = flattenColors(data.colors ?? {})
94
95
  const r = data.radius ?? {}
95
96
  const out = {
96
97
  $schema: 'https://design-tokens.github.io/community-group/format/',
97
- color: Object.fromEntries(Object.entries(c).map(([k, v]) => [k, { $value: String(v), $type: 'color' }])),
98
+ color: Object.fromEntries(
99
+ Object.entries(c)
100
+ .filter(([, v]) => typeof v === 'string')
101
+ .map(([k, v]) => [k, { $value: v, $type: 'color' }]),
102
+ ),
98
103
  radius: Object.fromEntries(
99
104
  Object.entries(r).map(([k, v]) => [
100
105
  k,
@@ -106,14 +111,16 @@ function toDtcg(data) {
106
111
  }
107
112
 
108
113
  function toFigma(data) {
109
- const c = data.colors ?? {}
114
+ const c = flattenColors(data.colors ?? {})
110
115
  const out = {
111
- variables: Object.entries(c).map(([k, v]) => ({
112
- name: k,
113
- type: 'COLOR',
114
- value: String(v),
115
- description: `${data.name} · ${k}`,
116
- })),
116
+ variables: Object.entries(c)
117
+ .filter(([, v]) => typeof v === 'string')
118
+ .map(([k, v]) => ({
119
+ name: k,
120
+ type: 'COLOR',
121
+ value: v,
122
+ description: `${data.name} · ${k}`,
123
+ })),
117
124
  }
118
125
  return JSON.stringify(out, null, 2)
119
126
  }
@@ -21,7 +21,7 @@ ${kleur.dim('COMMANDS')}
21
21
  dtcg W3C Design Tokens JSON
22
22
  figma Figma Variables import format
23
23
  extract <url> Extract a draft DESIGN.md from a production URL
24
- import <url> Alias of extract — name aligned with Google Labs
24
+ import <url> Alias of extract
25
25
  theme <slug> --dark Compute a dark-mode counterpart of a light design
26
26
  preview <slug> Open the directory detail page in your browser
27
27
  submit <file> Open a PR to add your DESIGN.md to the public catalog
@@ -1,10 +1,52 @@
1
1
  import { resolve } from 'node:path'
2
+ import { readFile } from 'node:fs/promises'
2
3
  import kleur from 'kleur'
4
+ import yaml from 'yaml'
3
5
  import { readDesignMd } from '../lib/parse.mjs'
4
6
 
5
7
  const REQUIRED = ['name', 'tagline', 'spec', 'colors', 'typography']
6
8
  const SECTIONS_RE = /^##\s+(.+)$/gm
7
9
 
10
+ // Pure function: lint raw DESIGN.md content (string).
11
+ // Returns { findings, summary } shaped for programmatic callers
12
+ // (the extract pipeline uses this to surface warnings on drafts).
13
+ export function lintContent(content) {
14
+ const findings = []
15
+ let data = {}
16
+ let body = content
17
+
18
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
19
+ if (fmMatch) {
20
+ try {
21
+ data = yaml.parse(fmMatch[1]) ?? {}
22
+ body = fmMatch[2]
23
+ } catch (err) {
24
+ findings.push({ level: 'error', code: 'invalid-frontmatter', msg: `YAML parse error: ${err.message}` })
25
+ }
26
+ } else {
27
+ findings.push({ level: 'error', code: 'missing-frontmatter', msg: 'No YAML frontmatter found.' })
28
+ }
29
+
30
+ for (const k of REQUIRED) {
31
+ if (data[k] == null) findings.push({ level: 'error', code: 'missing-field', field: k, msg: `Required field "${k}" is missing.` })
32
+ }
33
+ if (data.colors) {
34
+ if (!data.colors.bg) findings.push({ level: 'warn', code: 'missing-color-role', msg: 'colors.bg is recommended.' })
35
+ if (!data.colors.text) findings.push({ level: 'warn', code: 'missing-color-role', msg: 'colors.text is recommended.' })
36
+ if (!data.colors.brand && !data.colors.primary) findings.push({ level: 'warn', code: 'missing-primary', msg: 'A primary brand color (colors.brand or colors.primary) is recommended.' })
37
+ }
38
+
39
+ const sections = [...body.matchAll(SECTIONS_RE)].map((m) => m[1].trim())
40
+ if (sections.length < 3) findings.push({ level: 'warn', code: 'few-sections', msg: `Only ${sections.length} ## sections — recommend 5+.` })
41
+
42
+ const summary = {
43
+ errors: findings.filter((f) => f.level === 'error').length,
44
+ warnings: findings.filter((f) => f.level === 'warn').length,
45
+ infos: findings.filter((f) => f.level === 'info').length,
46
+ }
47
+ return { findings, summary, sections }
48
+ }
49
+
8
50
  function parseFlags(args) {
9
51
  const flags = { format: 'text' }
10
52
  const positional = []
@@ -26,22 +68,25 @@ export const lint = {
26
68
  console.error('Usage: design-md lint <file>')
27
69
  process.exit(2)
28
70
  }
29
- const findings = []
30
- const { data, body } = await readDesignMd(resolve(process.cwd(), file))
31
-
32
- for (const k of REQUIRED) {
33
- if (data[k] == null) findings.push({ level: 'error', code: 'missing-field', field: k, msg: `Required field "${k}" is missing.` })
71
+ const filePath = resolve(process.cwd(), file)
72
+ const content = await readFile(filePath, 'utf8')
73
+ const { findings, summary, sections } = lintContent(content)
74
+ // Also resolve via readDesignMd for the success-line color/type counts.
75
+ // readDesignMd parseDesignMd THROWS on missing/invalid frontmatter
76
+ // exactly the malformed inputs lint exists to diagnose. Swallow that here
77
+ // so findings + summary always print (and --format=json always emits);
78
+ // the findings already record those problems. Exit code stays driven off
79
+ // findings below, not this parse.
80
+ let data = {}
81
+ try {
82
+ ;({ data } = await readDesignMd(filePath))
83
+ } catch {
84
+ data = {}
34
85
  }
35
- if (data.colors && !data.colors.bg) findings.push({ level: 'warn', code: 'missing-color-role', msg: 'colors.bg is recommended.' })
36
- if (data.colors && !data.colors.text) findings.push({ level: 'warn', code: 'missing-color-role', msg: 'colors.text is recommended.' })
37
- if (data.colors && !data.colors.brand) findings.push({ level: 'warn', code: 'missing-color-role', msg: 'colors.brand is recommended.' })
38
-
39
- const sections = [...body.matchAll(SECTIONS_RE)].map((m) => m[1].trim())
40
- if (sections.length < 3) findings.push({ level: 'warn', code: 'few-sections', msg: `Only ${sections.length} ## sections — recommend 5+.` })
41
86
 
42
87
  if (flags.format === 'json') {
43
- console.log(JSON.stringify({ file, findings, sections }, null, 2))
44
- process.exit(findings.some((f) => f.level === 'error') ? 1 : 0)
88
+ console.log(JSON.stringify({ file, findings, summary, sections }, null, 2))
89
+ process.exit(summary.errors ? 1 : 0)
45
90
  }
46
91
 
47
92
  if (!findings.length) {
@@ -53,6 +98,6 @@ export const lint = {
53
98
  const tag = f.level === 'error' ? kleur.red('✗ error') : kleur.yellow('⚠ warn ')
54
99
  console.log(`${tag} ${kleur.dim(f.code)} ${f.msg}`)
55
100
  }
56
- if (findings.some((f) => f.level === 'error')) process.exit(1)
101
+ if (summary.errors) process.exit(1)
57
102
  },
58
103
  }
@@ -37,9 +37,14 @@ function parseFlags(args) {
37
37
  return { flags, positional }
38
38
  }
39
39
 
40
- function which(cmd) {
41
- const r = spawnSync('which', [cmd], { encoding: 'utf8' })
42
- return r.status === 0 ? r.stdout.trim() : null
40
+ // Cross-platform tool detection: probe the tool itself rather than relying on
41
+ // POSIX `which`. Native Windows shells (cmd.exe / PowerShell) have no `which`
42
+ // (they use `where`), so `spawnSync('which', ...)` returns ENOENT and would
43
+ // wrongly report an installed tool as missing. Running `<cmd> --version` and
44
+ // checking exit 0 (with no spawn error) works on macOS, Linux, and Windows.
45
+ function isInstalled(cmd) {
46
+ const r = spawnSync(cmd, ['--version'], { encoding: 'utf8' })
47
+ return !r.error && r.status === 0
43
48
  }
44
49
 
45
50
  function run(cmd, args, opts = {}) {
@@ -113,9 +118,8 @@ export const submit = {
113
118
 
114
119
  console.log(kleur.dim(`→ Submitting ${kleur.bold(data.name)} as slug "${kleur.bold(slug)}"`))
115
120
 
116
- // 3. Check `gh` CLI
117
- const ghPath = which('gh')
118
- if (!ghPath) {
121
+ // 3. Check `gh` CLI (cross-platform; spawn resolves `gh` from PATH later)
122
+ if (!isInstalled('gh')) {
119
123
  console.error()
120
124
  console.error(kleur.yellow('⚠ The GitHub CLI (`gh`) is not installed.'))
121
125
  console.error()
@@ -13,8 +13,21 @@ function parseFlags(args) {
13
13
  return { flags, positional }
14
14
  }
15
15
 
16
+ function flattenColors(c) {
17
+ // Multi-theme: pick the default theme (or first one) so we invert the
18
+ // actual token strings rather than copying theme objects verbatim.
19
+ // Mirrors flattenColors in export.mjs.
20
+ const values = Object.values(c)
21
+ if (values.length > 0 && values.every((v) => v && typeof v === 'object')) {
22
+ return values[0]
23
+ }
24
+ return c
25
+ }
26
+
16
27
  function hexToRgb(h) {
17
- const m = h.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
28
+ // Expand 3-digit shorthand (e.g. `fff` → `ffffff`) before matching.
29
+ const expanded = h.replace(/^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i, '#$1$1$2$2$3$3')
30
+ const m = expanded.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
18
31
  if (!m) return null
19
32
  return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)]
20
33
  }
@@ -43,7 +56,7 @@ export const theme = {
43
56
 
44
57
  const raw = await fetchRawMd(slug)
45
58
  const { data } = parseDesignMd(raw)
46
- const c = data.colors ?? {}
59
+ const c = flattenColors(data.colors ?? {})
47
60
  const flipped = {}
48
61
  for (const [k, v] of Object.entries(c)) {
49
62
  if (typeof v === 'string' && v.startsWith('#')) flipped[k] = invertLuma(v)
@@ -52,7 +65,10 @@ export const theme = {
52
65
  console.log(kleur.dim(`# ${data.name} · ${flags.dark ? 'dark' : 'light'} variant (computed)`))
53
66
  console.log()
54
67
  console.log('colors:')
55
- for (const [k, v] of Object.entries(flipped)) console.log(` ${k}: '${v}'`)
68
+ for (const [k, v] of Object.entries(flipped)) {
69
+ if (typeof v !== 'string') continue
70
+ console.log(` ${k}: '${v}'`)
71
+ }
56
72
  console.log()
57
73
  console.log(kleur.yellow('Note: this is a naive luminance flip. Refine the result by hand for production.'))
58
74
  },
package/src/index.mjs CHANGED
@@ -26,7 +26,7 @@ const COMMANDS = {
26
26
  diff,
27
27
  export: exportTokens,
28
28
  extract,
29
- import: extract, // alias — name aligned with Google Labs PR #40
29
+ import: extract, // alias — `import` reads natural for "bring this in"
30
30
  theme,
31
31
  preview,
32
32
  submit,