@dialpad/eslint-plugin-dialtone 1.12.0-next.3 → 1.12.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.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @fileoverview Detects usages of deprecated Dialtone components that should be replaced by newer alternatives.
3
+ * @author Brad Paugh
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ //------------------------------------------------------------------------------
10
+
11
+ /** @type {import('eslint').Rule.RuleModule} */
12
+ module.exports = {
13
+ meta: {
14
+ type: 'suggestion', // `problem`, `suggestion`, or `layout`
15
+ docs: {
16
+ description: "Detects usages of deprecated Dialtone components that should be replaced by newer alternatives",
17
+ recommended: false,
18
+ url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-dialtone-component.md',
19
+ },
20
+ fixable: null, // Or `code` or `whitespace`
21
+ schema: [], // Add a schema if the rule has options
22
+ messages: {
23
+ deprecatedDialtoneComponent: '{{ componentName }} is deprecated. Replace with {{ replacement }} from {{ package }}.',
24
+ deprecatedDtIcon: 'DtIcon is deprecated. Import icon components directly instead, for example: import { DtIconPhoneHangUp } from \'@dialpad/dialtone-icons/vue3\'.',
25
+ }
26
+ },
27
+
28
+ create(context) {
29
+ const deprecatedIconComponent = 'DtIcon';
30
+
31
+ const deprecatedComponents = [
32
+ { componentName: 'DtRecipeComboboxMultiSelect', replacement: 'DtComboboxMultiSelect', package: '@dialpad/dialtone' },
33
+ { componentName: 'DtRecipeComboboxWithPopover', replacement: 'DtComboboxWithPopover', package: '@dialpad/dialtone' },
34
+ { componentName: 'DtRecipeMotionText', replacement: 'DtMotionText', package: '@dialpad/dialtone' },
35
+ { componentName: 'DtRecipeCallbarButton', replacement: 'DpCallbarButton', package: '@dialpad/callbarkit' },
36
+ { componentName: 'DtRecipeCallbarButtonWithPopover', replacement: 'DpCallbarButtonWithPopover', package: '@dialpad/callbarkit' },
37
+ { componentName: 'DtRecipeCallbarButtonWithDropdown', replacement: 'DpCallbarButtonWithDropdown', package: '@dialpad/callbarkit' },
38
+ { componentName: 'DtRecipeGroupedChip', replacement: 'DpGroupedChip', package: '@dialpad/callbarkit' },
39
+ { componentName: 'DtRecipeTopBannerInfo', replacement: 'DpTopBannerInfo', package: '@dialpad/callbarkit' },
40
+ { componentName: 'DtRecipeAttachmentCarousel', replacement: 'DpAttachmentCarousel', package: '@dialpad/chatkit' },
41
+ { componentName: 'DtRecipeMessageInput', replacement: 'DpMessageInput', package: '@dialpad/chatkit' },
42
+ { componentName: 'DtRecipeContactInfo', replacement: 'DpContactInfo', package: '@dialpad/chatkit' },
43
+ { componentName: 'DtRecipeEditor', replacement: 'DpEditor', package: '@dialpad/chatkit' },
44
+ { componentName: 'DtRecipeEmojiRow', replacement: 'DpEmojiRow', package: '@dialpad/chatkit' },
45
+ { componentName: 'DtRecipeFeedItemPill', replacement: 'DpFeedItemPill', package: '@dialpad/chatkit' },
46
+ { componentName: 'DtRecipeFeedItemRow', replacement: 'DpFeedItemRow', package: '@dialpad/chatkit' },
47
+ { componentName: 'DtRecipeContactCentersRow', replacement: 'DpContactCentersRow', package: '@dialpad/navigationkit' },
48
+ { componentName: 'DtRecipeContactRow', replacement: 'DpContactRow', package: '@dialpad/navigationkit' },
49
+ { componentName: 'DtRecipeGeneralRow', replacement: 'DpGeneralRow', package: '@dialpad/navigationkit' },
50
+ { componentName: 'DtRecipeGroupRow', replacement: 'DpGroupRow', package: '@dialpad/navigationkit' },
51
+ { componentName: 'DtRecipeUnreadPill', replacement: 'DpUnreadPill', package: '@dialpad/navigationkit' },
52
+ { componentName: 'DtRecipeCallbox', replacement: 'DpCallbox', package: '@dialpad/navigationkit' },
53
+ { componentName: 'DtRecipeSettingsMenuButton', replacement: 'DpSettingsMenuButton', package: '@dialpad/navigationkit' },
54
+ { componentName: 'DtRecipeIvrNode', replacement: 'DpIvrNode', package: '@dialpad/workflowkit' },
55
+ ];
56
+
57
+ //----------------------------------------------------------------------
58
+ // Public
59
+ //----------------------------------------------------------------------
60
+
61
+ return {
62
+ ImportDeclaration(node) {
63
+ const importPath = node.source.value;
64
+
65
+ const isDialtoneSource = /^@dialpad\/dialtone(?:-vue)?(?:$|\/)/.test(importPath);
66
+ if (!isDialtoneSource) {
67
+ return;
68
+ }
69
+
70
+ node.specifiers.forEach(specifier => {
71
+ if (specifier.type !== 'ImportSpecifier') return;
72
+
73
+ const importedName = specifier.imported.name;
74
+
75
+ if (importedName === deprecatedIconComponent) {
76
+ context.report({
77
+ node: specifier,
78
+ messageId: 'deprecatedDtIcon',
79
+ });
80
+ return;
81
+ }
82
+
83
+ const found = deprecatedComponents.find(item => item.componentName === importedName);
84
+
85
+ if (found) {
86
+ context.report({
87
+ node: specifier,
88
+ messageId: 'deprecatedDialtoneComponent',
89
+ data: {
90
+ componentName: found.componentName,
91
+ replacement: found.replacement,
92
+ package: found.package,
93
+ }
94
+ });
95
+ }
96
+ });
97
+ },
98
+ };
99
+ },
100
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dialpad/eslint-plugin-dialtone",
3
- "version": "1.12.0-next.3",
3
+ "version": "1.12.0",
4
4
  "description": "dialtone eslint plugin",
5
5
  "keywords": [
6
6
  "Dialpad",
@@ -1,77 +0,0 @@
1
- /**
2
- * @fileoverview Detects usage of deprecated xxl/xxxl headline sizes which have been renamed to 2xl/3xl.
3
- * @author Dialtone Team
4
- */
5
- 'use strict';
6
-
7
- // ------------------------------------------------------------------------------
8
- // Rule Definition
9
- // ------------------------------------------------------------------------------
10
-
11
- const SIZE_MAP = {
12
- xxxl: '3xl',
13
- xxl: '2xl',
14
- };
15
-
16
- module.exports = {
17
- meta: {
18
- type: 'suggestion',
19
- docs: {
20
- description: 'Headline sizes xxl/xxxl have been renamed to 2xl/3xl.',
21
- recommended: false,
22
- url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-headline-sizes.md',
23
- },
24
- fixable: 'code',
25
- schema: [],
26
- messages: {
27
- deprecatedSize: 'Headline size "{{oldSize}}" has been renamed to "{{newSize}}". Update size="{{oldSize}}" to size="{{newSize}}".',
28
- deprecatedClass: 'CSS class "{{oldClass}}" has been renamed to "{{newClass}}". Update to use the new class name.',
29
- },
30
- },
31
-
32
- create(context) {
33
- const sourceCode = context.sourceCode ?? context.getSourceCode();
34
- return sourceCode.parserServices.defineTemplateBodyVisitor({
35
- VAttribute(node) {
36
- // Check size prop (e.g., size="xxl" or size="xxxl")
37
- if (node.key.name === 'size' && node.value && node.value.value) {
38
- const sizeValue = node.value.value;
39
- if (SIZE_MAP[sizeValue]) {
40
- context.report({
41
- node,
42
- messageId: 'deprecatedSize',
43
- data: {
44
- oldSize: sizeValue,
45
- newSize: SIZE_MAP[sizeValue],
46
- },
47
- fix(fixer) {
48
- return fixer.replaceText(node.value, `"${SIZE_MAP[sizeValue]}"`);
49
- },
50
- });
51
- }
52
- }
53
-
54
- // Check class attributes for deprecated d-text-headline--xxl/xxxl classes
55
- // Note: We only flag d-text-headline-- (current system), NOT d-headline-- (legacy system)
56
- if (node.key.name === 'class' && node.value && node.value.value) {
57
- const classValue = node.value.value;
58
- // Match d-text-headline--xxl or d-text-headline--xxxl (but NOT d-headline--xxl)
59
- const match = classValue.match(/\bd-text-headline--(xxx?l)\b/);
60
- if (match && SIZE_MAP[match[1]]) {
61
- const oldClass = `d-text-headline--${match[1]}`;
62
- const newClass = `d-text-headline--${SIZE_MAP[match[1]]}`;
63
- context.report({
64
- node,
65
- messageId: 'deprecatedClass',
66
- data: { oldClass, newClass },
67
- fix(fixer) {
68
- const newValue = classValue.replace(oldClass, newClass);
69
- return fixer.replaceText(node.value, `"${newValue}"`);
70
- },
71
- });
72
- }
73
- }
74
- },
75
- });
76
- },
77
- };
@@ -1,228 +0,0 @@
1
- /**
2
- * @fileoverview Detect deprecated physical direction names in Dialtone component
3
- * slots, props, prop values, and events. Suggests logical replacements.
4
- */
5
- 'use strict';
6
-
7
- //------------------------------------------------------------------------------
8
- // Constants
9
- //------------------------------------------------------------------------------
10
-
11
- /**
12
- * Maps component names to their deprecated slot names and replacements.
13
- * null value = special handling (ambiguous, e.g. #icon on dt-button).
14
- */
15
- const DEPRECATED_SLOTS = {
16
- 'dt-badge': { leftIcon: 'startIcon', rightIcon: 'endIcon' },
17
- 'dt-button': { icon: null },
18
- 'dt-input': { leftIcon: 'startIcon', rightIcon: 'endIcon' },
19
- 'dt-tab': { leftIcon: 'startIcon' },
20
- 'dt-split-button': { alphaIcon: 'startIcon', omegaIcon: 'endIcon', omega: 'end' },
21
- 'dt-item-layout': { left: 'start', right: 'end', bottom: 'blockEnd' },
22
- 'dt-recipe-callbox': { right: 'end', bottom: 'blockEnd' },
23
- 'dt-recipe-contact-centers-row': { right: 'end' },
24
- 'dt-recipe-general-row': { left: 'start' },
25
- 'dt-recipe-top-banner-info': { left: 'start', right: 'end' },
26
- 'dt-recipe-grouped-chip': {
27
- leftIcon: 'startIcon',
28
- rightIcon: 'endIcon',
29
- leftContent: 'startContent',
30
- rightContent: 'endContent',
31
- },
32
- };
33
-
34
- /**
35
- * Maps component names to their deprecated prop names (kebab-case) and replacements.
36
- */
37
- const DEPRECATED_PROPS = {
38
- 'dt-item-layout': {
39
- 'left-class': 'start-class',
40
- 'right-class': 'end-class',
41
- 'bottom-class': 'block-end-class',
42
- },
43
- 'dt-split-button': {
44
- 'alpha-active': 'start-active',
45
- 'alpha-aria-label': 'start-aria-label',
46
- 'alpha-icon-position': 'start-icon-position',
47
- 'alpha-leading-class': 'start-leading-class',
48
- 'alpha-trailing-class': 'start-trailing-class',
49
- 'alpha-label-class': 'start-label-class',
50
- 'alpha-disabled': 'start-disabled',
51
- 'alpha-loading': 'start-loading',
52
- 'alpha-tooltip-text': 'start-tooltip-text',
53
- 'omega-active': 'end-active',
54
- 'omega-aria-label': 'end-aria-label',
55
- 'omega-disabled': 'end-disabled',
56
- 'omega-id': 'end-id',
57
- 'omega-tooltip-text': 'end-tooltip-text',
58
- },
59
- };
60
-
61
- /**
62
- * Maps component names to props whose specific values are deprecated.
63
- */
64
- const DEPRECATED_PROP_VALUES = {
65
- 'dt-button': {
66
- 'icon-position': { left: 'start', right: 'end', top: 'blockStart', bottom: 'blockEnd' },
67
- },
68
- 'dt-root-layout': {
69
- 'sidebar-position': { left: 'start', right: 'end' },
70
- },
71
- };
72
-
73
- /**
74
- * Maps component names to their deprecated event names and replacements.
75
- */
76
- const DEPRECATED_EVENTS = {
77
- 'dt-split-button': { 'alpha-clicked': 'start-clicked', 'omega-clicked': 'end-clicked' },
78
- };
79
-
80
- //------------------------------------------------------------------------------
81
- // Helpers
82
- //------------------------------------------------------------------------------
83
-
84
- /**
85
- * Converts PascalCase or camelCase to kebab-case.
86
- * e.g. 'DtBadge' → 'dt-badge', 'DtRecipeCallbox' → 'dt-recipe-callbox'
87
- */
88
- function toKebabCase (str) {
89
- return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
90
- }
91
-
92
- //------------------------------------------------------------------------------
93
- // Rule Definition
94
- //------------------------------------------------------------------------------
95
-
96
- module.exports = {
97
- meta: {
98
- type: 'suggestion',
99
- docs: {
100
- description: 'Detect deprecated physical direction names in Dialtone component slots, props, prop values, and events',
101
- recommended: false,
102
- url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-physical-naming.md',
103
- },
104
- fixable: null,
105
- schema: [],
106
- messages: {
107
- deprecatedSlot:
108
- '#{{ oldSlot }} on <{{ component }}> is deprecated. Use #{{ newSlot }} instead.',
109
- deprecatedIconSlot:
110
- 'The #icon slot on <dt-button> is deprecated. Use #startIcon, #endIcon, #blockStartIcon, or #blockEndIcon instead.',
111
- deprecatedProp:
112
- '{{ oldProp }} on <{{ component }}> is deprecated. Use {{ newProp }} instead.',
113
- deprecatedPropValue:
114
- '{{ prop }}="{{ oldValue }}" on <{{ component }}> is deprecated. Use {{ prop }}="{{ newValue }}" instead.',
115
- deprecatedEvent:
116
- '@{{ oldEvent }} on <{{ component }}> is deprecated. Use @{{ newEvent }} instead.',
117
- },
118
- },
119
-
120
- create (context) {
121
- const sourceCode = context.sourceCode ?? context.getSourceCode();
122
- return sourceCode.parserServices.defineTemplateBodyVisitor({
123
-
124
- VElement (node) {
125
- const rawName = node.rawName || node.name;
126
- const elementName = rawName.includes('-') ? rawName : toKebabCase(rawName);
127
-
128
- const slotsMap = DEPRECATED_SLOTS[elementName];
129
- const propsMap = DEPRECATED_PROPS[elementName];
130
- const propValuesMap = DEPRECATED_PROP_VALUES[elementName];
131
- const eventsMap = DEPRECATED_EVENTS[elementName];
132
-
133
- // Check attributes in a single pass (props, prop values, events)
134
- if (propsMap || propValuesMap || eventsMap) {
135
- for (const attr of node.startTag.attributes) {
136
- if (!attr.directive) {
137
- // Static props: alpha-active, left-class="x", icon-position="left"
138
- const attrName = attr.key && (attr.key.rawName || attr.key.name);
139
- if (attrName && propsMap && propsMap[attrName]) {
140
- context.report({
141
- node: attr,
142
- messageId: 'deprecatedProp',
143
- data: {
144
- component: elementName,
145
- oldProp: attrName,
146
- newProp: propsMap[attrName],
147
- },
148
- });
149
- }
150
- if (attrName && propValuesMap && propValuesMap[attrName] && attr.value) {
151
- const val = attr.value.value;
152
- if (propValuesMap[attrName][val]) {
153
- context.report({
154
- node: attr,
155
- messageId: 'deprecatedPropValue',
156
- data: {
157
- component: elementName,
158
- prop: attrName,
159
- oldValue: val,
160
- newValue: propValuesMap[attrName][val],
161
- },
162
- });
163
- }
164
- }
165
- } else if (attr.directive && attr.key.name.name === 'bind') {
166
- // Dynamic props: :alpha-active="x", v-bind:omega-disabled="y"
167
- const bindName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
168
- if (bindName && propsMap && propsMap[bindName]) {
169
- context.report({
170
- node: attr,
171
- messageId: 'deprecatedProp',
172
- data: {
173
- component: elementName,
174
- oldProp: bindName,
175
- newProp: propsMap[bindName],
176
- },
177
- });
178
- }
179
- } else if (eventsMap && attr.key.name.name === 'on') {
180
- const eventName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
181
- if (eventName && eventsMap[eventName]) {
182
- context.report({
183
- node: attr,
184
- messageId: 'deprecatedEvent',
185
- data: {
186
- component: elementName,
187
- oldEvent: eventName,
188
- newEvent: eventsMap[eventName],
189
- },
190
- });
191
- }
192
- }
193
- }
194
- }
195
-
196
- // Check child <template> elements for deprecated slot names
197
- if (slotsMap) {
198
- for (const child of node.children) {
199
- if (child.type === 'VElement' && child.name === 'template') {
200
- for (const attr of child.startTag.attributes) {
201
- if (attr.directive && attr.key.name.name === 'slot') {
202
- const slotName = attr.key.argument && (attr.key.argument.rawName || attr.key.argument.name);
203
- if (slotName && slotName in slotsMap) {
204
- if (slotsMap[slotName] === null) {
205
- // #icon on dt-button is ambiguous — consumer must choose replacement
206
- context.report({ node: attr, messageId: 'deprecatedIconSlot' });
207
- } else {
208
- context.report({
209
- node: attr,
210
- messageId: 'deprecatedSlot',
211
- data: {
212
- component: elementName,
213
- oldSlot: slotName,
214
- newSlot: slotsMap[slotName],
215
- },
216
- });
217
- }
218
- }
219
- break;
220
- }
221
- }
222
- }
223
- }
224
- }
225
- },
226
- });
227
- },
228
- };
@@ -1,73 +0,0 @@
1
- /**
2
- * @fileoverview Detects usage of pixel-based utility classes (d-h16, d-p8, d-m8, etc.)
3
- * which should be replaced with token-stop-based equivalents (d-h-25, d-p-100, d-m-100).
4
- * @author Joshua Hynes
5
- */
6
- "use strict";
7
-
8
- //------------------------------------------------------------------------------
9
- // Rule Definition
10
- //------------------------------------------------------------------------------
11
-
12
- // Pixel values that have token-stop equivalents
13
- // MUST STAY IN SYNC with WIDTH_HEIGHTS_LAYOUT, MARGIN_SIZES_SPACING, MARGIN_SIZES_LAYOUT,
14
- // and NEGATIVE_SPACING_MAP in dialtone-css/postcss/constants.cjs
15
- // Sizing: layout stops (16px+)
16
- const SIZING_PIXELS = '16|32|48|64|80|96|112|128|160|192|224|256|288|320|352|384|416|448|480|512|544|576|608|640|672|704|736|768|800|832|864|896|928|960|992|1024';
17
- // Spacing: spacing stops (0-64px) + layout stops for margin/padding (96, 128)
18
- const SPACING_PIXELS = '0|1|2|4|6|8|10|12|14|16|20|24|32|48|64|96|128';
19
- // Negative spacing
20
- const NEGATIVE_PIXELS = '1|2|4|6|8|10|12|14|16|24|32|48|64';
21
-
22
- // Build patterns for each category
23
- // Sizing: d-h16, d-w64, d-hmn96, d-hmx128, d-wmn32, d-wmx512
24
- const SIZING_PATTERN = `d-(?:h|w|hmn|hmx|wmn|wmx)(?:${SIZING_PIXELS})\\b`;
25
- // Margin: d-m8, d-mt16, d-mr8, d-mb8, d-ml8, d-mx8, d-my8
26
- const MARGIN_PATTERN = `d-m(?:t|r|b|l|x|y)?(?:${SPACING_PIXELS})\\b`;
27
- // Negative margin: d-mtn8, d-mrn8, d-mbn8, d-mln8, d-mxn8, d-myn8, d-mn8
28
- const NEGATIVE_MARGIN_PATTERN = `d-m(?:t|r|b|l|x|y)?n(?:${NEGATIVE_PIXELS})\\b`;
29
- // Padding: d-p8, d-pt16, d-pr8, d-pb8, d-pl8, d-px8, d-py8
30
- const PADDING_PATTERN = `d-p(?:t|r|b|l|x|y)?(?:${SPACING_PIXELS})\\b`;
31
- // Gap: d-g8, d-rg8, d-cg8
32
- const GAP_PATTERN = `d-(?:g|rg|cg)(?:${SPACING_PIXELS})\\b`;
33
- // Position: d-t8, d-r8, d-b8, d-l8, d-x8, d-y8, d-all8
34
- const POSITION_PATTERN = `d-(?:t|r|b|l|x|y|all)(?:${SPACING_PIXELS})\\b`;
35
- // Negative position: d-tn8, d-rn8, d-bn8, d-ln8, d-xn8, d-yn8, d-alln8
36
- const NEGATIVE_POSITION_PATTERN = `d-(?:t|r|b|l|x|y|all)n(?:${NEGATIVE_PIXELS})\\b`;
37
-
38
- const COMBINED_PATTERN = new RegExp(
39
- `(?:${SIZING_PATTERN}|${NEGATIVE_MARGIN_PATTERN}|${MARGIN_PATTERN}|${PADDING_PATTERN}|${GAP_PATTERN}|${NEGATIVE_POSITION_PATTERN}|${POSITION_PATTERN})`
40
- );
41
-
42
- module.exports = {
43
- meta: {
44
- type: 'suggestion',
45
- docs: {
46
- description: "Pixel-based utility classes (d-h16, d-p8, d-m8) are deprecated. Use token-stop-based equivalents (d-h-25, d-p-100, d-m-100).",
47
- recommended: false,
48
- url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-pixel-utility-classes.md',
49
- },
50
- fixable: null,
51
- schema: [],
52
- messages: {
53
- deprecatedPixelClass: `Pixel-based utility classes are deprecated. Use token-stop-based equivalents instead (e.g. d-h16 → d-h-25, d-p8 → d-p-100). Run the "utility-class-to-token-stops" migration helper to update automatically.`,
54
- },
55
- },
56
-
57
- create (context) {
58
- const sourceCode = context.sourceCode ?? context.getSourceCode();
59
- return sourceCode.parserServices.defineTemplateBodyVisitor({
60
- VAttribute (node) {
61
- if (node.key.name === 'class') {
62
- const classes = node.value.value;
63
- if (COMBINED_PATTERN.test(classes)) {
64
- context.report({
65
- node,
66
- messageId: 'deprecatedPixelClass',
67
- });
68
- }
69
- }
70
- },
71
- });
72
- },
73
- };
@@ -1,136 +0,0 @@
1
- /**
2
- * @fileoverview Detects usage of deprecated t-shirt size props (xs, sm, md, lg, xl) on Dialtone
3
- * components and suggests numeric equivalents (100, 200, 300, 400, 500).
4
- * @author Dialtone Team
5
- */
6
- 'use strict';
7
-
8
- // ------------------------------------------------------------------------------
9
- // Rule Definition
10
- // ------------------------------------------------------------------------------
11
-
12
- const SIZE_MAP = {
13
- xs: '100',
14
- sm: '200',
15
- md: '300',
16
- lg: '400',
17
- xl: '500',
18
- '2xl': '600',
19
- '3xl': '700',
20
- };
21
-
22
- const TSHIRT_VALUES = new Set(Object.keys(SIZE_MAP));
23
-
24
- // Props that accept the component size scale
25
- const SIZE_PROPS = ['size', 'label-size', 'labelSize'];
26
-
27
- // Speed prop on motion-text also uses the same scale
28
- const SPEED_PROPS = ['speed'];
29
-
30
- // All size-related prop names
31
- const ALL_SIZE_PROPS = [...SIZE_PROPS, ...SPEED_PROPS];
32
-
33
- // Only flag on Dialtone components (dt-* or Dt*)
34
- function isDialtoneComponent (node) {
35
- const parent = node.parent;
36
- if (!parent || parent.type !== 'VStartTag') return false;
37
- const element = parent.parent;
38
- if (!element || element.type !== 'VElement') return false;
39
- const name = element.rawName || element.name || '';
40
- return name.startsWith('dt-') || name.startsWith('Dt');
41
- }
42
-
43
- module.exports = {
44
- meta: {
45
- type: 'suggestion',
46
- docs: {
47
- description: 'T-shirt sizes (xs, sm, md, lg, xl) are deprecated. Use numeric scale (100, 200, 300, 400, 500) instead.',
48
- recommended: false,
49
- url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/deprecated-tshirt-sizes.md',
50
- },
51
- fixable: 'code',
52
- schema: [],
53
- messages: {
54
- deprecatedSize: 'Size "{{oldSize}}" is deprecated. Use :{{prop}}="{{newSize}}" instead.',
55
- deprecatedSizeInBinding: 'T-shirt size "{{oldSize}}" in dynamic binding is deprecated. Use numeric {{newSize}} instead.',
56
- },
57
- },
58
-
59
- create (context) {
60
- const sourceCode = context.sourceCode ?? context.getSourceCode();
61
- return sourceCode.parserServices.defineTemplateBodyVisitor({
62
- VAttribute (node) {
63
- if (!isDialtoneComponent(node)) return;
64
-
65
- // Get the prop name and check if it's a size-related prop
66
- const isDirective = node.directive;
67
- const propName = isDirective
68
- ? (node.key.argument && node.key.argument.name)
69
- : node.key.name;
70
-
71
- if (!propName || !ALL_SIZE_PROPS.includes(propName)) return;
72
-
73
- // --- Static attributes: size="sm" → auto-fixable ---
74
- if (!isDirective && node.value && node.value.value) {
75
- const sizeValue = node.value.value;
76
- if (SIZE_MAP[sizeValue]) {
77
- context.report({
78
- node,
79
- messageId: 'deprecatedSize',
80
- data: {
81
- oldSize: sizeValue,
82
- newSize: SIZE_MAP[sizeValue],
83
- prop: propName,
84
- },
85
- fix (fixer) {
86
- const newAttr = `:${propName}="${SIZE_MAP[sizeValue]}"`;
87
- return fixer.replaceTextRange(
88
- [node.range[0], node.range[1]],
89
- newAttr,
90
- );
91
- },
92
- });
93
- }
94
- return;
95
- }
96
-
97
- // --- Dynamic bindings: :size="'sm'" or :size="x ? 'sm' : 'md'" ---
98
- if (isDirective && node.value && node.value.expression) {
99
- // Walk the expression tree for string literals with t-shirt values
100
- const expression = node.value.expression;
101
- const literals = [];
102
-
103
- (function findLiterals (n) {
104
- if (!n) return;
105
- if (n.type === 'Literal' && typeof n.value === 'string' && TSHIRT_VALUES.has(n.value)) {
106
- literals.push(n);
107
- }
108
- // Walk child nodes
109
- for (const key of Object.keys(n)) {
110
- if (key === 'parent') continue;
111
- const child = n[key];
112
- if (child && typeof child === 'object') {
113
- if (Array.isArray(child)) {
114
- child.forEach(c => { if (c && c.type) findLiterals(c); });
115
- } else if (child.type) {
116
- findLiterals(child);
117
- }
118
- }
119
- }
120
- })(expression);
121
-
122
- for (const literal of literals) {
123
- context.report({
124
- node: literal,
125
- messageId: 'deprecatedSizeInBinding',
126
- data: {
127
- oldSize: literal.value,
128
- newSize: SIZE_MAP[literal.value],
129
- },
130
- });
131
- }
132
- }
133
- },
134
- });
135
- },
136
- };
@@ -1,52 +0,0 @@
1
- /**
2
- * @fileoverview Warns when v-dt-focusgroup is used without an accessible label on the same element.
3
- * @author Dialtone
4
- */
5
- 'use strict';
6
-
7
- module.exports = {
8
- meta: {
9
- type: 'suggestion',
10
- docs: {
11
- description:
12
- 'Warns when v-dt-focusgroup is used on an element without aria-label or aria-labelledby. ' +
13
- 'Screen readers need an accessible name to identify the widget.',
14
- recommended: false,
15
- url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-label.md',
16
- },
17
- fixable: null,
18
- schema: [],
19
- messages: {
20
- missingLabel:
21
- 'v-dt-focusgroup requires an accessible name via "aria-label" or "aria-labelledby" ' +
22
- 'so screen readers can identify the widget.',
23
- },
24
- },
25
-
26
- create (context) {
27
- const sourceCode = context.sourceCode ?? context.getSourceCode();
28
- return sourceCode.parserServices.defineTemplateBodyVisitor({
29
- VAttribute (node) {
30
- if (!node.directive) return;
31
- if (node.key.name.name !== 'dt-focusgroup') return;
32
-
33
- const element = node.parent;
34
- const hasLabel = element.attributes.some(
35
- attr =>
36
- (!attr.directive && (
37
- attr.key.name === 'aria-label' ||
38
- attr.key.name === 'aria-labelledby'
39
- )) ||
40
- (attr.directive && attr.key.name.name === 'bind' && (
41
- attr.key.argument?.name === 'aria-label' ||
42
- attr.key.argument?.name === 'aria-labelledby'
43
- )),
44
- );
45
-
46
- if (!hasLabel) {
47
- context.report({ node, messageId: 'missingLabel' });
48
- }
49
- },
50
- });
51
- },
52
- };
@@ -1,46 +0,0 @@
1
- /**
2
- * @fileoverview Warns when v-dt-focusgroup is used without a role attribute on the same element.
3
- * @author Dialtone
4
- */
5
- 'use strict';
6
-
7
- module.exports = {
8
- meta: {
9
- type: 'suggestion',
10
- docs: {
11
- description:
12
- 'Warns when v-dt-focusgroup is used on an element without a role attribute. ' +
13
- 'Screen readers need a role to announce the widget correctly.',
14
- recommended: false,
15
- url: 'https://github.com/dialpad/dialtone/blob/staging/packages/eslint-plugin-dialtone/docs/rules/focusgroup-requires-role.md',
16
- },
17
- fixable: null,
18
- schema: [],
19
- messages: {
20
- missingRole:
21
- 'v-dt-focusgroup requires a "role" attribute (e.g. toolbar, tablist, listbox, radiogroup, menu) ' +
22
- 'so screen readers can announce the widget correctly.',
23
- },
24
- },
25
-
26
- create (context) {
27
- const sourceCode = context.sourceCode ?? context.getSourceCode();
28
- return sourceCode.parserServices.defineTemplateBodyVisitor({
29
- VAttribute (node) {
30
- if (!node.directive) return;
31
- if (node.key.name.name !== 'dt-focusgroup') return;
32
-
33
- const element = node.parent;
34
- const hasRole = element.attributes.some(
35
- attr =>
36
- (!attr.directive && attr.key.name === 'role') ||
37
- (attr.directive && attr.key.name.name === 'bind' && attr.key.argument?.name === 'role'),
38
- );
39
-
40
- if (!hasRole) {
41
- context.report({ node, messageId: 'missingRole' });
42
- }
43
- },
44
- });
45
- },
46
- };