@webdesignhot/design-md 0.1.0
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/LICENSE +21 -0
- package/README.md +83 -0
- package/bin/design-md.mjs +7 -0
- package/package.json +31 -0
- package/src/commands/add.mjs +41 -0
- package/src/commands/category.mjs +31 -0
- package/src/commands/diff.mjs +65 -0
- package/src/commands/export.mjs +91 -0
- package/src/commands/extract.mjs +34 -0
- package/src/commands/help.mjs +35 -0
- package/src/commands/init.mjs +63 -0
- package/src/commands/lint.mjs +58 -0
- package/src/commands/list.mjs +24 -0
- package/src/commands/preview.mjs +31 -0
- package/src/commands/theme.mjs +59 -0
- package/src/index.mjs +45 -0
- package/src/lib/api.mjs +30 -0
- package/src/lib/parse.mjs +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 webdesignhot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# design-md
|
|
2
|
+
|
|
3
|
+
> Drop any DESIGN.md into your repo in one command. Browse, install, lint, diff, and export design systems extracted from real production sites.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
$ npx @webdesignhot/design-md add linear
|
|
7
|
+
✓ Wrote DESIGN.md (8.2 KB · linear)
|
|
8
|
+
|
|
9
|
+
$ npx @webdesignhot/design-md list
|
|
10
|
+
★ agentkit AgentKit dark editorial sans dense
|
|
11
|
+
★ anthropic Anthropic light editorial serif
|
|
12
|
+
arc Arc light warm sans
|
|
13
|
+
…
|
|
14
|
+
|
|
15
|
+
$ npx @webdesignhot/design-md export DESIGN.md --to tailwind > tailwind.theme.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A DESIGN.md is a single file that captures a brand's design system as a YAML head + markdown body — engineered to be read by AI coding agents. This CLI is the fastest way to get one into your project.
|
|
21
|
+
|
|
22
|
+
The catalog at [webdesignhot.com/design.md](https://www.webdesignhot.com/design.md/) ships real-brand design systems extracted from production sites — Linear, Stripe, Vercel, Anthropic, Notion, and dozens more. One command and you have a token-rich style guide your agent can follow.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# one-off
|
|
28
|
+
npx @webdesignhot/design-md <command>
|
|
29
|
+
|
|
30
|
+
# global
|
|
31
|
+
npm i -g @webdesignhot/design-md
|
|
32
|
+
# the binary is `design-md` regardless of scope:
|
|
33
|
+
design-md add linear
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Node 18+.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
add <slug> [-o, --out <path>] [-f, --force]
|
|
42
|
+
Write the chosen DESIGN.md to your CWD.
|
|
43
|
+
|
|
44
|
+
list
|
|
45
|
+
Print the full catalog (slug · name · tags).
|
|
46
|
+
|
|
47
|
+
category [name]
|
|
48
|
+
Without a name, lists every category with a count.
|
|
49
|
+
With a name, lists every design in that category.
|
|
50
|
+
|
|
51
|
+
init
|
|
52
|
+
Interactive picker (default if no command given).
|
|
53
|
+
|
|
54
|
+
lint <file> [--format=text|json]
|
|
55
|
+
Validate a DESIGN.md for spec compliance.
|
|
56
|
+
|
|
57
|
+
diff <a> <b> [--format=text|json]
|
|
58
|
+
Token-level diff between two DESIGN.md files.
|
|
59
|
+
|
|
60
|
+
export <file> --to <tailwind|css|dtcg|figma>
|
|
61
|
+
Convert tokens to one of:
|
|
62
|
+
tailwind — theme.extend block for tailwind.config.js
|
|
63
|
+
css — :root { --color-bg, --radius-card, … }
|
|
64
|
+
dtcg — W3C Design Tokens Community Group JSON
|
|
65
|
+
figma — Figma Variables import format
|
|
66
|
+
|
|
67
|
+
extract <url> [-o <path>] [--token-only]
|
|
68
|
+
Extract a draft DESIGN.md from any production URL.
|
|
69
|
+
(Requires a webdesignhot session — opens the browser flow.)
|
|
70
|
+
|
|
71
|
+
theme <slug> [--dark|--light]
|
|
72
|
+
Compute a dark/light variant of any design.
|
|
73
|
+
|
|
74
|
+
preview <slug>
|
|
75
|
+
Open the directory detail page in your browser.
|
|
76
|
+
|
|
77
|
+
help
|
|
78
|
+
Print the full command list.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT — © 2026 webdesignhot
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webdesignhot/design-md",
|
|
3
|
+
"version": "0.1.0",
|
|
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"],
|
|
6
|
+
"author": "webdesignhot",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://www.webdesignhot.com/design.md/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/WebDesignHot/design-md"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"design-md": "bin/design-md.mjs"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@clack/prompts": "^0.7.0",
|
|
28
|
+
"kleur": "^4.1.5",
|
|
29
|
+
"yaml": "^2.5.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { writeFile, access } from 'node:fs/promises'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import kleur from 'kleur'
|
|
4
|
+
import { fetchRawMd } from '../lib/api.mjs'
|
|
5
|
+
|
|
6
|
+
function parseFlags(args) {
|
|
7
|
+
const flags = { out: 'DESIGN.md', force: false }
|
|
8
|
+
const positional = []
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
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
|
|
13
|
+
else positional.push(a)
|
|
14
|
+
}
|
|
15
|
+
return { flags, positional }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const add = {
|
|
19
|
+
usage: 'add <slug> [-o, --out <path>] [-f, --force]',
|
|
20
|
+
async run(args) {
|
|
21
|
+
const { flags, positional } = parseFlags(args)
|
|
22
|
+
const slug = positional[0]
|
|
23
|
+
if (!slug) {
|
|
24
|
+
console.error('Usage: design-md add <slug> [-o <path>] [-f]')
|
|
25
|
+
process.exit(2)
|
|
26
|
+
}
|
|
27
|
+
const out = resolve(process.cwd(), flags.out)
|
|
28
|
+
if (!flags.force) {
|
|
29
|
+
try {
|
|
30
|
+
await access(out)
|
|
31
|
+
console.error(kleur.red(`✗ ${flags.out} already exists. Pass --force to overwrite.`))
|
|
32
|
+
process.exit(1)
|
|
33
|
+
} catch {
|
|
34
|
+
// doesn't exist — proceed
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const md = await fetchRawMd(slug)
|
|
38
|
+
await writeFile(out, md, 'utf8')
|
|
39
|
+
console.log(kleur.green(`✓ Wrote ${flags.out}`) + kleur.dim(` (${(md.length / 1024).toFixed(1)} KB · ${slug})`))
|
|
40
|
+
},
|
|
41
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
import { fetchCatalog } from '../lib/api.mjs'
|
|
3
|
+
|
|
4
|
+
export const category = {
|
|
5
|
+
usage: 'category [name]',
|
|
6
|
+
async run(args) {
|
|
7
|
+
const catalog = await fetchCatalog()
|
|
8
|
+
const name = args[0]
|
|
9
|
+
if (!name) {
|
|
10
|
+
const counts = new Map()
|
|
11
|
+
for (const e of catalog.entries)
|
|
12
|
+
for (const c of e.categories ?? []) counts.set(c, (counts.get(c) ?? 0) + 1)
|
|
13
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1])
|
|
14
|
+
const w = Math.max(...sorted.map(([c]) => c.length), 4)
|
|
15
|
+
for (const [c, n] of sorted) {
|
|
16
|
+
console.log(`${kleur.cyan(c.padEnd(w))} ${kleur.dim(String(n).padStart(3))} designs`)
|
|
17
|
+
}
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const matches = catalog.entries.filter((e) => (e.categories ?? []).includes(name))
|
|
21
|
+
if (!matches.length) {
|
|
22
|
+
console.error(kleur.red(`No designs in category "${name}".`))
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
const w = Math.max(...matches.map((e) => e.slug.length), 4)
|
|
26
|
+
for (const e of matches) {
|
|
27
|
+
console.log(`${kleur.cyan(e.slug.padEnd(w))} ${e.name} ${kleur.dim((e.tags ?? []).join(' '))}`)
|
|
28
|
+
}
|
|
29
|
+
console.log(kleur.dim(`\n${matches.length} designs in category "${name}"`))
|
|
30
|
+
},
|
|
31
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import kleur from 'kleur'
|
|
3
|
+
import { readDesignMd, flattenColors } from '../lib/parse.mjs'
|
|
4
|
+
|
|
5
|
+
function parseFlags(args) {
|
|
6
|
+
const flags = { format: 'text' }
|
|
7
|
+
const positional = []
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const a = args[i]
|
|
10
|
+
if (a.startsWith('--format=')) flags.format = a.slice(9)
|
|
11
|
+
else if (a === '--format') flags.format = args[++i]
|
|
12
|
+
else positional.push(a)
|
|
13
|
+
}
|
|
14
|
+
return { flags, positional }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function diffMaps(left, right) {
|
|
18
|
+
const result = { added: {}, removed: {}, modified: {} }
|
|
19
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)])
|
|
20
|
+
for (const k of keys) {
|
|
21
|
+
const a = left[k]
|
|
22
|
+
const b = right[k]
|
|
23
|
+
if (a == null && b != null) result.added[k] = b
|
|
24
|
+
else if (a != null && b == null) result.removed[k] = a
|
|
25
|
+
else if (a !== b) result.modified[k] = { from: a, to: b }
|
|
26
|
+
}
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const diff = {
|
|
31
|
+
usage: 'diff <a> <b> [--format=text|json]',
|
|
32
|
+
async run(args) {
|
|
33
|
+
const { flags, positional } = parseFlags(args)
|
|
34
|
+
const [a, b] = positional
|
|
35
|
+
if (!a || !b) {
|
|
36
|
+
console.error('Usage: design-md diff <a> <b>')
|
|
37
|
+
process.exit(2)
|
|
38
|
+
}
|
|
39
|
+
const A = await readDesignMd(resolve(process.cwd(), a))
|
|
40
|
+
const B = await readDesignMd(resolve(process.cwd(), b))
|
|
41
|
+
const colors = diffMaps(flattenColors(A.data), flattenColors(B.data))
|
|
42
|
+
const radii = diffMaps(A.data.radius ?? {}, B.data.radius ?? {})
|
|
43
|
+
|
|
44
|
+
if (flags.format === 'json') {
|
|
45
|
+
console.log(JSON.stringify({ colors, radii }, null, 2))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printSection(title, d) {
|
|
50
|
+
console.log(kleur.bold(`\n${title}`))
|
|
51
|
+
const empty = !Object.keys(d.added).length && !Object.keys(d.removed).length && !Object.keys(d.modified).length
|
|
52
|
+
if (empty) {
|
|
53
|
+
console.log(kleur.dim(' no changes'))
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
for (const [k, v] of Object.entries(d.added)) console.log(kleur.green(` + ${k}`) + kleur.dim(` = ${v}`))
|
|
57
|
+
for (const [k, v] of Object.entries(d.removed)) console.log(kleur.red(` − ${k}`) + kleur.dim(` (was ${v})`))
|
|
58
|
+
for (const [k, v] of Object.entries(d.modified))
|
|
59
|
+
console.log(kleur.yellow(` ~ ${k}`) + kleur.dim(` ${v.from} → `) + kleur.cyan(v.to))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
printSection('Colors', colors)
|
|
63
|
+
printSection('Radii', radii)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { readDesignMd } from '../lib/parse.mjs'
|
|
3
|
+
|
|
4
|
+
const FORMATS = new Set(['tailwind', 'css', 'dtcg', 'figma'])
|
|
5
|
+
|
|
6
|
+
function parseFlags(args) {
|
|
7
|
+
const flags = { to: null }
|
|
8
|
+
const positional = []
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const a = args[i]
|
|
11
|
+
if (a.startsWith('--to=')) flags.to = a.slice(5)
|
|
12
|
+
else if (a === '--to') flags.to = args[++i]
|
|
13
|
+
else positional.push(a)
|
|
14
|
+
}
|
|
15
|
+
return { flags, positional }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toCss(data) {
|
|
19
|
+
const c = data.colors ?? {}
|
|
20
|
+
const r = data.radius ?? {}
|
|
21
|
+
const lines = [':root {']
|
|
22
|
+
for (const [k, v] of Object.entries(c)) lines.push(` --color-${k}: ${v};`)
|
|
23
|
+
for (const [k, v] of Object.entries(r)) lines.push(` --radius-${k}: ${typeof v === 'number' ? `${v}px` : v};`)
|
|
24
|
+
lines.push('}')
|
|
25
|
+
return lines.join('\n')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toTailwind(data) {
|
|
29
|
+
const c = data.colors ?? {}
|
|
30
|
+
const r = data.radius ?? {}
|
|
31
|
+
const out = {
|
|
32
|
+
theme: {
|
|
33
|
+
extend: {
|
|
34
|
+
colors: Object.fromEntries(Object.entries(c).map(([k, v]) => [k, String(v)])),
|
|
35
|
+
borderRadius: Object.fromEntries(
|
|
36
|
+
Object.entries(r).map(([k, v]) => [k, typeof v === 'number' ? `${v}px` : v]),
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
return `module.exports = ${JSON.stringify(out, null, 2)}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toDtcg(data) {
|
|
45
|
+
const c = data.colors ?? {}
|
|
46
|
+
const r = data.radius ?? {}
|
|
47
|
+
const out = {
|
|
48
|
+
$schema: 'https://design-tokens.github.io/community-group/format/',
|
|
49
|
+
color: Object.fromEntries(Object.entries(c).map(([k, v]) => [k, { $value: String(v), $type: 'color' }])),
|
|
50
|
+
radius: Object.fromEntries(
|
|
51
|
+
Object.entries(r).map(([k, v]) => [
|
|
52
|
+
k,
|
|
53
|
+
{ $value: typeof v === 'number' ? `${v}px` : v, $type: 'dimension' },
|
|
54
|
+
]),
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(out, null, 2)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toFigma(data) {
|
|
61
|
+
const c = data.colors ?? {}
|
|
62
|
+
const out = {
|
|
63
|
+
variables: Object.entries(c).map(([k, v]) => ({
|
|
64
|
+
name: k,
|
|
65
|
+
type: 'COLOR',
|
|
66
|
+
value: String(v),
|
|
67
|
+
description: `${data.name} · ${k}`,
|
|
68
|
+
})),
|
|
69
|
+
}
|
|
70
|
+
return JSON.stringify(out, null, 2)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const RENDERERS = { tailwind: toTailwind, css: toCss, dtcg: toDtcg, figma: toFigma }
|
|
74
|
+
|
|
75
|
+
export const exportTokens = {
|
|
76
|
+
usage: 'export <file> --to <tailwind|css|dtcg|figma>',
|
|
77
|
+
async run(args) {
|
|
78
|
+
const { flags, positional } = parseFlags(args)
|
|
79
|
+
const file = positional[0]
|
|
80
|
+
if (!file || !flags.to) {
|
|
81
|
+
console.error('Usage: design-md export <file> --to <tailwind|css|dtcg|figma>')
|
|
82
|
+
process.exit(2)
|
|
83
|
+
}
|
|
84
|
+
if (!FORMATS.has(flags.to)) {
|
|
85
|
+
console.error(`Unknown format "${flags.to}". Choose: ${[...FORMATS].join(', ')}`)
|
|
86
|
+
process.exit(2)
|
|
87
|
+
}
|
|
88
|
+
const { data } = await readDesignMd(resolve(process.cwd(), file))
|
|
89
|
+
process.stdout.write(RENDERERS[flags.to](data) + '\n')
|
|
90
|
+
},
|
|
91
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
import { BASE } from '../lib/api.mjs'
|
|
3
|
+
|
|
4
|
+
function parseFlags(args) {
|
|
5
|
+
const flags = { out: null, tokenOnly: false }
|
|
6
|
+
const positional = []
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const a = args[i]
|
|
9
|
+
if (a === '-o' || a === '--out') flags.out = args[++i]
|
|
10
|
+
else if (a === '--token-only') flags.tokenOnly = true
|
|
11
|
+
else positional.push(a)
|
|
12
|
+
}
|
|
13
|
+
return { flags, positional }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const extract = {
|
|
17
|
+
usage: 'extract <url> [-o <path>] [--token-only]',
|
|
18
|
+
async run(args) {
|
|
19
|
+
const { flags, positional } = parseFlags(args)
|
|
20
|
+
const url = positional[0]
|
|
21
|
+
if (!url) {
|
|
22
|
+
console.error('Usage: design-md extract <url>')
|
|
23
|
+
process.exit(2)
|
|
24
|
+
}
|
|
25
|
+
console.log(kleur.dim(`→ Submitting extraction for ${url}…`))
|
|
26
|
+
console.log(kleur.dim(` AI extraction is processed by webdesignhot — opening the create flow.`))
|
|
27
|
+
console.log()
|
|
28
|
+
const target = `${BASE}/design-md/create?url=${encodeURIComponent(url)}${flags.tokenOnly ? '&tokens=1' : ''}`
|
|
29
|
+
console.log(` ${kleur.cyan(target)}`)
|
|
30
|
+
console.log()
|
|
31
|
+
console.log(kleur.yellow('Tip: extract requires a webdesignhot session — visit the URL above to authorize.'))
|
|
32
|
+
console.log(kleur.dim(' The CLI will gain a direct extraction endpoint in v0.2.'))
|
|
33
|
+
},
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
|
|
3
|
+
const TEXT = `
|
|
4
|
+
${kleur.bold('design-md')} — drop any DESIGN.md into your repo in one command
|
|
5
|
+
|
|
6
|
+
${kleur.dim('USAGE')}
|
|
7
|
+
design-md <command> [args]
|
|
8
|
+
|
|
9
|
+
${kleur.dim('COMMANDS')}
|
|
10
|
+
add <slug> Write the design's DESIGN.md to your CWD
|
|
11
|
+
list Print the full catalog (slug · name · tags)
|
|
12
|
+
category [name] Browse categories — without a name lists all
|
|
13
|
+
init Interactive picker (default if no command)
|
|
14
|
+
lint <file> Validate a DESIGN.md for spec compliance
|
|
15
|
+
diff <a> <b> Token-level diff between two DESIGN.md files
|
|
16
|
+
export <file> --to <f> Convert tokens to tailwind|css|dtcg|figma
|
|
17
|
+
extract <url> Extract a draft DESIGN.md from any production URL
|
|
18
|
+
theme <slug> --dark Compute a dark-mode counterpart of a light design
|
|
19
|
+
preview <slug> Open the directory detail page in your browser
|
|
20
|
+
help This screen
|
|
21
|
+
|
|
22
|
+
${kleur.dim('EXAMPLES')}
|
|
23
|
+
$ design-md add linear ${kleur.dim('# writes DESIGN.md to CWD')}
|
|
24
|
+
$ design-md list | grep editorial
|
|
25
|
+
$ design-md export DESIGN.md --to tailwind > tailwind.theme.js
|
|
26
|
+
$ design-md diff DESIGN.md old.md
|
|
27
|
+
|
|
28
|
+
${kleur.dim('Spec:')} https://github.com/google-labs-code/design.md
|
|
29
|
+
${kleur.dim('Catalog:')} https://www.webdesignhot.com/design-md
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
export const help = {
|
|
33
|
+
run: async () => process.stdout.write(TEXT),
|
|
34
|
+
usage: 'help',
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as p from '@clack/prompts'
|
|
2
|
+
import { writeFile, access } from 'node:fs/promises'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import kleur from 'kleur'
|
|
5
|
+
import { fetchCatalog, fetchRawMd } from '../lib/api.mjs'
|
|
6
|
+
|
|
7
|
+
export const init = {
|
|
8
|
+
usage: 'init',
|
|
9
|
+
async run() {
|
|
10
|
+
p.intro(kleur.bold('design-md init'))
|
|
11
|
+
|
|
12
|
+
const s = p.spinner()
|
|
13
|
+
s.start('Loading catalog…')
|
|
14
|
+
const catalog = await fetchCatalog()
|
|
15
|
+
s.stop(`${catalog.count} designs available`)
|
|
16
|
+
|
|
17
|
+
const slug = await p.select({
|
|
18
|
+
message: 'Pick a design',
|
|
19
|
+
maxItems: 12,
|
|
20
|
+
options: catalog.entries.map((e) => ({
|
|
21
|
+
value: e.slug,
|
|
22
|
+
label: `${e.featured ? '★ ' : ' '}${e.name}`,
|
|
23
|
+
hint: (e.tags ?? []).slice(0, 4).join(' · '),
|
|
24
|
+
})),
|
|
25
|
+
})
|
|
26
|
+
if (p.isCancel(slug)) {
|
|
27
|
+
p.cancel('Cancelled.')
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const out = await p.text({
|
|
32
|
+
message: 'Write to',
|
|
33
|
+
placeholder: 'DESIGN.md',
|
|
34
|
+
defaultValue: 'DESIGN.md',
|
|
35
|
+
})
|
|
36
|
+
if (p.isCancel(out)) {
|
|
37
|
+
p.cancel('Cancelled.')
|
|
38
|
+
process.exit(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const target = resolve(process.cwd(), out || 'DESIGN.md')
|
|
42
|
+
let exists = false
|
|
43
|
+
try {
|
|
44
|
+
await access(target)
|
|
45
|
+
exists = true
|
|
46
|
+
} catch {}
|
|
47
|
+
if (exists) {
|
|
48
|
+
const overwrite = await p.confirm({ message: `${out} exists — overwrite?`, initialValue: false })
|
|
49
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
50
|
+
p.cancel('Cancelled.')
|
|
51
|
+
process.exit(0)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const s2 = p.spinner()
|
|
56
|
+
s2.start(`Fetching ${slug}…`)
|
|
57
|
+
const md = await fetchRawMd(slug)
|
|
58
|
+
await writeFile(target, md, 'utf8')
|
|
59
|
+
s2.stop(`Wrote ${out} (${(md.length / 1024).toFixed(1)} KB)`)
|
|
60
|
+
|
|
61
|
+
p.outro(kleur.green(`✓ ${slug} installed.`) + kleur.dim(' Point your AI agent at DESIGN.md and you are done.'))
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import kleur from 'kleur'
|
|
3
|
+
import { readDesignMd } from '../lib/parse.mjs'
|
|
4
|
+
|
|
5
|
+
const REQUIRED = ['name', 'tagline', 'spec', 'colors', 'typography']
|
|
6
|
+
const SECTIONS_RE = /^##\s+(.+)$/gm
|
|
7
|
+
|
|
8
|
+
function parseFlags(args) {
|
|
9
|
+
const flags = { format: 'text' }
|
|
10
|
+
const positional = []
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const a = args[i]
|
|
13
|
+
if (a.startsWith('--format=')) flags.format = a.slice(9)
|
|
14
|
+
else if (a === '--format') flags.format = args[++i]
|
|
15
|
+
else positional.push(a)
|
|
16
|
+
}
|
|
17
|
+
return { flags, positional }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const lint = {
|
|
21
|
+
usage: 'lint <file> [--format=text|json]',
|
|
22
|
+
async run(args) {
|
|
23
|
+
const { flags, positional } = parseFlags(args)
|
|
24
|
+
const file = positional[0]
|
|
25
|
+
if (!file) {
|
|
26
|
+
console.error('Usage: design-md lint <file>')
|
|
27
|
+
process.exit(2)
|
|
28
|
+
}
|
|
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.` })
|
|
34
|
+
}
|
|
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
|
+
|
|
42
|
+
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)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!findings.length) {
|
|
48
|
+
console.log(kleur.green('✓ No issues found.'))
|
|
49
|
+
console.log(kleur.dim(` ${sections.length} sections · ${Object.keys(data.colors ?? {}).length} colors · ${Object.keys(data.typography ?? {}).length} type roles`))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
for (const f of findings) {
|
|
53
|
+
const tag = f.level === 'error' ? kleur.red('✗ error') : kleur.yellow('⚠ warn ')
|
|
54
|
+
console.log(`${tag} ${kleur.dim(f.code)} ${f.msg}`)
|
|
55
|
+
}
|
|
56
|
+
if (findings.some((f) => f.level === 'error')) process.exit(1)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
import { fetchCatalog } from '../lib/api.mjs'
|
|
3
|
+
|
|
4
|
+
export const list = {
|
|
5
|
+
usage: 'list',
|
|
6
|
+
async run() {
|
|
7
|
+
const catalog = await fetchCatalog()
|
|
8
|
+
const rows = catalog.entries.map((e) => ({
|
|
9
|
+
slug: e.slug,
|
|
10
|
+
name: e.name,
|
|
11
|
+
tags: (e.tags ?? []).slice(0, 4).join(' '),
|
|
12
|
+
featured: e.featured ? '★' : ' ',
|
|
13
|
+
}))
|
|
14
|
+
const slugW = Math.max(...rows.map((r) => r.slug.length), 4)
|
|
15
|
+
const nameW = Math.max(...rows.map((r) => r.name.length), 4)
|
|
16
|
+
for (const r of rows) {
|
|
17
|
+
const fav = r.featured === '★' ? kleur.yellow('★') : ' '
|
|
18
|
+
console.log(
|
|
19
|
+
`${fav} ${kleur.cyan(r.slug.padEnd(slugW))} ${r.name.padEnd(nameW)} ${kleur.dim(r.tags)}`,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
console.log(kleur.dim(`\n${catalog.count} designs`))
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import kleur from 'kleur'
|
|
3
|
+
import { BASE } from '../lib/api.mjs'
|
|
4
|
+
|
|
5
|
+
function opener() {
|
|
6
|
+
if (process.platform === 'darwin') return ['open', []]
|
|
7
|
+
if (process.platform === 'win32') return ['cmd', ['/c', 'start', '']]
|
|
8
|
+
return ['xdg-open', []]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SAFE_SLUG = /^[a-z0-9][a-z0-9-]{0,80}$/
|
|
12
|
+
|
|
13
|
+
export const preview = {
|
|
14
|
+
usage: 'preview <slug>',
|
|
15
|
+
async run(args) {
|
|
16
|
+
const slug = args[0]
|
|
17
|
+
if (!slug || !SAFE_SLUG.test(slug)) {
|
|
18
|
+
console.error('Usage: design-md preview <slug> (slug = lowercase, hyphenated)')
|
|
19
|
+
process.exit(2)
|
|
20
|
+
}
|
|
21
|
+
const url = `${BASE}/design-md/${slug}/`
|
|
22
|
+
console.log(kleur.dim(`→ Opening ${url}`))
|
|
23
|
+
const [bin, head] = opener()
|
|
24
|
+
execFile(bin, [...head, url], (err) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
console.error(kleur.red('Could not launch browser. Open manually:'))
|
|
27
|
+
console.log(` ${url}`)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
},
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import kleur from 'kleur'
|
|
2
|
+
import { fetchRawMd } from '../lib/api.mjs'
|
|
3
|
+
import { parseDesignMd } from '../lib/parse.mjs'
|
|
4
|
+
|
|
5
|
+
function parseFlags(args) {
|
|
6
|
+
const flags = { dark: false, light: false }
|
|
7
|
+
const positional = []
|
|
8
|
+
for (const a of args) {
|
|
9
|
+
if (a === '--dark') flags.dark = true
|
|
10
|
+
else if (a === '--light') flags.light = true
|
|
11
|
+
else positional.push(a)
|
|
12
|
+
}
|
|
13
|
+
return { flags, positional }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hexToRgb(h) {
|
|
17
|
+
const m = h.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
|
|
18
|
+
if (!m) return null
|
|
19
|
+
return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)]
|
|
20
|
+
}
|
|
21
|
+
function rgbToHex(r, g, b) {
|
|
22
|
+
const c = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, '0')
|
|
23
|
+
return `#${c(r)}${c(g)}${c(b)}`
|
|
24
|
+
}
|
|
25
|
+
function invertLuma(hex) {
|
|
26
|
+
const rgb = hexToRgb(hex)
|
|
27
|
+
if (!rgb) return hex
|
|
28
|
+
const [r, g, b] = rgb
|
|
29
|
+
const inv = (n) => 255 - n
|
|
30
|
+
return rgbToHex(inv(r), inv(g), inv(b))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const theme = {
|
|
34
|
+
usage: 'theme <slug> [--dark|--light]',
|
|
35
|
+
async run(args) {
|
|
36
|
+
const { flags, positional } = parseFlags(args)
|
|
37
|
+
const slug = positional[0]
|
|
38
|
+
if (!slug) {
|
|
39
|
+
console.error('Usage: design-md theme <slug> [--dark|--light]')
|
|
40
|
+
process.exit(2)
|
|
41
|
+
}
|
|
42
|
+
if (!flags.dark && !flags.light) flags.dark = true
|
|
43
|
+
|
|
44
|
+
const raw = await fetchRawMd(slug)
|
|
45
|
+
const { data } = parseDesignMd(raw)
|
|
46
|
+
const c = data.colors ?? {}
|
|
47
|
+
const flipped = {}
|
|
48
|
+
for (const [k, v] of Object.entries(c)) {
|
|
49
|
+
if (typeof v === 'string' && v.startsWith('#')) flipped[k] = invertLuma(v)
|
|
50
|
+
else flipped[k] = v
|
|
51
|
+
}
|
|
52
|
+
console.log(kleur.dim(`# ${data.name} · ${flags.dark ? 'dark' : 'light'} variant (computed)`))
|
|
53
|
+
console.log()
|
|
54
|
+
console.log('colors:')
|
|
55
|
+
for (const [k, v] of Object.entries(flipped)) console.log(` ${k}: '${v}'`)
|
|
56
|
+
console.log()
|
|
57
|
+
console.log(kleur.yellow('Note: this is a naive luminance flip. Refine the result by hand for production.'))
|
|
58
|
+
},
|
|
59
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-md CLI router.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches the first positional arg to a command module under
|
|
5
|
+
* `./commands/`. Each command exports `run(args)` and a `usage` string.
|
|
6
|
+
*/
|
|
7
|
+
import { add } from './commands/add.mjs'
|
|
8
|
+
import { list } from './commands/list.mjs'
|
|
9
|
+
import { category } from './commands/category.mjs'
|
|
10
|
+
import { init } from './commands/init.mjs'
|
|
11
|
+
import { lint } from './commands/lint.mjs'
|
|
12
|
+
import { diff } from './commands/diff.mjs'
|
|
13
|
+
import { exportTokens } from './commands/export.mjs'
|
|
14
|
+
import { extract } from './commands/extract.mjs'
|
|
15
|
+
import { theme } from './commands/theme.mjs'
|
|
16
|
+
import { preview } from './commands/preview.mjs'
|
|
17
|
+
import { help } from './commands/help.mjs'
|
|
18
|
+
|
|
19
|
+
const COMMANDS = {
|
|
20
|
+
add,
|
|
21
|
+
list,
|
|
22
|
+
category,
|
|
23
|
+
init,
|
|
24
|
+
lint,
|
|
25
|
+
diff,
|
|
26
|
+
export: exportTokens,
|
|
27
|
+
extract,
|
|
28
|
+
theme,
|
|
29
|
+
preview,
|
|
30
|
+
help,
|
|
31
|
+
'--help': help,
|
|
32
|
+
'-h': help,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function run(argv) {
|
|
36
|
+
const [cmd, ...rest] = argv
|
|
37
|
+
if (!cmd) return init.run(rest)
|
|
38
|
+
const handler = COMMANDS[cmd]
|
|
39
|
+
if (!handler) {
|
|
40
|
+
console.error(`Unknown command: ${cmd}`)
|
|
41
|
+
console.error(`Run \`design-md help\` to see all commands.`)
|
|
42
|
+
process.exit(2)
|
|
43
|
+
}
|
|
44
|
+
return handler.run(rest)
|
|
45
|
+
}
|
package/src/lib/api.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client — fetches catalog and per-slug raw markdown from
|
|
3
|
+
* webdesignhot.com. The endpoints are prerendered + immutable so
|
|
4
|
+
* latency is essentially the CDN's.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_BASE = 'https://www.webdesignhot.com'
|
|
7
|
+
export const BASE = process.env.DESIGN_MD_BASE || DEFAULT_BASE
|
|
8
|
+
|
|
9
|
+
let _catalog = null
|
|
10
|
+
|
|
11
|
+
export async function fetchCatalog() {
|
|
12
|
+
if (_catalog) return _catalog
|
|
13
|
+
const res = await fetch(`${BASE}/api/design-md/index.json`)
|
|
14
|
+
if (!res.ok) throw new Error(`Catalog fetch failed: ${res.status}`)
|
|
15
|
+
_catalog = await res.json()
|
|
16
|
+
return _catalog
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchRawMd(slug) {
|
|
20
|
+
const res = await fetch(`${BASE}/api/design-md/${slug}.md`)
|
|
21
|
+
if (res.status === 404) throw new Error(`No design with slug "${slug}". Run \`design-md list\` to see available designs.`)
|
|
22
|
+
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`)
|
|
23
|
+
return res.text()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function fetchExport(slug, format) {
|
|
27
|
+
const res = await fetch(`${BASE}/api/design-md/${slug}/export/${format}`)
|
|
28
|
+
if (!res.ok) throw new Error(`Export ${format} failed: ${res.status}`)
|
|
29
|
+
return res.text()
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter parsing helpers. The DESIGN.md/v1 spec puts all tokens
|
|
3
|
+
* in the YAML head; the markdown body is human prose. We use the
|
|
4
|
+
* `yaml` package for round-trip-safe parsing.
|
|
5
|
+
*/
|
|
6
|
+
import YAML from 'yaml'
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
|
|
9
|
+
const FM_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/
|
|
10
|
+
|
|
11
|
+
export function parseDesignMd(source) {
|
|
12
|
+
const m = source.match(FM_RE)
|
|
13
|
+
if (!m) throw new Error('No frontmatter block found (expected leading `---`).')
|
|
14
|
+
const data = YAML.parse(m[1]) ?? {}
|
|
15
|
+
const body = m[2] ?? ''
|
|
16
|
+
return { data, body, raw: source }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readDesignMd(filePath) {
|
|
20
|
+
const text = await readFile(filePath, 'utf8')
|
|
21
|
+
return parseDesignMd(text)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function flattenColors(data) {
|
|
25
|
+
const out = {}
|
|
26
|
+
if (!data?.colors) return out
|
|
27
|
+
for (const [k, v] of Object.entries(data.colors)) out[k] = String(v)
|
|
28
|
+
return out
|
|
29
|
+
}
|