@tenphi/eslint-plugin-tasty 1.5.2 → 1.7.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.
Files changed (69) hide show
  1. package/README.md +160 -3
  2. package/dist/configs.js +3 -1
  3. package/dist/configs.js.map +1 -1
  4. package/dist/constants.js +1 -1
  5. package/dist/context.js +51 -28
  6. package/dist/context.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/rules/consistent-token-usage.js +88 -87
  11. package/dist/rules/consistent-token-usage.js.map +1 -1
  12. package/dist/rules/no-classname-prop.js +27 -0
  13. package/dist/rules/no-classname-prop.js.map +1 -0
  14. package/dist/rules/no-important.js +23 -28
  15. package/dist/rules/no-important.js.map +1 -1
  16. package/dist/rules/no-raw-color-values.js +40 -59
  17. package/dist/rules/no-raw-color-values.js.map +1 -1
  18. package/dist/rules/no-raw-motion-duration.js +6 -24
  19. package/dist/rules/no-raw-motion-duration.js.map +1 -1
  20. package/dist/rules/no-raw-transition-duration.js +7 -49
  21. package/dist/rules/no-raw-transition-duration.js.map +1 -1
  22. package/dist/rules/no-runtime-styles-mutation.js +2 -2
  23. package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
  24. package/dist/rules/no-state-in-selector.js +1 -1
  25. package/dist/rules/no-state-in-selector.js.map +1 -1
  26. package/dist/rules/no-style-prop.js +27 -0
  27. package/dist/rules/no-style-prop.js.map +1 -0
  28. package/dist/rules/no-style-spread.js +2 -2
  29. package/dist/rules/no-style-spread.js.map +1 -1
  30. package/dist/rules/no-styles-prop.js +7 -5
  31. package/dist/rules/no-styles-prop.js.map +1 -1
  32. package/dist/rules/prefer-auto-calc.js +5 -27
  33. package/dist/rules/prefer-auto-calc.js.map +1 -1
  34. package/dist/rules/prefer-custom-property-syntax.js +12 -31
  35. package/dist/rules/prefer-custom-property-syntax.js.map +1 -1
  36. package/dist/rules/prefer-shorthand-property.js +2 -2
  37. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  38. package/dist/rules/tasty-component-props.js +33 -0
  39. package/dist/rules/tasty-component-props.js.map +1 -0
  40. package/dist/rules/valid-boolean-property.js +6 -26
  41. package/dist/rules/valid-boolean-property.js.map +1 -1
  42. package/dist/rules/valid-color-token.js +12 -29
  43. package/dist/rules/valid-color-token.js.map +1 -1
  44. package/dist/rules/valid-custom-property.js +12 -28
  45. package/dist/rules/valid-custom-property.js.map +1 -1
  46. package/dist/rules/valid-custom-unit.js +10 -27
  47. package/dist/rules/valid-custom-unit.js.map +1 -1
  48. package/dist/rules/valid-directional-modifier.js +5 -27
  49. package/dist/rules/valid-directional-modifier.js.map +1 -1
  50. package/dist/rules/valid-preset.js +5 -29
  51. package/dist/rules/valid-preset.js.map +1 -1
  52. package/dist/rules/valid-radius-shape.js +5 -25
  53. package/dist/rules/valid-radius-shape.js.map +1 -1
  54. package/dist/rules/valid-recipe.js +5 -20
  55. package/dist/rules/valid-recipe.js.map +1 -1
  56. package/dist/rules/valid-transition.js +5 -25
  57. package/dist/rules/valid-transition.js.map +1 -1
  58. package/dist/rules/valid-value.js +5 -31
  59. package/dist/rules/valid-value.js.map +1 -1
  60. package/dist/style-values.js +79 -0
  61. package/dist/style-values.js.map +1 -0
  62. package/dist/type-aware-jsx.d.ts +9 -0
  63. package/dist/type-aware-jsx.js +275 -0
  64. package/dist/type-aware-jsx.js.map +1 -0
  65. package/dist/utils.js +1 -10
  66. package/dist/utils.js.map +1 -1
  67. package/dist/value-words.js +76 -0
  68. package/dist/value-words.js.map +1 -0
  69. package/package.json +6 -2
@@ -1,8 +1,10 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { LOGICAL_BORDER_STYLES } from "../constants.js";
3
- import { getKeyName, getStringValue } from "../utils.js";
4
- import { TastyContext, styleObjectListeners } from "../context.js";
5
- import { replaceInStringValue, replaceStringValue } from "../fix-utils.js";
2
+ import { LOGICAL_BORDER_STYLES, LOGICAL_STYLES } from "../constants.js";
3
+ import { getStringValue } from "../utils.js";
4
+ import { TastyContext } from "../context.js";
5
+ import { styleValueListeners } from "../style-values.js";
6
+ import { scanValueWords } from "../value-words.js";
7
+ import { replaceInStringValue } from "../fix-utils.js";
6
8
 
7
9
  //#region src/rules/consistent-token-usage.ts
8
10
  /**
@@ -15,6 +17,49 @@ import { replaceInStringValue, replaceStringValue } from "../fix-utils.js";
15
17
  * happened to use.
16
18
  */
17
19
  const BORDER_WIDTH_PROPERTIES = new Set(["border", ...LOGICAL_BORDER_STYLES]);
20
+ /** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */
21
+ const NUMERIC_PIXEL_PROPERTIES = new Set([
22
+ ...LOGICAL_STYLES,
23
+ "gap",
24
+ "radius",
25
+ "outlineOffset",
26
+ "width",
27
+ "minWidth",
28
+ "maxWidth",
29
+ "height",
30
+ "minHeight",
31
+ "maxHeight",
32
+ "padding",
33
+ "paddingTop",
34
+ "paddingRight",
35
+ "paddingBottom",
36
+ "paddingLeft",
37
+ "margin",
38
+ "marginTop",
39
+ "marginRight",
40
+ "marginBottom",
41
+ "marginLeft",
42
+ "inset",
43
+ "top",
44
+ "right",
45
+ "bottom",
46
+ "left",
47
+ "border",
48
+ "borderTop",
49
+ "borderRight",
50
+ "borderBottom",
51
+ "borderLeft",
52
+ "scrollPadding",
53
+ "scrollPaddingTop",
54
+ "scrollPaddingRight",
55
+ "scrollPaddingBottom",
56
+ "scrollPaddingLeft",
57
+ "scrollMargin",
58
+ "scrollMarginTop",
59
+ "scrollMarginRight",
60
+ "scrollMarginBottom",
61
+ "scrollMarginLeft"
62
+ ]);
18
63
  const PX_TO_UNIT = {
19
64
  "8px": "1x",
20
65
  "16px": "2x",
@@ -32,7 +77,8 @@ var consistent_token_usage_default = createRule({
32
77
  hasSuggestions: true,
33
78
  docs: { description: "Suggest using design tokens and custom units instead of raw CSS values" },
34
79
  messages: {
35
- preferToken: "Consider using '{{suggestion}}' instead of '{{raw}}'.",
80
+ rawPixelValue: "Use a design token or custom unit instead of raw pixel value '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.",
81
+ preferToken: "Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.",
36
82
  replaceWithToken: "Replace '{{raw}}' with '{{suggestion}}'"
37
83
  },
38
84
  schema: []
@@ -41,104 +87,59 @@ var consistent_token_usage_default = createRule({
41
87
  create(context) {
42
88
  const ctx = new TastyContext(context);
43
89
  function checkValue(property, value, node) {
44
- const trimmed = value.trim();
45
- if (trimmed in PX_TO_UNIT) {
46
- const suggestion = PX_TO_UNIT[trimmed];
47
- context.report({
48
- node,
49
- messageId: "preferToken",
50
- data: {
51
- suggestion,
52
- raw: trimmed
53
- },
54
- suggest: [{
55
- messageId: "replaceWithToken",
56
- data: {
57
- raw: trimmed,
58
- suggestion
59
- },
60
- fix(fixer) {
61
- return replaceStringValue(fixer, node, suggestion);
62
- }
63
- }]
90
+ if (/^[#$]/.test(property)) return;
91
+ const pixels = /* @__PURE__ */ new Map();
92
+ for (const word of scanValueWords(value)) {
93
+ if (!/^[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:e[+-]?\d+)?px$/i.test(word.value)) continue;
94
+ const amount = Number(word.value.slice(0, -2));
95
+ if (amount === 0) continue;
96
+ const raw = word.value;
97
+ const canonical = `${amount}px`;
98
+ const suggestion = property === "radius" && amount === 6 ? "1r" : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1 ? "1bw" : PX_TO_UNIT[canonical];
99
+ const edits = pixels.get(raw) ?? [];
100
+ edits.push({
101
+ start: word.offset,
102
+ end: word.offset + raw.length,
103
+ replacement: suggestion ?? ""
64
104
  });
65
- return;
105
+ pixels.set(raw, edits);
66
106
  }
67
- if (property === "radius" && trimmed === "6px") {
68
- context.report({
69
- node,
70
- messageId: "preferToken",
71
- data: {
72
- suggestion: "1r",
73
- raw: "6px"
74
- },
75
- suggest: [{
76
- messageId: "replaceWithToken",
77
- data: {
78
- raw: "6px",
79
- suggestion: "1r"
80
- },
81
- fix(fixer) {
82
- return replaceStringValue(fixer, node, "1r");
83
- }
84
- }]
85
- });
86
- return;
87
- }
88
- if (BORDER_WIDTH_PROPERTIES.has(property) && trimmed.includes("1px")) {
89
- const edits = [];
90
- const re = /\b1px\b/g;
91
- let match;
92
- while ((match = re.exec(value)) !== null) edits.push({
93
- start: match.index,
94
- end: match.index + match[0].length,
95
- replacement: "1bw"
96
- });
97
- if (edits.length === 0) return;
107
+ for (const [raw, edits] of pixels) {
108
+ const suggestion = edits[0].replacement;
109
+ if (!suggestion) {
110
+ context.report({
111
+ node,
112
+ messageId: "rawPixelValue",
113
+ data: { raw }
114
+ });
115
+ continue;
116
+ }
98
117
  context.report({
99
118
  node,
100
119
  messageId: "preferToken",
101
120
  data: {
102
- suggestion: "1bw",
103
- raw: "1px"
121
+ suggestion,
122
+ raw
104
123
  },
105
124
  suggest: [{
106
125
  messageId: "replaceWithToken",
107
126
  data: {
108
- raw: "1px",
109
- suggestion: "1bw"
127
+ raw,
128
+ suggestion
110
129
  },
111
- fix(fixer) {
112
- return replaceInStringValue(fixer, node, edits, context.sourceCode);
113
- }
130
+ fix: (fixer) => getStringValue(node) === null ? fixer.replaceText(node, `'${suggestion}'`) : replaceInStringValue(fixer, node, edits, context.sourceCode)
114
131
  }]
115
132
  });
116
133
  }
117
134
  }
118
- function handleStyleObject(node) {
119
- if (!ctx.isStyleObject(node)) return;
120
- for (const prop of node.properties) {
121
- if (prop.type !== "Property" || prop.computed) continue;
122
- const key = getKeyName(prop.key);
123
- if (key === null) continue;
124
- const str = getStringValue(prop.value);
125
- if (str) {
126
- checkValue(key, str, prop.value);
127
- continue;
128
- }
129
- if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
130
- if (stateProp.type !== "Property") continue;
131
- const stateStr = getStringValue(stateProp.value);
132
- if (stateStr) checkValue(key, stateStr, stateProp.value);
133
- }
135
+ return styleValueListeners(ctx, (property, node) => {
136
+ const value = getStringValue(node);
137
+ if (value !== null) checkValue(property, value, node);
138
+ else if (NUMERIC_PIXEL_PROPERTIES.has(property)) {
139
+ if (node.type === "Literal" && typeof node.value === "number") checkValue(property, `${node.value}px`, node);
140
+ else if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+") && node.argument.type === "Literal" && typeof node.argument.value === "number") checkValue(property, `${node.operator === "-" ? -node.argument.value : node.argument.value}px`, node);
134
141
  }
135
- }
136
- return {
137
- ImportDeclaration(node) {
138
- ctx.trackImport(node);
139
- },
140
- ...styleObjectListeners(handleStyleObject)
141
- };
142
+ });
142
143
  }
143
144
  });
144
145
 
@@ -1 +1 @@
1
- {"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { LOGICAL_BORDER_STYLES } from '../constants.js';\nimport { replaceStringValue, replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferToken' | 'replaceWithToken';\n\n/**\n * Properties whose value carries a border width, so a `1px` in one is the\n * `1bw` token spelled out.\n *\n * The logical axes belong here for the same reason `border` does: they take the\n * same width/style/colour value, parsed by the same `parseBorderValue`. Leaving\n * them out would make the advice depend on which axis vocabulary the author\n * happened to use.\n */\nconst BORDER_WIDTH_PROPERTIES = new Set<string>([\n 'border',\n ...LOGICAL_BORDER_STYLES,\n]);\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n const suggestion = PX_TO_UNIT[trimmed];\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw: trimmed },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: trimmed, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n },\n ],\n });\n return;\n }\n\n // Check 6px in radius context\n if (property === 'radius' && trimmed === '6px') {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1r', raw: '6px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '6px', suggestion: '1r' },\n fix(fixer) {\n return replaceStringValue(fixer, node, '1r');\n },\n },\n ],\n });\n return;\n }\n\n // Check 1px in border context\n if (BORDER_WIDTH_PROPERTIES.has(property) && trimmed.includes('1px')) {\n const edits: { start: number; end: number; replacement: string }[] = [];\n const re = /\\b1px\\b/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(value)) !== null) {\n edits.push({\n start: match.index,\n end: match.index + match[0].length,\n replacement: '1bw',\n });\n }\n if (edits.length === 0) return;\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1bw', raw: '1px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '1px', suggestion: '1bw' },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n );\n },\n },\n ],\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n // Only recurse into genuine state maps. Sub-element objects\n // (`Icon: { … }`) are style objects in their own right and are visited\n // separately by the listener, so treating them as state maps here both\n // double-reports and attributes the value to the wrong property.\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,0BAA0B,IAAI,IAAY,CAC9C,UACA,GAAG,sBACJ,CAAC;AAEF,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,0EACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,UAAU,MAAM,MAAM;AAG5B,OAAI,WAAW,YAAY;IACzB,MAAM,aAAa,WAAW;AAC9B,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAY,KAAK;MAAS;KAClC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAS;OAAY;MAClC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,WAAW;;MAErD,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,YAAY,OAAO;AAC9C,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAM,KAAK;MAAO;KACtC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAM;MACtC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,KAAK;;MAE/C,CACF;KACF,CAAC;AACF;;AAIF,OAAI,wBAAwB,IAAI,SAAS,IAAI,QAAQ,SAAS,MAAM,EAAE;IACpE,MAAM,QAA+D,EAAE;IACvE,MAAM,KAAK;IACX,IAAI;AACJ,YAAQ,QAAQ,GAAG,KAAK,MAAM,MAAM,KAClC,OAAM,KAAK;KACT,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,MAAM,GAAG;KAC5B,aAAa;KACd,CAAC;AAEJ,QAAI,MAAM,WAAW,EAAG;AACxB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAO,KAAK;MAAO;KACvC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAO;MACvC,IAAI,OAAO;AACT,cAAO,qBACL,OACA,MACA,OACA,QAAQ,WACT;;MAEJ,CACF;KACF,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;IAElB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;AAOF,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { styleValueListeners } from '../style-values.js';\nimport { LOGICAL_BORDER_STYLES, LOGICAL_STYLES } from '../constants.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'preferToken' | 'rawPixelValue' | 'replaceWithToken';\n\n/**\n * Properties whose value carries a border width, so a `1px` in one is the\n * `1bw` token spelled out.\n *\n * The logical axes belong here for the same reason `border` does: they take the\n * same width/style/colour value, parsed by the same `parseBorderValue`. Leaving\n * them out would make the advice depend on which axis vocabulary the author\n * happened to use.\n */\nconst BORDER_WIDTH_PROPERTIES = new Set<string>([\n 'border',\n ...LOGICAL_BORDER_STYLES,\n]);\n\n/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */\nconst NUMERIC_PIXEL_PROPERTIES = new Set<string>([\n ...LOGICAL_STYLES,\n 'gap',\n 'radius',\n 'outlineOffset',\n 'width',\n 'minWidth',\n 'maxWidth',\n 'height',\n 'minHeight',\n 'maxHeight',\n 'padding',\n 'paddingTop',\n 'paddingRight',\n 'paddingBottom',\n 'paddingLeft',\n 'margin',\n 'marginTop',\n 'marginRight',\n 'marginBottom',\n 'marginLeft',\n 'inset',\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'border',\n 'borderTop',\n 'borderRight',\n 'borderBottom',\n 'borderLeft',\n 'scrollPadding',\n 'scrollPaddingTop',\n 'scrollPaddingRight',\n 'scrollPaddingBottom',\n 'scrollPaddingLeft',\n 'scrollMargin',\n 'scrollMarginTop',\n 'scrollMarginRight',\n 'scrollMarginBottom',\n 'scrollMarginLeft',\n]);\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n rawPixelValue:\n \"Use a design token or custom unit instead of raw pixel value '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n preferToken:\n \"Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n // Definitions establish the scale itself; quoted text and URLs are opaque.\n if (/^[#$]/.test(property)) return;\n const pixels = new Map<\n string,\n { start: number; end: number; replacement: string }[]\n >();\n for (const word of scanValueWords(value)) {\n if (!/^[+-]?(?:\\d*\\.\\d+|\\d+\\.?\\d*)(?:e[+-]?\\d+)?px$/i.test(word.value))\n continue;\n const amount = Number(word.value.slice(0, -2));\n if (amount === 0) continue;\n const raw = word.value;\n const canonical = `${amount}px`;\n const suggestion =\n property === 'radius' && amount === 6\n ? '1r'\n : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1\n ? '1bw'\n : PX_TO_UNIT[canonical];\n const edits = pixels.get(raw) ?? [];\n edits.push({\n start: word.offset,\n end: word.offset + raw.length,\n replacement: suggestion ?? '',\n });\n pixels.set(raw, edits);\n }\n for (const [raw, edits] of pixels) {\n const suggestion = edits[0].replacement;\n if (!suggestion) {\n context.report({ node, messageId: 'rawPixelValue', data: { raw } });\n continue;\n }\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw, suggestion },\n fix: (fixer) =>\n getStringValue(node) === null\n ? fixer.replaceText(node, `'${suggestion}'`)\n : replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n ),\n },\n ],\n });\n }\n }\n\n return styleValueListeners(ctx, (property, node) => {\n const value = getStringValue(node);\n if (value !== null) {\n checkValue(property, value, node);\n } else if (NUMERIC_PIXEL_PROPERTIES.has(property)) {\n if (node.type === 'Literal' && typeof node.value === 'number') {\n checkValue(property, `${node.value}px`, node);\n } else if (\n node.type === 'UnaryExpression' &&\n (node.operator === '-' || node.operator === '+') &&\n node.argument.type === 'Literal' &&\n typeof node.argument.value === 'number'\n ) {\n const amount =\n node.operator === '-' ? -node.argument.value : node.argument.value;\n checkValue(property, `${amount}px`, node);\n }\n }\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,MAAM,0BAA0B,IAAI,IAAY,CAC9C,UACA,GAAG,sBACJ,CAAC;;AAGF,MAAM,2BAA2B,IAAI,IAAY;CAC/C,GAAG;CACH;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,0EACH;EACD,UAAU;GACR,eACE;GACF,aACE;GACF,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;AAEN,OAAI,QAAQ,KAAK,SAAS,CAAE;GAC5B,MAAM,yBAAS,IAAI,KAGhB;AACH,QAAK,MAAM,QAAQ,eAAe,MAAM,EAAE;AACxC,QAAI,CAAC,iDAAiD,KAAK,KAAK,MAAM,CACpE;IACF,MAAM,SAAS,OAAO,KAAK,MAAM,MAAM,GAAG,GAAG,CAAC;AAC9C,QAAI,WAAW,EAAG;IAClB,MAAM,MAAM,KAAK;IACjB,MAAM,YAAY,GAAG,OAAO;IAC5B,MAAM,aACJ,aAAa,YAAY,WAAW,IAChC,OACA,wBAAwB,IAAI,SAAS,IAAI,WAAW,IAClD,QACA,WAAW;IACnB,MAAM,QAAQ,OAAO,IAAI,IAAI,IAAI,EAAE;AACnC,UAAM,KAAK;KACT,OAAO,KAAK;KACZ,KAAK,KAAK,SAAS,IAAI;KACvB,aAAa,cAAc;KAC5B,CAAC;AACF,WAAO,IAAI,KAAK,MAAM;;AAExB,QAAK,MAAM,CAAC,KAAK,UAAU,QAAQ;IACjC,MAAM,aAAa,MAAM,GAAG;AAC5B,QAAI,CAAC,YAAY;AACf,aAAQ,OAAO;MAAE;MAAM,WAAW;MAAiB,MAAM,EAAE,KAAK;MAAE,CAAC;AACnE;;AAEF,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAY;MAAK;KACzB,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE;OAAK;OAAY;MACzB,MAAM,UACJ,eAAe,KAAK,KAAK,OACrB,MAAM,YAAY,MAAM,IAAI,WAAW,GAAG,GAC1C,qBACE,OACA,MACA,OACA,QAAQ,WACT;MACR,CACF;KACF,CAAC;;;AAIN,SAAO,oBAAoB,MAAM,UAAU,SAAS;GAClD,MAAM,QAAQ,eAAe,KAAK;AAClC,OAAI,UAAU,KACZ,YAAW,UAAU,OAAO,KAAK;YACxB,yBAAyB,IAAI,SAAS,EAC/C;QAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,SACnD,YAAW,UAAU,GAAG,KAAK,MAAM,KAAK,KAAK;aAE7C,KAAK,SAAS,sBACb,KAAK,aAAa,OAAO,KAAK,aAAa,QAC5C,KAAK,SAAS,SAAS,aACvB,OAAO,KAAK,SAAS,UAAU,SAI/B,YAAW,UAAU,GADnB,KAAK,aAAa,MAAM,CAAC,KAAK,SAAS,QAAQ,KAAK,SAAS,MAChC,KAAK,KAAK;;IAG7C;;CAEL,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { TastyContext } from "../context.js";
3
+ import { tastyComponentPropListeners } from "./tasty-component-props.js";
4
+
5
+ //#region src/rules/no-classname-prop.ts
6
+ var no_classname_prop_default = createRule({
7
+ name: "no-classname-prop",
8
+ meta: {
9
+ type: "suggestion",
10
+ docs: { description: "Prefer Tasty styling and data-element identities over className" },
11
+ messages: { noClassNameProp: "Avoid the 'className' prop on Tasty components. Keep styling in Tasty; for sub-element styling, use data-element=\"Name\" and define the matching capitalized sub-element in the parent's styles. If a third-party library requires classes, explicitly disable tasty/no-classname-prop for this usage with an ESLint comment and a reason." },
12
+ schema: []
13
+ },
14
+ defaultOptions: [],
15
+ create(context) {
16
+ return tastyComponentPropListeners(new TastyContext(context), (name, node) => {
17
+ if (name === "className") context.report({
18
+ node,
19
+ messageId: "noClassNameProp"
20
+ });
21
+ });
22
+ }
23
+ });
24
+
25
+ //#endregion
26
+ export { no_classname_prop_default as default };
27
+ //# sourceMappingURL=no-classname-prop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-classname-prop.js","names":[],"sources":["../../src/rules/no-classname-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { tastyComponentPropListeners } from './tasty-component-props.js';\n\nexport default createRule<[], 'noClassNameProp'>({\n name: 'no-classname-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer Tasty styling and data-element identities over className',\n },\n messages: {\n noClassNameProp:\n \"Avoid the 'className' prop on Tasty components. Keep styling in Tasty; for sub-element styling, use data-element=\\\"Name\\\" and define the matching capitalized sub-element in the parent's styles. If a third-party library requires classes, explicitly disable tasty/no-classname-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n return tastyComponentPropListeners(ctx, (name, node) => {\n if (name === 'className')\n context.report({ node, messageId: 'noClassNameProp' });\n });\n },\n});\n"],"mappings":";;;;;AAIA,gCAAe,WAAkC;CAC/C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,mEACH;EACD,UAAU,EACR,iBACE,+UACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;AAEd,SAAO,4BADK,IAAI,aAAa,QAAQ,GACI,MAAM,SAAS;AACtD,OAAI,SAAS,YACX,SAAQ,OAAO;IAAE;IAAM,WAAW;IAAmB,CAAC;IACxD;;CAEL,CAAC"}
@@ -1,7 +1,9 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { getStringValue } from "../utils.js";
3
- import { TastyContext, styleObjectListeners } from "../context.js";
4
- import { replaceStringValue } from "../fix-utils.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { stringStyleValueListeners } from "../style-values.js";
5
+ import { maskValueLiterals } from "../value-words.js";
6
+ import { replaceInStringValue } from "../fix-utils.js";
5
7
 
6
8
  //#region src/rules/no-important.ts
7
9
  var no_important_default = createRule({
@@ -18,33 +20,26 @@ var no_important_default = createRule({
18
20
  const ctx = new TastyContext(context);
19
21
  function checkNode(node) {
20
22
  const str = getStringValue(node);
21
- if (str && str.includes("!important")) {
22
- const stripped = str.replace(/\s*!important\s*/g, "").trim();
23
- context.report({
24
- node,
25
- messageId: "noImportant",
26
- fix(fixer) {
27
- return replaceStringValue(fixer, node, stripped);
28
- }
29
- });
30
- }
23
+ if (!str) return;
24
+ const edits = [...maskValueLiterals(str).matchAll(/!important\b(?!-)/gi)].map((match) => {
25
+ let start = match.index;
26
+ while (start > 0 && /\s/.test(str[start - 1])) start--;
27
+ return {
28
+ start,
29
+ end: match.index + match[0].length,
30
+ replacement: ""
31
+ };
32
+ });
33
+ if (!edits.length) return;
34
+ context.report({
35
+ node,
36
+ messageId: "noImportant",
37
+ fix: (fixer) => replaceInStringValue(fixer, node, edits, context.sourceCode)
38
+ });
31
39
  }
32
- function handleStyleObject(node) {
33
- if (!ctx.isStyleObject(node)) return;
34
- for (const prop of node.properties) {
35
- if (prop.type !== "Property") continue;
36
- checkNode(prop.value);
37
- if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) {
38
- for (const stateProp of prop.value.properties) if (stateProp.type === "Property") checkNode(stateProp.value);
39
- }
40
- }
41
- }
42
- return {
43
- ImportDeclaration(node) {
44
- ctx.trackImport(node);
45
- },
46
- ...styleObjectListeners(handleStyleObject)
47
- };
40
+ return stringStyleValueListeners(ctx, (_value, node) => {
41
+ checkNode(node);
42
+ });
48
43
  }
49
44
  });
50
45
 
@@ -1 +1 @@
1
- {"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n const stripped = str.replace(/\\s*!important\\s*/g, '').trim();\n context.report({\n node,\n messageId: 'noImportant',\n fix(fixer) {\n return replaceStringValue(fixer, node, stripped);\n },\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n checkNode(prop.value);\n\n // Only recurse into genuine state maps. Sub-element objects\n // (`Icon: { … }`) are style objects in their own right and are visited\n // separately by the listener, so recursing here reports them twice.\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type === 'Property') {\n checkNode(stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,OAAO,IAAI,SAAS,aAAa,EAAE;IACrC,MAAM,WAAW,IAAI,QAAQ,qBAAqB,GAAG,CAAC,MAAM;AAC5D,YAAQ,OAAO;KACb;KACA,WAAW;KACX,IAAI,OAAO;AACT,aAAO,mBAAmB,OAAO,MAAM,SAAS;;KAEnD,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,cAAU,KAAK,MAAM;AAKrB,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,EAEhC;UAAK,MAAM,aAAa,KAAK,MAAM,WACjC,KAAI,UAAU,SAAS,WACrB,WAAU,UAAU,MAAM;;;;AAOpC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { maskValueLiterals } from '../value-words.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (!str) return;\n const masked = maskValueLiterals(str);\n const edits = [...masked.matchAll(/!important\\b(?!-)/gi)].map((match) => {\n let start = match.index;\n while (start > 0 && /\\s/.test(str[start - 1])) start--;\n return { start, end: match.index + match[0].length, replacement: '' };\n });\n if (!edits.length) return;\n context.report({\n node,\n messageId: 'noImportant',\n fix: (fixer) =>\n replaceInStringValue(fixer, node, edits, context.sourceCode),\n });\n }\n\n return stringStyleValueListeners(ctx, (_value, node) => {\n checkNode(node);\n });\n },\n});\n"],"mappings":";;;;;;;;AAUA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,CAAC,IAAK;GAEV,MAAM,QAAQ,CAAC,GADA,kBAAkB,IAAI,CACZ,SAAS,sBAAsB,CAAC,CAAC,KAAK,UAAU;IACvE,IAAI,QAAQ,MAAM;AAClB,WAAO,QAAQ,KAAK,KAAK,KAAK,IAAI,QAAQ,GAAG,CAAE;AAC/C,WAAO;KAAE;KAAO,KAAK,MAAM,QAAQ,MAAM,GAAG;KAAQ,aAAa;KAAI;KACrE;AACF,OAAI,CAAC,MAAM,OAAQ;AACnB,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,UACJ,qBAAqB,OAAO,MAAM,OAAO,QAAQ,WAAW;IAC/D,CAAC;;AAGJ,SAAO,0BAA0B,MAAM,QAAQ,SAAS;AACtD,aAAU,KAAK;IACf;;CAEL,CAAC"}
@@ -1,21 +1,37 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from "../constants.js";
3
- import { getKeyName, getStringValue } from "../utils.js";
4
- import { TastyContext, styleObjectListeners } from "../context.js";
3
+ import { getStringValue, isRawHexColor } from "../utils.js";
4
+ import { TastyContext } from "../context.js";
5
+ import { stringStyleValueListeners } from "../style-values.js";
6
+ import { scanValueWords } from "../value-words.js";
5
7
 
6
8
  //#region src/rules/no-raw-color-values.ts
7
- const HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\b/g;
8
- const COLOR_FUNC_REGEX = /\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\s*\(/gi;
9
- const NAMED_COLOR_REGEX = new RegExp(`\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join("|")})\\b`, "gi");
9
+ const COLOR_FUNCTIONS = new Set([
10
+ "rgb",
11
+ "rgba",
12
+ "hsl",
13
+ "hsla",
14
+ "hwb",
15
+ "lab",
16
+ "lch",
17
+ "oklab",
18
+ "oklch",
19
+ "okhsl",
20
+ "okhsv",
21
+ "okhst",
22
+ "color",
23
+ "device-cmyk",
24
+ "light-dark"
25
+ ]);
10
26
  var no_raw_color_values_default = createRule({
11
27
  name: "no-raw-color-values",
12
28
  meta: {
13
29
  type: "suggestion",
14
30
  docs: { description: "Suggest using color tokens instead of raw hex/rgb/okhsl/named colors" },
15
31
  messages: {
16
- rawHexColor: "Use a color token instead of raw hex color '{{value}}'.",
17
- rawColorFunction: "Use a color token instead of raw {{func}}() color.",
18
- rawNamedColor: "Use a color token instead of raw named color '{{name}}'."
32
+ rawHexColor: "Use a color token instead of raw hex color '{{value}}'. For an intentional one-off color, explicitly disable tasty/no-raw-color-values on this line with a reason.",
33
+ rawColorFunction: "Use a color token instead of raw {{func}}() color. For an intentional one-off color, explicitly disable tasty/no-raw-color-values on this line with a reason.",
34
+ rawNamedColor: "Use a color token instead of raw named color '{{name}}'. For an intentional one-off color, explicitly disable tasty/no-raw-color-values on this line with a reason."
19
35
  },
20
36
  schema: []
21
37
  },
@@ -38,61 +54,26 @@ var no_raw_color_values_default = createRule({
38
54
  return false;
39
55
  }
40
56
  function checkValue(value, node, propertyKey) {
41
- if (isInTokenDefinition(node)) return;
42
- HEX_COLOR_REGEX.lastIndex = 0;
43
- let match;
44
- while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {
45
- const hex = match[1];
46
- if ([
47
- 3,
48
- 4,
49
- 6,
50
- 8
51
- ].includes(hex.length)) context.report({
52
- node,
53
- messageId: "rawHexColor",
54
- data: { value: match[0] }
55
- });
56
- }
57
- COLOR_FUNC_REGEX.lastIndex = 0;
58
- while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) context.report({
57
+ if (isInTokenDefinition(node) || propertyKey && /^[#$]/.test(propertyKey)) return;
58
+ for (const word of scanValueWords(value)) if (isRawHexColor(word.value)) context.report({
59
+ node,
60
+ messageId: "rawHexColor",
61
+ data: { value: word.value }
62
+ });
63
+ else if (word.isFunction && COLOR_FUNCTIONS.has(word.value.toLowerCase())) context.report({
59
64
  node,
60
65
  messageId: "rawColorFunction",
61
- data: { func: match[1] }
66
+ data: { func: word.value }
67
+ });
68
+ else if (!word.isFunction && propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey) && NAMED_CSS_COLORS.has(word.value.toLowerCase())) context.report({
69
+ node,
70
+ messageId: "rawNamedColor",
71
+ data: { name: word.value }
62
72
  });
63
- if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {
64
- NAMED_COLOR_REGEX.lastIndex = 0;
65
- while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {
66
- const prev = match.index > 0 ? value[match.index - 1] : "";
67
- if (prev === "#" || prev === "$") continue;
68
- context.report({
69
- node,
70
- messageId: "rawNamedColor",
71
- data: { name: match[1] }
72
- });
73
- }
74
- }
75
- }
76
- function handleStyleObject(node) {
77
- if (!ctx.isStyleObject(node)) return;
78
- for (const prop of node.properties) {
79
- if (prop.type !== "Property") continue;
80
- const key = getKeyName(prop.key);
81
- const str = getStringValue(prop.value);
82
- if (str) checkValue(str, prop.value, key);
83
- if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
84
- if (stateProp.type !== "Property") continue;
85
- const stateStr = getStringValue(stateProp.value);
86
- if (stateStr) checkValue(stateStr, stateProp.value, key);
87
- }
88
- }
89
73
  }
90
- return {
91
- ImportDeclaration(node) {
92
- ctx.trackImport(node);
93
- },
94
- ...styleObjectListeners(handleStyleObject)
95
- };
74
+ return stringStyleValueListeners(ctx, (value, node, property) => {
75
+ checkValue(value, node, property);
76
+ });
96
77
  }
97
78
  });
98
79
 
@@ -1 +1 @@
1
- {"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue, getKeyName } from '../utils.js';\nimport { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from '../constants.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction' | 'rawNamedColor';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX =\n /\\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\\s*\\(/gi;\nconst NAMED_COLOR_REGEX = new RegExp(\n `\\\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join('|')})\\\\b`,\n 'gi',\n);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using color tokens instead of raw hex/rgb/okhsl/named colors',\n },\n messages: {\n rawHexColor: \"Use a color token instead of raw hex color '{{value}}'.\",\n rawColorFunction: 'Use a color token instead of raw {{func}}() color.',\n rawNamedColor: \"Use a color token instead of raw named color '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n propertyKey?: string | null,\n ): void {\n if (isInTokenDefinition(node)) return;\n\n // Check hex colors\n HEX_COLOR_REGEX.lastIndex = 0;\n let match;\n while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {\n const hex = match[1];\n if ([3, 4, 6, 8].includes(hex.length)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: match[0] },\n });\n }\n }\n\n // Check color functions (rgb, hsl, okhsl, okhst, oklch, …)\n COLOR_FUNC_REGEX.lastIndex = 0;\n while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: match[1] },\n });\n }\n\n // Check named CSS colors — scoped to color-bearing properties to avoid\n // false positives on string properties like `content` or `cursor`. Skip\n // matches that are part of a `#token` or `$prop` reference (e.g. `#purple`,\n // `$red`) — those are Tasty tokens/custom properties, not raw colors.\n if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {\n NAMED_COLOR_REGEX.lastIndex = 0;\n while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {\n const prev = match.index > 0 ? value[match.index - 1] : '';\n if (prev === '#' || prev === '$') continue;\n context.report({\n node,\n messageId: 'rawNamedColor',\n data: { name: match[1] },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = getKeyName(prop.key);\n\n const str = getStringValue(prop.value);\n if (str) checkValue(str, prop.value, key);\n\n // Only recurse into genuine state maps. Sub-element objects\n // (`Icon: { … }`) are style objects in their own right and are visited\n // separately by the listener, so treating them as state maps here both\n // double-reports and passes the sub-element name as the property.\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,mBACJ;AACF,MAAM,oBAAoB,IAAI,OAC5B,OAAO,CAAC,GAAG,iBAAiB,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAC3E,KACD;AAED,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GAClB,eAAe;GAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WACP,OACA,MACA,aACM;AACN,OAAI,oBAAoB,KAAK,CAAE;AAG/B,mBAAgB,YAAY;GAC5B,IAAI;AACJ,WAAQ,QAAQ,gBAAgB,KAAK,MAAM,MAAM,MAAM;IACrD,MAAM,MAAM,MAAM;AAClB,QAAI;KAAC;KAAG;KAAG;KAAG;KAAE,CAAC,SAAS,IAAI,OAAO,CACnC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,IAAI;KAC1B,CAAC;;AAKN,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,KAChD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,MAAM,IAAI;IACzB,CAAC;AAOJ,OAAI,eAAe,yBAAyB,IAAI,YAAY,EAAE;AAC5D,sBAAkB,YAAY;AAC9B,YAAQ,QAAQ,kBAAkB,KAAK,MAAM,MAAM,MAAM;KACvD,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK;AACxD,SAAI,SAAS,OAAO,SAAS,IAAK;AAClC,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM,MAAM,IAAI;MACzB,CAAC;;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,WAAW,KAAK,IAAI;IAEhC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,IAAK,YAAW,KAAK,KAAK,OAAO,IAAI;AAMzC,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SAAU,YAAW,UAAU,UAAU,OAAO,IAAI;;;;AAMhE,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { getStringValue, isRawHexColor } from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\nimport { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from '../constants.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction' | 'rawNamedColor';\n\nconst COLOR_FUNCTIONS = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'okhsl',\n 'okhsv',\n 'okhst',\n 'color',\n 'device-cmyk',\n 'light-dark',\n]);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using color tokens instead of raw hex/rgb/okhsl/named colors',\n },\n messages: {\n rawHexColor:\n \"Use a color token instead of raw hex color '{{value}}'. For an intentional one-off color, explicitly disable tasty/no-raw-color-values on this line with a reason.\",\n rawColorFunction:\n 'Use a color token instead of raw {{func}}() color. For an intentional one-off color, explicitly disable tasty/no-raw-color-values on this line with a reason.',\n rawNamedColor:\n \"Use a color token instead of raw named color '{{name}}'. For an intentional one-off color, explicitly disable tasty/no-raw-color-values on this line with a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n propertyKey?: string | null,\n ): void {\n if (\n isInTokenDefinition(node) ||\n (propertyKey && /^[#$]/.test(propertyKey))\n )\n return;\n for (const word of scanValueWords(value)) {\n if (isRawHexColor(word.value)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: word.value },\n });\n } else if (\n word.isFunction &&\n COLOR_FUNCTIONS.has(word.value.toLowerCase())\n ) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: word.value },\n });\n } else if (\n !word.isFunction &&\n propertyKey &&\n COLOR_BEARING_PROPERTIES.has(propertyKey) &&\n NAMED_CSS_COLORS.has(word.value.toLowerCase())\n ) {\n context.report({\n node,\n messageId: 'rawNamedColor',\n data: { name: word.value },\n });\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n checkValue(value, node, property);\n });\n },\n});\n"],"mappings":";;;;;;;;AAUA,MAAM,kBAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU;GACR,aACE;GACF,kBACE;GACF,eACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WACP,OACA,MACA,aACM;AACN,OACE,oBAAoB,KAAK,IACxB,eAAe,QAAQ,KAAK,YAAY,CAEzC;AACF,QAAK,MAAM,QAAQ,eAAe,MAAM,CACtC,KAAI,cAAc,KAAK,MAAM,CAC3B,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,OAAO,KAAK,OAAO;IAC5B,CAAC;YAEF,KAAK,cACL,gBAAgB,IAAI,KAAK,MAAM,aAAa,CAAC,CAE7C,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,KAAK,OAAO;IAC3B,CAAC;YAEF,CAAC,KAAK,cACN,eACA,yBAAyB,IAAI,YAAY,IACzC,iBAAiB,IAAI,KAAK,MAAM,aAAa,CAAC,CAE9C,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,KAAK,OAAO;IAC3B,CAAC;;AAKR,SAAO,0BAA0B,MAAM,OAAO,MAAM,aAAa;AAC/D,cAAW,OAAO,MAAM,SAAS;IACjC;;CAEL,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { getKeyName, getStringValue } from "../utils.js";
3
- import { TastyContext, styleObjectListeners } from "../context.js";
2
+ import { TastyContext } from "../context.js";
3
+ import { stringStyleValueListeners } from "../style-values.js";
4
4
  import { scanTokens } from "../parsers/utils.js";
5
5
  import { replaceInStringValue } from "../fix-utils.js";
6
6
  import { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeExpression, isTimeLiteral, isZeroTime } from "./motion-duration-utils.js";
@@ -18,7 +18,7 @@ var no_raw_motion_duration_default = createRule({
18
18
  hasSuggestions: true,
19
19
  docs: { description: "Suggest motion tokens for animation and explicit CSS duration properties" },
20
20
  messages: {
21
- rawMotionDuration: "{{property}} duration '{{duration}}' is hardcoded. Use a duration token.",
21
+ rawMotionDuration: "{{property}} duration '{{duration}}' is hardcoded. Use a duration token. For intentional custom timing, explicitly disable tasty/no-raw-motion-duration on this line with a reason.",
22
22
  useDurationToken: "Replace '{{duration}}' with '{{token}}'"
23
23
  },
24
24
  schema: []
@@ -76,27 +76,9 @@ var no_raw_motion_duration_default = createRule({
76
76
  }
77
77
  checkGroup(group, property, node);
78
78
  }
79
- function handleStyleObject(node) {
80
- if (!ctx.isStyleObject(node)) return;
81
- for (const prop of node.properties) {
82
- if (prop.type !== "Property" || prop.computed) continue;
83
- const key = getKeyName(prop.key);
84
- if (!key || !PROPERTIES.has(key)) continue;
85
- const value = getStringValue(prop.value);
86
- if (value) checkValue(value, key, prop.value);
87
- else if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
88
- if (stateProp.type !== "Property") continue;
89
- const stateValue = getStringValue(stateProp.value);
90
- if (stateValue) checkValue(stateValue, key, stateProp.value);
91
- }
92
- }
93
- }
94
- return {
95
- ImportDeclaration(node) {
96
- ctx.trackImport(node);
97
- },
98
- ...styleObjectListeners(handleStyleObject)
99
- };
79
+ return stringStyleValueListeners(ctx, (value, node, property) => {
80
+ if (PROPERTIES.has(property)) checkValue(value, property, node);
81
+ });
100
82
  }
101
83
  });
102
84