mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 06:16:42 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
f0d170ee5c
commit
36c79de28b
2 changed files with 154 additions and 1 deletions
143
eslint-rules/no-hardcoded-tailwind-colors.js
Normal file
143
eslint-rules/no-hardcoded-tailwind-colors.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* ESLint rule: no-hardcoded-tailwind-colors
|
||||
*
|
||||
* Blocks Tailwind color utilities that reference Tailwind's default color
|
||||
* palette (e.g. `bg-emerald-500`, `text-blue-600`, `border-red-300/40`,
|
||||
* `ring-amber-500`) inside JSX className strings. This forces usage of the
|
||||
* semantic design tokens declared in `src/styles.css`
|
||||
* (e.g. `bg-success`, `text-destructive`, `border-warning/30`).
|
||||
*
|
||||
* Allowed:
|
||||
* - Neutral palette: `white`, `black`, `transparent`, `current`, `inherit`
|
||||
* - Semantic tokens: primary, secondary, accent, muted, destructive,
|
||||
* success, warning, info, foreground, background, card, popover, border,
|
||||
* input, ring, sidebar, chart-*, surface
|
||||
*
|
||||
* Disallowed example: `bg-emerald-500/15`, `text-blue-600`, `border-amber-400`
|
||||
* Suggested replacement: `bg-success/15`, `text-info`, `border-warning`
|
||||
*/
|
||||
|
||||
const TAILWIND_COLOR_NAMES = [
|
||||
"slate", "gray", "zinc", "neutral", "stone",
|
||||
"red", "orange", "amber", "yellow", "lime",
|
||||
"green", "emerald", "teal", "cyan", "sky",
|
||||
"blue", "indigo", "violet", "purple", "fuchsia",
|
||||
"pink", "rose",
|
||||
];
|
||||
|
||||
// Utility prefixes that take a color value
|
||||
const COLOR_UTILITY_PREFIXES = [
|
||||
"bg", "text", "border", "ring", "outline", "divide",
|
||||
"from", "via", "to", "fill", "stroke", "shadow",
|
||||
"accent", "caret", "decoration", "placeholder",
|
||||
];
|
||||
|
||||
// Build regex that matches things like:
|
||||
// bg-emerald-500
|
||||
// text-blue-600/40
|
||||
// hover:border-red-300
|
||||
// dark:focus:bg-amber-500/20
|
||||
// border-t-blue-500
|
||||
const PREFIX_GROUP = COLOR_UTILITY_PREFIXES.join("|");
|
||||
const COLOR_GROUP = TAILWIND_COLOR_NAMES.join("|");
|
||||
const HARDCODED_COLOR_RE = new RegExp(
|
||||
// Optional variants like `hover:`, `dark:`, `md:` etc. (any number)
|
||||
`(?:^|\\s)(?:[a-z0-9-]+:)*` +
|
||||
// Utility prefix, optional side suffix like `-t`, `-x`, `-l`
|
||||
`(?:${PREFIX_GROUP})(?:-[trblxy])?` +
|
||||
// The forbidden color name
|
||||
`-(?:${COLOR_GROUP})` +
|
||||
// Required shade (50, 100..900, 950)
|
||||
`-(?:50|100|200|300|400|500|600|700|800|900|950)` +
|
||||
// Optional opacity modifier `/40`, `/[0.5]`
|
||||
`(?:\\/[\\w.[\\]]+)?` +
|
||||
`(?=\\s|$)`,
|
||||
"g",
|
||||
);
|
||||
|
||||
function checkValue(context, node, value) {
|
||||
if (typeof value !== "string" || !value) return;
|
||||
const matches = value.match(HARDCODED_COLOR_RE);
|
||||
if (!matches) return;
|
||||
for (const m of matches) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
`Hardcoded Tailwind color "${m.trim()}" is not allowed. ` +
|
||||
`Use a semantic token from src/styles.css (e.g. bg-success, text-destructive, border-warning/30).`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
const rule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow hardcoded Tailwind palette colors in className; require semantic design tokens.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
JSXAttribute(node) {
|
||||
if (!node.name || node.name.name !== "className") return;
|
||||
const v = node.value;
|
||||
if (!v) return;
|
||||
if (v.type === "Literal") {
|
||||
checkValue(context, v, v.value);
|
||||
} else if (v.type === "JSXExpressionContainer") {
|
||||
walkExpression(context, v.expression);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function walkExpression(context, expr) {
|
||||
if (!expr) return;
|
||||
switch (expr.type) {
|
||||
case "Literal":
|
||||
checkValue(context, expr, expr.value);
|
||||
break;
|
||||
case "TemplateLiteral":
|
||||
for (const q of expr.quasis) checkValue(context, q, q.value.cooked);
|
||||
for (const e of expr.expressions) walkExpression(context, e);
|
||||
break;
|
||||
case "ConditionalExpression":
|
||||
walkExpression(context, expr.consequent);
|
||||
walkExpression(context, expr.alternate);
|
||||
break;
|
||||
case "LogicalExpression":
|
||||
case "BinaryExpression":
|
||||
walkExpression(context, expr.left);
|
||||
walkExpression(context, expr.right);
|
||||
break;
|
||||
case "ArrayExpression":
|
||||
for (const el of expr.elements) walkExpression(context, el);
|
||||
break;
|
||||
case "ObjectExpression":
|
||||
for (const p of expr.properties) {
|
||||
if (p.type === "Property") {
|
||||
// Tailwind/cn style: { "bg-red-500": isActive }
|
||||
if (p.key && p.key.type === "Literal") checkValue(context, p.key, p.key.value);
|
||||
walkExpression(context, p.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "CallExpression":
|
||||
// cn(...), clsx(...), cva(...) — inspect every argument
|
||||
for (const a of expr.arguments) walkExpression(context, a);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
rules: {
|
||||
"no-hardcoded-tailwind-colors": rule,
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue