mlola-ui 1.0.2 → 1.0.4
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 +30 -3
- package/package.json +1 -1
- package/registry/agents.json +419 -0
- package/registry/agents.md +453 -0
- package/registry/assets.json +734 -110
- package/registry/catalog.json +1187 -0
- package/registry/contract.json +1064 -0
- package/registry/index.json +65 -65
- package/registry/source/packages/components/alert/alert.tsx +1 -1
- package/registry/source/packages/components/badge/badge.tsx +2 -2
- package/registry/source/packages/components/breadcrumb/breadcrumb.tsx +1 -1
- package/registry/source/packages/components/carousel/carousel.tsx +1 -0
- package/registry/source/packages/components/checkbox/checkbox.tsx +3 -2
- package/registry/source/packages/components/color-picker/color-picker.tsx +15 -13
- package/registry/source/packages/components/color-picker/color.ts +1 -1
- package/registry/source/packages/components/combobox/combobox.tsx +33 -6
- package/registry/source/packages/components/date-picker/date-picker.tsx +5 -3
- package/registry/source/packages/components/dropzone/dropzone.tsx +1 -1
- package/registry/source/packages/components/empty-state/empty-state.tsx +1 -1
- package/registry/source/packages/components/hover-card/hover-card.tsx +1 -1
- package/registry/source/packages/components/number-input/number-input.tsx +6 -5
- package/registry/source/packages/components/otp-input/otp-input.tsx +6 -4
- package/registry/source/packages/components/progress/progress.tsx +1 -1
- package/registry/source/packages/components/segmented-control/segmented-control.tsx +5 -3
- package/registry/source/packages/components/select/select.tsx +6 -5
- package/registry/source/packages/components/tag-input/tag-input.tsx +7 -6
- package/registry/source/packages/components/textarea/textarea.tsx +1 -1
- package/registry/source/packages/components/time-picker/time-picker.tsx +8 -5
- package/registry/source/packages/components/timeline/timeline.tsx +1 -1
- package/registry/source/packages/components/toast/toast.tsx +1 -1
- package/registry/source/packages/components/toggle/toggle.tsx +1 -0
- package/registry/source/packages/components/tour/tour.tsx +3 -3
- package/registry/source-manifest.json +24 -24
- package/src/agents.js +119 -0
- package/src/cli.js +29 -4
- package/src/knowledge.js +235 -0
- package/src/mcp.js +241 -0
- package/src/pro.js +1 -1
|
@@ -8,43 +8,43 @@
|
|
|
8
8
|
"packages/components/accordion/accordion.tsx": "sha256-0ALaLJd1fvj8Vbx86HLBKm3qUItrtwbrGJTykVfNSMg=",
|
|
9
9
|
"packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-fMGDlXKpOfIOy0hmbpR8hKrbbN3fC386tXK27A21ZwM=",
|
|
10
10
|
"packages/components/activity-heatmap/calendar.ts": "sha256-NB+AjXY5Xc+KzR8MMTRBgZxdzOeoHhkW0KQOW7WJPuQ=",
|
|
11
|
-
"packages/components/alert/alert.tsx": "sha256-
|
|
11
|
+
"packages/components/alert/alert.tsx": "sha256-j680akXvCIh4MTVnUo79acvVrv51XyQ3UBJ5FBAy8Dw=",
|
|
12
12
|
"packages/components/app-shell/app-shell.tsx": "sha256-XAq82XOTH3ZR+hxTkYFu8P7Ih2aI6ERy/TpXc2VMUwM=",
|
|
13
13
|
"packages/components/avatar/avatar.tsx": "sha256-hvZUGhatN95ZyX6p6NBhTRf6h1/rrJ+bJrqBMtIspq0=",
|
|
14
|
-
"packages/components/badge/badge.tsx": "sha256
|
|
15
|
-
"packages/components/breadcrumb/breadcrumb.tsx": "sha256-
|
|
14
|
+
"packages/components/badge/badge.tsx": "sha256-flEH8S2jZ/64n3Y/ROhC9+9rvF7wXaN0xLcMMwqNOVQ=",
|
|
15
|
+
"packages/components/breadcrumb/breadcrumb.tsx": "sha256-Mt9mvmwwDgdp5/+mj5byIrh8+hicsp7R6UcvUlK0yog=",
|
|
16
16
|
"packages/components/button/button.tsx": "sha256-khqYxcV3hlZ+Q+cYNTRXEpAfIijDFSvkuDJXMPtlk7g=",
|
|
17
17
|
"packages/components/calendar/calendar.tsx": "sha256-uE22LI4jpU8ErR/oBmAkeKiu+Rp0bz3IXZVCurycpwc=",
|
|
18
18
|
"packages/components/calendar/dates.ts": "sha256-mygYkoC70av2ZQ0vkxf885trwW97Wd10I4dptYQe+4A=",
|
|
19
19
|
"packages/components/card/card.tsx": "sha256-aMKbv5DWYYWq7GZAFR9O1fuYE0H61WCjkvEbRMOd894=",
|
|
20
|
-
"packages/components/carousel/carousel.tsx": "sha256-
|
|
21
|
-
"packages/components/checkbox/checkbox.tsx": "sha256-
|
|
22
|
-
"packages/components/color-picker/color-picker.tsx": "sha256-
|
|
23
|
-
"packages/components/color-picker/color.ts": "sha256-
|
|
24
|
-
"packages/components/combobox/combobox.tsx": "sha256-
|
|
20
|
+
"packages/components/carousel/carousel.tsx": "sha256-pamugpB6Ujt9Ylxd37jNF/ZwSb8iRkW/MoYe12nY3i4=",
|
|
21
|
+
"packages/components/checkbox/checkbox.tsx": "sha256-hz4YbGwOxv/aJpJHrLXR4MKSWJGyjzoJEIGvdhyML9M=",
|
|
22
|
+
"packages/components/color-picker/color-picker.tsx": "sha256-E+QB6MbSxg+3AZX3A7bOkst4arxgpmFL2ZmochoWCl8=",
|
|
23
|
+
"packages/components/color-picker/color.ts": "sha256-YqSgYjzn7XfPWAHuyzbwOhbg/ScaSfB7V73e9w+OjJs=",
|
|
24
|
+
"packages/components/combobox/combobox.tsx": "sha256-CvuYc/hJoS7yw6QM9vv1SIkwflx0Kwd95UjchtC+AOs=",
|
|
25
25
|
"packages/components/command-menu/command-menu.tsx": "sha256-mF1z92yrPY609pjx5Dt1l5EhGHl9EWV8F8Sq0hyJRw4=",
|
|
26
26
|
"packages/components/context-menu/context-menu.tsx": "sha256-YgYsIzvLGFcJG1XX5zFJwrid5Z1Kn0lyWVzmLKmbMVU=",
|
|
27
|
-
"packages/components/date-picker/date-picker.tsx": "sha256-
|
|
27
|
+
"packages/components/date-picker/date-picker.tsx": "sha256-rsmDweD3RVfKTXR2i+/pnTb/MsrDAZXmHAYBvcUGxig=",
|
|
28
28
|
"packages/components/dropdown-menu/dropdown-menu.tsx": "sha256-xgybCu9HBxIieafu5CjAtY4aCOQoaAj/VUCH6D6vLP8=",
|
|
29
|
-
"packages/components/dropzone/dropzone.tsx": "sha256-
|
|
30
|
-
"packages/components/empty-state/empty-state.tsx": "sha256-
|
|
31
|
-
"packages/components/hover-card/hover-card.tsx": "sha256-
|
|
29
|
+
"packages/components/dropzone/dropzone.tsx": "sha256-tHCCpEWE6839UeLOszTo+R2vAL4Sp9UxuRdLkFLfnq4=",
|
|
30
|
+
"packages/components/empty-state/empty-state.tsx": "sha256-uCKqYpr9vkozXZHpdl4GFtdX9ijcGOriCHvgyarBqRw=",
|
|
31
|
+
"packages/components/hover-card/hover-card.tsx": "sha256-AyRbxfqDOYBgGlgjgrtQZm5DipArLEzQn6XnzjPd0ro=",
|
|
32
32
|
"packages/components/input/input.tsx": "sha256-lbbheo+ydQTzyujTf0W7DD96TogOjifCyqj9c3h3W0g=",
|
|
33
33
|
"packages/components/kbd/kbd.tsx": "sha256-wrPqzn1e82zJYoKzO7u94Bgq4+nPc0DXLc8GJ/4/3nY=",
|
|
34
34
|
"packages/components/modal/modal.tsx": "sha256-vybxgBGYCrMYvJRnO1wDAc2VFQylu+mQIf5OZIcabX4=",
|
|
35
|
-
"packages/components/number-input/number-input.tsx": "sha256-
|
|
35
|
+
"packages/components/number-input/number-input.tsx": "sha256-BDOyvCNtMULD/HK7uWHsYbGg79lrRDrVqtGQQrYHpmM=",
|
|
36
36
|
"packages/components/number-input/number.ts": "sha256-IrHCQrEhI5ein9ifMFuSViSoxQUBoT3tNmmCu/paGGM=",
|
|
37
|
-
"packages/components/otp-input/otp-input.tsx": "sha256-
|
|
37
|
+
"packages/components/otp-input/otp-input.tsx": "sha256-QURn8g5HqCQXBTdeENPbVxoPlqs3ZtoAPLRlFjQDxmI=",
|
|
38
38
|
"packages/components/pagination/pagination.tsx": "sha256-FBwk8tq4JcsK2D0dlWEYiDSRx3owgBmGzEIlVRaNBoE=",
|
|
39
39
|
"packages/components/popover/popover.tsx": "sha256-/kTiAuFklU4IuP97XaiCUGdEpKtNyMPLPS359mfzPHw=",
|
|
40
40
|
"packages/components/priority-icon/labels.ts": "sha256-zhmJIb7/gS16wU+xCJ7hkReBi9q5RI3SMwdjkJvvvKU=",
|
|
41
41
|
"packages/components/priority-icon/priority-icon.tsx": "sha256-3D6hFRpht3YuWouLhakE2DFdldavmJx0B/povEDwrtQ=",
|
|
42
|
-
"packages/components/progress/progress.tsx": "sha256-
|
|
42
|
+
"packages/components/progress/progress.tsx": "sha256-9OjecRNPWn7Bfe+C5IyXaKsW1NJae6X5gco1o0X5XkE=",
|
|
43
43
|
"packages/components/radio-group/radio-group.tsx": "sha256-jhnSEBLkdpe8dv5us/Q1sMsQF4sKGRgDXWGKm2JSFew=",
|
|
44
44
|
"packages/components/rating/rating.tsx": "sha256-zgPOQXzAiRE4qGNPQ8qat3SP1XSI/7567kA5VaT51P0=",
|
|
45
45
|
"packages/components/resizable/resizable.tsx": "sha256-40sK2UyIR9Ek72KAu5nvbr44tKEY6JhC5Ftb++Wf6cI=",
|
|
46
|
-
"packages/components/segmented-control/segmented-control.tsx": "sha256-
|
|
47
|
-
"packages/components/select/select.tsx": "sha256-
|
|
46
|
+
"packages/components/segmented-control/segmented-control.tsx": "sha256-MCBG5XHFWCwbACctsYKbqe8mvSSR6pf60tyRgdCDKDA=",
|
|
47
|
+
"packages/components/select/select.tsx": "sha256-m8QmKUGC4386XcklosSntal0XwUAFjKosTBQSF0dduQ=",
|
|
48
48
|
"packages/components/sheet/sheet.tsx": "sha256-EIn3OeRDmXGHbYf/uihRh8aw3LmzarIFiyC9DUsL1jg=",
|
|
49
49
|
"packages/components/skeleton/skeleton.tsx": "sha256-2Fk72NbLr3+CrXH7jGbHNsc3t+vskMqSyySe/UwbGCs=",
|
|
50
50
|
"packages/components/slider/slider.tsx": "sha256-l+VrBqPYHIIcemiVixxwDxNDB0HEMmYRZekx/H5HroQ=",
|
|
@@ -53,15 +53,15 @@
|
|
|
53
53
|
"packages/components/stepper/stepper.tsx": "sha256-uI/dRmkbijlixHiGUceWcpaDzpEixrzQ1UTmoPbGZAY=",
|
|
54
54
|
"packages/components/table/table.tsx": "sha256-ed1Qn2sIpxRvGdPDAb5ih5AcXr5PmHTHdlrJLYDYRp8=",
|
|
55
55
|
"packages/components/tabs/tabs.tsx": "sha256-kWAQjE9mshqZL7V3NJTUiurp6M5Nu3vx5O1LsIgdV5Y=",
|
|
56
|
-
"packages/components/tag-input/tag-input.tsx": "sha256
|
|
57
|
-
"packages/components/textarea/textarea.tsx": "sha256-
|
|
58
|
-
"packages/components/time-picker/time-picker.tsx": "sha256-
|
|
56
|
+
"packages/components/tag-input/tag-input.tsx": "sha256-EK7xJTe3Y/S5Kn3vS0zBQg9lC+RVoGvky6HOAt+DnAw=",
|
|
57
|
+
"packages/components/textarea/textarea.tsx": "sha256-C9jMYZ63w+m6bgdLvHjvULRuoCe26q5zbgS4+ahWzr8=",
|
|
58
|
+
"packages/components/time-picker/time-picker.tsx": "sha256-og8Cbf+e8oFYewfagUgjRzLHSIIlAZh+1OpAn9yNq70=",
|
|
59
59
|
"packages/components/time-picker/times.ts": "sha256-V/4uqFZPn9gVnyx8cB9vwp/RocmTYaPmLxjfy8DPQOI=",
|
|
60
|
-
"packages/components/timeline/timeline.tsx": "sha256-
|
|
61
|
-
"packages/components/toast/toast.tsx": "sha256-
|
|
62
|
-
"packages/components/toggle/toggle.tsx": "sha256-
|
|
60
|
+
"packages/components/timeline/timeline.tsx": "sha256-nVg7lBcAUAzqEb0YrHagJXSaHE2aWWE9bUbQMWQY6PU=",
|
|
61
|
+
"packages/components/toast/toast.tsx": "sha256-FvZnwN6Lk/QEHmcNxo7sVlEvR3Lx54aT5cH9lwrthhE=",
|
|
62
|
+
"packages/components/toggle/toggle.tsx": "sha256-8qPHeSliHNdzQS7OLJ47BrvRR75MYyUGJlK4s21XS/U=",
|
|
63
63
|
"packages/components/tooltip/tooltip.tsx": "sha256-NbaOUmlUbBlUIdovchrWwH2QqzuvXB97DjnJnrHf4Gc=",
|
|
64
|
-
"packages/components/tour/tour.tsx": "sha256-
|
|
64
|
+
"packages/components/tour/tour.tsx": "sha256-mrAE8aVDBVIibGvQah9Gh3mzIiWDFxB6aObzqFo5xXU=",
|
|
65
65
|
"packages/components/virtual-list/virtual-list.tsx": "sha256-raGEFn4HlAOIohqZcy0DgptsnUIPS/6bWLYeTe+M9E8=",
|
|
66
66
|
"packages/components/virtual-list/virtual.ts": "sha256-PyDIj3C2Vwb1i4ZssxSlYjgKmZPBIFLZFNq8YCW2Z78="
|
|
67
67
|
}
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { designGuide } from "./knowledge.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tells the project's coding agents that the UI is Mlola, where the design
|
|
7
|
+
* language is written down, and how to ask about it, so an assistant builds
|
|
8
|
+
* with Mlola without being reminded:
|
|
9
|
+
*
|
|
10
|
+
* - `mlola.agents.md` the design guide: classes, tokens, rules (refreshed)
|
|
11
|
+
* - `AGENTS.md` a short section, between markers, read by most agents
|
|
12
|
+
* - `CLAUDE.md` imports AGENTS.md for Claude Code
|
|
13
|
+
* - `.mcp.json` the Mlola MCP server, for Claude Code and others
|
|
14
|
+
* - `.cursor/…` a rule and the MCP server, when the project uses Cursor
|
|
15
|
+
* - `.vscode/mcp.json` the MCP server, when the project uses VS Code
|
|
16
|
+
*
|
|
17
|
+
* Every write merges: anything else in these files is left as it was.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const GUIDE_FILE = "mlola.agents.md";
|
|
21
|
+
const START = "<!-- mlola-ui:start -->";
|
|
22
|
+
const END = "<!-- mlola-ui:end -->";
|
|
23
|
+
const SERVER = { command: "npx", args: ["-y", "mlola-ui", "mcp"] };
|
|
24
|
+
|
|
25
|
+
const BRIEF = `## UI: Mlola UI
|
|
26
|
+
|
|
27
|
+
This project's interface is built with Mlola UI. Before writing or changing UI:
|
|
28
|
+
|
|
29
|
+
- Read \`${GUIDE_FILE}\` (and \`mlola-pro.agents.md\` when it exists): every
|
|
30
|
+
class, attribute and token, and the rules for new UI.
|
|
31
|
+
- Compose from Mlola components first: \`npx mlola-ui list\`,
|
|
32
|
+
\`npx mlola-ui add <name>\`. Write new CSS only for what they do not cover.
|
|
33
|
+
- Never write a color, spacing, radius, shadow or duration by hand: read the
|
|
34
|
+
\`--ml-*\` tokens. There is no Tailwind or other styling framework here.
|
|
35
|
+
- State and variants go in \`data-*\` and \`aria-*\` with the shared words
|
|
36
|
+
(\`data-tone\`, \`data-variant\`, \`data-size\`, \`data-status\`), never in extra
|
|
37
|
+
classes. \`data-theme\` and \`data-mode\` belong to the engine.
|
|
38
|
+
- The \`mlola\` MCP server (\`npx mlola-ui mcp\`) searches components, explains
|
|
39
|
+
tokens and checks markup against the contract. Check new markup with its
|
|
40
|
+
\`check_markup\` tool before finishing.`;
|
|
41
|
+
|
|
42
|
+
/** Replaces the marked section, or appends it; returns what happened. */
|
|
43
|
+
function mergeSection(filename, body) {
|
|
44
|
+
const block = `${START}\n${body}\n${END}`;
|
|
45
|
+
if (!fs.existsSync(filename)) {
|
|
46
|
+
fs.writeFileSync(filename, `${block}\n`);
|
|
47
|
+
return "created";
|
|
48
|
+
}
|
|
49
|
+
const current = fs.readFileSync(filename, "utf8");
|
|
50
|
+
const start = current.indexOf(START);
|
|
51
|
+
const end = current.indexOf(END);
|
|
52
|
+
const next = start >= 0 && end > start ? `${current.slice(0, start)}${block}${current.slice(end + END.length)}` : `${current.replace(/\s*$/, "")}\n\n${block}\n`;
|
|
53
|
+
if (next === current) return "unchanged";
|
|
54
|
+
fs.writeFileSync(filename, next);
|
|
55
|
+
return "updated";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mergeJson(filename, update) {
|
|
59
|
+
let data = {};
|
|
60
|
+
if (fs.existsSync(filename)) {
|
|
61
|
+
try {
|
|
62
|
+
data = JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
63
|
+
} catch {
|
|
64
|
+
return "skipped (not valid JSON; add the server by hand)";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const next = update(structuredClone(data));
|
|
68
|
+
if (JSON.stringify(next) === JSON.stringify(data)) return "unchanged";
|
|
69
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
70
|
+
const existed = fs.existsSync(filename);
|
|
71
|
+
fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`);
|
|
72
|
+
return existed ? "updated" : "created";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function writeAgentFiles(cwd) {
|
|
76
|
+
const results = [];
|
|
77
|
+
const record = (file, status) => results.push({ file, status });
|
|
78
|
+
|
|
79
|
+
const guide = designGuide();
|
|
80
|
+
if (guide) {
|
|
81
|
+
const filename = path.join(cwd, GUIDE_FILE);
|
|
82
|
+
const before = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : null;
|
|
83
|
+
if (before !== guide) fs.writeFileSync(filename, guide);
|
|
84
|
+
record(GUIDE_FILE, before === null ? "created" : before === guide ? "unchanged" : "updated");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
record("AGENTS.md", mergeSection(path.join(cwd, "AGENTS.md"), BRIEF));
|
|
88
|
+
|
|
89
|
+
// Claude Code reads CLAUDE.md, which can import AGENTS.md rather than repeat it.
|
|
90
|
+
const claude = path.join(cwd, "CLAUDE.md");
|
|
91
|
+
const claudeText = fs.existsSync(claude) ? fs.readFileSync(claude, "utf8") : null;
|
|
92
|
+
if (claudeText === null) {
|
|
93
|
+
fs.writeFileSync(claude, "@AGENTS.md\n");
|
|
94
|
+
record("CLAUDE.md", "created");
|
|
95
|
+
} else if (!/^@AGENTS\.md\s*$/m.test(claudeText)) {
|
|
96
|
+
record("CLAUDE.md", mergeSection(claude, "@AGENTS.md"));
|
|
97
|
+
} else {
|
|
98
|
+
record("CLAUDE.md", "unchanged");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
record(".mcp.json", mergeJson(path.join(cwd, ".mcp.json"), (data) => ({ ...data, mcpServers: { ...(data.mcpServers ?? {}), mlola: SERVER } })));
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(path.join(cwd, ".cursor"))) {
|
|
104
|
+
record(".cursor/mcp.json", mergeJson(path.join(cwd, ".cursor", "mcp.json"), (data) => ({ ...data, mcpServers: { ...(data.mcpServers ?? {}), mlola: SERVER } })));
|
|
105
|
+
const rule = path.join(cwd, ".cursor", "rules", "mlola.mdc");
|
|
106
|
+
const text = `---\ndescription: Mlola UI, how interface code is built and styled in this project\nalwaysApply: true\n---\n\n${BRIEF.replace(/^## .*\n\n/, "")}\n`;
|
|
107
|
+
const before = fs.existsSync(rule) ? fs.readFileSync(rule, "utf8") : null;
|
|
108
|
+
if (before !== text) {
|
|
109
|
+
fs.mkdirSync(path.dirname(rule), { recursive: true });
|
|
110
|
+
fs.writeFileSync(rule, text);
|
|
111
|
+
}
|
|
112
|
+
record(".cursor/rules/mlola.mdc", before === null ? "created" : before === text ? "unchanged" : "updated");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (fs.existsSync(path.join(cwd, ".vscode"))) {
|
|
116
|
+
record(".vscode/mcp.json", mergeJson(path.join(cwd, ".vscode", "mcp.json"), (data) => ({ ...data, servers: { ...(data.servers ?? {}), mlola: { type: "stdio", ...SERVER } } })));
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -7,17 +7,21 @@ import {
|
|
|
7
7
|
targetRoot,
|
|
8
8
|
writeDefaultConfig,
|
|
9
9
|
} from "./config.js";
|
|
10
|
+
import { writeAgentFiles } from "./agents.js";
|
|
10
11
|
import { addAssets, assetHint, loadAssetIndex } from "./assets.js";
|
|
11
12
|
import { runDoctor } from "./doctor.js";
|
|
12
13
|
import { collectEngineDependencies, installItems } from "./installer.js";
|
|
13
14
|
import { loadCatalogNames, loadRegistry, resolveItems } from "./registry.js";
|
|
14
15
|
import { clearCredentials, fetchProItems, GUIDE_FILENAME, hostFrom, readCredentials, saveCredentials, verifyToken, writeProGuide, writeProStyles } from "./pro.js";
|
|
16
|
+
import { serveMcp } from "./mcp.js";
|
|
15
17
|
import { pullTheme } from "./theme-pull.js";
|
|
16
18
|
|
|
17
19
|
const HELP = `Mlola UI — source-copy components with Theme
|
|
18
20
|
|
|
19
21
|
Usage:
|
|
20
|
-
mlola-ui init [--force]
|
|
22
|
+
mlola-ui init [--force] [--no-agents]
|
|
23
|
+
mlola-ui agents
|
|
24
|
+
mlola-ui mcp
|
|
21
25
|
mlola-ui add <name...> [--overwrite] [--install|--yes]
|
|
22
26
|
mlola-ui add asset <id...> [--overwrite]
|
|
23
27
|
mlola-ui login <token> [--host <url>]
|
|
@@ -27,15 +31,25 @@ Usage:
|
|
|
27
31
|
mlola-ui theme pull <theme-id | url> [--host <url>] [--overwrite]
|
|
28
32
|
|
|
29
33
|
Examples:
|
|
30
|
-
npx mlola-ui init
|
|
34
|
+
npx mlola-ui init (also tells your coding agents about Mlola)
|
|
31
35
|
npx mlola-ui add button card
|
|
32
36
|
npx mlola-ui login mlp_… (Mlola Pro: a token from /account)
|
|
33
37
|
npx mlola-ui add conversation bot
|
|
34
38
|
npx mlola-ui add asset empty-inbox orb
|
|
35
39
|
npx mlola-ui list --kind block
|
|
36
40
|
npx mlola-ui theme pull https://ui.mlola.com/t/th-4k2x9qf7wz3m
|
|
41
|
+
npx mlola-ui mcp (an MCP server for Claude Code, Cursor, Codex…)
|
|
37
42
|
`;
|
|
38
43
|
|
|
44
|
+
const CLI_VERSION = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
45
|
+
|
|
46
|
+
function reportAgents(results, output) {
|
|
47
|
+
const changed = results.filter((entry) => entry.status !== "unchanged");
|
|
48
|
+
for (const { file, status } of changed) output.log(`✓ ${status[0].toUpperCase()}${status.slice(1)} ${file}`);
|
|
49
|
+
if (!changed.length) output.log("• Agent instructions are up to date");
|
|
50
|
+
output.log("Coding agents now know this UI is Mlola: AGENTS.md points them at mlola.agents.md, and .mcp.json starts the Mlola MCP server.");
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
function hasFlag(args, ...names) {
|
|
40
54
|
return args.some((argument) => names.includes(argument));
|
|
41
55
|
}
|
|
@@ -113,6 +127,17 @@ export async function run(argv, options = {}) {
|
|
|
113
127
|
output.log(
|
|
114
128
|
`Install the engines you use, starting with: npm install ${config.engine.engine ?? "@mlola-ui/engine"} ${config.engine.motion}`,
|
|
115
129
|
);
|
|
130
|
+
if (!hasFlag(args, "--no-agents")) reportAgents(writeAgentFiles(cwd), output);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (command === "agents") {
|
|
135
|
+
reportAgents(writeAgentFiles(cwd), output);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === "mcp") {
|
|
140
|
+
await serveMcp({ cwd, run, version: CLI_VERSION });
|
|
116
141
|
return 0;
|
|
117
142
|
}
|
|
118
143
|
|
|
@@ -167,7 +192,7 @@ export async function run(argv, options = {}) {
|
|
|
167
192
|
if (!token) throw new Error(`Usage: mlola-ui login <token>. Create a token at ${host}/account.`);
|
|
168
193
|
const { licence } = await verifyToken(host, token, fetcher);
|
|
169
194
|
const filename = saveCredentials(env, token, host === hostFrom(env) ? undefined : host);
|
|
170
|
-
output.log(`✓ Logged in to Mlola Pro (${licence.plan}
|
|
195
|
+
output.log(`✓ Logged in to Mlola Pro (${licence.plan} license). Token saved to ${filename}.`);
|
|
171
196
|
return 0;
|
|
172
197
|
}
|
|
173
198
|
|
|
@@ -212,7 +237,7 @@ export async function run(argv, options = {}) {
|
|
|
212
237
|
if (!credentials) {
|
|
213
238
|
throw new Error(
|
|
214
239
|
`${proNames.map((name) => `"${name}"`).join(", ")} ${proNames.length === 1 ? "is" : "are"} part of Mlola Pro. ` +
|
|
215
|
-
`With a
|
|
240
|
+
`With a license, create a token at ${host}/account and run "npx mlola-ui login <token>". See ${host}/pricing to get Pro.`,
|
|
216
241
|
);
|
|
217
242
|
}
|
|
218
243
|
const delivered = await fetchProItems(host, credentials.token, proNames, fetcher);
|
package/src/knowledge.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_CONFIG, interpolateTarget } from "./config.js";
|
|
4
|
+
import { BUNDLED_REGISTRY_ROOT, loadRegistry, readJson, sourcePath } from "./registry.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What the CLI knows about Mlola UI, as answers rather than files: every
|
|
8
|
+
* component and catalog item, the tokens, the rules for new UI, and the
|
|
9
|
+
* element contract markup is checked against. All of it is read from the
|
|
10
|
+
* registry bundled with this CLI, so an answer always matches the version
|
|
11
|
+
* that `add` installs, and it works offline.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
function bundled(name) {
|
|
16
|
+
if (!cache.has(name)) {
|
|
17
|
+
const filename = path.join(BUNDLED_REGISTRY_ROOT, name);
|
|
18
|
+
cache.set(name, fs.existsSync(filename) ? (name.endsWith(".json") ? readJson(filename) : fs.readFileSync(filename, "utf8")) : null);
|
|
19
|
+
}
|
|
20
|
+
return cache.get(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const KIND = { "registry:ui": "component", "registry:block": "block", "registry:page": "page", "registry:template": "template" };
|
|
24
|
+
|
|
25
|
+
/** Every item, free and Pro, in one shape. */
|
|
26
|
+
export function allItems() {
|
|
27
|
+
const registry = loadRegistry();
|
|
28
|
+
const free = registry.items.map((item) => ({
|
|
29
|
+
name: item.name,
|
|
30
|
+
kind: KIND[item.type] ?? "component",
|
|
31
|
+
tier: "free",
|
|
32
|
+
title: item.title,
|
|
33
|
+
description: item.description,
|
|
34
|
+
category: item.category,
|
|
35
|
+
tags: item.tags ?? [],
|
|
36
|
+
variants: item.variants ?? [],
|
|
37
|
+
sizes: item.sizes ?? [],
|
|
38
|
+
}));
|
|
39
|
+
const pro = (bundled("catalog.json")?.items ?? []).map((item) => ({
|
|
40
|
+
name: item.name,
|
|
41
|
+
kind: KIND[item.type] ?? "component",
|
|
42
|
+
tier: "pro",
|
|
43
|
+
title: item.title,
|
|
44
|
+
description: item.description,
|
|
45
|
+
category: item.category,
|
|
46
|
+
tags: item.tags ?? [],
|
|
47
|
+
variants: [],
|
|
48
|
+
sizes: [],
|
|
49
|
+
}));
|
|
50
|
+
return [...free, ...pro];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Items matching a query: every word must appear in the name, title, description, category or tags. */
|
|
54
|
+
export function searchItems({ query = "", kind, category, tier } = {}) {
|
|
55
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
56
|
+
return allItems()
|
|
57
|
+
.filter((item) => (!kind || item.kind === kind) && (!category || item.category === category) && (!tier || item.tier === tier))
|
|
58
|
+
.map((item) => {
|
|
59
|
+
const haystack = [item.name, item.title, item.description, item.category, ...item.tags].join(" ").toLowerCase();
|
|
60
|
+
const score = words.reduce((sum, word) => sum + (item.name === word ? 5 : item.name.includes(word) ? 3 : haystack.includes(word) ? 1 : -100), 0);
|
|
61
|
+
return { item, score };
|
|
62
|
+
})
|
|
63
|
+
.filter(({ score }) => score >= 0)
|
|
64
|
+
.sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name))
|
|
65
|
+
.map(({ item }) => item);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The classes a component styles (its own prefix) and the attributes each reacts to. */
|
|
69
|
+
function elementsOf(name) {
|
|
70
|
+
const contract = bundled("contract.json");
|
|
71
|
+
if (!contract) return [];
|
|
72
|
+
const prefix = `ml-${name}`;
|
|
73
|
+
return contract.classes
|
|
74
|
+
.filter((cls) => cls === prefix || cls.startsWith(`${prefix}-`))
|
|
75
|
+
.map((cls) => ({ class: cls, attributes: contract.elements[cls] ?? {} }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function projectConfig(cwd) {
|
|
79
|
+
try {
|
|
80
|
+
return readJson(path.join(cwd, "mlola.config.json"));
|
|
81
|
+
} catch {
|
|
82
|
+
return DEFAULT_CONFIG;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Everything an agent needs to use one item correctly. */
|
|
87
|
+
export function describeItem(name, { cwd = process.cwd(), includeSource = false } = {}) {
|
|
88
|
+
const registry = loadRegistry();
|
|
89
|
+
const item = registry.items.find((entry) => entry.name === name);
|
|
90
|
+
if (!item) {
|
|
91
|
+
const pro = (bundled("catalog.json")?.items ?? []).find((entry) => entry.name === name);
|
|
92
|
+
if (pro) {
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
tier: "pro",
|
|
96
|
+
kind: KIND[pro.type] ?? "component",
|
|
97
|
+
title: pro.title,
|
|
98
|
+
description: pro.description,
|
|
99
|
+
category: pro.category,
|
|
100
|
+
install: `npx mlola-ui login <token> # once, with a token from https://ui.mlola.com/account\nnpx mlola-ui add ${name}`,
|
|
101
|
+
note: "Part of Mlola Pro. Its source arrives with a license token; its classes and attributes are listed in mlola-pro.agents.md after the first Pro install.",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const close = allItems().filter((entry) => entry.name.includes(name) || name.includes(entry.name)).map((entry) => entry.name).slice(0, 5);
|
|
105
|
+
throw new Error(`No item named "${name}".${close.length ? ` Did you mean: ${close.join(", ")}?` : ""} Search with search_components.`);
|
|
106
|
+
}
|
|
107
|
+
const config = projectConfig(cwd);
|
|
108
|
+
const usage = item.usage
|
|
109
|
+
? { import: `import { ${item.usage.exportName} } from "${interpolateTarget(item.usage.importPath, config, "import")}";`, exportName: item.usage.exportName }
|
|
110
|
+
: null;
|
|
111
|
+
const result = {
|
|
112
|
+
name,
|
|
113
|
+
tier: "free",
|
|
114
|
+
kind: KIND[item.type] ?? "component",
|
|
115
|
+
title: item.title,
|
|
116
|
+
description: item.description,
|
|
117
|
+
category: item.category,
|
|
118
|
+
variants: item.variants ?? [],
|
|
119
|
+
sizes: item.sizes ?? [],
|
|
120
|
+
usage,
|
|
121
|
+
install: `npx mlola-ui add ${name}`,
|
|
122
|
+
dependsOn: item.registryDependencies ?? [],
|
|
123
|
+
engineDependencies: item.engineDependencies ?? {},
|
|
124
|
+
elements: elementsOf(name),
|
|
125
|
+
};
|
|
126
|
+
if (includeSource) {
|
|
127
|
+
result.source = (item.files ?? [])
|
|
128
|
+
.filter((file) => !file.path.includes("/_internal/"))
|
|
129
|
+
.map((file) => ({ path: interpolateTarget(file.target, config), content: fs.readFileSync(sourcePath(file.path), "utf8") }));
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function designData() {
|
|
135
|
+
return bundled("agents.json");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function designGuide() {
|
|
139
|
+
return bundled("agents.md");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function tokens(group) {
|
|
143
|
+
const data = designData();
|
|
144
|
+
if (!data) return [];
|
|
145
|
+
const groups = data.tokens;
|
|
146
|
+
if (!group) return groups;
|
|
147
|
+
const wanted = group.toLowerCase();
|
|
148
|
+
return groups.filter((entry) => entry.group.toLowerCase().includes(wanted) || entry.names.some((name) => name.includes(wanted)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function themes() {
|
|
152
|
+
const registry = loadRegistry();
|
|
153
|
+
return registry.themes ?? designData()?.themes ?? [];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ── Checking markup against the contract ─────────────────────────── */
|
|
157
|
+
|
|
158
|
+
const SHARED = new Set(["data-tone", "data-variant", "data-size", "data-state", "data-status"]);
|
|
159
|
+
const UTILITY = /^(-?(m|p)[trblxy]?-\d|flex$|grid$|block$|inline|hidden$|items-|justify-|gap-|w-|h-|min-|max-|text-(xs|sm|base|lg|xl|\d|[a-z]+-\d)|font-(bold|medium|semibold|light)|bg-|border-|rounded|shadow|ring-|space-[xy]-|leading-|tracking-|opacity-|z-\d|absolute$|relative$|fixed$|sticky$|overflow-|col-span|row-span|sm:|md:|lg:|xl:|hover:|focus:|dark:)/;
|
|
160
|
+
const COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Checks HTML or JSX against the element contract. It reads string-valued
|
|
164
|
+
* class, className, style and data-* attributes; expressions in braces are
|
|
165
|
+
* skipped, since their value is only known at runtime.
|
|
166
|
+
*/
|
|
167
|
+
export function checkMarkup(markup) {
|
|
168
|
+
const contract = bundled("contract.json");
|
|
169
|
+
const known = new Set(contract?.classes ?? []);
|
|
170
|
+
const proPrefixes = (bundled("catalog.json")?.items ?? []).map((item) => `ml-${item.name}`);
|
|
171
|
+
const themeIds = new Set(themes().map((theme) => theme.id));
|
|
172
|
+
const issues = [];
|
|
173
|
+
const note = (severity, tag, message, fix) => issues.push({ severity, element: tag.slice(0, 120), message, ...(fix ? { fix } : {}) });
|
|
174
|
+
|
|
175
|
+
for (const match of markup.matchAll(/<([A-Za-z][\w.-]*)((?:\s+(?:[^\s"'>={}]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}))?))*)\s*\/?>/g)) {
|
|
176
|
+
const tag = match[0];
|
|
177
|
+
const attributes = new Map();
|
|
178
|
+
for (const attribute of match[2].matchAll(/([^\s"'>={}]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\{(?:[^{}]|\{[^{}]*\})*\})))?/g)) {
|
|
179
|
+
const [, name, double, single, expression] = attribute;
|
|
180
|
+
const value = double ?? single ?? (expression ? /^\{\s*["'`]([^"'`$]*)["'`]\s*\}$/.exec(expression)?.[1] ?? null : "");
|
|
181
|
+
attributes.set(name, value);
|
|
182
|
+
if (expression) attributes.set(`${name}:raw`, expression);
|
|
183
|
+
}
|
|
184
|
+
const classValue = attributes.get("class") ?? attributes.get("className");
|
|
185
|
+
const classes = typeof classValue === "string" ? classValue.split(/\s+/).filter(Boolean) : [];
|
|
186
|
+
const mlola = classes.filter((cls) => cls.startsWith("ml-"));
|
|
187
|
+
|
|
188
|
+
for (const cls of mlola) {
|
|
189
|
+
if (known.has(cls) || proPrefixes.some((prefix) => cls === prefix || cls.startsWith(`${prefix}-`))) continue;
|
|
190
|
+
const base = [...known].find((candidate) => cls.startsWith(`${candidate}-`) && elementsOf(candidate.slice(3)).length);
|
|
191
|
+
note(
|
|
192
|
+
"error",
|
|
193
|
+
tag,
|
|
194
|
+
`"${cls}" is not a Mlola class.`,
|
|
195
|
+
base ? `Mlola has no variant classes: use "${base}" with a data-* attribute (data-variant, data-tone, data-size).` : "Search with search_components, or name your own element with your own prefix (not ml-).",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const utilities = classes.filter((cls) => !cls.startsWith("ml-") && UTILITY.test(cls));
|
|
199
|
+
if (utilities.length) {
|
|
200
|
+
note("warning", tag, `Utility classes (${utilities.slice(0, 4).join(" ")}) do nothing here: Mlola ships no utility framework.`, "Use a component or composition primitive; for your own CSS, read --ml-* tokens.");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const [name, value] of attributes) {
|
|
204
|
+
if (!name.startsWith("data-") || typeof value !== "string") continue;
|
|
205
|
+
if (name === "data-theme" && value && !themeIds.has(value) && !/^th-[0-9a-z]{12}$/.test(value)) {
|
|
206
|
+
note("error", tag, `data-theme="${value}" is not a theme.`, `Use one of: ${[...themeIds].join(", ")}, or a Studio theme id.`);
|
|
207
|
+
}
|
|
208
|
+
if (name === "data-mode" && !["light", "dark"].includes(value)) {
|
|
209
|
+
note("error", tag, `data-mode is "light" or "dark"; "${value}" belongs in an attribute of your own.`, "data-theme and data-mode belong to the engine. Use data-kind or data-state for a component's own meaning.");
|
|
210
|
+
}
|
|
211
|
+
if (!SHARED.has(name) || !mlola.length) continue;
|
|
212
|
+
const allowed = new Set();
|
|
213
|
+
let reacts = false;
|
|
214
|
+
for (const cls of mlola) {
|
|
215
|
+
const values = contract?.elements?.[cls]?.[name];
|
|
216
|
+
if (values) {
|
|
217
|
+
reacts = true;
|
|
218
|
+
for (const entry of values) allowed.add(entry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (reacts && allowed.size && !allowed.has(value)) {
|
|
222
|
+
note("error", tag, `${name}="${value}" is not a value ${mlola.join(" ")} reacts to.`, `Allowed: ${[...allowed].sort().join(", ")}.`);
|
|
223
|
+
} else if (!reacts && mlola.every((cls) => known.has(cls))) {
|
|
224
|
+
note("warning", tag, `${name} has no effect on ${mlola.join(" ")}.`, `See get_component for the attributes it reacts to.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A style string, or a JSX style object, with a color written into it.
|
|
229
|
+
const style = attributes.get("style") ?? attributes.get("style:raw");
|
|
230
|
+
if (typeof style === "string" && COLOR.test(style)) {
|
|
231
|
+
note("error", tag, "A color is written by hand in style.", "Read a token: var(--ml-text), var(--ml-primary-text), var(--ml-surface)… (get_tokens).");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return issues;
|
|
235
|
+
}
|