@patternfly/context-for-ai 1.2.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 (99) hide show
  1. package/README.md +615 -0
  2. package/codemod/ALL_COMPONENTS_REFERENCE.md +815 -0
  3. package/codemod/ATTRIBUTE_DECISION_LOGIC.md +320 -0
  4. package/codemod/README.md +400 -0
  5. package/codemod/add-semantic-attributes.sh +69 -0
  6. package/codemod/component-attributes-reference.json +129 -0
  7. package/codemod/example-after.tsx +51 -0
  8. package/codemod/example-before.tsx +19 -0
  9. package/codemod/static-inference.js +5015 -0
  10. package/codemod/transform.js +1108 -0
  11. package/dist/components/advanced/index.d.ts +2 -0
  12. package/dist/components/advanced/index.d.ts.map +1 -0
  13. package/dist/components/core/Button.d.ts +14 -0
  14. package/dist/components/core/Button.d.ts.map +1 -0
  15. package/dist/components/core/Link.d.ts +15 -0
  16. package/dist/components/core/Link.d.ts.map +1 -0
  17. package/dist/components/core/StarIcon.d.ts +15 -0
  18. package/dist/components/core/StarIcon.d.ts.map +1 -0
  19. package/dist/components/core/index.d.ts +4 -0
  20. package/dist/components/core/index.d.ts.map +1 -0
  21. package/dist/components/data-display/Card.d.ts +14 -0
  22. package/dist/components/data-display/Card.d.ts.map +1 -0
  23. package/dist/components/data-display/StatusBadge.d.ts +13 -0
  24. package/dist/components/data-display/StatusBadge.d.ts.map +1 -0
  25. package/dist/components/data-display/Tbody.d.ts +12 -0
  26. package/dist/components/data-display/Tbody.d.ts.map +1 -0
  27. package/dist/components/data-display/Td.d.ts +14 -0
  28. package/dist/components/data-display/Td.d.ts.map +1 -0
  29. package/dist/components/data-display/Th.d.ts +14 -0
  30. package/dist/components/data-display/Th.d.ts.map +1 -0
  31. package/dist/components/data-display/Thead.d.ts +12 -0
  32. package/dist/components/data-display/Thead.d.ts.map +1 -0
  33. package/dist/components/data-display/Tr.d.ts +16 -0
  34. package/dist/components/data-display/Tr.d.ts.map +1 -0
  35. package/dist/components/data-display/index.d.ts +8 -0
  36. package/dist/components/data-display/index.d.ts.map +1 -0
  37. package/dist/components/feedback/index.d.ts +2 -0
  38. package/dist/components/feedback/index.d.ts.map +1 -0
  39. package/dist/components/forms/Checkbox.d.ts +16 -0
  40. package/dist/components/forms/Checkbox.d.ts.map +1 -0
  41. package/dist/components/forms/Form.d.ts +12 -0
  42. package/dist/components/forms/Form.d.ts.map +1 -0
  43. package/dist/components/forms/Radio.d.ts +32 -0
  44. package/dist/components/forms/Radio.d.ts.map +1 -0
  45. package/dist/components/forms/Select.d.ts +33 -0
  46. package/dist/components/forms/Select.d.ts.map +1 -0
  47. package/dist/components/forms/Switch.d.ts +31 -0
  48. package/dist/components/forms/Switch.d.ts.map +1 -0
  49. package/dist/components/forms/TextArea.d.ts +29 -0
  50. package/dist/components/forms/TextArea.d.ts.map +1 -0
  51. package/dist/components/forms/TextInput.d.ts +29 -0
  52. package/dist/components/forms/TextInput.d.ts.map +1 -0
  53. package/dist/components/forms/index.d.ts +8 -0
  54. package/dist/components/forms/index.d.ts.map +1 -0
  55. package/dist/components/index.d.ts +9 -0
  56. package/dist/components/index.d.ts.map +1 -0
  57. package/dist/components/layout/Flex.d.ts +16 -0
  58. package/dist/components/layout/Flex.d.ts.map +1 -0
  59. package/dist/components/layout/FlexItem.d.ts +16 -0
  60. package/dist/components/layout/FlexItem.d.ts.map +1 -0
  61. package/dist/components/layout/index.d.ts +3 -0
  62. package/dist/components/layout/index.d.ts.map +1 -0
  63. package/dist/components/navigation/DropdownItem.d.ts +8 -0
  64. package/dist/components/navigation/DropdownItem.d.ts.map +1 -0
  65. package/dist/components/navigation/MenuToggle.d.ts +8 -0
  66. package/dist/components/navigation/MenuToggle.d.ts.map +1 -0
  67. package/dist/components/navigation/index.d.ts +3 -0
  68. package/dist/components/navigation/index.d.ts.map +1 -0
  69. package/dist/components/overlay/Drawer.d.ts +12 -0
  70. package/dist/components/overlay/Drawer.d.ts.map +1 -0
  71. package/dist/components/overlay/Modal.d.ts +16 -0
  72. package/dist/components/overlay/Modal.d.ts.map +1 -0
  73. package/dist/components/overlay/index.d.ts +3 -0
  74. package/dist/components/overlay/index.d.ts.map +1 -0
  75. package/dist/context/SemanticContext.d.ts +28 -0
  76. package/dist/context/SemanticContext.d.ts.map +1 -0
  77. package/dist/hooks/index.d.ts +3 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/useAccessibility.d.ts +13 -0
  80. package/dist/hooks/useAccessibility.d.ts.map +1 -0
  81. package/dist/hooks/useSemanticMetadata.d.ts +9 -0
  82. package/dist/hooks/useSemanticMetadata.d.ts.map +1 -0
  83. package/dist/index.d.ts +574 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.esm.js +1362 -0
  86. package/dist/index.esm.js.map +1 -0
  87. package/dist/index.js +1426 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/types/index.d.ts +47 -0
  90. package/dist/types/index.d.ts.map +1 -0
  91. package/dist/utils/accessibility.d.ts +16 -0
  92. package/dist/utils/accessibility.d.ts.map +1 -0
  93. package/dist/utils/index.d.ts +4 -0
  94. package/dist/utils/index.d.ts.map +1 -0
  95. package/dist/utils/inference.d.ts +136 -0
  96. package/dist/utils/inference.d.ts.map +1 -0
  97. package/dist/utils/metadata.d.ts +17 -0
  98. package/dist/utils/metadata.d.ts.map +1 -0
  99. package/package.json +104 -0
@@ -0,0 +1,1108 @@
1
+ /**
2
+ * JSCodeShift Transform: Add Semantic Data Attributes to PatternFly Components
3
+ *
4
+ * This transform adds standardized data-* attributes to all PatternFly components
5
+ * in user code, making them more AI-friendly. Attributes appear on rendered DOM elements.
6
+ *
7
+ * Usage:
8
+ * jscodeshift -t codemod/transform.js path/to/files
9
+ */
10
+
11
+ const {
12
+ inferRole,
13
+ inferPurpose,
14
+ inferVariant,
15
+ inferContext,
16
+ inferState,
17
+ inferActionType,
18
+ inferSize,
19
+ inferActionListVariant,
20
+ isPatternFlyComponent,
21
+ STANDARD_ATTRIBUTES,
22
+ } = require('./static-inference');
23
+
24
+ /**
25
+ * Semantic attribute names (without 'data-' prefix)
26
+ */
27
+ const SEMANTIC_ATTR_NAMES = STANDARD_ATTRIBUTES.map(attr => attr.replace('data-', ''));
28
+
29
+ /**
30
+ * Check if element already has semantic attributes
31
+ */
32
+ function hasSemanticAttributes(attributes) {
33
+ if (!attributes || attributes.length === 0) {
34
+ return false;
35
+ }
36
+
37
+ return attributes.some(attr => {
38
+ const name = attr.name?.name || attr.name?.value;
39
+ if (!name) return false;
40
+
41
+ // Check for old format (data-semantic-*) or new format (data-role, etc.)
42
+ return name.startsWith('data-semantic-') || SEMANTIC_ATTR_NAMES.includes(name.replace('data-', ''));
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Find parent context by traversing up the AST
48
+ * Returns an object with { context, purpose } if parent is Form, or just context string for others
49
+ */
50
+ function findParentContext(path, imports) {
51
+ let current = path.parent;
52
+ let depth = 0;
53
+ const maxDepth = 10; // Prevent infinite loops
54
+
55
+ while (current && depth < maxDepth) {
56
+ if (current.value) {
57
+ const node = current.value;
58
+
59
+ // Check if parent is a JSX element
60
+ if (node.type === 'JSXElement' && node.openingElement) {
61
+ const parentName = node.openingElement.name?.name;
62
+ if (parentName && isPatternFlyComponent(parentName, imports)) {
63
+ const parentProps = node.openingElement.attributes || [];
64
+ const purpose = inferPurpose(parentName, parentProps);
65
+ const context = inferContext(parentName, parentProps);
66
+
67
+ // For Page, return page context so children inherit it
68
+ if (parentName.toLowerCase().includes('page') && !parentName.toLowerCase().includes('pagesection') &&
69
+ !parentName.toLowerCase().includes('pageheader') && !parentName.toLowerCase().includes('pagebody') &&
70
+ !parentName.toLowerCase().includes('pagefooter')) {
71
+ return 'page';
72
+ }
73
+
74
+ // For Sidebar, return sidebar context so children inherit it
75
+ if (parentName.toLowerCase().includes('sidebar') && !parentName.toLowerCase().includes('sidebarcontent') &&
76
+ !parentName.toLowerCase().includes('sidebarpanel')) {
77
+ return 'sidebar';
78
+ }
79
+
80
+ // For Toolbar, return toolbar context so children inherit it
81
+ if (parentName.toLowerCase().includes('toolbar') && !parentName.toLowerCase().includes('toolbaritem') &&
82
+ !parentName.toLowerCase().includes('toolbaritemgroup')) {
83
+ return 'toolbar';
84
+ }
85
+
86
+ // For Wizard, return wizard context so children inherit it
87
+ if (parentName.toLowerCase().includes('wizard') && !parentName.toLowerCase().includes('wizardnav') &&
88
+ !parentName.toLowerCase().includes('wizardnavitem') && !parentName.toLowerCase().includes('wizardbody') &&
89
+ !parentName.toLowerCase().includes('wizardfooter')) {
90
+ return 'wizard';
91
+ }
92
+
93
+ // For Form, return both context and purpose so children can inherit purpose
94
+ if (purpose === 'form-container') {
95
+ return { context, purpose };
96
+ }
97
+
98
+ // For LoginForm, return authentication context so children inherit it
99
+ if (purpose === 'authentication' || parentName.toLowerCase().includes('loginform')) {
100
+ return 'authentication';
101
+ }
102
+
103
+ // For overlay components, just return context
104
+ if (purpose === 'overlay') {
105
+ return context;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ current = current.parent;
112
+ depth++;
113
+ }
114
+
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Analyze ClipboardCopy children to infer content type variants (with-array, json-object)
120
+ * Traverses the AST to find array or JSON object content in children
121
+ */
122
+ function analyzeClipboardCopyChildren(j, path) {
123
+ let hasArray = false;
124
+ let hasJsonObject = false;
125
+
126
+ // Find the JSXElement that contains this opening element
127
+ let parentElement = null;
128
+ let current = path.parent;
129
+
130
+ // Traverse up to find the JSXElement
131
+ while (current && !parentElement) {
132
+ if (current.value && current.value.type === 'JSXElement') {
133
+ parentElement = current.value;
134
+ break;
135
+ }
136
+ current = current.parent;
137
+ }
138
+
139
+ if (!parentElement || !parentElement.children) {
140
+ return { hasArray: false, hasJsonObject: false };
141
+ }
142
+
143
+ // Look through children for text content that might indicate array or JSON
144
+ // Check for JSXText or JSXExpressionContainer with array/object patterns
145
+ parentElement.children.forEach(child => {
146
+ if (child.type === 'JSXText') {
147
+ const text = child.value.trim();
148
+ // Check for JSON object pattern (starts with { or contains JSON-like structure)
149
+ if (text.startsWith('{') || (text.includes('"') && (text.includes(':') || text.includes(',')))) {
150
+ hasJsonObject = true;
151
+ }
152
+ // Check for array pattern (starts with [)
153
+ if (text.startsWith('[')) {
154
+ hasArray = true;
155
+ }
156
+ } else if (child.type === 'JSXExpressionContainer' && child.expression) {
157
+ // Check for ArrayExpression or ObjectExpression in JSX expressions
158
+ if (child.expression.type === 'ArrayExpression') {
159
+ hasArray = true;
160
+ } else if (child.expression.type === 'ObjectExpression') {
161
+ hasJsonObject = true;
162
+ }
163
+ }
164
+ });
165
+
166
+ return { hasArray, hasJsonObject };
167
+ }
168
+
169
+ /**
170
+ * Analyze InputGroup children to infer variant (with-button-left, with-button-right, with-buttons-both, with-icon-before, with-icon-after, with-text-prefix, with-text-suffix, search)
171
+ * Traverses the AST to find buttons, text, icons, or search icons in InputGroup
172
+ */
173
+ function analyzeInputGroupChildren(j, path) {
174
+ let hasButtonLeft = false;
175
+ let hasButtonRight = false;
176
+ let hasIconLeft = false;
177
+ let hasIconRight = false;
178
+ let hasTextLeft = false;
179
+ let hasTextRight = false;
180
+ let hasSearch = false;
181
+
182
+ // Find the JSXElement that contains this opening element
183
+ let parentElement = null;
184
+ let current = path.parent;
185
+
186
+ // Traverse up to find the JSXElement
187
+ while (current && !parentElement) {
188
+ if (current.value && current.value.type === 'JSXElement') {
189
+ parentElement = current.value;
190
+ break;
191
+ }
192
+ current = current.parent;
193
+ }
194
+
195
+ if (!parentElement || !parentElement.children) {
196
+ return { hasButtonLeft: false, hasButtonRight: false, hasIconLeft: false, hasIconRight: false, hasTextLeft: false, hasTextRight: false, hasSearch: false };
197
+ }
198
+
199
+ // Look through children for buttons, text, or search icons
200
+ // InputGroup children are typically in order: [left element, input, right element]
201
+ let foundInput = false;
202
+ parentElement.children.forEach(child => {
203
+ if (child.type === 'JSXElement' && child.openingElement) {
204
+ const childName = child.openingElement.name?.name;
205
+ if (childName) {
206
+ const name = childName.toLowerCase();
207
+ // Check if this is the input (middle element)
208
+ if (name.includes('input') || name.includes('textinput') || name.includes('textarea') || name.includes('select')) {
209
+ foundInput = true;
210
+ return; // Skip input itself
211
+ }
212
+
213
+ // If we haven't found the input yet, this is a left element
214
+ // If we've found the input, this is a right element
215
+ const isLeft = !foundInput;
216
+
217
+ // Check for buttons
218
+ if (name.includes('button')) {
219
+ if (isLeft) {
220
+ hasButtonLeft = true;
221
+ } else {
222
+ hasButtonRight = true;
223
+ }
224
+ }
225
+ // Check for icons (components ending with "icon" or "Icon")
226
+ if (name.endsWith('icon')) {
227
+ if (isLeft) {
228
+ hasIconLeft = true;
229
+ } else {
230
+ hasIconRight = true;
231
+ }
232
+ }
233
+ // Check for InputGroupText (text prefix/suffix, not icon)
234
+ if (name.includes('inputgrouptext')) {
235
+ if (isLeft) {
236
+ hasTextLeft = true;
237
+ } else {
238
+ hasTextRight = true;
239
+ }
240
+ }
241
+ // Check for search icon/button
242
+ if (name.includes('search') || name.includes('searchicon')) {
243
+ hasSearch = true;
244
+ // Search button is typically on the right
245
+ if (!isLeft) {
246
+ hasButtonRight = true;
247
+ hasIconRight = true;
248
+ } else {
249
+ hasIconLeft = true;
250
+ }
251
+ }
252
+ }
253
+ }
254
+ });
255
+
256
+ return { hasButtonLeft, hasButtonRight, hasIconLeft, hasIconRight, hasTextLeft, hasTextRight, hasSearch };
257
+ }
258
+
259
+ /**
260
+ * Analyze List children to infer variant (with-icons, small-icons, big-icons)
261
+ * Traverses the AST to find icons in ListItem children
262
+ */
263
+ function analyzeListChildren(j, path) {
264
+ let hasIcons = false;
265
+ let hasSmallIcons = false;
266
+ let hasBigIcons = false;
267
+
268
+ // Find the JSXElement that contains this opening element
269
+ let parentElement = null;
270
+ let current = path.parent;
271
+
272
+ // Traverse up to find the JSXElement
273
+ while (current && !parentElement) {
274
+ if (current.value && current.value.type === 'JSXElement') {
275
+ parentElement = current.value;
276
+ break;
277
+ }
278
+ current = current.parent;
279
+ }
280
+
281
+ if (!parentElement || !parentElement.children) {
282
+ return { hasIcons: false, hasSmallIcons: false, hasBigIcons: false };
283
+ }
284
+
285
+ // Look through children for ListItem components with icons
286
+ parentElement.children.forEach(child => {
287
+ if (child.type === 'JSXElement' && child.openingElement) {
288
+ const childName = child.openingElement.name?.name;
289
+ if (childName && childName.toLowerCase().includes('listitem')) {
290
+ // Check if ListItem has icon children
291
+ if (child.children) {
292
+ child.children.forEach(grandchild => {
293
+ if (grandchild.type === 'JSXElement' && grandchild.openingElement) {
294
+ const grandchildName = grandchild.openingElement.name?.name;
295
+ if (grandchildName) {
296
+ const name = grandchildName.toLowerCase();
297
+ // Check for icon components (ending with "icon" or "Icon")
298
+ if (name.endsWith('icon')) {
299
+ hasIcons = true;
300
+ // Check icon size if available
301
+ const iconProps = grandchild.openingElement.attributes || [];
302
+ const sizeAttr = iconProps.find(attr =>
303
+ attr.name?.name === 'size'
304
+ );
305
+ if (sizeAttr && sizeAttr.value) {
306
+ const size = sizeAttr.value.type === 'StringLiteral'
307
+ ? sizeAttr.value.value.toLowerCase()
308
+ : null;
309
+ if (size === 'sm' || size === 'small') {
310
+ hasSmallIcons = true;
311
+ } else if (size === 'lg' || size === 'large' || size === 'xl') {
312
+ hasBigIcons = true;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ });
319
+ }
320
+ }
321
+ }
322
+ });
323
+
324
+ return { hasIcons, hasSmallIcons, hasBigIcons };
325
+ }
326
+
327
+ /**
328
+ * Analyze LoginForm children to infer variant (show-hide-password, customized-header-utilities)
329
+ * Traverses the AST to find password toggle or customized header utilities
330
+ */
331
+ function analyzeLoginFormChildren(j, path) {
332
+ let hasShowHidePassword = false;
333
+ let hasCustomizedHeaderUtilities = false;
334
+
335
+ // Find the JSXElement that contains this opening element
336
+ let parentElement = null;
337
+ let current = path.parent;
338
+
339
+ // Traverse up to find the JSXElement
340
+ while (current && !parentElement) {
341
+ if (current.value && current.value.type === 'JSXElement') {
342
+ parentElement = current.value;
343
+ break;
344
+ }
345
+ current = current.parent;
346
+ }
347
+
348
+ if (!parentElement || !parentElement.children) {
349
+ return { hasShowHidePassword: false, hasCustomizedHeaderUtilities: false };
350
+ }
351
+
352
+ // Look through children for password toggle or customized header utilities
353
+ parentElement.children.forEach(child => {
354
+ if (child.type === 'JSXElement' && child.openingElement) {
355
+ const childName = child.openingElement.name?.name;
356
+ if (childName) {
357
+ const name = childName.toLowerCase();
358
+ // Check for InputGroup containing password input with toggle button
359
+ if (name.includes('inputgroup')) {
360
+ // Check if InputGroup contains password input and button
361
+ let hasPasswordInput = false;
362
+ let hasToggleButton = false;
363
+ if (child.children) {
364
+ child.children.forEach(grandchild => {
365
+ if (grandchild.type === 'JSXElement' && grandchild.openingElement) {
366
+ const grandchildName = grandchild.openingElement.name?.name;
367
+ if (grandchildName) {
368
+ const gcName = grandchildName.toLowerCase();
369
+ // Check for password input
370
+ if (gcName.includes('textinput') || gcName.includes('input')) {
371
+ const inputProps = grandchild.openingElement.attributes || [];
372
+ const typeAttr = inputProps.find(attr => attr.name?.name === 'type');
373
+ if (typeAttr && typeAttr.value) {
374
+ const typeValue = typeAttr.value.type === 'StringLiteral'
375
+ ? typeAttr.value.value.toLowerCase()
376
+ : '';
377
+ if (typeValue === 'password') {
378
+ hasPasswordInput = true;
379
+ }
380
+ }
381
+ }
382
+ // Check for button (toggle)
383
+ if (gcName.includes('button')) {
384
+ hasToggleButton = true;
385
+ }
386
+ }
387
+ }
388
+ });
389
+ }
390
+ if (hasPasswordInput && hasToggleButton) {
391
+ hasShowHidePassword = true;
392
+ }
393
+ }
394
+ // Check for password input with show/hide toggle prop
395
+ if (name.includes('textinput') || name.includes('input')) {
396
+ const inputProps = child.openingElement.attributes || [];
397
+ const typeAttr = inputProps.find(attr => attr.name?.name === 'type');
398
+ const hasToggle = inputProps.some(attr =>
399
+ attr.name?.name === 'showPasswordToggle' ||
400
+ attr.name?.name === 'isPasswordToggleVisible' ||
401
+ attr.name?.name === 'onTogglePassword'
402
+ );
403
+ if (typeAttr && typeAttr.value) {
404
+ const typeValue = typeAttr.value.type === 'StringLiteral'
405
+ ? typeAttr.value.value.toLowerCase()
406
+ : '';
407
+ if (typeValue === 'password' && hasToggle) {
408
+ hasShowHidePassword = true;
409
+ }
410
+ }
411
+ }
412
+ // Check for customized header utilities (LoginHeader, LoginHeaderUtilities, etc.)
413
+ if (name.includes('loginheader') || name.includes('header') ||
414
+ (name.includes('utilities') && name.includes('login')) ||
415
+ (name.includes('brand') && name.includes('login')) ||
416
+ (name.includes('logo') && name.includes('login'))) {
417
+ hasCustomizedHeaderUtilities = true;
418
+ }
419
+ }
420
+ }
421
+ });
422
+
423
+ return { hasShowHidePassword, hasCustomizedHeaderUtilities };
424
+ }
425
+
426
+ /**
427
+ * Analyze Breadcrumb children to infer variant (with-dropdown, with-heading)
428
+ * Traverses the AST to find BreadcrumbDropdown or BreadcrumbHeading children
429
+ */
430
+ function analyzeBreadcrumbChildren(j, path) {
431
+ let hasDropdown = false;
432
+ let hasHeading = false;
433
+
434
+ // Find the JSXElement that contains this opening element
435
+ let parentElement = null;
436
+ let current = path.parent;
437
+
438
+ // Traverse up to find the JSXElement
439
+ while (current && !parentElement) {
440
+ if (current.value && current.value.type === 'JSXElement') {
441
+ parentElement = current.value;
442
+ break;
443
+ }
444
+ current = current.parent;
445
+ }
446
+
447
+ if (!parentElement || !parentElement.children) {
448
+ return { hasDropdown: false, hasHeading: false };
449
+ }
450
+
451
+ // Look through children for BreadcrumbDropdown or BreadcrumbHeading
452
+ parentElement.children.forEach(child => {
453
+ if (child.type === 'JSXElement' && child.openingElement) {
454
+ const childName = child.openingElement.name?.name;
455
+ if (childName) {
456
+ const name = childName.toLowerCase();
457
+ if (name.includes('breadcrumbdropdown') || name.includes('dropdown')) {
458
+ hasDropdown = true;
459
+ }
460
+ if (name.includes('breadcrumbheading') || name.includes('heading')) {
461
+ hasHeading = true;
462
+ }
463
+ }
464
+ }
465
+ });
466
+
467
+ return { hasDropdown, hasHeading };
468
+ }
469
+
470
+ /**
471
+ * Analyze ActionList children to infer grouping variant
472
+ * Traverses the AST to find ActionListItem children and their button/kebab contents
473
+ */
474
+ function analyzeActionListChildren(j, path) {
475
+ const children = [];
476
+
477
+ // Find the JSXElement that contains this opening element
478
+ // path.node is the JSXOpeningElement, we need the parent JSXElement
479
+ let parentElement = null;
480
+ let current = path.parent;
481
+
482
+ // Traverse up to find the JSXElement
483
+ while (current && !parentElement) {
484
+ if (current.value && current.value.type === 'JSXElement') {
485
+ parentElement = current.value;
486
+ break;
487
+ }
488
+ current = current.parent;
489
+ }
490
+
491
+ if (!parentElement || !parentElement.children) {
492
+ return children;
493
+ }
494
+
495
+ // Look through children for ActionListItem components
496
+ parentElement.children.forEach(child => {
497
+ if (child.type === 'JSXElement' && child.openingElement) {
498
+ const childName = child.openingElement.name?.name;
499
+ if (childName && childName.toLowerCase().includes('actionlistitem')) {
500
+ // Look for buttons or kebab inside ActionListItem
501
+ if (child.children) {
502
+ child.children.forEach(grandchild => {
503
+ if (grandchild.type === 'JSXElement' && grandchild.openingElement) {
504
+ const grandchildName = grandchild.openingElement.name?.name;
505
+ if (grandchildName) {
506
+ const name = grandchildName.toLowerCase();
507
+ if (name.includes('button')) {
508
+ // Check button variant
509
+ const buttonProps = grandchild.openingElement.attributes || [];
510
+ const variantAttr = buttonProps.find(attr =>
511
+ attr.name?.name === 'variant'
512
+ );
513
+ if (variantAttr && variantAttr.value) {
514
+ const variant = variantAttr.value.type === 'StringLiteral'
515
+ ? variantAttr.value.value
516
+ : 'button';
517
+ children.push(variant);
518
+ } else {
519
+ // No variant specified, default to 'button' (will be inferred as primary by button logic)
520
+ children.push('button');
521
+ }
522
+ } else if (name.includes('kebab') || name.includes('menutoggle') || name.includes('icon')) {
523
+ // Generic "icon" instead of "kebab" to cover kebab and other icons
524
+ children.push('icon');
525
+ }
526
+ }
527
+ }
528
+ });
529
+ }
530
+ }
531
+ }
532
+ });
533
+
534
+ return children;
535
+ }
536
+
537
+ /**
538
+ * Check if a form input has HelperText associated with it
539
+ * HelperText can be:
540
+ * 1. A parent wrapper: <HelperText><TextInput /></HelperText>
541
+ * 2. A sibling in the same parent: <FormGroup><TextInput /><HelperText /></FormGroup>
542
+ *
543
+ * HelperText is state-dependent (error, warning, success, indeterminate) and should be a variant of the input
544
+ */
545
+ function checkForHelperText(j, path) {
546
+ let hasHelperText = false;
547
+ let helperTextState = null; // 'error', 'warning', 'success', 'indeterminate' (default)
548
+
549
+ // Helper function to extract state from HelperText props
550
+ function extractHelperTextState(helperTextProps) {
551
+ const propsMap = new Map();
552
+ helperTextProps.forEach(attr => {
553
+ if (attr.name && attr.name.name) {
554
+ let value = true; // Boolean shorthand
555
+ if (attr.value) {
556
+ if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
557
+ value = attr.value.value;
558
+ } else if (attr.value.type === 'JSXExpressionContainer' && attr.value.expression) {
559
+ if (attr.value.expression.type === 'BooleanLiteral') {
560
+ value = attr.value.expression.value;
561
+ }
562
+ } else if (attr.value.type === 'BooleanLiteral') {
563
+ value = attr.value.value;
564
+ }
565
+ }
566
+ propsMap.set(attr.name.name, value);
567
+ }
568
+ });
569
+
570
+ // Determine state from HelperText props (priority: error > warning > success > indeterminate)
571
+ if (propsMap.has('isError') && propsMap.get('isError')) {
572
+ return 'error';
573
+ } else if (propsMap.has('isWarning') && propsMap.get('isWarning')) {
574
+ return 'warning';
575
+ } else if (propsMap.has('isSuccess') && propsMap.get('isSuccess')) {
576
+ return 'success';
577
+ } else if (propsMap.has('isIndeterminate') && propsMap.get('isIndeterminate')) {
578
+ return 'indeterminate';
579
+ } else {
580
+ // Default state if no explicit state prop
581
+ return 'indeterminate';
582
+ }
583
+ }
584
+
585
+ // CASE 1: Check if HelperText is a parent wrapper (traverse UP)
586
+ let current = path.parent;
587
+ let depth = 0;
588
+ const maxDepth = 10; // Prevent infinite loops
589
+
590
+ while (current && depth < maxDepth) {
591
+ if (current.value) {
592
+ const node = current.value;
593
+
594
+ // Check if parent is a JSX element
595
+ if (node.type === 'JSXElement' && node.openingElement) {
596
+ const parentName = node.openingElement.name?.name;
597
+ if (parentName) {
598
+ const name = parentName.toLowerCase();
599
+ // Check if this parent is HelperText
600
+ if (name.includes('helpertext') || name.includes('helper-text')) {
601
+ hasHelperText = true;
602
+ const helperTextProps = node.openingElement.attributes || [];
603
+ helperTextState = extractHelperTextState(helperTextProps);
604
+ // Found HelperText parent, return early
605
+ return { hasHelperText, helperTextState };
606
+ }
607
+ }
608
+ }
609
+ }
610
+
611
+ current = current.parent;
612
+ depth++;
613
+ }
614
+
615
+ // CASE 2: Check if HelperText is a sibling in the same parent container
616
+ // Find the parent JSXElement that contains this component
617
+ let parentElement = null;
618
+ current = path.parent;
619
+ depth = 0;
620
+
621
+ while (current && depth < maxDepth && !parentElement) {
622
+ if (current.value && current.value.type === 'JSXElement') {
623
+ parentElement = current.value;
624
+ break;
625
+ }
626
+ current = current.parent;
627
+ depth++;
628
+ }
629
+
630
+ if (parentElement && parentElement.children) {
631
+ // Look through siblings for HelperText component
632
+ parentElement.children.forEach(child => {
633
+ if (child.type === 'JSXElement' && child.openingElement) {
634
+ const childName = child.openingElement.name?.name;
635
+ if (childName) {
636
+ const name = childName.toLowerCase();
637
+ // Check for HelperText component (sibling)
638
+ if (name.includes('helpertext') || name.includes('helper-text')) {
639
+ hasHelperText = true;
640
+ const helperTextProps = child.openingElement.attributes || [];
641
+ helperTextState = extractHelperTextState(helperTextProps);
642
+ // Found HelperText sibling, stop looking
643
+ return;
644
+ }
645
+ }
646
+ }
647
+ });
648
+ }
649
+
650
+ return { hasHelperText, helperTextState };
651
+ }
652
+
653
+ /**
654
+ * Analyze DualListSelector children to infer variants (with-tooltips, with-search, with-actions, multiple-drop-zones)
655
+ * Traverses the AST to find search inputs, action menus, tooltips, and multiple drop zones
656
+ */
657
+ function analyzeDualListSelectorChildren(j, path) {
658
+ let hasTooltips = false;
659
+ let hasSearch = false;
660
+ let hasActions = false;
661
+ let hasMultipleDropZones = false;
662
+
663
+ // Find the JSXElement that contains this opening element
664
+ let parentElement = null;
665
+ let current = path.parent;
666
+
667
+ // Traverse up to find the JSXElement
668
+ while (current && !parentElement) {
669
+ if (current.value && current.value.type === 'JSXElement') {
670
+ parentElement = current.value;
671
+ break;
672
+ }
673
+ current = current.parent;
674
+ }
675
+
676
+ if (!parentElement || !parentElement.children) {
677
+ return { hasTooltips: false, hasSearch: false, hasActions: false, hasMultipleDropZones: false };
678
+ }
679
+
680
+ let dropZoneCount = 0;
681
+
682
+ // Look through children for search, actions, tooltips, and drop zones
683
+ parentElement.children.forEach(child => {
684
+ if (child.type === 'JSXElement' && child.openingElement) {
685
+ const childName = child.openingElement.name?.name;
686
+ if (childName) {
687
+ const name = childName.toLowerCase();
688
+ // Check for search/filter components
689
+ if (name.includes('search') || name.includes('filter') || name.includes('input')) {
690
+ hasSearch = true;
691
+ }
692
+ // Check for action menus (kebab, menu, actions)
693
+ if (name.includes('kebab') || name.includes('menutoggle') || name.includes('actionmenu') ||
694
+ name.includes('actions')) {
695
+ hasActions = true;
696
+ }
697
+ // Check for tooltips
698
+ if (name.includes('tooltip')) {
699
+ hasTooltips = true;
700
+ }
701
+ // Check for drop zones (Droppable components)
702
+ if (name.includes('droppable')) {
703
+ dropZoneCount++;
704
+ }
705
+ }
706
+ }
707
+ });
708
+
709
+ hasMultipleDropZones = dropZoneCount > 1;
710
+
711
+ return { hasTooltips, hasSearch, hasActions, hasMultipleDropZones };
712
+ }
713
+
714
+ /**
715
+ * Create semantic data attributes
716
+ * Only adds attributes when we can infer meaningful values (not null)
717
+ * parentContext can be a string (context) or an object { context, purpose } for Form parents
718
+ */
719
+ function createSemanticAttributes(j, componentName, props, parentContext, path = null) {
720
+ const role = inferRole(componentName);
721
+
722
+ // Extract parent context and purpose if parentContext is an object (Form case)
723
+ // Also used for purpose inference (ToggleGroup uses toolbar context for filtering)
724
+ let parentContextValue = null;
725
+ let parentPurpose = null;
726
+ if (typeof parentContext === 'object' && parentContext !== null) {
727
+ parentContextValue = parentContext.context;
728
+ parentPurpose = parentContext.purpose;
729
+ } else {
730
+ parentContextValue = parentContext;
731
+ }
732
+
733
+ const purpose = inferPurpose(componentName, props, parentContextValue);
734
+
735
+ // For ActionList, analyze children to infer grouping variant
736
+ // For Breadcrumb, analyze children to detect dropdown/heading variants
737
+ // For ClipboardCopy, analyze children to detect content type variants (array, json-object)
738
+ // For DualListSelector, analyze children to detect sub-variants (with-tooltips, with-search, with-actions, multiple-drop-zones)
739
+ // For form inputs (TextInput, TextArea, Select, etc.), check for HelperText as parent wrapper or sibling
740
+ // For InlineEdit, detect if it's in a table row context (row-editing variant)
741
+ // For InputGroup, analyze children to detect variant (with-button-left, with-button-right, with-buttons-both, with-icon-before, with-icon-after, with-text-prefix, with-text-suffix, search)
742
+ // For List, analyze children to detect icon variants (with-icons, small-icons, big-icons)
743
+ // For LoginForm, analyze children to detect variant (show-hide-password, customized-header-utilities)
744
+ let variant;
745
+ if (componentName.toLowerCase().includes('actionlist') && path) {
746
+ const children = analyzeActionListChildren(j, path);
747
+ variant = inferActionListVariant(children);
748
+ } else if (componentName.toLowerCase().includes('breadcrumb') &&
749
+ !componentName.toLowerCase().includes('breadcrumbitem') &&
750
+ !componentName.toLowerCase().includes('breadcrumbheading') &&
751
+ path) {
752
+ const childrenInfo = analyzeBreadcrumbChildren(j, path);
753
+ // Override variant if children analysis detects dropdown or heading
754
+ const baseVariant = inferVariant(componentName, props);
755
+ if (childrenInfo.hasDropdown) {
756
+ variant = 'with-dropdown';
757
+ } else if (childrenInfo.hasHeading) {
758
+ variant = 'with-heading';
759
+ } else {
760
+ variant = baseVariant;
761
+ }
762
+ } else if ((componentName.toLowerCase().includes('textinput') ||
763
+ componentName.toLowerCase().includes('textarea') ||
764
+ componentName.toLowerCase().includes('select') ||
765
+ componentName.toLowerCase().includes('checkbox') ||
766
+ componentName.toLowerCase().includes('radio') ||
767
+ componentName.toLowerCase().includes('switch')) &&
768
+ path) {
769
+ // Form inputs with HelperText (parent wrapper or sibling) get "with-helper-text" variant
770
+ const helperTextInfo = checkForHelperText(j, path);
771
+ const baseVariant = inferVariant(componentName, props);
772
+ if (helperTextInfo.hasHelperText) {
773
+ const variantParts = baseVariant ? baseVariant.split('-') : [];
774
+ if (!variantParts.includes('with-helper-text')) {
775
+ variantParts.push('with-helper-text');
776
+ }
777
+ variant = variantParts.length > 0 ? variantParts.join('-') : 'with-helper-text';
778
+ } else {
779
+ variant = baseVariant;
780
+ }
781
+ } else if (componentName.toLowerCase().includes('inlineedit') &&
782
+ !componentName.toLowerCase().includes('inlineedittoggle') &&
783
+ !componentName.toLowerCase().includes('inlineeditactiongroup') &&
784
+ !componentName.toLowerCase().includes('inlineeditinput') &&
785
+ path) {
786
+ // Detect if InlineEdit is in a table row context (row-editing variant)
787
+ // Traverse up the AST to find if parent is a table row (Tr)
788
+ let isInTableRow = false;
789
+ let current = path.parent;
790
+ let depth = 0;
791
+ const maxDepth = 10;
792
+
793
+ while (current && depth < maxDepth) {
794
+ if (current.value) {
795
+ const node = current.value;
796
+ if (node.type === 'JSXElement' && node.openingElement) {
797
+ const parentName = node.openingElement.name?.name;
798
+ if (parentName) {
799
+ const parentNameLower = parentName.toLowerCase();
800
+ // Check if parent is a table row (Tr)
801
+ if (parentNameLower === 'tr' || parentNameLower.includes('tablerow')) {
802
+ isInTableRow = true;
803
+ break;
804
+ }
805
+ // If we hit a table or tbody, we're in table context but not necessarily a row
806
+ // Continue searching for Tr
807
+ if (parentNameLower.includes('table') || parentNameLower.includes('tbody') ||
808
+ parentNameLower.includes('thead')) {
809
+ // Continue searching
810
+ } else if (!parentNameLower.includes('td') && !parentNameLower.includes('th')) {
811
+ // If we hit something that's not a table-related component, stop searching
812
+ break;
813
+ }
814
+ }
815
+ }
816
+ }
817
+ current = current.parent;
818
+ depth++;
819
+ }
820
+
821
+ const baseVariant = inferVariant(componentName, props);
822
+ // If in table row context and no explicit variant, set to row-editing
823
+ if (isInTableRow && !baseVariant) {
824
+ variant = 'row-editing';
825
+ } else {
826
+ variant = baseVariant;
827
+ }
828
+ } else if (componentName.toLowerCase().includes('inputgroup') && path) {
829
+ const childrenInfo = analyzeInputGroupChildren(j, path);
830
+ const baseVariant = inferVariant(componentName, props);
831
+
832
+ // Determine variant based on children analysis
833
+ if (childrenInfo.hasSearch) {
834
+ variant = 'search';
835
+ } else if (childrenInfo.hasButtonLeft && childrenInfo.hasButtonRight) {
836
+ variant = 'with-buttons-both';
837
+ } else if (childrenInfo.hasButtonLeft) {
838
+ variant = 'with-button-left';
839
+ } else if (childrenInfo.hasButtonRight) {
840
+ variant = 'with-button-right';
841
+ } else if (childrenInfo.hasIconLeft) {
842
+ variant = 'with-icon-before';
843
+ } else if (childrenInfo.hasIconRight) {
844
+ variant = 'with-icon-after';
845
+ } else if (childrenInfo.hasTextLeft) {
846
+ variant = 'with-text-prefix';
847
+ } else if (childrenInfo.hasTextRight) {
848
+ variant = 'with-text-suffix';
849
+ } else {
850
+ variant = baseVariant;
851
+ }
852
+ } else if (componentName.toLowerCase().includes('list') &&
853
+ !componentName.toLowerCase().includes('listitem') &&
854
+ !componentName.toLowerCase().includes('datalist') &&
855
+ !componentName.toLowerCase().includes('actionlist') &&
856
+ !componentName.toLowerCase().includes('duallistselector') &&
857
+ path) {
858
+ const childrenInfo = analyzeListChildren(j, path);
859
+ const baseVariant = inferVariant(componentName, props);
860
+
861
+ // Add icon variants if detected
862
+ const variantParts = baseVariant ? baseVariant.split('-') : [];
863
+ if (childrenInfo.hasIcons) {
864
+ if (childrenInfo.hasBigIcons && !variantParts.includes('big-icons')) {
865
+ variantParts.push('big-icons');
866
+ } else if (childrenInfo.hasSmallIcons && !variantParts.includes('small-icons')) {
867
+ variantParts.push('small-icons');
868
+ } else if (!variantParts.includes('with-icons')) {
869
+ variantParts.push('with-icons');
870
+ }
871
+ }
872
+
873
+ variant = variantParts.length > 0 ? variantParts.join('-') : baseVariant;
874
+ } else if (componentName.toLowerCase().includes('loginform') && path) {
875
+ const childrenInfo = analyzeLoginFormChildren(j, path);
876
+ const baseVariant = inferVariant(componentName, props);
877
+
878
+ // Determine variant based on children analysis
879
+ if (childrenInfo.hasCustomizedHeaderUtilities) {
880
+ variant = 'customized-header-utilities';
881
+ } else if (childrenInfo.hasShowHidePassword) {
882
+ variant = 'show-hide-password';
883
+ } else if (baseVariant) {
884
+ variant = baseVariant;
885
+ } else {
886
+ // Default to basic
887
+ variant = 'basic';
888
+ }
889
+ } else if (componentName.toLowerCase().includes('clipboardcopy') && path) {
890
+ const childrenInfo = analyzeClipboardCopyChildren(j, path);
891
+ const baseVariant = inferVariant(componentName, props);
892
+ // Add content type variants if detected via children analysis
893
+ const variantParts = baseVariant ? baseVariant.split('-') : [];
894
+ if (childrenInfo.hasArray) {
895
+ // Only add if expandable (expanded with array)
896
+ if (variantParts.includes('expandable')) {
897
+ variantParts.push('with-array');
898
+ }
899
+ }
900
+ if (childrenInfo.hasJsonObject && !variantParts.includes('json-object')) {
901
+ variantParts.push('json-object');
902
+ }
903
+ variant = variantParts.length > 0 ? variantParts.join('-') : baseVariant;
904
+ } else if (componentName.toLowerCase().includes('duallistselector') &&
905
+ !componentName.toLowerCase().includes('duallistselectorpane') &&
906
+ !componentName.toLowerCase().includes('duallistselectorlist') &&
907
+ !componentName.toLowerCase().includes('duallistselectorlistitem') &&
908
+ path) {
909
+ const childrenInfo = analyzeDualListSelectorChildren(j, path);
910
+ const baseVariant = inferVariant(componentName, props);
911
+ // Add sub-variants if detected via children analysis
912
+ const variantParts = baseVariant ? baseVariant.split('-') : ['basic'];
913
+ if (childrenInfo.hasTooltips && !variantParts.includes('with-tooltips')) {
914
+ variantParts.push('with-tooltips');
915
+ }
916
+ if (childrenInfo.hasSearch && !variantParts.includes('with-search')) {
917
+ variantParts.push('with-search');
918
+ }
919
+ if (childrenInfo.hasActions && !variantParts.includes('with-actions')) {
920
+ variantParts.push('with-actions');
921
+ }
922
+ if (childrenInfo.hasMultipleDropZones && variantParts.includes('draggable') &&
923
+ !variantParts.includes('multiple-drop-zones')) {
924
+ variantParts.push('multiple-drop-zones');
925
+ }
926
+ variant = variantParts.join('-');
927
+ } else {
928
+ variant = inferVariant(componentName, props);
929
+ }
930
+
931
+ const context = inferContext(componentName, props, parentContextValue, parentPurpose);
932
+
933
+ // For form inputs, check if HelperText state should be incorporated into the input's state
934
+ let state = inferState(componentName, props);
935
+ if ((componentName.toLowerCase().includes('textinput') ||
936
+ componentName.toLowerCase().includes('textarea') ||
937
+ componentName.toLowerCase().includes('select') ||
938
+ componentName.toLowerCase().includes('checkbox') ||
939
+ componentName.toLowerCase().includes('radio') ||
940
+ componentName.toLowerCase().includes('switch')) &&
941
+ path) {
942
+ const helperTextInfo = checkForHelperText(j, path);
943
+ // HelperText state (error, warning, success) should be reflected in the input's state
944
+ if (helperTextInfo.hasHelperText && helperTextInfo.helperTextState) {
945
+ // Only add meaningful states (not indeterminate)
946
+ if (helperTextInfo.helperTextState !== 'indeterminate') {
947
+ // Combine states: if input already has a state, combine them; otherwise use helper text state
948
+ if (state) {
949
+ // Combine existing state with helper text state (e.g., "disabled error")
950
+ state = `${state} ${helperTextInfo.helperTextState}`;
951
+ } else {
952
+ state = helperTextInfo.helperTextState;
953
+ }
954
+ }
955
+ }
956
+ }
957
+
958
+ const actionType = inferActionType(componentName, props);
959
+ const size = inferSize(componentName, props);
960
+
961
+ const attributes = [];
962
+
963
+ // Always add purpose (it always has a value)
964
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-purpose'), j.literal(purpose)));
965
+
966
+ // Only add role if we inferred a meaningful value (some structural children don't get roles)
967
+ if (role !== null) {
968
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-role'), j.literal(role)));
969
+ }
970
+
971
+ // Only add variant, context, state, action-type, and size if we inferred meaningful values
972
+ if (variant !== null) {
973
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-variant'), j.literal(variant)));
974
+ }
975
+
976
+ if (context !== null) {
977
+ // Render "in-page" instead of "page" for better clarity in HTML
978
+ const contextValue = context === 'page' ? 'in-page' : context;
979
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-context'), j.literal(contextValue)));
980
+ }
981
+
982
+ if (state !== null) {
983
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-state'), j.literal(state)));
984
+ }
985
+
986
+ if (actionType !== null) {
987
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-action-type'), j.literal(actionType)));
988
+ }
989
+
990
+ if (size !== null) {
991
+ attributes.push(j.jsxAttribute(j.jsxIdentifier('data-size'), j.literal(size)));
992
+ }
993
+
994
+ return attributes;
995
+ }
996
+
997
+ /**
998
+ * Main transform function
999
+ */
1000
+ module.exports = function transformer(fileInfo, api) {
1001
+ const j = api.jscodeshift;
1002
+ const root = j(fileInfo.source);
1003
+
1004
+ // Track imports to identify PatternFly components
1005
+ const imports = [];
1006
+ root.find(j.ImportDeclaration).forEach(path => {
1007
+ imports.push(path.node);
1008
+ });
1009
+
1010
+ // Process all JSX opening elements (handles both self-closing and regular elements)
1011
+ root.find(j.JSXOpeningElement).forEach(path => {
1012
+ const node = path.node;
1013
+ const componentName = node.name?.name;
1014
+
1015
+ // Skip if not a valid component name
1016
+ if (!componentName || typeof componentName !== 'string') {
1017
+ return;
1018
+ }
1019
+
1020
+ // Check if this is a PatternFly component
1021
+ if (!isPatternFlyComponent(componentName, imports)) {
1022
+ return;
1023
+ }
1024
+
1025
+ // Skip structural child components - they don't need separate semantic attributes
1026
+ // The parent component has the attributes that describe the whole structure
1027
+ // This applies to components that are always used as children of a parent component
1028
+ const name = componentName.toLowerCase();
1029
+
1030
+ // Structural children to skip:
1031
+ // - Breadcrumb: BreadcrumbItem, BreadcrumbHeading
1032
+ // - Accordion: AccordionItem, AccordionContent, AccordionToggle
1033
+ // - Card: CardBody, CardHeader, CardTitle
1034
+ // - ActionList: ActionListItem
1035
+ // - Modal: ModalContent, ModalHeader
1036
+ // - DataList: DataListAction (only this one is skipped - others get full attributes)
1037
+ // - DescriptionList: None skipped - all children get attributes (they have meaningful variants/states)
1038
+ // - Form: FormGroup, FormSection (if they exist)
1039
+ const structuralChildren = [
1040
+ 'breadcrumbitem', 'breadcrumbheading',
1041
+ 'accordionitem', 'accordioncontent', 'accordiontoggle',
1042
+ 'cardbody', 'cardheader', 'cardtitle',
1043
+ 'actionlistitem',
1044
+ 'modalcontent', 'modalheader', 'modalfooter', 'modalbody',
1045
+ // Modal structural children - ModalContent, ModalHeader, ModalFooter, ModalBody are structural
1046
+ 'datalistaction', // Only DataListAction is skipped, other DataList children get attributes
1047
+ // DescriptionList children are NOT skipped - they have meaningful variants/states
1048
+ // Droppable and Draggable are NOT skipped - they get all attributes except role (handled by parent)
1049
+ 'drawermain', 'drawerpanel', 'drawercontent', 'drawerbody',
1050
+ 'drawerhead', 'draweractions', 'drawersection', 'drawersectiongroup',
1051
+ // Drawer structural children - role and purpose handled by parent Drawer
1052
+ 'duallistselectorlist', // DualListSelectorList is purely structural - skipped
1053
+ // DualListSelectorPane and DualListSelectorListItem get attributes (they have meaningful variants/states)
1054
+ 'emptystateheader', 'emptystateicon', 'emptystatebody',
1055
+ 'emptystatefooter', 'emptystateactions',
1056
+ // EmptyState structural children - role and purpose handled by parent EmptyState
1057
+ // Structural children: EmptyStateHeader, EmptyStateIcon, EmptyStateBody, EmptyStateFooter, EmptyStateActions
1058
+ // Note: Nested components like Button and Spinner are NOT skipped - they get their own attributes
1059
+ // (they are independent components used within EmptyState, not structural children)
1060
+ 'expandablesectiontoggle', 'expandablesectioncontent',
1061
+ // ExpandableSection structural children - role and purpose handled by parent ExpandableSection
1062
+ // Structural children: ExpandableSectionToggle, ExpandableSectionContent
1063
+ 'helpertext', 'helper-text',
1064
+ // HelperText is a structural child of form inputs - its state is reflected in the input's variant/state
1065
+ 'mastheadbrand',
1066
+ // MastheadBrand is a structural wrapper for Brand component - skipped
1067
+ 'menulist', 'menugroup', 'menusearch', 'menusearchinput',
1068
+ // Menu structural children - MenuList, MenuGroup, MenuSearch, MenuSearchInput are structural
1069
+ 'selectoptiongroup',
1070
+ // SelectOptionGroup is a structural wrapper for grouping select options
1071
+ 'notificationdrawerheader', 'notificationdrawerbody',
1072
+ // NotificationDrawer structural children - NotificationDrawerHeader, NotificationDrawerBody are structural
1073
+ 'overflowmenucontent',
1074
+ // OverflowMenuContent is a structural wrapper for menu items
1075
+ // Note: PageSection, PageHeader, PageBody, PageFooter are NOT structural - they get their own attributes
1076
+ 'wizardnav', 'wizardbody', 'wizardfooter',
1077
+ // Wizard structural children - WizardNav, WizardBody, WizardFooter are structural
1078
+ // WizardNavItem is NOT structural - it gets its own attributes (visited, current, disabled states)
1079
+ ];
1080
+
1081
+ if (structuralChildren.some(child => name.includes(child))) {
1082
+ return;
1083
+ }
1084
+
1085
+ // Note: Independent components (Button, Link, etc.) keep their attributes
1086
+ // even when nested, because they can be used independently
1087
+
1088
+ // Skip if already has semantic attributes (don't duplicate)
1089
+ const existingAttrs = node.attributes || [];
1090
+ if (hasSemanticAttributes(existingAttrs)) {
1091
+ return;
1092
+ }
1093
+
1094
+ // Find parent context for nested components
1095
+ const parentContext = findParentContext(path, imports);
1096
+
1097
+ // Create and add semantic attributes
1098
+ const newAttributes = createSemanticAttributes(j, componentName, existingAttrs, parentContext, path);
1099
+ node.attributes = [...existingAttrs, ...newAttributes];
1100
+ });
1101
+
1102
+ return root.toSource({
1103
+ quote: 'single',
1104
+ trailingComma: true,
1105
+ lineTerminator: '\n',
1106
+ });
1107
+ };
1108
+