idml-ui 0.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.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # idml
2
+
3
+ A configuration-driven UI framework for Next.js 15 + TypeScript + Tailwind CSS.
4
+
5
+ Define your entire UI (layout, theming, data bindings) in a single JSON config file. Changes are reflected instantly in development without full page reloads.
6
+
7
+ ## Features
8
+
9
+ - **Config-first**: Declare pages, layouts, components, and design tokens in `ui.config.json`
10
+ - **Percentage-based scaling**: Enforced no-pixel rule — all layout dimensions use percentages of parent container
11
+ - **Design tokens**: Centralized colors, typography, and spacing scales
12
+ - **Data binding**: Reference named methods to wire data and event handlers
13
+ - **Live editor**: Browser-based visual editor with click-to-select and property panel
14
+ - **Hot reload**: File changes trigger instant UI updates in development
15
+ - **Type-safe**: Full TypeScript support with Zod runtime validation
16
+
17
+ ## Quick start
18
+
19
+ ### 1. Install
20
+
21
+ ```bash
22
+ npm install idml-ui
23
+ ```
24
+
25
+ ### 2. Initialize
26
+
27
+ ```bash
28
+ npx idml init
29
+ ```
30
+
31
+ This scaffolds `ui.config.json` and wires up the Next.js plugin.
32
+
33
+ ### 3. Define your UI
34
+
35
+ Edit `ui.config.json`:
36
+
37
+ ```json
38
+ {
39
+ "version": "1",
40
+ "tokens": {
41
+ "colors": [
42
+ { "name": "primary", "value": "#1a56db" }
43
+ ],
44
+ "typography": [
45
+ { "name": "heading-xl", "fontSize": "2.25rem", "fontWeight": 700 }
46
+ ],
47
+ "spacing": [
48
+ { "name": "gap-md", "value": "1rem" }
49
+ ]
50
+ },
51
+ "pages": [{
52
+ "route": "/",
53
+ "layout": {
54
+ "type": "flex",
55
+ "direction": "column",
56
+ "size": { "width": "100%", "height": "100%" },
57
+ "children": []
58
+ },
59
+ "components": []
60
+ }]
61
+ }
62
+ ```
63
+
64
+ ### 4. Render in your app
65
+
66
+ ```tsx
67
+ import { ConfigProvider, ConfigRenderer } from 'idml-ui';
68
+ import config from './ui.config.json';
69
+
70
+ export default function Home() {
71
+ return (
72
+ <ConfigProvider config={config} methods={[]} components={[]}>
73
+ <ConfigRenderer page="/" />
74
+ </ConfigProvider>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ### 5. Open the editor
80
+
81
+ ```bash
82
+ npm run dev
83
+ ```
84
+
85
+ Visit `http://localhost:3000/_isd-editor` to edit your UI visually.
86
+
87
+ ## Architecture
88
+
89
+ - **3 entry points**:
90
+ - `idml` (client/browser) — React components for rendering
91
+ - `idml-ui/server` (Node.js) — Next.js plugin, file watcher, SSE routes
92
+ - `idml/cli` (binary) — `npx idml init` scaffolding tool
93
+
94
+ - **No-pixel rule**: All `width`, `height`, `min-*`, `max-*` values must be percentages. This prevents CSS stacking issues and makes layouts predictable.
95
+
96
+ - **Design tokens**: Define once, use everywhere. Token values are injected as CSS custom properties.
97
+
98
+ - **Data binding**: Connect components to methods registered via `ConfigProvider.methods`.
99
+
100
+ ## License
101
+
102
+ MIT
package/dist/cli.cjs ADDED
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/cli/commands/init.ts
30
+ var import_node_fs = __toESM(require("fs"), 1);
31
+ var import_node_path = __toESM(require("path"), 1);
32
+
33
+ // src/cli/templates/starter-config.ts
34
+ var STARTER_CONFIG = {
35
+ $schema: "./node_modules/idml/ui.config.schema.json",
36
+ version: "1",
37
+ tokens: {
38
+ colors: [
39
+ { name: "primary", value: "#1a56db", darkValue: "#60a5fa" },
40
+ { name: "surface", value: "#ffffff", darkValue: "#1e1e2e" },
41
+ { name: "on-surface", value: "#111827", darkValue: "#f9fafb" },
42
+ { name: "danger", value: "#dc2626", darkValue: "#f87171" }
43
+ ],
44
+ typography: [
45
+ { name: "heading-xl", fontSize: "2.25rem", fontWeight: 700, lineHeight: "1.25" },
46
+ { name: "body-md", fontSize: "1rem", fontWeight: 400, lineHeight: "1.6" },
47
+ { name: "label-sm", fontSize: "0.75rem", fontWeight: 500, lineHeight: "1.4" }
48
+ ],
49
+ spacing: [
50
+ { name: "gap-sm", value: "0.5rem" },
51
+ { name: "gap-md", value: "1rem" },
52
+ { name: "gap-lg", value: "2rem" }
53
+ ]
54
+ },
55
+ pages: [
56
+ {
57
+ route: "/",
58
+ title: "Home",
59
+ layout: {
60
+ type: "flex",
61
+ direction: "column",
62
+ gap: "gap-lg",
63
+ size: { width: "100%", minHeight: "100%" },
64
+ children: []
65
+ },
66
+ components: [
67
+ {
68
+ id: "heading",
69
+ type: "Heading",
70
+ props: { level: 1, text: "Welcome to idml" },
71
+ tokenProps: { typography: "heading-xl", color: "on-surface" }
72
+ },
73
+ {
74
+ id: "description",
75
+ type: "Text",
76
+ props: { text: "Edit the config to build your UI. Changes appear instantly in the editor." },
77
+ tokenProps: { color: "on-surface" }
78
+ }
79
+ ]
80
+ }
81
+ ]
82
+ };
83
+
84
+ // src/cli/templates/editor-page-template.ts
85
+ var EDITOR_PAGE_TEMPLATE = `import { EditorPage } from 'idml-ui/editor';
86
+ import { notFound } from 'next/navigation';
87
+
88
+ export default function Page() {
89
+ if (process.env.NODE_ENV !== 'development') {
90
+ notFound();
91
+ }
92
+
93
+ return <EditorPage />;
94
+ }
95
+ `;
96
+
97
+ // src/cli/templates/events-route-template.ts
98
+ var EVENTS_ROUTE_TEMPLATE = `import { startWatcher, addSSEWriter } from 'idml-ui/server';
99
+
100
+ const CONFIG_PATH = process.env.ISD_UI_CONFIG_PATH!;
101
+
102
+ startWatcher(CONFIG_PATH);
103
+
104
+ export async function GET() {
105
+ const encoder = new TextEncoder();
106
+
107
+ const stream = new ReadableStream({
108
+ start(controller) {
109
+ controller.enqueue(encoder.encode(': ping\\n\\n'));
110
+
111
+ const interval = setInterval(() => {
112
+ try {
113
+ controller.enqueue(encoder.encode(': heartbeat\\n\\n'));
114
+ } catch {
115
+ clearInterval(interval);
116
+ }
117
+ }, 25_000);
118
+
119
+ const unregister = addSSEWriter(controller as any);
120
+
121
+ return () => {
122
+ clearInterval(interval);
123
+ unregister();
124
+ };
125
+ },
126
+ });
127
+
128
+ return new Response(stream, {
129
+ headers: {
130
+ 'Content-Type': 'text/event-stream',
131
+ 'Cache-Control': 'no-cache, no-transform',
132
+ 'X-Accel-Buffering': 'no',
133
+ Connection: 'keep-alive',
134
+ },
135
+ });
136
+ }
137
+
138
+ export const dynamic = 'force-dynamic';
139
+ export const runtime = 'nodejs';
140
+ `;
141
+
142
+ // src/cli/templates/config-route-template.ts
143
+ var CONFIG_ROUTE_TEMPLATE = `import { NextResponse } from 'next/server';
144
+ import type { NextRequest } from 'next/server';
145
+ import fs from 'node:fs/promises';
146
+
147
+ const CONFIG_PATH = process.env.ISD_UI_CONFIG_PATH!;
148
+
149
+ export async function GET() {
150
+ try {
151
+ const raw = await fs.readFile(CONFIG_PATH, 'utf-8');
152
+ return NextResponse.json(JSON.parse(raw));
153
+ } catch (err) {
154
+ return NextResponse.json(
155
+ { error: \`Failed to read config: \${err instanceof Error ? err.message : String(err)}\` },
156
+ { status: 500 }
157
+ );
158
+ }
159
+ }
160
+
161
+ export async function POST(req: NextRequest) {
162
+ if (process.env.NODE_ENV !== 'development') {
163
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
164
+ }
165
+
166
+ try {
167
+ const body = await req.json();
168
+ const tmp = CONFIG_PATH + '.tmp';
169
+
170
+ await fs.writeFile(tmp, JSON.stringify(body, null, 2), 'utf-8');
171
+ await fs.rename(tmp, CONFIG_PATH);
172
+
173
+ return NextResponse.json({ ok: true });
174
+ } catch (err) {
175
+ return NextResponse.json(
176
+ { error: \`Failed to write config: \${err instanceof Error ? err.message : String(err)}\` },
177
+ { status: 500 }
178
+ );
179
+ }
180
+ }
181
+
182
+ export const dynamic = 'force-dynamic';
183
+ export const runtime = 'nodejs';
184
+ `;
185
+
186
+ // src/cli/commands/init.ts
187
+ async function initCommand(options) {
188
+ const cwd = process.cwd();
189
+ const configPath = import_node_path.default.resolve(cwd, options.config);
190
+ console.log("[idml] Initializing...\n");
191
+ if (import_node_fs.default.existsSync(configPath)) {
192
+ console.log(`\u2713 Config already exists at ${options.config}`);
193
+ } else {
194
+ import_node_fs.default.writeFileSync(configPath, JSON.stringify(STARTER_CONFIG, null, 2), "utf-8");
195
+ console.log(`\u2713 Created ${options.config}`);
196
+ }
197
+ if (options.editor) {
198
+ const eventsDir = import_node_path.default.join(cwd, "app", "api", "isd", "events");
199
+ const configDir = import_node_path.default.join(cwd, "app", "api", "isd", "config");
200
+ import_node_fs.default.mkdirSync(eventsDir, { recursive: true });
201
+ import_node_fs.default.mkdirSync(configDir, { recursive: true });
202
+ const eventsRoutePath = import_node_path.default.join(eventsDir, "route.ts");
203
+ const configRoutePath = import_node_path.default.join(configDir, "route.ts");
204
+ if (!import_node_fs.default.existsSync(eventsRoutePath)) {
205
+ import_node_fs.default.writeFileSync(eventsRoutePath, EVENTS_ROUTE_TEMPLATE, "utf-8");
206
+ console.log(`\u2713 Created app/api/isd/events/route.ts`);
207
+ }
208
+ if (!import_node_fs.default.existsSync(configRoutePath)) {
209
+ import_node_fs.default.writeFileSync(configRoutePath, CONFIG_ROUTE_TEMPLATE, "utf-8");
210
+ console.log(`\u2713 Created app/api/isd/config/route.ts`);
211
+ }
212
+ const editorDir = import_node_path.default.join(cwd, "app", "isd", "editor");
213
+ import_node_fs.default.mkdirSync(editorDir, { recursive: true });
214
+ const editorPagePath = import_node_path.default.join(editorDir, "page.tsx");
215
+ if (!import_node_fs.default.existsSync(editorPagePath)) {
216
+ import_node_fs.default.writeFileSync(editorPagePath, EDITOR_PAGE_TEMPLATE, "utf-8");
217
+ console.log(`\u2713 Created app/isd/editor/page.tsx`);
218
+ }
219
+ }
220
+ const nextConfigPath = import_node_path.default.resolve(cwd, "next.config.ts");
221
+ const nextConfigJsPath = import_node_path.default.resolve(cwd, "next.config.js");
222
+ const targetPath = import_node_fs.default.existsSync(nextConfigPath) ? nextConfigPath : import_node_fs.default.existsSync(nextConfigJsPath) ? nextConfigJsPath : null;
223
+ if (!targetPath) {
224
+ console.warn(
225
+ '\n\u26A0 No next.config.ts/js found. Create one and manually add:\n import { withUIConfig } from "idml-ui/server";\n export default withUIConfig()({});\n'
226
+ );
227
+ } else {
228
+ const existing = import_node_fs.default.readFileSync(targetPath, "utf-8");
229
+ if (!existing.includes("withUIConfig")) {
230
+ const importLine = `import { withUIConfig } from 'idml-ui/server';
231
+ `;
232
+ const isJs = targetPath.endsWith(".js");
233
+ let patched = existing;
234
+ if (patched.includes("export default")) {
235
+ patched = patched.replace(
236
+ /export default\s+({|nextConfig)/,
237
+ `export default withUIConfig({ configPath: '${options.config}' })($1`
238
+ );
239
+ if (patched.includes("export default withUIConfig") && patched.includes("nextConfig;")) {
240
+ patched = patched.replace(/nextConfig;/, "nextConfig);");
241
+ } else if (patched.includes("export default withUIConfig") && patched.includes("{")) {
242
+ const lines = patched.split("\n");
243
+ const lastNonEmptyLine = lines[lines.length - 2];
244
+ if (lastNonEmptyLine?.trim() === "}") {
245
+ patched = patched.replace(/^(\s*})(\s*)$/, "$1);$2", "m");
246
+ }
247
+ }
248
+ } else {
249
+ patched = patched + "\nexport default withUIConfig()({});\n";
250
+ }
251
+ import_node_fs.default.writeFileSync(targetPath, importLine + patched, "utf-8");
252
+ console.log(`\u2713 Patched next.config.${isJs ? "js" : "ts"} with withUIConfig`);
253
+ } else {
254
+ console.log(`\u2713 next.config already contains withUIConfig`);
255
+ }
256
+ }
257
+ const tailwindPath = import_node_path.default.resolve(cwd, "tailwind.config.ts");
258
+ const tailwindJsPath = import_node_path.default.resolve(cwd, "tailwind.config.js");
259
+ const tailwindTarget = import_node_fs.default.existsSync(tailwindPath) ? tailwindPath : import_node_fs.default.existsSync(tailwindJsPath) ? tailwindJsPath : null;
260
+ if (tailwindTarget) {
261
+ const tailwindContent = import_node_fs.default.readFileSync(tailwindTarget, "utf-8");
262
+ const isdPath = "./node_modules/idml/dist/**/*.{js,mjs}";
263
+ if (!tailwindContent.includes("idml")) {
264
+ const patched = tailwindContent.replace(
265
+ /content:\s*\[/,
266
+ `content: [
267
+ '${isdPath}',`
268
+ );
269
+ import_node_fs.default.writeFileSync(tailwindTarget, patched, "utf-8");
270
+ console.log(`\u2713 Updated tailwind.config.ts content array`);
271
+ }
272
+ }
273
+ console.log("\n\u2705 idml initialized!\n");
274
+ console.log("Next steps:");
275
+ console.log(" 1. npm run dev");
276
+ console.log(" 2. Visit http://localhost:3000/_isd-editor");
277
+ console.log(" 3. Click on components in the preview to edit them");
278
+ console.log("\nDocumentation: https://github.com/...\n");
279
+ }
280
+
281
+ // src/cli.ts
282
+ var program = new import_commander.Command();
283
+ program.name("idml").description("UI configuration framework for Next.js + Tailwind").version("0.1.0");
284
+ program.command("init").description("Scaffold ui.config.json and wire up withUIConfig in next.config.ts").option("--config <path>", "Path for the config file", "./ui.config.json").option("--no-editor", "Disable the visual editor").action(async (options) => {
285
+ try {
286
+ await initCommand(options);
287
+ } catch (err) {
288
+ console.error("[idml] Error:", err instanceof Error ? err.message : String(err));
289
+ process.exit(1);
290
+ }
291
+ });
292
+ program.parse(process.argv);