@webdesignhot/design-md 0.1.0 → 0.4.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
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
4
 
5
5
  ```bash
6
- $ npx @webdesignhot/design-md add linear
6
+ $ npx @webdesignhot/design-md add stripe
7
7
  ✓ Wrote DESIGN.md (8.2 KB · linear)
8
8
 
9
9
  $ npx @webdesignhot/design-md list
@@ -12,7 +12,8 @@ $ npx @webdesignhot/design-md list
12
12
  arc Arc light warm sans
13
13
 
14
14
 
15
- $ npx @webdesignhot/design-md export DESIGN.md --to tailwind > tailwind.theme.js
15
+ $ npx @webdesignhot/design-md export DESIGN.md --to tailwind > theme.css # Tailwind v4 default
16
+ $ npx @webdesignhot/design-md export DESIGN.md --to tailwind --tailwind-version v3 > tailwind.config.js
16
17
  ```
17
18
 
18
19
  ## Why
@@ -30,7 +31,7 @@ npx @webdesignhot/design-md <command>
30
31
  # global
31
32
  npm i -g @webdesignhot/design-md
32
33
  # the binary is `design-md` regardless of scope:
33
- design-md add linear
34
+ design-md add stripe
34
35
  ```
35
36
 
36
37
  Requires Node 18+.
@@ -57,20 +58,40 @@ lint <file> [--format=text|json]
57
58
  diff <a> <b> [--format=text|json]
58
59
  Token-level diff between two DESIGN.md files.
59
60
 
60
- export <file> --to <tailwind|css|dtcg|figma>
61
+ export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailwind-version <v3|v4>]
61
62
  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
63
+ tailwind @theme {} CSS block (Tailwind v4 default).
64
+ Pass --tailwind-version v3 for the legacy
65
+ module.exports = { theme: { extend: { ... } } } shape.
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).
70
+ json-tailwind — v3 module.exports JSON (alias of
71
+ `--to tailwind --tailwind-version v3`, same
72
+ Google CLI compat reason as above).
73
+ css — :root { --color-bg, --radius-card, … }
74
+ dtcg — W3C Design Tokens Community Group JSON
75
+ figma — Figma Variables import format
66
76
 
67
77
  extract <url> [-o <path>] [--token-only]
68
78
  Extract a draft DESIGN.md from any production URL.
69
79
  (Requires a webdesignhot session — opens the browser flow.)
70
80
 
81
+ 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.
84
+
71
85
  theme <slug> [--dark|--light]
72
86
  Compute a dark/light variant of any design.
73
87
 
88
+ submit <file> [--dry-run]
89
+ Submit your DESIGN.md as a PR to the public catalog at
90
+ github.com/WebDesignHot/design-md. Auto-forks via the `gh` CLI,
91
+ syncs the fork with upstream main, branches, commits, pushes,
92
+ and opens a PR with templated body. Requires `gh` installed and
93
+ authed (https://cli.github.com).
94
+
74
95
  preview <slug>
75
96
  Open the directory detail page in your browser.
76
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdesignhot/design-md",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
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
5
  "keywords": ["design.md", "design-system", "design-tokens", "ai-agents", "cli", "tailwind", "figma", "dtcg"],
6
6
  "author": "webdesignhot",
@@ -1,15 +1,28 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { readDesignMd } from '../lib/parse.mjs'
3
3
 
4
- const FORMATS = new Set(['tailwind', 'css', 'dtcg', 'figma'])
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.
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)
13
+ // `--to tailwind --tailwind-version v3` → v3 JSON (legacy flag, kept)
14
+ const FORMATS = new Set(['tailwind', 'css-tailwind', 'json-tailwind', 'css', 'dtcg', 'figma'])
15
+ const TAILWIND_VERSIONS = new Set(['v3', 'v4'])
5
16
 
6
17
  function parseFlags(args) {
7
- const flags = { to: null }
18
+ const flags = { to: null, tailwindVersion: 'v4' }
8
19
  const positional = []
9
20
  for (let i = 0; i < args.length; i++) {
10
21
  const a = args[i]
11
22
  if (a.startsWith('--to=')) flags.to = a.slice(5)
12
23
  else if (a === '--to') flags.to = args[++i]
24
+ else if (a.startsWith('--tailwind-version=')) flags.tailwindVersion = a.slice(19)
25
+ else if (a === '--tailwind-version') flags.tailwindVersion = args[++i]
13
26
  else positional.push(a)
14
27
  }
15
28
  return { flags, positional }
@@ -25,13 +38,44 @@ function toCss(data) {
25
38
  return lines.join('\n')
26
39
  }
27
40
 
28
- function toTailwind(data) {
29
- const c = data.colors ?? {}
41
+ function flattenColors(c) {
42
+ // Multi-theme: pick the default theme (or first one) for export.
43
+ const values = Object.values(c)
44
+ if (values.length > 0 && values.every((v) => v && typeof v === 'object')) {
45
+ return values[0]
46
+ }
47
+ return c
48
+ }
49
+
50
+ function toTailwindV4(data) {
51
+ const c = flattenColors(data.colors ?? {})
52
+ const r = data.radius ?? {}
53
+ const s = data.spacing?.scale
54
+ const lines = ['@theme {']
55
+ for (const [k, v] of Object.entries(c)) {
56
+ if (typeof v === 'string') lines.push(` --color-${k}: ${v};`)
57
+ }
58
+ for (const [k, v] of Object.entries(r)) {
59
+ lines.push(` --radius-${k}: ${typeof v === 'number' ? `${v}px` : v};`)
60
+ }
61
+ if (Array.isArray(s)) {
62
+ s.forEach((unit, i) => lines.push(` --spacing-${i}: ${typeof unit === 'number' ? `${unit}px` : unit};`))
63
+ }
64
+ lines.push('}')
65
+ return lines.join('\n')
66
+ }
67
+
68
+ function toTailwindV3(data) {
69
+ const c = flattenColors(data.colors ?? {})
30
70
  const r = data.radius ?? {}
31
71
  const out = {
32
72
  theme: {
33
73
  extend: {
34
- colors: Object.fromEntries(Object.entries(c).map(([k, v]) => [k, String(v)])),
74
+ colors: Object.fromEntries(
75
+ Object.entries(c)
76
+ .filter(([, v]) => typeof v === 'string')
77
+ .map(([k, v]) => [k, String(v)]),
78
+ ),
35
79
  borderRadius: Object.fromEntries(
36
80
  Object.entries(r).map(([k, v]) => [k, typeof v === 'number' ? `${v}px` : v]),
37
81
  ),
@@ -41,6 +85,10 @@ function toTailwind(data) {
41
85
  return `module.exports = ${JSON.stringify(out, null, 2)}`
42
86
  }
43
87
 
88
+ function toTailwind(data, version = 'v4') {
89
+ return version === 'v3' ? toTailwindV3(data) : toTailwindV4(data)
90
+ }
91
+
44
92
  function toDtcg(data) {
45
93
  const c = data.colors ?? {}
46
94
  const r = data.radius ?? {}
@@ -73,19 +121,34 @@ function toFigma(data) {
73
121
  const RENDERERS = { tailwind: toTailwind, css: toCss, dtcg: toDtcg, figma: toFigma }
74
122
 
75
123
  export const exportTokens = {
76
- usage: 'export <file> --to <tailwind|css|dtcg|figma>',
124
+ usage: 'export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma> [--tailwind-version v3|v4]',
77
125
  async run(args) {
78
126
  const { flags, positional } = parseFlags(args)
79
127
  const file = positional[0]
80
128
  if (!file || !flags.to) {
81
- console.error('Usage: design-md export <file> --to <tailwind|css|dtcg|figma>')
129
+ console.error('Usage: design-md export <file> --to <tailwind|css-tailwind|json-tailwind|css|dtcg|figma>')
82
130
  process.exit(2)
83
131
  }
84
132
  if (!FORMATS.has(flags.to)) {
85
133
  console.error(`Unknown format "${flags.to}". Choose: ${[...FORMATS].join(', ')}`)
86
134
  process.exit(2)
87
135
  }
136
+ if (!TAILWIND_VERSIONS.has(flags.tailwindVersion)) {
137
+ console.error(`Unknown --tailwind-version "${flags.tailwindVersion}". Choose: v3, v4`)
138
+ process.exit(2)
139
+ }
88
140
  const { data } = await readDesignMd(resolve(process.cwd(), file))
89
- process.stdout.write(RENDERERS[flags.to](data) + '\n')
141
+
142
+ let out
143
+ if (flags.to === 'tailwind') {
144
+ out = toTailwind(data, flags.tailwindVersion)
145
+ } else if (flags.to === 'css-tailwind') {
146
+ out = toTailwindV4(data)
147
+ } else if (flags.to === 'json-tailwind') {
148
+ out = toTailwindV3(data)
149
+ } else {
150
+ out = RENDERERS[flags.to](data)
151
+ }
152
+ process.stdout.write(out + '\n')
90
153
  },
91
154
  }
@@ -29,6 +29,6 @@ export const extract = {
29
29
  console.log(` ${kleur.cyan(target)}`)
30
30
  console.log()
31
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.'))
32
+ console.log(kleur.dim(' A direct extraction endpoint is on the roadmap for now this opens the web flow.'))
33
33
  },
34
34
  }
@@ -7,26 +7,37 @@ ${kleur.dim('USAGE')}
7
7
  design-md <command> [args]
8
8
 
9
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
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 formats:
17
+ tailwind v4 CSS @theme (default; --tailwind-version v3 for legacy)
18
+ css-tailwind v4 CSS @theme (Google CLI alias)
19
+ json-tailwind v3 JSON theme.extend (Google CLI alias)
20
+ css :root { --color-* / --radius-* }
21
+ dtcg W3C Design Tokens JSON
22
+ figma Figma Variables import format
23
+ extract <url> Extract a draft DESIGN.md from a production URL
24
+ import <url> Alias of extract — name aligned with Google Labs
25
+ theme <slug> --dark Compute a dark-mode counterpart of a light design
26
+ preview <slug> Open the directory detail page in your browser
27
+ submit <file> Open a PR to add your DESIGN.md to the public catalog
28
+ help This screen
21
29
 
22
30
  ${kleur.dim('EXAMPLES')}
23
- $ design-md add linear ${kleur.dim('# writes DESIGN.md to CWD')}
31
+ $ design-md add stripe ${kleur.dim('# writes DESIGN.md to CWD')}
24
32
  $ design-md list | grep editorial
25
- $ design-md export DESIGN.md --to tailwind > tailwind.theme.js
33
+ $ design-md export DESIGN.md --to tailwind > theme.css
34
+ $ design-md export DESIGN.md --to tailwind --tailwind-version v3 > tailwind.config.js
35
+ $ design-md import https://stripe.com
26
36
  $ design-md diff DESIGN.md old.md
37
+ $ design-md submit ./DESIGN.md ${kleur.dim('# auto-fork + open PR via `gh` CLI')}
27
38
 
28
- ${kleur.dim('Spec:')} https://github.com/google-labs-code/design.md
29
- ${kleur.dim('Catalog:')} https://www.webdesignhot.com/design-md
39
+ ${kleur.dim('Spec:')} https://www.webdesignhot.com/design.md/spec ${kleur.dim('(webdesignhot/0.1)')}
40
+ ${kleur.dim('Catalog:')} https://www.webdesignhot.com/design.md/
30
41
  `
31
42
 
32
43
  export const help = {
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Submit a DESIGN.md to the public catalog at github.com/WebDesignHot/design-md.
3
+ *
4
+ * Flow:
5
+ * 1. Validate file (lint).
6
+ * 2. Resolve slug from frontmatter (or prompt).
7
+ * 3. Verify `gh` CLI is installed and authed.
8
+ * 4. Fork WebDesignHot/design-md to user's account (idempotent).
9
+ * 5. Clone fork to a temp dir.
10
+ * 6. Copy file to design-md/<slug>.md.
11
+ * 7. Branch + commit + push.
12
+ * 8. Open a PR against WebDesignHot/design-md:main with a templated body.
13
+ * 9. Print the PR URL.
14
+ *
15
+ * Aborts gracefully if `gh` is missing or unauthed; falls back to printing
16
+ * the manual-PR instructions (file path + raw upload guide).
17
+ */
18
+ import { resolve, join, basename } from 'node:path'
19
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises'
20
+ import { tmpdir } from 'node:os'
21
+ import { execSync, spawnSync } from 'node:child_process'
22
+ import kleur from 'kleur'
23
+ import * as p from '@clack/prompts'
24
+ import { readDesignMd } from '../lib/parse.mjs'
25
+
26
+ const UPSTREAM = 'WebDesignHot/design-md'
27
+ const UPSTREAM_BRANCH = 'main'
28
+
29
+ function parseFlags(args) {
30
+ const flags = { dryRun: false }
31
+ const positional = []
32
+ for (let i = 0; i < args.length; i++) {
33
+ const a = args[i]
34
+ if (a === '--dry-run') flags.dryRun = true
35
+ else positional.push(a)
36
+ }
37
+ return { flags, positional }
38
+ }
39
+
40
+ function which(cmd) {
41
+ const r = spawnSync('which', [cmd], { encoding: 'utf8' })
42
+ return r.status === 0 ? r.stdout.trim() : null
43
+ }
44
+
45
+ function run(cmd, args, opts = {}) {
46
+ const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
47
+ if (r.status !== 0) {
48
+ throw new Error(`${cmd} ${args.join(' ')} failed:\n${r.stderr || r.stdout || '(no output)'}`)
49
+ }
50
+ return r.stdout?.trim() ?? ''
51
+ }
52
+
53
+ function deriveSlug(name) {
54
+ return String(name)
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9-]+/g, '-')
57
+ .replace(/^-+|-+$/g, '')
58
+ .replace(/--+/g, '-')
59
+ }
60
+
61
+ async function exists(p) {
62
+ try {
63
+ await stat(p)
64
+ return true
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
70
+ export const submit = {
71
+ usage: 'submit <file> [--dry-run]',
72
+ async run(args) {
73
+ const { flags, positional } = parseFlags(args)
74
+ const file = positional[0]
75
+ if (!file) {
76
+ console.error('Usage: design-md submit <file>')
77
+ console.error(' Submits your DESIGN.md as a PR to github.com/WebDesignHot/design-md.')
78
+ process.exit(2)
79
+ }
80
+
81
+ const filePath = resolve(process.cwd(), file)
82
+ if (!(await exists(filePath))) {
83
+ console.error(kleur.red(`✗ File not found: ${filePath}`))
84
+ process.exit(1)
85
+ }
86
+
87
+ // 1. Lint inline (cheap re-check)
88
+ const { data } = await readDesignMd(filePath)
89
+ const errors = []
90
+ if (!data.name) errors.push('frontmatter.name is required')
91
+ if (!data.spec) errors.push('frontmatter.spec is required (e.g. webdesignhot/0.1)')
92
+ if (!data.source_url) errors.push('frontmatter.source_url is required')
93
+ if (!data.colors?.bg || !data.colors?.text || !data.colors?.brand) {
94
+ errors.push('frontmatter.colors must include bg, text, and brand')
95
+ }
96
+ if (errors.length) {
97
+ console.error(kleur.red('✗ File has issues that must be fixed before submitting:'))
98
+ for (const e of errors) console.error(` · ${e}`)
99
+ console.error(kleur.dim(' Run `design-md lint <file>` for full diagnostics.'))
100
+ process.exit(1)
101
+ }
102
+
103
+ // 2. Resolve slug
104
+ let slug = data.slug
105
+ if (!slug) {
106
+ const fromFile = basename(filePath, '.md')
107
+ slug = fromFile === 'DESIGN' ? deriveSlug(data.name) : fromFile
108
+ }
109
+ if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
110
+ console.error(kleur.red(`✗ Invalid slug "${slug}" — must be kebab-case lowercase.`))
111
+ process.exit(1)
112
+ }
113
+
114
+ console.log(kleur.dim(`→ Submitting ${kleur.bold(data.name)} as slug "${kleur.bold(slug)}"`))
115
+
116
+ // 3. Check `gh` CLI
117
+ const ghPath = which('gh')
118
+ if (!ghPath) {
119
+ console.error()
120
+ console.error(kleur.yellow('⚠ The GitHub CLI (`gh`) is not installed.'))
121
+ console.error()
122
+ console.error(' This command auto-forks + opens a PR for you. Install with:')
123
+ console.error(kleur.cyan(' brew install gh # macOS'))
124
+ console.error(kleur.cyan(' winget install GitHub.cli # Windows'))
125
+ console.error(kleur.cyan(' https://cli.github.com — other platforms'))
126
+ console.error()
127
+ console.error(' Or submit manually:')
128
+ console.error(` 1. Fork ${kleur.cyan(`https://github.com/${UPSTREAM}`)}`)
129
+ console.error(` 2. Add your file at ${kleur.cyan(`design-md/${slug}.md`)}`)
130
+ console.error(' 3. Open a PR against `main`.')
131
+ process.exit(1)
132
+ }
133
+
134
+ // Verify gh is authed
135
+ try {
136
+ run('gh', ['auth', 'status'], { stdio: ['ignore', 'pipe', 'pipe'] })
137
+ } catch {
138
+ console.error(kleur.yellow('⚠ `gh` is installed but not authenticated.'))
139
+ console.error(` Run: ${kleur.cyan('gh auth login')}`)
140
+ process.exit(1)
141
+ }
142
+
143
+ if (flags.dryRun) {
144
+ console.log(kleur.green(`✓ Dry-run passed. Would submit ${data.name} as ${slug}.`))
145
+ console.log(kleur.dim(` File: ${filePath}`))
146
+ console.log(kleur.dim(` Target: ${UPSTREAM}/design-md/${slug}.md`))
147
+ return
148
+ }
149
+
150
+ // 4. Fork (idempotent — gh handles already-forked case)
151
+ const ghUser = run('gh', ['api', 'user', '-q', '.login'])
152
+ console.log(kleur.dim(`→ Forking ${UPSTREAM} to ${ghUser}/design-md (no-op if already forked)`))
153
+ try {
154
+ run('gh', ['repo', 'fork', UPSTREAM, '--clone=false'], { stdio: ['ignore', 'pipe', 'pipe'] })
155
+ } catch {
156
+ // gh fork prints "already exists" to stderr but exits 0 sometimes; treat as ok
157
+ }
158
+
159
+ // 5. Clone fork to a temp dir
160
+ const work = join(tmpdir(), `design-md-submit-${slug}-${Date.now()}`)
161
+ await mkdir(work, { recursive: true })
162
+ console.log(kleur.dim(`→ Cloning fork to ${work}`))
163
+ run('gh', ['repo', 'clone', `${ghUser}/design-md`, work, '--', '--depth=1'])
164
+
165
+ // Sync fork with upstream's main (in case fork is stale)
166
+ try {
167
+ run('git', ['-C', work, 'remote', 'add', 'upstream', `https://github.com/${UPSTREAM}.git`])
168
+ run('git', ['-C', work, 'fetch', '--depth=1', 'upstream', UPSTREAM_BRANCH])
169
+ run('git', ['-C', work, 'reset', '--hard', `upstream/${UPSTREAM_BRANCH}`])
170
+ } catch (err) {
171
+ console.error(kleur.yellow(`⚠ Could not sync fork with upstream: ${err.message}`))
172
+ console.error(kleur.dim(' Continuing with current fork state.'))
173
+ }
174
+
175
+ // 6. Copy file
176
+ const targetPath = join(work, 'design-md', `${slug}.md`)
177
+ if (await exists(targetPath)) {
178
+ const overwrite = await p.confirm({
179
+ message: `design-md/${slug}.md already exists in the catalog. Open a PR to update it?`,
180
+ initialValue: false,
181
+ })
182
+ if (p.isCancel(overwrite) || !overwrite) {
183
+ console.log(kleur.yellow('Aborted.'))
184
+ process.exit(0)
185
+ }
186
+ }
187
+ const fileContent = await readFile(filePath, 'utf8')
188
+ await writeFile(targetPath, fileContent)
189
+
190
+ // 7. Branch + commit + push
191
+ const branch = `submit/${slug}-${Date.now().toString(36).slice(-4)}`
192
+ console.log(kleur.dim(`→ Branch: ${branch}`))
193
+ run('git', ['-C', work, 'checkout', '-b', branch])
194
+ run('git', ['-C', work, 'add', `design-md/${slug}.md`])
195
+ const commitMsg = `submit: ${data.name} (${slug})\n\nFrom: ${data.source_url ?? 'unspecified'}`
196
+ run('git', ['-C', work, '-c', 'commit.gpgsign=false', 'commit', '-m', commitMsg])
197
+ console.log(kleur.dim(`→ Pushing to ${ghUser}/design-md:${branch}`))
198
+ run('git', ['-C', work, 'push', '-u', 'origin', branch])
199
+
200
+ // 8. Open PR
201
+ const prTitle = `submit: ${data.name}`
202
+ const prBody = `## New entry: ${data.name}
203
+
204
+ **Slug**: \`${slug}\`
205
+ **Source URL**: ${data.source_url ?? '(not provided)'}
206
+ **Categories**: ${(data.categories ?? []).join(', ') || '(none)'}
207
+ **Tags**: ${(data.tags ?? []).join(', ') || '(none)'}
208
+
209
+ ${data.tagline ? `> ${data.tagline}\n` : ''}
210
+ ${data.description ?? ''}
211
+
212
+ ---
213
+
214
+ Submitted via \`design-md submit\`.
215
+
216
+ ### Reviewer checklist
217
+ - [ ] \`design-md lint\` passes
218
+ - [ ] All 15 sections present in body
219
+ - [ ] Tokens reflect actual production site
220
+ - [ ] Lineage block names real influences with URLs
221
+ - [ ] Categories + tags use existing taxonomy
222
+ `
223
+ console.log(kleur.dim('→ Opening PR'))
224
+ const prUrl = run('gh', [
225
+ 'pr', 'create',
226
+ '--repo', UPSTREAM,
227
+ '--base', UPSTREAM_BRANCH,
228
+ '--head', `${ghUser}:${branch}`,
229
+ '--title', prTitle,
230
+ '--body', prBody,
231
+ ])
232
+
233
+ // 9. Done
234
+ console.log()
235
+ console.log(kleur.green('✓ Submitted.'))
236
+ console.log(` ${kleur.cyan(prUrl)}`)
237
+ console.log()
238
+ console.log(kleur.dim('Your design will appear on webdesignhot.com once we review and merge.'))
239
+ },
240
+ }
package/src/index.mjs CHANGED
@@ -14,6 +14,7 @@ import { exportTokens } from './commands/export.mjs'
14
14
  import { extract } from './commands/extract.mjs'
15
15
  import { theme } from './commands/theme.mjs'
16
16
  import { preview } from './commands/preview.mjs'
17
+ import { submit } from './commands/submit.mjs'
17
18
  import { help } from './commands/help.mjs'
18
19
 
19
20
  const COMMANDS = {
@@ -25,8 +26,10 @@ const COMMANDS = {
25
26
  diff,
26
27
  export: exportTokens,
27
28
  extract,
29
+ import: extract, // alias — name aligned with Google Labs PR #40
28
30
  theme,
29
31
  preview,
32
+ submit,
30
33
  help,
31
34
  '--help': help,
32
35
  '-h': help,