@machinemetrics/mm-react-components 0.1.1-1 → 0.2.3-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 (222) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +246 -0
  3. package/dist/App.d.ts +3 -0
  4. package/dist/App.d.ts.map +1 -0
  5. package/dist/components/ui/accordion.d.ts +10 -0
  6. package/dist/components/ui/accordion.d.ts.map +1 -0
  7. package/dist/components/ui/badge.d.ts +10 -0
  8. package/dist/components/ui/badge.d.ts.map +1 -0
  9. package/dist/components/ui/button.d.ts +11 -0
  10. package/dist/components/ui/button.d.ts.map +1 -0
  11. package/dist/components/ui/calendar.d.ts +9 -0
  12. package/dist/components/ui/calendar.d.ts.map +1 -0
  13. package/dist/components/ui/checkbox.d.ts +5 -0
  14. package/dist/components/ui/checkbox.d.ts.map +1 -0
  15. package/dist/components/ui/collapsible.d.ts +7 -0
  16. package/dist/components/ui/collapsible.d.ts.map +1 -0
  17. package/dist/components/ui/data-table/TableView.d.ts +3 -0
  18. package/dist/components/ui/data-table/TableView.d.ts.map +1 -0
  19. package/dist/components/ui/data-table/cards/ResponsiveTable.d.ts +10 -0
  20. package/dist/components/ui/data-table/cards/ResponsiveTable.d.ts.map +1 -0
  21. package/dist/components/ui/data-table/cards/RowCard.d.ts +9 -0
  22. package/dist/components/ui/data-table/cards/RowCard.d.ts.map +1 -0
  23. package/dist/components/ui/data-table/cards/index.d.ts +2 -0
  24. package/dist/components/ui/data-table/cards/index.d.ts.map +1 -0
  25. package/dist/components/ui/data-table/columnTypes/badgeColumn.d.ts +16 -0
  26. package/dist/components/ui/data-table/columnTypes/badgeColumn.d.ts.map +1 -0
  27. package/dist/components/ui/data-table/columnTypes/createColumn.d.ts +8 -0
  28. package/dist/components/ui/data-table/columnTypes/createColumn.d.ts.map +1 -0
  29. package/dist/components/ui/data-table/columnTypes/dateColumn.d.ts +13 -0
  30. package/dist/components/ui/data-table/columnTypes/dateColumn.d.ts.map +1 -0
  31. package/dist/components/ui/data-table/columnTypes/index.d.ts +8 -0
  32. package/dist/components/ui/data-table/columnTypes/index.d.ts.map +1 -0
  33. package/dist/components/ui/data-table/columnTypes/multiBadgeColumn.d.ts +16 -0
  34. package/dist/components/ui/data-table/columnTypes/multiBadgeColumn.d.ts.map +1 -0
  35. package/dist/components/ui/data-table/columnTypes/numericColumn.d.ts +15 -0
  36. package/dist/components/ui/data-table/columnTypes/numericColumn.d.ts.map +1 -0
  37. package/dist/components/ui/data-table/columnTypes/selectionColumn.d.ts +10 -0
  38. package/dist/components/ui/data-table/columnTypes/selectionColumn.d.ts.map +1 -0
  39. package/dist/components/ui/data-table/columnTypes/textColumn.d.ts +13 -0
  40. package/dist/components/ui/data-table/columnTypes/textColumn.d.ts.map +1 -0
  41. package/dist/components/ui/data-table/index.d.ts +14 -0
  42. package/dist/components/ui/data-table/index.d.ts.map +1 -0
  43. package/dist/components/ui/data-table/metadata/ColumnRegistry.d.ts +90 -0
  44. package/dist/components/ui/data-table/metadata/ColumnRegistry.d.ts.map +1 -0
  45. package/dist/components/ui/data-table/metadata/alignment.d.ts +8 -0
  46. package/dist/components/ui/data-table/metadata/alignment.d.ts.map +1 -0
  47. package/dist/components/ui/data-table/pagination.d.ts +9 -0
  48. package/dist/components/ui/data-table/pagination.d.ts.map +1 -0
  49. package/dist/components/ui/data-table/state/index.d.ts +3 -0
  50. package/dist/components/ui/data-table/state/index.d.ts.map +1 -0
  51. package/dist/components/ui/data-table/state/useBreakpoint.d.ts +2 -0
  52. package/dist/components/ui/data-table/state/useBreakpoint.d.ts.map +1 -0
  53. package/dist/components/ui/data-table/state/useDataTableState.d.ts +19 -0
  54. package/dist/components/ui/data-table/state/useDataTableState.d.ts.map +1 -0
  55. package/dist/components/ui/data-table/tokens.d.ts +10 -0
  56. package/dist/components/ui/data-table/tokens.d.ts.map +1 -0
  57. package/dist/components/ui/data-table/toolbar/ColumnVisibilityMenu.d.ts +8 -0
  58. package/dist/components/ui/data-table/toolbar/ColumnVisibilityMenu.d.ts.map +1 -0
  59. package/dist/components/ui/data-table/toolbar/DataTableToolbar.d.ts +15 -0
  60. package/dist/components/ui/data-table/toolbar/DataTableToolbar.d.ts.map +1 -0
  61. package/dist/components/ui/data-table/toolbar/MenuHeader.d.ts +8 -0
  62. package/dist/components/ui/data-table/toolbar/MenuHeader.d.ts.map +1 -0
  63. package/dist/components/ui/data-table/toolbar/SortMenu.d.ts +7 -0
  64. package/dist/components/ui/data-table/toolbar/SortMenu.d.ts.map +1 -0
  65. package/dist/components/ui/data-table/toolbar/filters/DateRangeFilter.d.ts +5 -0
  66. package/dist/components/ui/data-table/toolbar/filters/DateRangeFilter.d.ts.map +1 -0
  67. package/dist/components/ui/data-table/toolbar/filters/FilterMenu.d.ts +10 -0
  68. package/dist/components/ui/data-table/toolbar/filters/FilterMenu.d.ts.map +1 -0
  69. package/dist/components/ui/data-table/toolbar/filters/InlineDateRangePicker.d.ts +10 -0
  70. package/dist/components/ui/data-table/toolbar/filters/InlineDateRangePicker.d.ts.map +1 -0
  71. package/dist/components/ui/data-table/toolbar/filters/NumericRangeFilter.d.ts +5 -0
  72. package/dist/components/ui/data-table/toolbar/filters/NumericRangeFilter.d.ts.map +1 -0
  73. package/dist/components/ui/data-table/toolbar/filters/SelectFilter.d.ts +6 -0
  74. package/dist/components/ui/data-table/toolbar/filters/SelectFilter.d.ts.map +1 -0
  75. package/dist/components/ui/data-table/toolbar/filters/TextFilter.d.ts +5 -0
  76. package/dist/components/ui/data-table/toolbar/filters/TextFilter.d.ts.map +1 -0
  77. package/dist/components/ui/data-table/toolbar/filters/index.d.ts +9 -0
  78. package/dist/components/ui/data-table/toolbar/filters/index.d.ts.map +1 -0
  79. package/dist/components/ui/data-table/toolbar/filters/types.d.ts +26 -0
  80. package/dist/components/ui/data-table/toolbar/filters/types.d.ts.map +1 -0
  81. package/dist/components/ui/data-table/toolbar/filters/useColumnFilters.d.ts +5 -0
  82. package/dist/components/ui/data-table/toolbar/filters/useColumnFilters.d.ts.map +1 -0
  83. package/dist/components/ui/data-table/toolbar/index.d.ts +5 -0
  84. package/dist/components/ui/data-table/toolbar/index.d.ts.map +1 -0
  85. package/dist/components/ui/data-table/types.d.ts +40 -0
  86. package/dist/components/ui/data-table/types.d.ts.map +1 -0
  87. package/dist/components/ui/data-table/useTableController.d.ts +29 -0
  88. package/dist/components/ui/data-table/useTableController.d.ts.map +1 -0
  89. package/dist/components/ui/data-table/utils.d.ts +3 -0
  90. package/dist/components/ui/data-table/utils.d.ts.map +1 -0
  91. package/dist/components/ui/date-range-picker.d.ts +9 -0
  92. package/dist/components/ui/date-range-picker.d.ts.map +1 -0
  93. package/dist/components/ui/dialog.d.ts +16 -0
  94. package/dist/components/ui/dialog.d.ts.map +1 -0
  95. package/dist/components/ui/drawer.d.ts +16 -0
  96. package/dist/components/ui/drawer.d.ts.map +1 -0
  97. package/dist/components/ui/dropdown-menu.d.ts +28 -0
  98. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  99. package/dist/components/ui/dropzone/index.d.ts +25 -0
  100. package/dist/components/ui/dropzone/index.d.ts.map +1 -0
  101. package/dist/components/ui/input.d.ts +5 -0
  102. package/dist/components/ui/input.d.ts.map +1 -0
  103. package/dist/components/ui/label.d.ts +5 -0
  104. package/dist/components/ui/label.d.ts.map +1 -0
  105. package/dist/components/ui/page-header/index.d.ts +3 -0
  106. package/dist/components/ui/page-header/index.d.ts.map +1 -0
  107. package/dist/components/ui/page-header/page-header-types.d.ts +44 -0
  108. package/dist/components/ui/page-header/page-header-types.d.ts.map +1 -0
  109. package/dist/components/ui/page-header/page-header.d.ts +5 -0
  110. package/dist/components/ui/page-header/page-header.d.ts.map +1 -0
  111. package/dist/components/ui/popover.d.ts +8 -0
  112. package/dist/components/ui/popover.d.ts.map +1 -0
  113. package/dist/components/ui/progress.d.ts +5 -0
  114. package/dist/components/ui/progress.d.ts.map +1 -0
  115. package/dist/components/ui/radio-group.d.ts +6 -0
  116. package/dist/components/ui/radio-group.d.ts.map +1 -0
  117. package/dist/components/ui/search-input.d.ts +7 -0
  118. package/dist/components/ui/search-input.d.ts.map +1 -0
  119. package/dist/components/ui/select.d.ts +16 -0
  120. package/dist/components/ui/select.d.ts.map +1 -0
  121. package/dist/components/ui/sheet.d.ts +14 -0
  122. package/dist/components/ui/sheet.d.ts.map +1 -0
  123. package/dist/components/ui/skeleton.d.ts +4 -0
  124. package/dist/components/ui/skeleton.d.ts.map +1 -0
  125. package/dist/components/ui/slider.d.ts +15 -0
  126. package/dist/components/ui/slider.d.ts.map +1 -0
  127. package/dist/components/ui/switch.d.ts +5 -0
  128. package/dist/components/ui/switch.d.ts.map +1 -0
  129. package/dist/components/ui/table/Table.d.ts +21 -0
  130. package/dist/components/ui/table/Table.d.ts.map +1 -0
  131. package/dist/components/ui/table/index.d.ts +2 -0
  132. package/dist/components/ui/table/index.d.ts.map +1 -0
  133. package/dist/components/ui/table.d.ts +11 -0
  134. package/dist/components/ui/table.d.ts.map +1 -0
  135. package/dist/components/ui/tabs.d.ts +15 -0
  136. package/dist/components/ui/tabs.d.ts.map +1 -0
  137. package/dist/components/ui/toggle.d.ts +10 -0
  138. package/dist/components/ui/toggle.d.ts.map +1 -0
  139. package/dist/components/ui/tooltip.d.ts +8 -0
  140. package/dist/components/ui/tooltip.d.ts.map +1 -0
  141. package/dist/docs/TAILWIND_SETUP.md +332 -0
  142. package/dist/index.css +536 -0
  143. package/dist/index.d.ts +30 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/lib/page-header-utils.d.ts +2 -0
  146. package/dist/lib/page-header-utils.d.ts.map +1 -0
  147. package/dist/lib/theme-utils.d.ts +104 -0
  148. package/dist/lib/theme-utils.d.ts.map +1 -0
  149. package/dist/lib/utils.d.ts +3 -0
  150. package/dist/lib/utils.d.ts.map +1 -0
  151. package/dist/main.d.ts +3 -0
  152. package/dist/main.d.ts.map +1 -0
  153. package/dist/mm-react-components.es.js +33709 -26
  154. package/dist/mm-react-components.es.js.map +1 -0
  155. package/dist/mm-react-components.umd.js +71 -1
  156. package/dist/mm-react-components.umd.js.map +1 -0
  157. package/dist/preview/AccordionPreview.d.ts +2 -0
  158. package/dist/preview/AccordionPreview.d.ts.map +1 -0
  159. package/dist/preview/BadgePreview.d.ts +2 -0
  160. package/dist/preview/BadgePreview.d.ts.map +1 -0
  161. package/dist/preview/ButtonPreview.d.ts +2 -0
  162. package/dist/preview/ButtonPreview.d.ts.map +1 -0
  163. package/dist/preview/CalendarPreview.d.ts +2 -0
  164. package/dist/preview/CalendarPreview.d.ts.map +1 -0
  165. package/dist/preview/CheckboxPreview.d.ts +2 -0
  166. package/dist/preview/CheckboxPreview.d.ts.map +1 -0
  167. package/dist/preview/CollapsiblePreview.d.ts +2 -0
  168. package/dist/preview/CollapsiblePreview.d.ts.map +1 -0
  169. package/dist/preview/DataTablePreview.d.ts +9 -0
  170. package/dist/preview/DataTablePreview.d.ts.map +1 -0
  171. package/dist/preview/DateRangePickerPreview.d.ts +2 -0
  172. package/dist/preview/DateRangePickerPreview.d.ts.map +1 -0
  173. package/dist/preview/DialogPreview.d.ts +2 -0
  174. package/dist/preview/DialogPreview.d.ts.map +1 -0
  175. package/dist/preview/DrawerPreview.d.ts +2 -0
  176. package/dist/preview/DrawerPreview.d.ts.map +1 -0
  177. package/dist/preview/DropdownMenuPreview.d.ts +2 -0
  178. package/dist/preview/DropdownMenuPreview.d.ts.map +1 -0
  179. package/dist/preview/DropzonePreview.d.ts +2 -0
  180. package/dist/preview/DropzonePreview.d.ts.map +1 -0
  181. package/dist/preview/InputPreview.d.ts +2 -0
  182. package/dist/preview/InputPreview.d.ts.map +1 -0
  183. package/dist/preview/LabelPreview.d.ts +2 -0
  184. package/dist/preview/LabelPreview.d.ts.map +1 -0
  185. package/dist/preview/PopoverPreview.d.ts +2 -0
  186. package/dist/preview/PopoverPreview.d.ts.map +1 -0
  187. package/dist/preview/ProgressPreview.d.ts +2 -0
  188. package/dist/preview/ProgressPreview.d.ts.map +1 -0
  189. package/dist/preview/RadioGroupPreview.d.ts +2 -0
  190. package/dist/preview/RadioGroupPreview.d.ts.map +1 -0
  191. package/dist/preview/SelectPreview.d.ts +2 -0
  192. package/dist/preview/SelectPreview.d.ts.map +1 -0
  193. package/dist/preview/SheetPreview.d.ts +2 -0
  194. package/dist/preview/SheetPreview.d.ts.map +1 -0
  195. package/dist/preview/SkeletonPreview.d.ts +2 -0
  196. package/dist/preview/SkeletonPreview.d.ts.map +1 -0
  197. package/dist/preview/SliderPreview.d.ts +2 -0
  198. package/dist/preview/SliderPreview.d.ts.map +1 -0
  199. package/dist/preview/SwitchPreview.d.ts +2 -0
  200. package/dist/preview/SwitchPreview.d.ts.map +1 -0
  201. package/dist/preview/TablePreview.d.ts +2 -0
  202. package/dist/preview/TablePreview.d.ts.map +1 -0
  203. package/dist/preview/TabsPreview.d.ts +2 -0
  204. package/dist/preview/TabsPreview.d.ts.map +1 -0
  205. package/dist/preview/TogglePreview.d.ts +2 -0
  206. package/dist/preview/TogglePreview.d.ts.map +1 -0
  207. package/dist/preview/TooltipPreview.d.ts +2 -0
  208. package/dist/preview/TooltipPreview.d.ts.map +1 -0
  209. package/dist/preview/data-table/data-table-preview_column-content.d.ts +18 -0
  210. package/dist/preview/data-table/data-table-preview_column-content.d.ts.map +1 -0
  211. package/dist/preview/page-header/PageHeaderPreview.d.ts +3 -0
  212. package/dist/preview/page-header/PageHeaderPreview.d.ts.map +1 -0
  213. package/dist/tailwind.config.export.js +153 -0
  214. package/dist/themes/carbide.css +1257 -0
  215. package/docs/TAILWIND_SETUP.md +332 -0
  216. package/package.json +119 -15
  217. package/scripts/README.md +171 -0
  218. package/scripts/chakra-to-shadcn-migrator/README.md +107 -0
  219. package/scripts/chakra-to-shadcn-migrator/bin/chakra-to-shadcn.js +1135 -0
  220. package/src/index.css +536 -0
  221. package/src/themes/carbide.css +1257 -0
  222. package/tailwind.config.export.js +153 -0
@@ -0,0 +1,1135 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import process from 'process';
5
+ import { bold, colorize } from '../lib/colors.js';
6
+ import { parseArgs } from '../lib/args.js';
7
+ import { resolveConfig, loadJson } from '../lib/config.js';
8
+ import { updateDependencies, renderDependencyReport } from '../lib/deps.js';
9
+ import { collectSourceFiles } from '../lib/file-io.js';
10
+ import {
11
+ renderImportReport,
12
+ renderProviderInstructions,
13
+ renderPostSteps,
14
+ } from '../lib/render.js';
15
+
16
+ const opts = parseArgs(process.argv.slice(2));
17
+ const projectRoot = path.resolve(process.cwd(), opts.project);
18
+ const configPath = resolveConfig(projectRoot, opts.config);
19
+ const config = configPath ? loadJson(configPath) : undefined;
20
+
21
+ if (!config) {
22
+ console.error(
23
+ colorize(
24
+ 'Unable to locate chakra-to-shadcn.config.json. Provide one with --config.',
25
+ 'red',
26
+ ),
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const moduleNames = Object.keys(config.moduleMappings ?? {});
32
+
33
+ console.log(bold(colorize('\nChakra UI → Shadcn migration helper', 'cyan')));
34
+ console.log(`Project: ${colorize(projectRoot, 'green')}`);
35
+ console.log(`Configuration: ${colorize(configPath, 'green')}`);
36
+ console.log(
37
+ `Mode: ${opts.apply ? colorize('apply changes', 'green') : colorize('dry-run / plan', 'yellow')}`,
38
+ );
39
+
40
+ const dependencyReport = updateDependencies(projectRoot, config, opts.apply);
41
+ renderDependencyReport(dependencyReport, opts.apply, { bold, colorize });
42
+ renderProviderInstructions(config, { bold });
43
+
44
+ if (moduleNames.length > 0) {
45
+ const sourceFiles = collectSourceFiles(
46
+ projectRoot,
47
+ opts.source,
48
+ opts.includeTests,
49
+ );
50
+ const importReports = analyzeImports(
51
+ sourceFiles,
52
+ moduleNames,
53
+ config,
54
+ opts.apply,
55
+ );
56
+ renderImportReport(importReports, opts.apply, config, { colorize });
57
+ } else {
58
+ console.log('\nNo module mappings configured.');
59
+ }
60
+
61
+ renderPostSteps(config, { bold });
62
+
63
+ function analyzeImports(files, moduleNames, config, applyMode) {
64
+ console.log(bold('\nStep 3 – Replace Chakra imports'));
65
+ const reports = [];
66
+ for (const file of files) {
67
+ const text = fs.readFileSync(file.absolute, 'utf8');
68
+ let mutated = text;
69
+ let dirty = false;
70
+ const fileReport = { file: file.relative, modules: [], applied: false };
71
+ const usedSpecifiersByModule = new Map();
72
+ for (const moduleName of moduleNames) {
73
+ const moduleConfig = config.moduleMappings[moduleName];
74
+ if (!moduleConfig) continue;
75
+ const matches = findImports(mutated, moduleName);
76
+ for (const match of matches.reverse()) {
77
+ const result = transformImport(match, moduleConfig, applyMode);
78
+ fileReport.modules.push({ module: moduleName, ...result });
79
+ // Track specifiers used so we can target JSX replacement to only those imported
80
+ const used = usedSpecifiersByModule.get(moduleName) ?? new Set();
81
+ for (const s of match.specifiers) {
82
+ if (s?.local) used.add(s.local);
83
+ else if (s?.imported) used.add(s.imported);
84
+ }
85
+ usedSpecifiersByModule.set(moduleName, used);
86
+ if (applyMode && result && result.replacement) {
87
+ mutated =
88
+ mutated.slice(0, match.start) +
89
+ result.replacement +
90
+ mutated.slice(match.end);
91
+ dirty = true;
92
+ }
93
+ }
94
+ // JSX tag replacement based on config.jsxSpecifiers
95
+ const jsxSpec = moduleConfig.jsxSpecifiers;
96
+ if (jsxSpec) {
97
+ const used = usedSpecifiersByModule.get(moduleName) ?? new Set();
98
+ const jsxResult = transformJSX(mutated, jsxSpec, used, applyMode);
99
+ if (jsxResult && jsxResult.changed) {
100
+ if (applyMode && jsxResult.output !== mutated) {
101
+ mutated = jsxResult.output;
102
+ dirty = true;
103
+ }
104
+ fileReport.modules.push({
105
+ module: moduleName,
106
+ jsxReplaced: Array.from(jsxResult.replaced ?? []),
107
+ });
108
+ }
109
+ }
110
+
111
+ // Transform compound components (e.g., Dialog.Root, Tabs.List)
112
+ const compoundSpec = moduleConfig.compoundComponents;
113
+ if (compoundSpec && applyMode) {
114
+ const compoundResult = transformCompoundComponents(
115
+ mutated,
116
+ compoundSpec,
117
+ );
118
+ if (compoundResult.changed) {
119
+ mutated = compoundResult.output;
120
+ dirty = true;
121
+ if (compoundResult.transformed.length > 0) {
122
+ fileReport.modules.push({
123
+ module: moduleName,
124
+ compoundTransformed: compoundResult.transformed,
125
+ });
126
+
127
+ // Add imports for new compound components
128
+ const newImports = compoundResult.newComponents;
129
+ if (newImports.length > 0) {
130
+ // Find existing imports from mm-react-components
131
+ const mmImportPattern =
132
+ /import\s+\{([^}]*)\}\s+from\s+['"]@machinemetrics\/mm-react-components['"];?/;
133
+ const match = mutated.match(mmImportPattern);
134
+
135
+ if (match) {
136
+ // Add to existing import and deduplicate
137
+ const existingImports = match[1].trim();
138
+ const existingList = existingImports
139
+ .split(',')
140
+ .map((s) => s.trim())
141
+ .filter(Boolean);
142
+ const allImports = [
143
+ ...new Set([...existingList, ...newImports]),
144
+ ];
145
+ mutated = mutated.replace(
146
+ mmImportPattern,
147
+ `import { ${allImports.join(', ')} } from '@machinemetrics/mm-react-components';`,
148
+ );
149
+ } else {
150
+ // Add new import at top of file (after existing imports)
151
+ const firstImportEnd = mutated.indexOf(';') + 1;
152
+ const newImport = `\nimport { ${newImports.join(', ')} } from '@machinemetrics/mm-react-components';`;
153
+ mutated =
154
+ mutated.slice(0, firstImportEnd) +
155
+ newImport +
156
+ mutated.slice(firstImportEnd);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // Cleanup unused Chakra imports
165
+ if (applyMode && dirty) {
166
+ const cleanupResult = cleanupUnusedImports(mutated, moduleNames);
167
+ if (cleanupResult.changed) {
168
+ mutated = cleanupResult.output;
169
+ fileReport.importsRemoved = cleanupResult.removed;
170
+ }
171
+ }
172
+
173
+ if (applyMode && dirty && mutated !== text) {
174
+ fs.writeFileSync(file.absolute, mutated, 'utf8');
175
+ fileReport.applied = true;
176
+ }
177
+ if (fileReport.modules.length) {
178
+ reports.push(fileReport);
179
+ }
180
+ }
181
+ return reports;
182
+ }
183
+
184
+ // Clean up unused Chakra imports after transformation
185
+ function cleanupUnusedImports(content, moduleNames) {
186
+ let output = content;
187
+ let changed = false;
188
+ const removed = [];
189
+
190
+ for (const moduleName of moduleNames) {
191
+ const imports = findImports(output, moduleName);
192
+
193
+ for (const importMatch of imports.reverse()) {
194
+ const unusedSpecifiers = [];
195
+ const usedSpecifiers = [];
196
+
197
+ for (const spec of importMatch.specifiers) {
198
+ const name = spec.local || spec.imported;
199
+ // Check if this import is actually used in the code (outside of imports)
200
+ // Look for the name as a JSX tag or in code
201
+ const codeWithoutImports = output.replace(
202
+ /import\s+.*?from\s+['"].*?['"];?/gs,
203
+ '',
204
+ );
205
+ const usagePattern = new RegExp(
206
+ `<${escapeRegex(name)}[\\s/>]|\\b${escapeRegex(name)}\\b`,
207
+ 'g',
208
+ );
209
+ const isUsed = usagePattern.test(codeWithoutImports);
210
+
211
+ if (isUsed) {
212
+ usedSpecifiers.push(spec);
213
+ } else {
214
+ unusedSpecifiers.push(spec);
215
+ }
216
+ }
217
+
218
+ if (unusedSpecifiers.length > 0) {
219
+ if (usedSpecifiers.length === 0) {
220
+ // Remove entire import statement
221
+ output =
222
+ output.slice(0, importMatch.start) + output.slice(importMatch.end);
223
+ changed = true;
224
+ removed.push({
225
+ module: moduleName,
226
+ specifiers: unusedSpecifiers.map((s) => s.local || s.imported),
227
+ });
228
+ } else {
229
+ // Keep only used specifiers
230
+ const isTypeOnly = importMatch.raw.includes('import type');
231
+ const specifierList = usedSpecifiers
232
+ .map((s) => {
233
+ const typePrefix = s.isType && !isTypeOnly ? 'type ' : '';
234
+ if (s.local && s.imported && s.local !== s.imported) {
235
+ return `${typePrefix}${s.imported} as ${s.local}`;
236
+ }
237
+ return `${typePrefix}${s.local || s.imported}`;
238
+ })
239
+ .join(', ');
240
+
241
+ const keyword = isTypeOnly ? 'import type' : 'import';
242
+ const newImport = `${keyword} { ${specifierList} } from '${moduleName}';`;
243
+ output =
244
+ output.slice(0, importMatch.start) +
245
+ newImport +
246
+ output.slice(importMatch.end);
247
+ changed = true;
248
+ removed.push({
249
+ module: moduleName,
250
+ specifiers: unusedSpecifiers.map((s) => s.local || s.imported),
251
+ });
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ return { output, changed, removed };
258
+ }
259
+
260
+ function escapeRegex(text) {
261
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
262
+ }
263
+
264
+ function findImports(content, moduleName) {
265
+ const pattern = new RegExp(
266
+ `import\\s+(type\\s+)?\\{([^}]*)\\}\\s+from\\s+['"]${escapeRegex(moduleName)}['"];?`,
267
+ 'gm',
268
+ );
269
+ const imports = [];
270
+ let match;
271
+ while ((match = pattern.exec(content))) {
272
+ const [full, typeKeyword, specifierBlock] = match;
273
+ const start = match.index;
274
+ const end = start + full.length;
275
+ const isTypeOnly = Boolean(typeKeyword);
276
+ const specifiers = parseSpecifiers(specifierBlock, isTypeOnly);
277
+ imports.push({ start, end, specifiers, raw: full });
278
+ }
279
+ return imports;
280
+ }
281
+
282
+ function parseSpecifiers(block, isTypeOnly) {
283
+ return block
284
+ .split(',')
285
+ .map((part) => part.trim())
286
+ .filter(Boolean)
287
+ .map((part) => {
288
+ let spec = part;
289
+ let specIsType = isTypeOnly;
290
+ if (spec.startsWith('type ')) {
291
+ specIsType = true;
292
+ spec = spec.slice(5).trim();
293
+ }
294
+ const [importedRaw, localRaw] = spec.split(/\s+as\s+/i);
295
+ const imported = (importedRaw ?? '').trim();
296
+ const local = (localRaw ?? importedRaw ?? '').trim();
297
+ return { imported, local, isType: specIsType };
298
+ });
299
+ }
300
+
301
+ function transformImport(importInfo, moduleConfig, applyMode) {
302
+ if (!moduleConfig) {
303
+ return { missingConfig: true, specifiers: importInfo.specifiers };
304
+ }
305
+ const convertible = [];
306
+ const manual = [];
307
+ const jsxHandled = new Set(Object.keys(moduleConfig.jsxSpecifiers ?? {}));
308
+
309
+ for (const specifier of importInfo.specifiers) {
310
+ const mapping =
311
+ moduleConfig.specifiers?.[specifier.imported] ||
312
+ moduleConfig.specifiers?.[specifier.local] ||
313
+ moduleConfig.default;
314
+ if (!mapping) {
315
+ // If we have a JSX replacement for this specifier, treat it as handled (to be dropped from imports)
316
+ if (
317
+ jsxHandled.has(specifier.imported) ||
318
+ jsxHandled.has(specifier.local)
319
+ ) {
320
+ continue;
321
+ }
322
+ manual.push(specifier);
323
+ continue;
324
+ }
325
+ convertible.push({ specifier, mapping });
326
+ }
327
+
328
+ if (!applyMode) {
329
+ return { convertible, manual };
330
+ }
331
+
332
+ // Build replacement imports even if some manual specifiers remain; we'll keep the remaining Chakra specifiers
333
+
334
+ const grouped = new Map();
335
+ for (const entry of convertible) {
336
+ const { specifier, mapping } = entry;
337
+ const modulePath = mapping.module ?? moduleConfig.default?.module;
338
+ if (!modulePath) {
339
+ manual.push(specifier);
340
+ continue;
341
+ }
342
+ const importName = mapping.import ?? specifier.imported;
343
+ const localName = mapping.alias ?? specifier.local;
344
+ const isType = mapping.type === true || specifier.isType;
345
+ const key = `${modulePath}|${isType ? 'type' : 'value'}`;
346
+ if (!grouped.has(key)) {
347
+ grouped.set(key, { modulePath, isType, names: [] });
348
+ }
349
+ grouped.get(key).names.push({ importName, localName });
350
+ }
351
+
352
+ // Construct import lines for mapped specifiers
353
+
354
+ const parts = [];
355
+ for (const group of grouped.values()) {
356
+ const names = group.names
357
+ .map(({ importName, localName }) =>
358
+ importName === localName ? importName : `${importName} as ${localName}`,
359
+ )
360
+ .join(', ');
361
+ const keyword = group.isType ? 'import type' : 'import';
362
+ parts.push(`${keyword} { ${names} } from '${group.modulePath}';`);
363
+ }
364
+
365
+ // Keep remaining manual specifiers (that are not JSX-handled) from the original module
366
+ if (manual.length) {
367
+ const remaining = manual.map((s) => s.local || s.imported).filter(Boolean);
368
+ if (remaining.length) {
369
+ parts.unshift(
370
+ `import { ${remaining.join(', ')} } from '${moduleConfig.moduleName || ''}@chakra-ui/react';`,
371
+ );
372
+ }
373
+ }
374
+
375
+ return { convertible, manual, replacement: parts.join(' ') };
376
+ }
377
+
378
+ // renderImportReport and renderPostSteps now come from lib/render.js
379
+
380
+ // --- JSX Transformation Helpers ---
381
+
382
+ // Transform compound components like Dialog.Root, Tabs.List, etc.
383
+ function transformCompoundComponents(input, compoundSpec) {
384
+ if (!input)
385
+ return {
386
+ output: input,
387
+ changed: false,
388
+ transformed: [],
389
+ newComponents: [],
390
+ };
391
+
392
+ let output = input;
393
+ let changed = false;
394
+ const transformed = [];
395
+ const newComponents = new Set();
396
+
397
+ // Process each compound component mapping
398
+ for (const [compoundName, config] of Object.entries(compoundSpec)) {
399
+ const escapedName = escapeRegex(compoundName);
400
+
401
+ if (config.remove) {
402
+ // Remove the component entirely (keep children)
403
+ // Opening tags: <Dialog.Backdrop ...>...</Dialog.Backdrop> → ...
404
+ const pattern = new RegExp(
405
+ `<${escapedName}([^>]*?)>([\\s\\S]*?)<\\/${escapedName}>`,
406
+ 'g',
407
+ );
408
+ const newOutput = output.replace(pattern, (match, attrs, children) => {
409
+ transformed.push(compoundName);
410
+ return children;
411
+ });
412
+ if (newOutput !== output) {
413
+ output = newOutput;
414
+ changed = true;
415
+ }
416
+
417
+ // Self-closing tags: <Dialog.Backdrop ... /> → (remove entirely)
418
+ const selfPattern = new RegExp(`<${escapedName}([^>]*?)\\/>`, 'g');
419
+ const newOutput2 = output.replace(selfPattern, () => {
420
+ transformed.push(compoundName);
421
+ return '';
422
+ });
423
+ if (newOutput2 !== output) {
424
+ output = newOutput2;
425
+ changed = true;
426
+ }
427
+ } else if (config.unwrap) {
428
+ // Unwrap: remove the component tags but keep children
429
+ // <Dialog.Body ...>children</Dialog.Body> → children
430
+ const pattern = new RegExp(
431
+ `<${escapedName}([^>]*?)>([\\s\\S]*?)<\\/${escapedName}>`,
432
+ 'g',
433
+ );
434
+ const newOutput = output.replace(pattern, (match, attrs, children) => {
435
+ transformed.push(compoundName);
436
+ return children;
437
+ });
438
+ if (newOutput !== output) {
439
+ output = newOutput;
440
+ changed = true;
441
+ }
442
+ } else if (config.component) {
443
+ // Replace with new component name
444
+ const newComponent = config.component;
445
+
446
+ // Opening tags: <Dialog.Root> → <Dialog>
447
+ const openPattern = new RegExp(`<${escapedName}([^/>]*?)>`, 'g');
448
+ output = output.replace(openPattern, (match, attrs) => {
449
+ transformed.push(`${compoundName} → ${newComponent}`);
450
+ newComponents.add(newComponent);
451
+ changed = true;
452
+ return `<${newComponent}${attrs}>`;
453
+ });
454
+
455
+ // Self-closing tags: <Dialog.Root ... /> → <Dialog ... />
456
+ const selfPattern = new RegExp(`<${escapedName}([^>]*?)\\/>`, 'g');
457
+ output = output.replace(selfPattern, (match, attrs) => {
458
+ transformed.push(`${compoundName} → ${newComponent}`);
459
+ newComponents.add(newComponent);
460
+ changed = true;
461
+ return `<${newComponent}${attrs}/>`;
462
+ });
463
+
464
+ // Closing tags: </Dialog.Root> → </Dialog>
465
+ const closePattern = new RegExp(`<\\/${escapedName}>`, 'g');
466
+ output = output.replace(closePattern, () => {
467
+ changed = true;
468
+ return `</${newComponent}>`;
469
+ });
470
+ } else if (config.tag) {
471
+ // Replace with JSX tag + Tailwind classes (like regular JSX transformations)
472
+ const toTag = config.tag;
473
+ const tw = config.class || '';
474
+
475
+ // Remove Chakra props by actually parsing attributes properly
476
+ // This helper removes known Chakra styling props
477
+ function removeChakraProps(attrString) {
478
+ const chakraPropsSet = new Set([
479
+ 'p',
480
+ 'm',
481
+ 'px',
482
+ 'py',
483
+ 'mx',
484
+ 'my',
485
+ 'pt',
486
+ 'pb',
487
+ 'pl',
488
+ 'pr',
489
+ 'mt',
490
+ 'mb',
491
+ 'ml',
492
+ 'mr',
493
+ 'w',
494
+ 'h',
495
+ 'bg',
496
+ 'bgColor',
497
+ 'color',
498
+ 'fontSize',
499
+ 'fontWeight',
500
+ 'textAlign',
501
+ 'display',
502
+ 'flex',
503
+ 'flexDirection',
504
+ 'direction',
505
+ 'align',
506
+ 'alignItems',
507
+ 'justify',
508
+ 'justifyContent',
509
+ 'gap',
510
+ 'borderRadius',
511
+ 'rounded',
512
+ 'border',
513
+ 'borderWidth',
514
+ 'borderColor',
515
+ 'borderStyle',
516
+ 'boxShadow',
517
+ 'position',
518
+ 'top',
519
+ 'right',
520
+ 'bottom',
521
+ 'left',
522
+ 'zIndex',
523
+ 'overflow',
524
+ 'variant',
525
+ 'size',
526
+ 'colorPalette',
527
+ 'colorScheme',
528
+ 'spacing',
529
+ 'maxW',
530
+ 'maxH',
531
+ 'minW',
532
+ 'minH',
533
+ 'transition',
534
+ '_hover',
535
+ '_active',
536
+ '_focus',
537
+ '_disabled',
538
+ '_focusVisible',
539
+ 'status',
540
+ 'aria',
541
+ 'loading',
542
+ ]);
543
+
544
+ let result = '';
545
+ let i = 0;
546
+ const str = attrString.trim();
547
+
548
+ while (i < str.length) {
549
+ // Skip whitespace
550
+ while (i < str.length && /\s/.test(str[i])) {
551
+ result += str[i];
552
+ i++;
553
+ }
554
+
555
+ if (i >= str.length) break;
556
+
557
+ // Match attribute name
558
+ let name = '';
559
+ while (i < str.length && /[\w-]/.test(str[i])) {
560
+ name += str[i];
561
+ i++;
562
+ }
563
+
564
+ if (!name) break;
565
+
566
+ // Skip whitespace after name
567
+ while (i < str.length && /\s/.test(str[i])) {
568
+ i++;
569
+ }
570
+
571
+ // Check if there's a value
572
+ if (i < str.length && str[i] === '=') {
573
+ i++; // skip '='
574
+
575
+ // Skip whitespace after '='
576
+ while (i < str.length && /\s/.test(str[i])) {
577
+ i++;
578
+ }
579
+
580
+ // Extract value
581
+ let value = '';
582
+ if (str[i] === '"' || str[i] === "'") {
583
+ // Quoted string
584
+ const quote = str[i];
585
+ value += str[i];
586
+ i++;
587
+ while (i < str.length && str[i] !== quote) {
588
+ value += str[i];
589
+ i++;
590
+ }
591
+ if (i < str.length) {
592
+ value += str[i]; // closing quote
593
+ i++;
594
+ }
595
+ } else if (str[i] === '{') {
596
+ // JSX expression - need to balance braces
597
+ let braceCount = 0;
598
+ while (i < str.length) {
599
+ if (str[i] === '{') braceCount++;
600
+ if (str[i] === '}') braceCount--;
601
+ value += str[i];
602
+ i++;
603
+ if (braceCount === 0) break;
604
+ }
605
+ } else {
606
+ // Unquoted value (boolean shorthand)
607
+ while (i < str.length && /[\w.-]/.test(str[i])) {
608
+ value += str[i];
609
+ i++;
610
+ }
611
+ }
612
+
613
+ // Only keep if not a Chakra prop
614
+ if (!chakraPropsSet.has(name)) {
615
+ result += name + '=' + value;
616
+ }
617
+ } else {
618
+ // Boolean prop (no value)
619
+ if (!chakraPropsSet.has(name)) {
620
+ result += name;
621
+ }
622
+ }
623
+ }
624
+
625
+ return result;
626
+ }
627
+
628
+ // Self-closing tags: <Card.Root ... /> → <div className="..." />
629
+ const selfPattern = new RegExp(`<${escapedName}([^>]*?)\\/>`, 'g');
630
+ output = output.replace(selfPattern, (match, attrs) => {
631
+ // Remove Chakra props
632
+ let cleanedAttrs = removeChakraProps(attrs);
633
+
634
+ // Add Tailwind classes
635
+ const tailwindClasses = tw ? tw.split(' ').filter(Boolean) : [];
636
+ let newAttrs = '';
637
+
638
+ if (cleanedAttrs.trim() || tailwindClasses.length > 0) {
639
+ const attrsToKeep = cleanedAttrs.trim();
640
+ if (tailwindClasses.length > 0) {
641
+ if (attrsToKeep) {
642
+ newAttrs = ` ${attrsToKeep} className="${tailwindClasses.join(' ')}"`;
643
+ } else {
644
+ newAttrs = ` className="${tailwindClasses.join(' ')}"`;
645
+ }
646
+ } else if (attrsToKeep) {
647
+ newAttrs = ` ${attrsToKeep}`;
648
+ }
649
+ }
650
+
651
+ transformed.push(`${compoundName} → ${toTag}`);
652
+ changed = true;
653
+ return `<${toTag}${newAttrs}/>`;
654
+ });
655
+
656
+ // Opening tags: <Card.Root> → <div className="...">
657
+ const openPattern = new RegExp(`<${escapedName}([^/>]*?)>`, 'g');
658
+ output = output.replace(openPattern, (match, attrs) => {
659
+ // Remove Chakra props
660
+ let cleanedAttrs = removeChakraProps(attrs);
661
+
662
+ // Add Tailwind classes
663
+ const tailwindClasses = tw ? tw.split(' ').filter(Boolean) : [];
664
+ let newAttrs = '';
665
+
666
+ if (cleanedAttrs.trim() || tailwindClasses.length > 0) {
667
+ const attrsToKeep = cleanedAttrs.trim();
668
+ if (tailwindClasses.length > 0) {
669
+ if (attrsToKeep) {
670
+ newAttrs = ` ${attrsToKeep} className="${tailwindClasses.join(' ')}"`;
671
+ } else {
672
+ newAttrs = ` className="${tailwindClasses.join(' ')}"`;
673
+ }
674
+ } else if (attrsToKeep) {
675
+ newAttrs = ` ${attrsToKeep}`;
676
+ }
677
+ }
678
+
679
+ transformed.push(`${compoundName} → ${toTag}`);
680
+ changed = true;
681
+ return `<${toTag}${newAttrs}>`;
682
+ });
683
+
684
+ // Closing tags: </Card.Root> → </div>
685
+ const closePattern = new RegExp(`<\\/${escapedName}>`, 'g');
686
+ output = output.replace(closePattern, () => {
687
+ changed = true;
688
+ return `</${toTag}>`;
689
+ });
690
+ }
691
+ }
692
+
693
+ return {
694
+ output,
695
+ changed,
696
+ transformed,
697
+ newComponents: Array.from(newComponents),
698
+ };
699
+ }
700
+
701
+ function transformJSX(input, jsxSpecifiers, usedSpecifiers) {
702
+ if (!input) return { output: input, changed: false, replaced: [] };
703
+ let output = input;
704
+ const replaced = new Set();
705
+ let changed = false;
706
+
707
+ // Chakra prop to Tailwind class mappings
708
+ const PROP_MAPPINGS = {
709
+ // Spacing
710
+ p: (v) => `p-${v}`,
711
+ px: (v) => `px-${v}`,
712
+ py: (v) => `py-${v}`,
713
+ pt: (v) => `pt-${v}`,
714
+ pb: (v) => `pb-${v}`,
715
+ pl: (v) => `pl-${v}`,
716
+ pr: (v) => `pr-${v}`,
717
+ m: (v) => `m-${v}`,
718
+ mx: (v) => `mx-${v}`,
719
+ my: (v) => `my-${v}`,
720
+ mt: (v) => `mt-${v}`,
721
+ mb: (v) => `mb-${v}`,
722
+ ml: (v) => `ml-${v}`,
723
+ mr: (v) => `mr-${v}`,
724
+ gap: (v) => `gap-${v}`,
725
+
726
+ // Sizing
727
+ w: (v) => (v === 'full' ? 'w-full' : `w-[${v}]`),
728
+ h: (v) => (v === 'full' ? 'h-full' : `h-[${v}]`),
729
+ width: (v) => (v === 'full' ? 'w-full' : `w-[${v}]`),
730
+ height: (v) => (v === 'full' ? 'h-full' : `h-[${v}]`),
731
+ maxW: (v) => (v === 'full' ? 'max-w-full' : `max-w-[${v}]`),
732
+ maxH: (v) => (v === 'full' ? 'max-h-full' : `max-h-[${v}]`),
733
+ minW: (v) => `min-w-[${v}]`,
734
+ minH: (v) => `min-h-[${v}]`,
735
+
736
+ // Flexbox
737
+ flex: (v) => (v === '1' || v === 1 ? 'flex-1' : `flex-[${v}]`),
738
+ flexDir: (v) =>
739
+ v === 'column' ? 'flex-col' : v === 'row' ? 'flex-row' : `flex-${v}`,
740
+ flexDirection: (v) =>
741
+ v === 'column' ? 'flex-col' : v === 'row' ? 'flex-row' : `flex-${v}`,
742
+ direction: (v) =>
743
+ v === 'column' ? 'flex-col' : v === 'row' ? 'flex-row' : `flex-${v}`,
744
+ align: (v) =>
745
+ v === 'center'
746
+ ? 'items-center'
747
+ : v === 'start'
748
+ ? 'items-start'
749
+ : v === 'end'
750
+ ? 'items-end'
751
+ : `items-${v}`,
752
+ alignItems: (v) =>
753
+ v === 'center'
754
+ ? 'items-center'
755
+ : v === 'start'
756
+ ? 'items-start'
757
+ : v === 'end'
758
+ ? 'items-end'
759
+ : `items-${v}`,
760
+ justify: (v) =>
761
+ v === 'center'
762
+ ? 'justify-center'
763
+ : v === 'between'
764
+ ? 'justify-between'
765
+ : v === 'around'
766
+ ? 'justify-around'
767
+ : `justify-${v}`,
768
+ justifyContent: (v) =>
769
+ v === 'center'
770
+ ? 'justify-center'
771
+ : v === 'between'
772
+ ? 'justify-between'
773
+ : v === 'around'
774
+ ? 'justify-around'
775
+ : `justify-${v}`,
776
+
777
+ // Display
778
+ display: (v) => v,
779
+
780
+ // Position
781
+ pos: (v) => v,
782
+ position: (v) => v,
783
+
784
+ // Overflow
785
+ overflow: (v) => `overflow-${v}`,
786
+ overflowX: (v) => `overflow-x-${v}`,
787
+ overflowY: (v) => `overflow-y-${v}`,
788
+
789
+ // Text
790
+ fontSize: (v) => `text-[${v}]`,
791
+ fontWeight: (v) =>
792
+ v === 'bold'
793
+ ? 'font-bold'
794
+ : v === 'semibold'
795
+ ? 'font-semibold'
796
+ : `font-${v}`,
797
+ textAlign: (v) => `text-${v}`,
798
+ color: (v) => `text-[${v}]`,
799
+
800
+ // Background
801
+ bg: (v) => `bg-[${v}]`,
802
+ bgColor: (v) => `bg-[${v}]`,
803
+
804
+ // Border
805
+ border: (v) => `border-[${v}]`,
806
+ borderLeft: (v) => `border-l-[${v}]`,
807
+ borderRight: (v) => `border-r-[${v}]`,
808
+ borderTop: (v) => `border-t-[${v}]`,
809
+ borderBottom: (v) => `border-b-[${v}]`,
810
+ borderColor: (v) => `border-[${v}]`,
811
+ borderRadius: (v) =>
812
+ v === 'sm'
813
+ ? 'rounded-sm'
814
+ : v === 'md'
815
+ ? 'rounded-md'
816
+ : v === 'lg'
817
+ ? 'rounded-lg'
818
+ : `rounded-[${v}]`,
819
+ rounded: (v) =>
820
+ v === 'sm'
821
+ ? 'rounded-sm'
822
+ : v === 'md'
823
+ ? 'rounded-md'
824
+ : v === 'lg'
825
+ ? 'rounded-lg'
826
+ : `rounded-[${v}]`,
827
+
828
+ // More flexbox
829
+ flexWrap: (v) => `flex-wrap${v === 'wrap' ? '' : `-${v}`}`,
830
+ flexShrink: (v) => `flex-shrink${v === '0' || v === 0 ? '-0' : ''}`,
831
+ flexGrow: (v) => `flex-grow${v === '1' || v === 1 ? '' : `-${v}`}`,
832
+
833
+ // Grid
834
+ columns: (v) => `grid-cols-${v}`,
835
+ rows: (v) => `grid-rows-${v}`,
836
+
837
+ // Opacity
838
+ opacity: (v) => `opacity-${Math.round(parseFloat(v) * 100)}`,
839
+
840
+ // Min/Max
841
+ minHeight: (v) => `min-h-[${v}]`,
842
+ minWidth: (v) => `min-w-[${v}]`,
843
+
844
+ // Text
845
+ lineClamp: (v) => `line-clamp-${v}`,
846
+ lineHeight: (v) => `leading-[${v}]`,
847
+ whiteSpace: (v) => `whitespace-${v}`,
848
+
849
+ // Border colors
850
+ borderLeftColor: (v) => `border-l-[${v}]`,
851
+ borderRightColor: (v) => `border-r-[${v}]`,
852
+ borderTopColor: (v) => `border-t-[${v}]`,
853
+ borderBottomColor: (v) => `border-b-[${v}]`,
854
+
855
+ // Misc
856
+ padding: (v) => `p-${v}`,
857
+ wrap: (v) => `flex-wrap${v === 'wrap' ? '' : `-${v}`}`,
858
+ };
859
+
860
+ // Component-specific prop mappings
861
+ const COMPONENT_SPECIFIC_PROPS = {
862
+ Heading: {
863
+ size: {
864
+ xs: 'text-md',
865
+ sm: 'text-lg',
866
+ md: 'text-xl',
867
+ lg: 'text-2xl',
868
+ xl: 'text-3xl',
869
+ '2xl': 'text-4xl',
870
+ },
871
+ },
872
+ Text: {
873
+ size: {
874
+ xs: 'text-xs',
875
+ sm: 'text-sm',
876
+ md: 'text-base',
877
+ lg: 'text-lg',
878
+ xl: 'text-xl',
879
+ '2xl': 'text-2xl',
880
+ },
881
+ },
882
+ };
883
+
884
+ // Parse JSX attributes from a string
885
+ function parseAttributes(attrString) {
886
+ const attrs = [];
887
+ let remaining = attrString.trim();
888
+
889
+ while (remaining) {
890
+ // Skip leading whitespace
891
+ remaining = remaining.replace(/^\s+/, '');
892
+ if (!remaining) break;
893
+
894
+ // Match attribute name (including hyphens for aria-*, data-*, etc.)
895
+ const nameMatch = remaining.match(/^([\w-]+)/);
896
+ if (!nameMatch) break;
897
+
898
+ const name = nameMatch[1];
899
+ remaining = remaining.slice(name.length);
900
+
901
+ // Check if there's a value
902
+ if (remaining.match(/^\s*=/)) {
903
+ remaining = remaining.replace(/^\s*=\s*/, '');
904
+
905
+ let value = null;
906
+ let isJSX = false;
907
+ let fullMatch = name + '=';
908
+
909
+ if (remaining[0] === '{') {
910
+ // JSX expression - need to balance braces
911
+ let depth = 0;
912
+ let i = 0;
913
+ for (; i < remaining.length; i++) {
914
+ if (remaining[i] === '{') depth++;
915
+ if (remaining[i] === '}') depth--;
916
+ if (depth === 0) break;
917
+ }
918
+ value = remaining.slice(1, i);
919
+ fullMatch += remaining.slice(0, i + 1);
920
+ remaining = remaining.slice(i + 1);
921
+ isJSX = true;
922
+ } else if (remaining[0] === '"') {
923
+ // Double quoted string
924
+ const endQuote = remaining.indexOf('"', 1);
925
+ value = remaining.slice(1, endQuote);
926
+ fullMatch += remaining.slice(0, endQuote + 1);
927
+ remaining = remaining.slice(endQuote + 1);
928
+ } else if (remaining[0] === "'") {
929
+ // Single quoted string
930
+ const endQuote = remaining.indexOf("'", 1);
931
+ value = remaining.slice(1, endQuote);
932
+ fullMatch += remaining.slice(0, endQuote + 1);
933
+ remaining = remaining.slice(endQuote + 1);
934
+ }
935
+
936
+ attrs.push({ name, value, isJSX, fullMatch });
937
+ } else {
938
+ // Boolean attribute
939
+ attrs.push({ name, value: true, isJSX: false, fullMatch: name });
940
+ }
941
+ }
942
+
943
+ return attrs;
944
+ }
945
+
946
+ // Transform Chakra props to Tailwind classes
947
+ function transformChakraProps(attrString, componentName) {
948
+ const attrs = parseAttributes(attrString);
949
+ const tailwindClasses = [];
950
+ const keepAttrs = [];
951
+ let asElement = null;
952
+
953
+ for (const attr of attrs) {
954
+ if (attr.name === 'className' || attr.name === 'class') {
955
+ // Keep className as-is, we'll merge with it later
956
+ keepAttrs.push(attr);
957
+ continue;
958
+ }
959
+
960
+ // Skip Chakra pseudo-props (require manual conversion)
961
+ if (attr.name.startsWith('_')) {
962
+ // _hover, _active, _focus, etc. - skip these (will be removed)
963
+ continue;
964
+ }
965
+
966
+ // Keep 'as' prop for now - it will cause type errors but won't break JSX
967
+ // TODO: Handle polymorphic 'as' prop transformation
968
+ if (attr.name === 'as') {
969
+ keepAttrs.push(attr);
970
+ continue;
971
+ }
972
+
973
+ // Keep data and aria attributes
974
+ if (attr.name.startsWith('data-') || attr.name.startsWith('aria-')) {
975
+ keepAttrs.push(attr);
976
+ continue;
977
+ }
978
+
979
+ // Check for component-specific prop handling
980
+ const componentProps = COMPONENT_SPECIFIC_PROPS[componentName];
981
+ if (componentProps && componentProps[attr.name]) {
982
+ // Component-specific transformation
983
+ try {
984
+ let value = attr.value;
985
+ if (attr.isJSX) {
986
+ value = value.trim().replace(/^["']|["']$/g, '');
987
+ }
988
+ const mapping = componentProps[attr.name];
989
+ const twClass = mapping[value] || mapping[value.toString()];
990
+ if (twClass) {
991
+ tailwindClasses.push(twClass);
992
+ continue; // Don't keep the original attribute
993
+ }
994
+ } catch {
995
+ // Fall through to default handling
996
+ }
997
+ }
998
+
999
+ const mapper = PROP_MAPPINGS[attr.name];
1000
+ if (mapper) {
1001
+ // Transform Chakra prop to Tailwind
1002
+ try {
1003
+ let value = attr.value;
1004
+ // Handle JSX expressions: extract the value if it's a simple literal
1005
+ if (attr.isJSX) {
1006
+ value = value.trim();
1007
+ // Remove quotes if present
1008
+ value = value.replace(/^["']|["']$/g, '');
1009
+ }
1010
+ const twClass = mapper(value);
1011
+ if (twClass) {
1012
+ tailwindClasses.push(twClass);
1013
+ }
1014
+ } catch {
1015
+ // If transformation fails, keep the original attribute
1016
+ keepAttrs.push(attr);
1017
+ }
1018
+ } else {
1019
+ // Keep attributes we don't recognize
1020
+ keepAttrs.push(attr);
1021
+ }
1022
+ }
1023
+
1024
+ return { tailwindClasses, keepAttrs, asElement };
1025
+ }
1026
+
1027
+ // Reconstruct attribute string from parsed attributes
1028
+ function reconstructAttrs(keepAttrs, tailwindClasses) {
1029
+ const parts = [];
1030
+
1031
+ // Add kept attributes
1032
+ for (const attr of keepAttrs) {
1033
+ if (attr.name === 'className' || attr.name === 'class') {
1034
+ // Merge Tailwind classes with existing className
1035
+ if (tailwindClasses.length > 0) {
1036
+ const existingClasses = attr.isJSX ? attr.value : attr.value;
1037
+ if (attr.isJSX) {
1038
+ // className={...} - merge into template literal
1039
+ parts.push(
1040
+ `className={\`${tailwindClasses.join(' ')} \${${existingClasses}}\`}`,
1041
+ );
1042
+ } else {
1043
+ // className="..." - simple string concatenation
1044
+ parts.push(
1045
+ `className="${tailwindClasses.join(' ')} ${existingClasses}"`,
1046
+ );
1047
+ }
1048
+ tailwindClasses.length = 0; // Clear so we don't add again
1049
+ } else {
1050
+ parts.push(attr.fullMatch);
1051
+ }
1052
+ } else {
1053
+ parts.push(attr.fullMatch);
1054
+ }
1055
+ }
1056
+
1057
+ // If we have Tailwind classes but no existing className, add it
1058
+ if (tailwindClasses.length > 0) {
1059
+ parts.push(`className="${tailwindClasses.join(' ')}"`);
1060
+ }
1061
+
1062
+ return parts.length > 0 ? ' ' + parts.join(' ') : '';
1063
+ }
1064
+
1065
+ // Inject Tailwind classes into className attribute (legacy - kept for backward compat)
1066
+ // legacy helper removed (unused)
1067
+
1068
+ for (const [fromName, spec] of Object.entries(jsxSpecifiers)) {
1069
+ if (usedSpecifiers && usedSpecifiers.size && !usedSpecifiers.has(fromName))
1070
+ continue;
1071
+ const toTag = spec.tag || 'div';
1072
+ const tw = spec.class || '';
1073
+ const escapedFromName = escapeRegex(fromName);
1074
+
1075
+ // Self-closing tags: <From ... />
1076
+ // Use negative lookbehind to avoid matching TypeScript generics (e.g., Array<Text>)
1077
+ // JSX tags are preceded by: =, (, {, >, whitespace, or start of string
1078
+ const selfClosing = new RegExp(
1079
+ `(?<=[=({>\\s]|^)<${escapedFromName}([^>]*?)\\/>`,
1080
+ 'gs',
1081
+ );
1082
+ output = output.replace(selfClosing, (match, attrs) => {
1083
+ // Transform Chakra props to Tailwind classes, passing component name for context
1084
+ const { tailwindClasses, keepAttrs } = transformChakraProps(
1085
+ attrs,
1086
+ fromName,
1087
+ );
1088
+ // Add component-specific Tailwind classes from config
1089
+ if (tw) {
1090
+ tailwindClasses.unshift(...tw.split(' ').filter(Boolean));
1091
+ }
1092
+ // Reconstruct attributes with transformed props
1093
+ let newAttrs = reconstructAttrs(keepAttrs, tailwindClasses);
1094
+
1095
+ changed = true;
1096
+ replaced.add(fromName);
1097
+ return `<${toTag}${newAttrs}/>`;
1098
+ });
1099
+
1100
+ // Opening tags: <From ...>
1101
+ // Use negative lookbehind to avoid matching TypeScript generics (e.g., React.FC<TextProps>)
1102
+ // JSX tags are preceded by: =, (, {, >, whitespace, or start of string
1103
+ const opening = new RegExp(
1104
+ `(?<=[=({>\\s]|^)<${escapedFromName}([^>]*?)>`,
1105
+ 'gs',
1106
+ );
1107
+ output = output.replace(opening, (match, attrs) => {
1108
+ // Transform Chakra props to Tailwind classes, passing component name for context
1109
+ const { tailwindClasses, keepAttrs } = transformChakraProps(
1110
+ attrs,
1111
+ fromName,
1112
+ );
1113
+ // Add component-specific Tailwind classes from config
1114
+ if (tw) {
1115
+ tailwindClasses.unshift(...tw.split(' ').filter(Boolean));
1116
+ }
1117
+ // Reconstruct attributes with transformed props
1118
+ let newAttrs = reconstructAttrs(keepAttrs, tailwindClasses);
1119
+
1120
+ changed = true;
1121
+ replaced.add(fromName);
1122
+ return `<${toTag}${newAttrs}>`;
1123
+ });
1124
+
1125
+ // Closing tags: </From>
1126
+ const closing = new RegExp(`<\\/${escapedFromName}\\s*>`, 'g');
1127
+ output = output.replace(closing, () => {
1128
+ changed = true;
1129
+ replaced.add(fromName);
1130
+ return `</${toTag}>`;
1131
+ });
1132
+ }
1133
+
1134
+ return { output, changed, replaced };
1135
+ }