sdtk-design-kit 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/handoff.js +357 -203
- package/src/commands/help.js +3 -0
- package/src/commands/prototype.js +436 -26
- package/src/commands/review.js +230 -0
- package/src/commands/start.js +105 -8
- package/src/commands/status.js +37 -1
- package/src/lib/component-contract.js +142 -0
- package/src/lib/design-input-contract.js +683 -0
- package/src/lib/design-paths.js +27 -0
- package/src/lib/design-profiles.js +58 -0
- package/src/lib/prototype-density.js +147 -0
- package/src/lib/prototype-renderer.js +325 -0
- package/src/lib/screen-briefs.js +340 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { describeDesignPaths } = require("./design-paths");
|
|
6
|
+
const { ValidationError } = require("./errors");
|
|
7
|
+
|
|
8
|
+
function escapeMarkdown(value) {
|
|
9
|
+
return String(value || "").replace(/\r?\n/g, " ").trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toScreenBriefFileName(screenId) {
|
|
13
|
+
return `${String(screenId || "")
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")}_DESIGN_BRIEF.md`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toJsonFileName(screenId) {
|
|
19
|
+
return `${String(screenId || "")
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")}.json`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cleanArray(values) {
|
|
25
|
+
return Array.isArray(values)
|
|
26
|
+
? values.map((item) => String(item || "").trim()).filter(Boolean)
|
|
27
|
+
: [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function uniqueStrings(values) {
|
|
31
|
+
return Array.from(new Set(cleanArray(values)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function needs(marker) {
|
|
35
|
+
return `NEEDS_${marker}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function listOrNeeds(values, marker) {
|
|
39
|
+
const items = cleanArray(values);
|
|
40
|
+
if (items.length > 0) {
|
|
41
|
+
return items.map((item) => `- ${escapeMarkdown(item)}`).join("\n");
|
|
42
|
+
}
|
|
43
|
+
return `- ${needs(marker)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tokenUsageOrDefault(value) {
|
|
47
|
+
const payload = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
48
|
+
const tokenUsage = {
|
|
49
|
+
palette_keys: cleanArray(payload.palette_keys || payload.paletteKeys),
|
|
50
|
+
typography_keys: cleanArray(payload.typography_keys || payload.typographyKeys),
|
|
51
|
+
spacing_keys: cleanArray(payload.spacing_keys || payload.spacingKeys),
|
|
52
|
+
radius_keys: cleanArray(payload.radius_keys || payload.radiusKeys),
|
|
53
|
+
};
|
|
54
|
+
if (
|
|
55
|
+
tokenUsage.palette_keys.length === 0 &&
|
|
56
|
+
tokenUsage.typography_keys.length === 0 &&
|
|
57
|
+
tokenUsage.spacing_keys.length === 0 &&
|
|
58
|
+
tokenUsage.radius_keys.length === 0
|
|
59
|
+
) {
|
|
60
|
+
return {
|
|
61
|
+
palette_keys: ["color.surface", "color.textPrimary", "color.accentPrimary"],
|
|
62
|
+
typography_keys: ["typography.fontFamily", "typography.uiWeight"],
|
|
63
|
+
spacing_keys: ["spacing.md", "spacing.lg", "spacing.sectionGap"],
|
|
64
|
+
radius_keys: ["radius.card", "radius.control"],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return tokenUsage;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tokenUsageMarkdown(tokenUsage) {
|
|
71
|
+
return [
|
|
72
|
+
`- Palette keys: ${tokenUsage.palette_keys.length > 0 ? tokenUsage.palette_keys.join(", ") : needs("PALETTE_KEYS")}`,
|
|
73
|
+
`- Typography keys: ${tokenUsage.typography_keys.length > 0 ? tokenUsage.typography_keys.join(", ") : needs("TYPOGRAPHY_KEYS")}`,
|
|
74
|
+
`- Spacing keys: ${tokenUsage.spacing_keys.length > 0 ? tokenUsage.spacing_keys.join(", ") : needs("SPACING_KEYS")}`,
|
|
75
|
+
`- Radius keys: ${tokenUsage.radius_keys.length > 0 ? tokenUsage.radius_keys.join(", ") : needs("RADIUS_KEYS")}`,
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function layoutModelForRole(templateRole) {
|
|
80
|
+
const models = {
|
|
81
|
+
home: "hero-catalog-support",
|
|
82
|
+
category: "filter-and-results",
|
|
83
|
+
"product-detail": "split-detail-with-purchase-panel",
|
|
84
|
+
search: "search-toolbar-results",
|
|
85
|
+
cart: "line-item-table-with-summary",
|
|
86
|
+
checkout: "stepper-with-order-summary",
|
|
87
|
+
"order-history": "table-list-with-filters",
|
|
88
|
+
"order-detail": "timeline-detail-summary",
|
|
89
|
+
"account-info": "sidebar-form-shell",
|
|
90
|
+
"configurator-bom": "wizard-with-preview-and-table",
|
|
91
|
+
};
|
|
92
|
+
return models[templateRole] || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function densityForRole(templateRole) {
|
|
96
|
+
const highDensityRoles = new Set(["checkout", "cart", "configurator-bom", "order-detail"]);
|
|
97
|
+
const mediumDensityRoles = new Set(["product-detail", "category", "search", "order-history", "account-info"]);
|
|
98
|
+
if (highDensityRoles.has(templateRole)) return "high";
|
|
99
|
+
if (mediumDensityRoles.has(templateRole)) return "medium";
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sectionMarkdown(sections) {
|
|
104
|
+
if (!Array.isArray(sections) || sections.length === 0) {
|
|
105
|
+
return `- ${needs("SECTIONS")}`;
|
|
106
|
+
}
|
|
107
|
+
return sections
|
|
108
|
+
.map((section, index) => {
|
|
109
|
+
const title = escapeMarkdown(section.title || `Section ${index + 1}`);
|
|
110
|
+
const details = [];
|
|
111
|
+
if (section.purpose) details.push(`purpose=${escapeMarkdown(section.purpose)}`);
|
|
112
|
+
if (Array.isArray(section.components) && section.components.length > 0) {
|
|
113
|
+
details.push(`components=${section.components.map(escapeMarkdown).join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(section.data_slots) && section.data_slots.length > 0) {
|
|
116
|
+
details.push(`data=${section.data_slots.map(escapeMarkdown).join(", ")}`);
|
|
117
|
+
}
|
|
118
|
+
if (Array.isArray(section.states) && section.states.length > 0) {
|
|
119
|
+
details.push(`states=${section.states.map(escapeMarkdown).join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
return `- ${title}${details.length > 0 ? ` (${details.join("; ")})` : ""}`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function flattenSectionValues(sections, key) {
|
|
127
|
+
if (!Array.isArray(sections)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return uniqueStrings(sections.flatMap((section) => cleanArray(section && section[key])));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function profileRoleHints(profilePrimitives, templateRole, screenId) {
|
|
134
|
+
if (!profilePrimitives || !profilePrimitives.screenRoleHints) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const hints = profilePrimitives.screenRoleHints[screenId] || profilePrimitives.screenRoleHints[templateRole];
|
|
138
|
+
return cleanArray(hints);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildBriefModel({ screen, profileSelection, profilePrimitives, sourceArtifacts }) {
|
|
142
|
+
const templateRole = screen.templateRole || "generic";
|
|
143
|
+
const sections = Array.isArray(screen.sections) ? screen.sections : [];
|
|
144
|
+
const sectionComponents = flattenSectionValues(sections, "components");
|
|
145
|
+
const sectionStates = flattenSectionValues(sections, "states");
|
|
146
|
+
const sectionInteractions = flattenSectionValues(sections, "interactions");
|
|
147
|
+
const sectionDataSlots = flattenSectionValues(sections, "data_slots");
|
|
148
|
+
const componentHints = profileRoleHints(profilePrimitives, templateRole, screen.screenId);
|
|
149
|
+
const components = uniqueStrings([...(screen.requiredComponents || []), ...sectionComponents, ...componentHints]);
|
|
150
|
+
const states = uniqueStrings([...(screen.requiredStates || []), ...sectionStates]);
|
|
151
|
+
const interactions = uniqueStrings([
|
|
152
|
+
...(screen.interactions || []),
|
|
153
|
+
...sectionInteractions,
|
|
154
|
+
...(screen.primaryAction ? [`Primary action: ${screen.primaryAction}`] : []),
|
|
155
|
+
]);
|
|
156
|
+
const dataSlots = uniqueStrings([...(screen.dataSlots || []), ...sectionDataSlots]);
|
|
157
|
+
const sourceEvidence = screen.sourceEvidence || {};
|
|
158
|
+
const supportingArtifacts = cleanArray(sourceArtifacts);
|
|
159
|
+
const model = {
|
|
160
|
+
schema: "sdtk.design.screen-brief.v2",
|
|
161
|
+
screen_id: screen.screenId,
|
|
162
|
+
screen_title: screen.title,
|
|
163
|
+
purpose: screen.purpose || needs("SCREEN_PURPOSE"),
|
|
164
|
+
user_intent: screen.userIntent || needs("USER_INTENT"),
|
|
165
|
+
route: screen.route || needs("ROUTE"),
|
|
166
|
+
sections,
|
|
167
|
+
section_titles: sections.map((section) => section.title).filter(Boolean),
|
|
168
|
+
layout_model: screen.layoutModel || layoutModelForRole(templateRole) || needs("LAYOUT_MODEL"),
|
|
169
|
+
density: screen.density || densityForRole(templateRole) || needs("DENSITY"),
|
|
170
|
+
components: components.length > 0 ? components : [needs("COMPONENTS")],
|
|
171
|
+
states: states.length > 0 ? states : [needs("STATES")],
|
|
172
|
+
interactions: interactions.length > 0 ? interactions : [needs("INTERACTIONS")],
|
|
173
|
+
data_slots: dataSlots.length > 0 ? dataSlots : [needs("DATA_SLOTS")],
|
|
174
|
+
accessibility_notes:
|
|
175
|
+
cleanArray(screen.accessibilityNotes).length > 0
|
|
176
|
+
? cleanArray(screen.accessibilityNotes)
|
|
177
|
+
: [
|
|
178
|
+
"Use one screen-level heading before nested section headings.",
|
|
179
|
+
"Expose keyboard focus order for navigation, filters, forms, and primary actions.",
|
|
180
|
+
"Keep contrast and touch target sizing aligned with DESIGN_TOKENS guidance.",
|
|
181
|
+
],
|
|
182
|
+
visual_token_usage: tokenUsageOrDefault(screen.visualTokenUsage),
|
|
183
|
+
acceptance_criteria:
|
|
184
|
+
cleanArray(screen.acceptanceCriteria).length > 0
|
|
185
|
+
? cleanArray(screen.acceptanceCriteria)
|
|
186
|
+
: [needs("ACCEPTANCE_CRITERIA")],
|
|
187
|
+
template_role: templateRole,
|
|
188
|
+
source_evidence: {
|
|
189
|
+
source_artifact: sourceEvidence.source_artifact || screen.sourceArtifact || null,
|
|
190
|
+
source_status: sourceEvidence.source_status || screen.sourceStatus || "explicit",
|
|
191
|
+
source_quote: sourceEvidence.source_quote || needs("SOURCE_QUOTE"),
|
|
192
|
+
source_line_range: Array.isArray(sourceEvidence.source_line_range)
|
|
193
|
+
? sourceEvidence.source_line_range
|
|
194
|
+
: null,
|
|
195
|
+
supporting_artifacts: supportingArtifacts,
|
|
196
|
+
},
|
|
197
|
+
profile: profileSelection || "generic",
|
|
198
|
+
confidence: screen.confidence || "low",
|
|
199
|
+
};
|
|
200
|
+
model.gaps = [
|
|
201
|
+
model.purpose,
|
|
202
|
+
model.user_intent,
|
|
203
|
+
model.route,
|
|
204
|
+
model.layout_model,
|
|
205
|
+
model.density,
|
|
206
|
+
...model.components,
|
|
207
|
+
...model.states,
|
|
208
|
+
...model.interactions,
|
|
209
|
+
...model.data_slots,
|
|
210
|
+
...model.acceptance_criteria,
|
|
211
|
+
model.source_evidence.source_quote,
|
|
212
|
+
].filter((value) => String(value).startsWith("NEEDS_"));
|
|
213
|
+
return model;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderScreenBrief({ screen, profileSelection, profilePrimitives, sourceArtifacts }) {
|
|
217
|
+
const model = buildBriefModel({ screen, profileSelection, profilePrimitives, sourceArtifacts });
|
|
218
|
+
|
|
219
|
+
return `# Screen Design Brief: ${escapeMarkdown(screen.title)}
|
|
220
|
+
|
|
221
|
+
## Screen Meta
|
|
222
|
+
- Screen ID: ${escapeMarkdown(screen.screenId)}
|
|
223
|
+
- Template role: ${escapeMarkdown(model.template_role)}
|
|
224
|
+
- Profile: ${escapeMarkdown(profileSelection || "none")}
|
|
225
|
+
- Confidence: ${escapeMarkdown(model.confidence)}
|
|
226
|
+
|
|
227
|
+
## Screen Purpose And User Intent
|
|
228
|
+
- Purpose: ${escapeMarkdown(model.purpose)}
|
|
229
|
+
- User intent: ${escapeMarkdown(model.user_intent)}
|
|
230
|
+
|
|
231
|
+
## Route
|
|
232
|
+
- Route: ${escapeMarkdown(model.route)}
|
|
233
|
+
|
|
234
|
+
## Required Sections Top To Bottom
|
|
235
|
+
${sectionMarkdown(model.sections)}
|
|
236
|
+
|
|
237
|
+
## Layout Model And Density
|
|
238
|
+
- Layout model: ${escapeMarkdown(model.layout_model)}
|
|
239
|
+
- Density: ${escapeMarkdown(model.density)}
|
|
240
|
+
- Navigation: preserve deterministic route and section order from explicit screen model.
|
|
241
|
+
|
|
242
|
+
## Component Requirements
|
|
243
|
+
${listOrNeeds(model.components, "COMPONENTS")}
|
|
244
|
+
|
|
245
|
+
## State Requirements
|
|
246
|
+
${listOrNeeds(model.states, "STATES")}
|
|
247
|
+
|
|
248
|
+
## Interaction Notes
|
|
249
|
+
${listOrNeeds(model.interactions, "INTERACTIONS")}
|
|
250
|
+
|
|
251
|
+
## Data / Content Slots
|
|
252
|
+
${listOrNeeds(model.data_slots, "DATA_SLOTS")}
|
|
253
|
+
|
|
254
|
+
## Visual Token Usage
|
|
255
|
+
${tokenUsageMarkdown(model.visual_token_usage)}
|
|
256
|
+
|
|
257
|
+
## Accessibility Notes
|
|
258
|
+
${listOrNeeds(model.accessibility_notes, "ACCESSIBILITY_NOTES")}
|
|
259
|
+
|
|
260
|
+
## Acceptance Criteria
|
|
261
|
+
${listOrNeeds(model.acceptance_criteria, "ACCEPTANCE_CRITERIA")}
|
|
262
|
+
|
|
263
|
+
## Source Evidence / Confidence
|
|
264
|
+
- Source artifact: ${escapeMarkdown(model.source_evidence.source_artifact || needs("SOURCE_ARTIFACT"))}
|
|
265
|
+
- Source status: ${escapeMarkdown(model.source_evidence.source_status)}
|
|
266
|
+
- Source quote: ${escapeMarkdown(model.source_evidence.source_quote)}
|
|
267
|
+
- Source line range: ${Array.isArray(model.source_evidence.source_line_range) ? model.source_evidence.source_line_range.join("-") : needs("SOURCE_LINE_RANGE")}
|
|
268
|
+
- Confidence level: ${escapeMarkdown(model.confidence)}
|
|
269
|
+
- Supporting explicit artifacts:
|
|
270
|
+
${model.source_evidence.supporting_artifacts.length > 0 ? model.source_evidence.supporting_artifacts.map((item) => ` - ${escapeMarkdown(item)}`).join("\n") : ` - ${needs("SUPPORTING_ARTIFACTS")}`}
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function writeScreenBriefArtifacts(projectPath, inputContractState) {
|
|
275
|
+
const paths = describeDesignPaths(projectPath);
|
|
276
|
+
if (
|
|
277
|
+
!inputContractState ||
|
|
278
|
+
inputContractState.analysisStatus !== "INPUT_CONTRACT_READY" ||
|
|
279
|
+
!inputContractState.screenModel ||
|
|
280
|
+
!Array.isArray(inputContractState.screenModel.screens) ||
|
|
281
|
+
inputContractState.screenModel.screens.length === 0
|
|
282
|
+
) {
|
|
283
|
+
throw new ValidationError(
|
|
284
|
+
"Screen brief generation requires INPUT_CONTRACT_READY with at least one explicit screen model entry."
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fs.mkdirSync(paths.screensPath, { recursive: true });
|
|
289
|
+
fs.mkdirSync(paths.designScreenBriefsStatePath, { recursive: true });
|
|
290
|
+
|
|
291
|
+
const sourceArtifacts = Object.values(inputContractState.artifacts || {})
|
|
292
|
+
.filter((artifact) => artifact && artifact.found && artifact.relativeToSpecRoot)
|
|
293
|
+
.map((artifact) => artifact.relativeToSpecRoot);
|
|
294
|
+
|
|
295
|
+
const generated = [];
|
|
296
|
+
for (const screen of inputContractState.screenModel.screens) {
|
|
297
|
+
const mdFileName = toScreenBriefFileName(screen.screenId);
|
|
298
|
+
const jsonFileName = toJsonFileName(screen.screenId);
|
|
299
|
+
const markdownPath = path.join(paths.screensPath, mdFileName);
|
|
300
|
+
const sidecarPath = path.join(paths.designScreenBriefsStatePath, jsonFileName);
|
|
301
|
+
const markdown = renderScreenBrief({
|
|
302
|
+
screen,
|
|
303
|
+
profileSelection: inputContractState.profileSelection || null,
|
|
304
|
+
profilePrimitives: inputContractState.profilePrimitives || null,
|
|
305
|
+
sourceArtifacts,
|
|
306
|
+
});
|
|
307
|
+
const briefModel = buildBriefModel({
|
|
308
|
+
screen,
|
|
309
|
+
profileSelection: inputContractState.profileSelection || null,
|
|
310
|
+
profilePrimitives: inputContractState.profilePrimitives || null,
|
|
311
|
+
sourceArtifacts,
|
|
312
|
+
});
|
|
313
|
+
const sidecar = {
|
|
314
|
+
...briefModel,
|
|
315
|
+
primary_action: screen.primaryAction || null,
|
|
316
|
+
required_states: briefModel.states,
|
|
317
|
+
major_sections: briefModel.section_titles,
|
|
318
|
+
profile: briefModel.profile,
|
|
319
|
+
confidence: briefModel.confidence,
|
|
320
|
+
source_artifact: screen.sourceArtifact || null,
|
|
321
|
+
};
|
|
322
|
+
fs.writeFileSync(markdownPath, `${markdown}\n`, "utf-8");
|
|
323
|
+
fs.writeFileSync(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`, "utf-8");
|
|
324
|
+
generated.push({
|
|
325
|
+
screenId: screen.screenId,
|
|
326
|
+
markdownRelativePath: path.relative(projectPath, markdownPath).split(path.sep).join("/"),
|
|
327
|
+
sidecarRelativePath: path.relative(projectPath, sidecarPath).split(path.sep).join("/"),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
generatedCount: generated.length,
|
|
333
|
+
generated,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = {
|
|
338
|
+
buildBriefModel,
|
|
339
|
+
writeScreenBriefArtifacts,
|
|
340
|
+
};
|