mlola-ui 1.0.1 → 1.0.3

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 (33) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/registry/assets.json +578 -0
  4. package/registry/index.json +65 -65
  5. package/registry/source/packages/components/alert/alert.tsx +1 -1
  6. package/registry/source/packages/components/badge/badge.tsx +2 -2
  7. package/registry/source/packages/components/breadcrumb/breadcrumb.tsx +1 -1
  8. package/registry/source/packages/components/carousel/carousel.tsx +1 -0
  9. package/registry/source/packages/components/checkbox/checkbox.tsx +3 -2
  10. package/registry/source/packages/components/color-picker/color-picker.tsx +15 -13
  11. package/registry/source/packages/components/color-picker/color.ts +1 -1
  12. package/registry/source/packages/components/combobox/combobox.tsx +33 -6
  13. package/registry/source/packages/components/date-picker/date-picker.tsx +5 -3
  14. package/registry/source/packages/components/dropzone/dropzone.tsx +1 -1
  15. package/registry/source/packages/components/empty-state/empty-state.tsx +1 -1
  16. package/registry/source/packages/components/hover-card/hover-card.tsx +1 -1
  17. package/registry/source/packages/components/number-input/number-input.tsx +6 -5
  18. package/registry/source/packages/components/otp-input/otp-input.tsx +6 -4
  19. package/registry/source/packages/components/progress/progress.tsx +1 -1
  20. package/registry/source/packages/components/segmented-control/segmented-control.tsx +5 -3
  21. package/registry/source/packages/components/select/select.tsx +6 -5
  22. package/registry/source/packages/components/tag-input/tag-input.tsx +7 -6
  23. package/registry/source/packages/components/textarea/textarea.tsx +1 -1
  24. package/registry/source/packages/components/time-picker/time-picker.tsx +8 -5
  25. package/registry/source/packages/components/timeline/timeline.tsx +1 -1
  26. package/registry/source/packages/components/toast/toast.tsx +1 -1
  27. package/registry/source/packages/components/toggle/toggle.tsx +1 -0
  28. package/registry/source/packages/components/tour/tour.tsx +3 -3
  29. package/registry/source-manifest.json +24 -24
  30. package/src/assets.js +90 -0
  31. package/src/cli.js +39 -5
  32. package/src/config.js +1 -0
  33. 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-kg//PTcUW7j7AEnhfO/pZFddm++RIo8yKur46OeoeNo=",
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-+mgudoxlPn+mjpQycUfQmv8sC/e/K5FXfXm2+wnH7bI=",
15
- "packages/components/breadcrumb/breadcrumb.tsx": "sha256-SiIpg0oAieyoZbMlBwwQDZhR/KVIFLFCLCQJ6vwv7Fs=",
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-LAPKz9DG9vEBv/8jKjqgH7XiSEERkWWLr4n/Kve1omQ=",
21
- "packages/components/checkbox/checkbox.tsx": "sha256-cqTFwYwNPRxKiBnXXWkcFeDlJ6tpJ9GX4IdqrYRp9Fs=",
22
- "packages/components/color-picker/color-picker.tsx": "sha256-UziuULSI7dIGLfu4jjWIz6Cbh2/HiaY2KYXlfQ+6iQg=",
23
- "packages/components/color-picker/color.ts": "sha256-3IO6DaelZ6TVn2aw+6o6pZCQGAQdeNRkN7sPypUzddE=",
24
- "packages/components/combobox/combobox.tsx": "sha256-CnuJP01m0ZPMB4MGjoKbJCBQ7inI6bPf5aMDrq4m1qI=",
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-pX7snZCO3SxVftkCvI/f5b82bNHPconUa50BJnjYNWc=",
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-jMVEnTki4Suhx7pOWfLX9MO1Q3nLz0xqDJ/2EPHdpYg=",
30
- "packages/components/empty-state/empty-state.tsx": "sha256-AWwOuZgm71hossjCGYygtvzGcDy6JOPryF+U1UrsLhM=",
31
- "packages/components/hover-card/hover-card.tsx": "sha256-HZyB9SUDcRPG8MvRX1sHqHA2rNcO63yVevH9hiGqVAA=",
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-D+8jH2YaW4lCv/F02qvkc9Huhxvz0y1idzgOBwsjiN8=",
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-MWDyTP99wVAQSYJsF78ol1Kv9LKOj1lybmp2zOomVqo=",
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-3SOA1JX54msiq/pF2UolesQqQZm0zSVyZQi6W38lOOA=",
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-sExMN2WHtzxcaCu7xN/1jaP+sRVwJ2OAW1WkKGhNYnU=",
47
- "packages/components/select/select.tsx": "sha256-MWHL/mlAO2ob4H3EmYSBZcC1qqy+M3cF1Z4s0R+frrc=",
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-+R1f2XljDYInsfuggKkpqz5m9TqEa0g3QYKMX2TE6h0=",
57
- "packages/components/textarea/textarea.tsx": "sha256-jeyvAlzFr25eWZ/Inkxo1UEOHXlCPB/QHeId124SfR8=",
58
- "packages/components/time-picker/time-picker.tsx": "sha256-OyIfJN6xVv6TwUYtRirkemJe5VMcu2HXxjaJj8fvu9I=",
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-FDZx+ihL5tbIY/sEl0444uj27JWHE5xl0vl8IqRUUPY=",
61
- "packages/components/toast/toast.tsx": "sha256-RrqUAAfKavEkZaLjywJHc0iTNHC1DHlIGVX97kftDkg=",
62
- "packages/components/toggle/toggle.tsx": "sha256-zwTbHMSs3n0eg5XIpREDnySYLXHaLly1Ew4FWlzvgJY=",
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-1aSh7hMVt/DM/kH3WGJzJMGNsNqYEOmnwAZwc8dwRv8=",
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/assets.js ADDED
@@ -0,0 +1,90 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { targetRoot } from "./config.js";
5
+ import { BUNDLED_REGISTRY_ROOT, readJson } from "./registry.js";
6
+
7
+ /**
8
+ * 2D and 3D assets.
9
+ *
10
+ * Their files are not bundled with the CLI: a project wants three of them,
11
+ * not every model. The bundled index lists each file with its integrity; the
12
+ * files download from the site and are checked before anything is written.
13
+ *
14
+ * Static files (SVG, GLB, posters) land in the `assets` target, public/mlola
15
+ * by default, so a web app serves them as /mlola/2d/… and /mlola/3d/…. A 2D
16
+ * asset's React component lands beside your components, in illustrations/.
17
+ */
18
+
19
+ export function loadAssetIndex() {
20
+ const filename = path.join(BUNDLED_REGISTRY_ROOT, "assets.json");
21
+ if (!fs.existsSync(filename)) return { items: [] };
22
+ return readJson(filename);
23
+ }
24
+
25
+ const integrityOf = (bytes) => `sha256-${createHash("sha256").update(bytes).digest("base64")}`;
26
+
27
+ /** Where each of an asset's files goes in the project. */
28
+ export function assetTarget(config, item, file) {
29
+ if (file.role === "react") return path.join(targetRoot(config, "components"), "illustrations", file.path);
30
+ return path.join(targetRoot(config, "assets"), item.kind, file.path);
31
+ }
32
+
33
+ export async function addAssets(cwd, config, ids, { source, fetcher, overwrite = false }) {
34
+ const index = loadAssetIndex();
35
+ const known = new Map(index.items.map((item) => [item.id, item]));
36
+ const unknown = ids.filter((id) => !known.has(id));
37
+ if (unknown.length) {
38
+ throw new Error(`No asset named ${unknown.map((id) => `"${id}"`).join(", ")}. See them all with "npx mlola-ui list --kind asset".`);
39
+ }
40
+
41
+ // Download and verify everything first, so a failure writes nothing.
42
+ const planned = [];
43
+ for (const id of ids) {
44
+ const item = known.get(id);
45
+ for (const file of item.files) {
46
+ const url = `${source}/${item.kind}/${item.id}/${file.path}`;
47
+ let response;
48
+ try {
49
+ response = await fetcher(url);
50
+ } catch (error) {
51
+ throw new Error(`Could not download ${url}: ${error.message}`);
52
+ }
53
+ if (!response.ok) throw new Error(`Could not download ${url}: the server answered ${response.status}.`);
54
+ const bytes = Buffer.from(await response.arrayBuffer());
55
+ if (integrityOf(bytes) !== file.integrity) throw new Error(`${item.id}/${file.path} failed integrity validation; nothing was written.`);
56
+ planned.push({ item, file, bytes, relative: assetTarget(config, item, file) });
57
+ }
58
+ }
59
+
60
+ const result = { written: [], unchanged: [], conflicts: [], items: ids.map((id) => known.get(id)) };
61
+ for (const { bytes, relative } of planned) {
62
+ const filename = path.join(cwd, relative);
63
+ if (fs.existsSync(filename)) {
64
+ if (fs.readFileSync(filename).equals(bytes)) {
65
+ result.unchanged.push(relative);
66
+ continue;
67
+ }
68
+ if (!overwrite) {
69
+ result.conflicts.push(relative);
70
+ continue;
71
+ }
72
+ }
73
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
74
+ fs.writeFileSync(filename, bytes);
75
+ result.written.push(relative);
76
+ }
77
+ return result;
78
+ }
79
+
80
+ /** A line on how to use each added asset. */
81
+ export function assetHint(config, item) {
82
+ const pascal = item.id.replace(/(^|-)([a-z0-9])/g, (_, __, letter) => letter.toUpperCase());
83
+ const assets = targetRoot(config, "assets");
84
+ const publicPath = assets.startsWith("public/") ? `/${assets.slice("public/".length)}` : `/${assets}`;
85
+ if (item.kind === "2d") {
86
+ const alias = (config.aliases?.components ?? "@/components/ui").replace(/\/$/, "");
87
+ return `${item.id}: import { ${pascal}Illustration } from "${alias}/illustrations/${item.id}"; it follows your theme. As a plain image: ${publicPath}/2d/${item.id}.svg`;
88
+ }
89
+ return `${item.id}: ${publicPath}/3d/${item.id}.glb, with its poster at ${publicPath}/3d/${item.id}-poster.webp. Materials are named primary, surface, accent, ink, neutral and metal, so a viewer can paint them from your theme.`;
90
+ }
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  targetRoot,
8
8
  writeDefaultConfig,
9
9
  } from "./config.js";
10
+ import { addAssets, assetHint, loadAssetIndex } from "./assets.js";
10
11
  import { runDoctor } from "./doctor.js";
11
12
  import { collectEngineDependencies, installItems } from "./installer.js";
12
13
  import { loadCatalogNames, loadRegistry, resolveItems } from "./registry.js";
@@ -18,9 +19,10 @@ const HELP = `Mlola UI — source-copy components with Theme
18
19
  Usage:
19
20
  mlola-ui init [--force]
20
21
  mlola-ui add <name...> [--overwrite] [--install|--yes]
22
+ mlola-ui add asset <id...> [--overwrite]
21
23
  mlola-ui login <token> [--host <url>]
22
24
  mlola-ui logout
23
- mlola-ui list [--kind component|block|template] [--json]
25
+ mlola-ui list [--kind component|block|template|asset] [--json]
24
26
  mlola-ui doctor
25
27
  mlola-ui theme pull <theme-id | url> [--host <url>] [--overwrite]
26
28
 
@@ -29,6 +31,7 @@ Examples:
29
31
  npx mlola-ui add button card
30
32
  npx mlola-ui login mlp_… (Mlola Pro: a token from /account)
31
33
  npx mlola-ui add conversation bot
34
+ npx mlola-ui add asset empty-inbox orb
32
35
  npx mlola-ui list --kind block
33
36
  npx mlola-ui theme pull https://ui.mlola.com/t/th-4k2x9qf7wz3m
34
37
  `;
@@ -116,8 +119,22 @@ export async function run(argv, options = {}) {
116
119
  if (command === "list") {
117
120
  const registry = loadRegistry();
118
121
  const kind = optionValue(args, "--kind");
119
- if (kind && !["component", "block", "template"].includes(kind)) {
120
- throw new Error(`--kind must be component, block, or template (received "${kind}")`);
122
+ if (kind && !["component", "block", "template", "asset"].includes(kind)) {
123
+ throw new Error(`--kind must be component, block, template or asset (received "${kind}")`);
124
+ }
125
+ if (kind === "asset") {
126
+ const assets = loadAssetIndex().items;
127
+ if (hasFlag(args, "--json")) {
128
+ output.log(JSON.stringify(assets, null, 2));
129
+ return 0;
130
+ }
131
+ for (const group of ["2d", "3d"]) {
132
+ const grouped = assets.filter((item) => item.kind === group);
133
+ if (!grouped.length) continue;
134
+ output.log(`${group === "2d" ? "Illustrations" : "3D models"} (${grouped.length})`);
135
+ for (const item of grouped) output.log(` ${item.id.padEnd(18)} ${item.summary}`);
136
+ }
137
+ return 0;
121
138
  }
122
139
  const items = kind
123
140
  ? registry.items.filter((item) => item.type === {
@@ -150,7 +167,7 @@ export async function run(argv, options = {}) {
150
167
  if (!token) throw new Error(`Usage: mlola-ui login <token>. Create a token at ${host}/account.`);
151
168
  const { licence } = await verifyToken(host, token, fetcher);
152
169
  const filename = saveCredentials(env, token, host === hostFrom(env) ? undefined : host);
153
- output.log(`✓ Logged in to Mlola Pro (${licence.plan} licence). Token saved to ${filename}.`);
170
+ output.log(`✓ Logged in to Mlola Pro (${licence.plan} license). Token saved to ${filename}.`);
154
171
  return 0;
155
172
  }
156
173
 
@@ -159,6 +176,23 @@ export async function run(argv, options = {}) {
159
176
  return 0;
160
177
  }
161
178
 
179
+ if (command === "add" && args[0] === "asset") {
180
+ const host = optionValue(args, "--host");
181
+ const ids = args.slice(1).filter((argument) => !argument.startsWith("-") && argument !== host);
182
+ if (!ids.length) throw new Error('Usage: mlola-ui add asset <id...>. See them with "npx mlola-ui list --kind asset".');
183
+ const config = loadConfig(cwd);
184
+ const source = env.MLOLA_ASSETS_URL?.replace(/\/$/, "") ?? `${hostFrom(env, host)}/asset-files`;
185
+ const result = await addAssets(cwd, config, ids, { source, fetcher, overwrite: hasFlag(args, "--overwrite") });
186
+ for (const filename of result.written) output.log(`✓ Added ${filename}`);
187
+ if (result.unchanged.length) output.log(`• ${result.unchanged.length} file(s) already up to date`);
188
+ if (result.conflicts.length) {
189
+ output.error(`✗ Refused to replace ${result.conflicts.length} modified file(s):\n ${result.conflicts.join("\n ")}\nRun again with --overwrite to replace them.`);
190
+ return 1;
191
+ }
192
+ for (const item of result.items) output.log(assetHint(config, item));
193
+ return 0;
194
+ }
195
+
162
196
  if (command === "add") {
163
197
  const names = args.filter((argument) => !argument.startsWith("-"));
164
198
  if (!names.length) throw new Error("Usage: mlola-ui add <name...>");
@@ -178,7 +212,7 @@ export async function run(argv, options = {}) {
178
212
  if (!credentials) {
179
213
  throw new Error(
180
214
  `${proNames.map((name) => `"${name}"`).join(", ")} ${proNames.length === 1 ? "is" : "are"} part of Mlola Pro. ` +
181
- `With a licence, create a token at ${host}/account and run "npx mlola-ui login <token>". See ${host}/pricing to get Pro.`,
215
+ `With a license, create a token at ${host}/account and run "npx mlola-ui login <token>". See ${host}/pricing to get Pro.`,
182
216
  );
183
217
  }
184
218
  const delivered = await fetchProItems(host, credentials.token, proNames, fetcher);
package/src/config.js CHANGED
@@ -22,6 +22,7 @@ export const DEFAULT_CONFIG = {
22
22
  templates: "app/templates",
23
23
  lib: "lib/mlola",
24
24
  styles: "styles",
25
+ assets: "public/mlola",
25
26
  },
26
27
  engine: {
27
28
  engine: "@mlola-ui/engine",
package/src/pro.js CHANGED
@@ -10,7 +10,7 @@ import { sha256 } from "./registry.js";
10
10
  * Pro source is never bundled. `mlola-ui login <token>` stores a token made at
11
11
  * /account (readable by the current user only); `add` sends it to the Pro
12
12
  * endpoint, which returns the items and their Pro dependencies stamped with
13
- * the licence. In CI, `MLOLA_PRO_TOKEN` takes the place of a login.
13
+ * the license. In CI, `MLOLA_PRO_TOKEN` takes the place of a login.
14
14
  */
15
15
 
16
16
  export const DEFAULT_HOST = "https://ui.mlola.com";