sdtk-design-kit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -9
- package/package.json +1 -1
- package/src/commands/handoff.js +83 -8
- package/src/commands/help.js +14 -6
- package/src/commands/prototype.js +352 -0
- package/src/commands/review.js +56 -4
- package/src/commands/start.js +15 -4
- package/src/commands/status.js +5 -1
- package/src/commands/system.js +49 -19
- package/src/index.js +4 -0
- package/src/lib/design-paths.js +6 -0
- package/src/lib/domain-profile.js +108 -0
- package/src/lib/style-presets.js +150 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseFlags } = require("../lib/args");
|
|
6
|
+
const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
|
|
7
|
+
const { inferDomainProfile } = require("../lib/domain-profile");
|
|
8
|
+
const { ValidationError } = require("../lib/errors");
|
|
9
|
+
const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
|
|
10
|
+
|
|
11
|
+
const PROTOTYPE_FLAG_DEFS = {
|
|
12
|
+
help: { type: "boolean" },
|
|
13
|
+
"project-path": { type: "string" },
|
|
14
|
+
force: { type: "boolean" },
|
|
15
|
+
style: { type: "string" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const REQUIRED_ARTIFACTS = [
|
|
19
|
+
["docs/design/DESIGN_BRIEF.md", "designBriefPath"],
|
|
20
|
+
["docs/design/SCREEN_MAP.md", "screenMapPath"],
|
|
21
|
+
["docs/design/DESIGN_SYSTEM.md", "designSystemPath"],
|
|
22
|
+
["docs/design/wireframes/LANDING.md", "landingWireframePath"],
|
|
23
|
+
["docs/design/wireframes/ONBOARDING.md", "onboardingWireframePath"],
|
|
24
|
+
["docs/design/wireframes/DASHBOARD.md", "dashboardWireframePath"],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const THEME_TOKENS = {
|
|
28
|
+
"minimal-saas": {
|
|
29
|
+
bg: "#F7F8FA",
|
|
30
|
+
surface: "#FFFFFF",
|
|
31
|
+
text: "#1F2933",
|
|
32
|
+
muted: "#667085",
|
|
33
|
+
primary: "#2563EB",
|
|
34
|
+
accent: "#0F8A5F",
|
|
35
|
+
border: "#D9DEE7",
|
|
36
|
+
shadow: "0 14px 40px rgba(31,41,51,0.10)",
|
|
37
|
+
},
|
|
38
|
+
"premium-dashboard": {
|
|
39
|
+
bg: "#F5F7FB",
|
|
40
|
+
surface: "#FFFFFF",
|
|
41
|
+
text: "#111827",
|
|
42
|
+
muted: "#64748B",
|
|
43
|
+
primary: "#0F766E",
|
|
44
|
+
accent: "#2563EB",
|
|
45
|
+
border: "#D8E0EA",
|
|
46
|
+
shadow: "0 18px 48px rgba(15,23,42,0.12)",
|
|
47
|
+
},
|
|
48
|
+
"bold-founder": {
|
|
49
|
+
bg: "#111111",
|
|
50
|
+
surface: "#18181B",
|
|
51
|
+
text: "#FAFAFA",
|
|
52
|
+
muted: "#A1A1AA",
|
|
53
|
+
primary: "#F97316",
|
|
54
|
+
accent: "#22C55E",
|
|
55
|
+
border: "#27272A",
|
|
56
|
+
shadow: "0 20px 0 rgba(249,115,22,0.24)",
|
|
57
|
+
},
|
|
58
|
+
"warm-editorial": {
|
|
59
|
+
bg: "#FAF7F2",
|
|
60
|
+
surface: "#FFFFFF",
|
|
61
|
+
text: "#1C1A17",
|
|
62
|
+
muted: "#8A817A",
|
|
63
|
+
primary: "#C0512F",
|
|
64
|
+
accent: "#2F5B4F",
|
|
65
|
+
border: "rgba(47,91,79,0.16)",
|
|
66
|
+
shadow: "0 16px 36px rgba(28,26,23,0.08)",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function cmdPrototypeHelp() {
|
|
71
|
+
console.log(`SDTK-DESIGN Prototype
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
sdtk-design prototype [--style <preset>] [--project-path <path>] [--force]
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
sdtk-design prototype --style premium-dashboard
|
|
78
|
+
|
|
79
|
+
Style presets:
|
|
80
|
+
${availableStyleNames().join(", ")}
|
|
81
|
+
Default: inferred from docs/design/DESIGN_SYSTEM.md, then ${DEFAULT_STYLE}
|
|
82
|
+
|
|
83
|
+
Reads:
|
|
84
|
+
docs/design/DESIGN_BRIEF.md
|
|
85
|
+
docs/design/SCREEN_MAP.md
|
|
86
|
+
docs/design/DESIGN_SYSTEM.md
|
|
87
|
+
docs/design/wireframes/LANDING.md
|
|
88
|
+
docs/design/wireframes/ONBOARDING.md
|
|
89
|
+
docs/design/wireframes/DASHBOARD.md
|
|
90
|
+
|
|
91
|
+
Creates:
|
|
92
|
+
docs/design/prototype/index.html
|
|
93
|
+
|
|
94
|
+
Safety:
|
|
95
|
+
Local files only.
|
|
96
|
+
Existing prototype index is not overwritten unless --force is explicit.
|
|
97
|
+
No production app code outside docs/design/prototype.
|
|
98
|
+
No JavaScript runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function wireframePath(paths, fileName) {
|
|
103
|
+
return path.join(paths.wireframesPath, fileName);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requiredArtifactTargets(paths) {
|
|
107
|
+
return REQUIRED_ARTIFACTS.map(([relativePath, key]) => {
|
|
108
|
+
const filePath =
|
|
109
|
+
key === "landingWireframePath"
|
|
110
|
+
? wireframePath(paths, "LANDING.md")
|
|
111
|
+
: key === "onboardingWireframePath"
|
|
112
|
+
? wireframePath(paths, "ONBOARDING.md")
|
|
113
|
+
: key === "dashboardWireframePath"
|
|
114
|
+
? wireframePath(paths, "DASHBOARD.md")
|
|
115
|
+
: paths[key];
|
|
116
|
+
return { relativePath, filePath };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstLine(content, fallback) {
|
|
121
|
+
const line = String(content || "")
|
|
122
|
+
.split(/\r?\n/)
|
|
123
|
+
.map((value) => value.trim())
|
|
124
|
+
.find(Boolean);
|
|
125
|
+
return line ? line.replace(/^#\s*/, "") : fallback;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function escapeHtml(value) {
|
|
129
|
+
return String(value)
|
|
130
|
+
.replace(/&/g, "&")
|
|
131
|
+
.replace(/</g, "<")
|
|
132
|
+
.replace(/>/g, ">")
|
|
133
|
+
.replace(/"/g, """);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function inferStyleFromDesignSystem(designSystemContent) {
|
|
137
|
+
const content = String(designSystemContent || "");
|
|
138
|
+
return availableStyleNames().find((styleName) => new RegExp(`Preset:\\s*${styleName}\\b`, "i").test(content)) || DEFAULT_STYLE;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function prototypeContent({ style, briefContent, screenMapContent, designSystemContent, wireframeContents = [] }) {
|
|
142
|
+
const styleName = resolveStyleName(style);
|
|
143
|
+
const tokens = THEME_TOKENS[styleName];
|
|
144
|
+
const profile = inferDomainProfile({ briefContent, screenMapContent, designSystemContent, wireframeContents });
|
|
145
|
+
|
|
146
|
+
return `<!doctype html>
|
|
147
|
+
<html lang="en">
|
|
148
|
+
<head>
|
|
149
|
+
<meta charset="utf-8">
|
|
150
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
151
|
+
<title>${escapeHtml(profile.productName)} Prototype</title>
|
|
152
|
+
<style>
|
|
153
|
+
:root {
|
|
154
|
+
--bg: ${tokens.bg};
|
|
155
|
+
--surface: ${tokens.surface};
|
|
156
|
+
--text: ${tokens.text};
|
|
157
|
+
--muted: ${tokens.muted};
|
|
158
|
+
--primary: ${tokens.primary};
|
|
159
|
+
--accent: ${tokens.accent};
|
|
160
|
+
--border: ${tokens.border};
|
|
161
|
+
--shadow: ${tokens.shadow};
|
|
162
|
+
}
|
|
163
|
+
* { box-sizing: border-box; }
|
|
164
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
165
|
+
.sdtk-prototype { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 32px 0 56px; }
|
|
166
|
+
.prototype-header { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-bottom: 22px; }
|
|
167
|
+
.brand { font-weight: 800; letter-spacing: 0; }
|
|
168
|
+
.style-pill, .status-pill { display: inline-flex; align-items: center; min-height: 28px; border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; color: var(--muted); background: var(--surface); font-size: 12px; font-weight: 750; }
|
|
169
|
+
.prototype-section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); margin: 18px 0; overflow: hidden; }
|
|
170
|
+
.section-inner { padding: clamp(22px, 4vw, 44px); }
|
|
171
|
+
.eyebrow { margin: 0 0 8px; color: var(--primary); font-size: 12px; font-weight: 850; text-transform: uppercase; letter-spacing: 0; }
|
|
172
|
+
h1, h2, h3, p { margin-top: 0; }
|
|
173
|
+
h1 { max-width: 860px; font-size: clamp(34px, 7vw, 72px); line-height: 1.02; margin-bottom: 16px; }
|
|
174
|
+
h2 { font-size: clamp(26px, 4vw, 40px); line-height: 1.12; margin-bottom: 12px; }
|
|
175
|
+
h3 { font-size: 16px; margin-bottom: 8px; }
|
|
176
|
+
.support { max-width: 700px; color: var(--muted); font-size: 18px; }
|
|
177
|
+
.button-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }
|
|
178
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 42px; border-radius: 8px; padding: 0 18px; font-weight: 800; text-decoration: none; }
|
|
179
|
+
.btn-primary { background: var(--primary); color: #fff; }
|
|
180
|
+
.btn-secondary { color: var(--text); border: 1px solid var(--border); background: transparent; }
|
|
181
|
+
.workflow-grid, .metric-grid, .dashboard-grid { display: grid; gap: 14px; }
|
|
182
|
+
.workflow-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 28px; }
|
|
183
|
+
.workflow-card, .metric-card, .item-card, .form-card, .empty-state { border: 1px solid var(--border); border-radius: 8px; padding: 16px; background: color-mix(in srgb, var(--surface) 92%, var(--bg)); }
|
|
184
|
+
.workflow-card strong, .metric-card strong { display: block; font-size: 24px; }
|
|
185
|
+
.onboarding-layout { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(320px, 0.55fr); gap: 20px; align-items: start; }
|
|
186
|
+
.field { display: grid; gap: 6px; margin-bottom: 12px; }
|
|
187
|
+
.input-preview { min-height: 42px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; padding: 0 12px; color: var(--muted); }
|
|
188
|
+
.metric-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 16px 0; }
|
|
189
|
+
.dashboard-grid { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); }
|
|
190
|
+
.item-card { display: grid; gap: 8px; }
|
|
191
|
+
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
192
|
+
.status-pill { color: var(--primary); }
|
|
193
|
+
.empty-state { color: var(--muted); border-style: dashed; }
|
|
194
|
+
.style-premium-dashboard .metric-card strong { font-size: 30px; }
|
|
195
|
+
.style-bold-founder h1 { text-transform: uppercase; }
|
|
196
|
+
.style-warm-editorial h1, .style-warm-editorial h2 { font-family: Georgia, "Times New Roman", serif; }
|
|
197
|
+
@media (max-width: 780px) {
|
|
198
|
+
.prototype-header, .onboarding-layout { display: block; }
|
|
199
|
+
.workflow-grid, .metric-grid, .dashboard-grid { grid-template-columns: 1fr; }
|
|
200
|
+
h1 { font-size: 36px; }
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
203
|
+
</head>
|
|
204
|
+
<body>
|
|
205
|
+
<main class="sdtk-prototype style-${styleName}" data-style-preset="${styleName}">
|
|
206
|
+
<header class="prototype-header">
|
|
207
|
+
<div class="brand">${escapeHtml(profile.productName)}</div>
|
|
208
|
+
<span class="style-pill">Style preset: ${styleName}</span>
|
|
209
|
+
</header>
|
|
210
|
+
|
|
211
|
+
<section class="prototype-section section-landing" aria-labelledby="landing-title">
|
|
212
|
+
<div class="section-inner">
|
|
213
|
+
<p class="eyebrow">Landing</p>
|
|
214
|
+
<h1 id="landing-title">${escapeHtml(profile.promise)}</h1>
|
|
215
|
+
<p class="support">A demo-ready first screen with one clear CTA, a focused product promise, and a workflow preview that shows what happens after signup.</p>
|
|
216
|
+
<div class="button-row">
|
|
217
|
+
<a class="btn btn-primary" href="#onboarding">${escapeHtml(profile.primaryAction)}</a>
|
|
218
|
+
<a class="btn btn-secondary" href="#dashboard">${escapeHtml(profile.secondaryAction)}</a>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="workflow-grid">
|
|
221
|
+
<div class="workflow-card"><strong>01</strong><span>Capture a ${escapeHtml(profile.itemSingular)} with status and context.</span></div>
|
|
222
|
+
<div class="workflow-card"><strong>02</strong><span>Record the next action before it slips.</span></div>
|
|
223
|
+
<div class="workflow-card"><strong>03</strong><span>Scan the dashboard and choose what to do next.</span></div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
|
|
228
|
+
<section class="prototype-section section-onboarding" id="onboarding" aria-labelledby="onboarding-title">
|
|
229
|
+
<div class="section-inner onboarding-layout">
|
|
230
|
+
<div>
|
|
231
|
+
<p class="eyebrow">Onboarding</p>
|
|
232
|
+
<h2 id="onboarding-title">Create a focused ${escapeHtml(profile.setupLabel.toLowerCase())} in one step.</h2>
|
|
233
|
+
<p class="support">The setup flow asks only for the minimum needed before the user can create the first ${escapeHtml(profile.itemSingular)}.</p>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="form-card">
|
|
236
|
+
<div class="field"><strong>${escapeHtml(profile.setupNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.setupNameValue)}</div></div>
|
|
237
|
+
<div class="field"><strong>Primary workflow</strong><div class="input-preview">${escapeHtml(profile.itemAction)}</div></div>
|
|
238
|
+
<a class="btn btn-primary" href="#dashboard">Continue</a>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</section>
|
|
242
|
+
|
|
243
|
+
<section class="prototype-section section-dashboard" id="dashboard" aria-labelledby="dashboard-title">
|
|
244
|
+
<div class="section-inner">
|
|
245
|
+
<p class="eyebrow">Dashboard</p>
|
|
246
|
+
<h2 id="dashboard-title">${escapeHtml(profile.dashboardTitle)}</h2>
|
|
247
|
+
<div class="metric-grid">
|
|
248
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[0])}</span><strong>${escapeHtml(profile.metricValues[0])}</strong></div>
|
|
249
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[1])}</span><strong>${escapeHtml(profile.metricValues[1])}</strong></div>
|
|
250
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[2])}</span><strong>${escapeHtml(profile.metricValues[2])}</strong></div>
|
|
251
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[3])}</span><strong>${escapeHtml(profile.metricValues[3])}</strong></div>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="dashboard-grid">
|
|
254
|
+
<div class="form-card">
|
|
255
|
+
<h3>${escapeHtml(profile.itemAction)}</h3>
|
|
256
|
+
<div class="field"><strong>${escapeHtml(profile.itemNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemNameValue)}</div></div>
|
|
257
|
+
<div class="field"><strong>${escapeHtml(profile.itemContextLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemContextValue)}</div></div>
|
|
258
|
+
<a class="btn btn-primary" href="#dashboard">${escapeHtml(profile.saveAction)}</a>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="item-card">
|
|
261
|
+
<h3>${escapeHtml(profile.itemNameValue)}</h3>
|
|
262
|
+
<div class="item-meta">
|
|
263
|
+
<span class="status-pill">${escapeHtml(profile.statusExample)}</span>
|
|
264
|
+
<span class="status-pill">${escapeHtml(profile.itemContextValue)}</span>
|
|
265
|
+
</div>
|
|
266
|
+
<p class="support">Keep context, status, and next action visible in the ${escapeHtml(profile.collectionSurface)}.</p>
|
|
267
|
+
<div class="empty-state">${escapeHtml(profile.emptyState)}</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</section>
|
|
272
|
+
</main>
|
|
273
|
+
</body>
|
|
274
|
+
</html>
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function runDesignPrototype({ projectPath, force = false, style }) {
|
|
279
|
+
const explicitStyleName = typeof style === "string" && style.trim() ? resolveStyleName(style) : null;
|
|
280
|
+
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
281
|
+
if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
|
|
282
|
+
throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No project files were changed.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const paths = describeDesignPaths(resolvedProjectPath);
|
|
286
|
+
const missing = requiredArtifactTargets(paths).filter((target) => !fs.existsSync(target.filePath));
|
|
287
|
+
if (missing.length > 0) {
|
|
288
|
+
const list = missing.map((target) => target.relativePath).join(", ");
|
|
289
|
+
throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
|
|
290
|
+
}
|
|
291
|
+
if (fs.existsSync(paths.prototypeIndexPath) && !force) {
|
|
292
|
+
throw new ValidationError("docs/design/prototype/index.html already exists. Re-run with --force to replace this managed prototype.");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const designSystemContent = fs.readFileSync(paths.designSystemPath, "utf-8");
|
|
296
|
+
const styleName = explicitStyleName || inferStyleFromDesignSystem(designSystemContent);
|
|
297
|
+
const wireframeContents = REQUIRED_ARTIFACTS
|
|
298
|
+
.filter(([relativePath]) => relativePath.startsWith("docs/design/wireframes/"))
|
|
299
|
+
.map(([, key]) => {
|
|
300
|
+
const filePath =
|
|
301
|
+
key === "landingWireframePath"
|
|
302
|
+
? wireframePath(paths, "LANDING.md")
|
|
303
|
+
: key === "onboardingWireframePath"
|
|
304
|
+
? wireframePath(paths, "ONBOARDING.md")
|
|
305
|
+
: wireframePath(paths, "DASHBOARD.md");
|
|
306
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
307
|
+
});
|
|
308
|
+
const content = prototypeContent({
|
|
309
|
+
style: styleName,
|
|
310
|
+
briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
|
|
311
|
+
screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
|
|
312
|
+
designSystemContent,
|
|
313
|
+
wireframeContents,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
|
|
317
|
+
fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
projectPath: resolvedProjectPath,
|
|
321
|
+
relativePrototypePath: "docs/design/prototype/index.html",
|
|
322
|
+
forced: Boolean(force),
|
|
323
|
+
style: styleName,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cmdPrototype(args) {
|
|
328
|
+
const { flags } = parseFlags(args || [], PROTOTYPE_FLAG_DEFS);
|
|
329
|
+
if (flags.help) return cmdPrototypeHelp();
|
|
330
|
+
|
|
331
|
+
const result = runDesignPrototype({
|
|
332
|
+
projectPath: flags["project-path"],
|
|
333
|
+
force: Boolean(flags.force),
|
|
334
|
+
style: flags.style,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
|
|
338
|
+
console.log(`[design] Style: ${result.style}`);
|
|
339
|
+
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
340
|
+
console.log("[design] Prototype mode: static HTML/CSS preview only.");
|
|
341
|
+
console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
|
|
342
|
+
console.log("[design] Next: sdtk-design review --artifact docs/design/prototype/index.html");
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
cmdPrototype,
|
|
348
|
+
cmdPrototypeHelp,
|
|
349
|
+
inferStyleFromDesignSystem,
|
|
350
|
+
prototypeContent,
|
|
351
|
+
runDesignPrototype,
|
|
352
|
+
};
|
package/src/commands/review.js
CHANGED
|
@@ -18,20 +18,22 @@ function cmdReviewHelp() {
|
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
20
20
|
sdtk-design review --artifact docs/design/wireframes/LANDING.md [--project-path <path>] [--force]
|
|
21
|
+
sdtk-design review --artifact docs/design/prototype/index.html [--project-path <path>] [--force]
|
|
21
22
|
|
|
22
23
|
Example:
|
|
23
24
|
sdtk-design review --artifact docs/design/wireframes/LANDING.md
|
|
25
|
+
sdtk-design review --artifact docs/design/prototype/index.html
|
|
24
26
|
|
|
25
27
|
Reads:
|
|
26
|
-
A project-local markdown design artifact.
|
|
28
|
+
A project-local markdown or static HTML design artifact.
|
|
27
29
|
|
|
28
30
|
Creates:
|
|
29
31
|
docs/design/reviews/DESIGN_REVIEW_YYYYMMDD.md
|
|
30
32
|
|
|
31
33
|
Safety:
|
|
32
|
-
Local
|
|
34
|
+
Local artifact review only.
|
|
33
35
|
Existing same-day review report is not overwritten unless --force is explicit.
|
|
34
|
-
No URL, browser, screenshot, vision, or DOM review
|
|
36
|
+
No URL, browser, screenshot, vision, or DOM review.
|
|
35
37
|
No .sdtk/atlas creation or mutation.
|
|
36
38
|
No SDTK-WIKI output mutation.
|
|
37
39
|
No network call, Pro entitlement, or production app code generation.`);
|
|
@@ -62,7 +64,55 @@ function hasHeading(content, heading) {
|
|
|
62
64
|
return content.toLowerCase().includes(heading.toLowerCase());
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function isPrototypeHtmlArtifact(artifactRelativePath, artifactContent) {
|
|
68
|
+
const lowerPath = toPosix(artifactRelativePath).toLowerCase();
|
|
69
|
+
const lowerContent = String(artifactContent || "").toLowerCase();
|
|
70
|
+
return lowerPath.endsWith(".html") || lowerContent.includes("<!doctype html") || lowerContent.includes("<html");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function includesAny(content, terms) {
|
|
74
|
+
const lower = String(content || "").toLowerCase();
|
|
75
|
+
return terms.some((term) => lower.includes(term.toLowerCase()));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function visualPolishChecks({ artifactRelativePath, artifactContent, prototypeHtml }) {
|
|
79
|
+
const lower = String(artifactContent || "").toLowerCase();
|
|
80
|
+
const hasLanding = lower.includes("section-landing") || lower.includes("landing");
|
|
81
|
+
const hasOnboarding = lower.includes("section-onboarding") || lower.includes("onboarding");
|
|
82
|
+
const hasDashboard = lower.includes("section-dashboard") || lower.includes("dashboard");
|
|
83
|
+
const hasPreset = lower.includes("data-style-preset") || lower.includes("style-premium-dashboard") || lower.includes("style-minimal-saas") || lower.includes("style-bold-founder") || lower.includes("style-warm-editorial");
|
|
84
|
+
const hasResponsive = lower.includes("@media") || lower.includes("viewport") || lower.includes("mobile");
|
|
85
|
+
const hasCta = includesAny(artifactContent, ["btn-primary", "Primary CTA", "Create workspace", "Add lead", "Continue", "Start"]);
|
|
86
|
+
const hasDashboardDensity = hasDashboard && includesAny(artifactContent, ["metric", "kpi", "pipeline", "status", "lead", "table", "card"]);
|
|
87
|
+
const hasAccessibility = includesAny(artifactContent, ["accessibility", "aria-", "button", "label", "44px", "focus"]);
|
|
88
|
+
const hasSpacing = includesAny(artifactContent, ["gap:", "padding:", "spacing", "density", "grid", "margin"]);
|
|
89
|
+
const modeEvidence = prototypeHtml
|
|
90
|
+
? "Prototype HTML is checked as visual direction only, not production application code."
|
|
91
|
+
: "Markdown artifact is checked for implementation-ready visual direction.";
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
"## Visual Polish Checks",
|
|
95
|
+
"",
|
|
96
|
+
"| Check | Result | Evidence |",
|
|
97
|
+
"|---|---|---|",
|
|
98
|
+
`| Hierarchy | ${hasLanding && hasOnboarding && hasDashboard ? "Pass" : "Needs attention"} | ${hasLanding && hasOnboarding && hasDashboard ? "Landing, onboarding, and dashboard sections create a clear read order." : "Ensure the artifact clearly separates landing, onboarding, and dashboard intent."} |`,
|
|
99
|
+
`| Spacing / Density | ${hasSpacing ? "Pass" : "Needs attention"} | ${hasSpacing ? "Artifact includes grid, gap, padding, margin, or spacing guidance." : "Add spacing and density rules so the implementation does not regress into a flat wireframe."} |`,
|
|
100
|
+
`| CTA Clarity | ${hasCta ? "Pass" : "Needs attention"} | ${hasCta ? "A concrete primary action is visible in the artifact." : "Add one dominant CTA with concrete action copy."} |`,
|
|
101
|
+
`| Dashboard Information Density | ${hasDashboardDensity ? "Pass" : "Needs attention"} | ${hasDashboardDensity ? "Dashboard direction includes metrics, pipeline/status, cards, lists, or table surfaces." : "Dashboard needs enough KPI, status, list, or table detail to guide a real MVP screen."} |`,
|
|
102
|
+
`| Responsive / Mobile Risk | ${hasResponsive ? "Pass" : "Needs attention"} | ${hasResponsive ? "Responsive or mobile behavior is represented." : "Add mobile breakpoint or narrow-screen behavior before implementation."} |`,
|
|
103
|
+
`| Accessibility Baseline | ${hasAccessibility ? "Pass" : "Needs attention"} | ${hasAccessibility ? "Artifact includes accessible-control or baseline accessibility signals." : "Add labels, focus, touch-target, and color-not-alone guidance."} |`,
|
|
104
|
+
`| Anti-Wireframe / Mockup Warning | ${prototypeHtml && hasPreset ? "Pass" : "Needs attention"} | ${prototypeHtml && hasPreset ? "Prototype includes style preset tokens/classes and should guide visual polish beyond basic wireframes." : "Do not treat the artifact as complete UI; add concrete style preset tokens/classes or visual contract details."} |`,
|
|
105
|
+
"",
|
|
106
|
+
`- Reviewed visual artifact: \`${artifactRelativePath}\`.`,
|
|
107
|
+
`- ${modeEvidence}`,
|
|
108
|
+
"- Keep the output local-first: no browser, screenshot, network, Pro entitlement, or app generation is required for this review.",
|
|
109
|
+
"",
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
65
113
|
function reviewContent({ artifactRelativePath, artifactContent }) {
|
|
114
|
+
const prototypeHtml = isPrototypeHtmlArtifact(artifactRelativePath, artifactContent);
|
|
115
|
+
const reviewMode = prototypeHtml ? "local prototype HTML review" : "local markdown artifact review";
|
|
66
116
|
const hasPrimaryCta = hasHeading(artifactContent, "Primary CTA") || artifactContent.toLowerCase().includes("cta");
|
|
67
117
|
const hasMobile = hasHeading(artifactContent, "Mobile Notes") || artifactContent.toLowerCase().includes("mobile");
|
|
68
118
|
const hasStates = hasHeading(artifactContent, "State Handling") || ["empty", "success", "error"].every((term) => artifactContent.toLowerCase().includes(term));
|
|
@@ -74,7 +124,7 @@ function reviewContent({ artifactRelativePath, artifactContent }) {
|
|
|
74
124
|
"## Reviewed Artifact",
|
|
75
125
|
"",
|
|
76
126
|
`- Artifact: \`${artifactRelativePath}\``,
|
|
77
|
-
|
|
127
|
+
`- Review mode: ${reviewMode}`,
|
|
78
128
|
"- No URL, browser, screenshot, vision, or DOM review was used.",
|
|
79
129
|
"",
|
|
80
130
|
"## Findings By Severity",
|
|
@@ -86,6 +136,7 @@ function reviewContent({ artifactRelativePath, artifactContent }) {
|
|
|
86
136
|
`| Medium | ${hasStates ? "State handling is represented." : "Empty, success, or error states are incomplete."} | ${hasStates ? "Carry the states into implementation acceptance criteria." : "Document empty, success, and error handling."} |`,
|
|
87
137
|
`| Low | ${hasAcceptance ? "Acceptance criteria are present." : "Acceptance criteria are missing."} | ${hasAcceptance ? "Use criteria as SDTK-CODE test obligations." : "Add concrete acceptance criteria before coding."} |`,
|
|
88
138
|
"",
|
|
139
|
+
...visualPolishChecks({ artifactRelativePath, artifactContent, prototypeHtml }),
|
|
89
140
|
"## UX Issues",
|
|
90
141
|
"",
|
|
91
142
|
"- Keep the screen focused on one job and one dominant next action.",
|
|
@@ -171,6 +222,7 @@ module.exports = {
|
|
|
171
222
|
cmdReview,
|
|
172
223
|
cmdReviewHelp,
|
|
173
224
|
formatDateYYYYMMDD,
|
|
225
|
+
isPrototypeHtmlArtifact,
|
|
174
226
|
reviewContent,
|
|
175
227
|
runDesignReview,
|
|
176
228
|
};
|
package/src/commands/start.js
CHANGED
|
@@ -9,12 +9,14 @@ const { runDesignWireframe } = require("./wireframe");
|
|
|
9
9
|
const { parseFlags } = require("../lib/args");
|
|
10
10
|
const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
|
|
11
11
|
const { ValidationError } = require("../lib/errors");
|
|
12
|
+
const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
|
|
12
13
|
|
|
13
14
|
const START_FLAG_DEFS = {
|
|
14
15
|
help: { type: "boolean" },
|
|
15
16
|
idea: { type: "string" },
|
|
16
17
|
"project-path": { type: "string" },
|
|
17
18
|
force: { type: "boolean" },
|
|
19
|
+
style: { type: "string" },
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
const REQUIRED_WIREFRAME_FILES = ["LANDING.md", "ONBOARDING.md", "DASHBOARD.md"];
|
|
@@ -23,10 +25,15 @@ function cmdStartHelp() {
|
|
|
23
25
|
console.log(`SDTK-DESIGN Start
|
|
24
26
|
|
|
25
27
|
Usage:
|
|
26
|
-
sdtk-design start --idea "<rough MVP idea>" [--project-path <path>] [--force]
|
|
28
|
+
sdtk-design start --idea "<rough MVP idea>" [--style <preset>] [--project-path <path>] [--force]
|
|
27
29
|
|
|
28
30
|
Example:
|
|
29
31
|
sdtk-design start --idea "I want to build a lightweight CRM for solo consultants to track leads."
|
|
32
|
+
sdtk-design start --idea "ClientPulse for consultants" --style premium-dashboard
|
|
33
|
+
|
|
34
|
+
Style presets:
|
|
35
|
+
${availableStyleNames().join(", ")}
|
|
36
|
+
Default: ${DEFAULT_STYLE}
|
|
30
37
|
|
|
31
38
|
Runs:
|
|
32
39
|
brief -> screens -> wireframe --screen all -> system
|
|
@@ -65,11 +72,12 @@ function coreOutputTargets(paths) {
|
|
|
65
72
|
];
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
function runDesignStart({ idea, projectPath, force = false }) {
|
|
75
|
+
function runDesignStart({ idea, projectPath, force = false, style = DEFAULT_STYLE }) {
|
|
69
76
|
const normalizedIdea = normalizeIdea(idea);
|
|
70
77
|
if (!normalizedIdea) {
|
|
71
78
|
throw new ValidationError('Missing required --idea "<rough MVP idea>". No project files were changed.');
|
|
72
79
|
}
|
|
80
|
+
const styleName = resolveStyleName(style);
|
|
73
81
|
|
|
74
82
|
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
75
83
|
if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
|
|
@@ -86,12 +94,13 @@ function runDesignStart({ idea, projectPath, force = false }) {
|
|
|
86
94
|
runDesignBrief({ idea: normalizedIdea, projectPath: resolvedProjectPath, force });
|
|
87
95
|
runDesignScreens({ projectPath: resolvedProjectPath, force });
|
|
88
96
|
runDesignWireframe({ screen: "all", projectPath: resolvedProjectPath, force });
|
|
89
|
-
runDesignSystem({ projectPath: resolvedProjectPath, force });
|
|
97
|
+
runDesignSystem({ projectPath: resolvedProjectPath, force, style: styleName });
|
|
90
98
|
|
|
91
99
|
return {
|
|
92
100
|
projectPath: resolvedProjectPath,
|
|
93
101
|
written: coreOutputTargets(paths).map((target) => target.relativePath),
|
|
94
102
|
forced: Boolean(force),
|
|
103
|
+
style: styleName,
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
|
|
@@ -103,13 +112,15 @@ function cmdStart(args) {
|
|
|
103
112
|
idea: flags.idea,
|
|
104
113
|
projectPath: flags["project-path"],
|
|
105
114
|
force: Boolean(flags.force),
|
|
115
|
+
style: flags.style,
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
console.log(`[design] Started SDTK-DESIGN package: ${result.projectPath}`);
|
|
109
119
|
console.log(`[design] Wrote core artifacts: ${result.written.join(", ")}`);
|
|
120
|
+
console.log(`[design] Style: ${result.style}`);
|
|
110
121
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
111
122
|
console.log("[design] No review, handoff, URL, browser, screenshot, vision, network, .sdtk/atlas, or SDTK-WIKI output was used.");
|
|
112
|
-
console.log("[design] Next: sdtk-design
|
|
123
|
+
console.log("[design] Next: sdtk-design prototype");
|
|
113
124
|
return 0;
|
|
114
125
|
}
|
|
115
126
|
|
package/src/commands/status.js
CHANGED
|
@@ -41,6 +41,7 @@ function artifactPlan(paths) {
|
|
|
41
41
|
{ label: "Onboarding wireframe", relativePath: "docs/design/wireframes/ONBOARDING.md", filePath: path.join(paths.wireframesPath, "ONBOARDING.md"), phase: "core" },
|
|
42
42
|
{ label: "Dashboard wireframe", relativePath: "docs/design/wireframes/DASHBOARD.md", filePath: path.join(paths.wireframesPath, "DASHBOARD.md"), phase: "core" },
|
|
43
43
|
{ label: "Design system", relativePath: "docs/design/DESIGN_SYSTEM.md", filePath: paths.designSystemPath, phase: "core" },
|
|
44
|
+
{ label: "Prototype preview", relativePath: "docs/design/prototype/index.html", filePath: paths.prototypeIndexPath, phase: "prototype" },
|
|
44
45
|
{ label: "Design handoff", relativePath: "docs/design/DESIGN_HANDOFF.md", filePath: paths.designHandoffPath, phase: "handoff" },
|
|
45
46
|
];
|
|
46
47
|
}
|
|
@@ -65,13 +66,16 @@ function inspectDesignStatus(projectPath) {
|
|
|
65
66
|
}));
|
|
66
67
|
const reviewExists = hasReview(paths);
|
|
67
68
|
const coreMissing = artifacts.filter((artifact) => artifact.phase === "core" && !artifact.exists);
|
|
69
|
+
const prototypeMissing = artifacts.find((artifact) => artifact.phase === "prototype" && !artifact.exists);
|
|
68
70
|
const handoffMissing = artifacts.find((artifact) => artifact.phase === "handoff" && !artifact.exists);
|
|
69
71
|
|
|
70
72
|
let nextCommand = "SDTK-CODE can consume docs/design/DESIGN_HANDOFF.md";
|
|
71
73
|
if (coreMissing.length > 0) {
|
|
72
74
|
nextCommand = 'sdtk-design start --idea "<idea>"';
|
|
75
|
+
} else if (prototypeMissing) {
|
|
76
|
+
nextCommand = "sdtk-design prototype";
|
|
73
77
|
} else if (!reviewExists) {
|
|
74
|
-
nextCommand = "sdtk-design review --artifact docs/design/
|
|
78
|
+
nextCommand = "sdtk-design review --artifact docs/design/prototype/index.html";
|
|
75
79
|
} else if (handoffMissing) {
|
|
76
80
|
nextCommand = "sdtk-design handoff";
|
|
77
81
|
}
|