@dialpad/eslint-plugin-dialtone 1.11.0 → 1.11.1

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.
@@ -1,7 +1,26 @@
1
1
  /**
2
- * @fileoverview Recommends using align/justify props instead of CSS utilities on Stack component
2
+ * @fileoverview Recommends using props instead of CSS utilities on Stack component
3
3
  */
4
- "use strict";
4
+ 'use strict';
5
+
6
+ //------------------------------------------------------------------------------
7
+ // Constants
8
+ //------------------------------------------------------------------------------
9
+
10
+ /**
11
+ * Gap utility classes that have DtStack equivalents.
12
+ * Maps utility class suffix to DtStack gap prop value.
13
+ * Gaps > 64px have no equivalent and should not be flagged.
14
+ */
15
+ const GAP_WITH_EQUIVALENTS = {
16
+ '0': '0',
17
+ '8': '400',
18
+ '16': '500',
19
+ '24': '550',
20
+ '32': '600',
21
+ '48': '650',
22
+ '64': '700',
23
+ };
5
24
 
6
25
  //------------------------------------------------------------------------------
7
26
  // Rule Definition
@@ -11,7 +30,7 @@ module.exports = {
11
30
  meta: {
12
31
  type: 'suggestion',
13
32
  docs: {
14
- description: "Recommend using align/justify props instead of CSS utilities on Stack component",
33
+ description: 'Recommend using props instead of CSS utilities on Stack component',
15
34
  recommended: false,
16
35
  url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-stack-alignment-classes.md',
17
36
  },
@@ -20,19 +39,23 @@ module.exports = {
20
39
  messages: {
21
40
  useAlignProp: 'Use the `align` prop instead of `d-ai-*` utility classes on <dt-stack>. See: https://dialtone.dialpad.com/components/stack.html#align',
22
41
  useJustifyProp: 'Use the `justify` prop instead of `d-jc-*` utility classes on <dt-stack>. See: https://dialtone.dialpad.com/components/stack.html#justify',
42
+ useDirectionProp: 'Use the `direction` prop instead of `d-fd-*` utility classes on <dt-stack>. See: https://dialtone.dialpad.com/components/stack.html#direction',
43
+ useGapProp: 'Use the `gap` prop instead of `d-g*` utility classes on <dt-stack>. See: https://dialtone.dialpad.com/components/stack.html#gap',
44
+ removeRedundantFlex: 'Remove `d-d-flex` from <dt-stack> - it is already a flex container.',
23
45
  },
24
46
  },
25
47
 
26
48
  create(context) {
27
49
  const sourceCode = context.sourceCode ?? context.getSourceCode();
28
50
  return sourceCode.parserServices.defineTemplateBodyVisitor({
51
+
29
52
  VElement(node) {
30
53
  // Check if element is dt-stack or DtStack
31
54
  const elementName = node.name || node.rawName;
32
55
  if (elementName === 'dt-stack' || elementName === 'DtStack') {
33
56
  // Find class attribute
34
57
  const classAttr = node.startTag.attributes.find(
35
- attr => attr.key && attr.key.name === 'class'
58
+ attr => attr.key && attr.key.name === 'class',
36
59
  );
37
60
 
38
61
  if (classAttr && classAttr.value && classAttr.value.value) {
@@ -53,9 +76,35 @@ module.exports = {
53
76
  messageId: 'useJustifyProp',
54
77
  });
55
78
  }
79
+
80
+ // Check for d-fd-* classes (flex-direction utilities)
81
+ if (/d-fd-(row|column|row-reverse|column-reverse)/.test(classes)) {
82
+ context.report({
83
+ node: classAttr,
84
+ messageId: 'useDirectionProp',
85
+ });
86
+ }
87
+
88
+ // Check for d-g* and d-gg* classes (gap utilities) - only those with DtStack equivalents
89
+ // d-gg* uses deprecated grid-gap property but works the same as d-g*
90
+ const gapMatch = classes.match(/\bd-gg?(\d+)\b/);
91
+ if (gapMatch && GAP_WITH_EQUIVALENTS[gapMatch[1]]) {
92
+ context.report({
93
+ node: classAttr,
94
+ messageId: 'useGapProp',
95
+ });
96
+ }
97
+
98
+ // Check for d-d-flex (redundant on DtStack)
99
+ if (/\bd-d-flex\b/.test(classes)) {
100
+ context.report({
101
+ node: classAttr,
102
+ messageId: 'removeRedundantFlex',
103
+ });
104
+ }
56
105
  }
57
106
  }
58
- }
107
+ },
59
108
  });
60
- }
109
+ },
61
110
  };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @fileoverview Prefer DtStack component over flex utility classes
3
+ */
4
+ 'use strict';
5
+
6
+ //------------------------------------------------------------------------------
7
+ // Rule Definition
8
+ //------------------------------------------------------------------------------
9
+
10
+ /** @type {import('eslint').Rule.RuleModule} */
11
+ module.exports = {
12
+ meta: {
13
+ type: 'suggestion',
14
+ docs: {
15
+ description: 'Prefer DtStack component over flex utility classes',
16
+ recommended: false,
17
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/prefer-stack-over-flex.md',
18
+ },
19
+ fixable: null,
20
+ schema: [],
21
+ messages: {
22
+ preferStack: 'Consider using `<dt-stack>` instead of `d-d-flex`. See: https://dialtone.dialpad.com/components/stack.html',
23
+ dynamicFlexBinding: 'Flex utilities detected in dynamic `:class` binding. Consider using `<dt-stack>` with conditional props instead. Manual migration required.',
24
+ },
25
+ },
26
+
27
+ create(context) {
28
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
29
+ return sourceCode.parserServices.defineTemplateBodyVisitor({
30
+ VElement(node) {
31
+ // Skip if already dt-stack or DtStack
32
+ const elementName = node.name || node.rawName;
33
+ if (elementName === 'dt-stack' || elementName === 'DtStack') return;
34
+
35
+ // Find class attribute
36
+ const classAttr = node.startTag.attributes.find(
37
+ attr => attr.key && attr.key.name === 'class',
38
+ );
39
+
40
+ if (classAttr && classAttr.value && classAttr.value.value) {
41
+ const classes = classAttr.value.value;
42
+
43
+ // Flag any element with d-d-flex (no exclusions)
44
+ if (/\bd-d-flex\b/.test(classes)) {
45
+ context.report({
46
+ node: classAttr,
47
+ messageId: 'preferStack',
48
+ });
49
+ }
50
+ }
51
+ },
52
+
53
+ VAttribute(node) {
54
+ // Check for :class or v-bind:class directives
55
+ if (node.directive &&
56
+ node.key.name.name === 'bind' &&
57
+ node.key.argument?.name === 'class') {
58
+
59
+ // Get the raw source of the binding expression
60
+ const bindingText = sourceCode.getText(node.value);
61
+
62
+ // Check if it contains flex utilities (as string literals)
63
+ // Look for patterns like 'd-d-flex', 'd-ai-', 'd-jc-', 'd-fd-', 'd-g\d', 'd-gg\d'
64
+ if (/['"]d-d-flex['"]|['"]d-ai-|['"]d-jc-|['"]d-fd-|['"]d-gg?\d/.test(bindingText)) {
65
+ context.report({
66
+ node: node,
67
+ messageId: 'dynamicFlexBinding',
68
+ });
69
+ }
70
+ }
71
+ },
72
+ });
73
+ },
74
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview Utilities to set Font family, Font weight, Font size, and Line height separately are discouraged in favor of composed typography utilities
2
+ * @fileoverview Combining multiple typography utility categories is discouraged in favor of composed typography utilities
3
3
  * @author Nina Repetto
4
4
  */
5
5
  'use strict';
@@ -8,21 +8,30 @@
8
8
  // Rule Definition
9
9
  // ------------------------------------------------------------------------------
10
10
 
11
+ const typographyCategories = {
12
+ 'font-weight': ['d-fw-normal', 'd-fw-medium', 'd-fw-semibold', 'd-fw-bold'],
13
+ 'font-size': ['d-fs'], // prefix match
14
+ 'line-height': ['d-lh'], // prefix match
15
+ 'font-family': ['d-ff-custom', 'd-ff-sans', 'd-ff-mono', 'd-ff-marketing', 'd-ff-unset'],
16
+ };
17
+
11
18
  module.exports = {
12
19
  meta: {
13
20
  type: 'suggestion', // `problem`, `suggestion`, or `layout`
14
21
  docs: {
15
- description: 'Utilities to set Font family, Font weight, Font size, and Line height separately are discouraged in favor of composed typography utilities',
22
+ description: 'Combining multiple typography utility categories is discouraged in favor of composed typography utilities',
16
23
  recommended: false,
17
24
  url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/recommend-typography-style.md', // URL to the documentation page for this rule
18
25
  },
19
26
  fixable: null, // Or `code` or `whitespace`
20
27
  schema: [], // Add a schema if the rule has options
21
28
  messages: {
22
- recommendTypographyStyle: `Utilities to set Font family, Font weight, Font size, and Line height separately are
23
- discouraged in favor of composed typography utilities. Checkout the available classes here:
29
+ recommendTypographyStyle: `Combining multiple typography utility categories (Font family, Font weight, Font size, Line height) is
30
+ discouraged in favor of composed typography utilities. Check out the available classes here:
24
31
  https://dialtone.dialpad.com/design/typography/#api. There can be cases where using these utilities is intentional and valid,
25
32
  in which case you can ignore this lint warning.`,
33
+ conflictingTypographyUtilities: `Conflicting typography utilities detected: multiple {{category}} classes found ({{classes}}).
34
+ Only one will be applied. Remove the conflicting classes.`,
26
35
  }, // Add messageId and message
27
36
  },
28
37
 
@@ -33,28 +42,43 @@ module.exports = {
33
42
  VAttribute (node) {
34
43
  if (node.key.name === 'class') {
35
44
  const classes = node.value.value.split(' ');
36
- const typographyClasses = [
37
- 'd-fs',
38
- 'd-fw-normal',
39
- 'd-fw-medium',
40
- 'd-fw-semibold',
41
- 'd-fw-bold',
42
- 'd-lh',
43
- 'd-ff-custom',
44
- 'd-ff-sans',
45
- 'd-ff-mono',
46
- 'd-ff-marketing',
47
- 'd-ff-unset',
48
- ];
49
- const typographyClassesFound = classes.filter((className) =>
50
- typographyClasses.some((typographyClass) => className.includes(typographyClass)),
51
- );
52
- if (typographyClassesFound.length > 0) {
45
+
46
+ // For each class, determine which category it belongs to and track all matches
47
+ const categoryCounts = {};
48
+ classes.forEach((className) => {
49
+ for (const [category, patterns] of Object.entries(typographyCategories)) {
50
+ if (patterns.some((pattern) => className.startsWith(pattern))) {
51
+ if (!categoryCounts[category]) {
52
+ categoryCounts[category] = [];
53
+ }
54
+ categoryCounts[category].push(className);
55
+ }
56
+ }
57
+ });
58
+
59
+ const categoriesFound = Object.keys(categoryCounts);
60
+
61
+ // Report if 2+ different categories are present
62
+ if (categoriesFound.length >= 2) {
53
63
  context.report({
54
64
  node,
55
65
  messageId: 'recommendTypographyStyle',
56
66
  });
57
67
  }
68
+
69
+ // Report conflicting utilities within the same category
70
+ for (const [category, matchedClasses] of Object.entries(categoryCounts)) {
71
+ if (matchedClasses.length >= 2) {
72
+ context.report({
73
+ node,
74
+ messageId: 'conflictingTypographyUtilities',
75
+ data: {
76
+ category,
77
+ classes: matchedClasses.join(', '),
78
+ },
79
+ });
80
+ }
81
+ }
58
82
  }
59
83
  },
60
84
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dialpad/eslint-plugin-dialtone",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "description": "dialtone eslint plugin",
5
5
  "keywords": [
6
6
  "Dialpad",
@@ -35,6 +35,11 @@
35
35
  "email": "dialtone@dialpad.com"
36
36
  },
37
37
  "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/dialpad/dialtone.git",
41
+ "directory": "packages/eslint-plugin-dialtone"
42
+ },
38
43
  "main": "./lib/index.js",
39
44
  "files": [
40
45
  "lib"