sdtk-design-kit 0.2.0 → 0.3.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 +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/help.js +3 -3
- package/src/commands/init.js +53 -0
- package/src/commands/prototype.js +139 -638
- package/src/commands/review.js +515 -458
- package/src/commands/start.js +22 -5
- package/src/commands/status.js +10 -2
- package/src/commands/system.js +186 -14
- package/src/lib/anti-slop-lint.js +199 -0
- 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/prototype-density.js +0 -147
- package/src/lib/prototype-renderer.js +0 -325
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { ROLE_FLOORS, assessPrototypeDensity, normalizeScreenRole } = require("./prototype-density");
|
|
6
|
-
|
|
7
|
-
function escapeHtml(value) {
|
|
8
|
-
return String(value == null ? "" : value)
|
|
9
|
-
.replace(/&/g, "&")
|
|
10
|
-
.replace(/</g, "<")
|
|
11
|
-
.replace(/>/g, ">")
|
|
12
|
-
.replace(/"/g, """);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function tokenOrDefault(tokens, keyPath, fallback) {
|
|
16
|
-
const value = keyPath.reduce((obj, key) => (obj && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : undefined), tokens);
|
|
17
|
-
return value == null ? fallback : value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function screenFileName(screen) {
|
|
21
|
-
return `${String(screen.screenId || "screen").replace(/[^a-zA-Z0-9_-]+/g, "-")}.html`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function screenRelativePath(screen) {
|
|
25
|
-
return `docs/design/prototype/screens/${screenFileName(screen)}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function sharedCss(tokens) {
|
|
29
|
-
return `:root {
|
|
30
|
-
--bg: ${tokenOrDefault(tokens, ["color", "surfaceAlt"], "#F5F7FB")};
|
|
31
|
-
--surface: ${tokenOrDefault(tokens, ["color", "surface"], "#FFFFFF")};
|
|
32
|
-
--text: ${tokenOrDefault(tokens, ["color", "textPrimary"], "#111827")};
|
|
33
|
-
--muted: ${tokenOrDefault(tokens, ["color", "textMuted"], "#64748B")};
|
|
34
|
-
--primary: ${tokenOrDefault(tokens, ["color", "accentPrimary"], "#0F766E")};
|
|
35
|
-
--accent: ${tokenOrDefault(tokens, ["color", "accentSecondary"], "#2563EB")};
|
|
36
|
-
--border: ${tokenOrDefault(tokens, ["color", "border"], "#D8E0EA")};
|
|
37
|
-
--shadow: 0 18px 40px rgba(15,23,42,0.10);
|
|
38
|
-
}
|
|
39
|
-
* { box-sizing: border-box; }
|
|
40
|
-
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 ${tokenOrDefault(tokens, ["typography", "fontFamily"], "Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif")}; }
|
|
41
|
-
a { color: inherit; }
|
|
42
|
-
.prototype-shell { width: min(1240px, calc(100vw - 24px)); margin: 0 auto; padding: 24px 0 48px; }
|
|
43
|
-
.topbar, .panel, .hero, .launcher-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); }
|
|
44
|
-
.topbar { position: sticky; top: 0; z-index: 4; padding: 14px; margin-bottom: 18px; }
|
|
45
|
-
.topline, .screen-nav, .button-row, .chip-row, .page-links { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
|
46
|
-
.topline { justify-content: space-between; }
|
|
47
|
-
.screen-nav { margin-top: 10px; overflow: auto; }
|
|
48
|
-
.screen-nav a, .page-links a, .chip { text-decoration: none; border: 1px solid var(--border); border-radius: 999px; padding: 6px 10px; background: color-mix(in srgb, var(--surface) 94%, var(--bg)); font-size: 12px; font-weight: 750; }
|
|
49
|
-
.meta, .support { color: var(--muted); }
|
|
50
|
-
.eyebrow { margin: 0 0 6px; color: var(--primary); font-size: 12px; font-weight: 850; text-transform: uppercase; letter-spacing: 0; }
|
|
51
|
-
h1, h2, h3, h4, p { margin-top: 0; }
|
|
52
|
-
h1 { font-size: clamp(34px, 6vw, 64px); line-height: 1.04; margin-bottom: 16px; }
|
|
53
|
-
h2 { font-size: clamp(26px, 4vw, 40px); line-height: 1.12; margin-bottom: 12px; }
|
|
54
|
-
h3 { font-size: 18px; margin-bottom: 8px; }
|
|
55
|
-
.launcher-grid, .feature-grid, .product-grid, .metric-grid { display: grid; gap: 14px; }
|
|
56
|
-
.launcher-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|
57
|
-
.feature-grid, .product-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
58
|
-
.metric-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
59
|
-
.screen-layout, .split-layout { display: grid; grid-template-columns: minmax(240px, 0.36fr) minmax(0, 1fr); gap: 14px; align-items: start; }
|
|
60
|
-
.panel, .hero, .launcher-card { padding: 16px; }
|
|
61
|
-
.btn { display: inline-flex; min-height: 42px; align-items: center; justify-content: center; border-radius: 8px; padding: 0 16px; text-decoration: none; font-weight: 850; border: 1px solid transparent; }
|
|
62
|
-
.btn-primary { background: var(--primary); color: #fff; }
|
|
63
|
-
.btn-secondary { background: transparent; border-color: var(--border); color: var(--text); }
|
|
64
|
-
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
65
|
-
.table th, .table td { border-bottom: 1px solid var(--border); text-align: left; padding: 8px 6px; }
|
|
66
|
-
.form-grid { display: grid; gap: 12px; }
|
|
67
|
-
label { display: grid; gap: 6px; font-weight: 750; }
|
|
68
|
-
input, select, textarea { width: 100%; min-height: 40px; border: 1px solid var(--border); border-radius: 8px; padding: 0 12px; background: #fff; color: var(--text); }
|
|
69
|
-
textarea { padding-top: 10px; min-height: 80px; }
|
|
70
|
-
.qty-stepper { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: 8px; padding: 4px 8px; }
|
|
71
|
-
.checkout-stepper { display: flex; gap: 10px; list-style: none; padding: 0; margin: 0 0 14px; flex-wrap: wrap; }
|
|
72
|
-
.checkout-stepper li, .stepper-step { border: 1px solid var(--border); border-radius: 999px; padding: 5px 11px; font-size: 12px; font-weight: 800; background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
|
|
73
|
-
.timeline { margin: 0; padding-left: 18px; }
|
|
74
|
-
.state-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin: 14px 0; }
|
|
75
|
-
.state-card, .product-card, .feature-card, .category-card, .history-row, .result-row { border: 1px solid var(--border); border-radius: 8px; padding: 12px; background: color-mix(in srgb, var(--surface) 96%, var(--bg)); }
|
|
76
|
-
.density-evidence { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-top: 14px; }
|
|
77
|
-
.density-evidence article { border: 1px dashed var(--border); border-radius: 8px; padding: 10px; color: var(--muted); background: color-mix(in srgb, var(--surface) 94%, var(--bg)); }
|
|
78
|
-
@media (max-width: 920px) {
|
|
79
|
-
.screen-layout, .split-layout, .feature-grid, .product-grid, .metric-grid { grid-template-columns: 1fr; }
|
|
80
|
-
h1 { font-size: 36px; }
|
|
81
|
-
}
|
|
82
|
-
`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function staticJs() {
|
|
86
|
-
return `"use strict";
|
|
87
|
-
document.addEventListener("click", (event) => {
|
|
88
|
-
const target = event.target.closest("[data-static-action]");
|
|
89
|
-
if (!target) return;
|
|
90
|
-
event.preventDefault();
|
|
91
|
-
target.setAttribute("data-triggered", "true");
|
|
92
|
-
});
|
|
93
|
-
`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function stateStrip(states) {
|
|
97
|
-
const values = Array.isArray(states) && states.length > 0 ? states : ["loading", "empty", "success", "error"];
|
|
98
|
-
return `<section class="state-strip" aria-label="State coverage"><div class="chip-row">${values
|
|
99
|
-
.map((state) => `<span class="chip" data-chip-type="state">${escapeHtml(state)}</span>`)
|
|
100
|
-
.join("")}</div>${values
|
|
101
|
-
.map((state) => `<article class="state-card ${escapeHtml(state)}-state"><strong>${escapeHtml(state)}</strong><p>Static prototype guidance for the ${escapeHtml(state)} state.</p></article>`)
|
|
102
|
-
.join("")}</section>`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function evidenceGrid(screen, role, count) {
|
|
106
|
-
return `<section class="density-evidence" aria-label="Renderer evidence">${Array.from({ length: count }, (_, index) => {
|
|
107
|
-
const slot = index + 1;
|
|
108
|
-
return `<article><strong>${escapeHtml(screen.title)} evidence ${slot}</strong><p>${escapeHtml(role)} layout contract covers visible data slots, control states, source-backed acceptance criteria, responsive grouping, and implementation-safe static interaction guidance.</p></article>`;
|
|
109
|
-
}).join("")}</section>`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function renderHome(screen) {
|
|
113
|
-
return `<section class="hero"><p class="eyebrow">Hero</p><h1>${escapeHtml(screen.title)} hero</h1><p class="support">Category-first commerce landing with featured products, clear entry points, and guided configurator access.</p><div class="button-row"><a class="btn btn-primary" href="#" data-static-action>Start purchase</a><a class="btn btn-secondary" href="#" data-static-action>Open configurator</a></div></section>
|
|
114
|
-
<section class="panel" aria-label="Featured products"><h2>Featured products</h2><div class="feature-grid">${["Category range", "Featured products", "Configurator quick start", "Order support"].map((label) => `<article class="feature-card category-card"><h3>${label}</h3><p>Structured entry point with summary, state, and action guidance.</p></article>`).join("")}</div></section>
|
|
115
|
-
${stateStrip(screen.requiredStates)}
|
|
116
|
-
${evidenceGrid(screen, "home", 18)}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function productCards(count) {
|
|
120
|
-
return Array.from({ length: count }, (_, index) => `<article class="product-card"><h3>Product card ${index + 1}</h3><p>SKU, availability, comparison attribute, price tier, and primary selection control.</p><div class="button-row"><a class="btn btn-secondary" href="#" data-static-action>Compare</a><a class="btn btn-primary" href="#" data-static-action>Select</a></div></article>`).join("");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function renderCategory(screen) {
|
|
124
|
-
return `<section class="screen-layout"><aside class="panel"><h3>Filter sidebar</h3>${["Category", "Availability", "Material", "Size", "Price", "Delivery"].map((label) => `<label>${label}<select><option>${label} option</option></select></label>`).join("")}</aside><div class="panel"><h3>Product grid</h3><div class="product-grid">${productCards(8)}</div></div></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "category", 10)}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function renderProductDetail(screen) {
|
|
128
|
-
return `<section class="split-layout"><div class="panel"><h3>Gallery and spec area</h3><table class="table spec-table"><tbody>${["Voltage", "Length", "Material", "Finish", "Certification"].map((label, index) => `<tr><th>${label}</th><td>Source-backed specification value ${index + 1}</td></tr>`).join("")}</tbody></table></div><aside class="panel"><h3>Price and quantity</h3><div class="qty-stepper"><button>-</button><span>1</span><button>+</button></div><div class="button-row"><a class="btn btn-primary" href="#" data-static-action>Add to cart</a></div></aside></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "product-detail", 10)}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function renderSearch(screen) {
|
|
132
|
-
return `<section class="panel"><h3>Search toolbar</h3><div class="search-toolbar"><input aria-label="Search query" placeholder="Search product"><select><option>Sort by relevance</option></select><button class="btn btn-primary">Search</button></div><div class="result-list">${Array.from({ length: 5 }, (_, index) => `<article class="result-row"><h4>Result item ${index + 1}</h4><p>Matched term, category, availability, and action.</p></article>`).join("")}<article class="result-row no-result"><h4>No-result state</h4><p>Suggest alternate query, category reset, and support route.</p></article></div></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "search", 7)}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function renderCart(screen) {
|
|
136
|
-
return `<section class="split-layout"><div class="panel"><h3>Cart table</h3><table class="table cart-table"><thead><tr><th>Item</th><th>Qty</th><th>Unit</th><th>Total</th><th>Action</th></tr></thead><tbody>${Array.from({ length: 4 }, (_, index) => `<tr><td>Line item ${index + 1}</td><td><div class="qty-stepper"><button>-</button><span>${index + 1}</span><button>+</button></div></td><td>1000</td><td>${(index + 1) * 1000}</td><td><button>Remove</button></td></tr>`).join("")}</tbody></table></div><aside class="panel summary-panel"><h3>Summary panel</h3><p>Subtotal, tax, freight, and checkout CTA.</p><a class="btn btn-primary" href="#" data-static-action>Checkout</a></aside></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "cart", 8)}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function renderCheckout(screen) {
|
|
140
|
-
return `<section class="split-layout"><div class="panel"><h3>Checkout stepper</h3><ol class="checkout-stepper">${["Information", "Delivery", "Payment", "Review"].map((step) => `<li class="stepper-step">${step}</li>`).join("")}</ol><form class="form-grid">${["Recipient", "Address", "Phone", "Purchase order"].map((label) => `<label>${label}<input type="text" value="${label} value"></label>`).join("")}</form></div><aside class="panel summary-panel"><h3>Summary card</h3><p>Order totals, delivery window, and final confirmation.</p><button class="btn btn-primary">Place order</button></aside></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "checkout", 8)}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function renderOrderHistory(screen) {
|
|
144
|
-
return `<section class="panel"><h3>Order history list</h3><table class="table"><thead><tr><th>Order</th><th>Status</th><th>Total</th><th>Action</th></tr></thead><tbody>${Array.from({ length: 5 }, (_, index) => `<tr class="history-row"><td>#10${index}</td><td>Processing</td><td>${(index + 2) * 1200}</td><td><a href="#" data-static-action>Open detail</a></td></tr>`).join("")}</tbody></table></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "order-history", 7)}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function renderOrderDetail(screen) {
|
|
148
|
-
return `<section class="split-layout"><div class="panel"><h3>Timeline</h3><ol class="timeline">${["Placed", "Approved", "Packed", "Shipping", "Delivered"].map((item) => `<li>${item}</li>`).join("")}</ol></div><aside class="panel detail-summary"><h3>Detail summary</h3><p>Delivery, payment, line item, and support snapshot.</p></aside></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "order-detail", 8)}`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function renderAccountInfo(screen) {
|
|
152
|
-
return `<section class="screen-layout"><aside class="panel"><h3>Account sidebar</h3><nav class="screen-nav"><a href="#">Company</a><a href="#">Users</a><a href="#">Billing</a><a href="#">Security</a></nav></aside><div class="panel"><h3>View or edit form</h3><form class="form-grid">${["Company", "Contact", "Email", "Phone", "Address"].map((label) => `<label>${label}<input type="text" value="${label}"></label>`).join("")}</form></div></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "account-info", 7)}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function renderConfigurator(screen) {
|
|
156
|
-
return `<section class="panel"><h3>Configurator wizard</h3><ol class="checkout-stepper">${["Project", "Assembly", "Construction", "Review", "Complete"].map((step) => `<li class="stepper-step">${step}</li>`).join("")}</ol><div class="split-layout"><div class="panel preview-panel"><h4>Preview panel</h4><p>Derived material preview with recalculation state and validation summary.</p>${stateStrip(screen.requiredStates)}</div><div class="panel"><h4>BOM table</h4><table class="table cart-table"><thead><tr><th>Material</th><th>Spec</th><th>Qty</th><th>Unit</th><th>Action</th></tr></thead><tbody>${Array.from({ length: 8 }, (_, index) => `<tr><td>Material ${index + 1}</td><td>Generated spec ${index + 1}</td><td>${index + 2}</td><td>pcs</td><td><button>Exclude</button></td></tr>`).join("")}</tbody></table><div class="button-row"><a class="btn btn-secondary" href="#" data-static-action>Recalculate</a><a class="btn btn-primary" href="#" data-static-action>Add BOM to cart</a></div></div></div></section>${evidenceGrid(screen, "configurator-bom", 24)}`;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function renderGeneric(screen) {
|
|
160
|
-
return `<section class="panel"><h3>${escapeHtml(screen.title)} workspace</h3><p>Renderer-ready generic screen with explicit sections, components, states, interactions, and source evidence from the screen brief.</p></section>${stateStrip(screen.requiredStates)}${evidenceGrid(screen, "generic", 8)}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function renderRoleContent(screen, role) {
|
|
164
|
-
if (role === "home") return renderHome(screen);
|
|
165
|
-
if (role === "category") return renderCategory(screen);
|
|
166
|
-
if (role === "product-detail") return renderProductDetail(screen);
|
|
167
|
-
if (role === "search") return renderSearch(screen);
|
|
168
|
-
if (role === "cart") return renderCart(screen);
|
|
169
|
-
if (role === "checkout") return renderCheckout(screen);
|
|
170
|
-
if (role === "order-history") return renderOrderHistory(screen);
|
|
171
|
-
if (role === "order-detail") return renderOrderDetail(screen);
|
|
172
|
-
if (role === "account-info") return renderAccountInfo(screen);
|
|
173
|
-
if (role === "configurator-bom" || role === "mode-b-configurator") return renderConfigurator(screen);
|
|
174
|
-
return renderGeneric(screen);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function pageChrome({ title, cssHref, bodyContent, backHref, prev, next, screenId, role }) {
|
|
178
|
-
return `<!doctype html>
|
|
179
|
-
<html lang="en">
|
|
180
|
-
<head>
|
|
181
|
-
<meta charset="utf-8">
|
|
182
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
183
|
-
<title>${escapeHtml(title)} Prototype</title>
|
|
184
|
-
<link rel="stylesheet" href="${cssHref}">
|
|
185
|
-
<script src="../assets/prototype.js" defer></script>
|
|
186
|
-
</head>
|
|
187
|
-
<body data-screen-role="${escapeHtml(role)}">
|
|
188
|
-
<main class="prototype-shell" id="screen-${escapeHtml(screenId)}">
|
|
189
|
-
<header class="topbar">
|
|
190
|
-
<div class="topline">
|
|
191
|
-
<a class="btn btn-secondary" href="${backHref}">Back to index</a>
|
|
192
|
-
<strong>${escapeHtml(title)}</strong>
|
|
193
|
-
</div>
|
|
194
|
-
<nav class="page-links" aria-label="Screen page navigation">
|
|
195
|
-
<a href="${prev.href}" rel="prev">Previous: ${escapeHtml(prev.title)}</a>
|
|
196
|
-
<a href="${next.href}" rel="next">Next: ${escapeHtml(next.title)}</a>
|
|
197
|
-
</nav>
|
|
198
|
-
</header>
|
|
199
|
-
${bodyContent}
|
|
200
|
-
</main>
|
|
201
|
-
</body>
|
|
202
|
-
</html>
|
|
203
|
-
`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function indexHtml({ screens, profile, densityReport, styleName }) {
|
|
207
|
-
return `<!doctype html>
|
|
208
|
-
<html lang="en">
|
|
209
|
-
<head>
|
|
210
|
-
<meta charset="utf-8">
|
|
211
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
212
|
-
<title>SDTK-DESIGN Prototype Launcher</title>
|
|
213
|
-
<link rel="stylesheet" href="assets/prototype.css">
|
|
214
|
-
</head>
|
|
215
|
-
<body>
|
|
216
|
-
<main class="prototype-shell" data-style-preset="${escapeHtml(styleName || "default")}">
|
|
217
|
-
<header class="topbar">
|
|
218
|
-
<div class="topline">
|
|
219
|
-
<strong>SDTK-DESIGN multi-page prototype</strong>
|
|
220
|
-
<span class="meta">screens=${screens.length} | profile=${escapeHtml(profile || "none")} | density=${densityReport.totalBytes}/${densityReport.minTotalBytes} bytes</span>
|
|
221
|
-
</div>
|
|
222
|
-
<nav class="screen-nav" aria-label="Screen navigation">
|
|
223
|
-
${screens.map((screen) => `<a href="screens/${screenFileName(screen)}">${escapeHtml(screen.title)}</a>`).join("")}
|
|
224
|
-
</nav>
|
|
225
|
-
</header>
|
|
226
|
-
<section class="launcher-grid" aria-label="Prototype pages">
|
|
227
|
-
${screens.map((screen) => `<article class="launcher-card"><p class="eyebrow">${escapeHtml(normalizeScreenRole(screen).replace(/-/g, " "))}</p><h2>${escapeHtml(screen.title)}</h2><p class="support">Route: ${escapeHtml(screen.route || "NEEDS_ROUTE")}</p><a class="btn btn-primary" href="screens/${screenFileName(screen)}">Open screen</a></article>`).join("")}
|
|
228
|
-
</section>
|
|
229
|
-
</main>
|
|
230
|
-
</body>
|
|
231
|
-
</html>
|
|
232
|
-
`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function renderScreenHtml({ screen, role, previousScreen, nextScreen }) {
|
|
236
|
-
const content = renderRoleContent(screen, role);
|
|
237
|
-
const initial = pageChrome({
|
|
238
|
-
title: screen.title,
|
|
239
|
-
cssHref: "../assets/prototype.css",
|
|
240
|
-
backHref: "../index.html",
|
|
241
|
-
prev: { title: previousScreen.title, href: screenFileName(previousScreen) },
|
|
242
|
-
next: { title: nextScreen.title, href: screenFileName(nextScreen) },
|
|
243
|
-
screenId: screen.screenId,
|
|
244
|
-
role,
|
|
245
|
-
bodyContent: `<section class="hero"><p class="eyebrow">${escapeHtml(role.replace(/-/g, " "))}</p><h1>${escapeHtml(screen.title)}</h1><p class="support">${escapeHtml(screen.purpose || screen.userIntent || "Renderer-ready screen generated from explicit design brief.")}</p></section>${content}`,
|
|
246
|
-
});
|
|
247
|
-
return ensureDensityTarget(initial, screen, role);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function ensureDensityTarget(html, screen, role) {
|
|
251
|
-
const floor = ROLE_FLOORS[role] || { minBytes: 0 };
|
|
252
|
-
const targetBytes = floor.minBytes > 0 ? floor.minBytes + 2300 : Buffer.byteLength(html, "utf8");
|
|
253
|
-
let result = html;
|
|
254
|
-
let index = 1;
|
|
255
|
-
while (Buffer.byteLength(result, "utf8") < targetBytes && index <= 80) {
|
|
256
|
-
const block = `<section class="density-evidence" aria-label="Additional screen detail ${index}"><article><strong>${escapeHtml(screen.title)} implementation detail ${index}</strong><p>${escapeHtml(role)} page includes source-backed layout notes, component state behavior, responsive grouping, data slot mapping, accessibility guidance, and static interaction acceptance evidence for renderer and QA review.</p></article><article><strong>${escapeHtml(screen.title)} QA checkpoint ${index}</strong><p>Generated content remains local static prototype output under docs/design/prototype and does not depend on sample files, network calls, app runtime code, or external UI references.</p></article></section>`;
|
|
257
|
-
result = result.replace("</main>", `${block}</main>`);
|
|
258
|
-
index += 1;
|
|
259
|
-
}
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function renderMultiPagePrototype({ paths, statePayload, contractBundle, styleName }) {
|
|
264
|
-
const screens = statePayload.screenModel.screens;
|
|
265
|
-
const tokens = contractBundle.tokens || {};
|
|
266
|
-
fs.mkdirSync(paths.prototypePath, { recursive: true });
|
|
267
|
-
fs.mkdirSync(paths.prototypeScreensPath, { recursive: true });
|
|
268
|
-
fs.mkdirSync(paths.prototypeAssetsPath, { recursive: true });
|
|
269
|
-
|
|
270
|
-
fs.writeFileSync(paths.prototypeCssPath, sharedCss(tokens), "utf-8");
|
|
271
|
-
fs.writeFileSync(paths.prototypeJsPath, staticJs(), "utf-8");
|
|
272
|
-
|
|
273
|
-
const pageRecords = screens.map((screen, index) => {
|
|
274
|
-
const previousScreen = screens[(index - 1 + screens.length) % screens.length];
|
|
275
|
-
const nextScreen = screens[(index + 1) % screens.length];
|
|
276
|
-
const role = normalizeScreenRole({
|
|
277
|
-
...screen,
|
|
278
|
-
template_role:
|
|
279
|
-
contractBundle.screenContracts &&
|
|
280
|
-
contractBundle.screenContracts.get(screen.screenId) &&
|
|
281
|
-
contractBundle.screenContracts.get(screen.screenId).sidecar
|
|
282
|
-
? contractBundle.screenContracts.get(screen.screenId).sidecar.template_role
|
|
283
|
-
: null,
|
|
284
|
-
});
|
|
285
|
-
const html = renderScreenHtml({ screen, role, previousScreen, nextScreen });
|
|
286
|
-
return {
|
|
287
|
-
screenId: screen.screenId,
|
|
288
|
-
title: screen.title,
|
|
289
|
-
role,
|
|
290
|
-
relativePath: screenRelativePath(screen),
|
|
291
|
-
filePath: path.join(paths.prototypeScreensPath, screenFileName(screen)),
|
|
292
|
-
html,
|
|
293
|
-
};
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
const densityReport = assessPrototypeDensity(pageRecords);
|
|
297
|
-
for (const page of pageRecords) {
|
|
298
|
-
fs.writeFileSync(page.filePath, page.html, "utf-8");
|
|
299
|
-
}
|
|
300
|
-
fs.writeFileSync(
|
|
301
|
-
paths.prototypeIndexPath,
|
|
302
|
-
indexHtml({ screens, profile: statePayload.profileSelection || "generic", densityReport, styleName }),
|
|
303
|
-
"utf-8"
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
return {
|
|
307
|
-
mode: "high-fidelity-multi-page",
|
|
308
|
-
indexRelativePath: "docs/design/prototype/index.html",
|
|
309
|
-
assetRelativePaths: ["docs/design/prototype/assets/prototype.css", "docs/design/prototype/assets/prototype.js"],
|
|
310
|
-
screenPages: pageRecords.map((page) => ({
|
|
311
|
-
screenId: page.screenId,
|
|
312
|
-
title: page.title,
|
|
313
|
-
role: page.role,
|
|
314
|
-
relativePath: page.relativePath,
|
|
315
|
-
byteLength: Buffer.byteLength(page.html, "utf8"),
|
|
316
|
-
})),
|
|
317
|
-
densityReport,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
module.exports = {
|
|
322
|
-
renderMultiPagePrototype,
|
|
323
|
-
screenFileName,
|
|
324
|
-
screenRelativePath,
|
|
325
|
-
};
|