sdtk-design-kit 0.1.0 → 0.1.2
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 +17 -6
- package/src/commands/prototype.js +503 -0
- package/src/commands/review.js +56 -4
- package/src/commands/start.js +97 -11
- package/src/commands/status.js +42 -2
- package/src/commands/system.js +49 -19
- package/src/index.js +4 -0
- package/src/lib/design-input-contract.js +423 -0
- package/src/lib/design-paths.js +9 -0
- package/src/lib/design-profiles.js +58 -0
- package/src/lib/domain-profile.js +108 -0
- package/src/lib/style-presets.js +150 -0
|
@@ -0,0 +1,503 @@
|
|
|
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 readInputContractState(paths) {
|
|
71
|
+
if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
|
|
76
|
+
} catch (_err) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cmdPrototypeHelp() {
|
|
82
|
+
console.log(`SDTK-DESIGN Prototype
|
|
83
|
+
|
|
84
|
+
Usage:
|
|
85
|
+
sdtk-design prototype [--style <preset>] [--project-path <path>] [--force]
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
sdtk-design prototype --style premium-dashboard
|
|
89
|
+
|
|
90
|
+
Style presets:
|
|
91
|
+
${availableStyleNames().join(", ")}
|
|
92
|
+
Default: inferred from docs/design/DESIGN_SYSTEM.md, then ${DEFAULT_STYLE}
|
|
93
|
+
|
|
94
|
+
Reads:
|
|
95
|
+
docs/design/DESIGN_BRIEF.md
|
|
96
|
+
docs/design/SCREEN_MAP.md
|
|
97
|
+
docs/design/DESIGN_SYSTEM.md
|
|
98
|
+
docs/design/wireframes/LANDING.md
|
|
99
|
+
docs/design/wireframes/ONBOARDING.md
|
|
100
|
+
docs/design/wireframes/DASHBOARD.md
|
|
101
|
+
.sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
|
|
102
|
+
|
|
103
|
+
Creates:
|
|
104
|
+
docs/design/prototype/index.html
|
|
105
|
+
|
|
106
|
+
Safety:
|
|
107
|
+
Local files only.
|
|
108
|
+
Existing prototype index is not overwritten unless --force is explicit.
|
|
109
|
+
No production app code outside docs/design/prototype.
|
|
110
|
+
No JavaScript runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function wireframePath(paths, fileName) {
|
|
115
|
+
return path.join(paths.wireframesPath, fileName);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requiredArtifactTargets(paths) {
|
|
119
|
+
return REQUIRED_ARTIFACTS.map(([relativePath, key]) => {
|
|
120
|
+
const filePath =
|
|
121
|
+
key === "landingWireframePath"
|
|
122
|
+
? wireframePath(paths, "LANDING.md")
|
|
123
|
+
: key === "onboardingWireframePath"
|
|
124
|
+
? wireframePath(paths, "ONBOARDING.md")
|
|
125
|
+
: key === "dashboardWireframePath"
|
|
126
|
+
? wireframePath(paths, "DASHBOARD.md")
|
|
127
|
+
: paths[key];
|
|
128
|
+
return { relativePath, filePath };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function firstLine(content, fallback) {
|
|
133
|
+
const line = String(content || "")
|
|
134
|
+
.split(/\r?\n/)
|
|
135
|
+
.map((value) => value.trim())
|
|
136
|
+
.find(Boolean);
|
|
137
|
+
return line ? line.replace(/^#\s*/, "") : fallback;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function escapeHtml(value) {
|
|
141
|
+
return String(value)
|
|
142
|
+
.replace(/&/g, "&")
|
|
143
|
+
.replace(/</g, "<")
|
|
144
|
+
.replace(/>/g, ">")
|
|
145
|
+
.replace(/"/g, """);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function inferStyleFromDesignSystem(designSystemContent) {
|
|
149
|
+
const content = String(designSystemContent || "");
|
|
150
|
+
return availableStyleNames().find((styleName) => new RegExp(`Preset:\\s*${styleName}\\b`, "i").test(content)) || DEFAULT_STYLE;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function prototypeContent({ style, briefContent, screenMapContent, designSystemContent, wireframeContents = [] }) {
|
|
154
|
+
const styleName = resolveStyleName(style);
|
|
155
|
+
const tokens = THEME_TOKENS[styleName];
|
|
156
|
+
const profile = inferDomainProfile({ briefContent, screenMapContent, designSystemContent, wireframeContents });
|
|
157
|
+
|
|
158
|
+
return `<!doctype html>
|
|
159
|
+
<html lang="en">
|
|
160
|
+
<head>
|
|
161
|
+
<meta charset="utf-8">
|
|
162
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
163
|
+
<title>${escapeHtml(profile.productName)} Prototype</title>
|
|
164
|
+
<style>
|
|
165
|
+
:root {
|
|
166
|
+
--bg: ${tokens.bg};
|
|
167
|
+
--surface: ${tokens.surface};
|
|
168
|
+
--text: ${tokens.text};
|
|
169
|
+
--muted: ${tokens.muted};
|
|
170
|
+
--primary: ${tokens.primary};
|
|
171
|
+
--accent: ${tokens.accent};
|
|
172
|
+
--border: ${tokens.border};
|
|
173
|
+
--shadow: ${tokens.shadow};
|
|
174
|
+
}
|
|
175
|
+
* { box-sizing: border-box; }
|
|
176
|
+
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; }
|
|
177
|
+
.sdtk-prototype { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 32px 0 56px; }
|
|
178
|
+
.prototype-header { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-bottom: 22px; }
|
|
179
|
+
.brand { font-weight: 800; letter-spacing: 0; }
|
|
180
|
+
.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; }
|
|
181
|
+
.prototype-section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); margin: 18px 0; overflow: hidden; }
|
|
182
|
+
.section-inner { padding: clamp(22px, 4vw, 44px); }
|
|
183
|
+
.eyebrow { margin: 0 0 8px; color: var(--primary); font-size: 12px; font-weight: 850; text-transform: uppercase; letter-spacing: 0; }
|
|
184
|
+
h1, h2, h3, p { margin-top: 0; }
|
|
185
|
+
h1 { max-width: 860px; font-size: clamp(34px, 7vw, 72px); line-height: 1.02; margin-bottom: 16px; }
|
|
186
|
+
h2 { font-size: clamp(26px, 4vw, 40px); line-height: 1.12; margin-bottom: 12px; }
|
|
187
|
+
h3 { font-size: 16px; margin-bottom: 8px; }
|
|
188
|
+
.support { max-width: 700px; color: var(--muted); font-size: 18px; }
|
|
189
|
+
.button-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }
|
|
190
|
+
.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; }
|
|
191
|
+
.btn-primary { background: var(--primary); color: #fff; }
|
|
192
|
+
.btn-secondary { color: var(--text); border: 1px solid var(--border); background: transparent; }
|
|
193
|
+
.workflow-grid, .metric-grid, .dashboard-grid { display: grid; gap: 14px; }
|
|
194
|
+
.workflow-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 28px; }
|
|
195
|
+
.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)); }
|
|
196
|
+
.workflow-card strong, .metric-card strong { display: block; font-size: 24px; }
|
|
197
|
+
.onboarding-layout { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(320px, 0.55fr); gap: 20px; align-items: start; }
|
|
198
|
+
.field { display: grid; gap: 6px; margin-bottom: 12px; }
|
|
199
|
+
.input-preview { min-height: 42px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; padding: 0 12px; color: var(--muted); }
|
|
200
|
+
.metric-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 16px 0; }
|
|
201
|
+
.dashboard-grid { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); }
|
|
202
|
+
.item-card { display: grid; gap: 8px; }
|
|
203
|
+
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
204
|
+
.status-pill { color: var(--primary); }
|
|
205
|
+
.empty-state { color: var(--muted); border-style: dashed; }
|
|
206
|
+
.style-premium-dashboard .metric-card strong { font-size: 30px; }
|
|
207
|
+
.style-bold-founder h1 { text-transform: uppercase; }
|
|
208
|
+
.style-warm-editorial h1, .style-warm-editorial h2 { font-family: Georgia, "Times New Roman", serif; }
|
|
209
|
+
@media (max-width: 780px) {
|
|
210
|
+
.prototype-header, .onboarding-layout { display: block; }
|
|
211
|
+
.workflow-grid, .metric-grid, .dashboard-grid { grid-template-columns: 1fr; }
|
|
212
|
+
h1 { font-size: 36px; }
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
215
|
+
</head>
|
|
216
|
+
<body>
|
|
217
|
+
<main class="sdtk-prototype style-${styleName}" data-style-preset="${styleName}">
|
|
218
|
+
<header class="prototype-header">
|
|
219
|
+
<div class="brand">${escapeHtml(profile.productName)}</div>
|
|
220
|
+
<span class="style-pill">Style preset: ${styleName}</span>
|
|
221
|
+
</header>
|
|
222
|
+
|
|
223
|
+
<section class="prototype-section section-landing" aria-labelledby="landing-title">
|
|
224
|
+
<div class="section-inner">
|
|
225
|
+
<p class="eyebrow">Landing</p>
|
|
226
|
+
<h1 id="landing-title">${escapeHtml(profile.promise)}</h1>
|
|
227
|
+
<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>
|
|
228
|
+
<div class="button-row">
|
|
229
|
+
<a class="btn btn-primary" href="#onboarding">${escapeHtml(profile.primaryAction)}</a>
|
|
230
|
+
<a class="btn btn-secondary" href="#dashboard">${escapeHtml(profile.secondaryAction)}</a>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="workflow-grid">
|
|
233
|
+
<div class="workflow-card"><strong>01</strong><span>Capture a ${escapeHtml(profile.itemSingular)} with status and context.</span></div>
|
|
234
|
+
<div class="workflow-card"><strong>02</strong><span>Record the next action before it slips.</span></div>
|
|
235
|
+
<div class="workflow-card"><strong>03</strong><span>Scan the dashboard and choose what to do next.</span></div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</section>
|
|
239
|
+
|
|
240
|
+
<section class="prototype-section section-onboarding" id="onboarding" aria-labelledby="onboarding-title">
|
|
241
|
+
<div class="section-inner onboarding-layout">
|
|
242
|
+
<div>
|
|
243
|
+
<p class="eyebrow">Onboarding</p>
|
|
244
|
+
<h2 id="onboarding-title">Create a focused ${escapeHtml(profile.setupLabel.toLowerCase())} in one step.</h2>
|
|
245
|
+
<p class="support">The setup flow asks only for the minimum needed before the user can create the first ${escapeHtml(profile.itemSingular)}.</p>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="form-card">
|
|
248
|
+
<div class="field"><strong>${escapeHtml(profile.setupNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.setupNameValue)}</div></div>
|
|
249
|
+
<div class="field"><strong>Primary workflow</strong><div class="input-preview">${escapeHtml(profile.itemAction)}</div></div>
|
|
250
|
+
<a class="btn btn-primary" href="#dashboard">Continue</a>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
|
|
255
|
+
<section class="prototype-section section-dashboard" id="dashboard" aria-labelledby="dashboard-title">
|
|
256
|
+
<div class="section-inner">
|
|
257
|
+
<p class="eyebrow">Dashboard</p>
|
|
258
|
+
<h2 id="dashboard-title">${escapeHtml(profile.dashboardTitle)}</h2>
|
|
259
|
+
<div class="metric-grid">
|
|
260
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[0])}</span><strong>${escapeHtml(profile.metricValues[0])}</strong></div>
|
|
261
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[1])}</span><strong>${escapeHtml(profile.metricValues[1])}</strong></div>
|
|
262
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[2])}</span><strong>${escapeHtml(profile.metricValues[2])}</strong></div>
|
|
263
|
+
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[3])}</span><strong>${escapeHtml(profile.metricValues[3])}</strong></div>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="dashboard-grid">
|
|
266
|
+
<div class="form-card">
|
|
267
|
+
<h3>${escapeHtml(profile.itemAction)}</h3>
|
|
268
|
+
<div class="field"><strong>${escapeHtml(profile.itemNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemNameValue)}</div></div>
|
|
269
|
+
<div class="field"><strong>${escapeHtml(profile.itemContextLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemContextValue)}</div></div>
|
|
270
|
+
<a class="btn btn-primary" href="#dashboard">${escapeHtml(profile.saveAction)}</a>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="item-card">
|
|
273
|
+
<h3>${escapeHtml(profile.itemNameValue)}</h3>
|
|
274
|
+
<div class="item-meta">
|
|
275
|
+
<span class="status-pill">${escapeHtml(profile.statusExample)}</span>
|
|
276
|
+
<span class="status-pill">${escapeHtml(profile.itemContextValue)}</span>
|
|
277
|
+
</div>
|
|
278
|
+
<p class="support">Keep context, status, and next action visible in the ${escapeHtml(profile.collectionSurface)}.</p>
|
|
279
|
+
<div class="empty-state">${escapeHtml(profile.emptyState)}</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</section>
|
|
284
|
+
</main>
|
|
285
|
+
</body>
|
|
286
|
+
</html>
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeScreenRole(screen) {
|
|
291
|
+
const token = `${screen.screenId || ""} ${screen.title || ""}`.toLowerCase();
|
|
292
|
+
if (token.includes("home")) return "home";
|
|
293
|
+
if (token.includes("category")) return "category";
|
|
294
|
+
if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
|
|
295
|
+
if (token.includes("search")) return "search";
|
|
296
|
+
if (token.includes("cart")) return "cart";
|
|
297
|
+
if (token.includes("checkout")) return "checkout";
|
|
298
|
+
if (token.includes("order-history") || token.includes("order history")) return "order-history";
|
|
299
|
+
if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
|
|
300
|
+
if (token.includes("account")) return "account-info";
|
|
301
|
+
if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
|
|
302
|
+
return screen.screenId || "screen";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderPrimitiveChips(primitiveNames) {
|
|
306
|
+
if (!Array.isArray(primitiveNames) || primitiveNames.length === 0) return "";
|
|
307
|
+
return `<div class="chip-row">${primitiveNames.map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join("")}</div>`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function renderStateChips(requiredStates) {
|
|
311
|
+
if (!Array.isArray(requiredStates) || requiredStates.length === 0) return "";
|
|
312
|
+
return `<div class="chip-row">${requiredStates.slice(0, 3).map((item) => `<span class="chip chip-state">${escapeHtml(item)}</span>`).join("")}</div>`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function multiScreenPrototypeContent({ style, statePayload }) {
|
|
316
|
+
const styleName = resolveStyleName(style);
|
|
317
|
+
const tokens = THEME_TOKENS[styleName];
|
|
318
|
+
const screens = statePayload.screenModel.screens;
|
|
319
|
+
const profileSelection = statePayload.profileSelection || null;
|
|
320
|
+
const profilePrimitives = statePayload.profilePrimitives || null;
|
|
321
|
+
const mappedRefs = statePayload.referenceMap && typeof statePayload.referenceMap.mappedCount === "number" ? statePayload.referenceMap.mappedCount : 0;
|
|
322
|
+
|
|
323
|
+
const sections = screens
|
|
324
|
+
.map((screen) => {
|
|
325
|
+
const screenRole = normalizeScreenRole(screen);
|
|
326
|
+
const primitiveHints =
|
|
327
|
+
profilePrimitives &&
|
|
328
|
+
profilePrimitives.screenRoleHints &&
|
|
329
|
+
Array.isArray(profilePrimitives.screenRoleHints[screenRole])
|
|
330
|
+
? profilePrimitives.screenRoleHints[screenRole]
|
|
331
|
+
: [];
|
|
332
|
+
const majorSections = Array.isArray(screen.majorSections) && screen.majorSections.length > 0 ? screen.majorSections : ["Primary content", "Supporting panel"];
|
|
333
|
+
return `
|
|
334
|
+
<section class="prototype-section screen-${escapeHtml(screen.screenId)}" id="screen-${escapeHtml(screen.screenId)}" aria-labelledby="title-${escapeHtml(screen.screenId)}">
|
|
335
|
+
<div class="section-inner">
|
|
336
|
+
<div class="section-header">
|
|
337
|
+
<p class="eyebrow">${escapeHtml(screenRole.replace(/-/g, " "))}</p>
|
|
338
|
+
<h2 id="title-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</h2>
|
|
339
|
+
${screen.route ? `<p class="support">Route: <code>${escapeHtml(screen.route)}</code></p>` : ""}
|
|
340
|
+
${screen.purpose ? `<p class="support">${escapeHtml(screen.purpose)}</p>` : ""}
|
|
341
|
+
</div>
|
|
342
|
+
${renderPrimitiveChips(primitiveHints)}
|
|
343
|
+
${renderStateChips(screen.requiredStates)}
|
|
344
|
+
<div class="screen-grid">
|
|
345
|
+
<article class="panel">
|
|
346
|
+
<h3>Main layout</h3>
|
|
347
|
+
<ul>${majorSections.slice(0, 5).map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
|
|
348
|
+
</article>
|
|
349
|
+
<article class="panel">
|
|
350
|
+
<h3>Primary action</h3>
|
|
351
|
+
<p>${escapeHtml(screen.primaryAction || "Primary CTA from explicit input")}</p>
|
|
352
|
+
<div class="button-row"><a class="btn btn-primary" href="#screen-${escapeHtml(screen.screenId)}">Trigger CTA</a></div>
|
|
353
|
+
</article>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</section>`;
|
|
357
|
+
})
|
|
358
|
+
.join("");
|
|
359
|
+
|
|
360
|
+
return `<!doctype html>
|
|
361
|
+
<html lang="en">
|
|
362
|
+
<head>
|
|
363
|
+
<meta charset="utf-8">
|
|
364
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
365
|
+
<title>SDTK-DESIGN Multi-Screen Prototype</title>
|
|
366
|
+
<style>
|
|
367
|
+
:root { --bg:${tokens.bg}; --surface:${tokens.surface}; --text:${tokens.text}; --muted:${tokens.muted}; --primary:${tokens.primary}; --accent:${tokens.accent}; --border:${tokens.border}; --shadow:${tokens.shadow}; }
|
|
368
|
+
* { box-sizing:border-box; }
|
|
369
|
+
body { margin:0; background:var(--bg); color:var(--text); font:15px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
370
|
+
.shell { width:min(1200px, calc(100vw - 24px)); margin:0 auto; padding:24px 0 44px; }
|
|
371
|
+
.topbar { position:sticky; top:0; z-index:4; background:color-mix(in srgb, var(--bg) 92%, var(--surface)); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); padding:14px; margin-bottom:18px; }
|
|
372
|
+
.topline { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
|
|
373
|
+
.meta { color:var(--muted); font-size:12px; font-weight:700; }
|
|
374
|
+
.nav { display:flex; gap:8px; overflow:auto; margin-top:10px; padding-bottom:4px; }
|
|
375
|
+
.nav a { text-decoration:none; border:1px solid var(--border); border-radius:999px; padding:6px 10px; white-space:nowrap; color:var(--text); background:var(--surface); font-size:12px; font-weight:700; }
|
|
376
|
+
.prototype-section { background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); margin:14px 0; }
|
|
377
|
+
.section-inner { padding:20px; }
|
|
378
|
+
.eyebrow { margin:0 0 6px; color:var(--primary); font-size:12px; font-weight:800; text-transform:uppercase; }
|
|
379
|
+
h2,h3,p { margin-top:0; }
|
|
380
|
+
h2 { margin-bottom:8px; font-size:28px; }
|
|
381
|
+
.support { color:var(--muted); margin-bottom:10px; }
|
|
382
|
+
.chip-row { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 12px; }
|
|
383
|
+
.chip { border:1px solid var(--border); border-radius:999px; padding:4px 10px; font-size:12px; background:color-mix(in srgb, var(--surface) 92%, var(--bg)); }
|
|
384
|
+
.chip-state { color:var(--primary); border-color:color-mix(in srgb, var(--primary) 35%, var(--border)); }
|
|
385
|
+
.screen-grid { display:grid; grid-template-columns:1.2fr 1fr; gap:12px; }
|
|
386
|
+
.panel { border:1px solid var(--border); border-radius:8px; padding:14px; }
|
|
387
|
+
.panel ul { margin:0; padding-left:18px; }
|
|
388
|
+
.btn { display:inline-flex; min-height:40px; align-items:center; justify-content:center; border-radius:8px; padding:0 14px; text-decoration:none; font-weight:800; }
|
|
389
|
+
.btn-primary { background:var(--primary); color:#fff; }
|
|
390
|
+
@media (max-width: 820px) { .screen-grid { grid-template-columns:1fr; } h2 { font-size:24px; } }
|
|
391
|
+
</style>
|
|
392
|
+
</head>
|
|
393
|
+
<body>
|
|
394
|
+
<main class="shell style-${styleName}" data-style-preset="${styleName}">
|
|
395
|
+
<header class="topbar">
|
|
396
|
+
<div class="topline">
|
|
397
|
+
<strong>SDTK-DESIGN multi-screen prototype</strong>
|
|
398
|
+
<span class="meta">screens=${screens.length} | profile=${escapeHtml(profileSelection || "none")} | mapped references=${mappedRefs}</span>
|
|
399
|
+
</div>
|
|
400
|
+
<nav class="nav" aria-label="Screen navigation">
|
|
401
|
+
${screens.map((screen) => `<a href="#screen-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</a>`).join("")}
|
|
402
|
+
</nav>
|
|
403
|
+
</header>
|
|
404
|
+
${sections}
|
|
405
|
+
</main>
|
|
406
|
+
</body>
|
|
407
|
+
</html>
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function runDesignPrototype({ projectPath, force = false, style }) {
|
|
412
|
+
const explicitStyleName = typeof style === "string" && style.trim() ? resolveStyleName(style) : null;
|
|
413
|
+
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
414
|
+
if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
|
|
415
|
+
throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No project files were changed.`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const paths = describeDesignPaths(resolvedProjectPath);
|
|
419
|
+
const inputContractState = readInputContractState(paths);
|
|
420
|
+
const canUseMultiScreen =
|
|
421
|
+
inputContractState &&
|
|
422
|
+
inputContractState.mode === "from-spec" &&
|
|
423
|
+
inputContractState.analysisStatus === "INPUT_CONTRACT_READY" &&
|
|
424
|
+
inputContractState.screenModel &&
|
|
425
|
+
Array.isArray(inputContractState.screenModel.screens) &&
|
|
426
|
+
inputContractState.screenModel.screens.length > 0;
|
|
427
|
+
const missing = canUseMultiScreen ? [] : requiredArtifactTargets(paths).filter((target) => !fs.existsSync(target.filePath));
|
|
428
|
+
if (missing.length > 0) {
|
|
429
|
+
const list = missing.map((target) => target.relativePath).join(", ");
|
|
430
|
+
throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
|
|
431
|
+
}
|
|
432
|
+
if (fs.existsSync(paths.prototypeIndexPath) && !force) {
|
|
433
|
+
throw new ValidationError("docs/design/prototype/index.html already exists. Re-run with --force to replace this managed prototype.");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const designSystemContent = fs.existsSync(paths.designSystemPath) ? fs.readFileSync(paths.designSystemPath, "utf-8") : "";
|
|
437
|
+
const styleName = explicitStyleName || (designSystemContent ? inferStyleFromDesignSystem(designSystemContent) : DEFAULT_STYLE);
|
|
438
|
+
const wireframeContents = canUseMultiScreen
|
|
439
|
+
? []
|
|
440
|
+
: REQUIRED_ARTIFACTS
|
|
441
|
+
.filter(([relativePath]) => relativePath.startsWith("docs/design/wireframes/"))
|
|
442
|
+
.map(([, key]) => {
|
|
443
|
+
const filePath =
|
|
444
|
+
key === "landingWireframePath"
|
|
445
|
+
? wireframePath(paths, "LANDING.md")
|
|
446
|
+
: key === "onboardingWireframePath"
|
|
447
|
+
? wireframePath(paths, "ONBOARDING.md")
|
|
448
|
+
: wireframePath(paths, "DASHBOARD.md");
|
|
449
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
450
|
+
});
|
|
451
|
+
const content = canUseMultiScreen
|
|
452
|
+
? multiScreenPrototypeContent({
|
|
453
|
+
style: styleName,
|
|
454
|
+
statePayload: inputContractState,
|
|
455
|
+
})
|
|
456
|
+
: prototypeContent({
|
|
457
|
+
style: styleName,
|
|
458
|
+
briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
|
|
459
|
+
screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
|
|
460
|
+
designSystemContent,
|
|
461
|
+
wireframeContents,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
|
|
465
|
+
fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
projectPath: resolvedProjectPath,
|
|
469
|
+
relativePrototypePath: "docs/design/prototype/index.html",
|
|
470
|
+
forced: Boolean(force),
|
|
471
|
+
style: styleName,
|
|
472
|
+
mode: canUseMultiScreen ? "multi-screen" : "simple",
|
|
473
|
+
screenCount: canUseMultiScreen ? inputContractState.screenModel.screens.length : 3,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function cmdPrototype(args) {
|
|
478
|
+
const { flags } = parseFlags(args || [], PROTOTYPE_FLAG_DEFS);
|
|
479
|
+
if (flags.help) return cmdPrototypeHelp();
|
|
480
|
+
|
|
481
|
+
const result = runDesignPrototype({
|
|
482
|
+
projectPath: flags["project-path"],
|
|
483
|
+
force: Boolean(flags.force),
|
|
484
|
+
style: flags.style,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
|
|
488
|
+
console.log(`[design] Style: ${result.style}`);
|
|
489
|
+
console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen section(s))`);
|
|
490
|
+
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
491
|
+
console.log("[design] Prototype mode: static HTML/CSS preview only.");
|
|
492
|
+
console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
|
|
493
|
+
console.log("[design] Next: sdtk-design review --artifact docs/design/prototype/index.html");
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = {
|
|
498
|
+
cmdPrototype,
|
|
499
|
+
cmdPrototypeHelp,
|
|
500
|
+
inferStyleFromDesignSystem,
|
|
501
|
+
prototypeContent,
|
|
502
|
+
runDesignPrototype,
|
|
503
|
+
};
|
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
|
};
|