sdtk-design-kit 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/handoff.js +357 -203
- package/src/commands/help.js +3 -0
- package/src/commands/prototype.js +436 -26
- package/src/commands/review.js +230 -0
- package/src/commands/start.js +105 -8
- package/src/commands/status.js +37 -1
- package/src/lib/component-contract.js +142 -0
- package/src/lib/design-input-contract.js +683 -0
- package/src/lib/design-paths.js +27 -0
- package/src/lib/design-profiles.js +58 -0
- package/src/lib/prototype-density.js +147 -0
- package/src/lib/prototype-renderer.js +325 -0
- package/src/lib/screen-briefs.js +340 -0
|
@@ -6,6 +6,7 @@ const { parseFlags } = require("../lib/args");
|
|
|
6
6
|
const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths");
|
|
7
7
|
const { inferDomainProfile } = require("../lib/domain-profile");
|
|
8
8
|
const { ValidationError } = require("../lib/errors");
|
|
9
|
+
const { renderMultiPagePrototype, screenFileName } = require("../lib/prototype-renderer");
|
|
9
10
|
const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
|
|
10
11
|
|
|
11
12
|
const PROTOTYPE_FLAG_DEFS = {
|
|
@@ -67,6 +68,17 @@ const THEME_TOKENS = {
|
|
|
67
68
|
},
|
|
68
69
|
};
|
|
69
70
|
|
|
71
|
+
function readInputContractState(paths) {
|
|
72
|
+
if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
|
|
77
|
+
} catch (_err) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
function cmdPrototypeHelp() {
|
|
71
83
|
console.log(`SDTK-DESIGN Prototype
|
|
72
84
|
|
|
@@ -87,18 +99,41 @@ Reads:
|
|
|
87
99
|
docs/design/wireframes/LANDING.md
|
|
88
100
|
docs/design/wireframes/ONBOARDING.md
|
|
89
101
|
docs/design/wireframes/DASHBOARD.md
|
|
102
|
+
.sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
|
|
90
103
|
|
|
91
104
|
Creates:
|
|
92
|
-
|
|
105
|
+
legacy/simple mode:
|
|
106
|
+
docs/design/prototype/index.html
|
|
107
|
+
from-spec multi-page mode:
|
|
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
|
|
93
112
|
|
|
94
113
|
Safety:
|
|
95
114
|
Local files only.
|
|
96
115
|
Existing prototype index is not overwritten unless --force is explicit.
|
|
97
116
|
No production app code outside docs/design/prototype.
|
|
98
|
-
No JavaScript runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
|
|
117
|
+
No JavaScript app runtime, server, network call, .sdtk/atlas, or SDTK-WIKI output mutation.`);
|
|
99
118
|
return 0;
|
|
100
119
|
}
|
|
101
120
|
|
|
121
|
+
function resolvePrototypeMode(canUseMultiScreen) {
|
|
122
|
+
if (!canUseMultiScreen) {
|
|
123
|
+
return "single";
|
|
124
|
+
}
|
|
125
|
+
const raw = String(process.env.SDTK_DESIGN_PROTOTYPE_MODE || "").trim().toLowerCase();
|
|
126
|
+
if (!raw) {
|
|
127
|
+
return "multi";
|
|
128
|
+
}
|
|
129
|
+
if (raw === "single" || raw === "multi") {
|
|
130
|
+
return raw;
|
|
131
|
+
}
|
|
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
|
+
|
|
102
137
|
function wireframePath(paths, fileName) {
|
|
103
138
|
return path.join(paths.wireframesPath, fileName);
|
|
104
139
|
}
|
|
@@ -275,6 +310,312 @@ function prototypeContent({ style, briefContent, screenMapContent, designSystemC
|
|
|
275
310
|
`;
|
|
276
311
|
}
|
|
277
312
|
|
|
313
|
+
function normalizeScreenRole(screen) {
|
|
314
|
+
const token = `${screen.screenId || ""} ${screen.title || ""}`.toLowerCase();
|
|
315
|
+
if (token.includes("home")) return "home";
|
|
316
|
+
if (token.includes("category")) return "category";
|
|
317
|
+
if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
|
|
318
|
+
if (token.includes("search")) return "search";
|
|
319
|
+
if (token.includes("cart")) return "cart";
|
|
320
|
+
if (token.includes("checkout")) return "checkout";
|
|
321
|
+
if (token.includes("order-history") || token.includes("order history")) return "order-history";
|
|
322
|
+
if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
|
|
323
|
+
if (token.includes("account")) return "account-info";
|
|
324
|
+
if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
|
|
325
|
+
return screen.screenId || "screen";
|
|
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>`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function renderStateChips(requiredStates) {
|
|
334
|
+
if (!Array.isArray(requiredStates) || requiredStates.length === 0) return "";
|
|
335
|
+
return `<div class="chip-row">${requiredStates
|
|
336
|
+
.slice(0, 3)
|
|
337
|
+
.map((item) => `<span class="chip" data-chip-type="state">${escapeHtml(item)}</span>`)
|
|
338
|
+
.join("")}</div>`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function screenBriefPath(paths, screenId) {
|
|
342
|
+
return path.join(paths.screensPath, `${screenId}_DESIGN_BRIEF.md`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function screenBriefSidecarPath(paths, screenId) {
|
|
346
|
+
return path.join(paths.designScreenBriefsStatePath, `${screenId}.json`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function readJsonSafe(filePath) {
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
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
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function highFidelityMultiScreenPrototypeContent({ style, statePayload, contractBundle }) {
|
|
448
|
+
const styleName = resolveStyleName(style);
|
|
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
|
|
454
|
+
.map((screen) => {
|
|
455
|
+
const screenRole = normalizeScreenRole(screen);
|
|
456
|
+
const contract = contractBundle.screenContracts.get(screen.screenId);
|
|
457
|
+
const sidecar = contract && contract.sidecar ? contract.sidecar : null;
|
|
458
|
+
const stateChips = renderStateChips(sidecar && Array.isArray(sidecar.required_states) ? sidecar.required_states : screen.requiredStates);
|
|
459
|
+
return `
|
|
460
|
+
<section class="prototype-section screen-${escapeHtml(screen.screenId)}" id="screen-${escapeHtml(screen.screenId)}" aria-labelledby="title-${escapeHtml(screen.screenId)}">
|
|
461
|
+
<div class="section-inner">
|
|
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>`;
|
|
472
|
+
})
|
|
473
|
+
.join("");
|
|
474
|
+
|
|
475
|
+
return `<!doctype html>
|
|
476
|
+
<html lang="en">
|
|
477
|
+
<head>
|
|
478
|
+
<meta charset="utf-8">
|
|
479
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
480
|
+
<title>SDTK-DESIGN High-Fidelity Prototype</title>
|
|
481
|
+
<style>
|
|
482
|
+
:root { --bg:${tokenOrDefault(tokens, ["color", "surfaceAlt"], "#F5F7FB")}; --surface:${tokenOrDefault(tokens, ["color", "surface"], "#FFFFFF")}; --text:${tokenOrDefault(tokens, ["color", "textPrimary"], "#111827")}; --muted:${tokenOrDefault(tokens, ["color", "textMuted"], "#64748B")}; --primary:${tokenOrDefault(tokens, ["color", "accentPrimary"], "#0F766E")}; --accent:${tokenOrDefault(tokens, ["color", "accentSecondary"], "#2563EB")}; --border:${tokenOrDefault(tokens, ["color", "border"], "#D8E0EA")}; --shadow:0 18px 40px rgba(15,23,42,0.10); }
|
|
483
|
+
* { box-sizing:border-box; } 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; }
|
|
484
|
+
.shell { width:min(1240px, calc(100vw - 24px)); margin:0 auto; padding:24px 0 48px; }
|
|
485
|
+
.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; }
|
|
486
|
+
.topline { display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
|
|
487
|
+
.meta { color:var(--muted); font-size:12px; font-weight:700; }
|
|
488
|
+
.nav { display:flex; gap:8px; overflow:auto; margin-top:10px; padding-bottom:4px; } .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; }
|
|
489
|
+
.prototype-section { background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); margin:14px 0; } .section-inner { padding:20px; }
|
|
490
|
+
.eyebrow { margin:0 0 6px; color:var(--primary); font-size:12px; font-weight:800; text-transform:uppercase; } h2,h3,h4,p { margin-top:0; } h2 { margin-bottom:8px; font-size:28px; } .support { color:var(--muted); margin-bottom:10px; }
|
|
491
|
+
.chip-row { display:flex; gap:8px; flex-wrap:wrap; margin:10px 0 12px; } .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)); } .chip-state { color:var(--primary); border-color:color-mix(in srgb, var(--primary) 35%, var(--border)); }
|
|
492
|
+
.panel { border:1px solid var(--border); border-radius:8px; padding:14px; background:color-mix(in srgb, var(--surface) 95%, var(--bg)); }
|
|
493
|
+
.hf-split { display:grid; grid-template-columns:minmax(220px,0.35fr) minmax(0,1fr); gap:12px; }
|
|
494
|
+
.hf-feature-grid, .hf-card-grid { display:grid; gap:12px; grid-template-columns:repeat(3, minmax(0, 1fr)); }
|
|
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; } }
|
|
503
|
+
</style>
|
|
504
|
+
</head>
|
|
505
|
+
<body>
|
|
506
|
+
<main class="shell style-${styleName}" data-style-preset="${styleName}">
|
|
507
|
+
<header class="topbar">
|
|
508
|
+
<div class="topline">
|
|
509
|
+
<strong>SDTK-DESIGN high-fidelity prototype</strong>
|
|
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>
|
|
515
|
+
</header>
|
|
516
|
+
${sections}
|
|
517
|
+
</main>
|
|
518
|
+
</body>
|
|
519
|
+
</html>
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function multiScreenPrototypeContent({ style, statePayload }) {
|
|
524
|
+
const styleName = resolveStyleName(style);
|
|
525
|
+
const tokens = THEME_TOKENS[styleName];
|
|
526
|
+
const screens = statePayload.screenModel.screens;
|
|
527
|
+
const profileSelection = statePayload.profileSelection || null;
|
|
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
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
|
|
278
619
|
function runDesignPrototype({ projectPath, force = false, style }) {
|
|
279
620
|
const explicitStyleName = typeof style === "string" && style.trim() ? resolveStyleName(style) : null;
|
|
280
621
|
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
@@ -283,44 +624,101 @@ function runDesignPrototype({ projectPath, force = false, style }) {
|
|
|
283
624
|
}
|
|
284
625
|
|
|
285
626
|
const paths = describeDesignPaths(resolvedProjectPath);
|
|
286
|
-
const
|
|
627
|
+
const inputContractState = readInputContractState(paths);
|
|
628
|
+
const canUseMultiScreen =
|
|
629
|
+
inputContractState &&
|
|
630
|
+
inputContractState.mode === "from-spec" &&
|
|
631
|
+
inputContractState.analysisStatus === "INPUT_CONTRACT_READY" &&
|
|
632
|
+
inputContractState.screenModel &&
|
|
633
|
+
Array.isArray(inputContractState.screenModel.screens) &&
|
|
634
|
+
inputContractState.screenModel.screens.length > 0;
|
|
635
|
+
const missing = canUseMultiScreen ? [] : requiredArtifactTargets(paths).filter((target) => !fs.existsSync(target.filePath));
|
|
287
636
|
if (missing.length > 0) {
|
|
288
637
|
const list = missing.map((target) => target.relativePath).join(", ");
|
|
289
638
|
throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
|
|
290
639
|
}
|
|
291
|
-
|
|
292
|
-
|
|
640
|
+
const prototypeMode = resolvePrototypeMode(canUseMultiScreen);
|
|
641
|
+
const existingManagedPrototype = managedPrototypeTargets(paths, inputContractState, prototypeMode).filter((target) =>
|
|
642
|
+
fs.existsSync(target.filePath)
|
|
643
|
+
);
|
|
644
|
+
if (existingManagedPrototype.length > 0 && !force) {
|
|
645
|
+
throw new ValidationError(
|
|
646
|
+
`docs/design/prototype/index.html already exists. Managed prototype output already exists: ${existingManagedPrototype
|
|
647
|
+
.map((target) => target.relativePath)
|
|
648
|
+
.join(", ")}. Re-run with --force to replace managed prototype outputs.`
|
|
649
|
+
);
|
|
293
650
|
}
|
|
294
651
|
|
|
295
|
-
const designSystemContent = fs.readFileSync(paths.designSystemPath, "utf-8");
|
|
296
|
-
const styleName = explicitStyleName || inferStyleFromDesignSystem(designSystemContent);
|
|
297
|
-
const wireframeContents =
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
key
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
652
|
+
const designSystemContent = fs.existsSync(paths.designSystemPath) ? fs.readFileSync(paths.designSystemPath, "utf-8") : "";
|
|
653
|
+
const styleName = explicitStyleName || (designSystemContent ? inferStyleFromDesignSystem(designSystemContent) : DEFAULT_STYLE);
|
|
654
|
+
const wireframeContents = canUseMultiScreen
|
|
655
|
+
? []
|
|
656
|
+
: REQUIRED_ARTIFACTS
|
|
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,
|
|
307
698
|
});
|
|
308
|
-
|
|
309
|
-
style: styleName,
|
|
310
|
-
briefContent: fs.readFileSync(paths.designBriefPath, "utf-8"),
|
|
311
|
-
screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
|
|
312
|
-
designSystemContent,
|
|
313
|
-
wireframeContents,
|
|
314
|
-
});
|
|
699
|
+
}
|
|
315
700
|
|
|
316
|
-
|
|
317
|
-
|
|
701
|
+
if (!renderResult) {
|
|
702
|
+
fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
|
|
703
|
+
fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
|
|
704
|
+
}
|
|
318
705
|
|
|
319
706
|
return {
|
|
320
707
|
projectPath: resolvedProjectPath,
|
|
321
708
|
relativePrototypePath: "docs/design/prototype/index.html",
|
|
322
709
|
forced: Boolean(force),
|
|
323
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,
|
|
324
722
|
};
|
|
325
723
|
}
|
|
326
724
|
|
|
@@ -336,6 +734,18 @@ function cmdPrototype(args) {
|
|
|
336
734
|
|
|
337
735
|
console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
|
|
338
736
|
console.log(`[design] Style: ${result.style}`);
|
|
737
|
+
console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen(s))`);
|
|
738
|
+
if (result.densityReport) {
|
|
739
|
+
console.log(
|
|
740
|
+
`[design] Density: ${result.densityReport.pass ? "PASS" : "FAIL"} total=${result.densityReport.totalBytes}/${result.densityReport.minTotalBytes} bytes`
|
|
741
|
+
);
|
|
742
|
+
for (const page of result.densityReport.pages) {
|
|
743
|
+
console.log(`[design] Density page: ${page.screenId} ${page.byteLength}/${page.minBytes} bytes ${page.pass ? "PASS" : "FAIL"}`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
747
|
+
console.log(`[design] Contract fallback: ${result.warnings.join("; ")}`);
|
|
748
|
+
}
|
|
339
749
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
340
750
|
console.log("[design] Prototype mode: static HTML/CSS preview only.");
|
|
341
751
|
console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
|