sdtk-design-kit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/prototype.js +20 -6
- package/src/commands/review.js +68 -1
- package/src/lib/anti-slop-lint.js +210 -0
- package/src/lib/prototype-briefs.js +125 -0
- package/src/lib/prototype-component-map.js +219 -0
- package/src/lib/prototype-density.js +286 -56
- package/src/lib/prototype-renderer.js +101 -44
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const {
|
|
5
|
+
const { briefForScreen, loadPrototypeBriefIndex } = require("./prototype-briefs");
|
|
6
|
+
const { componentClassFor, renderBriefComponent, renderDataSlotPlaceholder } = require("./prototype-component-map");
|
|
7
|
+
const { assessPrototypeDensity, normalizeScreenRole } = require("./prototype-density");
|
|
6
8
|
|
|
7
9
|
function escapeHtml(value) {
|
|
8
10
|
return String(value == null ? "" : value)
|
|
@@ -73,8 +75,6 @@ textarea { padding-top: 10px; min-height: 80px; }
|
|
|
73
75
|
.timeline { margin: 0; padding-left: 18px; }
|
|
74
76
|
.state-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; margin: 14px 0; }
|
|
75
77
|
.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
78
|
@media (max-width: 920px) {
|
|
79
79
|
.screen-layout, .split-layout, .feature-grid, .product-grid, .metric-grid { grid-template-columns: 1fr; }
|
|
80
80
|
h1 { font-size: 36px; }
|
|
@@ -96,24 +96,79 @@ document.addEventListener("click", (event) => {
|
|
|
96
96
|
function stateStrip(states) {
|
|
97
97
|
const values = Array.isArray(states) && states.length > 0 ? states : ["loading", "empty", "success", "error"];
|
|
98
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>`)
|
|
99
|
+
.map((state) => `<span class="chip" data-chip-type="state" data-state-id="${escapeHtml(state)}">${escapeHtml(state)}</span>`)
|
|
100
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>`)
|
|
101
|
+
.map((state) => `<article class="state-card ${escapeHtml(state)}-state" data-state-id="${escapeHtml(state)}"><strong>${escapeHtml(state)}</strong><p>Static prototype guidance for the ${escapeHtml(state)} state.</p></article>`)
|
|
102
102
|
.join("")}</section>`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
105
|
+
function slotLookupForBrief(brief) {
|
|
106
|
+
const lookup = new Map();
|
|
107
|
+
for (const slot of Array.isArray(brief && brief.data_slots) ? brief.data_slots : []) {
|
|
108
|
+
lookup.set(slot.id, slot);
|
|
109
|
+
}
|
|
110
|
+
return lookup;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderNeedsBrief(screen, role) {
|
|
114
|
+
const slots = [
|
|
115
|
+
{ id: "brief-status", label: "Brief status" },
|
|
116
|
+
{ id: "screen-contract", label: "Screen contract" },
|
|
117
|
+
{ id: "source-evidence", label: "Source evidence" },
|
|
118
|
+
{ id: "primary-record", label: "Primary record" },
|
|
119
|
+
{ id: "next-action", label: "Next action" },
|
|
120
|
+
];
|
|
121
|
+
const section = {
|
|
122
|
+
id: "needs-brief-actions",
|
|
123
|
+
title: "Brief recovery actions",
|
|
124
|
+
purpose: "Prototype generation continues with visible review evidence instead of silently falling back to role-default filler.",
|
|
125
|
+
data_slots: slots,
|
|
126
|
+
interactions: ["Review brief", "Regenerate brief", "Continue prototype review"],
|
|
127
|
+
};
|
|
128
|
+
const brief = { screen_title: screen.title, primary_action: "Review brief" };
|
|
129
|
+
return `<section class="panel needs-brief" data-section-id="needs-brief-summary" data-section-role="${escapeHtml(role)}"><p class="eyebrow">NEEDS_BRIEF</p><h2>NEEDS_BRIEF</h2><p class="support">${escapeHtml(screen.title)} requires .sdtk/design/screen-briefs/${escapeHtml(screen.screenId)}.json before high-fidelity composition.</p><div class="chip-row">${slots.map((slot) => renderDataSlotPlaceholder({ slot, section, escapeHtml })).join("")}</div></section><section class="panel" data-section-id="needs-brief-actions" data-section-role="${escapeHtml(role)}"><h2>Brief recovery actions</h2>${renderBriefComponent({ componentId: { id: "primary-panel", label: "Primary panel" }, section, brief, role, slotLookup: new Map(slots.map((slot) => [slot.id, slot])), escapeHtml })}${stateStrip(screen.requiredStates)}</section>`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderBriefSlotSummary(brief) {
|
|
133
|
+
const slots = Array.isArray(brief.data_slots) ? brief.data_slots : [];
|
|
134
|
+
if (slots.length === 0) return "";
|
|
135
|
+
return `<div class="panel" data-component-id="screen-data-slots"><h3>Data slots</h3><div class="chip-row">${slots
|
|
136
|
+
.map((slot) => renderDataSlotPlaceholder({ slot, section: { id: "screen-data-slots" }, escapeHtml }))
|
|
137
|
+
.join("")}</div></div>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sectionHasHeroComponent(section) {
|
|
141
|
+
const components = Array.isArray(section && section.components) ? section.components : [];
|
|
142
|
+
return components.some((component) => componentClassFor(component && component.id ? component.id : String(component || "component")) === "hero-section");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function briefHasHeroComponent(brief) {
|
|
146
|
+
return Array.isArray(brief && brief.sections) && brief.sections.some((section) => sectionHasHeroComponent(section));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderBriefSection({ section, brief, role, slotLookup, headingTag = "h2" }) {
|
|
150
|
+
const components = Array.isArray(section.components) && section.components.length > 0 ? section.components : [{ id: "component-placeholder", label: section.title }];
|
|
151
|
+
return `<section class="panel brief-section" data-section-id="${escapeHtml(section.id)}" data-section-role="${escapeHtml(role)}"><p class="eyebrow">${escapeHtml(role.replace(/-/g, " "))}</p><${headingTag}>${escapeHtml(section.title)}</${headingTag}><p class="support">${escapeHtml(section.purpose)}</p><div class="brief-component-stack">${components
|
|
152
|
+
.map((component) => renderBriefComponent({ componentId: component, section, brief, role, slotLookup, escapeHtml }))
|
|
153
|
+
.join("")}</div></section>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderBriefContent({ screen, role, brief }) {
|
|
157
|
+
if (!brief || !Array.isArray(brief.sections) || brief.sections.length === 0) {
|
|
158
|
+
return renderNeedsBrief(screen, role);
|
|
159
|
+
}
|
|
160
|
+
const slotLookup = slotLookupForBrief(brief);
|
|
161
|
+
const hasHeroComponent = briefHasHeroComponent(brief);
|
|
162
|
+
const sections = brief.sections
|
|
163
|
+
.map((section, index) => renderBriefSection({ section, brief, role, slotLookup, headingTag: !hasHeroComponent && index === 0 ? "h1" : "h2" }))
|
|
164
|
+
.join("");
|
|
165
|
+
return `${sections}${renderBriefSlotSummary(brief)}${stateStrip(brief.states.length > 0 ? brief.states : screen.requiredStates)}`;
|
|
110
166
|
}
|
|
111
167
|
|
|
112
168
|
function renderHome(screen) {
|
|
113
169
|
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
170
|
<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)}`;
|
|
171
|
+
${stateStrip(screen.requiredStates)}`;
|
|
117
172
|
}
|
|
118
173
|
|
|
119
174
|
function productCards(count) {
|
|
@@ -121,46 +176,51 @@ function productCards(count) {
|
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
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)}
|
|
179
|
+
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)}`;
|
|
125
180
|
}
|
|
126
181
|
|
|
127
182
|
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)}
|
|
183
|
+
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)}`;
|
|
129
184
|
}
|
|
130
185
|
|
|
131
186
|
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)}
|
|
187
|
+
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)}`;
|
|
133
188
|
}
|
|
134
189
|
|
|
135
190
|
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)}
|
|
191
|
+
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)}`;
|
|
137
192
|
}
|
|
138
193
|
|
|
139
194
|
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)}
|
|
195
|
+
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)}`;
|
|
141
196
|
}
|
|
142
197
|
|
|
143
198
|
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)}
|
|
199
|
+
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)}`;
|
|
145
200
|
}
|
|
146
201
|
|
|
147
202
|
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)}
|
|
203
|
+
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)}`;
|
|
149
204
|
}
|
|
150
205
|
|
|
151
206
|
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)}
|
|
207
|
+
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)}`;
|
|
153
208
|
}
|
|
154
209
|
|
|
155
210
|
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
|
|
211
|
+
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>`;
|
|
157
212
|
}
|
|
158
213
|
|
|
159
214
|
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)}
|
|
215
|
+
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)}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderRoleContent({ screen, role, brief }) {
|
|
219
|
+
if (brief) return renderBriefContent({ screen, role, brief });
|
|
220
|
+
return renderNeedsBrief(screen, role);
|
|
161
221
|
}
|
|
162
222
|
|
|
163
|
-
function
|
|
223
|
+
function renderLegacyRoleContent(screen, role) {
|
|
164
224
|
if (role === "home") return renderHome(screen);
|
|
165
225
|
if (role === "category") return renderCategory(screen);
|
|
166
226
|
if (role === "product-detail") return renderProductDetail(screen);
|
|
@@ -217,7 +277,7 @@ function indexHtml({ screens, profile, densityReport, styleName }) {
|
|
|
217
277
|
<header class="topbar">
|
|
218
278
|
<div class="topline">
|
|
219
279
|
<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>
|
|
280
|
+
<span class="meta">screens=${screens.length} | profile=${escapeHtml(profile || "none")} | density=${densityReport.mode === "semantic" ? `${densityReport.aggregate.sectionCount} sections / ${densityReport.aggregate.componentCount} components` : `${densityReport.totalBytes}/${densityReport.minTotalBytes} bytes`}</span>
|
|
221
281
|
</div>
|
|
222
282
|
<nav class="screen-nav" aria-label="Screen navigation">
|
|
223
283
|
${screens.map((screen) => `<a href="screens/${screenFileName(screen)}">${escapeHtml(screen.title)}</a>`).join("")}
|
|
@@ -232,9 +292,11 @@ function indexHtml({ screens, profile, densityReport, styleName }) {
|
|
|
232
292
|
`;
|
|
233
293
|
}
|
|
234
294
|
|
|
235
|
-
function renderScreenHtml({ screen, role, previousScreen, nextScreen }) {
|
|
236
|
-
const content = renderRoleContent(screen, role);
|
|
237
|
-
const
|
|
295
|
+
function renderScreenHtml({ screen, role, brief, previousScreen, nextScreen }) {
|
|
296
|
+
const content = renderRoleContent({ screen, role, brief });
|
|
297
|
+
const hasBriefSections = Boolean(brief && Array.isArray(brief.sections) && brief.sections.length > 0);
|
|
298
|
+
const heroHeadingTag = hasBriefSections ? "h2" : "h1";
|
|
299
|
+
return pageChrome({
|
|
238
300
|
title: screen.title,
|
|
239
301
|
cssHref: "../assets/prototype.css",
|
|
240
302
|
backHref: "../index.html",
|
|
@@ -242,27 +304,14 @@ function renderScreenHtml({ screen, role, previousScreen, nextScreen }) {
|
|
|
242
304
|
next: { title: nextScreen.title, href: screenFileName(nextScreen) },
|
|
243
305
|
screenId: screen.screenId,
|
|
244
306
|
role,
|
|
245
|
-
bodyContent: `<section class="hero"><p class="eyebrow">${escapeHtml(role.replace(/-/g, " "))}</p
|
|
307
|
+
bodyContent: `<section class="hero" data-component-id="hero"><p class="eyebrow">${escapeHtml(role.replace(/-/g, " "))}</p><${heroHeadingTag}>${escapeHtml(screen.title)}</${heroHeadingTag}><p class="support">${escapeHtml(screen.purpose || screen.userIntent || "Renderer-ready screen generated from explicit design brief.")}</p></section>${content}`,
|
|
246
308
|
});
|
|
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
309
|
}
|
|
262
310
|
|
|
263
|
-
function renderMultiPagePrototype({ paths, statePayload, contractBundle, styleName }) {
|
|
311
|
+
function renderMultiPagePrototype({ paths, statePayload, contractBundle, styleName, briefIndex }) {
|
|
264
312
|
const screens = statePayload.screenModel.screens;
|
|
265
313
|
const tokens = contractBundle.tokens || {};
|
|
314
|
+
const resolvedBriefIndex = briefIndex || loadPrototypeBriefIndex({ paths });
|
|
266
315
|
fs.mkdirSync(paths.prototypePath, { recursive: true });
|
|
267
316
|
fs.mkdirSync(paths.prototypeScreensPath, { recursive: true });
|
|
268
317
|
fs.mkdirSync(paths.prototypeAssetsPath, { recursive: true });
|
|
@@ -282,11 +331,17 @@ function renderMultiPagePrototype({ paths, statePayload, contractBundle, styleNa
|
|
|
282
331
|
? contractBundle.screenContracts.get(screen.screenId).sidecar.template_role
|
|
283
332
|
: null,
|
|
284
333
|
});
|
|
285
|
-
const
|
|
334
|
+
const screenContract =
|
|
335
|
+
contractBundle.screenContracts && contractBundle.screenContracts.get(screen.screenId)
|
|
336
|
+
? contractBundle.screenContracts.get(screen.screenId)
|
|
337
|
+
: null;
|
|
338
|
+
const brief = briefForScreen({ screen, briefIndex: resolvedBriefIndex }) || (screenContract && screenContract.sidecar ? screenContract.sidecar : null);
|
|
339
|
+
const html = renderScreenHtml({ screen, role, brief, previousScreen, nextScreen });
|
|
286
340
|
return {
|
|
287
341
|
screenId: screen.screenId,
|
|
288
342
|
title: screen.title,
|
|
289
343
|
role,
|
|
344
|
+
brief,
|
|
290
345
|
relativePath: screenRelativePath(screen),
|
|
291
346
|
filePath: path.join(paths.prototypeScreensPath, screenFileName(screen)),
|
|
292
347
|
html,
|
|
@@ -315,11 +370,13 @@ function renderMultiPagePrototype({ paths, statePayload, contractBundle, styleNa
|
|
|
315
370
|
byteLength: Buffer.byteLength(page.html, "utf8"),
|
|
316
371
|
})),
|
|
317
372
|
densityReport,
|
|
373
|
+
briefFindings: resolvedBriefIndex.findings || [],
|
|
318
374
|
};
|
|
319
375
|
}
|
|
320
376
|
|
|
321
377
|
module.exports = {
|
|
322
378
|
renderMultiPagePrototype,
|
|
379
|
+
renderLegacyRoleContent,
|
|
323
380
|
screenFileName,
|
|
324
381
|
screenRelativePath,
|
|
325
382
|
};
|