mlola-ui 1.0.13 → 1.1.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 (48) hide show
  1. package/package.json +3 -2
  2. package/registry/agents.md +2 -0
  3. package/registry/contract.json +4 -0
  4. package/registry/examples.json +29 -17
  5. package/registry/index.json +218 -137
  6. package/registry/source/packages/components/_internal/dialog.ts +30 -35
  7. package/registry/source/packages/components/_internal/dom.ts +14 -0
  8. package/registry/source/packages/components/_internal/floating.ts +13 -16
  9. package/registry/source/packages/components/_internal/media.ts +34 -0
  10. package/registry/source/packages/components/_internal/react.ts +43 -0
  11. package/registry/source/packages/components/accordion/accordion.tsx +11 -7
  12. package/registry/source/packages/components/activity-heatmap/activity-heatmap.tsx +2 -1
  13. package/registry/source/packages/components/alert/alert.tsx +2 -1
  14. package/registry/source/packages/components/app-shell/app-shell.tsx +8 -16
  15. package/registry/source/packages/components/avatar/avatar.tsx +4 -3
  16. package/registry/source/packages/components/button/button.tsx +4 -1
  17. package/registry/source/packages/components/card/card.tsx +8 -4
  18. package/registry/source/packages/components/carousel/carousel.tsx +2 -9
  19. package/registry/source/packages/components/color-picker/color-picker.tsx +15 -12
  20. package/registry/source/packages/components/combobox/combobox.tsx +18 -13
  21. package/registry/source/packages/components/command-menu/command-menu.tsx +5 -5
  22. package/registry/source/packages/components/context-menu/context-menu.tsx +1 -1
  23. package/registry/source/packages/components/date-picker/date-picker.tsx +12 -9
  24. package/registry/source/packages/components/empty-state/empty-state.tsx +5 -3
  25. package/registry/source/packages/components/hover-card/hover-card.tsx +3 -7
  26. package/registry/source/packages/components/input/input.tsx +43 -5
  27. package/registry/source/packages/components/modal/modal.tsx +21 -9
  28. package/registry/source/packages/components/number-input/number-input.tsx +11 -7
  29. package/registry/source/packages/components/otp-input/otp-input.tsx +19 -10
  30. package/registry/source/packages/components/popover/popover.tsx +2 -1
  31. package/registry/source/packages/components/progress/progress.tsx +14 -11
  32. package/registry/source/packages/components/select/select.tsx +34 -11
  33. package/registry/source/packages/components/sheet/sheet.tsx +22 -11
  34. package/registry/source/packages/components/slider/slider.tsx +8 -8
  35. package/registry/source/packages/components/tabs/tabs.tsx +8 -7
  36. package/registry/source/packages/components/tag-input/tag-input.tsx +11 -7
  37. package/registry/source/packages/components/textarea/textarea.tsx +5 -4
  38. package/registry/source/packages/components/time-picker/time-picker.tsx +12 -9
  39. package/registry/source/packages/components/tooltip/tooltip.tsx +4 -3
  40. package/registry/source/packages/components/tour/tour.tsx +6 -8
  41. package/registry/source/packages/components/virtual-list/virtual-list.tsx +12 -15
  42. package/registry/source-manifest.json +36 -34
  43. package/src/cli.js +8 -1
  44. package/src/config.js +6 -2
  45. package/src/doctor.js +11 -1
  46. package/src/mcp.js +94 -26
  47. package/src/theme-build.js +60 -0
  48. package/src/theme-pull.js +1 -1
@@ -1,71 +1,73 @@
1
1
  {
2
2
  "packages/components/_internal/anchor.ts": "sha256-/yW3RVjoY6QfTWnUYSeZ9rFkC9W4WGB1qop8Jp/H1r8=",
3
3
  "packages/components/_internal/clipboard.ts": "sha256-0y5p88CTXWuOrUYlC5J4XP1VCrOwkx9jYWVHmD2LouQ=",
4
- "packages/components/_internal/dialog.ts": "sha256-796y3oNOVWarLvICTdi+d0/2axqSrzLVJiyBeh9X7bU=",
5
- "packages/components/_internal/floating.ts": "sha256-+WxgC4CtAlSr2qA1EkoDTgto535XLIPsvtOfdOK6CHk=",
4
+ "packages/components/_internal/dialog.ts": "sha256-dfOsfaszM/rhBJXvl7vtM4Vdh7SGflY5Capi1YoY6ow=",
5
+ "packages/components/_internal/dom.ts": "sha256-TJp94MipbXHonH/PC7FFHu5pnAqcoxzLocksWLsTAsc=",
6
+ "packages/components/_internal/floating.ts": "sha256-8iKD2nqqwTY0grZ+KDnP81CcChrQK4CDvlDvApl0fF4=",
6
7
  "packages/components/_internal/match.ts": "sha256-3tUYob6xmD2nLerh634E3R5iPbzMeNBzdDynsDFjj28=",
7
- "packages/components/_internal/react.ts": "sha256-SH63/haiB4Q93uFjMIMe5xZbaxENu3VI+GcidV8HELM=",
8
+ "packages/components/_internal/media.ts": "sha256-ziH0YZtu8y3X7OcDI6lYxpvGJCi9zQXq4dynG8n8U9c=",
9
+ "packages/components/_internal/react.ts": "sha256-INfds1J2f1MLpN6kBXz0GkfBEMIiRlhv0DXX455qAZo=",
8
10
  "packages/components/_internal/scroll.ts": "sha256-/YxhPPJa6Iax4q5eEs8dmpQp634TbvB+421MphEyscQ=",
9
- "packages/components/accordion/accordion.tsx": "sha256-0ALaLJd1fvj8Vbx86HLBKm3qUItrtwbrGJTykVfNSMg=",
10
- "packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-IAcU8FLAJbgKAS+2X+mt6PMqJBzxr4QVdwsVAl67BH4=",
11
+ "packages/components/accordion/accordion.tsx": "sha256-NodsdXiIL5VE7C2m3C0IC854lNUOIWgQQmmSKr6v59E=",
12
+ "packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-nHXmfiLpZOH+PrqE1/fyIjevjyPD2/8hSPZRn/+cYcY=",
11
13
  "packages/components/activity-heatmap/calendar.ts": "sha256-NB+AjXY5Xc+KzR8MMTRBgZxdzOeoHhkW0KQOW7WJPuQ=",
12
- "packages/components/alert/alert.tsx": "sha256-j680akXvCIh4MTVnUo79acvVrv51XyQ3UBJ5FBAy8Dw=",
13
- "packages/components/app-shell/app-shell.tsx": "sha256-XAq82XOTH3ZR+hxTkYFu8P7Ih2aI6ERy/TpXc2VMUwM=",
14
- "packages/components/avatar/avatar.tsx": "sha256-EjqC8ytKqRtsTVdK6vncaDfhO/i7ICe+jwaYCg5bjIw=",
14
+ "packages/components/alert/alert.tsx": "sha256-vQ0pvYOpLmafcglpwub6aHLjDPSP4N8qoTCwjo5aEi8=",
15
+ "packages/components/app-shell/app-shell.tsx": "sha256-slT7UclIZtl2xu7MALrR/i52BHGOR05eCwFTD1mh9AI=",
16
+ "packages/components/avatar/avatar.tsx": "sha256-61GyO7vZiTEe05z8si1gXKrzgm7I9kN+OFdgk0H8u4Y=",
15
17
  "packages/components/badge/badge.tsx": "sha256-flEH8S2jZ/64n3Y/ROhC9+9rvF7wXaN0xLcMMwqNOVQ=",
16
18
  "packages/components/breadcrumb/breadcrumb.tsx": "sha256-mxQpach2kFayT8H1vMt5252LQLxVV+sohdVXDhPuKoU=",
17
- "packages/components/button/button.tsx": "sha256-sXVANbGeFSYPHpiWuPvHHqMoPjG0zl0HSsKEdTgl5ME=",
19
+ "packages/components/button/button.tsx": "sha256-sMEaFncanTcURNkkrEH6MK87wrY/HtJSSzqpY8kxaK8=",
18
20
  "packages/components/calendar/calendar.tsx": "sha256-rWiS4YAueZfVoo6j5Fi3uN7A+yApe5U6HMm1woTVjys=",
19
21
  "packages/components/calendar/dates.ts": "sha256-mygYkoC70av2ZQ0vkxf885trwW97Wd10I4dptYQe+4A=",
20
- "packages/components/card/card.tsx": "sha256-aMKbv5DWYYWq7GZAFR9O1fuYE0H61WCjkvEbRMOd894=",
21
- "packages/components/carousel/carousel.tsx": "sha256-Gsw0LhQsOuvdQN/F2KDGVC/gJq53+ifLs19u95RWCoU=",
22
+ "packages/components/card/card.tsx": "sha256-YNqJWotnswv0UiYGtmJV9KxylKopXC0EZD7hTt8eQ0E=",
23
+ "packages/components/carousel/carousel.tsx": "sha256-WxvQoG9/u190ka4x6cpZT8ZS6XyhEaAlJZ/JyXG+ACU=",
22
24
  "packages/components/checkbox/checkbox.tsx": "sha256-hz4YbGwOxv/aJpJHrLXR4MKSWJGyjzoJEIGvdhyML9M=",
23
- "packages/components/color-picker/color-picker.tsx": "sha256-E+QB6MbSxg+3AZX3A7bOkst4arxgpmFL2ZmochoWCl8=",
25
+ "packages/components/color-picker/color-picker.tsx": "sha256-/UyEbxqknsHheX444Imc3WphyixZbvj8hGL+r+zyG5w=",
24
26
  "packages/components/color-picker/color.ts": "sha256-YqSgYjzn7XfPWAHuyzbwOhbg/ScaSfB7V73e9w+OjJs=",
25
- "packages/components/combobox/combobox.tsx": "sha256-CvuYc/hJoS7yw6QM9vv1SIkwflx0Kwd95UjchtC+AOs=",
26
- "packages/components/command-menu/command-menu.tsx": "sha256-mF1z92yrPY609pjx5Dt1l5EhGHl9EWV8F8Sq0hyJRw4=",
27
- "packages/components/context-menu/context-menu.tsx": "sha256-YgYsIzvLGFcJG1XX5zFJwrid5Z1Kn0lyWVzmLKmbMVU=",
27
+ "packages/components/combobox/combobox.tsx": "sha256-fiK8UpFpI5G1tZGFXiUmjMvCWcL8aW7qy9FUaMmAqhw=",
28
+ "packages/components/command-menu/command-menu.tsx": "sha256-wZnlj17zVlcfp5I08ziggHtchfdqjlvNl+MyiP+Rdbo=",
29
+ "packages/components/context-menu/context-menu.tsx": "sha256-OGaKzthZJGaV5T+4sRWYecA64wEEg2KImKnIzMMg1i4=",
28
30
  "packages/components/copy-button/copy-button.tsx": "sha256-6fvfifoiDOtW+WxKWon4SGEn3THtERuCX2hqGSZDtVM=",
29
- "packages/components/date-picker/date-picker.tsx": "sha256-rsmDweD3RVfKTXR2i+/pnTb/MsrDAZXmHAYBvcUGxig=",
31
+ "packages/components/date-picker/date-picker.tsx": "sha256-QABmZzPKHc706/Rs+ln3ifDK3T2lXl34rehhuRXW+8o=",
30
32
  "packages/components/dropdown-menu/dropdown-menu.tsx": "sha256-xgybCu9HBxIieafu5CjAtY4aCOQoaAj/VUCH6D6vLP8=",
31
33
  "packages/components/dropzone/dropzone.tsx": "sha256-tHCCpEWE6839UeLOszTo+R2vAL4Sp9UxuRdLkFLfnq4=",
32
- "packages/components/empty-state/empty-state.tsx": "sha256-uCKqYpr9vkozXZHpdl4GFtdX9ijcGOriCHvgyarBqRw=",
33
- "packages/components/hover-card/hover-card.tsx": "sha256-AyRbxfqDOYBgGlgjgrtQZm5DipArLEzQn6XnzjPd0ro=",
34
- "packages/components/input/input.tsx": "sha256-lbbheo+ydQTzyujTf0W7DD96TogOjifCyqj9c3h3W0g=",
34
+ "packages/components/empty-state/empty-state.tsx": "sha256-/phk3A6TDaiqo61fCcAc/4Bi85e1yBGAtKXFaGhrjyw=",
35
+ "packages/components/hover-card/hover-card.tsx": "sha256-Bu5m03mFjiM7x38U1aSrcb9Th8RLYozyjwW/Cu3knlk=",
36
+ "packages/components/input/input.tsx": "sha256-K0YrpEsEJZTzw64t+Cyg/OGYwlU6LU1/yXTNUxYUIAI=",
35
37
  "packages/components/kbd/kbd.tsx": "sha256-e8EO2Gw6JiUyrU0YcEZOaQNMel1pe/EcJrxwwgd1Ggk=",
36
- "packages/components/modal/modal.tsx": "sha256-rIytVY+hz8aabJ53UGQ0VADgSlW0sAG920iMQaoak+0=",
37
- "packages/components/number-input/number-input.tsx": "sha256-BDOyvCNtMULD/HK7uWHsYbGg79lrRDrVqtGQQrYHpmM=",
38
+ "packages/components/modal/modal.tsx": "sha256-gqrMncs7lEgUjsWXXEl5FYIUoYAxj93gcVh1pZTuatQ=",
39
+ "packages/components/number-input/number-input.tsx": "sha256-mo7rVoOQ4p/I0Hhx5w+SwXUtl3sI4VOt3QX9FwdB2q4=",
38
40
  "packages/components/number-input/number.ts": "sha256-IrHCQrEhI5ein9ifMFuSViSoxQUBoT3tNmmCu/paGGM=",
39
- "packages/components/otp-input/otp-input.tsx": "sha256-QURn8g5HqCQXBTdeENPbVxoPlqs3ZtoAPLRlFjQDxmI=",
41
+ "packages/components/otp-input/otp-input.tsx": "sha256-0KeyDLCC27/GqOocMWXggJ255PCEpCOCcINAcabksSk=",
40
42
  "packages/components/pagination/pagination.tsx": "sha256-FBwk8tq4JcsK2D0dlWEYiDSRx3owgBmGzEIlVRaNBoE=",
41
43
  "packages/components/password-input/password-input.tsx": "sha256-oY//vTILhUicJyfAWZrSUTl8WWsf4P2S1Bh0x4OxwLs=",
42
- "packages/components/popover/popover.tsx": "sha256-/kTiAuFklU4IuP97XaiCUGdEpKtNyMPLPS359mfzPHw=",
44
+ "packages/components/popover/popover.tsx": "sha256-NYKKNo0sO3jvR5W4iuIlnTw7B+xyOhMZczAtQG/lmw4=",
43
45
  "packages/components/priority-icon/labels.ts": "sha256-zhmJIb7/gS16wU+xCJ7hkReBi9q5RI3SMwdjkJvvvKU=",
44
46
  "packages/components/priority-icon/priority-icon.tsx": "sha256-3D6hFRpht3YuWouLhakE2DFdldavmJx0B/povEDwrtQ=",
45
- "packages/components/progress/progress.tsx": "sha256-9OjecRNPWn7Bfe+C5IyXaKsW1NJae6X5gco1o0X5XkE=",
47
+ "packages/components/progress/progress.tsx": "sha256-oDxj1Ccl1pKV4+e6A7aaJL1FThW8qs9r4Gb0hEmExeM=",
46
48
  "packages/components/radio-group/radio-group.tsx": "sha256-jhnSEBLkdpe8dv5us/Q1sMsQF4sKGRgDXWGKm2JSFew=",
47
49
  "packages/components/rating/rating.tsx": "sha256-zgPOQXzAiRE4qGNPQ8qat3SP1XSI/7567kA5VaT51P0=",
48
50
  "packages/components/resizable/resizable.tsx": "sha256-Yxlq88qElhv2Q5DooPdriZkawRS0QWx08nd6ybz5oeY=",
49
51
  "packages/components/segmented-control/segmented-control.tsx": "sha256-MCBG5XHFWCwbACctsYKbqe8mvSSR6pf60tyRgdCDKDA=",
50
- "packages/components/select/select.tsx": "sha256-m8QmKUGC4386XcklosSntal0XwUAFjKosTBQSF0dduQ=",
51
- "packages/components/sheet/sheet.tsx": "sha256-EIn3OeRDmXGHbYf/uihRh8aw3LmzarIFiyC9DUsL1jg=",
52
+ "packages/components/select/select.tsx": "sha256-WfS//CI0wnqhsQvglElqPK1MOL5vwjSaBnaC1BpWVVc=",
53
+ "packages/components/sheet/sheet.tsx": "sha256-gmc8rZ87NHfN/bErEWHm6HlaAoeNT/Fw6y2PrW88WFI=",
52
54
  "packages/components/skeleton/skeleton.tsx": "sha256-2Fk72NbLr3+CrXH7jGbHNsc3t+vskMqSyySe/UwbGCs=",
53
- "packages/components/slider/slider.tsx": "sha256-l+VrBqPYHIIcemiVixxwDxNDB0HEMmYRZekx/H5HroQ=",
55
+ "packages/components/slider/slider.tsx": "sha256-tqffrl4r0kxrVFqX1ExBHZKzenVFAZcOCQl8khJ/QEU=",
54
56
  "packages/components/spinner/spinner.tsx": "sha256-FZQmcGkQ8N0485n9bYsLTpTnolbM729YKjkQrXriz9A=",
55
57
  "packages/components/status-icon/labels.ts": "sha256-EE8q+geQBbJUs7I3RmaHZnPwn7Vjk2GjCfxvI4ruXKM=",
56
58
  "packages/components/status-icon/status-icon.tsx": "sha256-TtrTeoePyfH1VhKuWC0v12FuwfgND1CHnH5x8fyf4AM=",
57
59
  "packages/components/stepper/stepper.tsx": "sha256-uI/dRmkbijlixHiGUceWcpaDzpEixrzQ1UTmoPbGZAY=",
58
60
  "packages/components/table/table.tsx": "sha256-xJlWzCH76Lrgg/HrlnVTHkG5dfCkbzjWXEdTVG5Jk4w=",
59
- "packages/components/tabs/tabs.tsx": "sha256-kWAQjE9mshqZL7V3NJTUiurp6M5Nu3vx5O1LsIgdV5Y=",
60
- "packages/components/tag-input/tag-input.tsx": "sha256-lc4ofz1/lUnb7B/GlFElVKXLbPwtJFAjE5vWwAY99fA=",
61
- "packages/components/textarea/textarea.tsx": "sha256-C9jMYZ63w+m6bgdLvHjvULRuoCe26q5zbgS4+ahWzr8=",
62
- "packages/components/time-picker/time-picker.tsx": "sha256-og8Cbf+e8oFYewfagUgjRzLHSIIlAZh+1OpAn9yNq70=",
61
+ "packages/components/tabs/tabs.tsx": "sha256-Wp0WhVFPwaX3aIGXNJBbSGNWPMT6X/Sbl+kE+zZSyvk=",
62
+ "packages/components/tag-input/tag-input.tsx": "sha256-LnOwotLuPB06nlkdzckjDVhk03D6c/exJXJs2blZNRQ=",
63
+ "packages/components/textarea/textarea.tsx": "sha256-zculOGfrK4LVcWne3+jS+7qKcfJPM/25vJdm7AXOJfk=",
64
+ "packages/components/time-picker/time-picker.tsx": "sha256-HquGogkQB7YGs/1iTSqftSJ310hgXqKdAc+fU1RtQ5Y=",
63
65
  "packages/components/time-picker/times.ts": "sha256-V/4uqFZPn9gVnyx8cB9vwp/RocmTYaPmLxjfy8DPQOI=",
64
66
  "packages/components/timeline/timeline.tsx": "sha256-nVg7lBcAUAzqEb0YrHagJXSaHE2aWWE9bUbQMWQY6PU=",
65
67
  "packages/components/toast/toast.tsx": "sha256-f0DwN0XuV46fSsQf3i9RJcfQbAs2mSz7A8UJoIIICHg=",
66
68
  "packages/components/toggle/toggle.tsx": "sha256-8qPHeSliHNdzQS7OLJ47BrvRR75MYyUGJlK4s21XS/U=",
67
- "packages/components/tooltip/tooltip.tsx": "sha256-hplqwWF/ZqQQ9oyoErrOA3VjkRCDCbqwt5xgZOdsFcA=",
68
- "packages/components/tour/tour.tsx": "sha256-mrAE8aVDBVIibGvQah9Gh3mzIiWDFxB6aObzqFo5xXU=",
69
- "packages/components/virtual-list/virtual-list.tsx": "sha256-raGEFn4HlAOIohqZcy0DgptsnUIPS/6bWLYeTe+M9E8=",
69
+ "packages/components/tooltip/tooltip.tsx": "sha256-OEghEwKydt5SU7XNGuQxmmy+S7YyLKu02pPQeoNZQKw=",
70
+ "packages/components/tour/tour.tsx": "sha256-b2XJokzSio2N9MFDbAbXn6w2f7diJb1KWedSCycRMmE=",
71
+ "packages/components/virtual-list/virtual-list.tsx": "sha256-oA8xFCwUgskmM+symA4gZxFqSyBCpeKfyM5zHyJaYaQ=",
70
72
  "packages/components/virtual-list/virtual.ts": "sha256-PyDIj3C2Vwb1i4ZssxSlYjgKmZPBIFLZFNq8YCW2Z78="
71
73
  }
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ import { loadCatalogNames, loadRegistry, resolveItems } from "./registry.js";
14
14
  import { clearCredentials, fetchProItems, GUIDE_FILENAME, hostFrom, readCredentials, saveCredentials, verifyToken, writeProGuide, writeProStyles } from "./pro.js";
15
15
  import { serveMcp } from "./mcp.js";
16
16
  import { pullTheme } from "./theme-pull.js";
17
+ import { buildTheme } from "./theme-build.js";
17
18
 
18
19
  const HELP = `Mlola UI — source-copy components with Theme
19
20
 
@@ -28,6 +29,7 @@ Usage:
28
29
  mlola-ui list [--kind component|block|template|asset] [--json]
29
30
  mlola-ui doctor
30
31
  mlola-ui theme pull <theme-id | url> [--host <url>] [--overwrite]
32
+ mlola-ui theme build [--file mlola.theme.json]
31
33
 
32
34
  Examples:
33
35
  npx mlola-ui init (also tells your coding agents about Mlola)
@@ -37,6 +39,7 @@ Examples:
37
39
  npx mlola-ui add asset empty-inbox orb
38
40
  npx mlola-ui list --kind block
39
41
  npx mlola-ui theme pull https://ui.mlola.com/t/th-4k2x9qf7wz3m
42
+ npx mlola-ui theme build (renders your mlola.theme.json, offline)
40
43
  npx mlola-ui mcp (an MCP server for Claude Code, Cursor, Codex…)
41
44
  `;
42
45
 
@@ -301,7 +304,11 @@ export async function run(argv, options = {}) {
301
304
 
302
305
  if (command === "theme") {
303
306
  const [subcommand, reference] = args;
304
- if (subcommand !== "pull") throw new Error(`Unknown theme command "${subcommand ?? ""}". Try: mlola-ui theme pull <theme-id | url>`);
307
+ if (subcommand === "build") {
308
+ await buildTheme(cwd, { file: optionValue(args, "--file") ?? undefined, output });
309
+ return 0;
310
+ }
311
+ if (subcommand !== "pull") throw new Error(`Unknown theme command "${subcommand ?? ""}". Try: mlola-ui theme build, or mlola-ui theme pull <theme-id | url>`);
305
312
  await pullTheme(cwd, reference, {
306
313
  host: optionValue(args, "--host") ?? (process.env.MLOLA_STUDIO_URL || undefined),
307
314
  overwrite: hasFlag(args, "--overwrite"),
package/src/config.js CHANGED
@@ -35,7 +35,9 @@ export const DEFAULT_CONFIG = {
35
35
  };
36
36
 
37
37
  const REQUIRED_ALIASES = ["components", "blocks", "templates", "lib", "styles"];
38
- const THEMES = new Set(["graphite", "atelier", "machined", "aerogel", "nordic"]);
38
+ export const THEMES = new Set(["graphite", "atelier", "machined", "aerogel", "nordic"]);
39
+ /** A project theme's id, as mlola.theme.json and data-theme spell it. */
40
+ const THEME_ID = /^[a-z][a-z0-9-]{0,47}$/;
39
41
 
40
42
  export function configPath(cwd) {
41
43
  return path.join(cwd, CONFIG_FILENAME);
@@ -45,7 +47,9 @@ export function validateConfig(config) {
45
47
  const errors = [];
46
48
  if (!config || typeof config !== "object") return ["configuration must be an object"];
47
49
  if (config.version !== 1) errors.push(`version must be 1 (received ${config.version})`);
48
- if (!THEMES.has(config.theme)) errors.push(`theme must be one of ${[...THEMES].join(", ")}`);
50
+ if (typeof config.theme !== "string" || !(THEMES.has(config.theme) || THEME_ID.test(config.theme))) {
51
+ errors.push(`theme must be one of ${[...THEMES].join(", ")}, or your own theme's id from mlola.theme.json (lowercase letters, digits and dashes)`);
52
+ }
49
53
  for (const key of REQUIRED_ALIASES) {
50
54
  if (typeof config.aliases?.[key] !== "string" || !config.aliases[key]) {
51
55
  errors.push(`aliases.${key} must be a non-empty string`);
package/src/doctor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { interpolateTarget, loadConfig, targetRoot } from "./config.js";
3
+ import { THEMES, interpolateTarget, loadConfig, targetRoot } from "./config.js";
4
4
  import { createTransformContext, expectedFile } from "./installer.js";
5
5
  import { loadRegistry, readJson, sha256, sourcePath } from "./registry.js";
6
6
 
@@ -110,6 +110,16 @@ export function runDoctor(cwd) {
110
110
  }
111
111
  }
112
112
 
113
+ // A project theme is rendered into the stylesheet, by `theme build` or `theme pull`.
114
+ if (!THEMES.has(config.theme)) {
115
+ const themeCss = path.join(cwd, targetRoot(config, "styles"), "mlola", "theme.css");
116
+ if (fs.existsSync(themeCss) && fs.readFileSync(themeCss, "utf8").includes(`[data-theme="${config.theme}"]`)) {
117
+ findings.push(finding("pass", `the project theme "${config.theme}" is rendered in ${path.relative(cwd, themeCss)}`));
118
+ } else {
119
+ findings.push(finding("warn", `the theme "${config.theme}" is not rendered yet; run mlola-ui theme build`));
120
+ }
121
+ }
122
+
113
123
  if (projectContains(cwd, /\bdata-theme\s*=/)) {
114
124
  findings.push(finding("pass", "data-theme is present in application source"));
115
125
  } else if (projectContains(cwd, /\bdata-skin\s*=/)) {
package/src/mcp.js CHANGED
@@ -23,17 +23,13 @@ const PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05
23
23
  const INSTRUCTIONS = `This project's UI is Mlola UI: native CSS classes (ml-*), data-* attributes for state and variant, and --ml-* tokens. There is no Tailwind.
24
24
  Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens instead of writing colors, sizes, shadows or durations. After writing markup, run check_markup on it and fix what it reports.`;
25
25
 
26
+ const REMOTE_INSTRUCTIONS = `Mlola UI is a component system on native CSS classes (ml-*), data-* attributes for state and variant, and --ml-* tokens. There is no Tailwind.
27
+ Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens. After writing markup, run check_markup and fix what it reports. This server cannot write files: get_install_command gives the commands to run in the project, and Mlola Pro items need a license token (npx mlola-ui login).`;
28
+
26
29
  const text = (value) => ({ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] });
27
30
 
28
- function tools({ cwd, run }) {
29
- const capture = async (argv) => {
30
- const lines = [];
31
- const output = { log: (...parts) => lines.push(parts.join(" ")), error: (...parts) => lines.push(parts.join(" ")) };
32
- // stdout is this server's protocol channel: a package manager started by
33
- // init or add writes to stderr and reads nothing.
34
- const code = await run(argv, { cwd, output, installStdio: ["ignore", 2, 2] });
35
- return { code, output: lines.join("\n") };
36
- };
31
+ /** Tools that only read the registry: the local and the remote server share them. */
32
+ function readTools({ cwd }) {
37
33
  return [
38
34
  {
39
35
  name: "get_design_rules",
@@ -100,6 +96,20 @@ function tools({ cwd, run }) {
100
96
  return text(issues.length ? issues : "No issues: every Mlola class exists and every data-* value is one its element reacts to.");
101
97
  },
102
98
  },
99
+ ];
100
+ }
101
+
102
+ function tools({ cwd, run }) {
103
+ const capture = async (argv) => {
104
+ const lines = [];
105
+ const output = { log: (...parts) => lines.push(parts.join(" ")), error: (...parts) => lines.push(parts.join(" ")) };
106
+ // stdout is this server's protocol channel: a package manager started by
107
+ // init or add writes to stderr and reads nothing.
108
+ const code = await run(argv, { cwd, output, installStdio: ["ignore", 2, 2] });
109
+ return { code, output: lines.join("\n") };
110
+ };
111
+ return [
112
+ ...readTools({ cwd }),
103
113
  {
104
114
  name: "add_components",
105
115
  title: "Install Mlola components",
@@ -169,26 +179,27 @@ function prompt(name, args) {
169
179
  };
170
180
  }
171
181
 
172
- export async function serveMcp({ cwd, run, version, input = process.stdin, output = process.stdout }) {
173
- const available = tools({ cwd, run });
174
- const send = (message) => output.write(`${JSON.stringify(message)}\n`);
175
- const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
176
- const fail = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
177
-
178
- async function handle(message) {
182
+ /**
183
+ * The protocol, apart from any transport: takes one JSON-RPC message and
184
+ * resolves to the reply, or to null for a notification. The stdio server
185
+ * here and the site's HTTP endpoint both answer through it.
186
+ */
187
+ export function createMcpHandler({ tools: available, listResources, readResource: read, version, instructions }) {
188
+ const reply = (id, result) => ({ jsonrpc: "2.0", id, result });
189
+ const fail = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
190
+ return async function handle(message) {
179
191
  const { id, method, params } = message ?? {};
180
192
  const isRequest = id !== undefined && id !== null;
181
193
  try {
182
194
  switch (method) {
183
195
  case "initialize": {
184
196
  const requested = params?.protocolVersion;
185
- reply(id, {
197
+ return reply(id, {
186
198
  protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
187
199
  capabilities: { tools: { listChanged: false }, resources: { listChanged: false }, prompts: { listChanged: false } },
188
200
  serverInfo: { name: "mlola-ui", title: "Mlola UI", version },
189
- instructions: INSTRUCTIONS,
201
+ instructions,
190
202
  });
191
- return;
192
203
  }
193
204
  case "ping":
194
205
  return reply(id, {});
@@ -205,23 +216,76 @@ export async function serveMcp({ cwd, run, version, input = process.stdin, outpu
205
216
  }
206
217
  }
207
218
  case "resources/list":
208
- return reply(id, { resources: resources(cwd) });
219
+ return reply(id, { resources: listResources() });
209
220
  case "resources/templates/list":
210
221
  return reply(id, { resourceTemplates: [] });
211
222
  case "resources/read":
212
- return reply(id, { contents: [{ uri: params?.uri, mimeType: "text/markdown", text: readResource(params?.uri, cwd) }] });
223
+ return reply(id, { contents: [{ uri: params?.uri, mimeType: "text/markdown", text: read(params?.uri) }] });
213
224
  case "prompts/list":
214
225
  return reply(id, { prompts: PROMPTS });
215
226
  case "prompts/get":
216
227
  return reply(id, prompt(params?.name, params?.arguments));
217
228
  default:
218
- if (method?.startsWith("notifications/")) return;
219
- if (isRequest) fail(id, -32601, `Method not found: ${method}`);
229
+ if (method?.startsWith("notifications/")) return null;
230
+ return isRequest ? fail(id, -32601, `Method not found: ${method}`) : null;
220
231
  }
221
232
  } catch (error) {
222
- if (isRequest) fail(id, error.code ?? -32603, error.message);
233
+ return isRequest ? fail(id, error.code ?? -32603, error.message) : null;
223
234
  }
224
- }
235
+ };
236
+ }
237
+
238
+ /**
239
+ * The server ui.mlola.com/mcp runs: the read-only tools, and install
240
+ * commands instead of installs, since a remote server cannot write to the
241
+ * project. Mlola Pro items are described; their source needs a license and
242
+ * arrives through `npx mlola-ui add`.
243
+ */
244
+ export function remoteMcpHandler({ version }) {
245
+ const cwd = process.cwd();
246
+ const install = {
247
+ name: "get_install_command",
248
+ title: "How to install Mlola components",
249
+ description: "The commands to run in the project to set Mlola up and add components, blocks, pages or templates, including the license step for Mlola Pro items.",
250
+ inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" }, minItems: 1 } }, required: ["names"], additionalProperties: false },
251
+ annotations: { readOnlyHint: true },
252
+ handler: ({ names }) => {
253
+ const found = names.map((name) => searchItems({ query: name }).find((item) => item.name === name));
254
+ const unknown = names.filter((_, index) => !found[index]);
255
+ if (unknown.length) return { ...text(`No Mlola item is called ${unknown.join(", ")}. Find names with search_components.`), isError: true };
256
+ const pro = found.filter((item) => item.tier === "pro").map((item) => item.name);
257
+ return text([
258
+ "Run in the project:",
259
+ "",
260
+ "npx mlola-ui init # once: config, stylesheet, the engine, and agent instructions",
261
+ ...(pro.length ? [`npx mlola-ui login <token> # ${pro.join(", ")} ${pro.length === 1 ? "is" : "are"} Mlola Pro: create a token at https://ui.mlola.com/account`] : []),
262
+ `npx mlola-ui add ${names.join(" ")}`,
263
+ "",
264
+ 'Then import styles/mlola/index.css once and set data-theme="graphite" (or atelier, machined, aerogel, nordic) on the root element.',
265
+ ].join("\n"));
266
+ },
267
+ };
268
+ return createMcpHandler({
269
+ tools: [...readTools({ cwd }), install],
270
+ listResources: () => [{ uri: "mlola://guide", name: "Mlola UI design guide", description: "Classes, attributes, tokens and rules, generated from the stylesheet.", mimeType: "text/markdown" }],
271
+ readResource: (uri) => {
272
+ if (uri === "mlola://guide") return designGuide() ?? "";
273
+ throw Object.assign(new Error(`Unknown resource ${uri}`), { code: -32002 });
274
+ },
275
+ version,
276
+ instructions: REMOTE_INSTRUCTIONS,
277
+ });
278
+ }
279
+
280
+ export async function serveMcp({ cwd, run, version, input = process.stdin, output = process.stdout }) {
281
+ const handle = createMcpHandler({
282
+ tools: tools({ cwd, run }),
283
+ listResources: () => resources(cwd),
284
+ readResource: (uri) => readResource(uri, cwd),
285
+ version,
286
+ instructions: INSTRUCTIONS,
287
+ });
288
+ const send = (message) => output.write(`${JSON.stringify(message)}\n`);
225
289
 
226
290
  const lines = readline.createInterface({ input, crlfDelay: Infinity });
227
291
  const pending = new Set();
@@ -235,7 +299,11 @@ export async function serveMcp({ cwd, run, version, input = process.stdin, outpu
235
299
  continue;
236
300
  }
237
301
  for (const entry of Array.isArray(message) ? message : [message]) {
238
- const task = handle(entry).finally(() => pending.delete(task));
302
+ const task = handle(entry)
303
+ .then((response) => {
304
+ if (response) send(response);
305
+ })
306
+ .finally(() => pending.delete(task));
239
307
  pending.add(task);
240
308
  }
241
309
  }
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { pathToFileURL } from "node:url";
5
+ import { DEFAULT_CONFIG, loadConfig, targetRoot } from "./config.js";
6
+ import { ensureImport } from "./theme-pull.js";
7
+
8
+ /**
9
+ * `mlola-ui theme build`: render the project's own theme, offline.
10
+ *
11
+ * Reads mlola.theme.json (written by hand or pulled from the Studio) and
12
+ * renders it with the engine installed in the project, the same function
13
+ * that renders every canonical theme, so its palette is solved for contrast
14
+ * the same way. Writes <styles>/mlola/theme.css, imported from index.css,
15
+ * which is where `theme pull` puts a Studio theme.
16
+ */
17
+
18
+ export const THEME_FILENAME = "mlola.theme.json";
19
+
20
+ /** The engine's theme renderer, from the project's own node_modules. */
21
+ async function projectEngine(cwd, engine) {
22
+ const require = createRequire(path.join(cwd, "package.json"));
23
+ let resolved;
24
+ try {
25
+ resolved = require.resolve(`${engine}/theme`);
26
+ } catch {
27
+ throw new Error(`Building a theme needs ${engine} 1.1 or later in this project. Install it: npm install ${engine}@latest`);
28
+ }
29
+ return import(pathToFileURL(resolved).href);
30
+ }
31
+
32
+ export async function buildTheme(cwd, { file = THEME_FILENAME, output = console, loadEngine = projectEngine } = {}) {
33
+ const themeFile = path.resolve(cwd, file);
34
+ if (!fs.existsSync(themeFile)) {
35
+ throw new Error(`No ${file} in ${cwd}. Write one (https://ui.mlola.com/docs/theming#custom) or pull one from the Studio: mlola-ui theme pull <theme-id>`);
36
+ }
37
+ let spec;
38
+ try {
39
+ spec = JSON.parse(fs.readFileSync(themeFile, "utf8"));
40
+ } catch (error) {
41
+ throw new Error(`${file} is not valid JSON: ${error.message}`);
42
+ }
43
+
44
+ const config = fs.existsSync(path.join(cwd, "mlola.config.json")) ? loadConfig(cwd) : DEFAULT_CONFIG;
45
+ const engine = await loadEngine(cwd, config.engine?.engine ?? "@mlola-ui/engine");
46
+ const { id } = engine.defineTheme(spec);
47
+ const css = engine.renderThemeCss(spec);
48
+
49
+ const stylesDirectory = path.join(cwd, targetRoot(config, "styles"), "mlola");
50
+ fs.mkdirSync(stylesDirectory, { recursive: true });
51
+ fs.writeFileSync(path.join(stylesDirectory, "theme.css"), css);
52
+ const imported = ensureImport(path.join(stylesDirectory, "index.css"), '@import "./theme.css";');
53
+
54
+ const stylesRelative = path.relative(cwd, stylesDirectory);
55
+ output.log(`✓ Built ${path.join(stylesRelative, "theme.css")} from ${path.relative(cwd, themeFile)}`);
56
+ if (imported) output.log(`✓ Imported it from ${path.join(stylesRelative, "index.css")}`);
57
+ if (config.theme !== id) output.log(`Set "theme": "${id}" in mlola.config.json, and data-theme="${id}" on <html> or any container.`);
58
+ else output.log(`Set data-theme="${id}" on <html> or any container.`);
59
+ return { id };
60
+ }
package/src/theme-pull.js CHANGED
@@ -45,7 +45,7 @@ async function fetchText(url, fetcher) {
45
45
  return response.text();
46
46
  }
47
47
 
48
- function ensureImport(indexFile, line) {
48
+ export function ensureImport(indexFile, line) {
49
49
  const current = fs.existsSync(indexFile) ? fs.readFileSync(indexFile, "utf8") : "";
50
50
  if (current.includes(line)) return false;
51
51
  fs.mkdirSync(path.dirname(indexFile), { recursive: true });