mlola-ui 1.0.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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/bin/index.js +4 -0
  4. package/package.json +44 -0
  5. package/registry/catalog.json +97 -0
  6. package/registry/index.json +2938 -0
  7. package/registry/source/packages/components/_internal/anchor.ts +54 -0
  8. package/registry/source/packages/components/_internal/dialog.ts +142 -0
  9. package/registry/source/packages/components/_internal/floating.ts +73 -0
  10. package/registry/source/packages/components/_internal/match.ts +39 -0
  11. package/registry/source/packages/components/_internal/react.ts +40 -0
  12. package/registry/source/packages/components/_internal/scroll.ts +15 -0
  13. package/registry/source/packages/components/accordion/accordion.tsx +164 -0
  14. package/registry/source/packages/components/activity-heatmap/activity-heatmap.tsx +204 -0
  15. package/registry/source/packages/components/activity-heatmap/calendar.ts +92 -0
  16. package/registry/source/packages/components/alert/alert.tsx +83 -0
  17. package/registry/source/packages/components/app-shell/app-shell.tsx +186 -0
  18. package/registry/source/packages/components/avatar/avatar.tsx +56 -0
  19. package/registry/source/packages/components/badge/badge.tsx +39 -0
  20. package/registry/source/packages/components/breadcrumb/breadcrumb.tsx +61 -0
  21. package/registry/source/packages/components/button/button.tsx +42 -0
  22. package/registry/source/packages/components/calendar/calendar.tsx +309 -0
  23. package/registry/source/packages/components/calendar/dates.ts +163 -0
  24. package/registry/source/packages/components/card/card.tsx +39 -0
  25. package/registry/source/packages/components/carousel/carousel.tsx +248 -0
  26. package/registry/source/packages/components/checkbox/checkbox.tsx +66 -0
  27. package/registry/source/packages/components/color-picker/color-picker.tsx +261 -0
  28. package/registry/source/packages/components/color-picker/color.ts +124 -0
  29. package/registry/source/packages/components/combobox/combobox.tsx +320 -0
  30. package/registry/source/packages/components/command-menu/command-menu.tsx +199 -0
  31. package/registry/source/packages/components/context-menu/context-menu.tsx +192 -0
  32. package/registry/source/packages/components/date-picker/date-picker.tsx +185 -0
  33. package/registry/source/packages/components/dropdown-menu/dropdown-menu.tsx +157 -0
  34. package/registry/source/packages/components/dropzone/dropzone.tsx +185 -0
  35. package/registry/source/packages/components/empty-state/empty-state.tsx +32 -0
  36. package/registry/source/packages/components/hover-card/hover-card.tsx +103 -0
  37. package/registry/source/packages/components/input/input.tsx +101 -0
  38. package/registry/source/packages/components/kbd/kbd.tsx +54 -0
  39. package/registry/source/packages/components/modal/modal.tsx +100 -0
  40. package/registry/source/packages/components/number-input/number-input.tsx +184 -0
  41. package/registry/source/packages/components/number-input/number.ts +50 -0
  42. package/registry/source/packages/components/otp-input/otp-input.tsx +116 -0
  43. package/registry/source/packages/components/pagination/pagination.tsx +65 -0
  44. package/registry/source/packages/components/popover/popover.tsx +107 -0
  45. package/registry/source/packages/components/priority-icon/labels.ts +13 -0
  46. package/registry/source/packages/components/priority-icon/priority-icon.tsx +60 -0
  47. package/registry/source/packages/components/progress/progress.tsx +110 -0
  48. package/registry/source/packages/components/radio-group/radio-group.tsx +106 -0
  49. package/registry/source/packages/components/rating/rating.tsx +94 -0
  50. package/registry/source/packages/components/resizable/resizable.tsx +171 -0
  51. package/registry/source/packages/components/segmented-control/segmented-control.tsx +78 -0
  52. package/registry/source/packages/components/select/select.tsx +332 -0
  53. package/registry/source/packages/components/sheet/sheet.tsx +109 -0
  54. package/registry/source/packages/components/skeleton/skeleton.tsx +84 -0
  55. package/registry/source/packages/components/slider/slider.tsx +99 -0
  56. package/registry/source/packages/components/status-icon/labels.ts +14 -0
  57. package/registry/source/packages/components/status-icon/status-icon.tsx +76 -0
  58. package/registry/source/packages/components/stepper/stepper.tsx +87 -0
  59. package/registry/source/packages/components/table/table.tsx +66 -0
  60. package/registry/source/packages/components/tabs/tabs.tsx +163 -0
  61. package/registry/source/packages/components/tag-input/tag-input.tsx +273 -0
  62. package/registry/source/packages/components/textarea/textarea.tsx +70 -0
  63. package/registry/source/packages/components/time-picker/time-picker.tsx +286 -0
  64. package/registry/source/packages/components/time-picker/times.ts +76 -0
  65. package/registry/source/packages/components/timeline/timeline.tsx +115 -0
  66. package/registry/source/packages/components/toast/toast.tsx +317 -0
  67. package/registry/source/packages/components/toggle/toggle.tsx +80 -0
  68. package/registry/source/packages/components/tooltip/tooltip.tsx +70 -0
  69. package/registry/source/packages/components/tour/tour.tsx +193 -0
  70. package/registry/source/packages/components/virtual-list/virtual-list.tsx +121 -0
  71. package/registry/source/packages/components/virtual-list/virtual.ts +33 -0
  72. package/registry/source-manifest.json +67 -0
  73. package/src/cli.js +262 -0
  74. package/src/config.js +114 -0
  75. package/src/doctor.js +156 -0
  76. package/src/installer.js +131 -0
  77. package/src/pro.js +160 -0
  78. package/src/registry.js +91 -0
  79. package/src/theme-pull.js +91 -0
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cx } from "../_internal/react";
5
+ import { offsetsOf, visibleRange } from "./virtual";
6
+
7
+ export { offsetsOf, rowAt, visibleRange } from "./virtual";
8
+
9
+ export interface VirtualListProps {
10
+ count: number;
11
+ /** One row. Rows may differ in height; each is measured once rendered. */
12
+ renderItem: (index: number) => React.ReactNode;
13
+ /** A guess for rows not yet measured, in pixels. */
14
+ estimateSize?: number;
15
+ /** A stable key per row; the index by default. */
16
+ getKey?: (index: number) => React.Key;
17
+ /** Height of the scrolling area: a CSS length, or fill the parent by default. */
18
+ height?: string | number;
19
+ overscan?: number;
20
+ /** Called when the reader nears the end, to load more. */
21
+ onEndReached?: () => void;
22
+ /** How near, in pixels. */
23
+ endThreshold?: number;
24
+ label?: string;
25
+ className?: string;
26
+ }
27
+
28
+ /**
29
+ * A list that renders only the rows on screen, so ten thousand rows scroll
30
+ * like ten. Rows may have any height; each is measured as it appears, and the
31
+ * list keeps its place while measurements arrive. Pair `onEndReached` with a
32
+ * growing `count` for infinite loading.
33
+ */
34
+ export function VirtualList({ count, renderItem, estimateSize = 44, getKey, height = "100%", overscan = 6, onEndReached, endThreshold = 400, label, className }: VirtualListProps) {
35
+ const scroller = React.useRef<HTMLDivElement>(null);
36
+ const sizes = React.useRef(new Map<number, number>());
37
+ const [version, setVersion] = React.useState(0);
38
+ const [view, setView] = React.useState({ top: 0, height: 600 });
39
+ const ended = React.useRef(-1);
40
+
41
+ const offsets = React.useMemo(() => offsetsOf(count, (index) => sizes.current.get(index) ?? estimateSize), [count, estimateSize, version]);
42
+ const { start, end } = visibleRange(offsets, view.top, view.height, overscan);
43
+
44
+ React.useLayoutEffect(() => {
45
+ const element = scroller.current;
46
+ if (!element) return;
47
+ const update = () => setView({ top: element.scrollTop, height: element.clientHeight });
48
+ update();
49
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
50
+ observer?.observe(element);
51
+ return () => observer?.disconnect();
52
+ }, []);
53
+
54
+ // One observer measures every rendered row; a change re-lays the list out.
55
+ const measurer = React.useMemo(
56
+ () =>
57
+ typeof ResizeObserver === "undefined"
58
+ ? null
59
+ : new ResizeObserver((entries) => {
60
+ let changed = false;
61
+ for (const entry of entries) {
62
+ const index = Number((entry.target as HTMLElement).dataset.index);
63
+ const size = Math.round(entry.borderBoxSize?.[0]?.blockSize ?? (entry.target as HTMLElement).offsetHeight);
64
+ if (sizes.current.get(index) !== size) {
65
+ sizes.current.set(index, size);
66
+ changed = true;
67
+ }
68
+ }
69
+ if (changed) setVersion((current) => current + 1);
70
+ }),
71
+ [],
72
+ );
73
+ React.useEffect(() => () => measurer?.disconnect(), [measurer]);
74
+
75
+ React.useEffect(() => {
76
+ if (!onEndReached || !count) return;
77
+ const bottom = view.top + view.height;
78
+ if (offsets[count] - bottom < endThreshold && ended.current !== count) {
79
+ ended.current = count;
80
+ onEndReached();
81
+ }
82
+ }, [view, offsets, count, onEndReached, endThreshold]);
83
+
84
+ const rows: React.ReactNode[] = [];
85
+ for (let index = start; index <= end; index++) {
86
+ rows.push(
87
+ <div
88
+ key={getKey?.(index) ?? index}
89
+ className="ml-virtual-list-row"
90
+ data-index={index}
91
+ role="listitem"
92
+ aria-setsize={count}
93
+ aria-posinset={index + 1}
94
+ style={{ transform: `translateY(${offsets[index]}px)` }}
95
+ ref={(element) => {
96
+ if (!element || !measurer) return;
97
+ measurer.observe(element);
98
+ return () => measurer.unobserve(element);
99
+ }}
100
+ >
101
+ {renderItem(index)}
102
+ </div>,
103
+ );
104
+ }
105
+
106
+ return (
107
+ <div
108
+ ref={scroller}
109
+ className={cx("ml-virtual-list", className)}
110
+ style={{ height }}
111
+ role="list"
112
+ aria-label={label}
113
+ tabIndex={0}
114
+ onScroll={(event) => setView({ top: event.currentTarget.scrollTop, height: event.currentTarget.clientHeight })}
115
+ >
116
+ <div className="ml-virtual-list-space" style={{ height: offsets[count] }}>
117
+ {rows}
118
+ </div>
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Windowing arithmetic: which rows of a long list are on screen, given each
3
+ * row's height (measured, or estimated until measured).
4
+ */
5
+
6
+ /** Running offsets: offsets[i] is where row i starts; offsets[count] is the total height. */
7
+ export function offsetsOf(count: number, sizeOf: (index: number) => number): number[] {
8
+ const offsets = new Array<number>(count + 1);
9
+ offsets[0] = 0;
10
+ for (let index = 0; index < count; index++) offsets[index + 1] = offsets[index] + sizeOf(index);
11
+ return offsets;
12
+ }
13
+
14
+ /** The first row whose bottom is below `y` (binary search). */
15
+ export function rowAt(offsets: number[], y: number): number {
16
+ let low = 0;
17
+ let high = offsets.length - 2;
18
+ while (low < high) {
19
+ const middle = (low + high) >> 1;
20
+ if (offsets[middle + 1] <= y) low = middle + 1;
21
+ else high = middle;
22
+ }
23
+ return Math.max(0, low);
24
+ }
25
+
26
+ /** Rows to render for a viewport, with `overscan` extra rows on each side. */
27
+ export function visibleRange(offsets: number[], scrollTop: number, viewport: number, overscan = 4) {
28
+ const count = offsets.length - 1;
29
+ if (count <= 0) return { start: 0, end: -1 };
30
+ const first = rowAt(offsets, scrollTop);
31
+ const last = rowAt(offsets, scrollTop + viewport);
32
+ return { start: Math.max(0, first - overscan), end: Math.min(count - 1, last + overscan) };
33
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "packages/components/_internal/anchor.ts": "sha256-Jo9N3iIaWnS1CJzYhgTv7vnW+8MLyzL8X4GRNjeEp+g=",
3
+ "packages/components/_internal/dialog.ts": "sha256-Q7DPI2BipgFjPd3T8g2GGicrVal0cp0eMKXWFqcj64k=",
4
+ "packages/components/_internal/floating.ts": "sha256-+WxgC4CtAlSr2qA1EkoDTgto535XLIPsvtOfdOK6CHk=",
5
+ "packages/components/_internal/match.ts": "sha256-nKBpKWeow5ubAHUhOW8yOjlrKs0lqYdjbaYYfE8VoII=",
6
+ "packages/components/_internal/react.ts": "sha256-SH63/haiB4Q93uFjMIMe5xZbaxENu3VI+GcidV8HELM=",
7
+ "packages/components/_internal/scroll.ts": "sha256-/YxhPPJa6Iax4q5eEs8dmpQp634TbvB+421MphEyscQ=",
8
+ "packages/components/accordion/accordion.tsx": "sha256-0ALaLJd1fvj8Vbx86HLBKm3qUItrtwbrGJTykVfNSMg=",
9
+ "packages/components/activity-heatmap/activity-heatmap.tsx": "sha256-fMGDlXKpOfIOy0hmbpR8hKrbbN3fC386tXK27A21ZwM=",
10
+ "packages/components/activity-heatmap/calendar.ts": "sha256-NB+AjXY5Xc+KzR8MMTRBgZxdzOeoHhkW0KQOW7WJPuQ=",
11
+ "packages/components/alert/alert.tsx": "sha256-kg//PTcUW7j7AEnhfO/pZFddm++RIo8yKur46OeoeNo=",
12
+ "packages/components/app-shell/app-shell.tsx": "sha256-XAq82XOTH3ZR+hxTkYFu8P7Ih2aI6ERy/TpXc2VMUwM=",
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=",
16
+ "packages/components/button/button.tsx": "sha256-khqYxcV3hlZ+Q+cYNTRXEpAfIijDFSvkuDJXMPtlk7g=",
17
+ "packages/components/calendar/calendar.tsx": "sha256-uE22LI4jpU8ErR/oBmAkeKiu+Rp0bz3IXZVCurycpwc=",
18
+ "packages/components/calendar/dates.ts": "sha256-mygYkoC70av2ZQ0vkxf885trwW97Wd10I4dptYQe+4A=",
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-yBfPogs6Q2lEoEbgErLTextGzc11BXn+0UX1RPABtBY=",
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=",
25
+ "packages/components/command-menu/command-menu.tsx": "sha256-mF1z92yrPY609pjx5Dt1l5EhGHl9EWV8F8Sq0hyJRw4=",
26
+ "packages/components/context-menu/context-menu.tsx": "sha256-YgYsIzvLGFcJG1XX5zFJwrid5Z1Kn0lyWVzmLKmbMVU=",
27
+ "packages/components/date-picker/date-picker.tsx": "sha256-pX7snZCO3SxVftkCvI/f5b82bNHPconUa50BJnjYNWc=",
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=",
32
+ "packages/components/input/input.tsx": "sha256-lbbheo+ydQTzyujTf0W7DD96TogOjifCyqj9c3h3W0g=",
33
+ "packages/components/kbd/kbd.tsx": "sha256-wrPqzn1e82zJYoKzO7u94Bgq4+nPc0DXLc8GJ/4/3nY=",
34
+ "packages/components/modal/modal.tsx": "sha256-vybxgBGYCrMYvJRnO1wDAc2VFQylu+mQIf5OZIcabX4=",
35
+ "packages/components/number-input/number-input.tsx": "sha256-D+8jH2YaW4lCv/F02qvkc9Huhxvz0y1idzgOBwsjiN8=",
36
+ "packages/components/number-input/number.ts": "sha256-IrHCQrEhI5ein9ifMFuSViSoxQUBoT3tNmmCu/paGGM=",
37
+ "packages/components/otp-input/otp-input.tsx": "sha256-MWDyTP99wVAQSYJsF78ol1Kv9LKOj1lybmp2zOomVqo=",
38
+ "packages/components/pagination/pagination.tsx": "sha256-FBwk8tq4JcsK2D0dlWEYiDSRx3owgBmGzEIlVRaNBoE=",
39
+ "packages/components/popover/popover.tsx": "sha256-/kTiAuFklU4IuP97XaiCUGdEpKtNyMPLPS359mfzPHw=",
40
+ "packages/components/priority-icon/labels.ts": "sha256-zhmJIb7/gS16wU+xCJ7hkReBi9q5RI3SMwdjkJvvvKU=",
41
+ "packages/components/priority-icon/priority-icon.tsx": "sha256-3D6hFRpht3YuWouLhakE2DFdldavmJx0B/povEDwrtQ=",
42
+ "packages/components/progress/progress.tsx": "sha256-3SOA1JX54msiq/pF2UolesQqQZm0zSVyZQi6W38lOOA=",
43
+ "packages/components/radio-group/radio-group.tsx": "sha256-jhnSEBLkdpe8dv5us/Q1sMsQF4sKGRgDXWGKm2JSFew=",
44
+ "packages/components/rating/rating.tsx": "sha256-zgPOQXzAiRE4qGNPQ8qat3SP1XSI/7567kA5VaT51P0=",
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=",
48
+ "packages/components/sheet/sheet.tsx": "sha256-EIn3OeRDmXGHbYf/uihRh8aw3LmzarIFiyC9DUsL1jg=",
49
+ "packages/components/skeleton/skeleton.tsx": "sha256-2Fk72NbLr3+CrXH7jGbHNsc3t+vskMqSyySe/UwbGCs=",
50
+ "packages/components/slider/slider.tsx": "sha256-l+VrBqPYHIIcemiVixxwDxNDB0HEMmYRZekx/H5HroQ=",
51
+ "packages/components/status-icon/labels.ts": "sha256-EE8q+geQBbJUs7I3RmaHZnPwn7Vjk2GjCfxvI4ruXKM=",
52
+ "packages/components/status-icon/status-icon.tsx": "sha256-TtrTeoePyfH1VhKuWC0v12FuwfgND1CHnH5x8fyf4AM=",
53
+ "packages/components/stepper/stepper.tsx": "sha256-uI/dRmkbijlixHiGUceWcpaDzpEixrzQ1UTmoPbGZAY=",
54
+ "packages/components/table/table.tsx": "sha256-ed1Qn2sIpxRvGdPDAb5ih5AcXr5PmHTHdlrJLYDYRp8=",
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=",
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=",
63
+ "packages/components/tooltip/tooltip.tsx": "sha256-NbaOUmlUbBlUIdovchrWwH2QqzuvXB97DjnJnrHf4Gc=",
64
+ "packages/components/tour/tour.tsx": "sha256-1aSh7hMVt/DM/kH3WGJzJMGNsNqYEOmnwAZwc8dwRv8=",
65
+ "packages/components/virtual-list/virtual-list.tsx": "sha256-raGEFn4HlAOIohqZcy0DgptsnUIPS/6bWLYeTe+M9E8=",
66
+ "packages/components/virtual-list/virtual.ts": "sha256-PyDIj3C2Vwb1i4ZssxSlYjgKmZPBIFLZFNq8YCW2Z78="
67
+ }
package/src/cli.js ADDED
@@ -0,0 +1,262 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import {
5
+ DEFAULT_CONFIG,
6
+ loadConfig,
7
+ targetRoot,
8
+ writeDefaultConfig,
9
+ } from "./config.js";
10
+ import { runDoctor } from "./doctor.js";
11
+ import { collectEngineDependencies, installItems } from "./installer.js";
12
+ import { loadCatalogNames, loadRegistry, resolveItems } from "./registry.js";
13
+ import { clearCredentials, fetchProItems, GUIDE_FILENAME, hostFrom, readCredentials, saveCredentials, verifyToken, writeProGuide, writeProStyles } from "./pro.js";
14
+ import { pullTheme } from "./theme-pull.js";
15
+
16
+ const HELP = `Mlola UI — source-copy components with Theme
17
+
18
+ Usage:
19
+ mlola-ui init [--force]
20
+ mlola-ui add <name...> [--overwrite] [--install|--yes]
21
+ mlola-ui login <token> [--host <url>]
22
+ mlola-ui logout
23
+ mlola-ui list [--kind component|block|template] [--json]
24
+ mlola-ui doctor
25
+ mlola-ui theme pull <theme-id | url> [--host <url>] [--overwrite]
26
+
27
+ Examples:
28
+ npx mlola-ui init
29
+ npx mlola-ui add button card
30
+ npx mlola-ui login mlp_… (Mlola Pro: a token from /account)
31
+ npx mlola-ui add conversation bot
32
+ npx mlola-ui list --kind block
33
+ npx mlola-ui theme pull https://ui.mlola.com/t/th-4k2x9qf7wz3m
34
+ `;
35
+
36
+ function hasFlag(args, ...names) {
37
+ return args.some((argument) => names.includes(argument));
38
+ }
39
+
40
+ function optionValue(args, name) {
41
+ const index = args.indexOf(name);
42
+ return index === -1 ? undefined : args[index + 1];
43
+ }
44
+
45
+ function writeStyles(cwd, config, force) {
46
+ const relative = path.join(targetRoot(config, "styles"), "mlola", "index.css");
47
+ const filename = path.join(cwd, relative);
48
+ if (fs.existsSync(filename) && !force) return { relative, created: false };
49
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
50
+ fs.writeFileSync(
51
+ filename,
52
+ `/* Generated by mlola-ui init. Import this file once in your app. */\n@import "${config.engine.engine ?? "@mlola-ui/engine"}";\n`,
53
+ );
54
+ return { relative, created: true };
55
+ }
56
+
57
+ function packageManager(cwd) {
58
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return ["pnpm", ["add"]];
59
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return ["yarn", ["add"]];
60
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
61
+ return ["bun", ["add"]];
62
+ }
63
+ return ["npm", ["install"]];
64
+ }
65
+
66
+ function installEngines(cwd, dependencies, output) {
67
+ if (!dependencies.size) return 0;
68
+ const [command, baseArgs] = packageManager(cwd);
69
+ const packages = [...dependencies].map(([name, range]) => `${name}@${range}`);
70
+ output.log(`Installing ${packages.join(", ")}...`);
71
+ const result = spawnSync(command, [...baseArgs, ...packages], { cwd, stdio: "inherit" });
72
+ if (result.error) {
73
+ output.error(`Unable to run ${command}: ${result.error.message}`);
74
+ return 1;
75
+ }
76
+ return result.status ?? 1;
77
+ }
78
+
79
+ export async function run(argv, options = {}) {
80
+ const cwd = path.resolve(options.cwd ?? process.cwd());
81
+ const output = options.output ?? console;
82
+ const env = options.env ?? process.env;
83
+ const fetcher = options.fetch ?? fetch;
84
+ const [command = "help", ...args] = argv;
85
+
86
+ try {
87
+ if (command === "help" || command === "--help" || command === "-h") {
88
+ output.log(HELP);
89
+ return 0;
90
+ }
91
+
92
+ if (command === "init") {
93
+ const force = hasFlag(args, "--force");
94
+ const configResult = writeDefaultConfig(cwd, { force });
95
+ const config = configResult.created ? DEFAULT_CONFIG : loadConfig(cwd);
96
+ const stylesResult = writeStyles(cwd, config, force);
97
+ output.log(
98
+ configResult.created
99
+ ? `✓ Created ${path.relative(cwd, configResult.filename)}`
100
+ : `• Kept existing ${path.relative(cwd, configResult.filename)}`,
101
+ );
102
+ output.log(
103
+ stylesResult.created
104
+ ? `✓ Created ${stylesResult.relative}`
105
+ : `• Kept existing ${stylesResult.relative}`,
106
+ );
107
+ output.log(
108
+ `Set data-theme="${config.theme}" on your document root and import ${stylesResult.relative}.`,
109
+ );
110
+ output.log(
111
+ `Install the engines you use, starting with: npm install ${config.engine.engine ?? "@mlola-ui/engine"} ${config.engine.motion}`,
112
+ );
113
+ return 0;
114
+ }
115
+
116
+ if (command === "list") {
117
+ const registry = loadRegistry();
118
+ 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}")`);
121
+ }
122
+ const items = kind
123
+ ? registry.items.filter((item) => item.type === {
124
+ component: "registry:ui",
125
+ block: "registry:block",
126
+ template: "registry:page",
127
+ }[kind])
128
+ : registry.items;
129
+ if (hasFlag(args, "--json")) {
130
+ output.log(JSON.stringify(items, null, 2));
131
+ } else {
132
+ for (const group of ["registry:ui", "registry:block", "registry:page"]) {
133
+ const grouped = items.filter((item) => item.type === group);
134
+ if (!grouped.length) continue;
135
+ const label = {
136
+ "registry:ui": "Components",
137
+ "registry:block": "Blocks",
138
+ "registry:page": "Templates",
139
+ }[group];
140
+ output.log(`${label} (${grouped.length})`);
141
+ for (const item of grouped) output.log(` ${item.name.padEnd(18)} ${item.description}`);
142
+ }
143
+ }
144
+ return 0;
145
+ }
146
+
147
+ if (command === "login") {
148
+ const token = args.find((argument) => !argument.startsWith("-") && argument !== optionValue(args, "--host"));
149
+ const host = hostFrom(env, optionValue(args, "--host"));
150
+ if (!token) throw new Error(`Usage: mlola-ui login <token>. Create a token at ${host}/account.`);
151
+ const { licence } = await verifyToken(host, token, fetcher);
152
+ 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}.`);
154
+ return 0;
155
+ }
156
+
157
+ if (command === "logout") {
158
+ output.log(clearCredentials(env) ? "✓ Logged out of Mlola Pro." : "• Not logged in.");
159
+ return 0;
160
+ }
161
+
162
+ if (command === "add") {
163
+ const names = args.filter((argument) => !argument.startsWith("-"));
164
+ if (!names.length) throw new Error("Usage: mlola-ui add <name...>");
165
+ const overwrite = hasFlag(args, "--overwrite");
166
+ const bundled = loadRegistry();
167
+ const config = loadConfig(cwd);
168
+ const catalogNames = loadCatalogNames();
169
+ const proNames = names.filter((name) => catalogNames.has(name));
170
+
171
+ // Pro items come from the service with their Pro dependencies; free ones from the bundle.
172
+ let registry = bundled;
173
+ let styles = [];
174
+ let guide = null;
175
+ if (proNames.length) {
176
+ const credentials = readCredentials(env);
177
+ const host = hostFrom(env, credentials?.host);
178
+ if (!credentials) {
179
+ throw new Error(
180
+ `${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.`,
182
+ );
183
+ }
184
+ const delivered = await fetchProItems(host, credentials.token, proNames, fetcher);
185
+ const known = new Set(bundled.items.map((item) => item.name));
186
+ registry = { ...bundled, items: [...bundled.items, ...delivered.items.filter((item) => !known.has(item.name))] };
187
+ styles = delivered.styles;
188
+ guide = delivered.guide;
189
+ }
190
+
191
+ const items = resolveItems(registry, names, catalogNames);
192
+ const result = installItems(cwd, registry, items, config, { overwrite });
193
+ const styleResult = writeProStyles(cwd, config, styles, { overwrite });
194
+ result.written.push(...styleResult.written);
195
+ result.unchanged.push(...styleResult.unchanged);
196
+ result.conflicts.push(...styleResult.conflicts);
197
+ const guideResult = writeProGuide(cwd, guide, { overwrite });
198
+ if (guideResult.status === "written") result.written.push(GUIDE_FILENAME);
199
+ if (guideResult.status === "unchanged") result.unchanged.push(GUIDE_FILENAME);
200
+ if (guideResult.status === "conflict") result.conflicts.push(GUIDE_FILENAME);
201
+ for (const filename of result.written) output.log(`✓ Added ${filename}`);
202
+ if (result.unchanged.length) {
203
+ output.log(`• ${result.unchanged.length} file(s) already match the registry`);
204
+ }
205
+ if (result.conflicts.length) {
206
+ output.error(
207
+ `✗ Refused to replace ${result.conflicts.length} modified file(s):\n ${result.conflicts.join(
208
+ "\n ",
209
+ )}\nRun again with --overwrite to replace them.`,
210
+ );
211
+ return 1;
212
+ }
213
+
214
+ if (styleResult.index) output.log(`Pro styles are gathered in ${styleResult.index}; import it once, after the engine stylesheet.`);
215
+ if (guideResult.status === "written") output.log(`Coding agents: point your AGENTS.md at ${GUIDE_FILENAME}, which lists every Pro class and attribute.`);
216
+
217
+ const engines = collectEngineDependencies(items);
218
+ if (engines.size && hasFlag(args, "--install", "--yes")) {
219
+ return installEngines(cwd, engines, output);
220
+ }
221
+ if (engines.size) {
222
+ output.log(
223
+ `Engine dependencies: ${[...engines]
224
+ .map(([name, range]) => `${name}@${range}`)
225
+ .join(" ")}`,
226
+ );
227
+ }
228
+ return 0;
229
+ }
230
+
231
+ if (command === "theme") {
232
+ const [subcommand, reference] = args;
233
+ if (subcommand !== "pull") throw new Error(`Unknown theme command "${subcommand ?? ""}". Try: mlola-ui theme pull <theme-id | url>`);
234
+ await pullTheme(cwd, reference, {
235
+ host: optionValue(args, "--host") ?? (process.env.MLOLA_STUDIO_URL || undefined),
236
+ overwrite: hasFlag(args, "--overwrite"),
237
+ fetcher,
238
+ output,
239
+ });
240
+ return 0;
241
+ }
242
+
243
+ if (command === "doctor") {
244
+ const findings = runDoctor(cwd);
245
+ for (const item of findings) {
246
+ const symbol = item.level === "pass" ? "✓" : item.level === "warn" ? "!" : "✗";
247
+ const writer = item.level === "error" ? output.error : output.log;
248
+ writer.call(output, `${symbol} ${item.message}`);
249
+ }
250
+ const errors = findings.filter((item) => item.level === "error").length;
251
+ const warnings = findings.filter((item) => item.level === "warn").length;
252
+ output.log(`Doctor completed with ${errors} error(s) and ${warnings} warning(s).`);
253
+ return errors ? 1 : 0;
254
+ }
255
+
256
+ output.error(`Unknown command "${command}".\n\n${HELP}`);
257
+ return 1;
258
+ } catch (error) {
259
+ output.error(`✗ ${error.message}`);
260
+ return 1;
261
+ }
262
+ }
package/src/config.js ADDED
@@ -0,0 +1,114 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const CONFIG_FILENAME = "mlola.config.json";
5
+
6
+ export const DEFAULT_CONFIG = {
7
+ $schema: "https://ui.mlola.com/schema/mlola.config.json",
8
+ version: 1,
9
+ theme: "graphite",
10
+ aliases: {
11
+ components: "@/components/ui",
12
+ blocks: "@/components/blocks",
13
+ pages: "@/app/pages",
14
+ templates: "@/app/templates",
15
+ lib: "@/lib/mlola",
16
+ styles: "@/styles",
17
+ },
18
+ targets: {
19
+ components: "components/ui",
20
+ blocks: "components/blocks",
21
+ pages: "app/pages",
22
+ templates: "app/templates",
23
+ lib: "lib/mlola",
24
+ styles: "styles",
25
+ },
26
+ engine: {
27
+ engine: "@mlola-ui/engine",
28
+ tokens: "@mlola-ui/tokens",
29
+ motion: "@mlola-ui/motion",
30
+ scene: "@mlola-ui/scene",
31
+ icons: "@mlola-ui/icons",
32
+ behavior: "@mlola-ui/behavior",
33
+ },
34
+ };
35
+
36
+ const REQUIRED_ALIASES = ["components", "blocks", "templates", "lib", "styles"];
37
+ const THEMES = new Set(["graphite", "atelier", "machined", "aerogel", "nordic"]);
38
+
39
+ export function configPath(cwd) {
40
+ return path.join(cwd, CONFIG_FILENAME);
41
+ }
42
+
43
+ export function validateConfig(config) {
44
+ const errors = [];
45
+ if (!config || typeof config !== "object") return ["configuration must be an object"];
46
+ if (config.version !== 1) errors.push(`version must be 1 (received ${config.version})`);
47
+ if (!THEMES.has(config.theme)) errors.push(`theme must be one of ${[...THEMES].join(", ")}`);
48
+ for (const key of REQUIRED_ALIASES) {
49
+ if (typeof config.aliases?.[key] !== "string" || !config.aliases[key]) {
50
+ errors.push(`aliases.${key} must be a non-empty string`);
51
+ }
52
+ }
53
+ if (!config.engine || typeof config.engine !== "object") {
54
+ errors.push("engine mappings are required");
55
+ }
56
+ return errors;
57
+ }
58
+
59
+ export function loadConfig(cwd) {
60
+ const filename = configPath(cwd);
61
+ if (!fs.existsSync(filename)) {
62
+ throw new Error(`No ${CONFIG_FILENAME} found. Run "npx mlola-ui init" first.`);
63
+ }
64
+ let config;
65
+ try {
66
+ config = JSON.parse(fs.readFileSync(filename, "utf8"));
67
+ } catch (error) {
68
+ throw new Error(`Cannot parse ${CONFIG_FILENAME}: ${error.message}`);
69
+ }
70
+ const errors = validateConfig(config);
71
+ if (errors.length) {
72
+ throw new Error(`Invalid ${CONFIG_FILENAME}:\n - ${errors.join("\n - ")}`);
73
+ }
74
+ return config;
75
+ }
76
+
77
+ function safeRelative(value, label) {
78
+ if (typeof value !== "string" || !value.trim()) {
79
+ throw new Error(`${label} must be a non-empty path.`);
80
+ }
81
+ let relative = value.trim().replaceAll("\\", "/");
82
+ if (relative.startsWith("@/") || relative.startsWith("~/")) relative = relative.slice(2);
83
+ relative = relative.replace(/^\.?\//, "");
84
+ if (path.isAbsolute(relative) || relative.split("/").includes("..")) {
85
+ throw new Error(`${label} must stay inside the project: ${value}`);
86
+ }
87
+ return relative;
88
+ }
89
+
90
+ export function targetRoot(config, key) {
91
+ const configured = config.targets?.[key] ?? config.aliases?.[key] ?? DEFAULT_CONFIG.targets[key];
92
+ return safeRelative(configured, `target ${key}`);
93
+ }
94
+
95
+ export function interpolateTarget(template, config, mode = "filesystem") {
96
+ const match = /^\{\{aliases\.([a-z]+)\}\}\/(.+)$/.exec(template);
97
+ if (!match) throw new Error(`Invalid registry target template: ${template}`);
98
+ const [, key, remainder] = match;
99
+ const base =
100
+ mode === "import"
101
+ ? config.aliases?.[key] ?? DEFAULT_CONFIG.aliases[key]
102
+ : targetRoot(config, key);
103
+ if (!base) throw new Error(`No configured alias for "${key}".`);
104
+ return `${base.replace(/\/$/, "")}/${remainder}`;
105
+ }
106
+
107
+ export function writeDefaultConfig(cwd, { force = false } = {}) {
108
+ const filename = configPath(cwd);
109
+ if (fs.existsSync(filename) && !force) {
110
+ return { filename, created: false };
111
+ }
112
+ fs.writeFileSync(filename, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`);
113
+ return { filename, created: true };
114
+ }