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 +102 -0
- package/dist/cli.cjs +292 -0
- package/dist/index.cjs +2434 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +599 -0
- package/dist/index.d.ts +599 -0
- package/dist/index.js +2380 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +1236 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +184 -0
- package/dist/server.d.ts +184 -0
- package/dist/server.js +1195 -0
- package/dist/server.js.map +1 -0
- package/package.json +78 -0
- package/ui.config.schema.json +495 -0
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);
|