@getcoherent/cli 0.5.8 → 0.5.9
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/dist/index.js +154 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3613,6 +3613,16 @@ ANTI-PATTERNS (NEVER DO):
|
|
|
3613
3613
|
- Mixing link styles on the same page \u2192 pick ONE style
|
|
3614
3614
|
- Interactive elements without hover/focus states
|
|
3615
3615
|
|
|
3616
|
+
COMPONENT VARIANT RULES (CRITICAL):
|
|
3617
|
+
- NEVER use <Button> with custom bg-*/text-* classes for navigation or tabs without variant="ghost".
|
|
3618
|
+
The default Button variant sets bg-primary, so custom text-muted-foreground or bg-accent classes will conflict.
|
|
3619
|
+
BAD: <Button className="text-muted-foreground hover:bg-accent">Tab</Button>
|
|
3620
|
+
GOOD: <Button variant="ghost" className="text-muted-foreground">Tab</Button>
|
|
3621
|
+
BEST: Use shadcn <Tabs> / <TabsList> / <TabsTrigger> for tab-style navigation.
|
|
3622
|
+
- For sidebar navigation buttons, ALWAYS use variant="ghost" with active-state classes:
|
|
3623
|
+
<Button variant="ghost" className={cn("w-full justify-start", isActive && "bg-accent font-medium")}>
|
|
3624
|
+
- For filter toggle buttons, use variant={isActive ? 'default' : 'outline'} \u2014 NOT className toggling.
|
|
3625
|
+
|
|
3616
3626
|
CONTENT (zero placeholders):
|
|
3617
3627
|
- NEVER: "Lorem ipsum", "Card content", "Description here"
|
|
3618
3628
|
- ALWAYS: Real, contextual content. Realistic metric names, values, dates.
|
|
@@ -5060,6 +5070,121 @@ function fixGlobalsCss(projectRoot, config2) {
|
|
|
5060
5070
|
writeFileSync7(layoutPath, layoutContent, "utf-8");
|
|
5061
5071
|
}
|
|
5062
5072
|
|
|
5073
|
+
// src/utils/component-rules.ts
|
|
5074
|
+
function extractJsxElementProps(code, openTagStart) {
|
|
5075
|
+
let i = openTagStart;
|
|
5076
|
+
if (code[i] !== "<") return null;
|
|
5077
|
+
i++;
|
|
5078
|
+
let braceDepth = 0;
|
|
5079
|
+
let inSingleQuote = false;
|
|
5080
|
+
let inDoubleQuote = false;
|
|
5081
|
+
let inTemplateLiteral = false;
|
|
5082
|
+
let escaped = false;
|
|
5083
|
+
while (i < code.length) {
|
|
5084
|
+
const ch = code[i];
|
|
5085
|
+
if (escaped) {
|
|
5086
|
+
escaped = false;
|
|
5087
|
+
i++;
|
|
5088
|
+
continue;
|
|
5089
|
+
}
|
|
5090
|
+
if (ch === "\\" && (inSingleQuote || inDoubleQuote || inTemplateLiteral)) {
|
|
5091
|
+
escaped = true;
|
|
5092
|
+
i++;
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
if (!inDoubleQuote && !inTemplateLiteral && ch === "'" && braceDepth > 0) {
|
|
5096
|
+
inSingleQuote = !inSingleQuote;
|
|
5097
|
+
} else if (!inSingleQuote && !inTemplateLiteral && ch === '"') {
|
|
5098
|
+
inDoubleQuote = !inDoubleQuote;
|
|
5099
|
+
} else if (!inSingleQuote && !inDoubleQuote && ch === "`") {
|
|
5100
|
+
inTemplateLiteral = !inTemplateLiteral;
|
|
5101
|
+
}
|
|
5102
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
5103
|
+
if (ch === "{") braceDepth++;
|
|
5104
|
+
else if (ch === "}") braceDepth--;
|
|
5105
|
+
else if (ch === ">" && braceDepth === 0) {
|
|
5106
|
+
return code.slice(openTagStart, i + 1);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
i++;
|
|
5110
|
+
}
|
|
5111
|
+
return null;
|
|
5112
|
+
}
|
|
5113
|
+
var NAV_STYLE_SIGNAL = /text-muted-foreground/;
|
|
5114
|
+
var buttonMissingGhostVariant = {
|
|
5115
|
+
id: "button-missing-ghost-variant",
|
|
5116
|
+
component: "Button",
|
|
5117
|
+
detect(code) {
|
|
5118
|
+
const issues = [];
|
|
5119
|
+
const buttonRe = /<Button\s/g;
|
|
5120
|
+
let match;
|
|
5121
|
+
while ((match = buttonRe.exec(code)) !== null) {
|
|
5122
|
+
const props = extractJsxElementProps(code, match.index);
|
|
5123
|
+
if (!props) continue;
|
|
5124
|
+
if (/\bvariant\s*=/.test(props)) continue;
|
|
5125
|
+
if (!NAV_STYLE_SIGNAL.test(props)) continue;
|
|
5126
|
+
const line = code.slice(0, match.index).split("\n").length;
|
|
5127
|
+
issues.push({
|
|
5128
|
+
line,
|
|
5129
|
+
type: "BUTTON_MISSING_VARIANT",
|
|
5130
|
+
message: '<Button> with navigation-style classes (text-muted-foreground) but no variant \u2014 add variant="ghost"',
|
|
5131
|
+
severity: "error"
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
return issues;
|
|
5135
|
+
},
|
|
5136
|
+
fix(code) {
|
|
5137
|
+
let result = code;
|
|
5138
|
+
let applied = false;
|
|
5139
|
+
const buttonRe = /<Button\s/g;
|
|
5140
|
+
let match;
|
|
5141
|
+
let offset = 0;
|
|
5142
|
+
while ((match = buttonRe.exec(code)) !== null) {
|
|
5143
|
+
const adjustedIndex = match.index + offset;
|
|
5144
|
+
const props = extractJsxElementProps(result, adjustedIndex);
|
|
5145
|
+
if (!props) continue;
|
|
5146
|
+
if (/\bvariant\s*=/.test(props)) continue;
|
|
5147
|
+
if (!NAV_STYLE_SIGNAL.test(props)) continue;
|
|
5148
|
+
const insertPos = adjustedIndex + "<Button".length;
|
|
5149
|
+
const insertion = "\n" + getIndent(result, adjustedIndex) + ' variant="ghost"';
|
|
5150
|
+
result = result.slice(0, insertPos) + insertion + result.slice(insertPos);
|
|
5151
|
+
offset += insertion.length;
|
|
5152
|
+
applied = true;
|
|
5153
|
+
}
|
|
5154
|
+
return {
|
|
5155
|
+
code: result,
|
|
5156
|
+
applied,
|
|
5157
|
+
description: 'added variant="ghost" to Button with nav-style classes'
|
|
5158
|
+
};
|
|
5159
|
+
}
|
|
5160
|
+
};
|
|
5161
|
+
function getIndent(code, pos) {
|
|
5162
|
+
const lineStart = code.lastIndexOf("\n", pos);
|
|
5163
|
+
const lineContent = code.slice(lineStart + 1, pos);
|
|
5164
|
+
const indentMatch = lineContent.match(/^(\s*)/);
|
|
5165
|
+
return indentMatch ? indentMatch[1] : "";
|
|
5166
|
+
}
|
|
5167
|
+
var rules = [buttonMissingGhostVariant];
|
|
5168
|
+
function detectComponentIssues(code) {
|
|
5169
|
+
const issues = [];
|
|
5170
|
+
for (const rule of rules) {
|
|
5171
|
+
issues.push(...rule.detect(code));
|
|
5172
|
+
}
|
|
5173
|
+
return issues;
|
|
5174
|
+
}
|
|
5175
|
+
function applyComponentRules(code) {
|
|
5176
|
+
const fixes = [];
|
|
5177
|
+
let result = code;
|
|
5178
|
+
for (const rule of rules) {
|
|
5179
|
+
const { code: fixed, applied, description } = rule.fix(result);
|
|
5180
|
+
if (applied) {
|
|
5181
|
+
result = fixed;
|
|
5182
|
+
fixes.push(description);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
return { code: result, fixes };
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5063
5188
|
// src/utils/quality-validator.ts
|
|
5064
5189
|
var RAW_COLOR_RE = /(?:(?:hover|focus|active|group-hover|focus-visible|focus-within):)?(?:bg|text|border|ring|outline|from|to|via)-(gray|blue|red|green|yellow|purple|pink|indigo|orange|slate|zinc|stone|neutral|emerald|teal|cyan|sky|violet|fuchsia|rose|amber|lime)-\d+/g;
|
|
5065
5190
|
var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
|
|
@@ -5482,6 +5607,7 @@ function validatePageQuality(code, validRoutes) {
|
|
|
5482
5607
|
severity: "error"
|
|
5483
5608
|
});
|
|
5484
5609
|
}
|
|
5610
|
+
issues.push(...detectComponentIssues(code));
|
|
5485
5611
|
return issues;
|
|
5486
5612
|
}
|
|
5487
5613
|
function replaceRawColors(classes, colorMap) {
|
|
@@ -5919,6 +6045,11 @@ ${selectImport}`
|
|
|
5919
6045
|
if (fixed !== beforeLinkFix) {
|
|
5920
6046
|
fixes.push("Link>Button \u2192 Button asChild>Link (DOM nesting fix)");
|
|
5921
6047
|
}
|
|
6048
|
+
const { code: fixedByRules, fixes: ruleFixes } = applyComponentRules(fixed);
|
|
6049
|
+
if (ruleFixes.length > 0) {
|
|
6050
|
+
fixed = fixedByRules;
|
|
6051
|
+
fixes.push(...ruleFixes);
|
|
6052
|
+
}
|
|
5922
6053
|
fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
|
|
5923
6054
|
const cleaned = inner.replace(/\s{2,}/g, " ").trim();
|
|
5924
6055
|
return `className="${cleaned}"`;
|
|
@@ -6944,7 +7075,29 @@ function extractAppNameFromPrompt(prompt) {
|
|
|
6944
7075
|
const m = prompt.match(re);
|
|
6945
7076
|
if (m && m[1] && m[1].length >= 2 && m[1].length <= 30) {
|
|
6946
7077
|
const name = m[1].replace(/[.,;:!?]$/, "");
|
|
6947
|
-
const skip = /* @__PURE__ */ new Set([
|
|
7078
|
+
const skip = /* @__PURE__ */ new Set([
|
|
7079
|
+
"a",
|
|
7080
|
+
"an",
|
|
7081
|
+
"the",
|
|
7082
|
+
"my",
|
|
7083
|
+
"our",
|
|
7084
|
+
"new",
|
|
7085
|
+
"full",
|
|
7086
|
+
"complete",
|
|
7087
|
+
"simple",
|
|
7088
|
+
"modern",
|
|
7089
|
+
"beautiful",
|
|
7090
|
+
"responsive",
|
|
7091
|
+
"fast",
|
|
7092
|
+
"cool",
|
|
7093
|
+
"great",
|
|
7094
|
+
"basic",
|
|
7095
|
+
"quick",
|
|
7096
|
+
"small",
|
|
7097
|
+
"large",
|
|
7098
|
+
"custom",
|
|
7099
|
+
"nice"
|
|
7100
|
+
]);
|
|
6948
7101
|
if (skip.has(name.toLowerCase())) continue;
|
|
6949
7102
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
6950
7103
|
}
|