uilint-eslint 0.2.163 → 0.2.164

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.
Files changed (42) hide show
  1. package/dist/index.d.ts +14 -2
  2. package/dist/index.js +458 -59
  3. package/dist/index.js.map +1 -1
  4. package/dist/rules/prefer-tailwind.js +444 -57
  5. package/dist/rules/prefer-tailwind.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/index.ts +14 -2
  8. package/src/rules/prefer-tailwind/index.test.ts +170 -0
  9. package/src/rules/prefer-tailwind/index.ts +604 -77
  10. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/coverage/coverage-final.json +0 -76
  11. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/Component.test.tsx +0 -8
  12. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/Component.tsx +0 -8
  13. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/NonJsxFile.tsx +0 -5
  14. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/WellTestedComponent.test.tsx +0 -8
  15. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/WellTestedComponent.tsx +0 -4
  16. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/useHook.ts +0 -14
  17. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/utils.ts +0 -8
  18. package/src/rules/__fixtures__/coverage/with-chunk-coverage/coverage/coverage-final.json +0 -52
  19. package/src/rules/__fixtures__/coverage/with-chunk-coverage/src/Button.tsx +0 -13
  20. package/src/rules/__fixtures__/coverage/with-chunk-coverage/src/useCounter.ts +0 -14
  21. package/src/rules/__fixtures__/coverage/with-chunk-coverage/src/utils.test.ts +0 -11
  22. package/src/rules/__fixtures__/coverage/with-chunk-coverage/src/utils.ts +0 -19
  23. package/src/rules/__fixtures__/coverage/with-full-coverage/coverage/coverage-final.json +0 -19
  24. package/src/rules/__fixtures__/coverage/with-full-coverage/src/utils.test.ts +0 -15
  25. package/src/rules/__fixtures__/coverage/with-full-coverage/src/utils.ts +0 -15
  26. package/src/rules/__fixtures__/coverage/with-git-changes/coverage/coverage-final.json +0 -22
  27. package/src/rules/__fixtures__/coverage/with-git-changes/src/modified.ts +0 -21
  28. package/src/rules/__fixtures__/coverage/with-jsx-coverage/coverage/coverage-final.json +0 -70
  29. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/ComponentWithHandlers.test.tsx +0 -10
  30. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/ComponentWithHandlers.tsx +0 -34
  31. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/WellTestedComponent.test.tsx +0 -10
  32. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/WellTestedComponent.tsx +0 -16
  33. package/src/rules/__fixtures__/coverage/with-no-coverage-data/src/utils.ts +0 -7
  34. package/src/rules/__fixtures__/coverage/with-no-tests/coverage/coverage-final.json +0 -15
  35. package/src/rules/__fixtures__/coverage/with-no-tests/src/hasTest.test.ts +0 -7
  36. package/src/rules/__fixtures__/coverage/with-no-tests/src/hasTest.ts +0 -7
  37. package/src/rules/__fixtures__/coverage/with-no-tests/src/noTest.ts +0 -11
  38. package/src/rules/__fixtures__/coverage/with-partial-coverage/coverage/coverage-final.json +0 -51
  39. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/covered.test.ts +0 -12
  40. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/covered.ts +0 -11
  41. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/uncovered.test.ts +0 -8
  42. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/uncovered.ts +0 -28
package/dist/index.d.ts CHANGED
@@ -529,7 +529,7 @@ declare const rules: {
529
529
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
530
530
  name: string;
531
531
  };
532
- "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion", [({
532
+ "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion" | "preferSemanticClassGroups" | "semanticOpacityModifier", [({
533
533
  styleRatioThreshold?: number;
534
534
  minElementsForAnalysis?: number;
535
535
  allowedStyleProperties?: string[];
@@ -537,6 +537,12 @@ declare const rules: {
537
537
  preferSemanticColors?: boolean;
538
538
  allowedHardCodedColors?: string[];
539
539
  useLlmSuggestions?: boolean;
540
+ preferSemanticClassGroups?: boolean;
541
+ visualUtilityThreshold?: number;
542
+ visualUtilityMinGroups?: number;
543
+ disallowSemanticOpacityModifiers?: boolean;
544
+ allowedOpacityModifierClasses?: string[];
545
+ allowedVisualUtilityClasses?: string[];
540
546
  } | undefined)?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
541
547
  name: string;
542
548
  };
@@ -649,7 +655,7 @@ declare const plugin: {
649
655
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
650
656
  name: string;
651
657
  };
652
- "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion", [({
658
+ "prefer-tailwind": _typescript_eslint_utils_ts_eslint.RuleModule<"preferTailwind" | "preferSemanticColors" | "preferSemanticColorsWithSuggestion" | "preferSemanticClassGroups" | "semanticOpacityModifier", [({
653
659
  styleRatioThreshold?: number;
654
660
  minElementsForAnalysis?: number;
655
661
  allowedStyleProperties?: string[];
@@ -657,6 +663,12 @@ declare const plugin: {
657
663
  preferSemanticColors?: boolean;
658
664
  allowedHardCodedColors?: string[];
659
665
  useLlmSuggestions?: boolean;
666
+ preferSemanticClassGroups?: boolean;
667
+ visualUtilityThreshold?: number;
668
+ visualUtilityMinGroups?: number;
669
+ disallowSemanticOpacityModifiers?: boolean;
670
+ allowedOpacityModifierClasses?: string[];
671
+ allowedVisualUtilityClasses?: string[];
660
672
  } | undefined)?], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
661
673
  name: string;
662
674
  };
package/dist/index.js CHANGED
@@ -4988,7 +4988,7 @@ function formatSuggestionsForMessage(suggestions, limit = 2) {
4988
4988
  // src/rules/prefer-tailwind/index.ts
4989
4989
  var meta14 = defineRuleMeta({
4990
4990
  id: "prefer-tailwind",
4991
- version: "1.1.0",
4991
+ version: "1.2.0",
4992
4992
  name: "Prefer Tailwind",
4993
4993
  description: "Encourage Tailwind className over inline style attributes",
4994
4994
  defaultSeverity: "warn",
@@ -5005,7 +5005,13 @@ var meta14 = defineRuleMeta({
5005
5005
  ignoreComponents: [],
5006
5006
  preferSemanticColors: true,
5007
5007
  allowedHardCodedColors: [],
5008
- useLlmSuggestions: false
5008
+ useLlmSuggestions: false,
5009
+ preferSemanticClassGroups: true,
5010
+ visualUtilityThreshold: 4,
5011
+ visualUtilityMinGroups: 2,
5012
+ disallowSemanticOpacityModifiers: true,
5013
+ allowedOpacityModifierClasses: [],
5014
+ allowedVisualUtilityClasses: []
5009
5015
  }
5010
5016
  ],
5011
5017
  optionSchema: {
@@ -5058,6 +5064,48 @@ var meta14 = defineRuleMeta({
5058
5064
  type: "boolean",
5059
5065
  defaultValue: false,
5060
5066
  description: "When enabled, uses Ollama to suggest semantic color replacements based on your project's theme"
5067
+ },
5068
+ {
5069
+ key: "preferSemanticClassGroups",
5070
+ label: "Prefer semantic class groups",
5071
+ type: "boolean",
5072
+ defaultValue: true,
5073
+ description: "Warn when one element uses many low-level visual utilities that should be captured by a semantic class"
5074
+ },
5075
+ {
5076
+ key: "visualUtilityThreshold",
5077
+ label: "Visual utility threshold",
5078
+ type: "number",
5079
+ defaultValue: 4,
5080
+ description: "Minimum number of visual utilities in one class string before warning"
5081
+ },
5082
+ {
5083
+ key: "visualUtilityMinGroups",
5084
+ label: "Visual utility group threshold",
5085
+ type: "number",
5086
+ defaultValue: 2,
5087
+ description: "Minimum number of distinct visual utility groups before warning"
5088
+ },
5089
+ {
5090
+ key: "disallowSemanticOpacityModifiers",
5091
+ label: "Disallow semantic opacity modifiers",
5092
+ type: "boolean",
5093
+ defaultValue: true,
5094
+ description: "Warn on token opacity like text-foreground/80 or border-border/40"
5095
+ },
5096
+ {
5097
+ key: "allowedOpacityModifierClasses",
5098
+ label: "Allowed opacity modifier classes",
5099
+ type: "text",
5100
+ defaultValue: "",
5101
+ description: "Comma-separated exact classes to allow with semantic opacity modifiers"
5102
+ },
5103
+ {
5104
+ key: "allowedVisualUtilityClasses",
5105
+ label: "Allowed visual utility classes",
5106
+ type: "text",
5107
+ defaultValue: "",
5108
+ description: "Comma-separated exact visual utility classes to ignore in cluster detection"
5061
5109
  }
5062
5110
  ]
5063
5111
  },
@@ -5108,7 +5156,11 @@ but only when the file exceeds a configurable threshold ratio.
5108
5156
  allowedStyleProperties: ["transform", "animation"], // Skip these properties
5109
5157
  ignoreComponents: ["motion.div", "animated.View"], // Skip animation libraries
5110
5158
  preferSemanticColors: true, // Warn on hard-coded colors like bg-red-500
5111
- allowedHardCodedColors: ["gray", "slate"] // Allow specific color palettes
5159
+ allowedHardCodedColors: ["gray", "slate"], // Allow specific color palettes
5160
+ preferSemanticClassGroups: true,
5161
+ visualUtilityThreshold: 4,
5162
+ visualUtilityMinGroups: 2,
5163
+ disallowSemanticOpacityModifiers: true
5112
5164
  }]
5113
5165
  \`\`\`
5114
5166
 
@@ -5136,6 +5188,39 @@ Semantic colors like \`bg-background\`, \`text-foreground\`, \`bg-primary\`, \`b
5136
5188
 
5137
5189
  Colors that are always allowed: \`white\`, \`black\`, \`transparent\`, \`inherit\`, \`current\`.
5138
5190
 
5191
+ ## Semantic Class Groups
5192
+
5193
+ When \`preferSemanticClassGroups\` is enabled, the rule warns when a single
5194
+ class string combines many low-level visual utilities such as background,
5195
+ border, radius, shadow, gradient, ring/outline, blur, and decoration classes.
5196
+ This catches generated component styling that should usually become a semantic
5197
+ project class such as \`brand-panel\`, \`ui-cell\`, or \`surface-card\`.
5198
+
5199
+ ### \u274C Dense visual utility cluster
5200
+
5201
+ \`\`\`tsx
5202
+ <section className="bg-card rounded-2xl shadow-md border border-border/40" />
5203
+ \`\`\`
5204
+
5205
+ ### \u2705 Semantic class
5206
+
5207
+ \`\`\`tsx
5208
+ <section className="brand-panel" />
5209
+ \`\`\`
5210
+
5211
+ ## Semantic Opacity Modifiers
5212
+
5213
+ When \`disallowSemanticOpacityModifiers\` is enabled, semantic color tokens with
5214
+ opacity suffixes are reported:
5215
+
5216
+ \`\`\`tsx
5217
+ <p className="text-foreground/80" />
5218
+ <div className="border-border/40 hover:bg-accent/50" />
5219
+ \`\`\`
5220
+
5221
+ Prefer a fully semantic token such as \`text-muted-foreground\`, or define a new
5222
+ theme token/class when the opacity represents a reusable state.
5223
+
5139
5224
  ## LLM-Powered Suggestions
5140
5225
 
5141
5226
  When \`useLlmSuggestions\` is enabled and Ollama is running locally, the rule will:
@@ -5235,19 +5320,6 @@ function createHardCodedColorRegex(colorNames) {
5235
5320
  "g"
5236
5321
  );
5237
5322
  }
5238
- function getClassNameValue(attr) {
5239
- if (!attr.value) return null;
5240
- if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
5241
- return attr.value.value;
5242
- }
5243
- if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "string") {
5244
- return attr.value.expression.value;
5245
- }
5246
- if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "TemplateLiteral") {
5247
- return attr.value.expression.quasis.map((q) => q.value.raw).join(" ");
5248
- }
5249
- return null;
5250
- }
5251
5323
  function findHardCodedColors(className, allowedColors) {
5252
5324
  const disallowedColorNames = HARD_CODED_COLOR_NAMES.filter(
5253
5325
  (c) => !allowedColors.includes(c)
@@ -5261,6 +5333,205 @@ function findHardCodedColors(className, allowedColors) {
5261
5333
  }
5262
5334
  return matches;
5263
5335
  }
5336
+ var CLASS_COMBINER_NAMES = /* @__PURE__ */ new Set([
5337
+ "cn",
5338
+ "clsx",
5339
+ "classnames",
5340
+ "cva",
5341
+ "twMerge"
5342
+ ]);
5343
+ var COLOR_UTILITY_PREFIXES = [
5344
+ "bg",
5345
+ "text",
5346
+ "border",
5347
+ "border-t",
5348
+ "border-r",
5349
+ "border-b",
5350
+ "border-l",
5351
+ "border-x",
5352
+ "border-y",
5353
+ "ring",
5354
+ "ring-offset",
5355
+ "outline",
5356
+ "decoration",
5357
+ "accent",
5358
+ "fill",
5359
+ "stroke",
5360
+ "from",
5361
+ "via",
5362
+ "to",
5363
+ "divide",
5364
+ "placeholder",
5365
+ "caret"
5366
+ ];
5367
+ var NON_SEMANTIC_COLOR_VALUES = /* @__PURE__ */ new Set([
5368
+ "black",
5369
+ "white",
5370
+ "transparent",
5371
+ "inherit",
5372
+ "current",
5373
+ "currentColor"
5374
+ ]);
5375
+ var TEXT_SIZE_VALUES = /* @__PURE__ */ new Set([
5376
+ "xs",
5377
+ "sm",
5378
+ "base",
5379
+ "lg",
5380
+ "xl",
5381
+ "2xl",
5382
+ "3xl",
5383
+ "4xl",
5384
+ "5xl",
5385
+ "6xl",
5386
+ "7xl",
5387
+ "8xl",
5388
+ "9xl"
5389
+ ]);
5390
+ var BORDER_WIDTH_VALUES = /* @__PURE__ */ new Set(["0", "2", "4", "8"]);
5391
+ var SHADOW_SIZE_VALUES = /* @__PURE__ */ new Set([
5392
+ "2xs",
5393
+ "xs",
5394
+ "sm",
5395
+ "md",
5396
+ "lg",
5397
+ "xl",
5398
+ "2xl",
5399
+ "inner",
5400
+ "none"
5401
+ ]);
5402
+ function stripImportant(value) {
5403
+ return value.replace(/^!/, "").replace(/!$/, "");
5404
+ }
5405
+ function getBaseClass2(token) {
5406
+ let bracketDepth = 0;
5407
+ let lastVariantColon = -1;
5408
+ for (let i = 0; i < token.length; i++) {
5409
+ const char = token[i];
5410
+ if (char === "[") {
5411
+ bracketDepth++;
5412
+ } else if (char === "]" && bracketDepth > 0) {
5413
+ bracketDepth--;
5414
+ } else if (char === ":" && bracketDepth === 0) {
5415
+ lastVariantColon = i;
5416
+ }
5417
+ }
5418
+ return stripImportant(token.slice(lastVariantColon + 1));
5419
+ }
5420
+ function extractClassTokens(className) {
5421
+ return className.split(/\s+/g).map((token) => token.trim()).filter(Boolean).map((token) => ({
5422
+ original: token,
5423
+ base: getBaseClass2(token)
5424
+ }));
5425
+ }
5426
+ function classIsAllowed(token, allowedClasses) {
5427
+ return allowedClasses.includes(token.original) || allowedClasses.includes(token.base);
5428
+ }
5429
+ function getColorUtilityPrefix(baseClass) {
5430
+ for (const prefix of COLOR_UTILITY_PREFIXES) {
5431
+ if (baseClass === prefix || baseClass.startsWith(`${prefix}-`)) {
5432
+ return prefix;
5433
+ }
5434
+ }
5435
+ return null;
5436
+ }
5437
+ function getUtilityValue(baseClass, prefix) {
5438
+ if (baseClass === prefix) return "";
5439
+ return baseClass.slice(prefix.length + 1);
5440
+ }
5441
+ function isBracketColorValue(value) {
5442
+ return value.startsWith("[") && (value.includes("var(") || value.startsWith("[color:") || value.startsWith("[--") || value.includes("oklch(") || value.includes("rgb(") || value.includes("hsl("));
5443
+ }
5444
+ function isHardCodedTailwindColorValue(value) {
5445
+ const [name, shade] = value.split("-");
5446
+ return HARD_CODED_COLOR_NAMES.includes(name) && /^\d{1,3}$/.test(shade ?? "");
5447
+ }
5448
+ function isSemanticColorValue(value, prefix) {
5449
+ if (!value) return false;
5450
+ if (NON_SEMANTIC_COLOR_VALUES.has(value)) return false;
5451
+ if (prefix === "text" && TEXT_SIZE_VALUES.has(value)) return false;
5452
+ if (prefix.startsWith("border") && BORDER_WIDTH_VALUES.has(value)) return false;
5453
+ if (prefix === "shadow" && SHADOW_SIZE_VALUES.has(value)) return false;
5454
+ if (isHardCodedTailwindColorValue(value)) return false;
5455
+ if (value.startsWith("[")) return isBracketColorValue(value);
5456
+ return true;
5457
+ }
5458
+ function getVisualUtilityGroup(baseClass) {
5459
+ if (baseClass === "border" || baseClass.startsWith("border-") || baseClass.startsWith("divide-")) {
5460
+ return "border";
5461
+ }
5462
+ if (baseClass === "rounded" || baseClass.startsWith("rounded-")) {
5463
+ return "radius";
5464
+ }
5465
+ if (baseClass === "shadow" || baseClass.startsWith("shadow-") || baseClass === "drop-shadow" || baseClass.startsWith("drop-shadow-")) {
5466
+ return "shadow";
5467
+ }
5468
+ if (baseClass.startsWith("bg-gradient-") || baseClass.startsWith("from-") || baseClass.startsWith("via-") || baseClass.startsWith("to-")) {
5469
+ return "gradient";
5470
+ }
5471
+ if (baseClass === "ring" || baseClass.startsWith("ring-") || baseClass === "outline" || baseClass.startsWith("outline-")) {
5472
+ return "ring";
5473
+ }
5474
+ if (baseClass === "blur" || baseClass.startsWith("blur-") || baseClass === "backdrop-blur" || baseClass.startsWith("backdrop-blur-")) {
5475
+ return "blur";
5476
+ }
5477
+ if (baseClass.startsWith("decoration-") || baseClass.startsWith("accent-") || baseClass.startsWith("fill-") || baseClass.startsWith("stroke-")) {
5478
+ return "decoration";
5479
+ }
5480
+ if (baseClass.startsWith("bg-")) {
5481
+ return "surface";
5482
+ }
5483
+ const prefix = getColorUtilityPrefix(baseClass);
5484
+ if (prefix) {
5485
+ const value = getUtilityValue(baseClass, prefix).split("/")[0] ?? "";
5486
+ if (isSemanticColorValue(value, prefix) || isBracketColorValue(value)) {
5487
+ return "surface";
5488
+ }
5489
+ }
5490
+ return null;
5491
+ }
5492
+ function findVisualUtilityCluster(className, threshold, minGroups, allowedClasses) {
5493
+ const matches = [];
5494
+ for (const token of extractClassTokens(className)) {
5495
+ if (classIsAllowed(token, allowedClasses)) {
5496
+ continue;
5497
+ }
5498
+ const group = getVisualUtilityGroup(token.base);
5499
+ if (group) {
5500
+ matches.push({ token: token.original, group });
5501
+ }
5502
+ }
5503
+ const groups = new Set(matches.map((match) => match.group));
5504
+ if (matches.length >= threshold && groups.size >= minGroups) {
5505
+ return matches.map((match) => match.token);
5506
+ }
5507
+ return [];
5508
+ }
5509
+ function findSemanticOpacityModifiers(className, allowedClasses) {
5510
+ const matches = [];
5511
+ for (const token of extractClassTokens(className)) {
5512
+ if (classIsAllowed(token, allowedClasses)) {
5513
+ continue;
5514
+ }
5515
+ const opacityMatch = token.base.match(/^(.*)\/(\d{1,3})$/);
5516
+ if (!opacityMatch) {
5517
+ continue;
5518
+ }
5519
+ const baseWithoutOpacity = opacityMatch[1];
5520
+ const opacityValue = Number(opacityMatch[2]);
5521
+ if (opacityValue < 0 || opacityValue > 100) {
5522
+ continue;
5523
+ }
5524
+ const prefix = getColorUtilityPrefix(baseWithoutOpacity);
5525
+ if (!prefix) {
5526
+ continue;
5527
+ }
5528
+ const value = getUtilityValue(baseWithoutOpacity, prefix);
5529
+ if (isSemanticColorValue(value, prefix) || isBracketColorValue(value)) {
5530
+ matches.push(token.original);
5531
+ }
5532
+ }
5533
+ return matches;
5534
+ }
5264
5535
  var prefer_tailwind_default = createRule({
5265
5536
  name: "prefer-tailwind",
5266
5537
  meta: {
@@ -5271,7 +5542,9 @@ var prefer_tailwind_default = createRule({
5271
5542
  messages: {
5272
5543
  preferTailwind: "Prefer Tailwind className over inline style. This element uses style attribute without className.",
5273
5544
  preferSemanticColors: "Hard-coded colors: {{colors}}. Use semantic classes instead.",
5274
- preferSemanticColorsWithSuggestion: "Hard-coded colors: {{colors}}. {{suggestion}}"
5545
+ preferSemanticColorsWithSuggestion: "Hard-coded colors: {{colors}}. {{suggestion}}",
5546
+ preferSemanticClassGroups: "Dense visual utility cluster: {{classes}}. Move repeated panel/card styling into a semantic class.",
5547
+ semanticOpacityModifier: "Semantic color opacity modifiers: {{classes}}. Use fully semantic classes or tokens instead."
5275
5548
  },
5276
5549
  schema: [
5277
5550
  {
@@ -5310,6 +5583,34 @@ var prefer_tailwind_default = createRule({
5310
5583
  useLlmSuggestions: {
5311
5584
  type: "boolean",
5312
5585
  description: "Use Ollama LLM to suggest semantic color replacements"
5586
+ },
5587
+ preferSemanticClassGroups: {
5588
+ type: "boolean",
5589
+ description: "Warn when one class string contains many low-level visual utilities"
5590
+ },
5591
+ visualUtilityThreshold: {
5592
+ type: "number",
5593
+ minimum: 1,
5594
+ description: "Minimum visual utility count in one class string before warning"
5595
+ },
5596
+ visualUtilityMinGroups: {
5597
+ type: "number",
5598
+ minimum: 1,
5599
+ description: "Minimum distinct visual utility groups before warning"
5600
+ },
5601
+ disallowSemanticOpacityModifiers: {
5602
+ type: "boolean",
5603
+ description: "Warn on semantic color opacity modifiers like text-foreground/80"
5604
+ },
5605
+ allowedOpacityModifierClasses: {
5606
+ type: "array",
5607
+ items: { type: "string" },
5608
+ description: "Exact classes to allow with semantic opacity modifiers"
5609
+ },
5610
+ allowedVisualUtilityClasses: {
5611
+ type: "array",
5612
+ items: { type: "string" },
5613
+ description: "Exact visual utility classes to ignore in cluster detection"
5313
5614
  }
5314
5615
  },
5315
5616
  additionalProperties: false
@@ -5324,7 +5625,13 @@ var prefer_tailwind_default = createRule({
5324
5625
  ignoreComponents: [],
5325
5626
  preferSemanticColors: true,
5326
5627
  allowedHardCodedColors: [],
5327
- useLlmSuggestions: false
5628
+ useLlmSuggestions: false,
5629
+ preferSemanticClassGroups: true,
5630
+ visualUtilityThreshold: 4,
5631
+ visualUtilityMinGroups: 2,
5632
+ disallowSemanticOpacityModifiers: true,
5633
+ allowedOpacityModifierClasses: [],
5634
+ allowedVisualUtilityClasses: []
5328
5635
  }
5329
5636
  ],
5330
5637
  create(context) {
@@ -5336,6 +5643,12 @@ var prefer_tailwind_default = createRule({
5336
5643
  const preferSemanticColors = options.preferSemanticColors ?? true;
5337
5644
  const allowedHardCodedColors = options.allowedHardCodedColors ?? [];
5338
5645
  const useLlmSuggestions = options.useLlmSuggestions ?? false;
5646
+ const preferSemanticClassGroups = options.preferSemanticClassGroups ?? true;
5647
+ const visualUtilityThreshold = options.visualUtilityThreshold ?? 4;
5648
+ const visualUtilityMinGroups = options.visualUtilityMinGroups ?? 2;
5649
+ const disallowSemanticOpacityModifiers = options.disallowSemanticOpacityModifiers ?? true;
5650
+ const allowedOpacityModifierClasses = options.allowedOpacityModifierClasses ?? [];
5651
+ const allowedVisualUtilityClasses = options.allowedVisualUtilityClasses ?? [];
5339
5652
  let fileContent = null;
5340
5653
  const filePath = context.filename;
5341
5654
  const fileDir = dirname5(filePath);
@@ -5356,6 +5669,92 @@ var prefer_tailwind_default = createRule({
5356
5669
  function isClassNameAttribute(attr) {
5357
5670
  return attr.name.type === "JSXIdentifier" && (attr.name.name === "className" || attr.name.name === "class");
5358
5671
  }
5672
+ function checkClassString(node, className) {
5673
+ if (preferSemanticColors) {
5674
+ const hardCodedColors = findHardCodedColors(
5675
+ className,
5676
+ allowedHardCodedColors
5677
+ );
5678
+ if (hardCodedColors.length > 0) {
5679
+ const colorsStr = hardCodedColors.join(", ");
5680
+ if (useLlmSuggestions) {
5681
+ const { suggestions } = getColorSuggestions(
5682
+ hardCodedColors,
5683
+ fileDir,
5684
+ getFileContent()
5685
+ );
5686
+ const suggestionStr = formatSuggestionsForMessage(suggestions);
5687
+ if (suggestionStr) {
5688
+ context.report({
5689
+ node,
5690
+ messageId: "preferSemanticColorsWithSuggestion",
5691
+ data: { colors: colorsStr, suggestion: suggestionStr }
5692
+ });
5693
+ } else {
5694
+ context.report({
5695
+ node,
5696
+ messageId: "preferSemanticColors",
5697
+ data: { colors: colorsStr }
5698
+ });
5699
+ }
5700
+ } else {
5701
+ context.report({
5702
+ node,
5703
+ messageId: "preferSemanticColors",
5704
+ data: { colors: colorsStr }
5705
+ });
5706
+ }
5707
+ }
5708
+ }
5709
+ if (preferSemanticClassGroups) {
5710
+ const visualUtilities = findVisualUtilityCluster(
5711
+ className,
5712
+ visualUtilityThreshold,
5713
+ visualUtilityMinGroups,
5714
+ allowedVisualUtilityClasses
5715
+ );
5716
+ if (visualUtilities.length > 0) {
5717
+ context.report({
5718
+ node,
5719
+ messageId: "preferSemanticClassGroups",
5720
+ data: { classes: visualUtilities.join(", ") }
5721
+ });
5722
+ }
5723
+ }
5724
+ if (disallowSemanticOpacityModifiers) {
5725
+ const opacityClasses = findSemanticOpacityModifiers(
5726
+ className,
5727
+ allowedOpacityModifierClasses
5728
+ );
5729
+ if (opacityClasses.length > 0) {
5730
+ context.report({
5731
+ node,
5732
+ messageId: "semanticOpacityModifier",
5733
+ data: { classes: opacityClasses.join(", ") }
5734
+ });
5735
+ }
5736
+ }
5737
+ }
5738
+ function processTemplateLiteral(node) {
5739
+ for (const quasi of node.quasis) {
5740
+ checkClassString(quasi, quasi.value.raw);
5741
+ }
5742
+ }
5743
+ function processClassAttribute(attr) {
5744
+ const value = attr.value;
5745
+ if (value?.type === "Literal" && typeof value.value === "string") {
5746
+ checkClassString(value, value.value);
5747
+ }
5748
+ if (value?.type === "JSXExpressionContainer") {
5749
+ const expr = value.expression;
5750
+ if (expr.type === "Literal" && typeof expr.value === "string") {
5751
+ checkClassString(expr, expr.value);
5752
+ }
5753
+ if (expr.type === "TemplateLiteral") {
5754
+ processTemplateLiteral(expr);
5755
+ }
5756
+ }
5757
+ }
5359
5758
  return {
5360
5759
  JSXOpeningElement(node) {
5361
5760
  const componentName = getComponentName2(node);
@@ -5375,45 +5774,7 @@ var prefer_tailwind_default = createRule({
5375
5774
  }
5376
5775
  if (isClassNameAttribute(attr)) {
5377
5776
  hasClassName = true;
5378
- if (preferSemanticColors) {
5379
- const classNameValue = getClassNameValue(attr);
5380
- if (classNameValue) {
5381
- const hardCodedColors = findHardCodedColors(
5382
- classNameValue,
5383
- allowedHardCodedColors
5384
- );
5385
- if (hardCodedColors.length > 0) {
5386
- const colorsStr = hardCodedColors.join(", ");
5387
- if (useLlmSuggestions) {
5388
- const { suggestions } = getColorSuggestions(
5389
- hardCodedColors,
5390
- fileDir,
5391
- getFileContent()
5392
- );
5393
- const suggestionStr = formatSuggestionsForMessage(suggestions);
5394
- if (suggestionStr) {
5395
- context.report({
5396
- node,
5397
- messageId: "preferSemanticColorsWithSuggestion",
5398
- data: { colors: colorsStr, suggestion: suggestionStr }
5399
- });
5400
- } else {
5401
- context.report({
5402
- node,
5403
- messageId: "preferSemanticColors",
5404
- data: { colors: colorsStr }
5405
- });
5406
- }
5407
- } else {
5408
- context.report({
5409
- node,
5410
- messageId: "preferSemanticColors",
5411
- data: { colors: colorsStr }
5412
- });
5413
- }
5414
- }
5415
- }
5416
- }
5777
+ processClassAttribute(attr);
5417
5778
  }
5418
5779
  }
5419
5780
  }
@@ -5448,6 +5809,32 @@ var prefer_tailwind_default = createRule({
5448
5809
  });
5449
5810
  }
5450
5811
  }
5812
+ },
5813
+ CallExpression(node) {
5814
+ if (node.callee.type !== "Identifier") {
5815
+ return;
5816
+ }
5817
+ if (!CLASS_COMBINER_NAMES.has(node.callee.name)) {
5818
+ return;
5819
+ }
5820
+ for (const arg of node.arguments) {
5821
+ if (arg.type === "Literal" && typeof arg.value === "string") {
5822
+ checkClassString(arg, arg.value);
5823
+ }
5824
+ if (arg.type === "TemplateLiteral") {
5825
+ processTemplateLiteral(arg);
5826
+ }
5827
+ if (arg.type === "ArrayExpression") {
5828
+ for (const element of arg.elements) {
5829
+ if (element?.type === "Literal" && typeof element.value === "string") {
5830
+ checkClassString(element, element.value);
5831
+ }
5832
+ if (element?.type === "TemplateLiteral") {
5833
+ processTemplateLiteral(element);
5834
+ }
5835
+ }
5836
+ }
5837
+ }
5451
5838
  }
5452
5839
  };
5453
5840
  }
@@ -5872,7 +6259,13 @@ var recommendedConfig = {
5872
6259
  "ignoreComponents": [],
5873
6260
  "preferSemanticColors": true,
5874
6261
  "allowedHardCodedColors": [],
5875
- "useLlmSuggestions": false
6262
+ "useLlmSuggestions": false,
6263
+ "preferSemanticClassGroups": true,
6264
+ "visualUtilityThreshold": 4,
6265
+ "visualUtilityMinGroups": 2,
6266
+ "disallowSemanticOpacityModifiers": true,
6267
+ "allowedOpacityModifierClasses": [],
6268
+ "allowedVisualUtilityClasses": []
5876
6269
  }
5877
6270
  ]]
5878
6271
  }
@@ -6006,7 +6399,13 @@ var strictConfig = {
6006
6399
  "ignoreComponents": [],
6007
6400
  "preferSemanticColors": true,
6008
6401
  "allowedHardCodedColors": [],
6009
- "useLlmSuggestions": false
6402
+ "useLlmSuggestions": false,
6403
+ "preferSemanticClassGroups": true,
6404
+ "visualUtilityThreshold": 4,
6405
+ "visualUtilityMinGroups": 2,
6406
+ "disallowSemanticOpacityModifiers": true,
6407
+ "allowedOpacityModifierClasses": [],
6408
+ "allowedVisualUtilityClasses": []
6010
6409
  }
6011
6410
  ]]
6012
6411
  }