@adyen/eslint-plugin-bento 2.7.6 → 2.8.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.
- package/CHANGELOG.md +16 -0
- package/README.md +48 -15
- package/configs/tailwind.js +17 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/rules/tailwind-v3-to-bento/README.md +114 -0
- package/rules/tailwind-v3-to-bento/__tests__/eslint.config.js +24 -0
- package/rules/tailwind-v3-to-bento/__tests__/tailwind-v3-to-bento.vue +15 -0
- package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.integration.test.ts +29 -0
- package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.js +377 -0
- package/rules/tailwind-v3-to-bento/tailwind-v3-to-bento.test.ts +403 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.8.0 (2026-04-15)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- added `tailwind-v3-to-bento` ESlint rule to help with the migration to Bento's Tailwind plugin ([d941b691a](https://github.com/Adyen/bento/commit/d941b691a))
|
|
8
|
+
|
|
9
|
+
### ❤️ Thank You
|
|
10
|
+
|
|
11
|
+
- daver
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## 2.7.7 (2026-04-08)
|
|
15
|
+
|
|
16
|
+
This was a version bump only for eslint to align it with other projects, there were no code changes.
|
|
17
|
+
|
|
18
|
+
|
|
3
19
|
## 2.7.6 (2026-04-01)
|
|
4
20
|
|
|
5
21
|
This was a version bump only for eslint to align it with other projects, there were no code changes.
|
package/README.md
CHANGED
|
@@ -51,6 +51,22 @@ module.exports = defineConfig([
|
|
|
51
51
|
]);
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
### Flat config tailwind
|
|
55
|
+
|
|
56
|
+
For projects migrating from Tailwind CSS v3 to Bento design tokens:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
const { defineConfig } = require('eslint/config');
|
|
60
|
+
const bento = require('@adyen/eslint-plugin-bento');
|
|
61
|
+
|
|
62
|
+
module.exports = defineConfig([
|
|
63
|
+
{
|
|
64
|
+
// Other configs
|
|
65
|
+
},
|
|
66
|
+
...bento.configs['tailwind'],
|
|
67
|
+
]);
|
|
68
|
+
```
|
|
69
|
+
|
|
54
70
|
### Custom
|
|
55
71
|
|
|
56
72
|
Without the recommended configs:
|
|
@@ -70,8 +86,9 @@ module.exports = defineConfig([
|
|
|
70
86
|
},
|
|
71
87
|
rules: {
|
|
72
88
|
'@adyen/bento/vue-old-spacer-tokens': 'warn',
|
|
73
|
-
'@adyen/bento/deprecation-currency-default-slot': 'warn'
|
|
74
|
-
'@adyen/bento/deprecation-tag-default-slot': 'warn'
|
|
89
|
+
'@adyen/bento/deprecation-currency-default-slot': 'warn',
|
|
90
|
+
'@adyen/bento/deprecation-tag-default-slot': 'warn',
|
|
91
|
+
'@adyen/bento/tailwind-v3-to-bento': 'warn',
|
|
75
92
|
},
|
|
76
93
|
files: ['**/*.vue'],
|
|
77
94
|
},
|
|
@@ -84,28 +101,44 @@ module.exports = defineConfig([
|
|
|
84
101
|
|
|
85
102
|
Disallow the use of ADL space utility classes in favor of Bento _spacer_ utility classes.
|
|
86
103
|
|
|
87
|
-
###
|
|
104
|
+
### `@adyen/bento/deprecation-currency-default-slot`
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
Disallow the use of the `default` slot in `bento-currency` instances.
|
|
90
107
|
|
|
91
|
-
|
|
108
|
+
### `@adyen/bento/deprecation-tag-default-slot`
|
|
92
109
|
|
|
93
|
-
|
|
110
|
+
Disallow the use of the `default` slot in `bento-tag` instances.
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
### `@adyen/bento/tailwind-v3-to-bento`
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
Replace Tailwind CSS v3 utility classes with Bento design token equivalents. Covers spacing, border radius, border
|
|
115
|
+
width, box shadows, breakpoints, and colors (auto-fixable via `eslint --fix`). Typography classes are flagged with a
|
|
116
|
+
warning — use the `bento-typography` component instead.
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
## Configurations
|
|
100
119
|
|
|
101
|
-
|
|
120
|
+
### Recommended (`vue-recommended`)
|
|
102
121
|
|
|
103
|
-
|
|
122
|
+
| Rule | Severity |
|
|
123
|
+
| ------------------------------------------------ | -------- |
|
|
124
|
+
| `@adyen/bento/deprecation-components-input-emit` | `warn` |
|
|
125
|
+
| `@adyen/bento/deprecation-components-value-prop` | `warn` |
|
|
126
|
+
| `@adyen/bento/deprecation-currency-default-slot` | `warn` |
|
|
127
|
+
| `@adyen/bento/deprecation-tag-default-slot` | `warn` |
|
|
128
|
+
| `@adyen/bento/vue-old-spacer-tokens` | `warn` |
|
|
104
129
|
|
|
105
|
-
|
|
130
|
+
### Strict (`vue-strict`)
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
| Rule | Severity |
|
|
133
|
+
| ------------------------------------------------ | -------- |
|
|
134
|
+
| `@adyen/bento/deprecation-components-input-emit` | `error` |
|
|
135
|
+
| `@adyen/bento/deprecation-components-value-prop` | `error` |
|
|
136
|
+
| `@adyen/bento/deprecation-currency-default-slot` | `error` |
|
|
137
|
+
| `@adyen/bento/deprecation-tag-default-slot` | `error` |
|
|
138
|
+
| `@adyen/bento/vue-old-spacer-tokens` | `error` |
|
|
108
139
|
|
|
109
|
-
|
|
140
|
+
### Tailwind (`tailwind`)
|
|
110
141
|
|
|
111
|
-
|
|
142
|
+
| Rule | Severity |
|
|
143
|
+
| ----------------------------------- | -------- |
|
|
144
|
+
| `@adyen/bento/tailwind-v3-to-bento` | `warn` |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = [
|
|
2
|
+
{
|
|
3
|
+
name: '@bento/adyen/tailwind',
|
|
4
|
+
files: ['*.vue', '**/*.vue'],
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parser: require('vue-eslint-parser'),
|
|
7
|
+
},
|
|
8
|
+
plugins: {
|
|
9
|
+
get '@adyen/bento'() {
|
|
10
|
+
return require('../index');
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
'@adyen/bento/tailwind-v3-to-bento': 'warn',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
];
|
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const plugin = {
|
|
|
9
9
|
configs: {
|
|
10
10
|
'vue-recommended': require('./configs/vue-recommended'),
|
|
11
11
|
'vue-strict': require('./configs/vue-strict'),
|
|
12
|
+
tailwind: require('./configs/tailwind'),
|
|
12
13
|
},
|
|
13
14
|
rules: {
|
|
14
15
|
'deprecation-components-input-emit': require('./rules/deprecation-components-input-emit/deprecation-components-input-emit'),
|
|
@@ -16,6 +17,7 @@ const plugin = {
|
|
|
16
17
|
'deprecation-currency-default-slot': require('./rules/deprecation-currency-default-slot/deprecation-currency-default-slot'),
|
|
17
18
|
'deprecation-tag-default-slot': require('./rules/deprecation-tag-default-slot/deprecation-tag-default-slot'),
|
|
18
19
|
'vue-old-spacer-tokens': require('./rules/vue-old-spacer-tokens/vue-old-spacer-tokens'),
|
|
20
|
+
'tailwind-v3-to-bento': require('./rules/tailwind-v3-to-bento/tailwind-v3-to-bento'),
|
|
19
21
|
},
|
|
20
22
|
};
|
|
21
23
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @adyen/bento/tailwind-v3-to-bento
|
|
2
|
+
|
|
3
|
+
> Replace Tailwind CSS v3 utility classes with Bento design token equivalents.
|
|
4
|
+
|
|
5
|
+
- This rule is included in the `plugin:@adyen/bento/recommended` configuration
|
|
6
|
+
- The `--fix` option can automatically fix most problems reported by this rule
|
|
7
|
+
|
|
8
|
+
This rule helps developers migrate from Tailwind CSS v3 default utility classes to Bento design token equivalents. It
|
|
9
|
+
covers spacing, border radius, border width, box shadows, breakpoints, and colors. Typography classes are flagged but
|
|
10
|
+
not auto-fixed — use the `bento-typography` component instead.
|
|
11
|
+
|
|
12
|
+
## Auto-fixable mappings
|
|
13
|
+
|
|
14
|
+
### Spacing
|
|
15
|
+
|
|
16
|
+
| Before (Tailwind 3) | After (Bento) | Value |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `p-0` | `p-000` | 0px |
|
|
19
|
+
| `p-0.5` | `p-010` | 2px |
|
|
20
|
+
| `p-1` | `p-020` | 4px |
|
|
21
|
+
| `p-1.5` | `p-030` | 6px |
|
|
22
|
+
| `p-2` | `p-040` | 8px |
|
|
23
|
+
| `p-2.5` | `p-050` | 10px |
|
|
24
|
+
| `p-3` | `p-060` | 12px |
|
|
25
|
+
| `p-4` | `p-070` | 16px |
|
|
26
|
+
| `p-5` | `p-080` | 20px |
|
|
27
|
+
| `p-6` | `p-090` | 24px |
|
|
28
|
+
| `p-8` | `p-100` | 32px |
|
|
29
|
+
| `p-10` | `p-110` | 40px |
|
|
30
|
+
| `p-12` | `p-120` | 48px |
|
|
31
|
+
| `p-14` | `p-130` | 56px |
|
|
32
|
+
| `p-16` | `p-140` | 64px |
|
|
33
|
+
|
|
34
|
+
The same mapping applies to all spacing utilities (`m-*`, `mx-*`, `my-*`, `gap-*`, `space-x-*`, `space-y-*`, `w-*`,
|
|
35
|
+
`h-*`, `inset-*`, `top-*`, `right-*`, `bottom-*`, `left-*`, etc.).
|
|
36
|
+
|
|
37
|
+
### Border radius
|
|
38
|
+
|
|
39
|
+
| Before | After | Value |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `rounded` | `rounded-s` | 4px |
|
|
42
|
+
| `rounded-sm` | `rounded-xs` | 2px |
|
|
43
|
+
| `rounded-md` | `rounded-m` | 8px |
|
|
44
|
+
| `rounded-lg` | `rounded-l` | 12px |
|
|
45
|
+
| `rounded-xl` | `rounded-xl` | 24px |
|
|
46
|
+
|
|
47
|
+
### Border width
|
|
48
|
+
|
|
49
|
+
| Before | After | Value |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `border` | `border-s` | 1px |
|
|
52
|
+
| `border-2` | `border-m` | 2px |
|
|
53
|
+
| `border-4` | `border-l` | 3px |
|
|
54
|
+
|
|
55
|
+
### Box shadows
|
|
56
|
+
|
|
57
|
+
| Before | After |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `shadow` | `shadow-medium` |
|
|
60
|
+
| `shadow-sm` | `shadow-low` |
|
|
61
|
+
| `shadow-md` | `shadow-medium` |
|
|
62
|
+
| `shadow-lg` | `shadow-high` |
|
|
63
|
+
| `shadow-xl` | `shadow-high` |
|
|
64
|
+
| `shadow-2xl` | `shadow-high` |
|
|
65
|
+
|
|
66
|
+
### Breakpoints
|
|
67
|
+
|
|
68
|
+
| Before | After | Value |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `sm:` | `s-min:` | min-width: 376px |
|
|
71
|
+
| `md:` | `m-min:` | min-width: 500px |
|
|
72
|
+
| `lg:` | `l-min:` | min-width: 768px |
|
|
73
|
+
| `xl:` | `xl-min:` | min-width: 1280px |
|
|
74
|
+
| `2xl:` | `xxl:` | min-width: 1536px |
|
|
75
|
+
|
|
76
|
+
### Colors (best-effort)
|
|
77
|
+
|
|
78
|
+
Color mappings are best-effort since Tailwind 3 uses raw color values while Bento uses semantic tokens. Examples:
|
|
79
|
+
|
|
80
|
+
| Before | After |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `text-red-500` | `text-label-critical` |
|
|
83
|
+
| `text-green-500` | `text-label-success` |
|
|
84
|
+
| `text-blue-500` | `text-label-highlight` |
|
|
85
|
+
| `text-gray-500` | `text-label-secondary` |
|
|
86
|
+
| `bg-gray-100` | `bg-secondary` |
|
|
87
|
+
| `bg-red-100` | `bg-critical-weak` |
|
|
88
|
+
| `border-gray-300` | `border-outline-primary` |
|
|
89
|
+
|
|
90
|
+
## Warn only (no auto-fix)
|
|
91
|
+
|
|
92
|
+
### Typography
|
|
93
|
+
|
|
94
|
+
Typography classes are flagged with a warning but not auto-fixed. Use the `bento-typography` component instead.
|
|
95
|
+
|
|
96
|
+
Detected patterns: `text-sm`, `text-lg`, `font-bold`, `font-sans`, `leading-tight`, `tracking-wide`, etc.
|
|
97
|
+
|
|
98
|
+
## ✗ BAD
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<template>
|
|
102
|
+
<div class="p-4 m-2 rounded-md shadow-lg sm:p-8 text-red-500">Content</div>
|
|
103
|
+
<p class="text-sm font-bold leading-tight">Hello</p>
|
|
104
|
+
</template>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## ✓ GOOD
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<template>
|
|
111
|
+
<div class="p-070 m-040 rounded-m shadow-high s-min:p-100 text-label-critical">Content</div>
|
|
112
|
+
<bento-typography variant="body" stronger>Hello</bento-typography>
|
|
113
|
+
</template>
|
|
114
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { defineConfig } = require('eslint/config');
|
|
2
|
+
const vueParser = require('vue-eslint-parser');
|
|
3
|
+
const tsParser = require('@typescript-eslint/parser');
|
|
4
|
+
const adyenBento = require('@adyen/eslint-plugin-bento');
|
|
5
|
+
|
|
6
|
+
module.exports = defineConfig([
|
|
7
|
+
{
|
|
8
|
+
languageOptions: {
|
|
9
|
+
ecmaVersion: 'latest',
|
|
10
|
+
sourceType: 'module',
|
|
11
|
+
parser: vueParser,
|
|
12
|
+
parserOptions: {
|
|
13
|
+
parser: tsParser,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
'@adyen/bento': adyenBento,
|
|
18
|
+
},
|
|
19
|
+
rules: {
|
|
20
|
+
'@adyen/bento/tailwind-v3-to-bento': 'warn',
|
|
21
|
+
},
|
|
22
|
+
files: ['**/*.vue'],
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="p-4 m-2 gap-8"></div>
|
|
4
|
+
<div class="rounded rounded-md rounded-lg"></div>
|
|
5
|
+
<div class="border border-2 border-4"></div>
|
|
6
|
+
<div class="shadow shadow-sm shadow-lg"></div>
|
|
7
|
+
<div class="sm:p-4 lg:p-8 2xl:m-2"></div>
|
|
8
|
+
<div class="text-red-500 bg-gray-100 border-gray-300"></div>
|
|
9
|
+
<div class="text-sm font-bold leading-tight tracking-wide"></div>
|
|
10
|
+
<div class="p-4 rounded-md shadow-lg sm:p-8 text-red-500"></div>
|
|
11
|
+
<div class="flex items-center justify-between"></div>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup></script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { execFileSyncEslintVueFiles } from '../../utils/exec-file-sync-eslint-vue-files';
|
|
3
|
+
|
|
4
|
+
describe('@adyen/bento/tailwind-v3-to-bento', () => {
|
|
5
|
+
it('should lint with warning', () => {
|
|
6
|
+
const eslintrc = path.resolve(__dirname, '__tests__', 'eslint.config.js');
|
|
7
|
+
const vueFile = path.resolve(__dirname, '__tests__', 'tailwind-v3-to-bento.vue');
|
|
8
|
+
|
|
9
|
+
const result = JSON.parse(execFileSyncEslintVueFiles(eslintrc, vueFile));
|
|
10
|
+
|
|
11
|
+
// 7 fixable warnings + 4 typography warnings = 8 total messages (typography classes are per-class)
|
|
12
|
+
expect(result[0].warningCount).toBeGreaterThan(0);
|
|
13
|
+
|
|
14
|
+
// Verify spacing replacement
|
|
15
|
+
const spacingMessage = result[0].messages.find(
|
|
16
|
+
(m: { ruleId: string; line: number }) => m.ruleId === '@adyen/bento/tailwind-v3-to-bento' && m.line === 3
|
|
17
|
+
);
|
|
18
|
+
expect(spacingMessage).toBeDefined();
|
|
19
|
+
expect(spacingMessage.messageId).toBe('updateTailwindClass');
|
|
20
|
+
|
|
21
|
+
// Verify typography warning includes suggested bento-typography usage
|
|
22
|
+
const typographyMessage = result[0].messages.find(
|
|
23
|
+
(m: { messageId: string }) => m.messageId === 'removeTailwindTypography'
|
|
24
|
+
);
|
|
25
|
+
expect(typographyMessage).toBeDefined();
|
|
26
|
+
expect(typographyMessage.message).toContain('<bento-typography');
|
|
27
|
+
expect(typographyMessage.message).toContain('variant=');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('eslint').Rule.RuleContext} RuleContext
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('estree').Node} Node
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('vue-eslint-parser').AST.VLiteral} VLiteral
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('eslint-plugin-vue/lib/utils').RuleModule} RuleModule
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const VueUtils = require('eslint-plugin-vue/lib/utils');
|
|
15
|
+
|
|
16
|
+
// Spacing: Tailwind 3 -> Bento spacer tokens
|
|
17
|
+
const SPACING_MAP = {
|
|
18
|
+
'0': '000',
|
|
19
|
+
'0.5': '010',
|
|
20
|
+
'1': '020',
|
|
21
|
+
'1.5': '030',
|
|
22
|
+
'2': '040',
|
|
23
|
+
'2.5': '050',
|
|
24
|
+
'3': '060',
|
|
25
|
+
'4': '070',
|
|
26
|
+
'5': '080',
|
|
27
|
+
'6': '090',
|
|
28
|
+
'8': '100',
|
|
29
|
+
'10': '110',
|
|
30
|
+
'12': '120',
|
|
31
|
+
'14': '130',
|
|
32
|
+
'16': '140',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Border radius: Tailwind 3 -> Bento border radius tokens
|
|
36
|
+
const BORDER_RADIUS_MAP = {
|
|
37
|
+
'rounded-sm': 'rounded-xs',
|
|
38
|
+
'rounded-md': 'rounded-m',
|
|
39
|
+
'rounded-lg': 'rounded-l',
|
|
40
|
+
'rounded-xl': 'rounded-xl',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Border width: Tailwind 3 -> Bento border width tokens
|
|
44
|
+
const BORDER_WIDTH_MAP = {
|
|
45
|
+
'border-0': 'border-0',
|
|
46
|
+
'border-2': 'border-m',
|
|
47
|
+
'border-4': 'border-l',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Box shadow: Tailwind 3 -> Bento shadow tokens
|
|
51
|
+
const SHADOW_MAP = {
|
|
52
|
+
'shadow-sm': 'shadow-low',
|
|
53
|
+
'shadow-md': 'shadow-medium',
|
|
54
|
+
'shadow-lg': 'shadow-high',
|
|
55
|
+
'shadow-xl': 'shadow-high',
|
|
56
|
+
'shadow-2xl': 'shadow-high',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Breakpoint prefixes: Tailwind 3 -> Bento breakpoint prefixes
|
|
60
|
+
const BREAKPOINT_MAP = {
|
|
61
|
+
'sm:': 's-min:',
|
|
62
|
+
'md:': 'm-min:',
|
|
63
|
+
'lg:': 'l-min:',
|
|
64
|
+
'xl:': 'xl-min:',
|
|
65
|
+
'2xl:': 'xxl:',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Color utilities: Tailwind 3 -> Bento color tokens (best-effort mapping)
|
|
69
|
+
const COLOR_MAP = {
|
|
70
|
+
'red-500': 'label-critical',
|
|
71
|
+
'red-600': 'label-critical',
|
|
72
|
+
'red-700': 'label-critical-hover',
|
|
73
|
+
'red-100': 'critical-weak',
|
|
74
|
+
'green-500': 'label-success',
|
|
75
|
+
'green-600': 'label-success',
|
|
76
|
+
'green-100': 'success-weak',
|
|
77
|
+
'blue-500': 'label-highlight',
|
|
78
|
+
'blue-600': 'label-highlight',
|
|
79
|
+
'blue-100': 'highlight-weak',
|
|
80
|
+
'yellow-500': 'label-warning',
|
|
81
|
+
'yellow-600': 'label-warning',
|
|
82
|
+
'yellow-100': 'warning-weak',
|
|
83
|
+
'gray-50': 'secondary',
|
|
84
|
+
'gray-100': 'secondary',
|
|
85
|
+
'gray-200': 'tertiary',
|
|
86
|
+
'gray-300': 'outline-primary',
|
|
87
|
+
'gray-400': 'outline-secondary',
|
|
88
|
+
'gray-500': 'label-secondary',
|
|
89
|
+
'gray-600': 'label-secondary',
|
|
90
|
+
'gray-700': 'label-primary',
|
|
91
|
+
'gray-800': 'label-primary',
|
|
92
|
+
'gray-900': 'label-primary',
|
|
93
|
+
white: 'primary',
|
|
94
|
+
black: 'label-primary',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Color prefix utilities that use the COLOR_MAP
|
|
98
|
+
const COLOR_PREFIXES = ['text-', 'bg-', 'border-'];
|
|
99
|
+
|
|
100
|
+
// Spacing prefix patterns (padding, margin, gap, space, inset, etc.)
|
|
101
|
+
const SPACING_PREFIXES = [
|
|
102
|
+
'p',
|
|
103
|
+
'px',
|
|
104
|
+
'py',
|
|
105
|
+
'pt',
|
|
106
|
+
'pr',
|
|
107
|
+
'pb',
|
|
108
|
+
'pl',
|
|
109
|
+
'ps',
|
|
110
|
+
'pe',
|
|
111
|
+
'm',
|
|
112
|
+
'mx',
|
|
113
|
+
'my',
|
|
114
|
+
'mt',
|
|
115
|
+
'mr',
|
|
116
|
+
'mb',
|
|
117
|
+
'ml',
|
|
118
|
+
'ms',
|
|
119
|
+
'me',
|
|
120
|
+
'gap',
|
|
121
|
+
'gap-x',
|
|
122
|
+
'gap-y',
|
|
123
|
+
'space-x',
|
|
124
|
+
'space-y',
|
|
125
|
+
'inset',
|
|
126
|
+
'inset-x',
|
|
127
|
+
'inset-y',
|
|
128
|
+
'top',
|
|
129
|
+
'right',
|
|
130
|
+
'bottom',
|
|
131
|
+
'left',
|
|
132
|
+
'start',
|
|
133
|
+
'end',
|
|
134
|
+
'w',
|
|
135
|
+
'min-w',
|
|
136
|
+
'max-w',
|
|
137
|
+
'h',
|
|
138
|
+
'min-h',
|
|
139
|
+
'max-h',
|
|
140
|
+
'size',
|
|
141
|
+
'basis',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// Typography utilities that should be flagged (no auto-fix, use bento-typography)
|
|
145
|
+
const TYPOGRAPHY_PATTERNS = [
|
|
146
|
+
/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
|
|
147
|
+
/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,
|
|
148
|
+
/^font-(sans|serif|mono)$/,
|
|
149
|
+
/^leading-(none|tight|snug|normal|relaxed|loose|\d+)$/,
|
|
150
|
+
/^tracking-(tighter|tight|normal|wide|wider|widest)$/,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
// Font size -> suggested bento-typography variant
|
|
154
|
+
const FONT_SIZE_SUGGESTION = {
|
|
155
|
+
'text-xs': 'caption',
|
|
156
|
+
'text-sm': 'caption',
|
|
157
|
+
'text-base': 'body',
|
|
158
|
+
'text-lg': 'title',
|
|
159
|
+
'text-xl': 'title" medium',
|
|
160
|
+
'text-2xl': 'title" large',
|
|
161
|
+
'text-3xl': 'title" large',
|
|
162
|
+
'text-4xl': 'title" large',
|
|
163
|
+
'text-5xl': 'title" large',
|
|
164
|
+
'text-6xl': 'title" large',
|
|
165
|
+
'text-7xl': 'title" large',
|
|
166
|
+
'text-8xl': 'title" large',
|
|
167
|
+
'text-9xl': 'title" large',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Font weight -> suggested bento-typography modifier prop
|
|
171
|
+
const FONT_WEIGHT_SUGGESTION = {
|
|
172
|
+
'font-medium': 'stronger',
|
|
173
|
+
'font-semibold': 'stronger',
|
|
174
|
+
'font-bold': 'strongest',
|
|
175
|
+
'font-extrabold': 'strongest',
|
|
176
|
+
'font-black': 'strongest',
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build a suggested bento-typography usage from detected typography classes.
|
|
181
|
+
* @param {string[]} typographyClasses
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function buildTypographySuggestion(typographyClasses) {
|
|
185
|
+
let variant = 'body';
|
|
186
|
+
const props = [];
|
|
187
|
+
|
|
188
|
+
for (const cls of typographyClasses) {
|
|
189
|
+
if (FONT_SIZE_SUGGESTION[cls]) {
|
|
190
|
+
const suggestion = FONT_SIZE_SUGGESTION[cls];
|
|
191
|
+
if (suggestion.includes('" ')) {
|
|
192
|
+
const [v, prop] = suggestion.split('" ');
|
|
193
|
+
variant = v;
|
|
194
|
+
props.push(prop);
|
|
195
|
+
} else {
|
|
196
|
+
variant = suggestion;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (FONT_WEIGHT_SUGGESTION[cls]) {
|
|
200
|
+
props.push(FONT_WEIGHT_SUGGESTION[cls]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const propsStr = props.length > 0 ? ' ' + props.join(' ') : '';
|
|
205
|
+
return `<bento-typography variant="${variant}"${propsStr}>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Try to replace a single CSS class with its Bento equivalent.
|
|
210
|
+
* @param {string} cssClass
|
|
211
|
+
* @returns {{ replaced: string, wasReplaced: boolean, isTypography: boolean }}
|
|
212
|
+
*/
|
|
213
|
+
function replaceClass(cssClass) {
|
|
214
|
+
let workingClass = cssClass;
|
|
215
|
+
let breakpointPrefix = '';
|
|
216
|
+
let hasVariantReplacement = false;
|
|
217
|
+
|
|
218
|
+
// Extract all variants (e.g., md:hover:p-4 -> variants=['m-min:', 'hover:'], workingClass='p-4')
|
|
219
|
+
while (workingClass.includes(':')) {
|
|
220
|
+
const colonIndex = workingClass.indexOf(':') + 1;
|
|
221
|
+
const variant = workingClass.slice(0, colonIndex);
|
|
222
|
+
const mapped = BREAKPOINT_MAP[variant];
|
|
223
|
+
if (mapped) {
|
|
224
|
+
hasVariantReplacement = true;
|
|
225
|
+
breakpointPrefix += mapped;
|
|
226
|
+
} else {
|
|
227
|
+
breakpointPrefix += variant;
|
|
228
|
+
}
|
|
229
|
+
workingClass = workingClass.slice(colonIndex);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check typography (warn only)
|
|
233
|
+
for (const pattern of TYPOGRAPHY_PATTERNS) {
|
|
234
|
+
if (pattern.test(workingClass)) {
|
|
235
|
+
return { replaced: cssClass, wasReplaced: false, isTypography: true };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check border radius: bare "rounded" (without further suffix) maps to "rounded-s"
|
|
240
|
+
if (workingClass === 'rounded') {
|
|
241
|
+
return { replaced: breakpointPrefix + 'rounded-s', wasReplaced: true, isTypography: false };
|
|
242
|
+
}
|
|
243
|
+
if (BORDER_RADIUS_MAP[workingClass]) {
|
|
244
|
+
return { replaced: breakpointPrefix + BORDER_RADIUS_MAP[workingClass], wasReplaced: true, isTypography: false };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check border width: bare "border" (without color suffix) maps to "border-s"
|
|
248
|
+
if (workingClass === 'border') {
|
|
249
|
+
return { replaced: breakpointPrefix + 'border-s', wasReplaced: true, isTypography: false };
|
|
250
|
+
}
|
|
251
|
+
if (BORDER_WIDTH_MAP[workingClass]) {
|
|
252
|
+
return { replaced: breakpointPrefix + BORDER_WIDTH_MAP[workingClass], wasReplaced: true, isTypography: false };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check box shadow: bare "shadow" maps to "shadow-medium"
|
|
256
|
+
if (workingClass === 'shadow') {
|
|
257
|
+
return { replaced: breakpointPrefix + 'shadow-medium', wasReplaced: true, isTypography: false };
|
|
258
|
+
}
|
|
259
|
+
if (SHADOW_MAP[workingClass]) {
|
|
260
|
+
return { replaced: breakpointPrefix + SHADOW_MAP[workingClass], wasReplaced: true, isTypography: false };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check color utilities
|
|
264
|
+
for (const prefix of COLOR_PREFIXES) {
|
|
265
|
+
if (workingClass.startsWith(prefix)) {
|
|
266
|
+
const colorPart = workingClass.slice(prefix.length);
|
|
267
|
+
if (COLOR_MAP[colorPart]) {
|
|
268
|
+
const bentoColor = COLOR_MAP[colorPart];
|
|
269
|
+
return { replaced: breakpointPrefix + prefix + bentoColor, wasReplaced: true, isTypography: false };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check spacing utilities (including negative values like -m-4)
|
|
275
|
+
for (const spacingPrefix of SPACING_PREFIXES) {
|
|
276
|
+
const isNegative = workingClass.startsWith('-');
|
|
277
|
+
const baseClass = isNegative ? workingClass.slice(1) : workingClass;
|
|
278
|
+
const prefixWithDash = spacingPrefix + '-';
|
|
279
|
+
if (baseClass.startsWith(prefixWithDash)) {
|
|
280
|
+
const value = baseClass.slice(prefixWithDash.length);
|
|
281
|
+
if (SPACING_MAP[value]) {
|
|
282
|
+
return {
|
|
283
|
+
replaced: breakpointPrefix + (isNegative ? '-' : '') + prefixWithDash + SPACING_MAP[value],
|
|
284
|
+
wasReplaced: true,
|
|
285
|
+
isTypography: false,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// If a breakpoint variant was mapped but the base class wasn't, still report the replacement
|
|
292
|
+
if (hasVariantReplacement && workingClass) {
|
|
293
|
+
return { replaced: breakpointPrefix + workingClass, wasReplaced: true, isTypography: false };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { replaced: cssClass, wasReplaced: false, isTypography: false };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @type {RuleModule} */
|
|
300
|
+
module.exports = {
|
|
301
|
+
meta: {
|
|
302
|
+
type: 'suggestion',
|
|
303
|
+
docs: {
|
|
304
|
+
description: 'Replace Tailwind CSS v3 utility classes with Bento design token equivalents',
|
|
305
|
+
category: 'Best Practices',
|
|
306
|
+
url: 'https://github.com/Adyen/bento/blob/main/packages/eslint/rules/tailwind-v3-to-bento/README.md',
|
|
307
|
+
recommended: true,
|
|
308
|
+
},
|
|
309
|
+
messages: {
|
|
310
|
+
updateTailwindClass:
|
|
311
|
+
'Found Tailwind v3 class "{{ oldClass }}" — replace with Bento equivalent "{{ newClass }}".',
|
|
312
|
+
removeTailwindTypography:
|
|
313
|
+
'Found Tailwind v3 typography class "{{ oldClass }}" — use {{ suggestion }} instead.',
|
|
314
|
+
},
|
|
315
|
+
fixable: 'code',
|
|
316
|
+
schema: [],
|
|
317
|
+
},
|
|
318
|
+
/** @param {RuleContext} context */
|
|
319
|
+
create(context) {
|
|
320
|
+
return VueUtils.defineTemplateBodyVisitor(context, {
|
|
321
|
+
VLiteral: (/** @type {any} */ node) => {
|
|
322
|
+
if (node.parent && node.parent.key && node.parent.key.name !== 'class') {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (typeof node.value !== 'string') {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const classList = /** @type {VLiteral['value']} */ node.value;
|
|
331
|
+
let hasReplacements = false;
|
|
332
|
+
let hasTypography = false;
|
|
333
|
+
|
|
334
|
+
const classes = classList.split(/\s+/).filter(Boolean);
|
|
335
|
+
const results = classes.map(cssClass => {
|
|
336
|
+
const result = replaceClass(cssClass);
|
|
337
|
+
if (result.wasReplaced) {
|
|
338
|
+
hasReplacements = true;
|
|
339
|
+
}
|
|
340
|
+
if (result.isTypography) {
|
|
341
|
+
hasTypography = true;
|
|
342
|
+
}
|
|
343
|
+
return result;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (hasReplacements) {
|
|
347
|
+
const fixed = results.map(r => r.replaced).join(' ');
|
|
348
|
+
context.report({
|
|
349
|
+
node,
|
|
350
|
+
messageId: 'updateTailwindClass',
|
|
351
|
+
data: {
|
|
352
|
+
oldClass: classList,
|
|
353
|
+
newClass: fixed,
|
|
354
|
+
},
|
|
355
|
+
fix(fixer) {
|
|
356
|
+
const quote = context.sourceCode.getText(node)[0];
|
|
357
|
+
return fixer.replaceText(node, `${quote}${fixed}${quote}`);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (hasTypography) {
|
|
363
|
+
const typographyClasses = results.filter(r => r.isTypography).map(r => r.replaced);
|
|
364
|
+
const suggestion = buildTypographySuggestion(typographyClasses);
|
|
365
|
+
context.report({
|
|
366
|
+
node,
|
|
367
|
+
messageId: 'removeTailwindTypography',
|
|
368
|
+
data: {
|
|
369
|
+
oldClass: typographyClasses.join(', '),
|
|
370
|
+
suggestion,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
};
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import vueParser from 'vue-eslint-parser';
|
|
3
|
+
import tsParser from '@typescript-eslint/parser';
|
|
4
|
+
import tailwindV3ToBento from './tailwind-v3-to-bento';
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: 'latest',
|
|
9
|
+
sourceType: 'module',
|
|
10
|
+
parser: vueParser,
|
|
11
|
+
parserOptions: {
|
|
12
|
+
parser: tsParser,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('@adyen/bento/tailwind-v3-to-bento', () => {
|
|
18
|
+
ruleTester.run('tailwind-v3-to-bento', tailwindV3ToBento, {
|
|
19
|
+
valid: [
|
|
20
|
+
{
|
|
21
|
+
name: 'Bento spacing classes are valid',
|
|
22
|
+
filename: 'test.vue',
|
|
23
|
+
code: `
|
|
24
|
+
<template>
|
|
25
|
+
<div class="p-070 m-040 gap-100"></div>
|
|
26
|
+
</template>
|
|
27
|
+
`,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Bento border radius classes are valid',
|
|
31
|
+
filename: 'test.vue',
|
|
32
|
+
code: `
|
|
33
|
+
<template>
|
|
34
|
+
<div class="rounded-s rounded-m rounded-l"></div>
|
|
35
|
+
</template>
|
|
36
|
+
`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Bento shadow classes are valid',
|
|
40
|
+
filename: 'test.vue',
|
|
41
|
+
code: `
|
|
42
|
+
<template>
|
|
43
|
+
<div class="shadow-low shadow-medium shadow-high"></div>
|
|
44
|
+
</template>
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Bento breakpoint classes are valid',
|
|
49
|
+
filename: 'test.vue',
|
|
50
|
+
code: `
|
|
51
|
+
<template>
|
|
52
|
+
<div class="s-min:p-070 l-min:p-100 xxl:m-040"></div>
|
|
53
|
+
</template>
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'Bento color classes are valid',
|
|
58
|
+
filename: 'test.vue',
|
|
59
|
+
code: `
|
|
60
|
+
<template>
|
|
61
|
+
<div class="text-label-critical bg-secondary border-outline-primary"></div>
|
|
62
|
+
</template>
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Non-themed Tailwind utilities are valid',
|
|
67
|
+
filename: 'test.vue',
|
|
68
|
+
code: `
|
|
69
|
+
<template>
|
|
70
|
+
<div class="flex items-center justify-between"></div>
|
|
71
|
+
</template>
|
|
72
|
+
`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'Non-class attributes are ignored',
|
|
76
|
+
filename: 'test.vue',
|
|
77
|
+
code: `
|
|
78
|
+
<template>
|
|
79
|
+
<div data-test="p-4 shadow-lg rounded-md"></div>
|
|
80
|
+
</template>
|
|
81
|
+
`,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
invalid: [
|
|
85
|
+
// Spacing
|
|
86
|
+
{
|
|
87
|
+
name: 'Replaces spacing utilities',
|
|
88
|
+
code: `
|
|
89
|
+
<template>
|
|
90
|
+
<div class="p-4 m-2 gap-8"></div>
|
|
91
|
+
</template>
|
|
92
|
+
`,
|
|
93
|
+
output: `
|
|
94
|
+
<template>
|
|
95
|
+
<div class="p-070 m-040 gap-100"></div>
|
|
96
|
+
</template>
|
|
97
|
+
`,
|
|
98
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'Replaces spacing variants (px, py, mt, etc.)',
|
|
102
|
+
code: `
|
|
103
|
+
<template>
|
|
104
|
+
<div class="px-4 py-2 mt-8 mb-3"></div>
|
|
105
|
+
</template>
|
|
106
|
+
`,
|
|
107
|
+
output: `
|
|
108
|
+
<template>
|
|
109
|
+
<div class="px-070 py-040 mt-100 mb-060"></div>
|
|
110
|
+
</template>
|
|
111
|
+
`,
|
|
112
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'Replaces fractional spacing',
|
|
116
|
+
code: `
|
|
117
|
+
<template>
|
|
118
|
+
<div class="p-0.5 m-1.5 gap-2.5"></div>
|
|
119
|
+
</template>
|
|
120
|
+
`,
|
|
121
|
+
output: `
|
|
122
|
+
<template>
|
|
123
|
+
<div class="p-010 m-030 gap-050"></div>
|
|
124
|
+
</template>
|
|
125
|
+
`,
|
|
126
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'Replaces negative spacing utilities',
|
|
130
|
+
code: `
|
|
131
|
+
<template>
|
|
132
|
+
<div class="-m-4 -mt-2 -inset-x-8"></div>
|
|
133
|
+
</template>
|
|
134
|
+
`,
|
|
135
|
+
output: `
|
|
136
|
+
<template>
|
|
137
|
+
<div class="-m-070 -mt-040 -inset-x-100"></div>
|
|
138
|
+
</template>
|
|
139
|
+
`,
|
|
140
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
141
|
+
},
|
|
142
|
+
// Border radius
|
|
143
|
+
{
|
|
144
|
+
name: 'Replaces bare rounded',
|
|
145
|
+
code: `
|
|
146
|
+
<template>
|
|
147
|
+
<div class="rounded"></div>
|
|
148
|
+
</template>
|
|
149
|
+
`,
|
|
150
|
+
output: `
|
|
151
|
+
<template>
|
|
152
|
+
<div class="rounded-s"></div>
|
|
153
|
+
</template>
|
|
154
|
+
`,
|
|
155
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'Replaces border radius utilities',
|
|
159
|
+
code: `
|
|
160
|
+
<template>
|
|
161
|
+
<div class="rounded-sm rounded-md rounded-lg"></div>
|
|
162
|
+
</template>
|
|
163
|
+
`,
|
|
164
|
+
output: `
|
|
165
|
+
<template>
|
|
166
|
+
<div class="rounded-xs rounded-m rounded-l"></div>
|
|
167
|
+
</template>
|
|
168
|
+
`,
|
|
169
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
170
|
+
},
|
|
171
|
+
// Border width
|
|
172
|
+
{
|
|
173
|
+
name: 'Replaces bare border',
|
|
174
|
+
code: `
|
|
175
|
+
<template>
|
|
176
|
+
<div class="border"></div>
|
|
177
|
+
</template>
|
|
178
|
+
`,
|
|
179
|
+
output: `
|
|
180
|
+
<template>
|
|
181
|
+
<div class="border-s"></div>
|
|
182
|
+
</template>
|
|
183
|
+
`,
|
|
184
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'Replaces border width utilities',
|
|
188
|
+
code: `
|
|
189
|
+
<template>
|
|
190
|
+
<div class="border-2 border-4"></div>
|
|
191
|
+
</template>
|
|
192
|
+
`,
|
|
193
|
+
output: `
|
|
194
|
+
<template>
|
|
195
|
+
<div class="border-m border-l"></div>
|
|
196
|
+
</template>
|
|
197
|
+
`,
|
|
198
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
199
|
+
},
|
|
200
|
+
// Box shadows
|
|
201
|
+
{
|
|
202
|
+
name: 'Replaces bare shadow',
|
|
203
|
+
code: `
|
|
204
|
+
<template>
|
|
205
|
+
<div class="shadow"></div>
|
|
206
|
+
</template>
|
|
207
|
+
`,
|
|
208
|
+
output: `
|
|
209
|
+
<template>
|
|
210
|
+
<div class="shadow-medium"></div>
|
|
211
|
+
</template>
|
|
212
|
+
`,
|
|
213
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'Replaces shadow utilities',
|
|
217
|
+
code: `
|
|
218
|
+
<template>
|
|
219
|
+
<div class="shadow-sm shadow-lg shadow-xl"></div>
|
|
220
|
+
</template>
|
|
221
|
+
`,
|
|
222
|
+
output: `
|
|
223
|
+
<template>
|
|
224
|
+
<div class="shadow-low shadow-high shadow-high"></div>
|
|
225
|
+
</template>
|
|
226
|
+
`,
|
|
227
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
228
|
+
},
|
|
229
|
+
// Breakpoints
|
|
230
|
+
{
|
|
231
|
+
name: 'Replaces breakpoint prefixes',
|
|
232
|
+
code: `
|
|
233
|
+
<template>
|
|
234
|
+
<div class="sm:p-4 lg:p-8 2xl:m-2"></div>
|
|
235
|
+
</template>
|
|
236
|
+
`,
|
|
237
|
+
output: `
|
|
238
|
+
<template>
|
|
239
|
+
<div class="s-min:p-070 l-min:p-100 xxl:m-040"></div>
|
|
240
|
+
</template>
|
|
241
|
+
`,
|
|
242
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'Replaces breakpoint with non-spacing class',
|
|
246
|
+
code: `
|
|
247
|
+
<template>
|
|
248
|
+
<div class="md:rounded-md xl:shadow-lg"></div>
|
|
249
|
+
</template>
|
|
250
|
+
`,
|
|
251
|
+
output: `
|
|
252
|
+
<template>
|
|
253
|
+
<div class="m-min:rounded-m xl-min:shadow-high"></div>
|
|
254
|
+
</template>
|
|
255
|
+
`,
|
|
256
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'Handles combined breakpoint and other variants',
|
|
260
|
+
code: `
|
|
261
|
+
<template>
|
|
262
|
+
<div class="md:hover:p-4 lg:focus:rounded-md"></div>
|
|
263
|
+
</template>
|
|
264
|
+
`,
|
|
265
|
+
output: `
|
|
266
|
+
<template>
|
|
267
|
+
<div class="m-min:hover:p-070 l-min:focus:rounded-m"></div>
|
|
268
|
+
</template>
|
|
269
|
+
`,
|
|
270
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'Preserves non-breakpoint variants without replacing',
|
|
274
|
+
code: `
|
|
275
|
+
<template>
|
|
276
|
+
<div class="hover:bg-gray-100 focus:border-gray-300"></div>
|
|
277
|
+
</template>
|
|
278
|
+
`,
|
|
279
|
+
output: `
|
|
280
|
+
<template>
|
|
281
|
+
<div class="hover:bg-secondary focus:border-outline-primary"></div>
|
|
282
|
+
</template>
|
|
283
|
+
`,
|
|
284
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
285
|
+
},
|
|
286
|
+
// Colors
|
|
287
|
+
{
|
|
288
|
+
name: 'Replaces text color utilities',
|
|
289
|
+
code: `
|
|
290
|
+
<template>
|
|
291
|
+
<div class="text-red-500 text-green-500 text-blue-500"></div>
|
|
292
|
+
</template>
|
|
293
|
+
`,
|
|
294
|
+
output: `
|
|
295
|
+
<template>
|
|
296
|
+
<div class="text-label-critical text-label-success text-label-highlight"></div>
|
|
297
|
+
</template>
|
|
298
|
+
`,
|
|
299
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'Replaces background color utilities',
|
|
303
|
+
code: `
|
|
304
|
+
<template>
|
|
305
|
+
<div class="bg-gray-100 bg-red-100 bg-green-100"></div>
|
|
306
|
+
</template>
|
|
307
|
+
`,
|
|
308
|
+
output: `
|
|
309
|
+
<template>
|
|
310
|
+
<div class="bg-secondary bg-critical-weak bg-success-weak"></div>
|
|
311
|
+
</template>
|
|
312
|
+
`,
|
|
313
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'Replaces border color utilities',
|
|
317
|
+
code: `
|
|
318
|
+
<template>
|
|
319
|
+
<div class="border-gray-300"></div>
|
|
320
|
+
</template>
|
|
321
|
+
`,
|
|
322
|
+
output: `
|
|
323
|
+
<template>
|
|
324
|
+
<div class="border-outline-primary"></div>
|
|
325
|
+
</template>
|
|
326
|
+
`,
|
|
327
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
328
|
+
},
|
|
329
|
+
// Typography (warn only, no fix)
|
|
330
|
+
{
|
|
331
|
+
name: 'Warns about typography with suggested variant',
|
|
332
|
+
code: `
|
|
333
|
+
<template>
|
|
334
|
+
<div class="text-sm font-bold leading-tight tracking-wide"></div>
|
|
335
|
+
</template>
|
|
336
|
+
`,
|
|
337
|
+
errors: [
|
|
338
|
+
{
|
|
339
|
+
message:
|
|
340
|
+
'Found Tailwind v3 typography class "text-sm, font-bold, leading-tight, tracking-wide" — use <bento-typography variant="caption" strongest> instead.',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'Suggests body variant for text-base',
|
|
346
|
+
code: `
|
|
347
|
+
<template>
|
|
348
|
+
<div class="text-base font-medium"></div>
|
|
349
|
+
</template>
|
|
350
|
+
`,
|
|
351
|
+
errors: [
|
|
352
|
+
{
|
|
353
|
+
message:
|
|
354
|
+
'Found Tailwind v3 typography class "text-base, font-medium" — use <bento-typography variant="body" stronger> instead.',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'Suggests title variant with large for text-2xl',
|
|
360
|
+
code: `
|
|
361
|
+
<template>
|
|
362
|
+
<div class="text-2xl font-semibold"></div>
|
|
363
|
+
</template>
|
|
364
|
+
`,
|
|
365
|
+
errors: [
|
|
366
|
+
{
|
|
367
|
+
message:
|
|
368
|
+
'Found Tailwind v3 typography class "text-2xl, font-semibold" — use <bento-typography variant="title" large stronger> instead.',
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'Suggests title medium for text-xl',
|
|
374
|
+
code: `
|
|
375
|
+
<template>
|
|
376
|
+
<div class="text-xl"></div>
|
|
377
|
+
</template>
|
|
378
|
+
`,
|
|
379
|
+
errors: [
|
|
380
|
+
{
|
|
381
|
+
message:
|
|
382
|
+
'Found Tailwind v3 typography class "text-xl" — use <bento-typography variant="title" medium> instead.',
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
// Mixed
|
|
387
|
+
{
|
|
388
|
+
name: 'Handles mixed classes',
|
|
389
|
+
code: `
|
|
390
|
+
<template>
|
|
391
|
+
<div class="p-4 rounded-md shadow-lg sm:p-8 text-red-500"></div>
|
|
392
|
+
</template>
|
|
393
|
+
`,
|
|
394
|
+
output: `
|
|
395
|
+
<template>
|
|
396
|
+
<div class="p-070 rounded-m shadow-high s-min:p-100 text-label-critical"></div>
|
|
397
|
+
</template>
|
|
398
|
+
`,
|
|
399
|
+
errors: [{ messageId: 'updateTailwindClass' }],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
});
|