sdtk-design-kit 0.2.1 → 0.3.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 +67 -125
- package/package.json +2 -1
- package/skills/design-prototype/SKILL.md +276 -0
- package/skills/design-prototype/references/craft.md +75 -0
- package/skills/design-prototype/references/designer-charter.md +56 -0
- package/src/commands/handoff.js +220 -220
- package/src/commands/help.js +7 -3
- package/src/commands/init.js +53 -0
- package/src/commands/prototype.js +140 -653
- package/src/commands/review.js +129 -139
- package/src/commands/start.js +22 -5
- package/src/commands/status.js +10 -2
- package/src/commands/system.js +186 -14
- package/src/commands/update.js +11 -0
- package/src/index.js +4 -0
- package/src/lib/anti-slop-lint.js +0 -11
- package/src/lib/component-contract.js +300 -34
- package/src/lib/design-input-contract.js +3 -2
- package/src/lib/design-paths.js +3 -0
- package/src/lib/design-profiles.js +31 -0
- package/src/lib/screen-briefs.js +235 -24
- package/src/lib/update.js +219 -0
- package/src/lib/prototype-briefs.js +0 -125
- package/src/lib/prototype-component-map.js +0 -219
- package/src/lib/prototype-density.js +0 -377
- package/src/lib/prototype-renderer.js +0 -382
|
@@ -4,620 +4,189 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { parseFlags } = require("../lib/args");
|
|
6
6
|
const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
|
|
7
|
-
const { inferDomainProfile } = require("../lib/domain-profile");
|
|
8
7
|
const { ValidationError } = require("../lib/errors");
|
|
9
|
-
const { renderMultiPagePrototype, screenFileName } = require("../lib/prototype-renderer");
|
|
10
|
-
const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
|
|
11
8
|
|
|
12
9
|
const PROTOTYPE_FLAG_DEFS = {
|
|
13
10
|
help: { type: "boolean" },
|
|
14
11
|
"project-path": { type: "string" },
|
|
15
12
|
force: { type: "boolean" },
|
|
16
|
-
style: { type: "string" },
|
|
17
13
|
};
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
["docs/design/DESIGN_SYSTEM.md", "designSystemPath"],
|
|
23
|
-
["docs/design/wireframes/LANDING.md", "landingWireframePath"],
|
|
24
|
-
["docs/design/wireframes/ONBOARDING.md", "onboardingWireframePath"],
|
|
25
|
-
["docs/design/wireframes/DASHBOARD.md", "dashboardWireframePath"],
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const THEME_TOKENS = {
|
|
29
|
-
"minimal-saas": {
|
|
30
|
-
bg: "#F7F8FA",
|
|
31
|
-
surface: "#FFFFFF",
|
|
32
|
-
text: "#1F2933",
|
|
33
|
-
muted: "#667085",
|
|
34
|
-
primary: "#2563EB",
|
|
35
|
-
accent: "#0F8A5F",
|
|
36
|
-
border: "#D9DEE7",
|
|
37
|
-
shadow: "0 14px 40px rgba(31,41,51,0.10)",
|
|
38
|
-
},
|
|
39
|
-
"premium-dashboard": {
|
|
40
|
-
bg: "#F5F7FB",
|
|
41
|
-
surface: "#FFFFFF",
|
|
42
|
-
text: "#111827",
|
|
43
|
-
muted: "#64748B",
|
|
44
|
-
primary: "#0F766E",
|
|
45
|
-
accent: "#2563EB",
|
|
46
|
-
border: "#D8E0EA",
|
|
47
|
-
shadow: "0 18px 48px rgba(15,23,42,0.12)",
|
|
48
|
-
},
|
|
49
|
-
"bold-founder": {
|
|
50
|
-
bg: "#111111",
|
|
51
|
-
surface: "#18181B",
|
|
52
|
-
text: "#FAFAFA",
|
|
53
|
-
muted: "#A1A1AA",
|
|
54
|
-
primary: "#F97316",
|
|
55
|
-
accent: "#22C55E",
|
|
56
|
-
border: "#27272A",
|
|
57
|
-
shadow: "0 20px 0 rgba(249,115,22,0.24)",
|
|
58
|
-
},
|
|
59
|
-
"warm-editorial": {
|
|
60
|
-
bg: "#FAF7F2",
|
|
61
|
-
surface: "#FFFFFF",
|
|
62
|
-
text: "#1C1A17",
|
|
63
|
-
muted: "#8A817A",
|
|
64
|
-
primary: "#C0512F",
|
|
65
|
-
accent: "#2F5B4F",
|
|
66
|
-
border: "rgba(47,91,79,0.16)",
|
|
67
|
-
shadow: "0 16px 36px rgba(28,26,23,0.08)",
|
|
68
|
-
},
|
|
69
|
-
};
|
|
15
|
+
function toPosix(value) {
|
|
16
|
+
return String(value || "").replace(/\\/g, "/");
|
|
17
|
+
}
|
|
70
18
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} catch (_err) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
19
|
+
function escapeHtml(value) {
|
|
20
|
+
return String(value == null ? "" : value)
|
|
21
|
+
.replace(/&/g, "&")
|
|
22
|
+
.replace(/</g, "<")
|
|
23
|
+
.replace(/>/g, ">")
|
|
24
|
+
.replace(/"/g, """);
|
|
80
25
|
}
|
|
81
26
|
|
|
82
27
|
function cmdPrototypeHelp() {
|
|
83
28
|
console.log(`SDTK-DESIGN Prototype
|
|
84
29
|
|
|
85
30
|
Usage:
|
|
86
|
-
sdtk-design prototype [--
|
|
31
|
+
sdtk-design prototype [--project-path <path>] [--force]
|
|
87
32
|
|
|
88
33
|
Example:
|
|
89
|
-
sdtk-design prototype
|
|
90
|
-
|
|
91
|
-
Style presets:
|
|
92
|
-
${availableStyleNames().join(", ")}
|
|
93
|
-
Default: inferred from docs/design/DESIGN_SYSTEM.md, then ${DEFAULT_STYLE}
|
|
34
|
+
sdtk-design prototype
|
|
94
35
|
|
|
95
36
|
Reads:
|
|
96
|
-
|
|
97
|
-
docs/design/SCREEN_MAP.md
|
|
37
|
+
.sdtk/design/START_INPUT_STATE.json (must be INPUT_CONTRACT_READY)
|
|
98
38
|
docs/design/DESIGN_SYSTEM.md
|
|
99
|
-
docs/design/
|
|
100
|
-
docs/design/
|
|
101
|
-
docs/design/wireframes/DASHBOARD.md
|
|
102
|
-
.sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
|
|
39
|
+
docs/design/DESIGN_TOKENS.json
|
|
40
|
+
docs/design/screens/*_DESIGN_BRIEF.md
|
|
103
41
|
|
|
104
42
|
Creates:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
docs/design/prototype/index.html
|
|
109
|
-
docs/design/prototype/screens/*.html
|
|
110
|
-
docs/design/prototype/assets/prototype.css
|
|
111
|
-
docs/design/prototype/assets/prototype.js
|
|
43
|
+
docs/design/prototype/.manifest.json
|
|
44
|
+
docs/design/prototype/index.html
|
|
45
|
+
docs/design/prototype/screens/ (empty output directory for the agent-owned screen HTML)
|
|
112
46
|
|
|
113
47
|
Safety:
|
|
114
48
|
Local files only.
|
|
115
|
-
Existing prototype index
|
|
49
|
+
Existing prototype manifest and index are not overwritten unless --force is explicit.
|
|
50
|
+
Screen HTML files under docs/design/prototype/screens are never overwritten by this command.
|
|
116
51
|
No production app code outside docs/design/prototype.
|
|
117
52
|
No JavaScript app runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
|
|
118
53
|
return 0;
|
|
119
54
|
}
|
|
120
55
|
|
|
121
|
-
function
|
|
122
|
-
if (!
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
const raw = String(process.env.SDTK_DESIGN_PROTOTYPE_MODE || "").trim().toLowerCase();
|
|
126
|
-
if (!raw) {
|
|
127
|
-
return "multi";
|
|
56
|
+
function readInputContractState(paths) {
|
|
57
|
+
if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
|
|
58
|
+
return null;
|
|
128
59
|
}
|
|
129
|
-
|
|
130
|
-
return
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
|
|
62
|
+
} catch (_err) {
|
|
63
|
+
return null;
|
|
131
64
|
}
|
|
132
|
-
throw new ValidationError(
|
|
133
|
-
`Unsupported SDTK_DESIGN_PROTOTYPE_MODE "${process.env.SDTK_DESIGN_PROTOTYPE_MODE}". Use "single" or "multi". No project files were changed.`
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function wireframePath(paths, fileName) {
|
|
138
|
-
return path.join(paths.wireframesPath, fileName);
|
|
139
65
|
}
|
|
140
66
|
|
|
141
|
-
function
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
: paths[key];
|
|
151
|
-
return { relativePath, filePath };
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function firstLine(content, fallback) {
|
|
156
|
-
const line = String(content || "")
|
|
157
|
-
.split(/\r?\n/)
|
|
158
|
-
.map((value) => value.trim())
|
|
159
|
-
.find(Boolean);
|
|
160
|
-
return line ? line.replace(/^#\s*/, "") : fallback;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function escapeHtml(value) {
|
|
164
|
-
return String(value)
|
|
165
|
-
.replace(/&/g, "&")
|
|
166
|
-
.replace(/</g, "<")
|
|
167
|
-
.replace(/>/g, ">")
|
|
168
|
-
.replace(/"/g, """);
|
|
67
|
+
function assertReadyInputContract(statePayload) {
|
|
68
|
+
return (
|
|
69
|
+
statePayload &&
|
|
70
|
+
statePayload.mode === "from-spec" &&
|
|
71
|
+
statePayload.analysisStatus === "INPUT_CONTRACT_READY" &&
|
|
72
|
+
statePayload.screenModel &&
|
|
73
|
+
Array.isArray(statePayload.screenModel.screens) &&
|
|
74
|
+
statePayload.screenModel.screens.length > 0
|
|
75
|
+
);
|
|
169
76
|
}
|
|
170
77
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
78
|
+
function screenBriefRelativePath(screenId) {
|
|
79
|
+
return `docs/design/screens/${String(screenId || "")
|
|
80
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")}_DESIGN_BRIEF.md`;
|
|
174
81
|
}
|
|
175
82
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const profile = inferDomainProfile({ briefContent, screenMapContent, designSystemContent, wireframeContents });
|
|
180
|
-
|
|
181
|
-
return `<!doctype html>
|
|
182
|
-
<html lang="en">
|
|
183
|
-
<head>
|
|
184
|
-
<meta charset="utf-8">
|
|
185
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
186
|
-
<title>${escapeHtml(profile.productName)} Prototype</title>
|
|
187
|
-
<style>
|
|
188
|
-
:root {
|
|
189
|
-
--bg: ${tokens.bg};
|
|
190
|
-
--surface: ${tokens.surface};
|
|
191
|
-
--text: ${tokens.text};
|
|
192
|
-
--muted: ${tokens.muted};
|
|
193
|
-
--primary: ${tokens.primary};
|
|
194
|
-
--accent: ${tokens.accent};
|
|
195
|
-
--border: ${tokens.border};
|
|
196
|
-
--shadow: ${tokens.shadow};
|
|
197
|
-
}
|
|
198
|
-
* { box-sizing: border-box; }
|
|
199
|
-
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; }
|
|
200
|
-
.sdtk-prototype { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 32px 0 56px; }
|
|
201
|
-
.prototype-header { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-bottom: 22px; }
|
|
202
|
-
.brand { font-weight: 800; letter-spacing: 0; }
|
|
203
|
-
.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; }
|
|
204
|
-
.prototype-section { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); margin: 18px 0; overflow: hidden; }
|
|
205
|
-
.section-inner { padding: clamp(22px, 4vw, 44px); }
|
|
206
|
-
.eyebrow { margin: 0 0 8px; color: var(--primary); font-size: 12px; font-weight: 850; text-transform: uppercase; letter-spacing: 0; }
|
|
207
|
-
h1, h2, h3, p { margin-top: 0; }
|
|
208
|
-
h1 { max-width: 860px; font-size: clamp(34px, 7vw, 72px); line-height: 1.02; margin-bottom: 16px; }
|
|
209
|
-
h2 { font-size: clamp(26px, 4vw, 40px); line-height: 1.12; margin-bottom: 12px; }
|
|
210
|
-
h3 { font-size: 16px; margin-bottom: 8px; }
|
|
211
|
-
.support { max-width: 700px; color: var(--muted); font-size: 18px; }
|
|
212
|
-
.button-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 22px; }
|
|
213
|
-
.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; }
|
|
214
|
-
.btn-primary { background: var(--primary); color: #fff; }
|
|
215
|
-
.btn-secondary { color: var(--text); border: 1px solid var(--border); background: transparent; }
|
|
216
|
-
.workflow-grid, .metric-grid, .dashboard-grid { display: grid; gap: 14px; }
|
|
217
|
-
.workflow-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 28px; }
|
|
218
|
-
.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)); }
|
|
219
|
-
.workflow-card strong, .metric-card strong { display: block; font-size: 24px; }
|
|
220
|
-
.onboarding-layout { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(320px, 0.55fr); gap: 20px; align-items: start; }
|
|
221
|
-
.field { display: grid; gap: 6px; margin-bottom: 12px; }
|
|
222
|
-
.input-preview { min-height: 42px; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center; padding: 0 12px; color: var(--muted); }
|
|
223
|
-
.metric-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 16px 0; }
|
|
224
|
-
.dashboard-grid { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); }
|
|
225
|
-
.item-card { display: grid; gap: 8px; }
|
|
226
|
-
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
227
|
-
.status-pill { color: var(--primary); }
|
|
228
|
-
.empty-state { color: var(--muted); border-style: dashed; }
|
|
229
|
-
.style-premium-dashboard .metric-card strong { font-size: 30px; }
|
|
230
|
-
.style-bold-founder h1 { text-transform: uppercase; }
|
|
231
|
-
.style-warm-editorial h1, .style-warm-editorial h2 { font-family: Georgia, "Times New Roman", serif; }
|
|
232
|
-
@media (max-width: 780px) {
|
|
233
|
-
.prototype-header, .onboarding-layout { display: block; }
|
|
234
|
-
.workflow-grid, .metric-grid, .dashboard-grid { grid-template-columns: 1fr; }
|
|
235
|
-
h1 { font-size: 36px; }
|
|
236
|
-
}
|
|
237
|
-
</style>
|
|
238
|
-
</head>
|
|
239
|
-
<body>
|
|
240
|
-
<main class="sdtk-prototype style-${styleName}" data-style-preset="${styleName}">
|
|
241
|
-
<header class="prototype-header">
|
|
242
|
-
<div class="brand">${escapeHtml(profile.productName)}</div>
|
|
243
|
-
<span class="style-pill">Style preset: ${styleName}</span>
|
|
244
|
-
</header>
|
|
245
|
-
|
|
246
|
-
<section class="prototype-section section-landing" aria-labelledby="landing-title">
|
|
247
|
-
<div class="section-inner">
|
|
248
|
-
<p class="eyebrow">Landing</p>
|
|
249
|
-
<h1 id="landing-title">${escapeHtml(profile.promise)}</h1>
|
|
250
|
-
<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>
|
|
251
|
-
<div class="button-row">
|
|
252
|
-
<a class="btn btn-primary" href="#onboarding">${escapeHtml(profile.primaryAction)}</a>
|
|
253
|
-
<a class="btn btn-secondary" href="#dashboard">${escapeHtml(profile.secondaryAction)}</a>
|
|
254
|
-
</div>
|
|
255
|
-
<div class="workflow-grid">
|
|
256
|
-
<div class="workflow-card"><strong>01</strong><span>Capture a ${escapeHtml(profile.itemSingular)} with status and context.</span></div>
|
|
257
|
-
<div class="workflow-card"><strong>02</strong><span>Record the next action before it slips.</span></div>
|
|
258
|
-
<div class="workflow-card"><strong>03</strong><span>Scan the dashboard and choose what to do next.</span></div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
</section>
|
|
262
|
-
|
|
263
|
-
<section class="prototype-section section-onboarding" id="onboarding" aria-labelledby="onboarding-title">
|
|
264
|
-
<div class="section-inner onboarding-layout">
|
|
265
|
-
<div>
|
|
266
|
-
<p class="eyebrow">Onboarding</p>
|
|
267
|
-
<h2 id="onboarding-title">Create a focused ${escapeHtml(profile.setupLabel.toLowerCase())} in one step.</h2>
|
|
268
|
-
<p class="support">The setup flow asks only for the minimum needed before the user can create the first ${escapeHtml(profile.itemSingular)}.</p>
|
|
269
|
-
</div>
|
|
270
|
-
<div class="form-card">
|
|
271
|
-
<div class="field"><strong>${escapeHtml(profile.setupNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.setupNameValue)}</div></div>
|
|
272
|
-
<div class="field"><strong>Primary workflow</strong><div class="input-preview">${escapeHtml(profile.itemAction)}</div></div>
|
|
273
|
-
<a class="btn btn-primary" href="#dashboard">Continue</a>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
</section>
|
|
277
|
-
|
|
278
|
-
<section class="prototype-section section-dashboard" id="dashboard" aria-labelledby="dashboard-title">
|
|
279
|
-
<div class="section-inner">
|
|
280
|
-
<p class="eyebrow">Dashboard</p>
|
|
281
|
-
<h2 id="dashboard-title">${escapeHtml(profile.dashboardTitle)}</h2>
|
|
282
|
-
<div class="metric-grid">
|
|
283
|
-
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[0])}</span><strong>${escapeHtml(profile.metricValues[0])}</strong></div>
|
|
284
|
-
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[1])}</span><strong>${escapeHtml(profile.metricValues[1])}</strong></div>
|
|
285
|
-
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[2])}</span><strong>${escapeHtml(profile.metricValues[2])}</strong></div>
|
|
286
|
-
<div class="metric-card"><span>${escapeHtml(profile.metricLabels[3])}</span><strong>${escapeHtml(profile.metricValues[3])}</strong></div>
|
|
287
|
-
</div>
|
|
288
|
-
<div class="dashboard-grid">
|
|
289
|
-
<div class="form-card">
|
|
290
|
-
<h3>${escapeHtml(profile.itemAction)}</h3>
|
|
291
|
-
<div class="field"><strong>${escapeHtml(profile.itemNameLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemNameValue)}</div></div>
|
|
292
|
-
<div class="field"><strong>${escapeHtml(profile.itemContextLabel)}</strong><div class="input-preview">${escapeHtml(profile.itemContextValue)}</div></div>
|
|
293
|
-
<a class="btn btn-primary" href="#dashboard">${escapeHtml(profile.saveAction)}</a>
|
|
294
|
-
</div>
|
|
295
|
-
<div class="item-card">
|
|
296
|
-
<h3>${escapeHtml(profile.itemNameValue)}</h3>
|
|
297
|
-
<div class="item-meta">
|
|
298
|
-
<span class="status-pill">${escapeHtml(profile.statusExample)}</span>
|
|
299
|
-
<span class="status-pill">${escapeHtml(profile.itemContextValue)}</span>
|
|
300
|
-
</div>
|
|
301
|
-
<p class="support">Keep context, status, and next action visible in the ${escapeHtml(profile.collectionSurface)}.</p>
|
|
302
|
-
<div class="empty-state">${escapeHtml(profile.emptyState)}</div>
|
|
303
|
-
</div>
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
</section>
|
|
307
|
-
</main>
|
|
308
|
-
</body>
|
|
309
|
-
</html>
|
|
310
|
-
`;
|
|
83
|
+
function screenOutRelativePath(screenId) {
|
|
84
|
+
return `docs/design/prototype/screens/${String(screenId || "")
|
|
85
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")}.html`;
|
|
311
86
|
}
|
|
312
87
|
|
|
313
88
|
function normalizeScreenRole(screen) {
|
|
314
|
-
const token = `${screen.screenId
|
|
89
|
+
const token = `${screen && screen.screenId ? screen.screenId : ""} ${screen && screen.title ? screen.title : ""} ${
|
|
90
|
+
screen && screen.templateRole ? screen.templateRole : ""
|
|
91
|
+
} ${screen && screen.template_role ? screen.template_role : ""}`.toLowerCase();
|
|
315
92
|
if (token.includes("home")) return "home";
|
|
316
|
-
if (token.includes("category")) return "category";
|
|
93
|
+
if (token.includes("category") || token.includes("catalog")) return "category";
|
|
317
94
|
if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
|
|
318
95
|
if (token.includes("search")) return "search";
|
|
319
|
-
if (token.includes("cart")) return "cart";
|
|
96
|
+
if (token.includes("cart") && !token.includes("checkout")) return "cart";
|
|
320
97
|
if (token.includes("checkout")) return "checkout";
|
|
321
98
|
if (token.includes("order-history") || token.includes("order history")) return "order-history";
|
|
322
99
|
if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
|
|
323
100
|
if (token.includes("account")) return "account-info";
|
|
324
|
-
if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
|
|
325
|
-
return screen.screenId
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function renderPrimitiveChips(primitiveNames) {
|
|
329
|
-
if (!Array.isArray(primitiveNames) || primitiveNames.length === 0) return "";
|
|
330
|
-
return `<div class="chip-row">${primitiveNames.map((item) => `<span class="chip">${escapeHtml(item)}</span>`).join("")}</div>`;
|
|
101
|
+
if (token.includes("configurator") || token.includes("bom") || token.includes("mode-b")) return "mode-b-configurator";
|
|
102
|
+
return screen && screen.screenId ? screen.screenId : "screen";
|
|
331
103
|
}
|
|
332
104
|
|
|
333
|
-
function
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
.
|
|
337
|
-
.map((
|
|
338
|
-
|
|
105
|
+
function requiredPrototypeInputs(paths, screens) {
|
|
106
|
+
return [
|
|
107
|
+
{ relativePath: "docs/design/DESIGN_SYSTEM.md", filePath: paths.designSystemPath },
|
|
108
|
+
{ relativePath: "docs/design/DESIGN_TOKENS.json", filePath: paths.designTokensPath },
|
|
109
|
+
...screens.map((screen) => {
|
|
110
|
+
const relativePath = screenBriefRelativePath(screen.screenId);
|
|
111
|
+
return {
|
|
112
|
+
relativePath,
|
|
113
|
+
filePath: path.join(paths.projectPath, relativePath),
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
339
117
|
}
|
|
340
118
|
|
|
341
|
-
function
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
} catch (_err) {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function loadHighFidelityContracts(paths, statePayload) {
|
|
358
|
-
const warnings = [];
|
|
359
|
-
if (!fs.existsSync(paths.componentPatternLibraryPath)) {
|
|
360
|
-
warnings.push("missing docs/design/COMPONENT_PATTERN_LIBRARY.md");
|
|
361
|
-
}
|
|
362
|
-
if (!fs.existsSync(paths.designTokensPath)) {
|
|
363
|
-
warnings.push("missing docs/design/DESIGN_TOKENS.json");
|
|
364
|
-
}
|
|
365
|
-
const missingBriefs = [];
|
|
366
|
-
const screenContracts = new Map();
|
|
367
|
-
for (const screen of statePayload.screenModel.screens) {
|
|
368
|
-
const briefPath = screenBriefPath(paths, screen.screenId);
|
|
369
|
-
if (!fs.existsSync(briefPath)) {
|
|
370
|
-
missingBriefs.push(screen.screenId);
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
const sidecarPath = screenBriefSidecarPath(paths, screen.screenId);
|
|
374
|
-
screenContracts.set(screen.screenId, {
|
|
375
|
-
briefPath,
|
|
376
|
-
briefContent: fs.readFileSync(briefPath, "utf-8"),
|
|
377
|
-
sidecar: fs.existsSync(sidecarPath) ? readJsonSafe(sidecarPath) : null,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
if (missingBriefs.length > 0) {
|
|
381
|
-
warnings.push(`missing per-screen briefs for: ${missingBriefs.join(", ")}`);
|
|
382
|
-
}
|
|
383
|
-
if (warnings.length > 0) {
|
|
384
|
-
return { ready: false, warnings, screenContracts: new Map(), tokens: null };
|
|
385
|
-
}
|
|
386
|
-
const tokens = readJsonSafe(paths.designTokensPath);
|
|
387
|
-
if (!tokens || typeof tokens !== "object") {
|
|
388
|
-
return { ready: false, warnings: ["invalid docs/design/DESIGN_TOKENS.json"], screenContracts: new Map(), tokens: null };
|
|
389
|
-
}
|
|
390
|
-
return { ready: true, warnings: [], screenContracts, tokens };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function managedPrototypeTargets(paths, statePayload, mode) {
|
|
394
|
-
const targets = [{ relativePath: "docs/design/prototype/index.html", filePath: paths.prototypeIndexPath }];
|
|
395
|
-
if (
|
|
396
|
-
mode === "multi" &&
|
|
397
|
-
statePayload &&
|
|
398
|
-
statePayload.screenModel &&
|
|
399
|
-
Array.isArray(statePayload.screenModel.screens)
|
|
400
|
-
) {
|
|
401
|
-
targets.push(
|
|
402
|
-
{ relativePath: "docs/design/prototype/assets/prototype.css", filePath: paths.prototypeCssPath },
|
|
403
|
-
{ relativePath: "docs/design/prototype/assets/prototype.js", filePath: paths.prototypeJsPath },
|
|
404
|
-
...statePayload.screenModel.screens.map((screen) => ({
|
|
405
|
-
relativePath: `docs/design/prototype/screens/${screenFileName(screen)}`,
|
|
406
|
-
filePath: path.join(paths.prototypeScreensPath, screenFileName(screen)),
|
|
407
|
-
}))
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
return targets;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function tokenOrDefault(tokens, keyPath, fallback) {
|
|
414
|
-
const value = keyPath.reduce((obj, key) => (obj && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined), tokens);
|
|
415
|
-
return value == null ? fallback : value;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function renderHighFidelityScreen(screenRole, title) {
|
|
419
|
-
switch (screenRole) {
|
|
420
|
-
case "home":
|
|
421
|
-
return `
|
|
422
|
-
<section class="hf-hero panel"><h3>${escapeHtml(title)} hero</h3><p>Category-first commerce landing with featured products and guided CTA.</p><div class="button-row"><a class="btn btn-primary" href="#">Start purchase</a><a class="btn btn-secondary" href="#">Open configurator</a></div></section>
|
|
423
|
-
<section class="hf-feature-grid"><article class="panel">Category range</article><article class="panel">Featured products</article><article class="panel">Configurator quick start</article></section>`;
|
|
424
|
-
case "category":
|
|
425
|
-
return `<section class="hf-split"><aside class="panel">Filter sidebar</aside><div class="panel"><h3>Product grid</h3><div class="hf-card-grid"><article class="product-card">Card A</article><article class="product-card">Card B</article><article class="product-card">Card C</article></div></div></section>`;
|
|
426
|
-
case "product-detail":
|
|
427
|
-
return `<section class="hf-split"><div class="panel"><h3>Gallery and spec area</h3><table class="spec-table"><tr><th>Spec</th><th>Value</th></tr><tr><td>Voltage</td><td>High pressure</td></tr></table></div><aside class="panel"><h3>Price and quantity</h3><div class="qty-stepper"><button>-</button><span>1</span><button>+</button></div><a class="btn btn-primary" href="#">Add to cart</a></aside></section>`;
|
|
428
|
-
case "search":
|
|
429
|
-
return `<section class="panel"><h3>Search and result state</h3><div class="search-toolbar"><input aria-label="Search query" placeholder="Search product"><button class="btn btn-primary">Search</button></div><div class="hf-list"><article class="result-row">Result item</article><article class="result-row no-result">No-result state</article></div></section>`;
|
|
430
|
-
case "cart":
|
|
431
|
-
return `<section class="hf-split"><div class="panel"><h3>Cart table</h3><table class="cart-table"><tr><th>Item</th><th>Qty</th><th>Total</th></tr><tr><td>Product</td><td><div class="qty-stepper"><button>-</button><span>2</span><button>+</button></div></td><td>100000</td></tr></table></div><aside class="panel"><h3>Summary</h3><p>Subtotal, tax, total</p><a class="btn btn-primary" href="#">Checkout</a></aside></section>`;
|
|
432
|
-
case "checkout":
|
|
433
|
-
return `<section class="panel"><ol class="checkout-stepper"><li>Information</li><li>Confirmation</li><li>Complete</li></ol><div class="hf-split"><div class="panel"><h3>Checkout form</h3><div class="field-row">Recipient / Address / Phone</div></div><aside class="panel"><h3>Summary card</h3><a class="btn btn-primary" href="#">Place order</a></aside></div></section>`;
|
|
434
|
-
case "order-history":
|
|
435
|
-
return `<section class="panel"><h3>Order history list</h3><div class="hf-list"><article class="result-row">Order #001 / paid</article><article class="result-row">Order #002 / shipping</article></div></section>`;
|
|
436
|
-
case "order-detail":
|
|
437
|
-
return `<section class="hf-split"><div class="panel"><h3>Order timeline</h3><ul class="timeline"><li>Placed</li><li>Paid</li><li>Shipping</li></ul></div><aside class="panel"><h3>Detail summary</h3><p>Delivery and payment snapshot</p></aside></section>`;
|
|
438
|
-
case "account-info":
|
|
439
|
-
return `<section class="hf-split"><aside class="panel">Account sidebar</aside><div class="panel"><h3>View or edit form</h3><div class="field-row">Company / Contact / Email / Phone</div><a class="btn btn-primary" href="#">Save profile</a></div></section>`;
|
|
440
|
-
case "mode-b-configurator":
|
|
441
|
-
return `<section class="panel"><h3>Configurator wizard</h3><ol class="checkout-stepper"><li>Project</li><li>Assembly</li><li>Construction</li><li>Review</li></ol><div class="hf-split"><div class="panel"><h4>Preview panel</h4><p>Derived material preview</p></div><div class="panel"><h4>BOM table</h4><table class="cart-table"><tr><th>Material</th><th>Qty</th><th>Action</th></tr><tr><td>Cable</td><td>4</td><td><button>Exclude</button></td></tr></table><div class="button-row"><a class="btn btn-secondary" href="#">Recalculate</a><a class="btn btn-primary" href="#">Add BOM to cart</a></div></div></div></section>`;
|
|
442
|
-
default:
|
|
443
|
-
return `<section class="panel"><h3>Screen contract section</h3><p>Render screen-specific layout from brief + component contract.</p></section>`;
|
|
444
|
-
}
|
|
119
|
+
function buildPrototypeManifest(screens) {
|
|
120
|
+
return {
|
|
121
|
+
schema: "sdtk.design.prototype-manifest.v1",
|
|
122
|
+
screens: screens.map((screen) => ({
|
|
123
|
+
screenId: screen.screenId,
|
|
124
|
+
title: screen.title || screen.screenId,
|
|
125
|
+
role: normalizeScreenRole(screen),
|
|
126
|
+
briefPath: screenBriefRelativePath(screen.screenId),
|
|
127
|
+
outPath: screenOutRelativePath(screen.screenId),
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
445
130
|
}
|
|
446
131
|
|
|
447
|
-
function
|
|
448
|
-
const
|
|
449
|
-
const tokens = contractBundle.tokens || {};
|
|
450
|
-
const screens = statePayload.screenModel.screens;
|
|
451
|
-
const profileSelection = statePayload.profileSelection || null;
|
|
452
|
-
const mappedRefs = statePayload.referenceMap && typeof statePayload.referenceMap.mappedCount === "number" ? statePayload.referenceMap.mappedCount : 0;
|
|
453
|
-
const sections = screens
|
|
132
|
+
function launcherHtml(manifest) {
|
|
133
|
+
const links = manifest.screens
|
|
454
134
|
.map((screen) => {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
<div class="section-header">
|
|
463
|
-
<p class="eyebrow">${escapeHtml(screenRole.replace(/-/g, " "))}</p>
|
|
464
|
-
<h2 id="title-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</h2>
|
|
465
|
-
${screen.route ? `<p class="support">Route: <code>${escapeHtml(screen.route)}</code></p>` : ""}
|
|
466
|
-
<p class="support">Source brief: <code>${escapeHtml(path.basename(contract.briefPath))}</code></p>
|
|
467
|
-
</div>
|
|
468
|
-
${stateChips}
|
|
469
|
-
${renderHighFidelityScreen(screenRole, screen.title)}
|
|
470
|
-
</div>
|
|
471
|
-
</section>`;
|
|
135
|
+
const href = toPosix(path.relative("docs/design/prototype", screen.outPath));
|
|
136
|
+
return ` <article class="screen-card">
|
|
137
|
+
<p class="role">${escapeHtml(screen.role)}</p>
|
|
138
|
+
<h2>${escapeHtml(screen.title)}</h2>
|
|
139
|
+
<p><code>${escapeHtml(screen.screenId)}</code></p>
|
|
140
|
+
<a href="${escapeHtml(href)}">Open generated screen</a>
|
|
141
|
+
</article>`;
|
|
472
142
|
})
|
|
473
|
-
.join("");
|
|
143
|
+
.join("\n");
|
|
474
144
|
|
|
475
145
|
return `<!doctype html>
|
|
476
146
|
<html lang="en">
|
|
477
147
|
<head>
|
|
478
148
|
<meta charset="utf-8">
|
|
479
149
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
480
|
-
<title>SDTK-DESIGN
|
|
150
|
+
<title>SDTK-DESIGN Prototype Launcher</title>
|
|
481
151
|
<style>
|
|
482
|
-
:root { --bg
|
|
483
|
-
* { box-sizing:border-box; }
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
.
|
|
489
|
-
.
|
|
490
|
-
.
|
|
491
|
-
.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
.product-card, .result-row { border:1px solid var(--border); border-radius:8px; padding:10px; background:var(--surface); }
|
|
496
|
-
.search-toolbar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px; } input { min-height:40px; border:1px solid var(--border); border-radius:8px; padding:0 12px; min-width:220px; }
|
|
497
|
-
.button-row { display:flex; flex-wrap:wrap; gap:10px; margin-top:10px; } .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; border:1px solid transparent; } .btn-primary { background:var(--primary); color:#fff; } .btn-secondary { border-color:var(--border); color:var(--text); background:transparent; }
|
|
498
|
-
.spec-table, .cart-table { width:100%; border-collapse:collapse; font-size:13px; } .spec-table th, .spec-table td, .cart-table th, .cart-table td { border-bottom:1px solid var(--border); text-align:left; padding:8px 6px; }
|
|
499
|
-
.qty-stepper { display:inline-flex; align-items:center; gap:8px; border:1px solid var(--border); border-radius:8px; padding:4px 8px; }
|
|
500
|
-
.checkout-stepper { display:flex; gap:10px; list-style:none; padding:0; margin:0 0 12px; } .checkout-stepper li { border:1px solid var(--border); border-radius:999px; padding:4px 10px; font-size:12px; font-weight:700; }
|
|
501
|
-
.timeline { margin:0; padding-left:16px; } .field-row { border:1px dashed var(--border); border-radius:8px; padding:10px; color:var(--muted); }
|
|
502
|
-
@media (max-width: 920px) { .hf-split,.hf-feature-grid,.hf-card-grid { grid-template-columns:1fr; } h2 { font-size:24px; } }
|
|
152
|
+
:root { --bg:#f7f8fa; --surface:#ffffff; --text:#111827; --muted:#64748b; --border:#d8e0ea; --accent:#0f766e; }
|
|
153
|
+
* { box-sizing:border-box; }
|
|
154
|
+
body { margin:0; background:var(--bg); color:var(--text); font:15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
155
|
+
main { width:min(1120px, calc(100vw - 32px)); margin:0 auto; padding:32px 0 56px; }
|
|
156
|
+
header { margin-bottom:24px; }
|
|
157
|
+
h1 { margin:0 0 8px; font-size:32px; line-height:1.15; }
|
|
158
|
+
.summary { margin:0; color:var(--muted); max-width:72ch; }
|
|
159
|
+
.grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:14px; }
|
|
160
|
+
.screen-card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:16px; }
|
|
161
|
+
.role { color:var(--accent); font-size:12px; font-weight:800; margin:0 0 6px; text-transform:uppercase; }
|
|
162
|
+
h2 { margin:0 0 8px; font-size:18px; }
|
|
163
|
+
code { color:var(--muted); }
|
|
164
|
+
a { display:inline-flex; min-height:40px; align-items:center; margin-top:10px; color:var(--accent); font-weight:750; }
|
|
503
165
|
</style>
|
|
504
166
|
</head>
|
|
505
167
|
<body>
|
|
506
|
-
<main
|
|
507
|
-
<header
|
|
508
|
-
<
|
|
509
|
-
|
|
510
|
-
<span class="meta">screens=${screens.length} | profile=${escapeHtml(profileSelection || "none")} | mapped references=${mappedRefs}</span>
|
|
511
|
-
</div>
|
|
512
|
-
<nav class="nav" aria-label="Screen navigation">
|
|
513
|
-
${screens.map((screen) => `<a href="#screen-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</a>`).join("")}
|
|
514
|
-
</nav>
|
|
168
|
+
<main>
|
|
169
|
+
<header>
|
|
170
|
+
<h1>Prototype generation manifest ready</h1>
|
|
171
|
+
<p class="summary">This launcher lists the screen HTML files owned by the running agent via the design-prototype skill. The CLI emitted the manifest and did not generate screen HTML.</p>
|
|
515
172
|
</header>
|
|
516
|
-
|
|
173
|
+
<section class="grid" aria-label="Prototype screens">
|
|
174
|
+
${links}
|
|
175
|
+
</section>
|
|
517
176
|
</main>
|
|
518
177
|
</body>
|
|
519
178
|
</html>
|
|
520
179
|
`;
|
|
521
180
|
}
|
|
522
181
|
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const profilePrimitives = statePayload.profilePrimitives || null;
|
|
529
|
-
const mappedRefs = statePayload.referenceMap && typeof statePayload.referenceMap.mappedCount === "number" ? statePayload.referenceMap.mappedCount : 0;
|
|
530
|
-
|
|
531
|
-
const sections = screens
|
|
532
|
-
.map((screen) => {
|
|
533
|
-
const screenRole = normalizeScreenRole(screen);
|
|
534
|
-
const primitiveHints =
|
|
535
|
-
profilePrimitives &&
|
|
536
|
-
profilePrimitives.screenRoleHints &&
|
|
537
|
-
Array.isArray(profilePrimitives.screenRoleHints[screenRole])
|
|
538
|
-
? profilePrimitives.screenRoleHints[screenRole]
|
|
539
|
-
: [];
|
|
540
|
-
const majorSections = Array.isArray(screen.majorSections) && screen.majorSections.length > 0 ? screen.majorSections : ["Primary content", "Supporting panel"];
|
|
541
|
-
return `
|
|
542
|
-
<section class="prototype-section screen-${escapeHtml(screen.screenId)}" id="screen-${escapeHtml(screen.screenId)}" aria-labelledby="title-${escapeHtml(screen.screenId)}">
|
|
543
|
-
<div class="section-inner">
|
|
544
|
-
<div class="section-header">
|
|
545
|
-
<p class="eyebrow">${escapeHtml(screenRole.replace(/-/g, " "))}</p>
|
|
546
|
-
<h2 id="title-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</h2>
|
|
547
|
-
${screen.route ? `<p class="support">Route: <code>${escapeHtml(screen.route)}</code></p>` : ""}
|
|
548
|
-
${screen.purpose ? `<p class="support">${escapeHtml(screen.purpose)}</p>` : ""}
|
|
549
|
-
</div>
|
|
550
|
-
${renderPrimitiveChips(primitiveHints)}
|
|
551
|
-
${renderStateChips(screen.requiredStates)}
|
|
552
|
-
<div class="screen-grid">
|
|
553
|
-
<article class="panel">
|
|
554
|
-
<h3>Main layout</h3>
|
|
555
|
-
<ul>${majorSections.slice(0, 5).map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>
|
|
556
|
-
</article>
|
|
557
|
-
<article class="panel">
|
|
558
|
-
<h3>Primary action</h3>
|
|
559
|
-
<p>${escapeHtml(screen.primaryAction || "Primary CTA from explicit input")}</p>
|
|
560
|
-
<div class="button-row"><a class="btn btn-primary" href="#screen-${escapeHtml(screen.screenId)}">Trigger CTA</a></div>
|
|
561
|
-
</article>
|
|
562
|
-
</div>
|
|
563
|
-
</div>
|
|
564
|
-
</section>`;
|
|
565
|
-
})
|
|
566
|
-
.join("");
|
|
567
|
-
|
|
568
|
-
return `<!doctype html>
|
|
569
|
-
<html lang="en">
|
|
570
|
-
<head>
|
|
571
|
-
<meta charset="utf-8">
|
|
572
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
573
|
-
<title>SDTK-DESIGN Multi-Screen Prototype</title>
|
|
574
|
-
<style>
|
|
575
|
-
: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}; }
|
|
576
|
-
* { box-sizing:border-box; }
|
|
577
|
-
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; }
|
|
578
|
-
.shell { width:min(1200px, calc(100vw - 24px)); margin:0 auto; padding:24px 0 44px; }
|
|
579
|
-
.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; }
|
|
580
|
-
.topline { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
|
|
581
|
-
.meta { color:var(--muted); font-size:12px; font-weight:700; }
|
|
582
|
-
.nav { display:flex; gap:8px; overflow:auto; margin-top:10px; padding-bottom:4px; }
|
|
583
|
-
.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; }
|
|
584
|
-
.prototype-section { background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); margin:14px 0; }
|
|
585
|
-
.section-inner { padding:20px; }
|
|
586
|
-
.eyebrow { margin:0 0 6px; color:var(--primary); font-size:12px; font-weight:800; text-transform:uppercase; }
|
|
587
|
-
h2,h3,p { margin-top:0; }
|
|
588
|
-
h2 { margin-bottom:8px; font-size:28px; }
|
|
589
|
-
.support { color:var(--muted); margin-bottom:10px; }
|
|
590
|
-
.chip-row { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 12px; }
|
|
591
|
-
.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)); }
|
|
592
|
-
.chip-state { color:var(--primary); border-color:color-mix(in srgb, var(--primary) 35%, var(--border)); }
|
|
593
|
-
.screen-grid { display:grid; grid-template-columns:1.2fr 1fr; gap:12px; }
|
|
594
|
-
.panel { border:1px solid var(--border); border-radius:8px; padding:14px; }
|
|
595
|
-
.panel ul { margin:0; padding-left:18px; }
|
|
596
|
-
.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; }
|
|
597
|
-
.btn-primary { background:var(--primary); color:#fff; }
|
|
598
|
-
@media (max-width: 820px) { .screen-grid { grid-template-columns:1fr; } h2 { font-size:24px; } }
|
|
599
|
-
</style>
|
|
600
|
-
</head>
|
|
601
|
-
<body>
|
|
602
|
-
<main class="shell style-${styleName}" data-style-preset="${styleName}">
|
|
603
|
-
<header class="topbar">
|
|
604
|
-
<div class="topline">
|
|
605
|
-
<strong>SDTK-DESIGN multi-screen prototype</strong>
|
|
606
|
-
<span class="meta">screens=${screens.length} | profile=${escapeHtml(profileSelection || "none")} | mapped references=${mappedRefs}</span>
|
|
607
|
-
</div>
|
|
608
|
-
<nav class="nav" aria-label="Screen navigation">
|
|
609
|
-
${screens.map((screen) => `<a href="#screen-${escapeHtml(screen.screenId)}">${escapeHtml(screen.title)}</a>`).join("")}
|
|
610
|
-
</nav>
|
|
611
|
-
</header>
|
|
612
|
-
${sections}
|
|
613
|
-
</main>
|
|
614
|
-
</body>
|
|
615
|
-
</html>
|
|
616
|
-
`;
|
|
182
|
+
function managedPrototypeTargets(paths) {
|
|
183
|
+
return [
|
|
184
|
+
{ relativePath: "docs/design/prototype/.manifest.json", filePath: paths.prototypeManifestPath },
|
|
185
|
+
{ relativePath: "docs/design/prototype/index.html", filePath: paths.prototypeIndexPath },
|
|
186
|
+
];
|
|
617
187
|
}
|
|
618
188
|
|
|
619
|
-
function runDesignPrototype({ projectPath, force = false
|
|
620
|
-
const explicitStyleName = typeof style === "string" && style.trim() ? resolveStyleName(style) : null;
|
|
189
|
+
function runDesignPrototype({ projectPath, force = false }) {
|
|
621
190
|
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
622
191
|
if (!fs.existsSync(resolvedProjectPath) || !fs.statSync(resolvedProjectPath).isDirectory()) {
|
|
623
192
|
throw new ValidationError(`--project-path is not a valid directory: ${resolvedProjectPath}. No project files were changed.`);
|
|
@@ -625,152 +194,70 @@ function runDesignPrototype({ projectPath, force = false, style }) {
|
|
|
625
194
|
|
|
626
195
|
const paths = describeDesignPaths(resolvedProjectPath);
|
|
627
196
|
const inputContractState = readInputContractState(paths);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const missing =
|
|
197
|
+
if (!assertReadyInputContract(inputContractState)) {
|
|
198
|
+
throw new ValidationError(
|
|
199
|
+
"sdtk-design prototype requires .sdtk/design/START_INPUT_STATE.json with analysisStatus=INPUT_CONTRACT_READY. Run sdtk-design start --from-spec with explicit design inputs first. No project files were changed."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const screens = inputContractState.screenModel.screens;
|
|
204
|
+
const missing = requiredPrototypeInputs(paths, screens).filter((target) => !fs.existsSync(target.filePath));
|
|
636
205
|
if (missing.length > 0) {
|
|
637
206
|
const list = missing.map((target) => target.relativePath).join(", ");
|
|
638
|
-
throw new ValidationError(`Missing required
|
|
207
|
+
throw new ValidationError(`Missing required prototype generation artifacts: ${list}. No project files were changed.`);
|
|
639
208
|
}
|
|
640
|
-
|
|
641
|
-
const existingManagedPrototype = managedPrototypeTargets(paths
|
|
642
|
-
fs.existsSync(target.filePath)
|
|
643
|
-
);
|
|
209
|
+
|
|
210
|
+
const existingManagedPrototype = managedPrototypeTargets(paths).filter((target) => fs.existsSync(target.filePath));
|
|
644
211
|
if (existingManagedPrototype.length > 0 && !force) {
|
|
645
212
|
throw new ValidationError(
|
|
646
|
-
`
|
|
213
|
+
`Managed prototype launcher output already exists: ${existingManagedPrototype
|
|
647
214
|
.map((target) => target.relativePath)
|
|
648
|
-
.join(", ")}. Re-run with --force to replace
|
|
215
|
+
.join(", ")}. Re-run with --force to replace manifest and launcher. Screen HTML files are never overwritten by this command.`
|
|
649
216
|
);
|
|
650
217
|
}
|
|
651
218
|
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
.filter(([relativePath]) => relativePath.startsWith("docs/design/wireframes/"))
|
|
658
|
-
.map(([, key]) => {
|
|
659
|
-
const filePath =
|
|
660
|
-
key === "landingWireframePath"
|
|
661
|
-
? wireframePath(paths, "LANDING.md")
|
|
662
|
-
: key === "onboardingWireframePath"
|
|
663
|
-
? wireframePath(paths, "ONBOARDING.md")
|
|
664
|
-
: wireframePath(paths, "DASHBOARD.md");
|
|
665
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
666
|
-
});
|
|
667
|
-
let renderResult = null;
|
|
668
|
-
let content = null;
|
|
669
|
-
let bundle = null;
|
|
670
|
-
if (canUseMultiScreen) {
|
|
671
|
-
bundle = loadHighFidelityContracts(paths, inputContractState);
|
|
672
|
-
if (prototypeMode === "multi" && bundle.ready) {
|
|
673
|
-
renderResult = renderMultiPagePrototype({
|
|
674
|
-
paths,
|
|
675
|
-
statePayload: inputContractState,
|
|
676
|
-
contractBundle: bundle,
|
|
677
|
-
styleName,
|
|
678
|
-
});
|
|
679
|
-
} else if (bundle.ready) {
|
|
680
|
-
content = highFidelityMultiScreenPrototypeContent({
|
|
681
|
-
style: styleName,
|
|
682
|
-
statePayload: inputContractState,
|
|
683
|
-
contractBundle: bundle,
|
|
684
|
-
});
|
|
685
|
-
} else {
|
|
686
|
-
content = multiScreenPrototypeContent({
|
|
687
|
-
style: styleName,
|
|
688
|
-
statePayload: inputContractState,
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
} else {
|
|
692
|
-
content = prototypeContent({
|
|
693
|
-
style: styleName,
|
|
694
|
-
briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
|
|
695
|
-
screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
|
|
696
|
-
designSystemContent,
|
|
697
|
-
wireframeContents,
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (!renderResult) {
|
|
702
|
-
fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
|
|
703
|
-
fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
|
|
704
|
-
}
|
|
219
|
+
const manifest = buildPrototypeManifest(screens);
|
|
220
|
+
fs.mkdirSync(paths.prototypePath, { recursive: true });
|
|
221
|
+
fs.mkdirSync(paths.prototypeScreensPath, { recursive: true });
|
|
222
|
+
fs.writeFileSync(paths.prototypeManifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
223
|
+
fs.writeFileSync(paths.prototypeIndexPath, launcherHtml(manifest), "utf-8");
|
|
705
224
|
|
|
706
225
|
return {
|
|
707
226
|
projectPath: resolvedProjectPath,
|
|
227
|
+
relativeManifestPath: "docs/design/prototype/.manifest.json",
|
|
708
228
|
relativePrototypePath: "docs/design/prototype/index.html",
|
|
229
|
+
screenCount: manifest.screens.length,
|
|
709
230
|
forced: Boolean(force),
|
|
710
|
-
style: styleName,
|
|
711
|
-
mode: renderResult
|
|
712
|
-
? renderResult.mode
|
|
713
|
-
: canUseMultiScreen
|
|
714
|
-
? bundle && bundle.ready
|
|
715
|
-
? "high-fidelity-single-file"
|
|
716
|
-
: "multi-screen"
|
|
717
|
-
: "simple",
|
|
718
|
-
screenCount: canUseMultiScreen ? inputContractState.screenModel.screens.length : 3,
|
|
719
|
-
warnings: canUseMultiScreen && bundle ? bundle.warnings : [],
|
|
720
|
-
generatedPages: renderResult ? renderResult.screenPages : [],
|
|
721
|
-
densityReport: renderResult ? renderResult.densityReport : null,
|
|
722
231
|
};
|
|
723
232
|
}
|
|
724
233
|
|
|
725
234
|
function cmdPrototype(args) {
|
|
726
|
-
const { flags } = parseFlags(args || [], PROTOTYPE_FLAG_DEFS);
|
|
235
|
+
const { flags, positionals } = parseFlags(args || [], PROTOTYPE_FLAG_DEFS);
|
|
727
236
|
if (flags.help) return cmdPrototypeHelp();
|
|
237
|
+
if (positionals.length > 0) {
|
|
238
|
+
throw new ValidationError(
|
|
239
|
+
`Unsupported prototype argument: ${positionals.join(" ")}. Use sdtk-design prototype [--project-path <path>] [--force]. No project files were changed.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
728
242
|
|
|
729
243
|
const result = runDesignPrototype({
|
|
730
244
|
projectPath: flags["project-path"],
|
|
731
245
|
force: Boolean(flags.force),
|
|
732
|
-
style: flags.style,
|
|
733
246
|
});
|
|
734
247
|
|
|
248
|
+
console.log(`[design] Wrote ${result.relativeManifestPath}: ${result.projectPath}`);
|
|
735
249
|
console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
|
|
736
|
-
console.log(`[design]
|
|
737
|
-
console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen(s))`);
|
|
738
|
-
if (result.densityReport) {
|
|
739
|
-
if (result.densityReport.mode === "bytes") {
|
|
740
|
-
console.log("[design] Density mode: bytes (deprecated rollback; report shape only, no filler emission)");
|
|
741
|
-
console.log(
|
|
742
|
-
`[design] Density: ${result.densityReport.pass ? "PASS" : "FAIL"} total=${result.densityReport.totalBytes}/${result.densityReport.minTotalBytes} bytes`
|
|
743
|
-
);
|
|
744
|
-
for (const page of result.densityReport.pages) {
|
|
745
|
-
console.log(`[design] Density page: ${page.screenId} ${page.byteLength}/${page.minBytes} bytes ${page.pass ? "PASS" : "FAIL"}`);
|
|
746
|
-
}
|
|
747
|
-
} else {
|
|
748
|
-
const aggregate = result.densityReport.aggregate || {};
|
|
749
|
-
console.log(
|
|
750
|
-
`[design] Density: ${result.densityReport.pass ? "PASS" : "FAIL"} mode=semantic sections=${aggregate.sectionCount || 0} components=${aggregate.componentCount || 0} dataSlots=${aggregate.dataSlotCount || 0} states=${aggregate.stateCount || 0}`
|
|
751
|
-
);
|
|
752
|
-
for (const page of result.densityReport.pages) {
|
|
753
|
-
const missing = Array.isArray(page.findings) && page.findings.length > 0 ? ` (${page.findings.join("; ")})` : "";
|
|
754
|
-
console.log(
|
|
755
|
-
`[design] Density page: ${page.screenId} sections=${page.sectionCount} components=${page.componentCount} dataSlots=${page.dataSlotCount} states=${page.stateCount} ${page.pass ? "PASS" : "FAIL"}${missing}`
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
761
|
-
console.log(`[design] Contract fallback: ${result.warnings.join("; ")}`);
|
|
762
|
-
}
|
|
250
|
+
console.log(`[design] Mode: manifest-only (${result.screenCount} screen(s))`);
|
|
763
251
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
764
|
-
console.log("[design]
|
|
252
|
+
console.log("[design] Screen HTML generation is owned by the design-prototype skill and running agent.");
|
|
765
253
|
console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
|
|
766
|
-
console.log("[design] Next: sdtk-design review --artifact docs/design/prototype/index.html");
|
|
767
|
-
return
|
|
254
|
+
console.log("[design] Next: run the design-prototype skill, then sdtk-design review --artifact docs/design/prototype/index.html");
|
|
255
|
+
return 0;
|
|
768
256
|
}
|
|
769
257
|
|
|
770
258
|
module.exports = {
|
|
259
|
+
buildPrototypeManifest,
|
|
771
260
|
cmdPrototype,
|
|
772
261
|
cmdPrototypeHelp,
|
|
773
|
-
inferStyleFromDesignSystem,
|
|
774
|
-
prototypeContent,
|
|
775
262
|
runDesignPrototype,
|
|
776
263
|
};
|