sdtk-design-kit 0.1.2 → 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 +2 -2
- package/src/commands/prototype.js +276 -17
- package/src/commands/review.js +230 -0
- package/src/commands/start.js +23 -1
- package/src/lib/component-contract.js +142 -0
- package/src/lib/design-input-contract.js +271 -11
- package/src/lib/design-paths.js +24 -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 = {
|
|
@@ -101,16 +102,38 @@ Reads:
|
|
|
101
102
|
.sdtk/design/START_INPUT_STATE.json (optional; enables explicit multi-screen rendering when ready)
|
|
102
103
|
|
|
103
104
|
Creates:
|
|
104
|
-
|
|
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
|
|
105
112
|
|
|
106
113
|
Safety:
|
|
107
114
|
Local files only.
|
|
108
115
|
Existing prototype index is not overwritten unless --force is explicit.
|
|
109
116
|
No production app code outside docs/design/prototype.
|
|
110
|
-
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.`);
|
|
111
118
|
return 0;
|
|
112
119
|
}
|
|
113
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
|
+
|
|
114
137
|
function wireframePath(paths, fileName) {
|
|
115
138
|
return path.join(paths.wireframesPath, fileName);
|
|
116
139
|
}
|
|
@@ -309,7 +332,192 @@ function renderPrimitiveChips(primitiveNames) {
|
|
|
309
332
|
|
|
310
333
|
function renderStateChips(requiredStates) {
|
|
311
334
|
if (!Array.isArray(requiredStates) || requiredStates.length === 0) return "";
|
|
312
|
-
return `<div class="chip-row">${requiredStates
|
|
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
|
+
`;
|
|
313
521
|
}
|
|
314
522
|
|
|
315
523
|
function multiScreenPrototypeContent({ style, statePayload }) {
|
|
@@ -429,8 +637,16 @@ function runDesignPrototype({ projectPath, force = false, style }) {
|
|
|
429
637
|
const list = missing.map((target) => target.relativePath).join(", ");
|
|
430
638
|
throw new ValidationError(`Missing required design artifacts: ${list}. Run sdtk-design start --idea "<idea>" first. No project files were changed.`);
|
|
431
639
|
}
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
);
|
|
434
650
|
}
|
|
435
651
|
|
|
436
652
|
const designSystemContent = fs.existsSync(paths.designSystemPath) ? fs.readFileSync(paths.designSystemPath, "utf-8") : "";
|
|
@@ -448,29 +664,61 @@ function runDesignPrototype({ projectPath, force = false, style }) {
|
|
|
448
664
|
: wireframePath(paths, "DASHBOARD.md");
|
|
449
665
|
return fs.readFileSync(filePath, "utf-8");
|
|
450
666
|
});
|
|
451
|
-
|
|
452
|
-
|
|
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({
|
|
453
681
|
style: styleName,
|
|
454
682
|
statePayload: inputContractState,
|
|
455
|
-
|
|
456
|
-
|
|
683
|
+
contractBundle: bundle,
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
content = multiScreenPrototypeContent({
|
|
457
687
|
style: styleName,
|
|
458
|
-
|
|
459
|
-
screenMapContent: fs.readFileSync(paths.screenMapPath, "utf-8"),
|
|
460
|
-
designSystemContent,
|
|
461
|
-
wireframeContents,
|
|
688
|
+
statePayload: inputContractState,
|
|
462
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
|
+
}
|
|
463
700
|
|
|
464
|
-
|
|
465
|
-
|
|
701
|
+
if (!renderResult) {
|
|
702
|
+
fs.mkdirSync(path.dirname(paths.prototypeIndexPath), { recursive: true });
|
|
703
|
+
fs.writeFileSync(paths.prototypeIndexPath, content, "utf-8");
|
|
704
|
+
}
|
|
466
705
|
|
|
467
706
|
return {
|
|
468
707
|
projectPath: resolvedProjectPath,
|
|
469
708
|
relativePrototypePath: "docs/design/prototype/index.html",
|
|
470
709
|
forced: Boolean(force),
|
|
471
710
|
style: styleName,
|
|
472
|
-
mode:
|
|
711
|
+
mode: renderResult
|
|
712
|
+
? renderResult.mode
|
|
713
|
+
: canUseMultiScreen
|
|
714
|
+
? bundle && bundle.ready
|
|
715
|
+
? "high-fidelity-single-file"
|
|
716
|
+
: "multi-screen"
|
|
717
|
+
: "simple",
|
|
473
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,
|
|
474
722
|
};
|
|
475
723
|
}
|
|
476
724
|
|
|
@@ -486,7 +734,18 @@ function cmdPrototype(args) {
|
|
|
486
734
|
|
|
487
735
|
console.log(`[design] Wrote ${result.relativePrototypePath}: ${result.projectPath}`);
|
|
488
736
|
console.log(`[design] Style: ${result.style}`);
|
|
489
|
-
console.log(`[design] Mode: ${result.mode} (${result.screenCount} screen
|
|
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
|
+
}
|
|
490
749
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
491
750
|
console.log("[design] Prototype mode: static HTML/CSS preview only.");
|
|
492
751
|
console.log("[design] No app code outside docs/design/prototype, server, network, .sdtk/atlas, or SDTK-WIKI output was modified.");
|
package/src/commands/review.js
CHANGED
|
@@ -75,6 +75,197 @@ function includesAny(content, terms) {
|
|
|
75
75
|
return terms.some((term) => lower.includes(term.toLowerCase()));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function readInputContractState(paths) {
|
|
79
|
+
if (!fs.existsSync(paths.designStartInputStatePath) || !fs.statSync(paths.designStartInputStatePath).isFile()) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(fs.readFileSync(paths.designStartInputStatePath, "utf-8"));
|
|
84
|
+
} catch (_err) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeScreenRole(screen) {
|
|
90
|
+
const token = `${screen.screenId || ""} ${screen.title || ""}`.toLowerCase();
|
|
91
|
+
if (token.includes("home")) return "home";
|
|
92
|
+
if (token.includes("category")) return "category";
|
|
93
|
+
if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
|
|
94
|
+
if (token.includes("search")) return "search";
|
|
95
|
+
if (token.includes("cart")) return "cart";
|
|
96
|
+
if (token.includes("checkout")) return "checkout";
|
|
97
|
+
if (token.includes("order-history") || token.includes("order history")) return "order-history";
|
|
98
|
+
if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
|
|
99
|
+
if (token.includes("account")) return "account-info";
|
|
100
|
+
if (token.includes("configurator") || token.includes("mode-b")) return "mode-b-configurator";
|
|
101
|
+
return screen.screenId || "screen";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function requiredRoleMarkers(role) {
|
|
105
|
+
const map = {
|
|
106
|
+
home: ["hero", "featured products", "configurator"],
|
|
107
|
+
category: ["filter sidebar", "product grid"],
|
|
108
|
+
"product-detail": ["spec-table", "add to cart"],
|
|
109
|
+
search: ["search-toolbar", "no-result"],
|
|
110
|
+
cart: ["cart-table", "checkout"],
|
|
111
|
+
checkout: ["checkout-stepper", "summary card"],
|
|
112
|
+
"order-history": ["order history list"],
|
|
113
|
+
"order-detail": ["timeline", "detail summary"],
|
|
114
|
+
"account-info": ["account sidebar", "view or edit form"],
|
|
115
|
+
"mode-b-configurator": ["configurator wizard", "preview panel", "bom table", "recalculate", "add bom to cart"],
|
|
116
|
+
};
|
|
117
|
+
return map[role] || [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function listPrototypeScreenFiles(paths) {
|
|
121
|
+
if (!fs.existsSync(paths.prototypeScreensPath) || !fs.statSync(paths.prototypeScreensPath).isDirectory()) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
return fs
|
|
125
|
+
.readdirSync(paths.prototypeScreensPath)
|
|
126
|
+
.filter((name) => name.toLowerCase().endsWith(".html"))
|
|
127
|
+
.sort()
|
|
128
|
+
.map((name) => path.join(paths.prototypeScreensPath, name));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function combinedPrototypeReviewContent({ artifactPath, artifactContent, paths }) {
|
|
132
|
+
const screenFiles = listPrototypeScreenFiles(paths);
|
|
133
|
+
if (screenFiles.length === 0) {
|
|
134
|
+
return artifactContent;
|
|
135
|
+
}
|
|
136
|
+
const parts = [artifactContent];
|
|
137
|
+
if (path.resolve(artifactPath) !== path.resolve(paths.prototypeIndexPath) && fs.existsSync(paths.prototypeIndexPath)) {
|
|
138
|
+
parts.push(fs.readFileSync(paths.prototypeIndexPath, "utf-8"));
|
|
139
|
+
}
|
|
140
|
+
for (const filePath of screenFiles) {
|
|
141
|
+
if (path.resolve(filePath) === path.resolve(artifactPath)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
parts.push(fs.readFileSync(filePath, "utf-8"));
|
|
145
|
+
}
|
|
146
|
+
return parts.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function evaluateHighFidelityGate({ artifactContent, statePayload, projectPath, paths }) {
|
|
150
|
+
const html = String(artifactContent || "").toLowerCase();
|
|
151
|
+
const screens = statePayload.screenModel.screens || [];
|
|
152
|
+
const profile = statePayload.profileSelection || "none";
|
|
153
|
+
const hasTokens = fs.existsSync(paths.designTokensPath);
|
|
154
|
+
const hasComponentLibrary = fs.existsSync(paths.componentPatternLibraryPath);
|
|
155
|
+
const hasPre = html.includes("<pre");
|
|
156
|
+
const hasRawJsonRisk = html.includes("json.stringify") || html.includes("raw json") || html.includes("{\"") || html.includes("not_found");
|
|
157
|
+
const hasNav = html.includes('aria-label="screen navigation"') || html.includes("screen navigation");
|
|
158
|
+
const hasStateChips = html.includes('data-chip-type="state"') || html.includes("chip");
|
|
159
|
+
const sectionRows = [];
|
|
160
|
+
let missingCount = 0;
|
|
161
|
+
for (const screen of screens) {
|
|
162
|
+
const id = String(screen.screenId || "");
|
|
163
|
+
const title = String(screen.title || id);
|
|
164
|
+
const role = normalizeScreenRole(screen);
|
|
165
|
+
const idMarker = `id="screen-${id}"`;
|
|
166
|
+
const navMarkers = [`href="#screen-${id}"`, `href="screens/${id}.html"`, `href="${id}.html"`];
|
|
167
|
+
const roleMarkers = requiredRoleMarkers(role);
|
|
168
|
+
const missing = [];
|
|
169
|
+
if (!html.includes(idMarker.toLowerCase())) missing.push("missing screen section id");
|
|
170
|
+
if (!navMarkers.some((marker) => html.includes(marker.toLowerCase()))) missing.push("missing navigation link");
|
|
171
|
+
for (const marker of roleMarkers) {
|
|
172
|
+
if (!html.includes(marker)) missing.push(`missing component marker: ${marker}`);
|
|
173
|
+
}
|
|
174
|
+
if (missing.length > 0) missingCount += 1;
|
|
175
|
+
sectionRows.push({
|
|
176
|
+
title,
|
|
177
|
+
role,
|
|
178
|
+
missing,
|
|
179
|
+
pass: missing.length === 0,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let verdict = "PASS_HIGH_FIDELITY_READY";
|
|
184
|
+
const blockers = [];
|
|
185
|
+
if (screens.length === 0) {
|
|
186
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
187
|
+
blockers.push("explicit screen model is empty");
|
|
188
|
+
}
|
|
189
|
+
if (!hasComponentLibrary) {
|
|
190
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
191
|
+
blockers.push("component pattern library missing");
|
|
192
|
+
}
|
|
193
|
+
if (!hasTokens) {
|
|
194
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
195
|
+
blockers.push("design tokens missing");
|
|
196
|
+
}
|
|
197
|
+
if (!hasNav) {
|
|
198
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
199
|
+
blockers.push("inter-screen navigation contract missing");
|
|
200
|
+
}
|
|
201
|
+
if (!hasStateChips) {
|
|
202
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
203
|
+
blockers.push("state chip coverage missing");
|
|
204
|
+
}
|
|
205
|
+
if (missingCount > 0) {
|
|
206
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
207
|
+
blockers.push(`${missingCount} screen(s) missing required component markers`);
|
|
208
|
+
}
|
|
209
|
+
if (hasPre || hasRawJsonRisk) {
|
|
210
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
211
|
+
blockers.push("debug/raw JSON UI risk detected");
|
|
212
|
+
}
|
|
213
|
+
if (profile === "b2b-commerce" && !html.includes("configurator wizard")) {
|
|
214
|
+
verdict = "FAIL_WITH_PRODUCT_GAPS";
|
|
215
|
+
blockers.push("configurator completeness missing");
|
|
216
|
+
}
|
|
217
|
+
if (html.includes("main layout</h3>") && html.includes("primary action</h3>")) {
|
|
218
|
+
verdict = "FAIL_HIGH_FIDELITY_NOT_READY";
|
|
219
|
+
blockers.push("generic scaffold markers detected (Main layout / Primary action)");
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
verdict,
|
|
223
|
+
blockers,
|
|
224
|
+
profile,
|
|
225
|
+
sectionRows,
|
|
226
|
+
hasTokens,
|
|
227
|
+
hasComponentLibrary,
|
|
228
|
+
hasNav,
|
|
229
|
+
hasStateChips,
|
|
230
|
+
hasPre,
|
|
231
|
+
hasRawJsonRisk,
|
|
232
|
+
projectPath,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fidelityReviewContent(result) {
|
|
237
|
+
return [
|
|
238
|
+
"# Design Fidelity Review",
|
|
239
|
+
"",
|
|
240
|
+
`- Verdict: ${result.verdict}`,
|
|
241
|
+
`- Profile: ${result.profile}`,
|
|
242
|
+
`- Component library: ${result.hasComponentLibrary ? "present" : "missing"}`,
|
|
243
|
+
`- Design tokens: ${result.hasTokens ? "present" : "missing"}`,
|
|
244
|
+
`- Navigation contract: ${result.hasNav ? "present" : "missing"}`,
|
|
245
|
+
`- State coverage markers: ${result.hasStateChips ? "present" : "missing"}`,
|
|
246
|
+
`- Raw <pre> debug risk: ${result.hasPre ? "detected" : "not detected"}`,
|
|
247
|
+
`- Raw JSON debug risk: ${result.hasRawJsonRisk ? "detected" : "not detected"}`,
|
|
248
|
+
"",
|
|
249
|
+
"## Screen-By-Screen Coverage",
|
|
250
|
+
"",
|
|
251
|
+
"| Screen | Role | Result | Actionable notes |",
|
|
252
|
+
"|---|---|---|---|",
|
|
253
|
+
...result.sectionRows.map((row) => `| ${row.title} | ${row.role} | ${row.pass ? "PASS" : "FAIL"} | ${row.pass ? "coverage complete" : row.missing.join("; ")} |`),
|
|
254
|
+
"",
|
|
255
|
+
"## Gate Findings",
|
|
256
|
+
"",
|
|
257
|
+
...(result.blockers.length > 0 ? result.blockers.map((item) => `- ${item}`) : ["- No blocking fidelity gaps found."]),
|
|
258
|
+
"",
|
|
259
|
+
"## Actions",
|
|
260
|
+
"",
|
|
261
|
+
"- Fill missing component markers per failed screen.",
|
|
262
|
+
"- Ensure design tokens and component pattern library are generated before review.",
|
|
263
|
+
"- Remove raw debug UI surfaces (`<pre>` and JSON dumps) from customer-facing prototype output.",
|
|
264
|
+
"- Re-run `sdtk-design prototype` and `sdtk-design review --artifact docs/design/prototype/index.html`.",
|
|
265
|
+
"",
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
78
269
|
function visualPolishChecks({ artifactRelativePath, artifactContent, prototypeHtml }) {
|
|
79
270
|
const lower = String(artifactContent || "").toLowerCase();
|
|
80
271
|
const hasLanding = lower.includes("section-landing") || lower.includes("landing");
|
|
@@ -191,12 +382,47 @@ function runDesignReview({ artifact, projectPath, force = false }) {
|
|
|
191
382
|
|
|
192
383
|
const artifactContent = fs.readFileSync(artifactPath, "utf-8");
|
|
193
384
|
const artifactRelativePath = toPosix(path.relative(resolvedProjectPath, artifactPath));
|
|
385
|
+
const statePayload = readInputContractState(paths);
|
|
386
|
+
const shouldRunFidelityGate =
|
|
387
|
+
isPrototypeHtmlArtifact(artifactRelativePath, artifactContent) &&
|
|
388
|
+
statePayload &&
|
|
389
|
+
statePayload.mode === "from-spec" &&
|
|
390
|
+
statePayload.analysisStatus === "INPUT_CONTRACT_READY" &&
|
|
391
|
+
statePayload.screenModel &&
|
|
392
|
+
Array.isArray(statePayload.screenModel.screens) &&
|
|
393
|
+
statePayload.screenModel.screens.length >= 4;
|
|
194
394
|
fs.mkdirSync(paths.reviewsPath, { recursive: true });
|
|
195
395
|
fs.writeFileSync(reportPath, reviewContent({ artifactRelativePath, artifactContent }), "utf-8");
|
|
196
396
|
|
|
397
|
+
let fidelityReportRelativePath = null;
|
|
398
|
+
let fidelityVerdict = null;
|
|
399
|
+
if (shouldRunFidelityGate) {
|
|
400
|
+
const fidelityArtifactContent = combinedPrototypeReviewContent({
|
|
401
|
+
artifactPath,
|
|
402
|
+
artifactContent,
|
|
403
|
+
paths,
|
|
404
|
+
});
|
|
405
|
+
const fidelityResult = evaluateHighFidelityGate({
|
|
406
|
+
artifactContent: fidelityArtifactContent,
|
|
407
|
+
statePayload,
|
|
408
|
+
projectPath: resolvedProjectPath,
|
|
409
|
+
paths,
|
|
410
|
+
});
|
|
411
|
+
const fidelityName = `DESIGN_FIDELITY_REVIEW_${formatDateYYYYMMDD()}.md`;
|
|
412
|
+
const fidelityPath = path.join(paths.reviewsPath, fidelityName);
|
|
413
|
+
if (fs.existsSync(fidelityPath) && !force) {
|
|
414
|
+
throw new ValidationError(`docs/design/reviews/${fidelityName} already exists. Re-run with --force to replace this managed fidelity review report.`);
|
|
415
|
+
}
|
|
416
|
+
fs.writeFileSync(fidelityPath, fidelityReviewContent(fidelityResult), "utf-8");
|
|
417
|
+
fidelityReportRelativePath = `docs/design/reviews/${fidelityName}`;
|
|
418
|
+
fidelityVerdict = fidelityResult.verdict;
|
|
419
|
+
}
|
|
420
|
+
|
|
197
421
|
return {
|
|
198
422
|
projectPath: resolvedProjectPath,
|
|
199
423
|
relativeReportPath: `docs/design/reviews/${reportName}`,
|
|
424
|
+
fidelityReportRelativePath,
|
|
425
|
+
fidelityVerdict,
|
|
200
426
|
forced: Boolean(force),
|
|
201
427
|
};
|
|
202
428
|
}
|
|
@@ -212,6 +438,10 @@ function cmdReview(args) {
|
|
|
212
438
|
});
|
|
213
439
|
|
|
214
440
|
console.log(`[design] Wrote ${result.relativeReportPath}: ${result.projectPath}`);
|
|
441
|
+
if (result.fidelityReportRelativePath) {
|
|
442
|
+
console.log(`[design] Wrote ${result.fidelityReportRelativePath}: ${result.projectPath}`);
|
|
443
|
+
console.log(`[design] Fidelity verdict: ${result.fidelityVerdict}`);
|
|
444
|
+
}
|
|
215
445
|
console.log(`[design] Review mode: local artifact`);
|
|
216
446
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
217
447
|
console.log("[design] No URL, browser, screenshot, vision, network, .sdtk/atlas, or SDTK-WIKI output was used.");
|