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
package/src/commands/start.js
CHANGED
|
@@ -11,7 +11,7 @@ const { describeDesignPaths, resolveProjectPath } = require("../lib/design-paths
|
|
|
11
11
|
const { availableProfileNames } = require("../lib/design-profiles");
|
|
12
12
|
const { buildInputContractState, writeInputContractState } = require("../lib/design-input-contract");
|
|
13
13
|
const { writeScreenBriefArtifacts } = require("../lib/screen-briefs");
|
|
14
|
-
const { writeComponentContractArtifacts } = require("../lib/component-contract");
|
|
14
|
+
const { writeComponentContractArtifacts, loadBrandTokensV2, loadProjectFactsV1 } = require("../lib/component-contract");
|
|
15
15
|
const { ValidationError } = require("../lib/errors");
|
|
16
16
|
const { DEFAULT_STYLE, availableStyleNames, resolveStyleName } = require("../lib/style-presets");
|
|
17
17
|
|
|
@@ -25,6 +25,8 @@ const START_FLAG_DEFS = {
|
|
|
25
25
|
"design-brief": { type: "string" },
|
|
26
26
|
"reference-dir": { type: "string" },
|
|
27
27
|
profile: { type: "string" },
|
|
28
|
+
"brand-tokens": { type: "string" },
|
|
29
|
+
"project-facts": { type: "string" },
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
const REQUIRED_WIREFRAME_FILES = ["LANDING.md", "ONBOARDING.md", "DASHBOARD.md"];
|
|
@@ -34,12 +36,14 @@ function cmdStartHelp() {
|
|
|
34
36
|
|
|
35
37
|
Usage:
|
|
36
38
|
sdtk-design start --idea "<rough MVP idea>" [--style <preset>] [--project-path <path>] [--force]
|
|
37
|
-
sdtk-design start --from-spec <projectPath> [--design-brief <file>] [--reference-dir <dir>] [--profile <name>] [--project-path <path>]
|
|
39
|
+
sdtk-design start --from-spec <projectPath> [--design-brief <file>] [--reference-dir <dir>] [--profile <name>] [--brand-tokens <file>] [--project-facts <file>] [--project-path <path>]
|
|
38
40
|
|
|
39
41
|
Example:
|
|
40
42
|
sdtk-design start --idea "I want to build a lightweight CRM for solo consultants to track leads."
|
|
41
43
|
sdtk-design start --idea "ClientPulse for consultants" --style premium-dashboard
|
|
42
44
|
sdtk-design start --from-spec . --reference-dir ./docs/design/reference-export --profile b2b-commerce
|
|
45
|
+
sdtk-design start --from-spec . --profile b2b-commerce --brand-tokens ./brand-tokens.json
|
|
46
|
+
sdtk-design start --from-spec . --profile b2b-industrial-commerce --project-facts ./facts.json
|
|
43
47
|
|
|
44
48
|
Style presets:
|
|
45
49
|
${availableStyleNames().join(", ")}
|
|
@@ -134,7 +138,12 @@ function runDesignStartFromSpec({
|
|
|
134
138
|
designBrief,
|
|
135
139
|
referenceDir,
|
|
136
140
|
profile,
|
|
141
|
+
brandTokensPath,
|
|
142
|
+
projectFactsPath,
|
|
137
143
|
}) {
|
|
144
|
+
// BK-223 + BK-229: validate explicit inputs BEFORE any artifact write (fail-closed).
|
|
145
|
+
if (brandTokensPath) loadBrandTokensV2(brandTokensPath);
|
|
146
|
+
if (projectFactsPath) loadProjectFactsV1(projectFactsPath);
|
|
138
147
|
const contractState = buildInputContractState({
|
|
139
148
|
fromSpecPath,
|
|
140
149
|
projectPath,
|
|
@@ -147,7 +156,10 @@ function runDesignStartFromSpec({
|
|
|
147
156
|
let componentContract = null;
|
|
148
157
|
if (contractState.analysisStatus === "INPUT_CONTRACT_READY") {
|
|
149
158
|
screenBriefs = writeScreenBriefArtifacts(contractState.projectPath, contractState);
|
|
150
|
-
componentContract = writeComponentContractArtifacts(contractState.projectPath, contractState
|
|
159
|
+
componentContract = writeComponentContractArtifacts(contractState.projectPath, contractState, {
|
|
160
|
+
brandTokensPath: brandTokensPath || null,
|
|
161
|
+
projectFactsPath: projectFactsPath || null,
|
|
162
|
+
});
|
|
151
163
|
}
|
|
152
164
|
return {
|
|
153
165
|
...contractState,
|
|
@@ -162,7 +174,10 @@ function cmdStart(args) {
|
|
|
162
174
|
const { flags } = parseFlags(args || [], START_FLAG_DEFS);
|
|
163
175
|
if (flags.help) return cmdStartHelp();
|
|
164
176
|
|
|
165
|
-
const contractMode = Boolean(
|
|
177
|
+
const contractMode = Boolean(
|
|
178
|
+
flags["from-spec"] || flags["design-brief"] || flags["reference-dir"] ||
|
|
179
|
+
flags.profile || flags["brand-tokens"] || flags["project-facts"]
|
|
180
|
+
);
|
|
166
181
|
if (contractMode) {
|
|
167
182
|
const contractResult = runDesignStartFromSpec({
|
|
168
183
|
fromSpecPath: flags["from-spec"],
|
|
@@ -170,6 +185,8 @@ function cmdStart(args) {
|
|
|
170
185
|
designBrief: flags["design-brief"],
|
|
171
186
|
referenceDir: flags["reference-dir"],
|
|
172
187
|
profile: flags.profile,
|
|
188
|
+
brandTokensPath: flags["brand-tokens"] || null,
|
|
189
|
+
projectFactsPath: flags["project-facts"] || null,
|
|
173
190
|
});
|
|
174
191
|
if (contractResult.blockers.length > 0) {
|
|
175
192
|
throw new ValidationError(
|
|
@@ -216,7 +233,7 @@ function cmdStart(args) {
|
|
|
216
233
|
console.log(`[design] Style: ${result.style}`);
|
|
217
234
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
218
235
|
console.log("[design] No review, handoff, URL, browser, screenshot, vision, network, .sdtk/atlas, or SDTK-WIKI output was used.");
|
|
219
|
-
console.log("[design] Next: sdtk-design
|
|
236
|
+
console.log("[design] Next: sdtk-design handoff");
|
|
220
237
|
return 0;
|
|
221
238
|
}
|
|
222
239
|
|
package/src/commands/status.js
CHANGED
|
@@ -85,13 +85,21 @@ function inspectDesignStatus(projectPath) {
|
|
|
85
85
|
if (inputContractState && inputContractState.mode === "from-spec") {
|
|
86
86
|
if (Array.isArray(inputContractState.blockers) && inputContractState.blockers.length > 0) {
|
|
87
87
|
nextCommand = "Provide missing explicit SPEC/design artifacts and re-run sdtk-design start --from-spec.";
|
|
88
|
-
} else {
|
|
88
|
+
} else if (prototypeMissing) {
|
|
89
89
|
nextCommand = "sdtk-design prototype";
|
|
90
|
+
} else if (!reviewExists) {
|
|
91
|
+
nextCommand = "sdtk-design review --artifact docs/design/prototype/index.html";
|
|
92
|
+
} else if (handoffMissing) {
|
|
93
|
+
nextCommand = "sdtk-design handoff";
|
|
94
|
+
} else {
|
|
95
|
+
nextCommand = "SDTK-CODE can consume docs/design/DESIGN_HANDOFF.md";
|
|
90
96
|
}
|
|
91
97
|
} else if (coreMissing.length > 0) {
|
|
92
98
|
nextCommand = 'sdtk-design start --idea "<idea>"';
|
|
99
|
+
} else if (prototypeMissing && handoffMissing) {
|
|
100
|
+
nextCommand = "sdtk-design handoff";
|
|
93
101
|
} else if (prototypeMissing) {
|
|
94
|
-
nextCommand = "sdtk-design prototype";
|
|
102
|
+
nextCommand = "Run sdtk-design start --from-spec with explicit design inputs before sdtk-design prototype.";
|
|
95
103
|
} else if (!reviewExists) {
|
|
96
104
|
nextCommand = "sdtk-design review --artifact docs/design/prototype/index.html";
|
|
97
105
|
} else if (handoffMissing) {
|
package/src/commands/system.js
CHANGED
|
@@ -18,17 +18,18 @@ function cmdSystemHelp() {
|
|
|
18
18
|
console.log(`SDTK-DESIGN System
|
|
19
19
|
|
|
20
20
|
Usage:
|
|
21
|
-
sdtk-design system [--
|
|
21
|
+
sdtk-design system [--project-path <path>] [--force]
|
|
22
22
|
|
|
23
23
|
Examples:
|
|
24
24
|
sdtk-design system
|
|
25
|
-
sdtk-design system --
|
|
25
|
+
sdtk-design system --force
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Reads (token-driven path, from-spec flow):
|
|
28
|
+
docs/design/DESIGN_TOKENS.json (schema sdtk.design.tokens.v2 from start --from-spec)
|
|
29
|
+
docs/design/DESIGN_BRIEF.md
|
|
30
|
+
docs/design/SCREEN_MAP.md
|
|
30
31
|
|
|
31
|
-
Reads:
|
|
32
|
+
Reads (legacy path, start --idea flow):
|
|
32
33
|
docs/design/DESIGN_BRIEF.md
|
|
33
34
|
docs/design/SCREEN_MAP.md
|
|
34
35
|
|
|
@@ -44,6 +45,7 @@ Safety:
|
|
|
44
45
|
return 0;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// Legacy preset-driven path (used when DESIGN_TOKENS.json is absent — start --idea flow).
|
|
47
49
|
function includesAny(text, terms) {
|
|
48
50
|
const value = text.toLowerCase();
|
|
49
51
|
return terms.some((term) => value.includes(term));
|
|
@@ -53,8 +55,9 @@ function linesForBullets(items) {
|
|
|
53
55
|
return items.map((item) => `- ${item}`);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function systemContent(briefContent, screenMapContent, style
|
|
57
|
-
const
|
|
58
|
+
function systemContent(briefContent, screenMapContent, style) {
|
|
59
|
+
const resolvedStyle = style || DEFAULT_STYLE;
|
|
60
|
+
const preset = getStylePreset(resolvedStyle);
|
|
58
61
|
const source = `${briefContent}\n${screenMapContent}`;
|
|
59
62
|
const isCrm = includesAny(source, ["crm", "lead", "follow-up", "pipeline"]);
|
|
60
63
|
const primaryAction = isCrm ? "Add first lead" : "Start first workflow";
|
|
@@ -71,7 +74,7 @@ function systemContent(briefContent, screenMapContent, style = DEFAULT_STYLE) {
|
|
|
71
74
|
"",
|
|
72
75
|
"## Visual Preset",
|
|
73
76
|
"",
|
|
74
|
-
`- Preset: ${resolveStyleName(
|
|
77
|
+
`- Preset: ${resolveStyleName(resolvedStyle)} (${preset.label}).`,
|
|
75
78
|
`- Direction: ${preset.summary}`,
|
|
76
79
|
"- Presets are compact SDTK-DESIGN guidance adapted from reference patterns, not imported runtime code.",
|
|
77
80
|
"",
|
|
@@ -162,6 +165,159 @@ function systemContent(briefContent, screenMapContent, style = DEFAULT_STYLE) {
|
|
|
162
165
|
].join("\n");
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
// Token-driven path (BK-224): 9-section content from DESIGN_TOKENS.json v2.
|
|
169
|
+
function designSystemContentFromTokens({ tokens }) {
|
|
170
|
+
const { brand, palette, typography, section, component, profile } = tokens;
|
|
171
|
+
const state = (palette && palette.state) || {};
|
|
172
|
+
const ramp = (typography && typography.ramp) || {};
|
|
173
|
+
const fontFamily = (typography && typography.fontFamily) || {};
|
|
174
|
+
const radius = (component && component.radius) || {};
|
|
175
|
+
const shadow = (component && component.shadow) || {};
|
|
176
|
+
const borderWidth = (component && component.borderWidth) || {};
|
|
177
|
+
|
|
178
|
+
const stateRow = (stateKey) => {
|
|
179
|
+
const s = state[stateKey];
|
|
180
|
+
return s ? `${s.base} / soft: ${s.soft}` : "N/A";
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const rampRows = Object.entries(ramp).map(
|
|
184
|
+
([level, spec]) => `- ${level}: ${spec.fontSize}px / weight ${spec.fontWeight} / line-height ${spec.lineHeight}`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return [
|
|
188
|
+
"# Design System",
|
|
189
|
+
"",
|
|
190
|
+
"## Visual Theme & Atmosphere",
|
|
191
|
+
"",
|
|
192
|
+
`- Profile: ${profile}`,
|
|
193
|
+
`- Brand mood: ${brand.mood}`,
|
|
194
|
+
`- Density: ${brand.density}`,
|
|
195
|
+
`- Surface: ${palette.surface} (primary background)`,
|
|
196
|
+
`- Surface alt: ${palette.surfaceAlt} (content areas, alternating rows)`,
|
|
197
|
+
`- Primary color: ${palette.primary}`,
|
|
198
|
+
`- Accent color: ${palette.accent}`,
|
|
199
|
+
"- Favor clarity, scannability, and obvious next actions over decorative marketing composition.",
|
|
200
|
+
"- No decorative background gradients, glow layers, or unsourced pattern fills.",
|
|
201
|
+
"",
|
|
202
|
+
"## Color Palette & Roles",
|
|
203
|
+
"",
|
|
204
|
+
`- Primary: ${palette.primary} / hover: ${palette.primaryHover} / soft: ${palette.primarySoft}`,
|
|
205
|
+
`- Accent: ${palette.accent} / hover: ${palette.accentHover} / soft: ${palette.accentSoft}`,
|
|
206
|
+
`- Surface: ${palette.surface}`,
|
|
207
|
+
`- Surface alt: ${palette.surfaceAlt}`,
|
|
208
|
+
`- Surface inverse: ${palette.surfaceInverse}`,
|
|
209
|
+
`- Text primary: ${palette.textPrimary}`,
|
|
210
|
+
`- Text secondary: ${palette.textSecondary}`,
|
|
211
|
+
`- Text muted: ${palette.textMuted}`,
|
|
212
|
+
`- Border: ${palette.border}`,
|
|
213
|
+
`- Border strong: ${palette.borderStrong}`,
|
|
214
|
+
`- State success: ${stateRow("success")}`,
|
|
215
|
+
`- State warning: ${stateRow("warning")}`,
|
|
216
|
+
`- State error: ${stateRow("error")}`,
|
|
217
|
+
`- State info: ${stateRow("info")}`,
|
|
218
|
+
"",
|
|
219
|
+
"## Typography Rules",
|
|
220
|
+
"",
|
|
221
|
+
`- Font family display: ${fontFamily.display || "N/A"}`,
|
|
222
|
+
`- Font family body: ${fontFamily.body || "N/A"}`,
|
|
223
|
+
`- Font family mono: ${fontFamily.mono || "N/A"}`,
|
|
224
|
+
...rampRows,
|
|
225
|
+
"- Do not scale font size with viewport width.",
|
|
226
|
+
"- Keep letter spacing at 0 unless explicitly overridden by brand-tokens.",
|
|
227
|
+
"- One screen-level h1 before any section heading.",
|
|
228
|
+
"",
|
|
229
|
+
"## Component Stylings",
|
|
230
|
+
"",
|
|
231
|
+
`- Card radius: ${radius.card}px`,
|
|
232
|
+
`- Control radius: ${radius.control}px`,
|
|
233
|
+
`- Pill radius: ${radius.pill}px`,
|
|
234
|
+
`- Border width default: ${borderWidth.default}px solid ${palette.border}`,
|
|
235
|
+
`- Border width strong: ${borderWidth.strong}px solid ${palette.borderStrong}`,
|
|
236
|
+
"- Primary button: filled palette.primary, white label, 40px minimum height.",
|
|
237
|
+
"- Secondary button: palette.surface background, palette.border outline, palette.textPrimary label.",
|
|
238
|
+
"- Disabled: 45% opacity, no hover elevation.",
|
|
239
|
+
"- Cards: use only for repeated records or framed tool surfaces; no nested cards.",
|
|
240
|
+
"",
|
|
241
|
+
"## Layout Principles",
|
|
242
|
+
"",
|
|
243
|
+
`- Container max width: ${section.containerMaxWidth}px`,
|
|
244
|
+
`- Content max width: ${section.contentMaxWidth}px`,
|
|
245
|
+
`- Section gap: ${section.sectionGap}px`,
|
|
246
|
+
`- Hero min height: ${section.heroMinHeight}px`,
|
|
247
|
+
`- Grid columns: ${section.gridColumns}`,
|
|
248
|
+
`- Density: ${brand.density}`,
|
|
249
|
+
"- Navigation: deterministic route and section order from explicit screen model.",
|
|
250
|
+
"- Keep fixed-format controls stable with explicit min-height and predictable grid tracks.",
|
|
251
|
+
"",
|
|
252
|
+
"## Depth & Elevation",
|
|
253
|
+
"",
|
|
254
|
+
`- Shadow none: ${shadow.none}`,
|
|
255
|
+
`- Shadow sm: ${shadow.sm}`,
|
|
256
|
+
`- Shadow md: ${shadow.md}`,
|
|
257
|
+
`- Shadow lg: ${shadow.lg}`,
|
|
258
|
+
"- Use shadow.sm for cards; shadow.md for modals and dialogs.",
|
|
259
|
+
"- Do not use heavy drop shadows for decorative layering.",
|
|
260
|
+
"",
|
|
261
|
+
"## Do's & Don'ts",
|
|
262
|
+
"",
|
|
263
|
+
"- Do: use palette token paths (e.g. palette.primary, palette.state.success.base) in component references.",
|
|
264
|
+
"- Do: use typography.ramp token paths for heading hierarchy.",
|
|
265
|
+
"- Do: use section token paths for layout rhythm and container widths.",
|
|
266
|
+
"- Don't: embed raw hex values outside of generated CSS variables.",
|
|
267
|
+
"- Don't: extract brand colors or copy from requirement text.",
|
|
268
|
+
"- Don't: add unsupported decorative backgrounds, blurs, or gradient panels.",
|
|
269
|
+
"- Don't: invent copy, headings, or imagery from requirement text.",
|
|
270
|
+
"- Don't: use placeholder or TBD content in renderer output.",
|
|
271
|
+
"",
|
|
272
|
+
"## Responsive Behavior",
|
|
273
|
+
"",
|
|
274
|
+
`- Max layout width: ${section.containerMaxWidth}px; below this, flow to 100%.`,
|
|
275
|
+
`- Grid: ${section.gridColumns} columns desktop; collapse to 1 column mobile.`,
|
|
276
|
+
`- Section gap: ${section.sectionGap}px desktop; reduce proportionally at tablet/mobile.`,
|
|
277
|
+
"- Typography ramp: use ramp values as-is; do not scale with viewport.",
|
|
278
|
+
"- Navigation: collapse to hamburger or bottom nav at mobile breakpoint.",
|
|
279
|
+
"- Touch targets: minimum 44px by 44px for interactive controls.",
|
|
280
|
+
"",
|
|
281
|
+
"## Renderer Prompt Guide",
|
|
282
|
+
"",
|
|
283
|
+
"- Use token key paths in all component references: palette.primary, typography.ramp.h1, section.heroMinHeight, component.radius.card.",
|
|
284
|
+
"- Enforce one screen-level h1 before any section heading.",
|
|
285
|
+
"- Use semantic HTML landmarks: header, main, nav, section, footer.",
|
|
286
|
+
"- Preserve data/state/component markers from the screen brief; do not invent new ones.",
|
|
287
|
+
"- Avoid placeholder panels; emit NEEDS_* markers for unsourced content.",
|
|
288
|
+
`- Token reference: palette.primary=${palette.primary}, typography.ramp.h1=${ramp.h1 ? ramp.h1.fontSize + "px/" + ramp.h1.fontWeight : "N/A"}, section.heroMinHeight=${section.heroMinHeight}px, component.radius.card=${radius.card}px.`,
|
|
289
|
+
"- State tokens: palette.state.success.base for success states, palette.state.error.base for errors.",
|
|
290
|
+
`- Layout: respect section.containerMaxWidth (${section.containerMaxWidth}px) and section.gridColumns (${section.gridColumns}) from this contract.`,
|
|
291
|
+
"",
|
|
292
|
+
].join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function loadDesignTokensV2ForSystem(projectPath) {
|
|
296
|
+
const paths = describeDesignPaths(projectPath);
|
|
297
|
+
if (!fs.existsSync(paths.designTokensPath)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
let raw;
|
|
301
|
+
try {
|
|
302
|
+
raw = JSON.parse(fs.readFileSync(paths.designTokensPath, "utf-8"));
|
|
303
|
+
} catch (_err) {
|
|
304
|
+
throw new ValidationError(
|
|
305
|
+
"docs/design/DESIGN_TOKENS.json is not valid JSON. Re-run sdtk-design start --from-spec to regenerate. No project files were changed."
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (raw.schema === "sdtk.design.tokens.v1") {
|
|
309
|
+
throw new ValidationError(
|
|
310
|
+
"docs/design/DESIGN_TOKENS.json is schema v1. Re-run sdtk-design start --from-spec to upgrade to v2 tokens. No project files were changed."
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (raw.schema !== "sdtk.design.tokens.v2") {
|
|
314
|
+
throw new ValidationError(
|
|
315
|
+
"docs/design/DESIGN_TOKENS.json has an unrecognized schema. Re-run sdtk-design start --from-spec to regenerate. No project files were changed."
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return raw;
|
|
319
|
+
}
|
|
320
|
+
|
|
165
321
|
function runDesignSystem({ projectPath, force = false, style = DEFAULT_STYLE }) {
|
|
166
322
|
const styleName = resolveStyleName(style);
|
|
167
323
|
const resolvedProjectPath = resolveProjectPath(projectPath || process.cwd());
|
|
@@ -180,16 +336,29 @@ function runDesignSystem({ projectPath, force = false, style = DEFAULT_STYLE })
|
|
|
180
336
|
throw new ValidationError("docs/design/DESIGN_SYSTEM.md already exists. Re-run with --force to replace this managed design system.");
|
|
181
337
|
}
|
|
182
338
|
|
|
183
|
-
|
|
184
|
-
const
|
|
339
|
+
// Try v2 token-driven path; fall back to legacy preset path when tokens are absent.
|
|
340
|
+
const tokens = loadDesignTokensV2ForSystem(resolvedProjectPath);
|
|
341
|
+
|
|
342
|
+
let content;
|
|
343
|
+
let tokenDriven = false;
|
|
344
|
+
if (tokens) {
|
|
345
|
+
content = designSystemContentFromTokens({ tokens });
|
|
346
|
+
tokenDriven = true;
|
|
347
|
+
} else {
|
|
348
|
+
const briefContent = fs.readFileSync(paths.designBriefPath, "utf-8");
|
|
349
|
+
const screenMapContent = fs.readFileSync(paths.screenMapPath, "utf-8");
|
|
350
|
+
content = systemContent(briefContent, screenMapContent, styleName);
|
|
351
|
+
}
|
|
352
|
+
|
|
185
353
|
fs.mkdirSync(path.dirname(paths.designSystemPath), { recursive: true });
|
|
186
|
-
fs.writeFileSync(paths.designSystemPath,
|
|
354
|
+
fs.writeFileSync(paths.designSystemPath, content, "utf-8");
|
|
187
355
|
|
|
188
356
|
return {
|
|
189
357
|
projectPath: resolvedProjectPath,
|
|
190
358
|
relativeDesignSystemPath: "docs/design/DESIGN_SYSTEM.md",
|
|
191
359
|
forced: Boolean(force),
|
|
192
|
-
style: styleName,
|
|
360
|
+
style: tokenDriven ? null : styleName,
|
|
361
|
+
tokenDriven,
|
|
193
362
|
};
|
|
194
363
|
}
|
|
195
364
|
|
|
@@ -204,7 +373,9 @@ function cmdSystem(args) {
|
|
|
204
373
|
});
|
|
205
374
|
|
|
206
375
|
console.log(`[design] Wrote ${result.relativeDesignSystemPath}: ${result.projectPath}`);
|
|
207
|
-
|
|
376
|
+
if (!result.tokenDriven && result.style) {
|
|
377
|
+
console.log(`[design] Style: ${result.style}`);
|
|
378
|
+
}
|
|
208
379
|
console.log(`[design] Overwrite: ${result.forced ? "enabled by --force" : "not needed"}`);
|
|
209
380
|
console.log("[design] No .sdtk/atlas, SDTK-WIKI output, source files, network, or app code was modified.");
|
|
210
381
|
console.log("[design] Next: sdtk-design handoff");
|
|
@@ -216,4 +387,5 @@ module.exports = {
|
|
|
216
387
|
cmdSystemHelp,
|
|
217
388
|
runDesignSystem,
|
|
218
389
|
systemContent,
|
|
390
|
+
designSystemContentFromTokens,
|
|
219
391
|
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function stripHtmlComments(html) {
|
|
4
|
+
return String(html || "").replace(/<!--[\s\S]*?-->/g, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function stripTags(html) {
|
|
8
|
+
return String(html || "")
|
|
9
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
10
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
11
|
+
.replace(/<[^>]+>/g, " ")
|
|
12
|
+
.replace(/\s+/g, " ")
|
|
13
|
+
.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function snippet(value, index = 0, length = 100) {
|
|
17
|
+
const text = stripTags(value);
|
|
18
|
+
if (text) return text.slice(0, length);
|
|
19
|
+
return String(value || "").slice(Math.max(0, index - 30), index + length).replace(/\s+/g, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function finding(severity, id, message, fix, text, screenId) {
|
|
23
|
+
return { severity, id, message, fix, snippet: snippet(text), ...(screenId ? { screenId } : {}) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function addRegexFindings(findings, clean, regex, severity, id, message, fix, screenId) {
|
|
27
|
+
let match;
|
|
28
|
+
const pattern = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
29
|
+
while ((match = pattern.exec(clean))) {
|
|
30
|
+
findings.push(finding(severity, id, message, fix, match[0], screenId));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function articleTexts(clean) {
|
|
35
|
+
const result = [];
|
|
36
|
+
const regex = /<article\b[^>]*>([\s\S]*?)<\/article>/gi;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = regex.exec(clean))) {
|
|
39
|
+
const text = stripTags(match[1]).toLowerCase();
|
|
40
|
+
if (text) result.push(text);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function articleBodyTexts(clean) {
|
|
46
|
+
const result = [];
|
|
47
|
+
const regex = /<article\b[^>]*>([\s\S]*?)<\/article>/gi;
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = regex.exec(clean))) {
|
|
50
|
+
const bodyOnly = match[1].replace(/<h[1-6]\b[\s\S]*?<\/h[1-6]>/i, "");
|
|
51
|
+
const text = stripTags(bodyOnly).toLowerCase().replace(/\s+/g, " ").trim();
|
|
52
|
+
if (text.length >= 20) result.push(text);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function lintArtifactHtml(html, options = {}) {
|
|
58
|
+
const screenId = options.screenId;
|
|
59
|
+
const clean = stripHtmlComments(html);
|
|
60
|
+
const findings = [];
|
|
61
|
+
|
|
62
|
+
addRegexFindings(
|
|
63
|
+
findings,
|
|
64
|
+
clean,
|
|
65
|
+
/\b(line\s*item|product\s*card|material|result\s*item|feature)\s+(one|two|three|[1-9]|\d+)\b|\blorem\s+ipsum\b|\bdolor\s+sit\s+amet\b|\bsample\s+content\b|\bplaceholder\s+text\b/i,
|
|
66
|
+
"P0",
|
|
67
|
+
"filler-copy",
|
|
68
|
+
"Auto-numbered filler copy detected.",
|
|
69
|
+
"Replace auto-numbered filler with brief-derived data_slot placeholders or grey blocks.",
|
|
70
|
+
screenId
|
|
71
|
+
);
|
|
72
|
+
addRegexFindings(
|
|
73
|
+
findings,
|
|
74
|
+
clean,
|
|
75
|
+
/#[ ]*(6366f1|4f46e5|4338ca|3730a3|8b5cf6|7c3aed|a855f7)\b/i,
|
|
76
|
+
"P0",
|
|
77
|
+
"ai-default-indigo",
|
|
78
|
+
"AI-default indigo/purple hex detected.",
|
|
79
|
+
"Replace with var(--primary), var(--accent), or an explicit brand token override.",
|
|
80
|
+
screenId
|
|
81
|
+
);
|
|
82
|
+
addRegexFindings(
|
|
83
|
+
findings,
|
|
84
|
+
clean,
|
|
85
|
+
/<h[1-6]\b[^>]*>[^<]*✨[^<]*<\/h[1-6]>|<button\b[^>]*>[^<]*✨[^<]*<\/button>|<a\b[^>]*class="[^"]*\bbtn\b[^"]*"[^>]*>[^<]*✨[^<]*<\/a>/i,
|
|
86
|
+
"P0",
|
|
87
|
+
"slop-emoji",
|
|
88
|
+
"Decorative emoji used in heading, button, CTA, or icon surface.",
|
|
89
|
+
"Use a 1.6-1.8px stroke monoline SVG with currentColor, or remove the icon.",
|
|
90
|
+
screenId
|
|
91
|
+
);
|
|
92
|
+
addRegexFindings(
|
|
93
|
+
findings,
|
|
94
|
+
clean,
|
|
95
|
+
/<(h[1-6]|button|a\b[^>]*class="[^"]*\bbtn\b[^"]*"|[^>]*class="[^"]*\bicon\b[^"]*")[^>]*>[\s\S]*?(✨|笨ィ|噫|識|笞。|櫨|庁)[\s\S]*?<\/\1>/i,
|
|
96
|
+
"P0",
|
|
97
|
+
"slop-emoji",
|
|
98
|
+
"Decorative emoji used in heading, button, CTA, or icon surface.",
|
|
99
|
+
"Use a 1.6-1.8px stroke monoline SVG with currentColor, or remove the icon.",
|
|
100
|
+
screenId
|
|
101
|
+
);
|
|
102
|
+
addRegexFindings(
|
|
103
|
+
findings,
|
|
104
|
+
clean,
|
|
105
|
+
/\b\d+x\s+(faster|better|easier)\b|\b99\.\d+%\s+uptime\b|\bzero[- ]downtime\b|\b3x\s+more\s+(productive|efficient)\b/i,
|
|
106
|
+
"P0",
|
|
107
|
+
"invented-metric",
|
|
108
|
+
"Unsourced marketing metric detected.",
|
|
109
|
+
"Pull the metric from a real source or remove the claim.",
|
|
110
|
+
screenId
|
|
111
|
+
);
|
|
112
|
+
const counts = new Map();
|
|
113
|
+
for (const text of articleTexts(clean)) counts.set(text, (counts.get(text) || 0) + 1);
|
|
114
|
+
const fullDuplicates = new Set();
|
|
115
|
+
for (const [text, count] of counts.entries()) {
|
|
116
|
+
if (count >= 3) {
|
|
117
|
+
fullDuplicates.add(text);
|
|
118
|
+
findings.push(finding("P0", "duplicate-article-text", "Three or more article nodes share identical text.", "Replace duplicated articles with brief-derived data or a single labelled state card.", text, screenId));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (fullDuplicates.size === 0) {
|
|
122
|
+
const bodyCounts = new Map();
|
|
123
|
+
for (const text of articleBodyTexts(clean)) bodyCounts.set(text, (bodyCounts.get(text) || 0) + 1);
|
|
124
|
+
for (const [text, count] of bodyCounts.entries()) {
|
|
125
|
+
if (count >= 3) {
|
|
126
|
+
findings.push(finding("P1", "near-duplicate-article-body", "Three or more article elements share identical body text while headings differ.", "Pull each article body from a distinct data_slot purpose or remove the repeated paragraph.", text, screenId));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const accentCount = (clean.match(/var\(--(accent|primary)\)/gi) || []).length;
|
|
132
|
+
if (accentCount >= 6) {
|
|
133
|
+
findings.push(finding("P1", "accent-overuse", "Primary/accent token is overused in one page.", "Reduce accent use to primary actions and key state markers.", `accent count ${accentCount}`, screenId));
|
|
134
|
+
}
|
|
135
|
+
const withoutRoot = clean.replace(/:root\s*\{[\s\S]*?\}/gi, "");
|
|
136
|
+
const rawHexCount = (withoutRoot.match(/#[0-9a-fA-F]{3,8}\b/g) || []).length;
|
|
137
|
+
if (rawHexCount > 12) {
|
|
138
|
+
findings.push(finding("P1", "raw-hex-outside-root", "Too many raw hex values outside :root.", "Move colors into design tokens or CSS variables.", `raw hex count ${rawHexCount}`, screenId));
|
|
139
|
+
}
|
|
140
|
+
if (options.tokens && options.tokens.typography && /serif/i.test(String(options.tokens.typography.displayFamily || ""))) {
|
|
141
|
+
addRegexFindings(
|
|
142
|
+
findings,
|
|
143
|
+
clean,
|
|
144
|
+
/<h[1-3]\b[^>]*style="[^"]*font-family:\s*(Inter|Roboto|Arial|system-ui|-apple-system)[^"]*"/i,
|
|
145
|
+
"P1",
|
|
146
|
+
"display-sans-on-serif-binding",
|
|
147
|
+
"Heading overrides serif display token with sans font.",
|
|
148
|
+
"Use the active display typography token for headings.",
|
|
149
|
+
screenId
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
addRegexFindings(
|
|
153
|
+
findings,
|
|
154
|
+
clean,
|
|
155
|
+
/<section\b(?![^>]*(data-section-id|data-slot-id|data-component-id|aria-label|id=))[^>]*>/i,
|
|
156
|
+
"P2",
|
|
157
|
+
"missing-data-od-id",
|
|
158
|
+
"Section is missing a stable target attribute.",
|
|
159
|
+
"Add data-section-id or an analogous stable target attribute.",
|
|
160
|
+
screenId
|
|
161
|
+
);
|
|
162
|
+
addRegexFindings(
|
|
163
|
+
findings,
|
|
164
|
+
clean,
|
|
165
|
+
/<svg\b(?![^>]*class="[^"]*(icon|semantic)[^"]*")[\s\S]*?<path\b[^>]*d="[^"]*Q[^"]*Q[^"]*"/i,
|
|
166
|
+
"P2",
|
|
167
|
+
"decorative-blob-svg",
|
|
168
|
+
"Decorative blob SVG detected.",
|
|
169
|
+
"Use semantic icons or remove decorative blob geometry.",
|
|
170
|
+
screenId
|
|
171
|
+
);
|
|
172
|
+
return findings;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function lintArtifactTree(perScreenHtmlMap, options = {}) {
|
|
176
|
+
const findings = [];
|
|
177
|
+
for (const [screenId, html] of perScreenHtmlMap.entries()) {
|
|
178
|
+
findings.push(...lintArtifactHtml(html, { ...options, screenId }));
|
|
179
|
+
}
|
|
180
|
+
return findings;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function summarizeLintFindings(findings) {
|
|
184
|
+
const summary = { p0: 0, p1: 0, p2: 0, blocking: false };
|
|
185
|
+
for (const item of Array.isArray(findings) ? findings : []) {
|
|
186
|
+
if (item.severity === "P0") summary.p0 += 1;
|
|
187
|
+
if (item.severity === "P1") summary.p1 += 1;
|
|
188
|
+
if (item.severity === "P2") summary.p2 += 1;
|
|
189
|
+
}
|
|
190
|
+
summary.blocking = summary.p0 > 0;
|
|
191
|
+
return summary;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
lintArtifactHtml,
|
|
196
|
+
lintArtifactTree,
|
|
197
|
+
stripHtmlComments,
|
|
198
|
+
summarizeLintFindings,
|
|
199
|
+
};
|