@atlaskit/eslint-plugin-platform 2.9.2 → 2.10.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 (27) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/index.js +11 -1
  3. package/dist/cjs/rules/compiled/expand-motion-shorthand/index.js +281 -0
  4. package/dist/cjs/rules/compiled/use-motion-token-values/index.js +506 -0
  5. package/dist/cjs/rules/editor-example-type-import-required/index.js +321 -0
  6. package/dist/cjs/rules/import/one-value-export-per-file/index.js +203 -0
  7. package/dist/es2019/index.js +11 -1
  8. package/dist/es2019/rules/compiled/expand-motion-shorthand/index.js +239 -0
  9. package/dist/es2019/rules/compiled/use-motion-token-values/index.js +444 -0
  10. package/dist/es2019/rules/editor-example-type-import-required/index.js +286 -0
  11. package/dist/es2019/rules/import/one-value-export-per-file/index.js +191 -0
  12. package/dist/esm/index.js +11 -1
  13. package/dist/esm/rules/compiled/expand-motion-shorthand/index.js +275 -0
  14. package/dist/esm/rules/compiled/use-motion-token-values/index.js +499 -0
  15. package/dist/esm/rules/editor-example-type-import-required/index.js +314 -0
  16. package/dist/esm/rules/import/one-value-export-per-file/index.js +196 -0
  17. package/dist/types/index.d.ts +6 -0
  18. package/dist/types/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
  19. package/dist/types/rules/compiled/use-motion-token-values/index.d.ts +3 -0
  20. package/dist/types/rules/editor-example-type-import-required/index.d.ts +4 -0
  21. package/dist/types/rules/import/one-value-export-per-file/index.d.ts +3 -0
  22. package/dist/types-ts4.5/index.d.ts +6 -0
  23. package/dist/types-ts4.5/rules/compiled/expand-motion-shorthand/index.d.ts +3 -0
  24. package/dist/types-ts4.5/rules/compiled/use-motion-token-values/index.d.ts +3 -0
  25. package/dist/types-ts4.5/rules/editor-example-type-import-required/index.d.ts +4 -0
  26. package/dist/types-ts4.5/rules/import/one-value-export-per-file/index.d.ts +3 -0
  27. package/package.json +2 -1
@@ -0,0 +1,239 @@
1
+ const EASING_KEYWORDS = ['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear', 'step-start', 'step-end'];
2
+ const KEYWORD_VALUES = ['none', 'all', 'inherit', 'initial', 'unset'];
3
+ const isDuration = token => /^\d+(?:\.\d+)?m?s$/.test(token);
4
+ const isEasing = token => EASING_KEYWORDS.includes(token) || token.startsWith('cubic-bezier(') || token.startsWith('steps(');
5
+ /**
6
+ * Tokenizes a CSS shorthand value string, respecting function boundaries.
7
+ * e.g. 'opacity 200ms cubic-bezier(0.4, 0, 0, 1) 0ms' →
8
+ * ['opacity', '200ms', 'cubic-bezier(0.4, 0, 0, 1)', '0ms']
9
+ * Splits on whitespace only when not inside parentheses.
10
+ */
11
+ const tokenizeShorthand = value => {
12
+ const tokens = [];
13
+ let depth = 0;
14
+ let current = '';
15
+ for (let i = 0; i < value.length; i++) {
16
+ const ch = value[i];
17
+ if (ch === '(') {
18
+ depth++;
19
+ current += ch;
20
+ } else if (ch === ')') {
21
+ depth--;
22
+ current += ch;
23
+ } else if (/\s/.test(ch) && depth === 0) {
24
+ if (current.length > 0) {
25
+ tokens.push(current);
26
+ current = '';
27
+ }
28
+ } else {
29
+ current += ch;
30
+ }
31
+ }
32
+ if (current.length > 0) {
33
+ tokens.push(current);
34
+ }
35
+ return tokens;
36
+ };
37
+
38
+ // Splits on top-level commas (outside function parens) — preserves cubic-bezier(...) commas.
39
+ const splitOnTopLevelCommas = value => {
40
+ const parts = [];
41
+ let depth = 0;
42
+ let current = '';
43
+ for (const ch of value) {
44
+ if (ch === '(') {
45
+ depth++;
46
+ current += ch;
47
+ } else if (ch === ')') {
48
+ depth--;
49
+ current += ch;
50
+ } else if (ch === ',' && depth === 0) {
51
+ parts.push(current.trim());
52
+ current = '';
53
+ } else {
54
+ current += ch;
55
+ }
56
+ }
57
+ if (current.trim().length > 0) {
58
+ parts.push(current.trim());
59
+ }
60
+ return parts;
61
+ };
62
+ const parseTransition = value => {
63
+ const parts = tokenizeShorthand(value.trim());
64
+ const result = {};
65
+ let durationCount = 0;
66
+ for (const part of parts) {
67
+ if (isDuration(part)) {
68
+ if (durationCount === 0) {
69
+ result.transitionDuration = part;
70
+ } else {
71
+ result.transitionDelay = part;
72
+ }
73
+ durationCount++;
74
+ } else if (isEasing(part)) {
75
+ result.transitionTimingFunction = part;
76
+ } else {
77
+ result.transitionProperty = part;
78
+ }
79
+ }
80
+ return result;
81
+ };
82
+ const parseAnimation = value => {
83
+ const parts = tokenizeShorthand(value.trim());
84
+ const result = {};
85
+ let durationCount = 0;
86
+ for (const part of parts) {
87
+ if (isDuration(part)) {
88
+ if (durationCount === 0) {
89
+ result.animationDuration = part;
90
+ } else {
91
+ result.animationDelay = part;
92
+ }
93
+ durationCount++;
94
+ } else if (isEasing(part)) {
95
+ result.animationTimingFunction = part;
96
+ } else if (part === 'infinite' || /^\d+(\.\d+)?$/.test(part)) {
97
+ result.animationIterationCount = part;
98
+ } else if (['normal', 'reverse', 'alternate', 'alternate-reverse'].includes(part)) {
99
+ result.animationDirection = part;
100
+ } else if (['none', 'forwards', 'backwards', 'both'].includes(part)) {
101
+ result.animationFillMode = part;
102
+ } else if (['running', 'paused'].includes(part)) {
103
+ result.animationPlayState = part;
104
+ } else {
105
+ result.animationName = part;
106
+ }
107
+ }
108
+ return result;
109
+ };
110
+
111
+ // Combine sub-property values across comma-separated transitions/animations.
112
+ // If no segment explicitly set this sub-property, omit it entirely.
113
+ // Otherwise, fill missing slots with the CSS spec default to preserve positional alignment.
114
+ const combineSubPropertyValues = (segments, subProperty, defaultValue) => {
115
+ if (segments.every(s => s[subProperty] === undefined)) {
116
+ return undefined;
117
+ }
118
+ return segments.map(s => {
119
+ var _s$subProperty;
120
+ return (_s$subProperty = s[subProperty]) !== null && _s$subProperty !== void 0 ? _s$subProperty : defaultValue;
121
+ }).join(', ');
122
+ };
123
+ const buildTransitionFix = (segments, indent) => {
124
+ const lines = [];
125
+ const property = combineSubPropertyValues(segments, 'transitionProperty', 'all');
126
+ const duration = combineSubPropertyValues(segments, 'transitionDuration', '0s');
127
+ const timing = combineSubPropertyValues(segments, 'transitionTimingFunction', 'ease');
128
+ const delay = combineSubPropertyValues(segments, 'transitionDelay', '0s');
129
+ if (property !== undefined) lines.push(`transitionProperty: '${property}'`);
130
+ if (duration !== undefined) lines.push(`transitionDuration: '${duration}'`);
131
+ if (timing !== undefined) lines.push(`transitionTimingFunction: '${timing}'`);
132
+ if (delay !== undefined) lines.push(`transitionDelay: '${delay}'`);
133
+ return lines.join(`,\n${indent}`);
134
+ };
135
+ const buildAnimationFix = (segments, indent) => {
136
+ const lines = [];
137
+ const name = combineSubPropertyValues(segments, 'animationName', 'none');
138
+ const duration = combineSubPropertyValues(segments, 'animationDuration', '0s');
139
+ const timing = combineSubPropertyValues(segments, 'animationTimingFunction', 'ease');
140
+ const delay = combineSubPropertyValues(segments, 'animationDelay', '0s');
141
+ const iter = combineSubPropertyValues(segments, 'animationIterationCount', '1');
142
+ const direction = combineSubPropertyValues(segments, 'animationDirection', 'normal');
143
+ const fill = combineSubPropertyValues(segments, 'animationFillMode', 'none');
144
+ const playState = combineSubPropertyValues(segments, 'animationPlayState', 'running');
145
+ if (name !== undefined) lines.push(`animationName: '${name}'`);
146
+ if (duration !== undefined) lines.push(`animationDuration: '${duration}'`);
147
+ if (timing !== undefined) lines.push(`animationTimingFunction: '${timing}'`);
148
+ if (delay !== undefined) lines.push(`animationDelay: '${delay}'`);
149
+ if (iter !== undefined) lines.push(`animationIterationCount: '${iter}'`);
150
+ if (direction !== undefined) lines.push(`animationDirection: '${direction}'`);
151
+ if (fill !== undefined) lines.push(`animationFillMode: '${fill}'`);
152
+ if (playState !== undefined) lines.push(`animationPlayState: '${playState}'`);
153
+ return lines.join(`,\n${indent}`);
154
+ };
155
+ const TRANSITION_SUB_PROPERTIES = ['transitionProperty', 'transitionDuration', 'transitionTimingFunction', 'transitionDelay'];
156
+ const ANIMATION_SUB_PROPERTIES = ['animationName', 'animationDuration', 'animationTimingFunction', 'animationDelay', 'animationIterationCount', 'animationDirection', 'animationFillMode', 'animationPlayState'];
157
+ const executeExpandTransitionRule = (context, node, property) => {
158
+ var _context$sourceCode, _node$loc;
159
+ if (node.value.type === 'CallExpression') {
160
+ return;
161
+ }
162
+ if (node.value.type === 'TemplateLiteral') {
163
+ return;
164
+ }
165
+ if (node.value.type !== 'Literal' || typeof node.value.value !== 'string') {
166
+ return;
167
+ }
168
+ const rawValue = node.value.value;
169
+ if (KEYWORD_VALUES.includes(rawValue)) {
170
+ return;
171
+ }
172
+ const subProperties = property === 'transition' ? TRANSITION_SUB_PROPERTIES : ANIMATION_SUB_PROPERTIES;
173
+
174
+ // Extract leading whitespace to preserve indentation style (tabs vs spaces)
175
+ const sourceCode = (_context$sourceCode = context.sourceCode) !== null && _context$sourceCode !== void 0 ? _context$sourceCode : context.getSourceCode();
176
+ const nodeStart = (_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start;
177
+ let indent = '\t';
178
+ if (nodeStart) {
179
+ var _sourceCode$lines;
180
+ const lineText = (_sourceCode$lines = sourceCode.lines[nodeStart.line - 1]) !== null && _sourceCode$lines !== void 0 ? _sourceCode$lines : '';
181
+ const leadingWhitespace = lineText.match(/^(\s*)/);
182
+ if (leadingWhitespace) {
183
+ indent = leadingWhitespace[1];
184
+ }
185
+ }
186
+ const segmentStrings = splitOnTopLevelCommas(rawValue);
187
+ if (property === 'transition') {
188
+ const segments = segmentStrings.map(parseTransition);
189
+ const fixText = buildTransitionFix(segments, indent);
190
+ context.report({
191
+ node,
192
+ messageId: 'expandTransitionShorthand',
193
+ data: {
194
+ property,
195
+ subProperties: subProperties.join(', ')
196
+ },
197
+ fix(fixer) {
198
+ return fixer.replaceText(node, fixText);
199
+ }
200
+ });
201
+ } else {
202
+ const segments = segmentStrings.map(parseAnimation);
203
+ const fixText = buildAnimationFix(segments, indent);
204
+ context.report({
205
+ node,
206
+ messageId: 'expandTransitionShorthand',
207
+ data: {
208
+ property,
209
+ subProperties: subProperties.join(', ')
210
+ },
211
+ fix(fixer) {
212
+ return fixer.replaceText(node, fixText);
213
+ }
214
+ });
215
+ }
216
+ };
217
+ export const expandTransitionShorthand = {
218
+ meta: {
219
+ type: 'suggestion',
220
+ fixable: 'code',
221
+ docs: {
222
+ url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/expand-transition-shorthand/'
223
+ },
224
+ messages: {
225
+ expandTransitionShorthand: "Use {{ subProperties }} instead of the '{{ property }}' shorthand so that individual values can be replaced with motion tokens."
226
+ }
227
+ },
228
+ create(context) {
229
+ return {
230
+ 'Property[key.name="transition"]': function (node) {
231
+ executeExpandTransitionRule(context, node, 'transition');
232
+ },
233
+ 'Property[key.name="animation"]': function (node) {
234
+ executeExpandTransitionRule(context, node, 'animation');
235
+ }
236
+ };
237
+ }
238
+ };
239
+ export default expandTransitionShorthand;
@@ -0,0 +1,444 @@
1
+ import tokenDefaultValues from '@atlaskit/tokens/token-default-values';
2
+ const DURATION_TOKEN_NAMES = ['motion.duration.instant', 'motion.duration.xxshort', 'motion.duration.xshort', 'motion.duration.short', 'motion.duration.medium', 'motion.duration.long', 'motion.duration.xlong', 'motion.duration.xxlong'];
3
+ function parseDurationMs(value) {
4
+ const ms = value.match(/^(\d+(?:\.\d+)?)ms$/);
5
+ if (ms) {
6
+ return parseFloat(ms[1]);
7
+ }
8
+ const s = value.match(/^(\d+(?:\.\d+)?)s$/);
9
+ if (s) {
10
+ return parseFloat(s[1]) * 1000;
11
+ }
12
+ return null;
13
+ }
14
+ const DURATION_TOKENS = DURATION_TOKEN_NAMES.map(name => {
15
+ const rawValue = tokenDefaultValues[name];
16
+ const ms = parseDurationMs(rawValue);
17
+ if (ms === null) {
18
+ throw new Error(`use-motion-token-values: could not parse duration for token ${name}: ${rawValue}`);
19
+ }
20
+ return {
21
+ ms,
22
+ token: name
23
+ };
24
+ }).sort((a, b) => a.ms - b.ms);
25
+ const EASING_TOKEN_NAMES = ['motion.easing.in.practical', 'motion.easing.inout.bold', 'motion.easing.out.practical', 'motion.easing.out.bold'];
26
+ function parseCubicBezierParams(value) {
27
+ const match = value.match(/^cubic-bezier\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
28
+ if (!match) {
29
+ return null;
30
+ }
31
+ return [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])];
32
+ }
33
+ const EASING_TOKENS = EASING_TOKEN_NAMES.map(name => {
34
+ const rawValue = tokenDefaultValues[name];
35
+ const params = parseCubicBezierParams(rawValue);
36
+ if (!params) {
37
+ throw new Error(`use-motion-token-values: could not parse cubic-bezier for token ${name}: ${rawValue}`);
38
+ }
39
+ return {
40
+ value: rawValue,
41
+ token: name,
42
+ params
43
+ };
44
+ });
45
+
46
+ // Splits on top-level commas (outside function parens) — preserves cubic-bezier(...) commas.
47
+ function splitOnTopLevelCommas(value) {
48
+ const parts = [];
49
+ let depth = 0;
50
+ let current = '';
51
+ for (const ch of value) {
52
+ if (ch === '(') {
53
+ depth++;
54
+ current += ch;
55
+ } else if (ch === ')') {
56
+ depth--;
57
+ current += ch;
58
+ } else if (ch === ',' && depth === 0) {
59
+ parts.push(current.trim());
60
+ current = '';
61
+ } else {
62
+ current += ch;
63
+ }
64
+ }
65
+ if (current.trim().length > 0) {
66
+ parts.push(current.trim());
67
+ }
68
+ return parts;
69
+ }
70
+ const DURATION_PROPERTIES = new Set(['transitionDuration', 'animationDuration']);
71
+ const EASING_PROPERTIES = new Set(['transitionTimingFunction', 'animationTimingFunction']);
72
+
73
+ // Explicit semantic mappings for CSS keyword easings to motion tokens.
74
+ // Pinned by design intent, confirmed with design system team (Alex + Akshay).
75
+ const CSS_KEYWORD_EASING_TOKEN_MAP = {
76
+ ease: 'motion.easing.out.practical',
77
+ 'ease-out': 'motion.easing.out.practical',
78
+ 'ease-in': 'motion.easing.in.practical',
79
+ 'ease-in-out': 'motion.easing.inout.bold'
80
+ // linear (0,0,1,1) — warn only, no autofix (per Akshay: too generic, no good token match)
81
+ };
82
+
83
+ // Non-curve easing values with no meaningful cubic-bezier representation — skip entirely
84
+ const SKIP_EASING_VALUES = new Set(['step-start', 'step-end', 'inherit', 'initial', 'unset', 'none']);
85
+ function euclideanDistance(a, b) {
86
+ return Math.sqrt(a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0));
87
+ }
88
+
89
+ // Maximum Euclidean distance for easing autofix — beyond this threshold, we report-only
90
+ const EASING_AUTOFIX_THRESHOLD = 0.5;
91
+ function findClosestEasingToken(params) {
92
+ let minDist = Infinity;
93
+ let closest = EASING_TOKENS[0];
94
+ for (const entry of EASING_TOKENS) {
95
+ const dist = euclideanDistance(params, entry.params);
96
+ if (dist < minDist) {
97
+ minDist = dist;
98
+ closest = entry;
99
+ }
100
+ }
101
+ if (minDist > EASING_AUTOFIX_THRESHOLD) {
102
+ return null;
103
+ }
104
+ return {
105
+ token: closest.token,
106
+ value: closest.value,
107
+ dist: minDist
108
+ };
109
+ }
110
+ function findClosestDurationTokens(ms) {
111
+ const exact = DURATION_TOKENS.find(t => t.ms === ms);
112
+ if (exact) {
113
+ return [exact];
114
+ }
115
+ let minDist = Infinity;
116
+ for (const entry of DURATION_TOKENS) {
117
+ const dist = Math.abs(entry.ms - ms);
118
+ if (dist < minDist) {
119
+ minDist = dist;
120
+ }
121
+ }
122
+ const closest = DURATION_TOKENS.filter(t => Math.abs(t.ms - ms) === minDist);
123
+ return closest;
124
+ }
125
+ export const useMotionTokenValues = {
126
+ meta: {
127
+ type: 'suggestion',
128
+ fixable: 'code',
129
+ docs: {
130
+ url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/use-motion-token-values/'
131
+ },
132
+ messages: {
133
+ useMotionDurationToken: "Use a motion duration token instead of the hard-coded value '{{ value }}'. Replace with {{ suggestion }}.",
134
+ useMotionDurationTokenNearest: "No exact token match for '{{ value }}'. Nearest: {{ suggestion1 }} or {{ suggestion2 }}.",
135
+ useMotionEasingToken: "Use a motion easing token instead of the hard-coded value '{{ value }}'. Replace with {{ suggestion }}.",
136
+ useMotionEasingTokenUnknown: "Use a motion easing token from @atlaskit/tokens instead of the hard-coded value '{{ value }}'."
137
+ }
138
+ },
139
+ create(context) {
140
+ let tokensImportNode = null;
141
+ let hasTokenSpecifier = false;
142
+ function buildTokenCall(tokenName, fallback) {
143
+ return `token('${tokenName}', '${fallback}')`;
144
+ }
145
+ function getImportFix(fixer) {
146
+ var _context$sourceCode;
147
+ if (hasTokenSpecifier) {
148
+ return [];
149
+ }
150
+ if (tokensImportNode) {
151
+ // @atlaskit/tokens is imported but without `token` — add `token` to existing import
152
+ const lastSpecifier = tokensImportNode.specifiers[tokensImportNode.specifiers.length - 1];
153
+ if (lastSpecifier) {
154
+ return [fixer.insertTextAfter(lastSpecifier, ', token')];
155
+ }
156
+ // Empty import — replace the whole declaration
157
+ return [fixer.replaceText(tokensImportNode, `import { token } from '@atlaskit/tokens';`)];
158
+ }
159
+ const sourceCode = (_context$sourceCode = context.sourceCode) !== null && _context$sourceCode !== void 0 ? _context$sourceCode : context.getSourceCode();
160
+ const programBody = sourceCode.ast.body;
161
+ // Insert after the last existing import, or at top if no imports exist
162
+ const lastImport = [...programBody].reverse().find(n => n.type === 'ImportDeclaration');
163
+ if (lastImport) {
164
+ return [fixer.insertTextAfter(lastImport, `\nimport { token } from '@atlaskit/tokens';`)];
165
+ }
166
+ if (programBody.length > 0) {
167
+ return [fixer.insertTextBefore(programBody[0], `import { token } from '@atlaskit/tokens';\n`)];
168
+ }
169
+ return [];
170
+ }
171
+
172
+ // Returns autofix string for a single duration value, or null if ambiguous (equidistant)
173
+ function resolveDurationToken(value) {
174
+ const ms = parseDurationMs(value);
175
+ if (ms === null) {
176
+ return null;
177
+ }
178
+ const result = findClosestDurationTokens(ms);
179
+ if (result.length === 1) {
180
+ return buildTokenCall(result[0].token, value);
181
+ }
182
+ return null;
183
+ }
184
+ function handleDurationProperty(node, rawValue) {
185
+ const segments = splitOnTopLevelCommas(rawValue);
186
+
187
+ // Single value path keeps the existing equidistant message
188
+ if (segments.length === 1) {
189
+ const ms = parseDurationMs(rawValue);
190
+ if (ms === null) {
191
+ return;
192
+ }
193
+ const result = findClosestDurationTokens(ms);
194
+ if (result.length === 1) {
195
+ const suggestion = buildTokenCall(result[0].token, rawValue);
196
+ context.report({
197
+ node,
198
+ messageId: 'useMotionDurationToken',
199
+ data: {
200
+ value: rawValue,
201
+ suggestion
202
+ },
203
+ fix(fixer) {
204
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
205
+ }
206
+ });
207
+ } else {
208
+ const suggestion1 = buildTokenCall(result[0].token, rawValue);
209
+ const suggestion2 = buildTokenCall(result[1].token, rawValue);
210
+ context.report({
211
+ node,
212
+ messageId: 'useMotionDurationTokenNearest',
213
+ data: {
214
+ value: rawValue,
215
+ suggestion1: `${suggestion1} (${result[0].ms}ms)`,
216
+ suggestion2: `${suggestion2} (${result[1].ms}ms)`
217
+ }
218
+ });
219
+ }
220
+ return;
221
+ }
222
+
223
+ // Multi-value path: every segment must resolve to a single token for autofix
224
+ const resolved = segments.map(resolveDurationToken);
225
+ if (resolved.some(s => s === null)) {
226
+ return;
227
+ }
228
+ // Build a template literal: `${token(...)}, ${token(...)}`
229
+ const templateLiteral = '`' + resolved.map(s => `\${${s}}`).join(', ') + '`';
230
+ context.report({
231
+ node,
232
+ messageId: 'useMotionDurationToken',
233
+ data: {
234
+ value: rawValue,
235
+ suggestion: templateLiteral
236
+ },
237
+ fix(fixer) {
238
+ return [...getImportFix(fixer), fixer.replaceText(node.value, templateLiteral)];
239
+ }
240
+ });
241
+ }
242
+
243
+ // Returns autofix string for a single easing value, or null if no token suggestion is possible
244
+ function resolveEasingToken(value) {
245
+ const trimmed = value.trim();
246
+ if (SKIP_EASING_VALUES.has(trimmed)) {
247
+ return null;
248
+ }
249
+ if (trimmed in CSS_KEYWORD_EASING_TOKEN_MAP) {
250
+ return buildTokenCall(CSS_KEYWORD_EASING_TOKEN_MAP[trimmed], trimmed);
251
+ }
252
+ // linear has no curve (0,0,1,1) — warn only, no autofix
253
+ if (trimmed === 'linear') {
254
+ return null;
255
+ }
256
+ if (trimmed.startsWith('linear(')) {
257
+ // linear() is used for spring animations — motion.easing.spring is experimental, skip
258
+ return null;
259
+ }
260
+ const params = parseCubicBezierParams(trimmed);
261
+ if (!params) {
262
+ return null;
263
+ }
264
+ const exact = EASING_TOKENS.find(t => t.value === trimmed);
265
+ if (exact) {
266
+ return buildTokenCall(exact.token, trimmed);
267
+ }
268
+ const closest = findClosestEasingToken(params);
269
+ return closest ? buildTokenCall(closest.token, trimmed) : null;
270
+ }
271
+ function handleEasingProperty(node, rawValue) {
272
+ const segments = splitOnTopLevelCommas(rawValue);
273
+
274
+ // Multi-value path: resolve each segment, autofix only if all resolve cleanly
275
+ if (segments.length > 1) {
276
+ const resolved = segments.map(resolveEasingToken);
277
+ if (resolved.some(s => s === null)) {
278
+ return;
279
+ }
280
+ const templateLiteral = '`' + resolved.map(s => `\${${s}}`).join(', ') + '`';
281
+ context.report({
282
+ node,
283
+ messageId: 'useMotionEasingToken',
284
+ data: {
285
+ value: rawValue,
286
+ suggestion: templateLiteral
287
+ },
288
+ fix(fixer) {
289
+ return [...getImportFix(fixer), fixer.replaceText(node.value, templateLiteral)];
290
+ }
291
+ });
292
+ return;
293
+ }
294
+ const trimmed = rawValue.trim();
295
+ if (SKIP_EASING_VALUES.has(trimmed)) {
296
+ return;
297
+ }
298
+
299
+ // CSS keyword easings: convert to cubic-bezier equivalent and find closest token
300
+ if (trimmed in CSS_KEYWORD_EASING_TOKEN_MAP) {
301
+ const suggestion = buildTokenCall(CSS_KEYWORD_EASING_TOKEN_MAP[trimmed], trimmed);
302
+ context.report({
303
+ node,
304
+ messageId: 'useMotionEasingToken',
305
+ data: {
306
+ value: trimmed,
307
+ suggestion
308
+ },
309
+ fix(fixer) {
310
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
311
+ }
312
+ });
313
+ return;
314
+ }
315
+ // linear has no curve (0,0,1,1) — warn only, no autofix
316
+ if (trimmed === 'linear') {
317
+ context.report({
318
+ node,
319
+ messageId: 'useMotionEasingTokenUnknown',
320
+ data: {
321
+ value: trimmed
322
+ }
323
+ });
324
+ return;
325
+ }
326
+ if (trimmed.startsWith('linear(')) {
327
+ // linear() is used for spring animations — motion.easing.spring is experimental, skip
328
+ return;
329
+ }
330
+ const params = parseCubicBezierParams(trimmed);
331
+ if (!params) {
332
+ context.report({
333
+ node,
334
+ messageId: 'useMotionEasingTokenUnknown',
335
+ data: {
336
+ value: rawValue
337
+ }
338
+ });
339
+ return;
340
+ }
341
+ const exact = EASING_TOKENS.find(t => t.value === trimmed);
342
+ if (exact) {
343
+ const suggestion = buildTokenCall(exact.token, rawValue);
344
+ context.report({
345
+ node,
346
+ messageId: 'useMotionEasingToken',
347
+ data: {
348
+ value: rawValue,
349
+ suggestion
350
+ },
351
+ fix(fixer) {
352
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
353
+ }
354
+ });
355
+ return;
356
+ }
357
+ const closest = findClosestEasingToken(params);
358
+ if (closest) {
359
+ const suggestion = buildTokenCall(closest.token, rawValue);
360
+ context.report({
361
+ node,
362
+ messageId: 'useMotionEasingToken',
363
+ data: {
364
+ value: rawValue,
365
+ suggestion
366
+ },
367
+ fix(fixer) {
368
+ return [...getImportFix(fixer), fixer.replaceText(node.value, suggestion)];
369
+ }
370
+ });
371
+ } else {
372
+ context.report({
373
+ node,
374
+ messageId: 'useMotionEasingTokenUnknown',
375
+ data: {
376
+ value: rawValue
377
+ }
378
+ });
379
+ }
380
+ }
381
+ function handleProperty(node) {
382
+ const key = node.key;
383
+ if (key.type !== 'Identifier') {
384
+ return;
385
+ }
386
+ const isDuration = DURATION_PROPERTIES.has(key.name);
387
+ const isEasing = EASING_PROPERTIES.has(key.name);
388
+ if (!isDuration && !isEasing) {
389
+ return;
390
+ }
391
+ const value = node.value;
392
+ if (value.type === 'TemplateLiteral') {
393
+ // Only handle no-interpolation template literals (e.g. `200ms`) — treat as string
394
+ const tl = value;
395
+ if (tl.expressions.length === 0 && tl.quasis.length === 1) {
396
+ var _tl$quasis$0$value$co;
397
+ const rawValue = (_tl$quasis$0$value$co = tl.quasis[0].value.cooked) !== null && _tl$quasis$0$value$co !== void 0 ? _tl$quasis$0$value$co : tl.quasis[0].value.raw;
398
+ if (isDuration) {
399
+ handleDurationProperty(node, rawValue);
400
+ } else {
401
+ handleEasingProperty(node, rawValue);
402
+ }
403
+ }
404
+ return;
405
+ }
406
+ if (value.type === 'CallExpression') {
407
+ const ce = value;
408
+ if (ce.callee.type === 'Identifier' && ce.callee.name === 'token') {
409
+ return;
410
+ }
411
+ return;
412
+ }
413
+ if (value.type === 'Literal') {
414
+ const lit = value;
415
+ let rawValue;
416
+ if (typeof lit.value === 'string') {
417
+ rawValue = lit.value;
418
+ } else if (typeof lit.value === 'number') {
419
+ // Treat bare numbers as ms
420
+ rawValue = `${lit.value}ms`;
421
+ } else {
422
+ return;
423
+ }
424
+ if (isDuration) {
425
+ handleDurationProperty(node, rawValue);
426
+ } else {
427
+ handleEasingProperty(node, rawValue);
428
+ }
429
+ }
430
+ }
431
+ return {
432
+ ImportDeclaration(node) {
433
+ if (node.source.value === '@atlaskit/tokens') {
434
+ tokensImportNode = node;
435
+ hasTokenSpecifier = node.specifiers.some(s => s.type === 'ImportSpecifier' && s.local.name === 'token');
436
+ }
437
+ },
438
+ Property(node) {
439
+ handleProperty(node);
440
+ }
441
+ };
442
+ }
443
+ };
444
+ export default useMotionTokenValues;