sdtk-design-kit 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -125
- package/package.json +2 -1
- package/skills/design-prototype/SKILL.md +276 -0
- package/skills/design-prototype/references/craft.md +75 -0
- package/skills/design-prototype/references/designer-charter.md +56 -0
- package/src/commands/handoff.js +220 -220
- package/src/commands/help.js +7 -3
- package/src/commands/init.js +53 -0
- package/src/commands/prototype.js +140 -653
- package/src/commands/review.js +129 -139
- package/src/commands/start.js +22 -5
- package/src/commands/status.js +10 -2
- package/src/commands/system.js +186 -14
- package/src/commands/update.js +11 -0
- package/src/index.js +4 -0
- package/src/lib/anti-slop-lint.js +0 -11
- package/src/lib/component-contract.js +300 -34
- package/src/lib/design-input-contract.js +3 -2
- package/src/lib/design-paths.js +3 -0
- package/src/lib/design-profiles.js +31 -0
- package/src/lib/screen-briefs.js +235 -24
- package/src/lib/update.js +219 -0
- package/src/lib/prototype-briefs.js +0 -125
- package/src/lib/prototype-component-map.js +0 -219
- package/src/lib/prototype-density.js +0 -377
- package/src/lib/prototype-renderer.js +0 -382
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const ROLE_CTA_DEFAULTS = {
|
|
4
|
-
home: "Start shopping",
|
|
5
|
-
category: "Browse products",
|
|
6
|
-
"product-detail": "Add to cart",
|
|
7
|
-
cart: "Checkout",
|
|
8
|
-
checkout: "Place order",
|
|
9
|
-
search: "Search",
|
|
10
|
-
"order-history": "View orders",
|
|
11
|
-
"order-detail": "View detail",
|
|
12
|
-
"account-info": "Save changes",
|
|
13
|
-
"configurator-bom": "Open configurator",
|
|
14
|
-
"mode-b-configurator": "Open configurator",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function truncatePrimaryAction(value, role) {
|
|
18
|
-
const text = String(value || "").trim();
|
|
19
|
-
if (text.length > 0 && text.length <= 60) return text;
|
|
20
|
-
return ROLE_CTA_DEFAULTS[role] || "Continue";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function classForComponent(componentId) {
|
|
24
|
-
const id = String(componentId || "").toLowerCase();
|
|
25
|
-
if (id === "configurator-cta" || id.includes("configurator-cta")) return "btn-primary";
|
|
26
|
-
if (id.includes("cta-secondary")) return "btn-secondary";
|
|
27
|
-
if (id.includes("cta-primary")) return "btn-primary";
|
|
28
|
-
if (id.includes("hero")) return "hero-section";
|
|
29
|
-
if (id.includes("feature")) return "feature-card";
|
|
30
|
-
if (id.includes("category")) return "category-card";
|
|
31
|
-
if (id.includes("product")) return "product-card";
|
|
32
|
-
if (id.includes("cart-table")) return "cart-table";
|
|
33
|
-
if (id.includes("bom-table")) return "cart-table bom-table";
|
|
34
|
-
if (id.includes("spec-table")) return "spec-table";
|
|
35
|
-
if (id.includes("qty-stepper") || id.includes("quantity")) return "qty-stepper";
|
|
36
|
-
if (id.includes("checkout-stepper") || id.includes("stepper") || id.includes("wizard")) return "checkout-stepper";
|
|
37
|
-
if (id.includes("summary")) return "summary-panel";
|
|
38
|
-
if (id.includes("preview")) return "preview-panel";
|
|
39
|
-
if (id.includes("detail")) return "detail-summary";
|
|
40
|
-
if (id.includes("filter") || id.includes("sidebar")) return "filter-sidebar account-sidebar";
|
|
41
|
-
if (id.includes("search-toolbar")) return "search-toolbar";
|
|
42
|
-
if (id.includes("result-list")) return "result-list";
|
|
43
|
-
if (id.includes("no-result")) return "no-result";
|
|
44
|
-
if (id.includes("history")) return "history-row";
|
|
45
|
-
if (id.includes("timeline")) return "timeline";
|
|
46
|
-
if (id.includes("form")) return "form-grid";
|
|
47
|
-
return "component-placeholder";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function componentClassFor(componentId) {
|
|
51
|
-
return classForComponent(componentId);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function renderDataSlotPlaceholder({ slot, section, escapeHtml }) {
|
|
55
|
-
const id = slot && slot.id ? slot.id : "slot";
|
|
56
|
-
const label = slot && slot.label ? slot.label : id;
|
|
57
|
-
return `<span class="data-slot-placeholder" data-slot-id="${escapeHtml(id)}" data-slot-label="${escapeHtml(label)}">${escapeHtml(label)} --</span>`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function renderSlotList({ section, escapeHtml }) {
|
|
61
|
-
const slots = Array.isArray(section && section.data_slots) ? section.data_slots : [];
|
|
62
|
-
if (slots.length === 0) return "";
|
|
63
|
-
return `<div class="chip-row data-slot-row">${slots.map((slot) => renderDataSlotPlaceholder({ slot, section, escapeHtml })).join("")}</div>`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function structuralSlots(slots, labels) {
|
|
67
|
-
const values = Array.isArray(slots) && slots.length > 0 ? slots.slice() : [];
|
|
68
|
-
for (const label of labels) {
|
|
69
|
-
if (values.length >= labels.length) break;
|
|
70
|
-
values.push({ id: label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""), label });
|
|
71
|
-
}
|
|
72
|
-
return values;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function renderRolePrimary({ role, section, brief, traceAttrs, slots, escapeHtml }) {
|
|
76
|
-
if (role === "home") {
|
|
77
|
-
const cards = structuralSlots(slots, ["Category range", "Featured selection", "Configurator quick start"]);
|
|
78
|
-
return `<div class="feature-grid product-grid" ${traceAttrs}><h3>Featured products</h3><p class="support">Configurator quick start</p>${cards
|
|
79
|
-
.slice(0, 3)
|
|
80
|
-
.map((slot) => `<article class="feature-card category-card product-card" data-component-id="feature-card" data-slot-id="${escapeHtml(slot.id)}"><h3>${escapeHtml(slot.label)}</h3><p>${escapeHtml(section.purpose)}</p></article>`)
|
|
81
|
-
.join("")}<div class="button-row" data-component-id="cta-pair"><a class="btn btn-primary" href="#" data-static-action data-component-id="cta-primary">${escapeHtml(truncatePrimaryAction(brief.primary_action, role))}</a><a class="btn btn-secondary" href="#" data-static-action data-component-id="cta-secondary">Review options</a></div></div>`;
|
|
82
|
-
}
|
|
83
|
-
if (role === "category") {
|
|
84
|
-
const cards = structuralSlots(slots, ["Category option", "Availability option", "Material option", "Size option", "Price option", "Delivery option"]);
|
|
85
|
-
return `<div class="screen-layout" ${traceAttrs}><aside class="panel filter-sidebar" data-component-id="filter-sidebar"><h3>Filter sidebar</h3>${cards
|
|
86
|
-
.slice(0, 6)
|
|
87
|
-
.map((slot) => `<label>${escapeHtml(slot.label)}<select data-slot-id="${escapeHtml(slot.id)}"><option>${escapeHtml(slot.label)}</option></select></label>`)
|
|
88
|
-
.join("")}</aside><div class="panel"><h3>Product grid</h3><div class="product-grid">${cards
|
|
89
|
-
.slice(0, 6)
|
|
90
|
-
.map((slot) => `<article class="product-card" data-component-id="product-card" data-slot-id="${escapeHtml(slot.id)}"><h3>${escapeHtml(slot.label)}</h3><p>${escapeHtml(section.purpose)}</p></article>`)
|
|
91
|
-
.join("")}</div></div></div>`;
|
|
92
|
-
}
|
|
93
|
-
if (role === "product-detail") {
|
|
94
|
-
const rows = structuralSlots(slots, ["Voltage", "Length", "Material", "Finish"]);
|
|
95
|
-
return `<div class="split-layout" ${traceAttrs}><div class="panel"><table class="table spec-table" data-component-id="spec-table"><tbody>${rows
|
|
96
|
-
.slice(0, 4)
|
|
97
|
-
.map((slot) => `<tr><th>${escapeHtml(slot.label)}</th><td data-slot-id="${escapeHtml(slot.id)}">${escapeHtml(section.title)}</td></tr>`)
|
|
98
|
-
.join("")}</tbody></table></div><aside class="panel"><div class="qty-stepper" data-component-id="qty-stepper"><button>-</button><span>${escapeHtml(rows[0].label)}</span><button>+</button></div><a class="btn btn-primary" href="#" data-static-action data-component-id="cta-primary">Add to cart</a></aside></div>`;
|
|
99
|
-
}
|
|
100
|
-
if (role === "search") {
|
|
101
|
-
const rows = structuralSlots(slots, ["Search query", "Matched term", "Category", "Availability", "Support route"]);
|
|
102
|
-
return `<div class="panel" ${traceAttrs}><div class="search-toolbar" data-component-id="search-toolbar"><input aria-label="Search query" data-slot-id="${escapeHtml(rows[0].id)}" placeholder="${escapeHtml(rows[0].label)}"><select><option>${escapeHtml(section.title)}</option></select><button class="btn btn-primary">Search</button></div><div class="result-list" data-component-id="result-list">${rows
|
|
103
|
-
.slice(1, 4)
|
|
104
|
-
.map((slot) => `<article class="result-row" data-slot-id="${escapeHtml(slot.id)}"><h4>${escapeHtml(slot.label)}</h4><p>${escapeHtml(section.purpose)}</p></article>`)
|
|
105
|
-
.join("")}<article class="result-row no-result" data-component-id="no-result"><h4>No-result state</h4><p>${escapeHtml(section.purpose)}</p></article></div></div>`;
|
|
106
|
-
}
|
|
107
|
-
if (role === "cart") {
|
|
108
|
-
const rows = structuralSlots(slots, ["Cart item", "Quantity", "Unit", "Total"]);
|
|
109
|
-
return `<div class="split-layout" ${traceAttrs}><div class="panel"><table class="table cart-table" data-component-id="cart-table"><thead><tr><th>Item</th><th>Qty</th><th>Unit</th><th>Total</th><th>Action</th></tr></thead><tbody>${rows
|
|
110
|
-
.slice(0, 4)
|
|
111
|
-
.map((slot) => `<tr><td data-slot-id="${escapeHtml(slot.id)}">${escapeHtml(slot.label)}</td><td><div class="qty-stepper"><button>-</button><span>Qty</span><button>+</button></div></td><td>Unit</td><td>Total</td><td><button>Remove</button></td></tr>`)
|
|
112
|
-
.join("")}</tbody></table></div><aside class="panel summary-panel" data-component-id="summary-panel"><h3>Summary panel</h3><a class="btn btn-primary" href="#" data-static-action data-component-id="cta-primary">Checkout</a></aside></div>`;
|
|
113
|
-
}
|
|
114
|
-
if (role === "checkout") {
|
|
115
|
-
const rows = structuralSlots(slots, ["Recipient", "Address", "Phone", "Purchase order"]);
|
|
116
|
-
return `<div class="split-layout" ${traceAttrs}><div class="panel"><ol class="checkout-stepper" data-component-id="checkout-stepper">${["Information", "Delivery", "Payment"].map((step) => `<li class="stepper-step" data-component-id="stepper-step">${escapeHtml(step)}</li>`).join("")}</ol><form class="form-grid" data-component-id="form">${rows
|
|
117
|
-
.slice(0, 4)
|
|
118
|
-
.map((slot) => `<label>${escapeHtml(slot.label)}<input type="text" data-slot-id="${escapeHtml(slot.id)}" value="${escapeHtml(slot.label)}"></label>`)
|
|
119
|
-
.join("")}</form></div><aside class="panel summary-panel" data-component-id="summary-panel"><h3>Summary card</h3><button class="btn btn-primary">Place order</button></aside></div>`;
|
|
120
|
-
}
|
|
121
|
-
if (role === "order-history") {
|
|
122
|
-
const rows = structuralSlots(slots, ["Order status", "Order total", "Order action"]);
|
|
123
|
-
return `<div ${traceAttrs}><h3>Order history list</h3><table class="table" data-component-id="history-row"><tbody>${rows
|
|
124
|
-
.slice(0, 3)
|
|
125
|
-
.map((slot) => `<tr class="history-row"><td data-slot-id="${escapeHtml(slot.id)}">${escapeHtml(slot.label)}</td><td>${escapeHtml(section.title)}</td><td><a href="#" data-static-action>Open detail</a></td></tr>`)
|
|
126
|
-
.join("")}</tbody></table></div>`;
|
|
127
|
-
}
|
|
128
|
-
if (role === "order-detail") {
|
|
129
|
-
const rows = structuralSlots(slots, ["Placed", "Approved", "Delivered"]);
|
|
130
|
-
return `<div class="split-layout" ${traceAttrs}><ol class="timeline" data-component-id="timeline">${rows
|
|
131
|
-
.slice(0, 3)
|
|
132
|
-
.map((slot) => `<li data-slot-id="${escapeHtml(slot.id)}">${escapeHtml(slot.label)}</li>`)
|
|
133
|
-
.join("")}</ol><aside class="panel detail-summary" data-component-id="detail-summary"><h3>Detail summary</h3><p>${escapeHtml(section.purpose)}</p></aside></div>`;
|
|
134
|
-
}
|
|
135
|
-
if (role === "account-info") {
|
|
136
|
-
const rows = structuralSlots(slots, ["Company", "Contact", "Email", "Phone"]);
|
|
137
|
-
return `<div class="screen-layout" ${traceAttrs}><aside class="panel account-sidebar" data-component-id="account-sidebar"><h3>Account sidebar</h3><nav class="screen-nav"><a href="#">Company</a><a href="#">Users</a></nav></aside><form class="form-grid" data-component-id="form"><h3>View or edit form</h3>${rows
|
|
138
|
-
.slice(0, 4)
|
|
139
|
-
.map((slot) => `<label>${escapeHtml(slot.label)}<input type="text" data-slot-id="${escapeHtml(slot.id)}" value="${escapeHtml(slot.label)}"></label>`)
|
|
140
|
-
.join("")}</form></div>`;
|
|
141
|
-
}
|
|
142
|
-
if (role === "configurator-bom" || role === "mode-b-configurator") {
|
|
143
|
-
const rows = structuralSlots(slots, ["Project", "Assembly", "Construction", "Review", "Material"]);
|
|
144
|
-
return `<div class="panel" ${traceAttrs}><h3>Configurator wizard</h3><ol class="checkout-stepper" data-component-id="configurator-wizard">${rows
|
|
145
|
-
.slice(0, 4)
|
|
146
|
-
.map((slot) => `<li class="stepper-step" data-component-id="stepper-step" data-slot-id="${escapeHtml(slot.id)}">${escapeHtml(slot.label)}</li>`)
|
|
147
|
-
.join("")}</ol><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 class="split-layout"><aside class="panel preview-panel" data-component-id="preview-panel"><h4>Preview panel</h4><p>${escapeHtml(section.purpose)}</p></aside><div class="panel"><h4>BOM table</h4><table class="table cart-table bom-table" data-component-id="bom-table"><thead><tr><th>Material</th><th>Spec</th><th>Qty</th><th>Unit</th><th>Action</th></tr></thead><tbody>${rows
|
|
148
|
-
.map((slot) => `<tr><td data-slot-id="${escapeHtml(slot.id)}">${escapeHtml(slot.label)}</td><td>${escapeHtml(section.title)}</td><td>Qty</td><td>Unit</td><td><button>Exclude</button></td></tr>`)
|
|
149
|
-
.join("")}</tbody></table></div></div></div>`;
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function renderBriefComponent({ componentId, section, brief, role, slotLookup, escapeHtml }) {
|
|
155
|
-
const id = componentId && componentId.id ? componentId.id : String(componentId || "component");
|
|
156
|
-
const label = componentId && componentId.label ? componentId.label : id;
|
|
157
|
-
const klass = componentClassFor(id);
|
|
158
|
-
const traceAttrs = `data-component-id="${escapeHtml(id)}" data-section-id="${escapeHtml(section.id)}"`;
|
|
159
|
-
const slots = Array.isArray(section.data_slots) && section.data_slots.length > 0 ? section.data_slots : Array.from((slotLookup || new Map()).values());
|
|
160
|
-
const isButtonComponent = klass === "btn-primary" || klass === "btn-secondary";
|
|
161
|
-
const rolePrimary = !isButtonComponent && /primary|work-area|action-toolbar|header-navigation|status-chips|panel|toolbar|navigation/i.test(`${id} ${label}`)
|
|
162
|
-
? renderRolePrimary({ role, section, brief, traceAttrs, slots, escapeHtml })
|
|
163
|
-
: null;
|
|
164
|
-
if (rolePrimary) return rolePrimary;
|
|
165
|
-
const slotRows = slots.slice(0, 6).map((slot) => `<tr><td>${renderDataSlotPlaceholder({ slot, section, escapeHtml })}</td><td>${escapeHtml(section.title)}</td></tr>`).join("");
|
|
166
|
-
const slotCards = slots.slice(0, 6).map((slot) => `<article class="feature-card category-card product-card" ${traceAttrs} data-slot-id="${escapeHtml(slot.id)}"><h3>${escapeHtml(slot.label)}</h3><p>${escapeHtml(section.purpose)}</p></article>`).join("");
|
|
167
|
-
|
|
168
|
-
if (klass === "hero-section") {
|
|
169
|
-
return `<header class="panel hero-section" ${traceAttrs}><h1>${escapeHtml(brief.screen_title || section.title)}</h1><p class="support">${escapeHtml(section.purpose || brief.purpose || brief.user_intent)}</p><div class="button-row"><a class="btn btn-primary" href="#" data-static-action data-component-id="cta-primary">${escapeHtml(truncatePrimaryAction(brief.primary_action, role))}</a></div></header>`;
|
|
170
|
-
}
|
|
171
|
-
if (klass === "btn-primary") {
|
|
172
|
-
return `<a class="btn btn-primary" href="#" data-static-action ${traceAttrs}>${escapeHtml(truncatePrimaryAction(brief.primary_action, role))}</a>`;
|
|
173
|
-
}
|
|
174
|
-
if (klass === "btn-secondary") {
|
|
175
|
-
return `<a class="btn btn-secondary" href="#" data-static-action ${traceAttrs}>${escapeHtml(section.title || "Review options")}</a>`;
|
|
176
|
-
}
|
|
177
|
-
if (klass.includes("product-card") || klass.includes("feature-card") || klass.includes("category-card")) {
|
|
178
|
-
return `<div class="feature-grid product-grid" ${traceAttrs}>${slotCards || `<article class="${escapeHtml(klass)}" ${traceAttrs}><h3>${escapeHtml(label)}</h3><p>${escapeHtml(section.purpose)}</p></article>`}</div>`;
|
|
179
|
-
}
|
|
180
|
-
if (klass.includes("cart-table") || klass.includes("bom-table") || klass.includes("spec-table")) {
|
|
181
|
-
return `<table class="table ${escapeHtml(klass)}" ${traceAttrs}><thead><tr><th>Data slot</th><th>Section</th><th>Status</th><th>Action</th><th>Trace</th></tr></thead><tbody>${slotRows || `<tr><td>${escapeHtml(label)}</td><td>${escapeHtml(section.title)}</td><td>Ready</td><td>Review</td><td>${escapeHtml(role)}</td></tr>`}</tbody></table>`;
|
|
182
|
-
}
|
|
183
|
-
if (klass === "qty-stepper") {
|
|
184
|
-
return `<div class="qty-stepper" ${traceAttrs}><button>-</button><span>${escapeHtml(label)}</span><button>+</button></div>`;
|
|
185
|
-
}
|
|
186
|
-
if (klass === "checkout-stepper") {
|
|
187
|
-
const steps = (Array.isArray(section.interactions) && section.interactions.length > 0 ? section.interactions : [section.title, "Review", "Confirm", "Complete"]).slice(0, 5);
|
|
188
|
-
return `<ol class="checkout-stepper" ${traceAttrs}>${steps.map((step) => `<li class="stepper-step" data-component-id="${escapeHtml(id)}">${escapeHtml(step)}</li>`).join("")}</ol>`;
|
|
189
|
-
}
|
|
190
|
-
if (klass === "form-grid") {
|
|
191
|
-
return `<form class="form-grid" ${traceAttrs}>${slots.slice(0, 6).map((slot) => `<label>${escapeHtml(slot.label)}<input type="text" value="${escapeHtml(slot.label)}"></label>`).join("")}</form>`;
|
|
192
|
-
}
|
|
193
|
-
if (klass.includes("filter-sidebar") || klass.includes("summary-panel") || klass.includes("preview-panel") || klass.includes("detail-summary")) {
|
|
194
|
-
return `<aside class="panel ${escapeHtml(klass)}" ${traceAttrs}><h3>${escapeHtml(label)}</h3><p>${escapeHtml(section.purpose)}</p>${renderSlotList({ section, escapeHtml })}</aside>`;
|
|
195
|
-
}
|
|
196
|
-
if (klass === "search-toolbar") {
|
|
197
|
-
return `<div class="search-toolbar" ${traceAttrs}><input aria-label="Search query" placeholder="${escapeHtml(label)}"><select><option>${escapeHtml(section.title)}</option></select><button class="btn btn-primary">Search</button></div>`;
|
|
198
|
-
}
|
|
199
|
-
if (klass === "result-list") {
|
|
200
|
-
return `<div class="result-list" ${traceAttrs}>${slots.slice(0, 5).map((slot) => `<article class="result-row" data-slot-id="${escapeHtml(slot.id)}"><h4>${escapeHtml(slot.label)}</h4><p>${escapeHtml(section.purpose)}</p></article>`).join("")}</div>`;
|
|
201
|
-
}
|
|
202
|
-
if (klass === "no-result") {
|
|
203
|
-
return `<article class="result-row no-result" ${traceAttrs}><h4>${escapeHtml(label)}</h4><p>${escapeHtml(section.purpose)}</p></article>`;
|
|
204
|
-
}
|
|
205
|
-
if (klass === "timeline") {
|
|
206
|
-
const steps = (Array.isArray(section.interactions) && section.interactions.length > 0 ? section.interactions : [section.title, "Review", "Complete"]).slice(0, 5);
|
|
207
|
-
return `<ol class="timeline" ${traceAttrs}>${steps.map((step) => `<li>${escapeHtml(step)}</li>`).join("")}</ol>`;
|
|
208
|
-
}
|
|
209
|
-
if (klass === "history-row") {
|
|
210
|
-
return `<table class="table" ${traceAttrs}><tbody>${slots.slice(0, 5).map((slot) => `<tr class="history-row"><td>${escapeHtml(slot.label)}</td><td>${escapeHtml(section.title)}</td><td><a href="#" data-static-action>Open detail</a></td></tr>`).join("")}</tbody></table>`;
|
|
211
|
-
}
|
|
212
|
-
return `<article class="component-placeholder" ${traceAttrs} data-component-status="unknown"><h3>${escapeHtml(label)}</h3><p>${escapeHtml(section.purpose)}</p>${renderSlotList({ section, escapeHtml })}</article>`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
module.exports = {
|
|
216
|
-
componentClassFor,
|
|
217
|
-
renderBriefComponent,
|
|
218
|
-
renderDataSlotPlaceholder,
|
|
219
|
-
};
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const MIN_TOTAL_SEMANTIC_HTML_BYTES = 10 * 1024;
|
|
4
|
-
|
|
5
|
-
const LEGACY_MIN_TOTAL_SCREEN_BYTES = 80 * 1024;
|
|
6
|
-
|
|
7
|
-
const LEGACY_ROLE_FLOORS = {
|
|
8
|
-
home: { minBytes: 8 * 1024 },
|
|
9
|
-
category: { minBytes: 6 * 1024 },
|
|
10
|
-
"product-detail": { minBytes: 6 * 1024 },
|
|
11
|
-
search: { minBytes: 4 * 1024 },
|
|
12
|
-
cart: { minBytes: 5 * 1024 },
|
|
13
|
-
checkout: { minBytes: 5 * 1024 },
|
|
14
|
-
"order-history": { minBytes: 4 * 1024 },
|
|
15
|
-
"order-detail": { minBytes: 4 * 1024 },
|
|
16
|
-
"account-info": { minBytes: 4 * 1024 },
|
|
17
|
-
"configurator-bom": { minBytes: 15 * 1024 },
|
|
18
|
-
"mode-b-configurator": { minBytes: 15 * 1024 },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const STRUCTURAL_ROLE_FLOORS = {
|
|
22
|
-
home: {
|
|
23
|
-
minSections: 2,
|
|
24
|
-
minDataSlots: 1,
|
|
25
|
-
requiredComponents: ["hero", "feature-card"],
|
|
26
|
-
checks: { heroSection: true, featureOrCategoryCards: 3, ctaPair: 2 },
|
|
27
|
-
},
|
|
28
|
-
category: {
|
|
29
|
-
minSections: 2,
|
|
30
|
-
minDataSlots: 2,
|
|
31
|
-
requiredComponents: ["filter-sidebar", "product-card"],
|
|
32
|
-
checks: { filterSidebar: true, productCards: 6, resultGrid: true },
|
|
33
|
-
},
|
|
34
|
-
"product-detail": {
|
|
35
|
-
minSections: 2,
|
|
36
|
-
minDataSlots: 3,
|
|
37
|
-
requiredComponents: ["spec-table", "qty-stepper", "cta-primary"],
|
|
38
|
-
checks: { specTableRows: 4, quantityStepper: true, addToCartCta: true },
|
|
39
|
-
},
|
|
40
|
-
search: {
|
|
41
|
-
minSections: 2,
|
|
42
|
-
minDataSlots: 1,
|
|
43
|
-
requiredComponents: ["search-toolbar", "result-list", "no-result"],
|
|
44
|
-
checks: { searchToolbar: true, resultList: true, noResultState: true },
|
|
45
|
-
},
|
|
46
|
-
cart: {
|
|
47
|
-
minSections: 2,
|
|
48
|
-
minDataSlots: 3,
|
|
49
|
-
requiredComponents: ["cart-table", "summary-panel", "cta-primary"],
|
|
50
|
-
checks: { cartTableColumns: 3, summaryPanel: true, checkoutCta: true },
|
|
51
|
-
},
|
|
52
|
-
checkout: {
|
|
53
|
-
minSections: 2,
|
|
54
|
-
minDataSlots: 4,
|
|
55
|
-
requiredComponents: ["checkout-stepper", "form", "summary-panel"],
|
|
56
|
-
checks: { stepperSteps: 3, formFields: 4, summary: true },
|
|
57
|
-
},
|
|
58
|
-
"order-history": {
|
|
59
|
-
minSections: 1,
|
|
60
|
-
minDataSlots: 2,
|
|
61
|
-
requiredComponents: ["history-row"],
|
|
62
|
-
checks: { listOrTableRows: 3 },
|
|
63
|
-
},
|
|
64
|
-
"order-detail": {
|
|
65
|
-
minSections: 2,
|
|
66
|
-
minDataSlots: 2,
|
|
67
|
-
requiredComponents: ["timeline", "detail-summary"],
|
|
68
|
-
checks: { timelineSteps: 3, detailSummary: true },
|
|
69
|
-
},
|
|
70
|
-
"account-info": {
|
|
71
|
-
minSections: 2,
|
|
72
|
-
minDataSlots: 4,
|
|
73
|
-
requiredComponents: ["account-sidebar", "form"],
|
|
74
|
-
checks: { sidebarNav: true, formFields: 4 },
|
|
75
|
-
},
|
|
76
|
-
"configurator-bom": {
|
|
77
|
-
minSections: 3,
|
|
78
|
-
minDataSlots: 4,
|
|
79
|
-
requiredComponents: ["configurator-wizard", "bom-table", "preview-panel", "stepper-step"],
|
|
80
|
-
checks: { wizard: true, bomTableColumns: 5, previewPanel: true, stepperSteps: 4 },
|
|
81
|
-
},
|
|
82
|
-
"mode-b-configurator": {
|
|
83
|
-
minSections: 3,
|
|
84
|
-
minDataSlots: 4,
|
|
85
|
-
requiredComponents: ["configurator-wizard", "bom-table", "preview-panel", "stepper-step"],
|
|
86
|
-
checks: { wizard: true, bomTableColumns: 5, previewPanel: true, stepperSteps: 4 },
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
function normalizeScreenRole(screen) {
|
|
91
|
-
const token = `${screen && screen.screenId ? screen.screenId : ""} ${screen && screen.title ? screen.title : ""} ${screen && screen.template_role ? screen.template_role : ""}`.toLowerCase();
|
|
92
|
-
if (token.includes("home")) return "home";
|
|
93
|
-
if (token.includes("category") || token.includes("catalog")) return "category";
|
|
94
|
-
if (token.includes("product-detail") || token.includes("product detail") || token.includes("pdp")) return "product-detail";
|
|
95
|
-
if (token.includes("search")) return "search";
|
|
96
|
-
if (token.includes("cart") && !token.includes("checkout")) return "cart";
|
|
97
|
-
if (token.includes("checkout")) return "checkout";
|
|
98
|
-
if (token.includes("order-history") || token.includes("order history")) return "order-history";
|
|
99
|
-
if (token.includes("order-detail") || token.includes("order detail")) return "order-detail";
|
|
100
|
-
if (token.includes("account")) return "account-info";
|
|
101
|
-
if (token.includes("configurator") || token.includes("bom") || token.includes("mode-b")) return "configurator-bom";
|
|
102
|
-
return screen && screen.screenId ? screen.screenId : "generic";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function resolveDensityMode(env = process.env) {
|
|
106
|
-
return env && String(env.SDTK_DESIGN_DENSITY_MODE || "").toLowerCase() === "bytes" ? "bytes" : "semantic";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function countMatches(html, pattern) {
|
|
110
|
-
const matches = String(html || "").match(pattern);
|
|
111
|
-
return matches ? matches.length : 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function countTableColumns(html) {
|
|
115
|
-
const headerRows = String(html || "").match(/<tr[\s\S]*?<\/tr>/gi) || [];
|
|
116
|
-
let max = 0;
|
|
117
|
-
for (const row of headerRows) {
|
|
118
|
-
max = Math.max(max, countMatches(row, /<t[hd]\b/gi));
|
|
119
|
-
}
|
|
120
|
-
return max;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function hasClassToken(html, token) {
|
|
124
|
-
const pattern = new RegExp(`class="[^"]*\\b${token.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b[^"]*"`, "i");
|
|
125
|
-
return pattern.test(String(html || ""));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function countClassToken(html, token) {
|
|
129
|
-
const pattern = new RegExp(`class="[^"]*\\b${token.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b[^"]*"`, "gi");
|
|
130
|
-
return countMatches(html, pattern);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function countDataAttribute(html, attribute) {
|
|
134
|
-
const pattern = new RegExp(`\\s${attribute}=`, "gi");
|
|
135
|
-
return countMatches(html, pattern);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function countSections(html) {
|
|
139
|
-
return Math.max(countDataAttribute(html, "data-section-id"), countMatches(html, /<section\b/gi));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function countComponents(html) {
|
|
143
|
-
const dataComponents = countDataAttribute(html, "data-component-id");
|
|
144
|
-
if (dataComponents > 0) return dataComponents;
|
|
145
|
-
return countMatches(
|
|
146
|
-
html,
|
|
147
|
-
/class="[^"]*\b(hero|feature-card|category-card|product-card|filter-sidebar|spec-table|qty-stepper|search-toolbar|result-list|no-result|cart-table|summary-panel|checkout-stepper|stepper-step|history-row|timeline|detail-summary|account-sidebar|preview-panel)\b[^"]*"/gi
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function countDataSlots(html) {
|
|
152
|
-
const dataSlots = countDataAttribute(html, "data-slot-id");
|
|
153
|
-
if (dataSlots > 0) return dataSlots;
|
|
154
|
-
return (
|
|
155
|
-
countMatches(html, /<(input|select|textarea)\b/gi) +
|
|
156
|
-
countMatches(html, /<tr\b/gi) +
|
|
157
|
-
countClassToken(html, "feature-card") +
|
|
158
|
-
countClassToken(html, "category-card") +
|
|
159
|
-
countClassToken(html, "product-card") +
|
|
160
|
-
countClassToken(html, "result-row") +
|
|
161
|
-
countClassToken(html, "history-row") +
|
|
162
|
-
countMatches(html, /<li\b/gi)
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function countStates(html) {
|
|
167
|
-
return Math.max(countDataAttribute(html, "data-state-id"), countDataAttribute(html, "data-chip-type"));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function makeCheck(id, actual, expected, pass) {
|
|
171
|
-
return { id, actual, expected, pass: Boolean(pass) };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function roleStructuralChecks(role, html) {
|
|
175
|
-
const lower = String(html || "").toLowerCase();
|
|
176
|
-
const checks = [];
|
|
177
|
-
const textCheck = (id, text, expected = true) => {
|
|
178
|
-
checks.push(makeCheck(id, lower.includes(text), expected, lower.includes(text) === expected));
|
|
179
|
-
};
|
|
180
|
-
const minCountCheck = (id, pattern, minimum) => {
|
|
181
|
-
const actual = countMatches(html, pattern);
|
|
182
|
-
checks.push(makeCheck(id, actual, minimum, actual >= minimum));
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
if (role === "home") {
|
|
186
|
-
textCheck("heroSection", "hero");
|
|
187
|
-
minCountCheck("featureOrCategoryCards", /class="[^"]*(feature-card|category-card|product-card)[^"]*"/gi, 3);
|
|
188
|
-
minCountCheck("ctaPair", /class="[^"]*\bbtn\b[^"]*"/gi, 2);
|
|
189
|
-
} else if (role === "category") {
|
|
190
|
-
textCheck("filterSidebar", "filter sidebar");
|
|
191
|
-
minCountCheck("productCards", /class="[^"]*product-card[^"]*"/gi, 6);
|
|
192
|
-
const resultGridPass =
|
|
193
|
-
lower.includes("product grid") ||
|
|
194
|
-
hasClassToken(html, "product-grid") ||
|
|
195
|
-
/data-component-id="(product-card|result-grid)"/i.test(String(html || ""));
|
|
196
|
-
checks.push(makeCheck("resultGrid", resultGridPass, true, resultGridPass));
|
|
197
|
-
} else if (role === "product-detail") {
|
|
198
|
-
minCountCheck("specTableRows", /<tr\b/gi, 4);
|
|
199
|
-
textCheck("quantityStepper", "qty-stepper");
|
|
200
|
-
textCheck("addToCartCta", "add to cart");
|
|
201
|
-
} else if (role === "search") {
|
|
202
|
-
textCheck("searchToolbar", "search-toolbar");
|
|
203
|
-
textCheck("resultList", "result-list");
|
|
204
|
-
textCheck("noResultState", "no-result");
|
|
205
|
-
} else if (role === "cart") {
|
|
206
|
-
textCheck("cartTableColumns", "cart-table");
|
|
207
|
-
checks.push(makeCheck("cartTableColumns", countTableColumns(html), 3, countTableColumns(html) >= 3));
|
|
208
|
-
textCheck("summaryPanel", "summary panel");
|
|
209
|
-
textCheck("checkoutCta", "checkout");
|
|
210
|
-
} else if (role === "checkout") {
|
|
211
|
-
textCheck("stepperSteps", "checkout-stepper");
|
|
212
|
-
minCountCheck("stepperSteps", /class="[^"]*stepper-step[^"]*"/gi, 3);
|
|
213
|
-
minCountCheck("formFields", /<(input|select|textarea)\b/gi, 4);
|
|
214
|
-
textCheck("summary", "summary");
|
|
215
|
-
} else if (role === "order-history") {
|
|
216
|
-
minCountCheck("listOrTableRows", /<tr\b|class="[^"]*history-row/gi, 3);
|
|
217
|
-
} else if (role === "order-detail") {
|
|
218
|
-
textCheck("timelineSteps", "timeline");
|
|
219
|
-
minCountCheck("timelineSteps", /<li\b/gi, 3);
|
|
220
|
-
textCheck("detailSummary", "detail summary");
|
|
221
|
-
} else if (role === "account-info") {
|
|
222
|
-
textCheck("sidebarNav", "account sidebar");
|
|
223
|
-
minCountCheck("formFields", /<(input|select|textarea)\b/gi, 4);
|
|
224
|
-
} else if (role === "configurator-bom" || role === "mode-b-configurator") {
|
|
225
|
-
textCheck("wizard", "configurator wizard");
|
|
226
|
-
checks.push(makeCheck("bomTableColumns", countTableColumns(html), 5, countTableColumns(html) >= 5));
|
|
227
|
-
textCheck("previewPanel", "preview panel");
|
|
228
|
-
minCountCheck("stepperSteps", /class="[^"]*stepper-step[^"]*"/gi, 4);
|
|
229
|
-
}
|
|
230
|
-
return checks;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function requiredComponentPresent(html, component) {
|
|
234
|
-
const lower = String(html || "").toLowerCase();
|
|
235
|
-
if (countDataAttribute(html, "data-component-id") > 0) {
|
|
236
|
-
return new RegExp(`data-component-id="[^"]*${component.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}[^"]*"`, "i").test(html);
|
|
237
|
-
}
|
|
238
|
-
if (component === "hero") return hasClassToken(html, "hero") || lower.includes("hero");
|
|
239
|
-
if (component === "filter-sidebar") return lower.includes("filter sidebar") || hasClassToken(html, "filter-sidebar");
|
|
240
|
-
if (component === "result-grid") return lower.includes("product grid");
|
|
241
|
-
if (component === "form") return /<(input|select|textarea)\b/i.test(html);
|
|
242
|
-
if (component === "cta-primary") return hasClassToken(html, "btn-primary");
|
|
243
|
-
if (component === "configurator-wizard") return lower.includes("configurator wizard");
|
|
244
|
-
if (component === "bom-table") return lower.includes("bom table") || hasClassToken(html, "cart-table");
|
|
245
|
-
return hasClassToken(html, component) || lower.includes(component.replace(/-/g, " "));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function assessSemanticPage({ screenId, title, role, relativePath, html }) {
|
|
249
|
-
const normalizedRole = role || normalizeScreenRole({ screenId, title });
|
|
250
|
-
const floor = STRUCTURAL_ROLE_FLOORS[normalizedRole] || { minSections: 1, minDataSlots: 0, requiredComponents: [], checks: {} };
|
|
251
|
-
const sectionCount = countSections(html);
|
|
252
|
-
const componentCount = countComponents(html);
|
|
253
|
-
const dataSlotCount = countDataSlots(html);
|
|
254
|
-
const stateCount = countStates(html);
|
|
255
|
-
const requiredComponents = floor.requiredComponents || [];
|
|
256
|
-
const missingComponents = requiredComponents.filter((component) => !requiredComponentPresent(String(html || ""), component));
|
|
257
|
-
const structuralChecks = roleStructuralChecks(normalizedRole, html);
|
|
258
|
-
const findings = [];
|
|
259
|
-
if (sectionCount < floor.minSections) findings.push(`section count ${sectionCount} < ${floor.minSections}`);
|
|
260
|
-
if (dataSlotCount < floor.minDataSlots) findings.push(`data slot count ${dataSlotCount} < ${floor.minDataSlots}`);
|
|
261
|
-
if (missingComponents.length > 0) findings.push(`missing components: ${missingComponents.join(", ")}`);
|
|
262
|
-
for (const check of structuralChecks) {
|
|
263
|
-
if (!check.pass) findings.push(`${check.id} ${check.actual} < ${check.expected}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
screenId,
|
|
268
|
-
title,
|
|
269
|
-
role: normalizedRole,
|
|
270
|
-
relativePath,
|
|
271
|
-
sectionCount,
|
|
272
|
-
componentCount,
|
|
273
|
-
dataSlotCount,
|
|
274
|
-
stateCount,
|
|
275
|
-
requiredComponents,
|
|
276
|
-
missingComponents,
|
|
277
|
-
structuralChecks,
|
|
278
|
-
pass: findings.length === 0,
|
|
279
|
-
findings,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function assessSemanticDensity(pageRecords) {
|
|
284
|
-
const pages = (Array.isArray(pageRecords) ? pageRecords : []).map(assessSemanticPage);
|
|
285
|
-
const aggregate = pages.reduce(
|
|
286
|
-
(acc, page) => {
|
|
287
|
-
acc.pageCount += 1;
|
|
288
|
-
acc.sectionCount += page.sectionCount;
|
|
289
|
-
acc.componentCount += page.componentCount;
|
|
290
|
-
acc.dataSlotCount += page.dataSlotCount;
|
|
291
|
-
acc.stateCount += page.stateCount;
|
|
292
|
-
return acc;
|
|
293
|
-
},
|
|
294
|
-
{ pageCount: 0, sectionCount: 0, componentCount: 0, dataSlotCount: 0, stateCount: 0 }
|
|
295
|
-
);
|
|
296
|
-
const totalBytes = (Array.isArray(pageRecords) ? pageRecords : []).reduce((sum, page) => sum + Buffer.byteLength(String(page.html || ""), "utf8"), 0);
|
|
297
|
-
const findings = [];
|
|
298
|
-
if (pages.length === 0) findings.push("No per-screen prototype pages found.");
|
|
299
|
-
if (pages.length > 0 && totalBytes < MIN_TOTAL_SEMANTIC_HTML_BYTES) {
|
|
300
|
-
findings.push(`Total semantic HTML bytes ${totalBytes} < ${MIN_TOTAL_SEMANTIC_HTML_BYTES}`);
|
|
301
|
-
}
|
|
302
|
-
for (const page of pages) {
|
|
303
|
-
if (!page.pass) findings.push(`${page.screenId}: ${page.findings.join("; ")}`);
|
|
304
|
-
}
|
|
305
|
-
return {
|
|
306
|
-
mode: "semantic",
|
|
307
|
-
pass: findings.length === 0,
|
|
308
|
-
aggregate,
|
|
309
|
-
totalBytes,
|
|
310
|
-
minTotalBytes: MIN_TOTAL_SEMANTIC_HTML_BYTES,
|
|
311
|
-
pages,
|
|
312
|
-
findings,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function roleByteStructureFindings(role, html) {
|
|
317
|
-
return roleStructuralChecks(role, html)
|
|
318
|
-
.filter((check) => !check.pass)
|
|
319
|
-
.map((check) => `${check.id} ${check.actual} < ${check.expected}`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function assessBytePage({ screenId, title, role, relativePath, html }) {
|
|
323
|
-
const normalizedRole = role || normalizeScreenRole({ screenId, title });
|
|
324
|
-
const byteLength = Buffer.byteLength(String(html || ""), "utf8");
|
|
325
|
-
const floor = LEGACY_ROLE_FLOORS[normalizedRole] || { minBytes: 0 };
|
|
326
|
-
const findings = [];
|
|
327
|
-
if (byteLength < floor.minBytes) findings.push(`HTML bytes ${byteLength} < ${floor.minBytes}`);
|
|
328
|
-
findings.push(...roleByteStructureFindings(normalizedRole, html));
|
|
329
|
-
return {
|
|
330
|
-
screenId,
|
|
331
|
-
title,
|
|
332
|
-
role: normalizedRole,
|
|
333
|
-
relativePath,
|
|
334
|
-
byteLength,
|
|
335
|
-
minBytes: floor.minBytes,
|
|
336
|
-
pass: findings.length === 0,
|
|
337
|
-
findings,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function assessByteDensity(pageRecords) {
|
|
342
|
-
const pages = (Array.isArray(pageRecords) ? pageRecords : []).map(assessBytePage);
|
|
343
|
-
const totalBytes = pages.reduce((sum, page) => sum + page.byteLength, 0);
|
|
344
|
-
const findings = [];
|
|
345
|
-
if (totalBytes < LEGACY_MIN_TOTAL_SCREEN_BYTES) {
|
|
346
|
-
findings.push(`Total per-screen HTML bytes ${totalBytes} < ${LEGACY_MIN_TOTAL_SCREEN_BYTES}`);
|
|
347
|
-
}
|
|
348
|
-
for (const page of pages) {
|
|
349
|
-
if (!page.pass) findings.push(`${page.screenId}: ${page.findings.join("; ")}`);
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
mode: "bytes",
|
|
353
|
-
pass: findings.length === 0,
|
|
354
|
-
totalBytes,
|
|
355
|
-
minTotalBytes: LEGACY_MIN_TOTAL_SCREEN_BYTES,
|
|
356
|
-
pages,
|
|
357
|
-
findings,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function assessPrototypeDensity(pageRecords, options = {}) {
|
|
362
|
-
const mode = options.mode || resolveDensityMode();
|
|
363
|
-
return mode === "bytes" ? assessByteDensity(pageRecords) : assessSemanticDensity(pageRecords, options.briefIndex);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
module.exports = {
|
|
367
|
-
LEGACY_MIN_TOTAL_SCREEN_BYTES,
|
|
368
|
-
LEGACY_ROLE_FLOORS,
|
|
369
|
-
MIN_TOTAL_SEMANTIC_HTML_BYTES,
|
|
370
|
-
ROLE_FLOORS: STRUCTURAL_ROLE_FLOORS,
|
|
371
|
-
STRUCTURAL_ROLE_FLOORS,
|
|
372
|
-
assessPrototypeDensity,
|
|
373
|
-
assessSemanticDensity,
|
|
374
|
-
assessSemanticPage,
|
|
375
|
-
normalizeScreenRole,
|
|
376
|
-
resolveDensityMode,
|
|
377
|
-
};
|