mlola-ui 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ Tailwind's preflight in either import order.
45
45
  | `list [--json]` | print every installable item |
46
46
  | `doctor` | report project and registry problems |
47
47
  | `check [path…] [--json] [--strict] [--all]` | check every file's markup and CSS against the contract, and count the colors and spacing typed by hand |
48
+ | `migrate [path…] [--write] [--json]` | move a shadcn/ui project's props, toasts and icons to Mlola, and list what needs a person |
48
49
  | `login <token>` | save a Mlola Pro token (from /account) for this user |
49
50
  | `logout` | forget the saved token |
50
51
 
@@ -83,6 +84,28 @@ jobs:
83
84
  - run: npx -y mlola-ui@latest check src
84
85
  ```
85
86
 
87
+ ## Moving from shadcn/ui
88
+
89
+ `migrate` makes the changes a machine can make without guessing, and lists
90
+ the rest with the line and what to do. Mlola installs a component at the
91
+ same file shadcn/ui did (`components/ui/button.tsx`), so imports stay as
92
+ they are. It reads by default; `--write` makes the changes.
93
+
94
+ ```bash
95
+ npx mlola-ui migrate # what would change, and what needs a person
96
+ npx mlola-ui migrate --write # make the changes
97
+ npx mlola-ui add button badge toast --overwrite # the command it prints
98
+ ```
99
+
100
+ It rewrites props written out as literals (`variant="destructive"` becomes
101
+ `variant="danger"`, a Badge's variant becomes its `tone`), moves `sonner`
102
+ and the older `use-toast` to Mlola's toast (`toast.error` becomes
103
+ `toast.danger`), `switch` to `toggle`, and the lucide-react glyphs Mlola
104
+ has to `@mlola-ui/icons`, in the file's own import style. It leaves a value
105
+ computed at runtime, a `cva()` helper and a component whose API differs
106
+ (Dialog, Select, Dropdown Menu…) to you, naming the attributes or the
107
+ component that replace each one.
108
+
86
109
  ## Coding agents
87
110
 
88
111
  `init` (or `agents`, in an existing project) tells the project's coding agents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlola-ui",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "mcpName": "io.github.mlolahq/mlola-ui",
5
5
  "description": "Source-copy CLI that writes framework-free Mlola UI items into your project",
6
6
  "type": "module",
@@ -110,7 +110,8 @@
110
110
  "marketing-site",
111
111
  "docs-site",
112
112
  "finance",
113
- "smart-home"
113
+ "smart-home",
114
+ "islamic-finance"
114
115
  ],
115
116
  "items": [
116
117
  {
@@ -1522,6 +1523,23 @@
1522
1523
  "dashboard",
1523
1524
  "premium"
1524
1525
  ]
1526
+ },
1527
+ {
1528
+ "name": "islamic-finance",
1529
+ "type": "registry:template",
1530
+ "title": "Islamic Finance",
1531
+ "description": "Sharia finance for a household: zakat reckoned on what is held a lunar year against the nisab in gold or silver and given to the eight who may receive it, dates on the Hijri calendar, sadaqah and waqf endowments, sukuk that pay a share of what their assets earn, and a home bought by murabahah at a price agreed once",
1532
+ "category": "application",
1533
+ "tags": [
1534
+ "template",
1535
+ "finance",
1536
+ "islamic-finance",
1537
+ "zakat",
1538
+ "sukuk",
1539
+ "waqf",
1540
+ "murabahah",
1541
+ "premium"
1542
+ ]
1525
1543
  }
1526
1544
  ]
1527
1545
  }
@@ -0,0 +1,284 @@
1
+ {
2
+ "version": 1,
3
+ "names": [
4
+ "Archive",
5
+ "ArrowDown",
6
+ "ArrowLeft",
7
+ "ArrowRight",
8
+ "ArrowUp",
9
+ "ArrowUpDown",
10
+ "ArrowUpRight",
11
+ "Ban",
12
+ "BarChart",
13
+ "Bath",
14
+ "Bed",
15
+ "Bell",
16
+ "Blinds",
17
+ "Blocks",
18
+ "BookOpen",
19
+ "Bookmark",
20
+ "Box",
21
+ "Calendar",
22
+ "Car",
23
+ "Check",
24
+ "ChevronDown",
25
+ "ChevronLeft",
26
+ "ChevronRight",
27
+ "ChevronUp",
28
+ "ChevronsLeft",
29
+ "ChevronsRight",
30
+ "ChevronsUpDown",
31
+ "CircleAlert",
32
+ "CircleCheck",
33
+ "CircleDashed",
34
+ "CircleHelp",
35
+ "CircleX",
36
+ "Clipboard",
37
+ "Clock",
38
+ "Cloud",
39
+ "Code2",
40
+ "Coins",
41
+ "CookingPot",
42
+ "Copy",
43
+ "CreditCard",
44
+ "Database",
45
+ "DoorClosed",
46
+ "Download",
47
+ "Droplet",
48
+ "ExternalLink",
49
+ "Eye",
50
+ "EyeOff",
51
+ "Filter",
52
+ "Flag",
53
+ "Globe",
54
+ "Heart",
55
+ "Home",
56
+ "Hourglass",
57
+ "IconArchive",
58
+ "IconArrowDown",
59
+ "IconArrowLeft",
60
+ "IconArrowRight",
61
+ "IconArrowUp",
62
+ "IconArrowUpDown",
63
+ "IconArrowUpRight",
64
+ "IconBan",
65
+ "IconBarChart",
66
+ "IconBathtub",
67
+ "IconBed",
68
+ "IconBell",
69
+ "IconBlinds",
70
+ "IconBlocks",
71
+ "IconBold",
72
+ "IconBookOpen",
73
+ "IconBookmark",
74
+ "IconBox3D",
75
+ "IconBrain",
76
+ "IconCalendar",
77
+ "IconCar",
78
+ "IconCertificate",
79
+ "IconCheck",
80
+ "IconCheckSquare",
81
+ "IconChevronDown",
82
+ "IconChevronLeft",
83
+ "IconChevronRight",
84
+ "IconChevronUp",
85
+ "IconChevronsLeft",
86
+ "IconChevronsRight",
87
+ "IconChevronsUpDown",
88
+ "IconCircle",
89
+ "IconCircleAlert",
90
+ "IconCircleCheck",
91
+ "IconCircleDashed",
92
+ "IconCircleX",
93
+ "IconClearFormat",
94
+ "IconClipboard",
95
+ "IconClock",
96
+ "IconCloud",
97
+ "IconCode",
98
+ "IconCodeBlock",
99
+ "IconCoins",
100
+ "IconCommand",
101
+ "IconCookingPot",
102
+ "IconCopy",
103
+ "IconCreditCard",
104
+ "IconDatabase",
105
+ "IconDiamond",
106
+ "IconDoor",
107
+ "IconDownload",
108
+ "IconDroplet",
109
+ "IconEdit",
110
+ "IconEraser",
111
+ "IconExternalLink",
112
+ "IconEye",
113
+ "IconEyeOff",
114
+ "IconFile",
115
+ "IconFilePlus",
116
+ "IconFilter",
117
+ "IconFlag",
118
+ "IconFolder",
119
+ "IconFolderOpen",
120
+ "IconFridge",
121
+ "IconGarage",
122
+ "IconGitBranch",
123
+ "IconGitCommit",
124
+ "IconGitPullRequest",
125
+ "IconGlobe",
126
+ "IconGoldBar",
127
+ "IconGrip",
128
+ "IconHand",
129
+ "IconHeading",
130
+ "IconHeart",
131
+ "IconHelp",
132
+ "IconHome",
133
+ "IconHourglass",
134
+ "IconImage",
135
+ "IconInbox",
136
+ "IconInfo",
137
+ "IconItalic",
138
+ "IconKey",
139
+ "IconLayers",
140
+ "IconLayoutGrid",
141
+ "IconLayoutList",
142
+ "IconLightbulb",
143
+ "IconLink",
144
+ "IconList",
145
+ "IconListOrdered",
146
+ "IconLoader",
147
+ "IconLock",
148
+ "IconLogIn",
149
+ "IconLogOut",
150
+ "IconMail",
151
+ "IconMapPin",
152
+ "IconMaximize",
153
+ "IconMenu",
154
+ "IconMessage",
155
+ "IconMic",
156
+ "IconMicOff",
157
+ "IconMinimize",
158
+ "IconMinus",
159
+ "IconMonitor",
160
+ "IconMoon",
161
+ "IconMoreHorizontal",
162
+ "IconMoreVertical",
163
+ "IconMotion",
164
+ "IconOven",
165
+ "IconPalette",
166
+ "IconPanelLeft",
167
+ "IconPaperclip",
168
+ "IconPause",
169
+ "IconPen",
170
+ "IconPhone",
171
+ "IconPin",
172
+ "IconPlay",
173
+ "IconPlug",
174
+ "IconPlus",
175
+ "IconPointer",
176
+ "IconPrinter",
177
+ "IconQuote",
178
+ "IconReceipt",
179
+ "IconRedo",
180
+ "IconRefresh",
181
+ "IconScale",
182
+ "IconSearch",
183
+ "IconSend",
184
+ "IconSettings",
185
+ "IconShare",
186
+ "IconShield",
187
+ "IconShoppingCart",
188
+ "IconSliders",
189
+ "IconSmartphone",
190
+ "IconSofa",
191
+ "IconSolarPanel",
192
+ "IconSpark",
193
+ "IconSprout",
194
+ "IconSquare",
195
+ "IconStar",
196
+ "IconStickyNote",
197
+ "IconStop",
198
+ "IconStrikethrough",
199
+ "IconSun",
200
+ "IconTag",
201
+ "IconTarget",
202
+ "IconTemplate",
203
+ "IconTerminal",
204
+ "IconThermometer",
205
+ "IconThumbDown",
206
+ "IconThumbUp",
207
+ "IconTool",
208
+ "IconTrash",
209
+ "IconTrendingUp",
210
+ "IconTriangleAlert",
211
+ "IconType",
212
+ "IconUnderline",
213
+ "IconUndo",
214
+ "IconUnlock",
215
+ "IconUpload",
216
+ "IconUser",
217
+ "IconVolume",
218
+ "IconWallet",
219
+ "IconWasher",
220
+ "IconWind",
221
+ "IconWindow",
222
+ "IconWorkflow",
223
+ "IconX",
224
+ "IconZap",
225
+ "IconZoomIn",
226
+ "IconZoomOut",
227
+ "Inbox",
228
+ "Info",
229
+ "Key",
230
+ "Layers",
231
+ "LayoutGrid",
232
+ "LayoutList",
233
+ "LayoutTemplate",
234
+ "Lightbulb",
235
+ "Loader2",
236
+ "LogIn",
237
+ "LogOut",
238
+ "Mail",
239
+ "MapPin",
240
+ "Menu",
241
+ "MessageSquare",
242
+ "Minimize",
243
+ "Minus",
244
+ "Monitor",
245
+ "Moon",
246
+ "MoreHorizontal",
247
+ "MoreVertical",
248
+ "Palette",
249
+ "PanelLeft",
250
+ "Pause",
251
+ "Phone",
252
+ "Pin",
253
+ "Plug",
254
+ "Plus",
255
+ "Printer",
256
+ "Receipt",
257
+ "Refrigerator",
258
+ "Scale",
259
+ "Search",
260
+ "Send",
261
+ "Settings",
262
+ "ShoppingCart",
263
+ "Smartphone",
264
+ "Sofa",
265
+ "Sparkles",
266
+ "Sprout",
267
+ "Star",
268
+ "Sun",
269
+ "Tag",
270
+ "Target",
271
+ "Thermometer",
272
+ "TrendingUp",
273
+ "TriangleAlert",
274
+ "Upload",
275
+ "User",
276
+ "Wallet",
277
+ "WashingMachine",
278
+ "Wind",
279
+ "X",
280
+ "Zap",
281
+ "ZoomIn",
282
+ "ZoomOut"
283
+ ]
284
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "./schema/registry-index.schema.json",
3
3
  "schemaVersion": 2,
4
4
  "name": "mlola-ui",
5
- "version": "1.4.2",
5
+ "version": "1.5.0",
6
6
  "homepage": "https://ui.mlola.com",
7
7
  "themeContract": {
8
8
  "attribute": "data-theme",
@@ -178,8 +178,8 @@
178
178
  },
179
179
  "registryDependencies": [],
180
180
  "engineDependencies": {
181
- "@mlola-ui/behavior": "^1.4.2",
182
- "@mlola-ui/icons": "^1.4.2"
181
+ "@mlola-ui/behavior": "^1.5.0",
182
+ "@mlola-ui/icons": "^1.5.0"
183
183
  },
184
184
  "options": [
185
185
  {
@@ -240,7 +240,7 @@
240
240
  },
241
241
  "registryDependencies": [],
242
242
  "engineDependencies": {
243
- "@mlola-ui/icons": "^1.4.2"
243
+ "@mlola-ui/icons": "^1.5.0"
244
244
  },
245
245
  "options": [
246
246
  {
@@ -402,7 +402,7 @@
402
402
  },
403
403
  "registryDependencies": [],
404
404
  "engineDependencies": {
405
- "@mlola-ui/icons": "^1.4.2"
405
+ "@mlola-ui/icons": "^1.5.0"
406
406
  },
407
407
  "options": [
408
408
  {
@@ -494,7 +494,7 @@
494
494
  },
495
495
  "registryDependencies": [],
496
496
  "engineDependencies": {
497
- "@mlola-ui/icons": "^1.4.2"
497
+ "@mlola-ui/icons": "^1.5.0"
498
498
  },
499
499
  "options": [],
500
500
  "variants": [],
@@ -547,7 +547,7 @@
547
547
  "spinner"
548
548
  ],
549
549
  "engineDependencies": {
550
- "@mlola-ui/motion": "^1.4.2"
550
+ "@mlola-ui/motion": "^1.5.0"
551
551
  },
552
552
  "options": [
553
553
  {
@@ -715,8 +715,8 @@
715
715
  },
716
716
  "registryDependencies": [],
717
717
  "engineDependencies": {
718
- "@mlola-ui/behavior": "^1.4.2",
719
- "@mlola-ui/icons": "^1.4.2"
718
+ "@mlola-ui/behavior": "^1.5.0",
719
+ "@mlola-ui/icons": "^1.5.0"
720
720
  },
721
721
  "options": [
722
722
  {
@@ -785,7 +785,7 @@
785
785
  },
786
786
  "registryDependencies": [],
787
787
  "engineDependencies": {
788
- "@mlola-ui/icons": "^1.4.2"
788
+ "@mlola-ui/icons": "^1.5.0"
789
789
  },
790
790
  "options": [],
791
791
  "variants": [],
@@ -840,7 +840,7 @@
840
840
  "modal"
841
841
  ],
842
842
  "engineDependencies": {
843
- "@mlola-ui/icons": "^1.4.2"
843
+ "@mlola-ui/icons": "^1.5.0"
844
844
  },
845
845
  "options": [],
846
846
  "variants": [],
@@ -904,8 +904,8 @@
904
904
  },
905
905
  "registryDependencies": [],
906
906
  "engineDependencies": {
907
- "@mlola-ui/behavior": "^1.4.2",
908
- "@mlola-ui/icons": "^1.4.2"
907
+ "@mlola-ui/behavior": "^1.5.0",
908
+ "@mlola-ui/icons": "^1.5.0"
909
909
  },
910
910
  "options": [
911
911
  {
@@ -994,7 +994,7 @@
994
994
  "input"
995
995
  ],
996
996
  "engineDependencies": {
997
- "@mlola-ui/icons": "^1.4.2"
997
+ "@mlola-ui/icons": "^1.5.0"
998
998
  },
999
999
  "options": [],
1000
1000
  "variants": [],
@@ -1219,8 +1219,8 @@
1219
1219
  },
1220
1220
  "registryDependencies": [],
1221
1221
  "engineDependencies": {
1222
- "@mlola-ui/behavior": "^1.4.2",
1223
- "@mlola-ui/icons": "^1.4.2"
1222
+ "@mlola-ui/behavior": "^1.5.0",
1223
+ "@mlola-ui/icons": "^1.5.0"
1224
1224
  },
1225
1225
  "options": [
1226
1226
  {
@@ -1371,7 +1371,7 @@
1371
1371
  },
1372
1372
  "registryDependencies": [],
1373
1373
  "engineDependencies": {
1374
- "@mlola-ui/icons": "^1.4.2"
1374
+ "@mlola-ui/icons": "^1.5.0"
1375
1375
  },
1376
1376
  "options": [],
1377
1377
  "variants": [],
@@ -1422,7 +1422,7 @@
1422
1422
  },
1423
1423
  "registryDependencies": [],
1424
1424
  "engineDependencies": {
1425
- "@mlola-ui/behavior": "^1.4.2"
1425
+ "@mlola-ui/behavior": "^1.5.0"
1426
1426
  },
1427
1427
  "options": [
1428
1428
  {
@@ -1800,7 +1800,7 @@
1800
1800
  },
1801
1801
  "registryDependencies": [],
1802
1802
  "engineDependencies": {
1803
- "@mlola-ui/behavior": "^1.4.2"
1803
+ "@mlola-ui/behavior": "^1.5.0"
1804
1804
  },
1805
1805
  "options": [],
1806
1806
  "variants": [],
@@ -1853,8 +1853,8 @@
1853
1853
  "input"
1854
1854
  ],
1855
1855
  "engineDependencies": {
1856
- "@mlola-ui/behavior": "^1.4.2",
1857
- "@mlola-ui/icons": "^1.4.2"
1856
+ "@mlola-ui/behavior": "^1.5.0",
1857
+ "@mlola-ui/icons": "^1.5.0"
1858
1858
  },
1859
1859
  "options": [
1860
1860
  {
@@ -1932,8 +1932,8 @@
1932
1932
  },
1933
1933
  "registryDependencies": [],
1934
1934
  "engineDependencies": {
1935
- "@mlola-ui/behavior": "^1.4.2",
1936
- "@mlola-ui/icons": "^1.4.2"
1935
+ "@mlola-ui/behavior": "^1.5.0",
1936
+ "@mlola-ui/icons": "^1.5.0"
1937
1937
  },
1938
1938
  "options": [
1939
1939
  {
@@ -2079,7 +2079,7 @@
2079
2079
  "input"
2080
2080
  ],
2081
2081
  "engineDependencies": {
2082
- "@mlola-ui/behavior": "^1.4.2"
2082
+ "@mlola-ui/behavior": "^1.5.0"
2083
2083
  },
2084
2084
  "options": [
2085
2085
  {
@@ -2147,7 +2147,7 @@
2147
2147
  "input"
2148
2148
  ],
2149
2149
  "engineDependencies": {
2150
- "@mlola-ui/behavior": "^1.4.2"
2150
+ "@mlola-ui/behavior": "^1.5.0"
2151
2151
  },
2152
2152
  "options": [
2153
2153
  {
@@ -2307,7 +2307,7 @@
2307
2307
  },
2308
2308
  "registryDependencies": [],
2309
2309
  "engineDependencies": {
2310
- "@mlola-ui/behavior": "^1.4.2"
2310
+ "@mlola-ui/behavior": "^1.5.0"
2311
2311
  },
2312
2312
  "options": [
2313
2313
  {
@@ -2439,7 +2439,7 @@
2439
2439
  },
2440
2440
  "registryDependencies": [],
2441
2441
  "engineDependencies": {
2442
- "@mlola-ui/icons": "^1.4.2"
2442
+ "@mlola-ui/icons": "^1.5.0"
2443
2443
  },
2444
2444
  "options": [
2445
2445
  {
@@ -2527,8 +2527,8 @@
2527
2527
  "spinner"
2528
2528
  ],
2529
2529
  "engineDependencies": {
2530
- "@mlola-ui/behavior": "^1.4.2",
2531
- "@mlola-ui/icons": "^1.4.2"
2530
+ "@mlola-ui/behavior": "^1.5.0",
2531
+ "@mlola-ui/icons": "^1.5.0"
2532
2532
  },
2533
2533
  "options": [
2534
2534
  {
@@ -2683,7 +2683,7 @@
2683
2683
  },
2684
2684
  "registryDependencies": [],
2685
2685
  "engineDependencies": {
2686
- "@mlola-ui/behavior": "^1.4.2"
2686
+ "@mlola-ui/behavior": "^1.5.0"
2687
2687
  },
2688
2688
  "options": [
2689
2689
  {
@@ -2760,7 +2760,7 @@
2760
2760
  },
2761
2761
  "registryDependencies": [],
2762
2762
  "engineDependencies": {
2763
- "@mlola-ui/behavior": "^1.4.2"
2763
+ "@mlola-ui/behavior": "^1.5.0"
2764
2764
  },
2765
2765
  "options": [
2766
2766
  {
@@ -2842,7 +2842,7 @@
2842
2842
  },
2843
2843
  "registryDependencies": [],
2844
2844
  "engineDependencies": {
2845
- "@mlola-ui/icons": "^1.4.2"
2845
+ "@mlola-ui/icons": "^1.5.0"
2846
2846
  },
2847
2847
  "options": [
2848
2848
  {
@@ -2914,7 +2914,7 @@
2914
2914
  "popover"
2915
2915
  ],
2916
2916
  "engineDependencies": {
2917
- "@mlola-ui/icons": "^1.4.2"
2917
+ "@mlola-ui/icons": "^1.5.0"
2918
2918
  },
2919
2919
  "options": [
2920
2920
  {
@@ -2984,7 +2984,7 @@
2984
2984
  "popover"
2985
2985
  ],
2986
2986
  "engineDependencies": {
2987
- "@mlola-ui/icons": "^1.4.2"
2987
+ "@mlola-ui/icons": "^1.5.0"
2988
2988
  },
2989
2989
  "options": [],
2990
2990
  "variants": [],
@@ -3182,8 +3182,8 @@
3182
3182
  "input"
3183
3183
  ],
3184
3184
  "engineDependencies": {
3185
- "@mlola-ui/behavior": "^1.4.2",
3186
- "@mlola-ui/icons": "^1.4.2"
3185
+ "@mlola-ui/behavior": "^1.5.0",
3186
+ "@mlola-ui/icons": "^1.5.0"
3187
3187
  },
3188
3188
  "options": [
3189
3189
  {
@@ -3272,7 +3272,7 @@
3272
3272
  "input"
3273
3273
  ],
3274
3274
  "engineDependencies": {
3275
- "@mlola-ui/icons": "^1.4.2"
3275
+ "@mlola-ui/icons": "^1.5.0"
3276
3276
  },
3277
3277
  "options": [],
3278
3278
  "variants": [],
@@ -3327,7 +3327,7 @@
3327
3327
  "input"
3328
3328
  ],
3329
3329
  "engineDependencies": {
3330
- "@mlola-ui/icons": "^1.4.2"
3330
+ "@mlola-ui/icons": "^1.5.0"
3331
3331
  },
3332
3332
  "options": [],
3333
3333
  "variants": [],
@@ -3390,7 +3390,7 @@
3390
3390
  "popover"
3391
3391
  ],
3392
3392
  "engineDependencies": {
3393
- "@mlola-ui/icons": "^1.4.2"
3393
+ "@mlola-ui/icons": "^1.5.0"
3394
3394
  },
3395
3395
  "options": [
3396
3396
  {
@@ -3461,7 +3461,7 @@
3461
3461
  },
3462
3462
  "registryDependencies": [],
3463
3463
  "engineDependencies": {
3464
- "@mlola-ui/icons": "^1.4.2"
3464
+ "@mlola-ui/icons": "^1.5.0"
3465
3465
  },
3466
3466
  "options": [
3467
3467
  {
@@ -3580,8 +3580,8 @@
3580
3580
  },
3581
3581
  "registryDependencies": [],
3582
3582
  "engineDependencies": {
3583
- "@mlola-ui/behavior": "^1.4.2",
3584
- "@mlola-ui/icons": "^1.4.2"
3583
+ "@mlola-ui/behavior": "^1.5.0",
3584
+ "@mlola-ui/icons": "^1.5.0"
3585
3585
  },
3586
3586
  "options": [],
3587
3587
  "variants": [],
@@ -3645,7 +3645,7 @@
3645
3645
  },
3646
3646
  "registryDependencies": [],
3647
3647
  "engineDependencies": {
3648
- "@mlola-ui/behavior": "^1.4.2"
3648
+ "@mlola-ui/behavior": "^1.5.0"
3649
3649
  },
3650
3650
  "options": [
3651
3651
  {
@@ -3732,8 +3732,8 @@
3732
3732
  },
3733
3733
  "registryDependencies": [],
3734
3734
  "engineDependencies": {
3735
- "@mlola-ui/behavior": "^1.4.2",
3736
- "@mlola-ui/icons": "^1.4.2"
3735
+ "@mlola-ui/behavior": "^1.5.0",
3736
+ "@mlola-ui/icons": "^1.5.0"
3737
3737
  },
3738
3738
  "options": [],
3739
3739
  "variants": [],
@@ -3807,7 +3807,7 @@
3807
3807
  "sheet"
3808
3808
  ],
3809
3809
  "engineDependencies": {
3810
- "@mlola-ui/icons": "^1.4.2"
3810
+ "@mlola-ui/icons": "^1.5.0"
3811
3811
  },
3812
3812
  "options": [],
3813
3813
  "variants": [],
@@ -3930,7 +3930,7 @@
3930
3930
  "button"
3931
3931
  ],
3932
3932
  "engineDependencies": {
3933
- "@mlola-ui/icons": "^1.4.2"
3933
+ "@mlola-ui/icons": "^1.5.0"
3934
3934
  },
3935
3935
  "options": [
3936
3936
  {
@@ -4034,8 +4034,8 @@
4034
4034
  "input"
4035
4035
  ],
4036
4036
  "engineDependencies": {
4037
- "@mlola-ui/behavior": "^1.4.2",
4038
- "@mlola-ui/icons": "^1.4.2"
4037
+ "@mlola-ui/behavior": "^1.5.0",
4038
+ "@mlola-ui/icons": "^1.5.0"
4039
4039
  },
4040
4040
  "options": [
4041
4041
  {
package/src/check.js CHANGED
@@ -23,7 +23,7 @@ const checkable = (file) => {
23
23
  const inSkipped = (file) => file.split(/[\\/]/).some((part) => SKIPPED.has(part));
24
24
 
25
25
  /** The files under each target, relative to cwd: git's list when there is one, so .gitignore holds; a walk otherwise. */
26
- function listFiles(cwd, targets) {
26
+ export function listFiles(cwd, targets) {
27
27
  const git = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", ...targets], { cwd, encoding: "utf8" });
28
28
  let files;
29
29
  if (!git.error && git.status === 0) {
package/src/cli.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./config.js";
9
9
  import { writeAgentFiles } from "./agents.js";
10
10
  import { checkProject, printReport } from "./check.js";
11
+ import { migrateProject, printMigration } from "./migrate.js";
11
12
  import { addAssets, assetHint, loadAssetIndex } from "./assets.js";
12
13
  import { runDoctor } from "./doctor.js";
13
14
  import { collectEngineDependencies, installItems } from "./installer.js";
@@ -30,6 +31,7 @@ Usage:
30
31
  mlola-ui list [--kind component|block|template|asset] [--json]
31
32
  mlola-ui doctor
32
33
  mlola-ui check [path...] [--json] [--strict] [--all]
34
+ mlola-ui migrate [path...] [--write] [--json]
33
35
  mlola-ui theme pull <theme-id | url> [--host <url>] [--overwrite]
34
36
  mlola-ui theme build [--file mlola.theme.json]
35
37
 
@@ -44,6 +46,7 @@ Examples:
44
46
  npx mlola-ui theme build (renders your mlola.theme.json, offline)
45
47
  npx mlola-ui mcp (an MCP server for Claude Code, Cursor, Codex…)
46
48
  npx mlola-ui check src (colors and spacing typed by hand, classes that do not exist)
49
+ npx mlola-ui migrate (from shadcn/ui: what changes, then --write to make it)
47
50
  `;
48
51
 
49
52
  const CLI_VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -357,6 +360,14 @@ export async function run(argv, options = {}) {
357
360
  return report.errors || (hasFlag(args, "--strict") && report.warnings) ? 1 : 0;
358
361
  }
359
362
 
363
+ if (command === "migrate") {
364
+ const targets = args.filter((argument) => !argument.startsWith("--"));
365
+ const result = migrateProject(cwd, { targets: targets.length ? targets : ["."], write: hasFlag(args, "--write") });
366
+ if (hasFlag(args, "--json")) output.log(JSON.stringify(result, null, 2));
367
+ else printMigration(result, output);
368
+ return 0;
369
+ }
370
+
360
371
  if (command === "doctor") {
361
372
  const findings = runDoctor(cwd);
362
373
  for (const item of findings) {
package/src/migrate.js ADDED
@@ -0,0 +1,379 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { listFiles } from "./check.js";
4
+ import { BUNDLED_REGISTRY_ROOT, readJson } from "./registry.js";
5
+
6
+ /**
7
+ * `mlola-ui migrate`: moves a project from shadcn/ui to Mlola, the part a
8
+ * machine can do without guessing. Mlola installs a component at the same
9
+ * path (components/ui/button.tsx), so most imports stay as they are; what
10
+ * changes is the props: variant="destructive" becomes variant="danger",
11
+ * a Badge's variant becomes its tone, toast.error becomes toast.danger, and
12
+ * lucide-react icons come from @mlola-ui/icons. Only literal values are
13
+ * rewritten. A value computed at runtime, a component whose API differs
14
+ * (Dialog, Select, Dropdown Menu…) and a cva() helper are listed with what
15
+ * to do, never changed. It reads by default and writes only with --write.
16
+ */
17
+
18
+ const GUIDE = "https://ui.mlola.com/docs/migrate-from-shadcn";
19
+
20
+ /** Each prop value, as the attributes it becomes ([] drops the attribute: the default). */
21
+ const BUTTON = {
22
+ variant: { default: [["variant", "primary"]], destructive: [["variant", "danger"]], ghost: [["variant", "subtle"]], secondary: [["variant", "secondary"]], outline: [["variant", "outline"]], link: [["variant", "link"]] },
23
+ size: { default: [["size", "md"]], sm: [["size", "sm"]], lg: [["size", "lg"]], icon: [["size", "icon"]], "icon-sm": [["size", "icon"]], "icon-lg": [["size", "icon"]] },
24
+ };
25
+ const BADGE = {
26
+ variant: { default: [["tone", "primary"], ["variant", "solid"]], secondary: [["tone", "neutral"]], destructive: [["tone", "danger"], ["variant", "solid"]], outline: [["variant", "outline"]] },
27
+ };
28
+ const ALERT = { variant: { default: [], destructive: [["tone", "danger"]] } };
29
+
30
+ /**
31
+ * shadcn/ui's modules (the file under components/ui), and what each is in
32
+ * Mlola: the item to add, the module when its name differs, the props that
33
+ * change, or, when the API differs, what to do by hand.
34
+ */
35
+ export const MODULES = {
36
+ accordion: { item: "accordion" },
37
+ alert: { item: "alert", props: { Alert: ALERT } },
38
+ badge: { item: "badge", props: { Badge: BADGE } },
39
+ breadcrumb: { item: "breadcrumb" },
40
+ button: { item: "button", props: { Button: BUTTON } },
41
+ card: { item: "card" },
42
+ checkbox: { item: "checkbox" },
43
+ input: { item: "input" },
44
+ kbd: { item: "kbd" },
45
+ pagination: { item: "pagination" },
46
+ progress: { item: "progress" },
47
+ "radio-group": { item: "radio-group" },
48
+ skeleton: { item: "skeleton" },
49
+ sonner: { item: "toast", module: "toast" },
50
+ toaster: { item: "toast", module: "toast" },
51
+ "use-toast": { item: "toast", module: "toast" },
52
+ switch: { item: "toggle", module: "toggle" },
53
+ table: { item: "table" },
54
+ tabs: { item: "tabs" },
55
+ textarea: { item: "textarea" },
56
+ "alert-dialog": { item: "modal", manual: 'Alert Dialog becomes Modal with role="alertdialog": ModalHeader, ModalBody and ModalFooter.' },
57
+ "aspect-ratio": { manual: "Use the native CSS aspect-ratio." },
58
+ avatar: { item: "avatar", manual: "Avatar takes name and src as props; AvatarImage and AvatarFallback go away." },
59
+ calendar: { item: "calendar", manual: "mode is single, multiple or range; check the selected value's shape." },
60
+ carousel: { item: "carousel", manual: "Rebuild with Carousel's own parts." },
61
+ chart: { item: "chart", manual: "Use Chart, LineChart, BarChart or DonutChart: native SVG, with no charting library." },
62
+ collapsible: { item: "accordion", manual: "Use an Accordion with one item, or a native <details>." },
63
+ combobox: { item: "combobox", manual: "Options are data passed as a prop." },
64
+ command: { item: "command-menu", manual: "Command becomes CommandMenu; its items are data." },
65
+ "context-menu": { item: "context-menu", manual: "Items are data passed as items, not children." },
66
+ dialog: { item: "modal", manual: "Dialog becomes Modal, with ModalHeader, ModalBody and ModalFooter." },
67
+ drawer: { item: "sheet", manual: 'Drawer becomes Sheet with side="bottom".' },
68
+ "dropdown-menu": { item: "dropdown-menu", manual: "Items are data passed as items, not children." },
69
+ form: { item: "input", manual: "Field wires the label, hint and error; keep your form library, Input forwards its ref." },
70
+ "hover-card": { item: "hover-card", manual: "Rebuild with HoverCard's props." },
71
+ "input-otp": { item: "otp-input", manual: "Input OTP becomes OtpInput." },
72
+ label: { item: "input", manual: "Pass label to Input, Select or Textarea; Field wires it." },
73
+ menubar: { item: "dropdown-menu", manual: "One DropdownMenu per menu." },
74
+ "navigation-menu": { item: "app-shell", manual: "For application navigation, use the app shell's Sidebar." },
75
+ popover: { item: "popover", manual: "The trigger and the content are props." },
76
+ resizable: { item: "resizable", manual: "Rebuild with Resizable's panels." },
77
+ "scroll-area": { manual: "Native scrolling: scroll areas inside components already use thin, themed scrollbars." },
78
+ select: { item: "select", manual: "Options are data; searchable, clearable and multiple are props." },
79
+ separator: { manual: 'Use the ml-divider primitive: <hr class="ml-divider" />.' },
80
+ sheet: { item: "sheet", manual: "Rebuild with Sheet's props: side, and the content as children." },
81
+ sidebar: { item: "app-shell", manual: "Sidebar, SidebarSection and SidebarItem, with a drawer on phones." },
82
+ slider: { item: "slider", manual: "value is a number, not an array of one." },
83
+ toggle: { item: "button", manual: 'Use Button with variant="subtle" or "secondary" and aria-pressed.' },
84
+ "toggle-group": { item: "segmented-control", manual: "Toggle Group becomes SegmentedControl; options are data." },
85
+ tooltip: { item: "tooltip", manual: "The text is the content prop; there is no TooltipContent." },
86
+ };
87
+
88
+ const SOURCE = new Set([".tsx", ".jsx", ".ts", ".js", ".mjs"]);
89
+
90
+ /** The project's components alias, from shadcn's components.json: "@/components/ui" unless it says otherwise. */
91
+ function uiAlias(cwd) {
92
+ try {
93
+ const config = JSON.parse(fs.readFileSync(path.join(cwd, "components.json"), "utf8"));
94
+ return config?.aliases?.ui ?? `${config?.aliases?.components ?? "@/components"}/ui`;
95
+ } catch {
96
+ return "@/components/ui";
97
+ }
98
+ }
99
+
100
+ let iconNames = null;
101
+ /** Every name @mlola-ui/icons exports. */
102
+ function mlolaIcons() {
103
+ const filename = path.join(BUNDLED_REGISTRY_ROOT, "icons.json");
104
+ iconNames ??= new Set(fs.existsSync(filename) ? (readJson(filename).names ?? []) : []);
105
+ return iconNames;
106
+ }
107
+
108
+ /** Glyphs lucide-react renamed, by their old names, as Mlola names them. */
109
+ const LUCIDE_RENAMED = { AlertTriangle: "TriangleAlert", AlertCircle: "CircleAlert", CheckCircle: "CircleCheck", CheckCircle2: "CircleCheck", XCircle: "CircleX", HelpCircle: "CircleHelp", Edit: "Pencil", Trash2: "Trash" };
110
+
111
+ /** The Mlola name a lucide-react import stands for (XIcon and LucideX are both X; LucideProps is IconProps), or null. */
112
+ function lucideName(name, icons) {
113
+ if (name === "LucideProps") return "IconProps";
114
+ const bare = name.replace(/^Lucide(?=[A-Z])/, "").replace(/(?<=.)Icon$/, "");
115
+ const renamed = LUCIDE_RENAMED[bare] ?? bare;
116
+ if (icons.has(renamed)) return renamed;
117
+ return icons.has(`Icon${renamed}`) ? `Icon${renamed}` : null;
118
+ }
119
+
120
+ /** A call's object of string literals ({ variant: "ghost", size: "sm" }), or null when anything in it is computed. */
121
+ function literalObject(text) {
122
+ const pairs = {};
123
+ const rest = text.replace(/(\w+)\s*:\s*(?:"([^"\\]*)"|'([^'\\]*)'|`([^`$\\]*)`)/g, (_, key, double, single, template) => {
124
+ pairs[key] = double ?? single ?? template;
125
+ return "";
126
+ });
127
+ return /^[\s,]*$/.test(rest) ? pairs : null;
128
+ }
129
+
130
+ /** The text between the parentheses of a call whose "(" is at `open`, and where it ends, or null when it does not close. */
131
+ function argumentsAt(source, open) {
132
+ let depth = 0;
133
+ let quote = null;
134
+ for (let index = open; index < source.length; index += 1) {
135
+ const char = source[index];
136
+ if (quote) {
137
+ if (char === quote && source[index - 1] !== "\\") quote = null;
138
+ } else if (char === '"' || char === "'" || char === "`") quote = char;
139
+ else if (char === "(") depth += 1;
140
+ else if (char === ")" && (depth -= 1) === 0) return { text: source.slice(open + 1, index), end: index };
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /** Where an import's specifiers are, and each one's imported and local name. */
146
+ function specifiersOf(clause) {
147
+ const named = /\{([\s\S]*)\}/.exec(clause);
148
+ if (!named) return null;
149
+ return named[1]
150
+ .split(",")
151
+ .map((part) => part.trim())
152
+ .filter(Boolean)
153
+ .map((part) => {
154
+ const [imported, local] = part.replace(/^type\s+/, "").split(/\s+as\s+/);
155
+ return { imported: imported.trim(), local: (local ?? imported).trim(), type: part.startsWith("type ") };
156
+ });
157
+ }
158
+
159
+ /** The end of the JSX opening tag that starts at `start` (its "<"), past strings and braces. */
160
+ function openingTagEnd(source, start) {
161
+ let depth = 0;
162
+ let quote = null;
163
+ for (let index = start + 1; index < source.length; index += 1) {
164
+ const char = source[index];
165
+ if (quote) {
166
+ if (char === quote && source[index - 1] !== "\\") quote = null;
167
+ } else if (char === '"' || char === "'" || char === "`") quote = char;
168
+ else if (char === "{") depth += 1;
169
+ else if (char === "}") depth -= 1;
170
+ else if (char === ">" && depth === 0) return index;
171
+ }
172
+ return -1;
173
+ }
174
+
175
+ const lineOf = (source, index) => source.slice(0, index).split("\n").length;
176
+
177
+ /**
178
+ * One file's migration: the source after it, each change made, and each
179
+ * thing left to do by hand, with its line. `alias` is the project's ui alias.
180
+ */
181
+ export function migrateSource(source, { alias = "@/components/ui", icons = mlolaIcons() } = {}) {
182
+ const changes = [];
183
+ const manual = [];
184
+ const items = new Set();
185
+ let needsIcons = false;
186
+ let output = source;
187
+ // Edits are collected against the original and applied from the end, so every index stays true.
188
+ const edits = [];
189
+ const edit = (start, end, text) => edits.push({ start, end, text });
190
+
191
+ const uiPath = new RegExp(`^(?:${alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|\\.{1,2}(?:/\\.\\.)*(?:/components)?/ui)/([a-z-]+)$`);
192
+ const localProps = new Map();
193
+
194
+ for (const match of source.matchAll(/import\s+([\s\S]*?)\s+from\s+(["'])([^"']+)\2/g)) {
195
+ const [whole, clause, quote, from] = match;
196
+ const fromAt = match.index + whole.lastIndexOf(from) - 1;
197
+ const line = lineOf(source, match.index);
198
+
199
+ if (from === "sonner") {
200
+ edit(fromAt, fromAt + from.length + 2, `${quote}${alias}/toast${quote}`);
201
+ changes.push({ line, text: `toast from "sonner" → "${alias}/toast"` });
202
+ items.add("toast");
203
+ continue;
204
+ }
205
+
206
+ if (from === "lucide-react") {
207
+ const specifiers = specifiersOf(clause);
208
+ if (!specifiers) continue;
209
+ // The glyphs Mlola has move; the rest stay on lucide-react, named, until someone picks theirs.
210
+ const moved = specifiers.filter((specifier) => lucideName(specifier.imported, icons));
211
+ const kept = specifiers.filter((specifier) => !lucideName(specifier.imported, icons));
212
+ if (!moved.length) {
213
+ manual.push({ line, text: `lucide-react: ${kept.map((specifier) => specifier.imported).join(", ")} ${kept.length === 1 ? "has" : "have"} no glyph of the same name in @mlola-ui/icons. Pick one at https://ui.mlola.com/assets, or keep lucide-react for ${kept.length === 1 ? "it" : "them"}.` });
214
+ continue;
215
+ }
216
+ // Written the way the project writes imports: one per line when it did, a semicolon when it uses them.
217
+ const multiline = clause.includes("\n");
218
+ const semicolon = source[match.index + whole.length] === ";" ? ";" : "";
219
+ const write = (list, module) => {
220
+ const names = list.map(({ imported, local, type }) => `${type ? "type " : ""}${imported === local ? imported : `${imported} as ${local}`}`);
221
+ const body = multiline ? `{\n${names.map((name) => ` ${name},`).join("\n")}\n}` : `{ ${names.join(", ")} }`;
222
+ return `import ${body} from ${quote}${module}${quote}`;
223
+ };
224
+ const mlola = moved.map(({ imported, local, type }) => ({ imported: lucideName(imported, icons), local, type }));
225
+ const statement = kept.length ? `${write(mlola, "@mlola-ui/icons")}${semicolon}\n${write(kept, "lucide-react")}` : write(mlola, "@mlola-ui/icons");
226
+ edit(match.index, match.index + whole.length, statement);
227
+ changes.push({ line, text: `${moved.length} ${moved.length === 1 ? "icon" : "icons"} from lucide-react → @mlola-ui/icons` });
228
+ if (kept.length) manual.push({ line, text: `lucide-react: ${kept.map((specifier) => specifier.imported).join(", ")} ${kept.length === 1 ? "has" : "have"} no glyph of the same name in @mlola-ui/icons, so ${kept.length === 1 ? "it stays" : "they stay"} on lucide-react. Pick ${kept.length === 1 ? "one" : "them"} at https://ui.mlola.com/assets.` });
229
+ needsIcons = true;
230
+ continue;
231
+ }
232
+
233
+ const module = uiPath.exec(from)?.[1];
234
+ if (!module) continue;
235
+ const entry = MODULES[module];
236
+ if (!entry) {
237
+ manual.push({ line, text: `${from}: Mlola has no component by this name. See ${GUIDE}#components.` });
238
+ continue;
239
+ }
240
+ if (entry.item) items.add(entry.item);
241
+ if (entry.manual) {
242
+ manual.push({ line, text: `${module}: ${entry.manual} ${GUIDE}#components` });
243
+ continue;
244
+ }
245
+ const specifiers = specifiersOf(clause) ?? [];
246
+ // cva helpers (buttonVariants) have no Mlola counterpart: variants are data-* attributes. Each call is named with what replaces it.
247
+ for (const specifier of specifiers.filter((entry) => /Variants$/.test(entry.imported))) {
248
+ const props = entry.props?.[specifier.imported.replace(/Variants$/, "").replace(/^./, (first) => first.toUpperCase())] ?? {};
249
+ for (const call of source.matchAll(new RegExp(`\\b${specifier.local}\\(`, "g"))) {
250
+ const args = argumentsAt(source, call.index + specifier.local.length);
251
+ const written = args ? literalObject(args.text.trim().replace(/^\{|\}$/g, "")) : null;
252
+ // A call with no variant draws the default one, which Mlola names: "default" is primary.
253
+ const given = written && props.variant?.default ? { variant: "default", ...written } : written;
254
+ const attributes = given
255
+ ? Object.entries(given).flatMap(([prop, value]) => props[prop]?.[value] ?? [[prop, value]]).map(([name, value]) => ` data-${name}="${value}"`).join("")
256
+ : ' data-variant="…"';
257
+ manual.push({ line: lineOf(source, call.index), text: `${specifier.local}(): Mlola has no class-string helper. Write className="ml-${module}"${attributes} on the element (keep its other classes), or use the component itself.` });
258
+ }
259
+ }
260
+ if (module === "use-toast" && specifiers.some((specifier) => specifier.imported === "useToast")) {
261
+ manual.push({ line, text: `useToast: Mlola's toast needs no hook. import { toast } from "${alias}/toast" and call it where the hook was read.` });
262
+ }
263
+ if (entry.module) {
264
+ edit(fromAt, fromAt + from.length + 2, `${quote}${from.replace(/[a-z-]+$/, entry.module)}${quote}`);
265
+ changes.push({ line, text: `"${from}" → "${from.replace(/[a-z-]+$/, entry.module)}"` });
266
+ }
267
+ for (const { imported, local } of specifiers) if (entry.props?.[imported]) localProps.set(local, entry.props[imported]);
268
+ }
269
+
270
+ // Props on the components imported above, where their values are written out.
271
+ for (const [local, props] of localProps) {
272
+ for (const match of source.matchAll(new RegExp(`<${local}(?=[\\s/>])`, "g"))) {
273
+ const end = openingTagEnd(source, match.index);
274
+ if (end < 0) continue;
275
+ const tag = source.slice(match.index, end + 1);
276
+ const line = lineOf(source, match.index);
277
+ for (const [prop, values] of Object.entries(props)) {
278
+ const attribute = new RegExp(`(\\s)${prop}=(?:"([^"]*)"|'([^']*)'|\\{\\s*["']([^"']*)["']\\s*\\}|(\\{[^}]*\\}))`).exec(tag);
279
+ if (!attribute) continue;
280
+ const value = attribute[2] ?? attribute[3] ?? attribute[4];
281
+ const at = match.index + attribute.index + attribute[1].length;
282
+ const length = attribute[0].length - attribute[1].length;
283
+ if (value === undefined) {
284
+ manual.push({ line, text: `<${local} ${prop}=${attribute[5]}>: the value is only known at runtime. Map it yourself: ${Object.entries(values).map(([from, to]) => `${from} → ${to.map(([name, next]) => `${name}="${next}"`).join(" ") || "(default)"}`).join(", ")}.` });
285
+ continue;
286
+ }
287
+ const next = values[value];
288
+ if (!next) continue;
289
+ const text = next.map(([name, nextValue]) => `${name}="${nextValue}"`).join(" ");
290
+ if (text === `${prop}="${value}"`) continue;
291
+ edit(text ? at : at - attribute[1].length, at + length, text);
292
+ changes.push({ line, text: `<${local} ${prop}="${value}"> → ${text ? `<${local} ${text}>` : `<${local}> (the default)`}` });
293
+ }
294
+ }
295
+ }
296
+
297
+ if (items.has("toast")) {
298
+ // sonner's toast.error is toast.danger.
299
+ for (const match of source.matchAll(/\btoast\.error\(/g)) {
300
+ edit(match.index, match.index + "toast.error".length, "toast.danger");
301
+ changes.push({ line: lineOf(source, match.index), text: "toast.error → toast.danger" });
302
+ }
303
+ // The older use-toast takes one object: toast({ title, description, variant }) is toast(title, { description }), by tone.
304
+ for (const match of source.matchAll(/\btoast\(\s*\{/g)) {
305
+ const open = match.index + "toast".length;
306
+ const args = argumentsAt(source, open);
307
+ const line = lineOf(source, match.index);
308
+ const given = args ? literalObject(args.text.trim().replace(/^\{|\}$/g, "")) : null;
309
+ const known = given && given.title !== undefined && Object.keys(given).every((key) => ["title", "description", "variant"].includes(key));
310
+ if (!known) {
311
+ manual.push({ line, text: 'toast({ … }): Mlola takes the message first. Write toast(title, { description }), and toast.danger(…) for variant: "destructive".' });
312
+ continue;
313
+ }
314
+ const call = `toast${given.variant === "destructive" ? ".danger" : ""}(${JSON.stringify(given.title)}${given.description !== undefined ? `, { description: ${JSON.stringify(given.description)} }` : ""})`;
315
+ edit(match.index, args.end + 1, call);
316
+ changes.push({ line, text: `toast({ title… }) → ${call.length > 60 ? `${call.slice(0, 57)}…` : call}` });
317
+ }
318
+ }
319
+
320
+ for (const { start, end, text } of edits.sort((a, b) => b.start - a.start)) output = output.slice(0, start) + text + output.slice(end);
321
+ return { output, changes: changes.sort((a, b) => a.line - b.line), manual: manual.sort((a, b) => a.line - b.line), items: [...items], needsIcons };
322
+ }
323
+
324
+ /** The project's own source files under the targets, leaving out the shadcn/ui components being replaced. */
325
+ function sourceFiles(cwd, targets, alias) {
326
+ const uiDirectory = alias.replace(/^[@~]\//, "").replace(/^\.\//, "");
327
+ const roots = [uiDirectory, `src/${uiDirectory}`];
328
+ return listFiles(cwd, targets).filter((file) => {
329
+ const normalized = file.split(path.sep).join("/");
330
+ return SOURCE.has(path.extname(file)) && !roots.some((root) => normalized.startsWith(`${root}/`)) && !/\.d\.ts$/.test(file);
331
+ });
332
+ }
333
+
334
+ /** Reads (and with `write`, rewrites) every source file, and returns what it found. */
335
+ export function migrateProject(cwd, { targets = ["."], write = false } = {}) {
336
+ const alias = uiAlias(cwd);
337
+ const files = [];
338
+ const items = new Set();
339
+ let needsIcons = false;
340
+ for (const file of sourceFiles(cwd, targets, alias)) {
341
+ const absolute = path.join(cwd, file);
342
+ const source = fs.readFileSync(absolute, "utf8");
343
+ if (!/components\/ui\/|lucide-react|["']sonner["']/.test(source)) continue;
344
+ const result = migrateSource(source, { alias });
345
+ if (!result.changes.length && !result.manual.length && !result.items.length) continue;
346
+ result.items.forEach((item) => items.add(item));
347
+ needsIcons ||= result.needsIcons;
348
+ if (write && result.output !== source) fs.writeFileSync(absolute, result.output);
349
+ files.push({ file, changes: result.changes, manual: result.manual });
350
+ }
351
+ return { alias, files, items: [...items].sort(), needsIcons, written: write };
352
+ }
353
+
354
+ export function printMigration(result, output = console) {
355
+ const changes = result.files.reduce((sum, file) => sum + file.changes.length, 0);
356
+ const manual = result.files.reduce((sum, file) => sum + file.manual.length, 0);
357
+ if (!result.files.length) {
358
+ output.log("Nothing to migrate: no file imports shadcn/ui components, sonner or lucide-react.");
359
+ return;
360
+ }
361
+ for (const file of result.files) {
362
+ if (!file.changes.length) continue;
363
+ output.log(file.file);
364
+ for (const change of file.changes) output.log(` ${String(change.line).padStart(4)} ${change.text}`);
365
+ }
366
+ if (manual) {
367
+ output.log(`\nTo do by hand (${manual}):`);
368
+ for (const file of result.files) for (const entry of file.manual) output.log(` ${file.file}:${entry.line} ${entry.text}`);
369
+ }
370
+ output.log("");
371
+ if (result.written) output.log(`✓ Made ${changes} ${changes === 1 ? "change" : "changes"} in ${result.files.filter((file) => file.changes.length).length} files.`);
372
+ else if (changes) output.log(`${changes} ${changes === 1 ? "change" : "changes"} to make. Nothing was written: run again with --write to make them.`);
373
+ if (result.items.length) {
374
+ output.log(`${result.written ? "Next" : "Then"}, put Mlola's components where shadcn/ui's are (the same files):`);
375
+ output.log(` npx mlola-ui add ${result.items.join(" ")} --overwrite`);
376
+ }
377
+ if (result.needsIcons) output.log(" npm install @mlola-ui/icons");
378
+ output.log(`And check what you have: npx mlola-ui check. The guide: ${GUIDE}`);
379
+ }