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 +19 -5
- package/package.json +1 -1
- package/skills/design-prototype/SKILL.md +22 -0
- package/src/commands/help.js +4 -0
- package/src/commands/init.js +84 -24
- package/src/commands/preview.js +195 -0
- package/src/commands/styles.js +140 -0
- package/src/commands/system.js +6 -0
- package/src/index.js +8 -0
- package/src/lib/design-server.js +437 -0
- package/src/lib/runtime-skills.js +46 -0
- package/src/lib/style-presets.js +160 -0
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.
|
|
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,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`.
|
package/src/commands/help.js
CHANGED
|
@@ -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.
|
package/src/commands/init.js
CHANGED
|
@@ -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 [--
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
178
|
+
const targetDir = path.join(skillsDir, "design-prototype");
|
|
179
|
+
if (isPathInsideOrEqual(targetDir, projectPath)) {
|
|
149
180
|
assertProjectLocalPath(targetDir, projectPath, "skill directory");
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
};
|
package/src/commands/system.js
CHANGED
|
@@ -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}`,
|