@idealyst/components 1.2.6 → 1.2.8
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/package.json +3 -3
- package/plugin/__tests__/web.test.ts +611 -0
- package/plugin/web.js +30 -0
- package/src/Accordion/Accordion.native.tsx +1 -1
- package/src/Accordion/Accordion.styles.tsx +3 -0
- package/src/Accordion/Accordion.web.tsx +21 -30
- package/src/Alert/Alert.styles.tsx +21 -8
- package/src/Alert/Alert.web.tsx +4 -4
- package/src/Button/Button.native.tsx +46 -20
- package/src/Button/Button.styles.tsx +15 -0
- package/src/Button/Button.web.tsx +51 -8
- package/src/Button/types.ts +7 -0
- package/src/Icon/Icon.native.tsx +2 -1
- package/src/Icon/Icon.styles.tsx +8 -4
- package/src/Icon/Icon.web.tsx +8 -4
- package/src/Icon/types.ts +34 -7
- package/src/Input/Input.styles.tsx +5 -1
- package/src/Input/Input.web.tsx +18 -12
- package/src/List/List.native.tsx +14 -2
- package/src/List/List.styles.tsx +6 -3
- package/src/List/List.web.tsx +14 -2
- package/src/List/ListItem.native.tsx +3 -2
- package/src/List/ListItem.web.tsx +3 -2
- package/src/examples/ButtonExamples.tsx +110 -0
- package/src/examples/IconExamples.tsx +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Shared component library for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"publish:npm": "npm publish"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@idealyst/theme": "^1.2.
|
|
59
|
+
"@idealyst/theme": "^1.2.8",
|
|
60
60
|
"@mdi/js": ">=7.0.0",
|
|
61
61
|
"@mdi/react": ">=1.0.0",
|
|
62
62
|
"@react-native-vector-icons/common": ">=12.0.0",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
-
"@idealyst/theme": "^1.2.
|
|
109
|
+
"@idealyst/theme": "^1.2.8",
|
|
110
110
|
"@idealyst/tooling": "^1.2.4",
|
|
111
111
|
"@mdi/react": "^1.6.1",
|
|
112
112
|
"@types/react": "^19.1.0",
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import * as babel from '@babel/core';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
// Import the plugin
|
|
5
|
+
const plugin = require('../web.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper to transform code and extract detected icon names
|
|
9
|
+
*/
|
|
10
|
+
function transform(code: string, options = {}, filename = 'test.tsx') {
|
|
11
|
+
const result = babel.transformSync(code, {
|
|
12
|
+
filename,
|
|
13
|
+
presets: [
|
|
14
|
+
['@babel/preset-react', { runtime: 'automatic' }],
|
|
15
|
+
'@babel/preset-typescript',
|
|
16
|
+
],
|
|
17
|
+
plugins: [[plugin, options]],
|
|
18
|
+
babelrc: false,
|
|
19
|
+
configFile: false,
|
|
20
|
+
});
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract icon names that were detected by checking the generated code
|
|
26
|
+
*/
|
|
27
|
+
function getDetectedIcons(transformedCode: string | null | undefined): string[] {
|
|
28
|
+
if (!transformedCode) return [];
|
|
29
|
+
|
|
30
|
+
// Look for the registerMany call and extract icon names
|
|
31
|
+
// Pattern: IconRegistry.registerMany({ "icon-name": _mdiIconName, ... })
|
|
32
|
+
// Note: Babel outputs double quotes
|
|
33
|
+
const match = transformedCode.match(/registerMany\(\{([^}]+)\}\)/);
|
|
34
|
+
if (!match) return [];
|
|
35
|
+
|
|
36
|
+
// Match both single and double quoted strings
|
|
37
|
+
const iconMatches = match[1].matchAll(/["']([^"']+)["']:/g);
|
|
38
|
+
return Array.from(iconMatches).map(m => m[1]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('MDI Icon Registry Babel Plugin', () => {
|
|
42
|
+
describe('JSX String Literal Detection', () => {
|
|
43
|
+
it('detects icon name in Icon component', () => {
|
|
44
|
+
const code = `<Icon name="home" />`;
|
|
45
|
+
const result = transform(code);
|
|
46
|
+
const icons = getDetectedIcons(result?.code);
|
|
47
|
+
|
|
48
|
+
expect(icons).toContain('home');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('detects icon name in IconSvg component', () => {
|
|
52
|
+
const code = `<IconSvg name="account" />`;
|
|
53
|
+
const result = transform(code);
|
|
54
|
+
const icons = getDetectedIcons(result?.code);
|
|
55
|
+
|
|
56
|
+
expect(icons).toContain('account');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('detects icon name with mdi: prefix', () => {
|
|
60
|
+
const code = `<Icon name="mdi:home" />`;
|
|
61
|
+
const result = transform(code);
|
|
62
|
+
const icons = getDetectedIcons(result?.code);
|
|
63
|
+
|
|
64
|
+
expect(icons).toContain('home');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('detects icon in JSX expression container', () => {
|
|
68
|
+
const code = `<Icon name={"cog"} />`;
|
|
69
|
+
const result = transform(code);
|
|
70
|
+
const icons = getDetectedIcons(result?.code);
|
|
71
|
+
|
|
72
|
+
expect(icons).toContain('cog');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('detects kebab-case icon names', () => {
|
|
76
|
+
const code = `<Icon name="account-circle" />`;
|
|
77
|
+
const result = transform(code);
|
|
78
|
+
const icons = getDetectedIcons(result?.code);
|
|
79
|
+
|
|
80
|
+
expect(icons).toContain('account-circle');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('Button Component Icon Props', () => {
|
|
85
|
+
it('detects leftIcon prop', () => {
|
|
86
|
+
const code = `<Button leftIcon="chevron-left">Back</Button>`;
|
|
87
|
+
const result = transform(code);
|
|
88
|
+
const icons = getDetectedIcons(result?.code);
|
|
89
|
+
|
|
90
|
+
expect(icons).toContain('chevron-left');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('detects rightIcon prop', () => {
|
|
94
|
+
const code = `<Button rightIcon="chevron-right">Next</Button>`;
|
|
95
|
+
const result = transform(code);
|
|
96
|
+
const icons = getDetectedIcons(result?.code);
|
|
97
|
+
|
|
98
|
+
expect(icons).toContain('chevron-right');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('detects both leftIcon and rightIcon', () => {
|
|
102
|
+
const code = `<Button leftIcon="arrow-left" rightIcon="arrow-right">Navigate</Button>`;
|
|
103
|
+
const result = transform(code);
|
|
104
|
+
const icons = getDetectedIcons(result?.code);
|
|
105
|
+
|
|
106
|
+
expect(icons).toContain('arrow-left');
|
|
107
|
+
expect(icons).toContain('arrow-right');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Input Component Icon Props', () => {
|
|
112
|
+
it('detects leftIcon in Input', () => {
|
|
113
|
+
const code = `<Input leftIcon="magnify" placeholder="Search..." />`;
|
|
114
|
+
const result = transform(code);
|
|
115
|
+
const icons = getDetectedIcons(result?.code);
|
|
116
|
+
|
|
117
|
+
expect(icons).toContain('magnify');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('detects rightIcon in Input', () => {
|
|
121
|
+
const code = `<Input rightIcon="close" />`;
|
|
122
|
+
const result = transform(code);
|
|
123
|
+
const icons = getDetectedIcons(result?.code);
|
|
124
|
+
|
|
125
|
+
expect(icons).toContain('close');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Other Components with Icon Props', () => {
|
|
130
|
+
it('detects icon in Badge', () => {
|
|
131
|
+
const code = `<Badge icon="check" />`;
|
|
132
|
+
const result = transform(code);
|
|
133
|
+
const icons = getDetectedIcons(result?.code);
|
|
134
|
+
|
|
135
|
+
expect(icons).toContain('check');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('detects icon in Alert', () => {
|
|
139
|
+
const code = `<Alert icon="alert-circle" />`;
|
|
140
|
+
const result = transform(code);
|
|
141
|
+
const icons = getDetectedIcons(result?.code);
|
|
142
|
+
|
|
143
|
+
expect(icons).toContain('alert-circle');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('detects icon and deleteIcon in Chip', () => {
|
|
147
|
+
const code = `<Chip icon="tag" deleteIcon="close-circle" />`;
|
|
148
|
+
const result = transform(code);
|
|
149
|
+
const icons = getDetectedIcons(result?.code);
|
|
150
|
+
|
|
151
|
+
expect(icons).toContain('tag');
|
|
152
|
+
expect(icons).toContain('close-circle');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('detects icon in MenuItem', () => {
|
|
156
|
+
const code = `<MenuItem icon="cog">Settings</MenuItem>`;
|
|
157
|
+
const result = transform(code);
|
|
158
|
+
const icons = getDetectedIcons(result?.code);
|
|
159
|
+
|
|
160
|
+
expect(icons).toContain('cog');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('detects leading and trailing in ListItem', () => {
|
|
164
|
+
const code = `<ListItem leading="account" trailing="chevron-right" />`;
|
|
165
|
+
const result = transform(code);
|
|
166
|
+
const icons = getDetectedIcons(result?.code);
|
|
167
|
+
|
|
168
|
+
expect(icons).toContain('account');
|
|
169
|
+
expect(icons).toContain('chevron-right');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Ternary Expression Detection', () => {
|
|
174
|
+
it('detects both icons in ternary expression in props', () => {
|
|
175
|
+
const code = `<Icon name={isVisible ? "eye" : "eye-off"} />`;
|
|
176
|
+
const result = transform(code);
|
|
177
|
+
const icons = getDetectedIcons(result?.code);
|
|
178
|
+
|
|
179
|
+
expect(icons).toContain('eye');
|
|
180
|
+
expect(icons).toContain('eye-off');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('detects icons in ternary with different conditions', () => {
|
|
184
|
+
const code = `<Icon name={hasError ? "alert" : "check"} />`;
|
|
185
|
+
const result = transform(code);
|
|
186
|
+
const icons = getDetectedIcons(result?.code);
|
|
187
|
+
|
|
188
|
+
expect(icons).toContain('alert');
|
|
189
|
+
expect(icons).toContain('check');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Logical Expression Detection', () => {
|
|
194
|
+
it('detects icon in logical AND expression', () => {
|
|
195
|
+
const code = `<Icon name={hasIcon && "star"} />`;
|
|
196
|
+
const result = transform(code);
|
|
197
|
+
const icons = getDetectedIcons(result?.code);
|
|
198
|
+
|
|
199
|
+
expect(icons).toContain('star');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('detects icon in logical OR expression', () => {
|
|
203
|
+
const code = `<Icon name={customIcon || "star"} />`;
|
|
204
|
+
const result = transform(code);
|
|
205
|
+
const icons = getDetectedIcons(result?.code);
|
|
206
|
+
|
|
207
|
+
// This tests the right side of OR
|
|
208
|
+
expect(icons).toContain('star');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Variable Reference Detection', () => {
|
|
213
|
+
it('detects icon from variable with string literal', () => {
|
|
214
|
+
const code = `
|
|
215
|
+
const iconName = "home";
|
|
216
|
+
<Icon name={iconName} />
|
|
217
|
+
`;
|
|
218
|
+
const result = transform(code);
|
|
219
|
+
const icons = getDetectedIcons(result?.code);
|
|
220
|
+
|
|
221
|
+
expect(icons).toContain('home');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('detects both icons from variable with ternary expression', () => {
|
|
225
|
+
const code = `
|
|
226
|
+
const iconName = isPasswordVisible ? 'eye-off' : 'eye';
|
|
227
|
+
<IconSvg name={iconName} />
|
|
228
|
+
`;
|
|
229
|
+
const result = transform(code);
|
|
230
|
+
const icons = getDetectedIcons(result?.code);
|
|
231
|
+
|
|
232
|
+
expect(icons).toContain('eye');
|
|
233
|
+
expect(icons).toContain('eye-off');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('detects icon from variable with logical expression', () => {
|
|
237
|
+
const code = `
|
|
238
|
+
const iconName = showIcon && 'star';
|
|
239
|
+
<Icon name={iconName} />
|
|
240
|
+
`;
|
|
241
|
+
const result = transform(code);
|
|
242
|
+
const icons = getDetectedIcons(result?.code);
|
|
243
|
+
|
|
244
|
+
expect(icons).toContain('star');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('detects icons from multiple variable assignments', () => {
|
|
248
|
+
const code = `
|
|
249
|
+
const icon1 = "home";
|
|
250
|
+
const icon2 = isActive ? "check" : "close";
|
|
251
|
+
<>
|
|
252
|
+
<Icon name={icon1} />
|
|
253
|
+
<Icon name={icon2} />
|
|
254
|
+
</>
|
|
255
|
+
`;
|
|
256
|
+
const result = transform(code);
|
|
257
|
+
const icons = getDetectedIcons(result?.code);
|
|
258
|
+
|
|
259
|
+
expect(icons).toContain('home');
|
|
260
|
+
expect(icons).toContain('check');
|
|
261
|
+
expect(icons).toContain('close');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('Object Property Detection', () => {
|
|
266
|
+
it('detects icon in object literal', () => {
|
|
267
|
+
const code = `
|
|
268
|
+
const menuItem = { icon: "home", label: "Home" };
|
|
269
|
+
`;
|
|
270
|
+
const result = transform(code);
|
|
271
|
+
const icons = getDetectedIcons(result?.code);
|
|
272
|
+
|
|
273
|
+
expect(icons).toContain('home');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('detects leftIcon and rightIcon in object', () => {
|
|
277
|
+
const code = `
|
|
278
|
+
const buttonConfig = {
|
|
279
|
+
leftIcon: "arrow-left",
|
|
280
|
+
rightIcon: "arrow-right",
|
|
281
|
+
};
|
|
282
|
+
`;
|
|
283
|
+
const result = transform(code);
|
|
284
|
+
const icons = getDetectedIcons(result?.code);
|
|
285
|
+
|
|
286
|
+
expect(icons).toContain('arrow-left');
|
|
287
|
+
expect(icons).toContain('arrow-right');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('detects leading and trailing in object', () => {
|
|
291
|
+
const code = `
|
|
292
|
+
const listItemConfig = {
|
|
293
|
+
leading: "account",
|
|
294
|
+
trailing: "chevron-right",
|
|
295
|
+
};
|
|
296
|
+
`;
|
|
297
|
+
const result = transform(code);
|
|
298
|
+
const icons = getDetectedIcons(result?.code);
|
|
299
|
+
|
|
300
|
+
expect(icons).toContain('account');
|
|
301
|
+
expect(icons).toContain('chevron-right');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('detects icons in array of objects', () => {
|
|
305
|
+
const code = `
|
|
306
|
+
const menuItems = [
|
|
307
|
+
{ icon: "home", label: "Home" },
|
|
308
|
+
{ icon: "cog", label: "Settings" },
|
|
309
|
+
{ icon: "account", label: "Profile" },
|
|
310
|
+
];
|
|
311
|
+
`;
|
|
312
|
+
const result = transform(code);
|
|
313
|
+
const icons = getDetectedIcons(result?.code);
|
|
314
|
+
|
|
315
|
+
expect(icons).toContain('home');
|
|
316
|
+
expect(icons).toContain('cog');
|
|
317
|
+
expect(icons).toContain('account');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('Config Icons Option', () => {
|
|
322
|
+
it('includes icons from config option', () => {
|
|
323
|
+
const code = `const x = 1;`; // No icon usage in code
|
|
324
|
+
const result = transform(code, { icons: ['star', 'heart'] });
|
|
325
|
+
const icons = getDetectedIcons(result?.code);
|
|
326
|
+
|
|
327
|
+
expect(icons).toContain('star');
|
|
328
|
+
expect(icons).toContain('heart');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('combines config icons with detected icons', () => {
|
|
332
|
+
const code = `<Icon name="home" />`;
|
|
333
|
+
const result = transform(code, { icons: ['star'] });
|
|
334
|
+
const icons = getDetectedIcons(result?.code);
|
|
335
|
+
|
|
336
|
+
expect(icons).toContain('home');
|
|
337
|
+
expect(icons).toContain('star');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('Multiple Icons in Same File', () => {
|
|
342
|
+
it('deduplicates repeated icon names', () => {
|
|
343
|
+
const code = `
|
|
344
|
+
<>
|
|
345
|
+
<Icon name="home" />
|
|
346
|
+
<Icon name="home" />
|
|
347
|
+
<Button leftIcon="home">Go Home</Button>
|
|
348
|
+
</>
|
|
349
|
+
`;
|
|
350
|
+
const result = transform(code);
|
|
351
|
+
const icons = getDetectedIcons(result?.code);
|
|
352
|
+
|
|
353
|
+
// Should only appear once
|
|
354
|
+
expect(icons.filter(i => i === 'home')).toHaveLength(1);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('collects all unique icons from complex component', () => {
|
|
358
|
+
const code = `
|
|
359
|
+
function MyComponent({ isEditing }) {
|
|
360
|
+
const statusIcon = isEditing ? "pencil" : "check";
|
|
361
|
+
return (
|
|
362
|
+
<div>
|
|
363
|
+
<Button leftIcon="arrow-left" rightIcon="arrow-right">Navigate</Button>
|
|
364
|
+
<Input leftIcon="magnify" rightIcon="close" />
|
|
365
|
+
<Icon name={statusIcon} />
|
|
366
|
+
<Alert icon="information" />
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
`;
|
|
371
|
+
const result = transform(code);
|
|
372
|
+
const icons = getDetectedIcons(result?.code);
|
|
373
|
+
|
|
374
|
+
expect(icons).toContain('arrow-left');
|
|
375
|
+
expect(icons).toContain('arrow-right');
|
|
376
|
+
expect(icons).toContain('magnify');
|
|
377
|
+
expect(icons).toContain('close');
|
|
378
|
+
expect(icons).toContain('pencil');
|
|
379
|
+
expect(icons).toContain('check');
|
|
380
|
+
expect(icons).toContain('information');
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('Cases That Should NOT Be Detected', () => {
|
|
385
|
+
it('does not detect icons for non-icon components', () => {
|
|
386
|
+
const code = `<div name="home" />`;
|
|
387
|
+
const result = transform(code);
|
|
388
|
+
const icons = getDetectedIcons(result?.code);
|
|
389
|
+
|
|
390
|
+
expect(icons).not.toContain('home');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('does not detect non-icon props', () => {
|
|
394
|
+
const code = `<Icon size="large" color="red" />`;
|
|
395
|
+
const result = transform(code);
|
|
396
|
+
const icons = getDetectedIcons(result?.code);
|
|
397
|
+
|
|
398
|
+
expect(icons).toHaveLength(0);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('does not detect icons with invalid characters', () => {
|
|
402
|
+
const code = `<Icon name="home@invalid" />`;
|
|
403
|
+
const result = transform(code);
|
|
404
|
+
const icons = getDetectedIcons(result?.code);
|
|
405
|
+
|
|
406
|
+
expect(icons).not.toContain('home@invalid');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('does not detect dynamic template literals', () => {
|
|
410
|
+
const code = '<Icon name={`icon-${dynamicPart}`} />';
|
|
411
|
+
const result = transform(code);
|
|
412
|
+
const icons = getDetectedIcons(result?.code);
|
|
413
|
+
|
|
414
|
+
// Dynamic template literals cannot be statically analyzed
|
|
415
|
+
expect(icons).toHaveLength(0);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('does not detect icons from function calls', () => {
|
|
419
|
+
const code = `<Icon name={getIconName()} />`;
|
|
420
|
+
const result = transform(code);
|
|
421
|
+
const icons = getDetectedIcons(result?.code);
|
|
422
|
+
|
|
423
|
+
// Function return values cannot be statically analyzed
|
|
424
|
+
expect(icons).toHaveLength(0);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('does not detect icons from complex expressions', () => {
|
|
428
|
+
const code = `<Icon name={icons[currentIndex]} />`;
|
|
429
|
+
const result = transform(code);
|
|
430
|
+
const icons = getDetectedIcons(result?.code);
|
|
431
|
+
|
|
432
|
+
// Array access cannot be statically analyzed
|
|
433
|
+
expect(icons).toHaveLength(0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('does not detect icons from object member access', () => {
|
|
437
|
+
const code = `<Icon name={config.iconName} />`;
|
|
438
|
+
const result = transform(code);
|
|
439
|
+
const icons = getDetectedIcons(result?.code);
|
|
440
|
+
|
|
441
|
+
// Object property access cannot be statically analyzed
|
|
442
|
+
expect(icons).toHaveLength(0);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('does not detect non-icon object properties', () => {
|
|
446
|
+
const code = `
|
|
447
|
+
const config = {
|
|
448
|
+
title: "home",
|
|
449
|
+
description: "star",
|
|
450
|
+
};
|
|
451
|
+
`;
|
|
452
|
+
const result = transform(code);
|
|
453
|
+
const icons = getDetectedIcons(result?.code);
|
|
454
|
+
|
|
455
|
+
// title and description are not icon props
|
|
456
|
+
expect(icons).not.toContain('home');
|
|
457
|
+
expect(icons).not.toContain('star');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('does not detect text children as icon names', () => {
|
|
461
|
+
const code = `<Text>eye</Text>`;
|
|
462
|
+
const result = transform(code);
|
|
463
|
+
const icons = getDetectedIcons(result?.code);
|
|
464
|
+
|
|
465
|
+
// Text content should not be detected as icons
|
|
466
|
+
expect(icons).not.toContain('eye');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('does not detect icon-like strings in other props', () => {
|
|
470
|
+
const code = `
|
|
471
|
+
<Button title="home" aria-label="eye">Click me</Button>
|
|
472
|
+
`;
|
|
473
|
+
const result = transform(code);
|
|
474
|
+
const icons = getDetectedIcons(result?.code);
|
|
475
|
+
|
|
476
|
+
// title and aria-label are not icon props
|
|
477
|
+
expect(icons).not.toContain('home');
|
|
478
|
+
expect(icons).not.toContain('eye');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('does not detect icon names in className or style props', () => {
|
|
482
|
+
const code = `
|
|
483
|
+
<div className="icon-home" style={{ icon: "star" }}>
|
|
484
|
+
<span data-icon="account">Content</span>
|
|
485
|
+
</div>
|
|
486
|
+
`;
|
|
487
|
+
const result = transform(code);
|
|
488
|
+
const icons = getDetectedIcons(result?.code);
|
|
489
|
+
|
|
490
|
+
// className values and data attributes should not be detected
|
|
491
|
+
expect(icons).not.toContain('home');
|
|
492
|
+
expect(icons).not.toContain('account');
|
|
493
|
+
// Note: style.icon might be detected by ObjectProperty visitor - that's intentional
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('does not detect string literals in JSX text content', () => {
|
|
497
|
+
const code = `
|
|
498
|
+
<View>
|
|
499
|
+
<Text>The eye icon shows visibility</Text>
|
|
500
|
+
<Text>Click home to go back</Text>
|
|
501
|
+
</View>
|
|
502
|
+
`;
|
|
503
|
+
const result = transform(code);
|
|
504
|
+
const icons = getDetectedIcons(result?.code);
|
|
505
|
+
|
|
506
|
+
expect(icons).not.toContain('eye');
|
|
507
|
+
expect(icons).not.toContain('home');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('does not detect icons from string concatenation', () => {
|
|
511
|
+
const code = `<Icon name={"home" + "-outline"} />`;
|
|
512
|
+
const result = transform(code);
|
|
513
|
+
const icons = getDetectedIcons(result?.code);
|
|
514
|
+
|
|
515
|
+
// Binary expression (string concat) cannot be statically analyzed
|
|
516
|
+
expect(icons).not.toContain('home');
|
|
517
|
+
expect(icons).not.toContain('home-outline');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('does not inject code when no icons are found', () => {
|
|
521
|
+
const code = `const x = 1 + 2;`;
|
|
522
|
+
const result = transform(code);
|
|
523
|
+
|
|
524
|
+
// Should not have IconRegistry import or registerMany call
|
|
525
|
+
expect(result?.code).not.toContain('IconRegistry');
|
|
526
|
+
expect(result?.code).not.toContain('registerMany');
|
|
527
|
+
expect(result?.code).not.toContain('@mdi/js');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('does not detect invalid icon names that are not in @mdi/js', () => {
|
|
531
|
+
const code = `<Icon name="definitely-not-a-real-mdi-icon-xyz123" />`;
|
|
532
|
+
const result = transform(code);
|
|
533
|
+
const icons = getDetectedIcons(result?.code);
|
|
534
|
+
|
|
535
|
+
// Invalid icon names should be filtered out during validation
|
|
536
|
+
expect(icons).not.toContain('definitely-not-a-real-mdi-icon-xyz123');
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe('Edge Cases', () => {
|
|
541
|
+
it('handles empty icon name gracefully', () => {
|
|
542
|
+
const code = `<Icon name="" />`;
|
|
543
|
+
const result = transform(code);
|
|
544
|
+
const icons = getDetectedIcons(result?.code);
|
|
545
|
+
|
|
546
|
+
expect(icons).toHaveLength(0);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('handles null-like values gracefully', () => {
|
|
550
|
+
const code = `<Icon name={null} />`;
|
|
551
|
+
const result = transform(code);
|
|
552
|
+
const icons = getDetectedIcons(result?.code);
|
|
553
|
+
|
|
554
|
+
expect(icons).toHaveLength(0);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('handles spread props without crashing', () => {
|
|
558
|
+
const code = `<Icon {...iconProps} />`;
|
|
559
|
+
const result = transform(code);
|
|
560
|
+
|
|
561
|
+
// Should not crash, just not detect any icons
|
|
562
|
+
expect(result?.code).toBeDefined();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('handles nested ternary expressions', () => {
|
|
566
|
+
const code = `<Icon name={a ? "home" : b ? "settings" : "account"} />`;
|
|
567
|
+
const result = transform(code);
|
|
568
|
+
const icons = getDetectedIcons(result?.code);
|
|
569
|
+
|
|
570
|
+
// Should at least detect the top-level branches
|
|
571
|
+
expect(icons).toContain('home');
|
|
572
|
+
// Nested ternary may or may not be fully detected depending on implementation
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('preserves original code functionality', () => {
|
|
576
|
+
const code = `
|
|
577
|
+
const MyComponent = () => {
|
|
578
|
+
const [visible, setVisible] = useState(false);
|
|
579
|
+
return <Icon name="home" onClick={() => setVisible(!visible)} />;
|
|
580
|
+
};
|
|
581
|
+
`;
|
|
582
|
+
const result = transform(code);
|
|
583
|
+
|
|
584
|
+
// Original code should still be present
|
|
585
|
+
expect(result?.code).toContain('useState');
|
|
586
|
+
expect(result?.code).toContain('setVisible');
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe('Real-World Input Password Toggle Pattern', () => {
|
|
591
|
+
it('detects eye icons from password toggle pattern', () => {
|
|
592
|
+
// This is the actual pattern used in Input.web.tsx
|
|
593
|
+
const code = `
|
|
594
|
+
const renderPasswordToggleIcon = () => {
|
|
595
|
+
const iconName = isPasswordVisible ? 'eye-off' : 'eye';
|
|
596
|
+
return (
|
|
597
|
+
<IconSvg
|
|
598
|
+
name={iconName}
|
|
599
|
+
aria-label={iconName}
|
|
600
|
+
/>
|
|
601
|
+
);
|
|
602
|
+
};
|
|
603
|
+
`;
|
|
604
|
+
const result = transform(code);
|
|
605
|
+
const icons = getDetectedIcons(result?.code);
|
|
606
|
+
|
|
607
|
+
expect(icons).toContain('eye');
|
|
608
|
+
expect(icons).toContain('eye-off');
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
package/plugin/web.js
CHANGED
|
@@ -335,6 +335,36 @@ module.exports = function ({ types: t }, options = {}) {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
+
// Handle ternary expression in variable: const iconName = cond ? 'eye' : 'eye-off'
|
|
339
|
+
else if (t.isConditionalExpression(init)) {
|
|
340
|
+
[init.consequent, init.alternate].forEach(branch => {
|
|
341
|
+
if (t.isStringLiteral(branch)) {
|
|
342
|
+
const iconName = extractIconName(branch.value);
|
|
343
|
+
if (iconName) {
|
|
344
|
+
const normalized = normalizeIconName(iconName);
|
|
345
|
+
if (normalized) {
|
|
346
|
+
state.iconNames.add(normalized);
|
|
347
|
+
debugLog(`Found icon via variable ternary ${expr.name}: ${normalized}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
// Handle logical expression in variable: const iconName = hasIcon && 'home'
|
|
354
|
+
else if (t.isLogicalExpression(init)) {
|
|
355
|
+
[init.left, init.right].forEach(side => {
|
|
356
|
+
if (t.isStringLiteral(side)) {
|
|
357
|
+
const iconName = extractIconName(side.value);
|
|
358
|
+
if (iconName) {
|
|
359
|
+
const normalized = normalizeIconName(iconName);
|
|
360
|
+
if (normalized) {
|
|
361
|
+
state.iconNames.add(normalized);
|
|
362
|
+
debugLog(`Found icon via variable logical ${expr.name}: ${normalized}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
338
368
|
}
|
|
339
369
|
}
|
|
340
370
|
}
|
|
@@ -43,7 +43,7 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
|
|
|
43
43
|
const iconStyle = (accordionStyles.icon as any)({});
|
|
44
44
|
const contentStyle = (accordionStyles.content as any)({});
|
|
45
45
|
const titleStyle = (accordionStyles.title as any)({});
|
|
46
|
-
const contentInnerStyle = (
|
|
46
|
+
const contentInnerStyle = (accordionStyles.contentInner as any)({});
|
|
47
47
|
|
|
48
48
|
// Animate height and icon rotation when expanded state changes
|
|
49
49
|
useEffect(() => {
|
|
@@ -117,8 +117,11 @@ export const accordionStyles = defineStyle('Accordion', (theme: Theme) => ({
|
|
|
117
117
|
},
|
|
118
118
|
},
|
|
119
119
|
_web: {
|
|
120
|
+
appearance: 'none',
|
|
121
|
+
background: 'none',
|
|
120
122
|
border: 'none',
|
|
121
123
|
outline: 'none',
|
|
124
|
+
margin: 0,
|
|
122
125
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
123
126
|
transition: 'background-color 0.2s ease',
|
|
124
127
|
_hover: disabled ? {} : { backgroundColor: theme.colors.surface.secondary },
|