From 36c79de28b2daea8d1bc7bc6737adf9fe4e3e7a8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:10:54 +0000 Subject: [PATCH 1/3] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- eslint-rules/no-hardcoded-tailwind-colors.js | 143 +++++++++++++++++++ eslint.config.js | 12 +- 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 eslint-rules/no-hardcoded-tailwind-colors.js diff --git a/eslint-rules/no-hardcoded-tailwind-colors.js b/eslint-rules/no-hardcoded-tailwind-colors.js new file mode 100644 index 0000000..0ea980d --- /dev/null +++ b/eslint-rules/no-hardcoded-tailwind-colors.js @@ -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, + }, +}; diff --git a/eslint.config.js b/eslint.config.js index 62968bb..321b866 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,9 +4,10 @@ import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; +import mikaDesignSystem from "./eslint-rules/no-hardcoded-tailwind-colors.js"; export default tseslint.config( - { ignores: ["dist", ".output", ".vinxi"] }, + { ignores: ["dist", ".output", ".vinxi", "eslint-rules/**"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ["**/*.{ts,tsx}"], @@ -17,11 +18,20 @@ export default tseslint.config( plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, + "mika-design-system": mikaDesignSystem, }, rules: { ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "@typescript-eslint/no-unused-vars": "off", + "mika-design-system/no-hardcoded-tailwind-colors": "error", + }, + }, + // UI primitives (shadcn) keep their original palette utilities — exempt them. + { + files: ["src/components/ui/**/*.{ts,tsx}"], + rules: { + "mika-design-system/no-hardcoded-tailwind-colors": "off", }, }, eslintPluginPrettier, From 4c2cbed9a20a2780c714f4426bcbf417aa4dcfaf Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:11:37 +0000 Subject: [PATCH 2/3] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/skills/SkillTestPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/mika/skills/SkillTestPanel.tsx b/src/components/mika/skills/SkillTestPanel.tsx index 79bb869..98d9ddb 100644 --- a/src/components/mika/skills/SkillTestPanel.tsx +++ b/src/components/mika/skills/SkillTestPanel.tsx @@ -199,7 +199,7 @@ export function SkillTestPanel({ variant="outline" className={ run.status === "success" - ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border-emerald-500/30" + ? "bg-success/15 text-success border-success/30" : "bg-destructive/15 text-destructive border-destructive/30" } > From d1130dd55b499b9bdd9fa49c0ad75c2b753623f8 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:11:44 +0000 Subject: [PATCH 3/3] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/skills/AgentProvisioningState.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/mika/skills/AgentProvisioningState.tsx b/src/components/mika/skills/AgentProvisioningState.tsx index 062094b..1e0d12a 100644 --- a/src/components/mika/skills/AgentProvisioningState.tsx +++ b/src/components/mika/skills/AgentProvisioningState.tsx @@ -5,8 +5,8 @@ import { Hourglass } from "lucide-react"; export function AgentProvisioningState() { return (