sdtk-design-kit 0.3.1 → 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  `sdtk-design-kit` is the public CLI package for SDTK-DESIGN.
4
4
 
5
- Package version in this source snapshot: `0.3.1`
5
+ Package version in this source snapshot: `0.4.0`
6
6
  CLI command: `sdtk-design`
7
7
 
8
8
  SDTK-DESIGN is a local-first MVP design planner and reviewer. It turns either a rough MVP idea or explicit SDTK-SPEC design artifacts into reviewable design docs, a static prototype, visual review evidence, and an SDTK-CODE handoff.
@@ -60,20 +60,33 @@ sdtk-design screens
60
60
  sdtk-design wireframe --screen landing
61
61
  sdtk-design system --style minimal-saas
62
62
  sdtk-design prototype
63
+ sdtk-design preview
64
+ sdtk-design styles
63
65
  sdtk-design review --artifact docs/design/prototype/index.html
64
66
  sdtk-design handoff
65
67
  sdtk-design status
66
68
  ```
67
69
 
70
+ `preview` starts a local Preview Studio (browser): it renders the prototype screens, lets you click/shift-click elements to annotate them with a note, and (via the **Tokens** panel) live-preview design-token tweaks on the `:root` CSS variables a screen declares. On "Send to agent" it shows a confirm summary, writes `docs/design/feedback/DESIGN_FEEDBACK_<timestamp>.md` (schema `sdtk.design.feedback.v1`), then a copy-ready instruction to apply it. Run the `design-prototype` skill afterward to apply that scoped feedback. The studio serves the prototype read-only, writes only under `docs/design/feedback/`, and never runs the agent itself.
71
+
72
+ `styles` opens the same studio focused on the **Style Gallery** — a visual menu of the category-oriented style presets (swatches + type sample + summary). Click a card to copy its `start --style <preset>` command. It needs no prototype (pick a style before generating) and is choose-only: it copies a command, it writes nothing.
73
+
68
74
  Visual style presets:
69
75
 
70
76
  ```text
71
- minimal-saas
72
- premium-dashboard
73
- bold-founder
74
- warm-editorial
77
+ minimal-saas SaaS & Productivity
78
+ premium-dashboard Dashboard & Data
79
+ bold-founder Marketing & Launch
80
+ warm-editorial Editorial & Content
81
+ ecommerce-retail E-Commerce & Retail
82
+ fintech-trust Fintech & Data
83
+ editorial-content Editorial & Content
75
84
  ```
76
85
 
86
+ Presets are category-oriented: pick the one closest to the product you are
87
+ designing (for example `ecommerce-retail` for a storefront). They are
88
+ unbranded SDTK-original style directions, not copies of any third-party brand.
89
+
77
90
  ## Outputs
78
91
 
79
92
  Human-facing artifacts are written under `docs/design/`, including:
@@ -85,6 +98,7 @@ DESIGN_SYSTEM.md
85
98
  DESIGN_HANDOFF.md
86
99
  prototype/index.html
87
100
  reviews/DESIGN_REVIEW_YYYYMMDD.md
101
+ feedback/DESIGN_FEEDBACK_YYYYMMDDThhmmss.md
88
102
  wireframes/
89
103
  ```
90
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdtk-design-kit",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Local-first MVP design planner and reviewer for SDTK workspaces.",
5
5
  "bin": {
6
6
  "sdtk-design": "bin/sdtk-design.js"
@@ -1,6 +1,17 @@
1
1
  ---
2
2
  name: design-prototype
3
3
  description: Generate high-fidelity standalone HTML prototypes from an SDTK-DESIGN generation manifest, design system, design tokens, and per-screen briefs.
4
+ sdtk:
5
+ preview:
6
+ entry: sdtk-design preview
7
+ parameters:
8
+ - { name: --accent, type: color }
9
+ - { name: --primary, type: color }
10
+ - { name: --surface, type: color }
11
+ - { name: --text, type: color }
12
+ - { name: --radius-card, type: spacing, min: 0, max: 24 }
13
+ - { name: --space-section, type: spacing, min: 8, max: 96 }
14
+ capabilities_required: [surgical_edit]
4
15
  ---
5
16
 
6
17
  # Design Prototype
@@ -265,6 +276,17 @@ For each screen with multiple `<nav>` elements, verify before output:
265
276
 
266
277
  If any check fails, fix the CSS before emitting the HTML file.
267
278
 
279
+ ## Consume design feedback (BK-275)
280
+
281
+ Use this when the user asks you to apply Preview Studio feedback (the `sdtk-design preview` annotate→send loop).
282
+
283
+ 1. Read the **newest** `docs/design/feedback/DESIGN_FEEDBACK_*.md` (sort by filename timestamp; the name is `DESIGN_FEEDBACK_<YYYYMMDDThhmmss>.md`). Its front-matter is `schema: sdtk.design.feedback.v1`.
284
+ 2. Parse the `<attached-preview-comments>` block. Each numbered entry names a `targetKind` (`element`, `pod`, or `token`), and — for element/pod — a `screen` (with its `file:` path), a `selector`, a `stable-id`, a `position`, and a `comment`. Pod entries list `member.N` sub-targets. **Token entries** instead name a `token` (a CSS custom property such as `--accent`), an `oldValue`, a `newValue`, a `scope: global`, and a `comment`.
285
+ 3. **Hard scope — obey it literally.** For `element`/`pod` marks, change ONLY the elements named by selector / stable-id / position, in ONLY the named screen file(s) under `docs/design/prototype/screens/`. Do NOT modify sibling screens, parent layout, global CSS, `DESIGN_SYSTEM.md`, or `DESIGN_TOKENS.json` — even if you notice issues there. Surface any out-of-scope observation as a short note in your reply instead of editing it. If a request cannot be satisfied without touching outside the scope, ask the user first.
286
+ - **`token` marks are the one deliberate exception** to "do not touch tokens": apply the named `token` change globally to its source of truth — update the value in `docs/design/DESIGN_TOKENS.json` if the token maps to a token field there, and/or the `:root { --token: … }` declaration shared by the screens. Change ONLY the named token(s) to the given `newValue`; do not retune other tokens, palettes, or unmarked screens. If a token has no clear source of truth, ask before editing.
287
+ 4. When you (re)write a screen, **stamp `data-sdtk-id="<stable id>"` on the stable structural elements you touch** (headers, cards, sections, nav, primary CTAs). This is best-effort and never required, but it lets future Preview Studio marks re-anchor by id instead of by DOM-path.
288
+ 5. After editing, keep each screen a complete standalone `<!doctype html>` document under ~1000 lines, exactly as elsewhere in this skill. Do not delete the feedback file.
289
+
268
290
  ## Boundaries
269
291
 
270
292
  - Brand, palette, typography, spacing, mood, and component direction come only from `docs/design/DESIGN_SYSTEM.md` and `docs/design/DESIGN_TOKENS.json`.
@@ -14,6 +14,8 @@ Usage:
14
14
  sdtk-design wireframe --screen landing
15
15
  sdtk-design system --style minimal-saas
16
16
  sdtk-design prototype
17
+ sdtk-design preview
18
+ sdtk-design styles
17
19
  sdtk-design review --artifact docs/design/prototype/index.html
18
20
  sdtk-design handoff
19
21
  sdtk-design status
@@ -57,6 +59,8 @@ Command status:
57
59
  wireframe Available.
58
60
  system Available.
59
61
  prototype Available SPEC-driven prototype manifest launcher.
62
+ preview Available local Preview Studio (render screens + element-level annotate to feedback).
63
+ styles Available visual Style Gallery (browse + copy a "start --style" command).
60
64
  handoff Available.
61
65
  review Available for local markdown and static HTML artifacts.
62
66
  start Available beginner facade.
@@ -7,41 +7,67 @@ const { ValidationError } = require("../lib/errors");
7
7
  const {
8
8
  assertProjectLocalPath,
9
9
  describeDesignPaths,
10
+ isPathInsideOrEqual,
10
11
  resolveProjectPath,
11
12
  } = require("../lib/design-paths");
13
+ const {
14
+ VALID_RUNTIMES,
15
+ VALID_SCOPES,
16
+ defaultScope,
17
+ resolveSkillsDir,
18
+ } = require("../lib/runtime-skills");
12
19
 
13
20
  const INIT_FLAG_DEFS = {
14
21
  help: { type: "boolean" },
15
22
  "project-path": { type: "string" },
16
23
  force: { type: "boolean" },
17
24
  "no-state": { type: "boolean" },
25
+ runtime: { type: "string" },
26
+ "runtime-scope": { type: "string" },
27
+ global: { type: "boolean" },
18
28
  };
19
29
 
20
30
  const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
21
31
  const DESIGN_PROTOTYPE_SKILL_SOURCE = path.join(PACKAGE_ROOT, "skills", "design-prototype");
22
- const DESIGN_PROTOTYPE_SKILL_TARGETS = [
23
- path.join(".claude", "skills", "design-prototype"),
24
- path.join(".agents", "skills", "design-prototype"),
25
- ];
26
32
 
27
33
  function toPosix(value) {
28
34
  return String(value || "").replace(/\\/g, "/");
29
35
  }
30
36
 
37
+ // Report a target either project-relative (when inside the project) or as an
38
+ // absolute posix path (e.g. for user/global skills under the home directory).
39
+ function reportPath(targetPath, projectPath) {
40
+ if (isPathInsideOrEqual(targetPath, projectPath)) {
41
+ return toPosix(path.relative(projectPath, targetPath));
42
+ }
43
+ return toPosix(targetPath);
44
+ }
45
+
31
46
  function cmdInitHelp() {
32
47
  console.log(`SDTK-DESIGN Init
33
48
 
34
49
  Usage:
35
- sdtk-design init [--project-path <path>] [--force] [--no-state]
50
+ sdtk-design init [--runtime <claude|codex>] [--runtime-scope <project|user>]
51
+ [--global] [--project-path <path>] [--force] [--no-state]
36
52
 
37
53
  Example:
38
54
  sdtk-design init
55
+ sdtk-design init --runtime codex
56
+ sdtk-design init --runtime claude --global
57
+
58
+ Runtime:
59
+ --runtime claude Install the design-prototype skill under .claude/skills (default).
60
+ --runtime codex Install it under .codex/skills (launch Codex with CODEX_HOME).
61
+ --runtime-scope project Project-local skills dir (default for claude).
62
+ --runtime-scope user User/global skills dir (default for codex).
63
+ --global Shorthand for --runtime-scope user (~/.claude or ~/.codex).
39
64
 
40
65
  Creates:
41
66
  docs/design/
42
67
  docs/design/wireframes/
43
68
  docs/design/reviews/
44
69
  docs/design/README.md
70
+ <runtime skills dir>/design-prototype/ (e.g. .claude/skills or .codex/skills)
45
71
  .sdtk/design/manifest.json unless --no-state is used
46
72
 
47
73
  Safety:
@@ -139,41 +165,70 @@ function listSkillFiles(sourceDir) {
139
165
  return results.sort();
140
166
  }
141
167
 
142
- function installDesignPrototypeSkill(projectPath, force, result) {
168
+ // Install the bundled design-prototype skill into a single runtime skills dir
169
+ // (.claude/skills or .codex/skills, per the resolved runtime/scope). For a
170
+ // project-local target the path is guarded inside the project root; for a
171
+ // user/global target (home directory) traversal is guarded against the skills
172
+ // target dir instead, since the destination is intentionally outside the project.
173
+ function installDesignPrototypeSkill(skillsDir, projectPath, force, result) {
143
174
  if (!fs.existsSync(DESIGN_PROTOTYPE_SKILL_SOURCE) || !fs.statSync(DESIGN_PROTOTYPE_SKILL_SOURCE).isDirectory()) {
144
175
  throw new ValidationError(`Bundled design-prototype skill is missing: ${DESIGN_PROTOTYPE_SKILL_SOURCE}. No project files were changed.`);
145
176
  }
146
177
 
147
- for (const relativeTargetDir of DESIGN_PROTOTYPE_SKILL_TARGETS) {
148
- const targetDir = path.join(projectPath, relativeTargetDir);
178
+ const targetDir = path.join(skillsDir, "design-prototype");
179
+ if (isPathInsideOrEqual(targetDir, projectPath)) {
149
180
  assertProjectLocalPath(targetDir, projectPath, "skill directory");
150
- fs.mkdirSync(targetDir, { recursive: true });
151
- result.created.push(toPosix(relativeTargetDir) + "/");
152
-
153
- for (const sourceFile of listSkillFiles(DESIGN_PROTOTYPE_SKILL_SOURCE)) {
154
- const relativeFile = path.relative(DESIGN_PROTOTYPE_SKILL_SOURCE, sourceFile);
155
- const targetFile = path.join(targetDir, relativeFile);
156
- assertProjectLocalPath(targetFile, projectPath, "skill file");
157
- if (fs.existsSync(targetFile) && !force) {
158
- result.skipped.push(toPosix(path.relative(projectPath, targetFile)));
159
- continue;
160
- }
161
- fs.mkdirSync(path.dirname(targetFile), { recursive: true });
162
- fs.copyFileSync(sourceFile, targetFile);
163
- result.created.push(toPosix(path.relative(projectPath, targetFile)));
181
+ }
182
+ fs.mkdirSync(targetDir, { recursive: true });
183
+ result.created.push(reportPath(targetDir, projectPath) + "/");
184
+
185
+ for (const sourceFile of listSkillFiles(DESIGN_PROTOTYPE_SKILL_SOURCE)) {
186
+ const relativeFile = path.relative(DESIGN_PROTOTYPE_SKILL_SOURCE, sourceFile);
187
+ const targetFile = path.join(targetDir, relativeFile);
188
+ if (!isPathInsideOrEqual(targetFile, targetDir)) {
189
+ throw new ValidationError(`Refusing to write skill file outside its target directory: ${targetFile}`);
190
+ }
191
+ if (fs.existsSync(targetFile) && !force) {
192
+ result.skipped.push(reportPath(targetFile, projectPath));
193
+ continue;
164
194
  }
195
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
196
+ fs.copyFileSync(sourceFile, targetFile);
197
+ result.created.push(reportPath(targetFile, projectPath));
198
+ }
199
+ }
200
+
201
+ function resolveRuntimeAndScope({ runtime, runtimeScope, global: globalFlag }) {
202
+ const resolvedRuntime = runtime || "claude";
203
+ if (!VALID_RUNTIMES.includes(resolvedRuntime)) {
204
+ throw new ValidationError(`--runtime must be one of: ${VALID_RUNTIMES.join(", ")}. Got: ${resolvedRuntime}.`);
205
+ }
206
+ const resolvedScope = globalFlag ? "user" : (runtimeScope || defaultScope(resolvedRuntime));
207
+ if (!VALID_SCOPES.includes(resolvedScope)) {
208
+ throw new ValidationError(`--runtime-scope must be one of: ${VALID_SCOPES.join(", ")}. Got: ${resolvedScope}.`);
165
209
  }
210
+ return { runtime: resolvedRuntime, scope: resolvedScope };
166
211
  }
167
212
 
168
- function runDesignInit({ projectPath, force = false, state = true }) {
213
+ function runDesignInit({ projectPath, force = false, state = true, runtime, runtimeScope, global: globalFlag }) {
169
214
  const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
170
215
  if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
171
216
  throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No project files were changed.`);
172
217
  }
173
218
 
219
+ const { runtime: resolvedRuntime, scope: resolvedScope } = resolveRuntimeAndScope({
220
+ runtime,
221
+ runtimeScope,
222
+ global: globalFlag,
223
+ });
224
+ const skillsDir = resolveSkillsDir(resolvedRuntime, resolvedScope, resolvedProjectPath);
225
+
174
226
  const paths = describeDesignPaths(resolvedProjectPath);
175
227
  const result = {
176
228
  projectPath: resolvedProjectPath,
229
+ runtime: resolvedRuntime,
230
+ scope: resolvedScope,
231
+ skillsDir,
177
232
  created: [],
178
233
  skipped: [],
179
234
  stateEnabled: Boolean(state),
@@ -183,7 +238,7 @@ function runDesignInit({ projectPath, force = false, state = true }) {
183
238
  ensureDirectory(paths.wireframesPath, resolvedProjectPath, result);
184
239
  ensureDirectory(paths.reviewsPath, resolvedProjectPath, result);
185
240
  writeManagedFile(paths.designReadmePath, designReadmeContent(), resolvedProjectPath, force, result);
186
- installDesignPrototypeSkill(resolvedProjectPath, force, result);
241
+ installDesignPrototypeSkill(skillsDir, resolvedProjectPath, force, result);
187
242
 
188
243
  if (state) {
189
244
  ensureDirectory(paths.designStatePath, resolvedProjectPath, result);
@@ -201,9 +256,14 @@ function cmdInit(args) {
201
256
  projectPath: flags["project-path"],
202
257
  force: Boolean(flags.force),
203
258
  state: !flags["no-state"],
259
+ runtime: flags.runtime,
260
+ runtimeScope: flags["runtime-scope"],
261
+ global: Boolean(flags.global),
204
262
  });
205
263
 
206
264
  console.log(`[design] Initialized SDTK-DESIGN workspace: ${result.projectPath}`);
265
+ console.log(`[design] Runtime: ${result.runtime} (scope: ${result.scope})`);
266
+ console.log(`[design] Skill: ${path.join(result.skillsDir, "design-prototype")}`);
207
267
  console.log(`[design] Created: ${result.created.length}`);
208
268
  console.log(`[design] Skipped: ${result.skipped.length}`);
209
269
  console.log(`[design] State: ${result.stateEnabled ? ".sdtk/design enabled" : "disabled by --no-state"}`);
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ // BK-275 — `sdtk-design preview`: serve the Preview Studio (rendered prototype
4
+ // screens + element-level annotate→feedback loop) from a local server. Reuses
5
+ // the sdtk-wiki viewer rail pattern; writes nothing except via the studio's
6
+ // /api/feedback endpoint (which lands in docs/design/feedback/).
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { spawn } = require("child_process");
11
+ const { parseFlags } = require("../lib/args");
12
+ const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
13
+ const { ValidationError } = require("../lib/errors");
14
+ const { DEFAULT_STYLE, styleCatalog } = require("../lib/style-presets");
15
+ const {
16
+ startDesignServer,
17
+ findFreePort,
18
+ waitForServer,
19
+ } = require("../lib/design-server");
20
+
21
+ const PREVIEW_FLAG_DEFS = {
22
+ help: { type: "boolean" },
23
+ "project-path": { type: "string" },
24
+ port: { type: "string" },
25
+ "no-open": { type: "boolean" },
26
+ };
27
+
28
+ const DEFAULT_HOST = "127.0.0.1";
29
+ const DEFAULT_PORT = 3210;
30
+ const STUDIO_BOOTSTRAP_MARKER = "/*__SDTK_STUDIO_BOOTSTRAP__*/";
31
+
32
+ function cmdPreviewHelp() {
33
+ console.log(`SDTK-DESIGN Preview Studio
34
+
35
+ Usage:
36
+ sdtk-design preview [--project-path <path>] [--port <n>] [--no-open]
37
+
38
+ Example:
39
+ sdtk-design preview
40
+ sdtk-design preview --port 3210 --no-open
41
+
42
+ Reads:
43
+ docs/design/prototype/.manifest.json (run sdtk-design prototype first)
44
+ docs/design/prototype/screens/*.html (skill-owned screen HTML)
45
+
46
+ In the studio:
47
+ Annotate — click/shift-click elements to mark them with a note.
48
+ Tokens — live-preview design-token tweaks on the :root CSS variables a screen
49
+ declares (no file is written); "Add changes to queue" turns them into marks.
50
+ Send to agent — shows a confirm summary, writes the feedback file, then a
51
+ copy-ready instruction to apply it (the studio never runs the agent itself).
52
+
53
+ Creates (only via the in-browser "Send to agent" action):
54
+ docs/design/feedback/DESIGN_FEEDBACK_<timestamp>.md (schema sdtk.design.feedback.v1)
55
+
56
+ Safety:
57
+ Local server on 127.0.0.1 only; no external network.
58
+ Serves the prototype directory read-only; writes only under docs/design/feedback.
59
+ Never shells out to an agent; token live-preview is in-browser only (no file write).
60
+ Does not generate or modify screen HTML, the design system, tokens, .sdtk/atlas, or SDTK-WIKI output.
61
+ In WSL2/Docker, use --no-open and tunnel the printed URL (e.g. cloudflared).`);
62
+ return 0;
63
+ }
64
+
65
+ function loadStudioHtml(manifest) {
66
+ const assetPath = path.join(__dirname, "..", "..", "assets", "design-studio.html");
67
+ const template = fs.readFileSync(assetPath, "utf-8");
68
+ // Escape "<" so a screen title containing "</script>" (or "<!--") cannot break
69
+ // out of the inline <script> the bootstrap is injected into.
70
+ const json = JSON.stringify({
71
+ schema: "sdtk.design.feedback.v1",
72
+ manifest: { screens: (manifest.screens || []).map((s) => ({ screenId: s.screenId, title: s.title || s.screenId, role: s.role || "" })) },
73
+ }).replace(/</g, "\\u003c");
74
+ const bootstrap = `window.__SDTK_STUDIO__ = ${json};`;
75
+ // Use a function replacer so "$" sequences in the JSON are inserted literally
76
+ // (String.replace honors $& / $` / $' patterns in a string replacement).
77
+ if (template.includes(STUDIO_BOOTSTRAP_MARKER)) {
78
+ return template.replace(STUDIO_BOOTSTRAP_MARKER, () => bootstrap);
79
+ }
80
+ // Defensive: if the marker is missing, inject before </head> so the studio still boots.
81
+ return template.replace("</head>", () => `<script>${bootstrap}</script>\n</head>`);
82
+ }
83
+
84
+ function tryOpenBrowser(url) {
85
+ const platform = process.platform;
86
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
87
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
88
+ try {
89
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
90
+ child.on("error", () => {});
91
+ child.unref();
92
+ } catch (_) {
93
+ // Best-effort only; non-fatal (WSL2/Docker has no browser — use the printed URL).
94
+ }
95
+ }
96
+
97
+ async function runDesignPreview({ projectPath, port, noOpen = false }) {
98
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
99
+ if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
100
+ throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No server started.`);
101
+ }
102
+ const paths = describeDesignPaths(resolvedProjectPath);
103
+ if (!fs.existsSync(paths.prototypeManifestPath)) {
104
+ throw new ValidationError(
105
+ "Missing docs/design/prototype/.manifest.json. Run sdtk-design prototype first. No server started."
106
+ );
107
+ }
108
+ let manifest;
109
+ try {
110
+ manifest = JSON.parse(fs.readFileSync(paths.prototypeManifestPath, "utf-8"));
111
+ } catch (err) {
112
+ throw new ValidationError(`Could not read prototype manifest: ${err.message}. No server started.`);
113
+ }
114
+ const screenCount = Array.isArray(manifest.screens) ? manifest.screens.length : 0;
115
+
116
+ const studioHtml = loadStudioHtml(manifest);
117
+
118
+ let chosenPort = DEFAULT_PORT;
119
+ if (port !== undefined && port !== "") {
120
+ const parsed = Number.parseInt(port, 10);
121
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
122
+ throw new ValidationError(`Invalid --port: ${port}. No server started.`);
123
+ }
124
+ chosenPort = parsed;
125
+ } else {
126
+ chosenPort = await findFreePort(DEFAULT_HOST, DEFAULT_PORT);
127
+ }
128
+
129
+ // The studio reads the design-prototype skill's optional `sdtk:` front-matter
130
+ // (preview parameters) to filter which token sliders to show. Best-effort:
131
+ // the kit-bundled SKILL.md if present, else the endpoint returns {}.
132
+ const skillMetaPath = path.join(
133
+ __dirname, "..", "..", "skills", "design-prototype", "SKILL.md"
134
+ );
135
+
136
+ const { server, url } = await startDesignServer({
137
+ host: DEFAULT_HOST,
138
+ port: chosenPort,
139
+ projectPath: resolvedProjectPath,
140
+ studioHtml,
141
+ skillMetaPath: fs.existsSync(skillMetaPath) ? skillMetaPath : undefined,
142
+ styles: styleCatalog(),
143
+ defaultStyle: DEFAULT_STYLE,
144
+ });
145
+ await waitForServer(DEFAULT_HOST, chosenPort);
146
+
147
+ return { server, url, screenCount, projectPath: resolvedProjectPath, noOpen };
148
+ }
149
+
150
+ async function cmdPreview(args) {
151
+ const { flags, positionals } = parseFlags(args || [], PREVIEW_FLAG_DEFS);
152
+ if (flags.help) return cmdPreviewHelp();
153
+ if (positionals.length > 0) {
154
+ throw new ValidationError(
155
+ `Unsupported preview argument: ${positionals.join(" ")}. Use sdtk-design preview [--project-path <path>] [--port <n>] [--no-open]. No server started.`
156
+ );
157
+ }
158
+
159
+ const result = await runDesignPreview({
160
+ projectPath: flags["project-path"],
161
+ port: flags.port,
162
+ noOpen: Boolean(flags["no-open"]),
163
+ });
164
+
165
+ console.log(`[design] Preview Studio: ${result.url}`);
166
+ console.log(`[design] Serving ${result.screenCount} screen(s) from docs/design/prototype/`);
167
+ console.log("[design] Annotate elements, then \"Send to agent\" writes docs/design/feedback/DESIGN_FEEDBACK_<timestamp>.md");
168
+ console.log("[design] Then run the design-prototype skill to apply the scoped feedback.");
169
+ if (result.noOpen) {
170
+ console.log("[design] --no-open: not launching a browser. Open the URL above (tunnel it on WSL2/Docker).");
171
+ } else {
172
+ tryOpenBrowser(result.url);
173
+ }
174
+ console.log("[design] Press Ctrl+C to stop the preview server.");
175
+
176
+ return new Promise((resolve) => {
177
+ const shutdown = () => {
178
+ try { result.server.close(); } catch (_) {}
179
+ resolve(0);
180
+ };
181
+ process.on("SIGINT", shutdown);
182
+ process.on("SIGTERM", shutdown);
183
+ });
184
+ }
185
+
186
+ module.exports = {
187
+ DEFAULT_HOST,
188
+ DEFAULT_PORT,
189
+ STUDIO_BOOTSTRAP_MARKER,
190
+ cmdPreview,
191
+ cmdPreviewHelp,
192
+ loadStudioHtml,
193
+ runDesignPreview,
194
+ tryOpenBrowser,
195
+ };
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ // BK-277 P-B — `sdtk-design styles`: open the Preview Studio focused on the
4
+ // visual Style Gallery so the user can browse style directions and copy the
5
+ // `start --style <preset>` command BEFORE generating. Unlike `preview`, it does
6
+ // NOT require a prototype manifest (you pick a style first). Read-only / choose-
7
+ // only: it never generates or mutates anything — the gallery just copies a
8
+ // command to the clipboard.
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { parseFlags } = require("../lib/args");
13
+ const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
14
+ const { ValidationError } = require("../lib/errors");
15
+ const { DEFAULT_STYLE, styleCatalog } = require("../lib/style-presets");
16
+ const { startDesignServer, findFreePort, waitForServer } = require("../lib/design-server");
17
+ const { DEFAULT_HOST, DEFAULT_PORT, loadStudioHtml, tryOpenBrowser } = require("./preview");
18
+
19
+ const STYLES_FLAG_DEFS = {
20
+ help: { type: "boolean" },
21
+ "project-path": { type: "string" },
22
+ port: { type: "string" },
23
+ "no-open": { type: "boolean" },
24
+ };
25
+
26
+ function cmdStylesHelp() {
27
+ console.log(`SDTK-DESIGN Style Gallery
28
+
29
+ Usage:
30
+ sdtk-design styles [--project-path <path>] [--port <n>] [--no-open]
31
+
32
+ Example:
33
+ sdtk-design styles
34
+ sdtk-design styles --no-open
35
+
36
+ What it does:
37
+ Opens the Preview Studio focused on the visual Style Gallery. Browse the
38
+ category-oriented style presets (e.g. ecommerce-retail) as cards — swatches,
39
+ a type sample, and a summary — and click one to copy its
40
+ "sdtk-design start --idea ... --style <preset>" command. Does NOT require a
41
+ prototype; pick a style before you generate.
42
+
43
+ Reads:
44
+ Nothing required. If docs/design/prototype/.manifest.json exists, its screens
45
+ are also previewable; otherwise only the gallery is shown.
46
+
47
+ Creates:
48
+ Nothing. The gallery is choose-only — it copies a command, it does not write.
49
+
50
+ Safety:
51
+ Local server on 127.0.0.1 only; no external network. Never shells out.
52
+ In WSL2/Docker, use --no-open and tunnel the printed URL (e.g. cloudflared).`);
53
+ return 0;
54
+ }
55
+
56
+ async function runDesignStyles({ projectPath, port, noOpen = false }) {
57
+ const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
58
+ if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
59
+ throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No server started.`);
60
+ }
61
+ const paths = describeDesignPaths(resolvedProjectPath);
62
+
63
+ // The gallery works without a prototype; load screens only if a manifest exists.
64
+ let manifest = { screens: [] };
65
+ if (fs.existsSync(paths.prototypeManifestPath)) {
66
+ try {
67
+ manifest = JSON.parse(fs.readFileSync(paths.prototypeManifestPath, "utf-8"));
68
+ } catch (_) {
69
+ manifest = { screens: [] }; // a malformed manifest must not block the gallery
70
+ }
71
+ }
72
+ const studioHtml = loadStudioHtml(manifest);
73
+
74
+ let chosenPort = DEFAULT_PORT;
75
+ if (port !== undefined && port !== "") {
76
+ const parsed = Number.parseInt(port, 10);
77
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
78
+ throw new ValidationError(`Invalid --port: ${port}. No server started.`);
79
+ }
80
+ chosenPort = parsed;
81
+ } else {
82
+ chosenPort = await findFreePort(DEFAULT_HOST, DEFAULT_PORT);
83
+ }
84
+
85
+ const skillMetaPath = path.join(__dirname, "..", "..", "skills", "design-prototype", "SKILL.md");
86
+
87
+ const { server, url } = await startDesignServer({
88
+ host: DEFAULT_HOST,
89
+ port: chosenPort,
90
+ projectPath: resolvedProjectPath,
91
+ studioHtml,
92
+ skillMetaPath: fs.existsSync(skillMetaPath) ? skillMetaPath : undefined,
93
+ styles: styleCatalog(),
94
+ defaultStyle: DEFAULT_STYLE,
95
+ });
96
+ await waitForServer(DEFAULT_HOST, chosenPort);
97
+
98
+ const galleryUrl = `${url}#styles`;
99
+ return { server, url: galleryUrl, styleCount: styleCatalog().length, projectPath: resolvedProjectPath, noOpen };
100
+ }
101
+
102
+ async function cmdStyles(args) {
103
+ const { flags, positionals } = parseFlags(args || [], STYLES_FLAG_DEFS);
104
+ if (flags.help) return cmdStylesHelp();
105
+ if (positionals.length > 0) {
106
+ throw new ValidationError(
107
+ `Unsupported styles argument: ${positionals.join(" ")}. Use sdtk-design styles [--project-path <path>] [--port <n>] [--no-open]. No server started.`
108
+ );
109
+ }
110
+
111
+ const result = await runDesignStyles({
112
+ projectPath: flags["project-path"],
113
+ port: flags.port,
114
+ noOpen: Boolean(flags["no-open"]),
115
+ });
116
+
117
+ console.log(`[design] Style Gallery: ${result.url}`);
118
+ console.log(`[design] ${result.styleCount} style preset(s). Click a card to copy its start command.`);
119
+ if (result.noOpen) {
120
+ console.log("[design] --no-open: not launching a browser. Open the URL above (tunnel it on WSL2/Docker).");
121
+ } else {
122
+ tryOpenBrowser(result.url);
123
+ }
124
+ console.log("[design] Press Ctrl+C to stop the server.");
125
+
126
+ return new Promise((resolve) => {
127
+ const shutdown = () => {
128
+ try { result.server.close(); } catch (_) {}
129
+ resolve(0);
130
+ };
131
+ process.on("SIGINT", shutdown);
132
+ process.on("SIGTERM", shutdown);
133
+ });
134
+ }
135
+
136
+ module.exports = {
137
+ cmdStyles,
138
+ cmdStylesHelp,
139
+ runDesignStyles,
140
+ };
@@ -118,6 +118,9 @@ function systemContent(briefContent, screenMapContent, style) {
118
118
  "- Avoid nested cards and decorative gradient backgrounds.",
119
119
  `- Preset density rule: ${preset.density}`,
120
120
  "",
121
+ ...(preset.elevation
122
+ ? ["## Depth & Elevation", "", `- ${preset.elevation}`, ""]
123
+ : []),
121
124
  "## Table / Dashboard Density",
122
125
  "",
123
126
  `- ${preset.dashboard}`,
@@ -134,6 +137,9 @@ function systemContent(briefContent, screenMapContent, style) {
134
137
  "",
135
138
  ...linesForBullets(preset.components),
136
139
  "",
140
+ ...(preset.dosAndDonts && preset.dosAndDonts.length
141
+ ? ["## Do's and Don'ts", "", ...linesForBullets(preset.dosAndDonts), ""]
142
+ : []),
137
143
  "## Mobile Layout Rules",
138
144
  "",
139
145
  `- ${preset.mobile}`,