create-middag-ui 0.1.0 → 0.2.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/lib/prompts.js ADDED
@@ -0,0 +1,120 @@
1
+ /* global console, process */
2
+ /**
3
+ * prompts.js — readline helpers (ask, select, confirm).
4
+ *
5
+ * Uses Node.js built-in readline. No external deps.
6
+ */
7
+
8
+ import { createInterface } from "node:readline";
9
+
10
+ /**
11
+ * Ask a free-text question. Returns trimmed answer.
12
+ */
13
+ export function ask(question) {
14
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
15
+ return new Promise((resolve) =>
16
+ rl.question(question, (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
19
+ }),
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Ask a free-text question with masked input (for tokens/passwords).
25
+ * Characters are replaced with '*' on screen.
26
+ */
27
+ export function askSecret(question) {
28
+ return new Promise((resolve) => {
29
+ const rl = createInterface({ input: process.stdin, terminal: false });
30
+ process.stdout.write(question);
31
+
32
+ // Disable echo by switching stdin to raw mode
33
+ if (process.stdin.isTTY) {
34
+ process.stdin.setRawMode(true);
35
+ }
36
+ process.stdin.resume();
37
+
38
+ let input = "";
39
+ const onData = (ch) => {
40
+ const c = ch.toString();
41
+ // Enter
42
+ if (c === "\n" || c === "\r" || c === "\u0004") {
43
+ if (process.stdin.isTTY) {
44
+ process.stdin.setRawMode(false);
45
+ }
46
+ process.stdin.removeListener("data", onData);
47
+ process.stdin.pause();
48
+ rl.close();
49
+ process.stdout.write("\n");
50
+ resolve(input.trim());
51
+ return;
52
+ }
53
+ // Ctrl+C
54
+ if (c === "\u0003") {
55
+ if (process.stdin.isTTY) {
56
+ process.stdin.setRawMode(false);
57
+ }
58
+ process.stdin.removeListener("data", onData);
59
+ rl.close();
60
+ process.stdout.write("\n");
61
+ process.exit(1);
62
+ }
63
+ // Backspace
64
+ if (c === "\u007F" || c === "\b") {
65
+ if (input.length > 0) {
66
+ input = input.slice(0, -1);
67
+ process.stdout.write("\b \b");
68
+ }
69
+ return;
70
+ }
71
+ input += c;
72
+ process.stdout.write("*");
73
+ };
74
+
75
+ process.stdin.on("data", onData);
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Show numbered options, return the selected value.
81
+ *
82
+ * @param {string} question - Prompt text
83
+ * @param {{ label: string, value: string }[]} options - Choices
84
+ * @param {string} [defaultValue] - Default if user presses Enter
85
+ * @returns {Promise<string>} Selected value
86
+ */
87
+ export async function select(question, options, defaultValue) {
88
+ for (let i = 0; i < options.length; i++) {
89
+ const marker = options[i].value === defaultValue ? " (default)" : "";
90
+ console.log(` ${i + 1}) ${options[i].label}${marker}`);
91
+ }
92
+ const answer = await ask(`\n ${question} [1-${options.length}]: `);
93
+
94
+ if (!answer && defaultValue) {
95
+ return defaultValue;
96
+ }
97
+
98
+ const idx = parseInt(answer, 10) - 1;
99
+ if (idx >= 0 && idx < options.length) {
100
+ return options[idx].value;
101
+ }
102
+
103
+ return defaultValue || options[0].value;
104
+ }
105
+
106
+ /**
107
+ * Yes/No confirmation. Returns boolean.
108
+ *
109
+ * @param {string} question - Prompt text
110
+ * @param {boolean} [defaultYes=true] - Default if user presses Enter
111
+ * @returns {Promise<boolean>}
112
+ */
113
+ export async function confirm(question, defaultYes = true) {
114
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
115
+ const answer = await ask(` ${question} ${hint}: `);
116
+
117
+ if (!answer) return defaultYes;
118
+
119
+ return answer.toLowerCase().startsWith("y");
120
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * scaffold.js — File creation for all scaffolded files.
3
+ *
4
+ * Creates directory structure, config files, demo files, and mock files.
5
+ * Every I/O operation is wrapped with error handling.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
9
+ import { join, basename } from "node:path";
10
+ import { success, warn, error } from "./ui.js";
11
+
12
+ // ── Helpers ──────────────────────────────────────────────────────────────
13
+
14
+ function writeFile(filePath, content, label) {
15
+ try {
16
+ writeFileSync(filePath, content);
17
+ success(`Created ${label}`);
18
+ return true;
19
+ } catch (err) {
20
+ error(`Failed to create ${label}: ${err.message}`);
21
+ return false;
22
+ }
23
+ }
24
+
25
+ function ensureDir(dirPath) {
26
+ try {
27
+ mkdirSync(dirPath, { recursive: true });
28
+ return true;
29
+ } catch (err) {
30
+ error(`Failed to create directory ${dirPath}: ${err.message}`);
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function skipIfExists(filePath, label) {
36
+ if (existsSync(filePath)) {
37
+ warn(`${label} already exists \u2014 skipping`);
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ // ── Create target directory ──────────────────────────────────────────────
44
+
45
+ /**
46
+ * Create the target directory.
47
+ * @param {string} targetDir - Absolute path
48
+ * @returns {boolean} true if directory exists/was created
49
+ */
50
+ export function createTargetDir(targetDir) {
51
+ if (existsSync(targetDir)) {
52
+ warn(`${basename(targetDir)}/ directory already exists \u2014 will scaffold inside it`);
53
+ return true;
54
+ }
55
+ return ensureDir(targetDir);
56
+ }
57
+
58
+ // ── Config files ─────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Scaffold package.json.
62
+ */
63
+ export function scaffoldPackageJson(targetDir, host, cwd) {
64
+ const filePath = join(targetDir, "package.json");
65
+ if (skipIfExists(filePath, "package.json")) return;
66
+
67
+ const projectName = basename(cwd) || "project";
68
+ const pkg = {
69
+ name: `${projectName}-ui`,
70
+ private: true,
71
+ type: "module",
72
+ scripts: {
73
+ dev: "vite --config vite.mock.config.ts",
74
+ "dev:mock": "vite --config vite.mock.config.ts",
75
+ build: "vite build",
76
+ "build:mock": "vite build --config vite.mock.config.ts",
77
+ typecheck: "tsc --noEmit",
78
+ lint: "eslint .",
79
+ "lint:fix": "eslint . --fix",
80
+ },
81
+ dependencies: {
82
+ "@middag-io/react": "^0.1.0",
83
+ },
84
+ devDependencies: {
85
+ "@types/react": "^19.0.0",
86
+ "@types/react-dom": "^19.0.0",
87
+ react: "^19.0.0",
88
+ "react-dom": "^19.0.0",
89
+ "@inertiajs/react": "^2.0.0",
90
+ "@inertiajs/core": "^2.0.0",
91
+ typescript: "^5.7.0",
92
+ vite: "^6.0.0",
93
+ "@vitejs/plugin-react": "^4.0.0",
94
+ tailwindcss: "^4.0.0",
95
+ "@tailwindcss/vite": "^4.0.0",
96
+ },
97
+ };
98
+
99
+ writeFile(filePath, JSON.stringify(pkg, null, 2) + "\n", "package.json");
100
+ }
101
+
102
+ /**
103
+ * Scaffold tsconfig.json.
104
+ */
105
+ export function scaffoldTsconfig(targetDir) {
106
+ const filePath = join(targetDir, "tsconfig.json");
107
+ if (skipIfExists(filePath, "tsconfig.json")) return;
108
+
109
+ const tsconfig = {
110
+ compilerOptions: {
111
+ target: "ES2022",
112
+ module: "ESNext",
113
+ moduleResolution: "bundler",
114
+ jsx: "react-jsx",
115
+ strict: true,
116
+ noUnusedLocals: true,
117
+ noUnusedParameters: true,
118
+ skipLibCheck: true,
119
+ paths: { "@/*": ["./src/*"], "@mock/*": ["./mock/*"] },
120
+ baseUrl: ".",
121
+ },
122
+ include: ["src", "mock"],
123
+ };
124
+
125
+ writeFile(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "tsconfig.json");
126
+ }
127
+
128
+ /**
129
+ * Scaffold vite.mock.config.ts.
130
+ */
131
+ export function scaffoldViteConfig(targetDir, host) {
132
+ const filePath = join(targetDir, "vite.mock.config.ts");
133
+ if (skipIfExists(filePath, "vite.mock.config.ts")) return;
134
+
135
+ const content = `import { defineConfig } from "vite";
136
+ import react from "@vitejs/plugin-react";
137
+ import tailwindcss from "@tailwindcss/vite";
138
+ import { resolve } from "path";
139
+
140
+ export default defineConfig({
141
+ plugins: [react(), tailwindcss()],
142
+ root: "mock",
143
+ server: { port: ${host.port} },
144
+ resolve: {
145
+ alias: {
146
+ "@/": resolve(__dirname, "src") + "/",
147
+ "@mock/": resolve(__dirname, "mock") + "/",
148
+ },
149
+ },
150
+ });
151
+ `;
152
+
153
+ writeFile(filePath, content, "vite.mock.config.ts");
154
+ }
155
+
156
+ // ── Demo files in src/ ──────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Scaffold demo files in src/ that teach the contract system.
160
+ */
161
+ export function scaffoldDemoFiles(targetDir) {
162
+ ensureDir(join(targetDir, "src", "blocks"));
163
+ ensureDir(join(targetDir, "src", "components"));
164
+
165
+ // src/blocks/hello-block.tsx
166
+ const helloBlockPath = join(targetDir, "src", "blocks", "hello-block.tsx");
167
+ if (!skipIfExists(helloBlockPath, "src/blocks/hello-block.tsx")) {
168
+ writeFile(
169
+ helloBlockPath,
170
+ `// RENAME ME \u2014 This is an example custom block.
171
+ //
172
+ // It demonstrates how to create a block that integrates with
173
+ // the MIDDAG contract system via registerBlock().
174
+ //
175
+ // After renaming, update the registration in your setup code.
176
+ //
177
+ // Docs: https://docs.middag.io/blocks/custom-blocks
178
+
179
+ import type { BlockProps } from "@middag-io/react";
180
+
181
+ /** Data shape for this block \u2014 define what the backend sends. */
182
+ export interface HelloBlockData {
183
+ greeting: string;
184
+ name: string;
185
+ }
186
+
187
+ /** Custom block component. Receives data from the PageContract. */
188
+ export function HelloBlock({ data }: BlockProps<HelloBlockData>) {
189
+ return (
190
+ <div className="rounded-lg border bg-card p-6 text-card-foreground">
191
+ <h2 className="text-lg font-semibold text-foreground">
192
+ {data.greeting}
193
+ </h2>
194
+ <p className="mt-2 text-muted-foreground">
195
+ Welcome, {data.name}! This is a custom block.
196
+ </p>
197
+ <p className="mt-4 text-sm text-muted-foreground">
198
+ Edit this file at <code className="text-xs bg-muted px-1 py-0.5 rounded">src/blocks/hello-block.tsx</code>
199
+ </p>
200
+ </div>
201
+ );
202
+ }
203
+ `,
204
+ "src/blocks/hello-block.tsx",
205
+ );
206
+ }
207
+
208
+ // src/components/greeting.tsx
209
+ const greetingPath = join(targetDir, "src", "components", "greeting.tsx");
210
+ if (!skipIfExists(greetingPath, "src/components/greeting.tsx")) {
211
+ writeFile(
212
+ greetingPath,
213
+ `// RENAME ME \u2014 This is an example standalone component.
214
+ //
215
+ // It shows you can write normal React components alongside
216
+ // contract-driven blocks. Not everything needs to be a block.
217
+ //
218
+ // Use standalone components for UI that doesn't come from
219
+ // a PageContract \u2014 headers, sidebars, modals, etc.
220
+
221
+ interface GreetingProps {
222
+ name: string;
223
+ role?: string;
224
+ }
225
+
226
+ export function Greeting({ name, role = "developer" }: GreetingProps) {
227
+ return (
228
+ <div className="flex items-center gap-3 rounded-md bg-muted px-4 py-3">
229
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-medium">
230
+ {name.charAt(0).toUpperCase()}
231
+ </div>
232
+ <div>
233
+ <p className="text-sm font-medium text-foreground">{name}</p>
234
+ <p className="text-xs text-muted-foreground">{role}</p>
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+ `,
240
+ "src/components/greeting.tsx",
241
+ );
242
+ }
243
+
244
+ // src/contracts.ts
245
+ const contractsPath = join(targetDir, "src", "contracts.ts");
246
+ if (!skipIfExists(contractsPath, "src/contracts.ts")) {
247
+ writeFile(
248
+ contractsPath,
249
+ `// Re-export PageContract types from the lib for convenience.
250
+ // Import from here instead of directly from @middag-io/react.
251
+ //
252
+ // Example:
253
+ // import type { PageContract } from '@/contracts';
254
+
255
+ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/react";
256
+ `,
257
+ "src/contracts.ts",
258
+ );
259
+ }
260
+ }
261
+
262
+ // ── Mock files ──────────────────────────────────────────────────────────
263
+
264
+ /**
265
+ * Scaffold mock/ files (hello-contract, main.tsx, index.html, tailwind.css).
266
+ */
267
+ export function scaffoldMockFiles(targetDir) {
268
+ const mockDir = join(targetDir, "mock");
269
+ ensureDir(mockDir);
270
+
271
+ // mock/hello-contract.ts
272
+ const helloContractPath = join(mockDir, "hello-contract.ts");
273
+ if (!skipIfExists(helloContractPath, "mock/hello-contract.ts")) {
274
+ writeFile(
275
+ helloContractPath,
276
+ `import type { PageContract } from "@middag-io/react";
277
+
278
+ /**
279
+ * Hello World contract \u2014 a minimal PageContract to verify your setup.
280
+ *
281
+ * This is what your backend will send via Inertia.
282
+ * Replace this with real data from your server.
283
+ */
284
+ export const helloContract: PageContract = {
285
+ shell: "admin",
286
+ meta: {
287
+ title: "Hello MIDDAG",
288
+ breadcrumbs: [
289
+ { label: "Home", href: "/" },
290
+ { label: "Hello", href: "/hello" },
291
+ ],
292
+ },
293
+ layout: {
294
+ template: "stack",
295
+ regions: {
296
+ content: [
297
+ {
298
+ key: "welcome_metrics",
299
+ type: "metric_card",
300
+ data: {
301
+ title: "Setup Complete",
302
+ value: "1",
303
+ subtitle: "@middag-io/react is working",
304
+ trend: { direction: "up", value: "100%", label: "Ready" },
305
+ },
306
+ },
307
+ {
308
+ key: "hello_table",
309
+ type: "dense_table",
310
+ data: {
311
+ title: "Example Data",
312
+ columns: [
313
+ { key: "id", label: "ID", sortable: true },
314
+ { key: "name", label: "Name", sortable: true },
315
+ { key: "status", label: "Status" },
316
+ ],
317
+ rows: [
318
+ { id: 1, name: "First item", status: "Active" },
319
+ { id: 2, name: "Second item", status: "Draft" },
320
+ { id: 3, name: "Third item", status: "Active" },
321
+ ],
322
+ pagination: { current: 1, total: 1, perPage: 10, totalRows: 3 },
323
+ },
324
+ },
325
+ ],
326
+ },
327
+ },
328
+ };
329
+ `,
330
+ "mock/hello-contract.ts",
331
+ );
332
+ }
333
+
334
+ // mock/main.tsx
335
+ const mainPath = join(mockDir, "main.tsx");
336
+ if (!skipIfExists(mainPath, "mock/main.tsx")) {
337
+ writeFile(
338
+ mainPath,
339
+ `import { StrictMode } from "react";
340
+ import { createRoot } from "react-dom/client";
341
+ import { ContractPage, registerDefaults } from "@middag-io/react";
342
+ import "@middag-io/react/style.css";
343
+ import "./tailwind.css";
344
+ import { helloContract } from "./hello-contract";
345
+
346
+ // Register all default shells, layouts, and blocks
347
+ registerDefaults();
348
+
349
+ createRoot(document.getElementById("root")!).render(
350
+ <StrictMode>
351
+ <ContractPage contract={helloContract} />
352
+ </StrictMode>,
353
+ );
354
+ `,
355
+ "mock/main.tsx",
356
+ );
357
+ }
358
+
359
+ // mock/index.html
360
+ const indexPath = join(mockDir, "index.html");
361
+ if (!skipIfExists(indexPath, "mock/index.html")) {
362
+ writeFile(
363
+ indexPath,
364
+ `<!doctype html>
365
+ <html lang="en">
366
+ <head>
367
+ <meta charset="UTF-8" />
368
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
369
+ <title>MIDDAG React UI \u2014 Mock</title>
370
+ </head>
371
+ <body>
372
+ <div id="root"></div>
373
+ <script type="module" src="./main.tsx"></script>
374
+ </body>
375
+ </html>
376
+ `,
377
+ "mock/index.html",
378
+ );
379
+ }
380
+
381
+ // mock/tailwind.css
382
+ const cssPath = join(mockDir, "tailwind.css");
383
+ if (!skipIfExists(cssPath, "mock/tailwind.css")) {
384
+ writeFile(cssPath, '@import "tailwindcss";\n', "mock/tailwind.css");
385
+ }
386
+ }
package/lib/ui.js ADDED
@@ -0,0 +1,85 @@
1
+ /* global console, process, setInterval, clearInterval */
2
+ /**
3
+ * ui.js — ANSI output helpers (log, success, warn, error, spinner).
4
+ *
5
+ * Zero external deps — uses raw ANSI escape codes.
6
+ */
7
+
8
+ const PREFIX = "\x1b[36mcreate-middag-ui\x1b[0m";
9
+ const RESET = "\x1b[0m";
10
+ const GREEN = "\x1b[32m";
11
+ const YELLOW = "\x1b[33m";
12
+ const RED = "\x1b[31m";
13
+ const DIM = "\x1b[2m";
14
+ const BOLD = "\x1b[1m";
15
+ const CYAN = "\x1b[36m";
16
+
17
+ export function log(msg) {
18
+ console.log(`${PREFIX} ${msg}`);
19
+ }
20
+
21
+ export function success(msg) {
22
+ console.log(` ${GREEN}\u2713${RESET} ${msg}`);
23
+ }
24
+
25
+ export function warn(msg) {
26
+ console.log(` ${YELLOW}\u26A0${RESET} ${msg}`);
27
+ }
28
+
29
+ export function error(msg) {
30
+ console.log(` ${RED}\u2717${RESET} ${msg}`);
31
+ }
32
+
33
+ export function info(msg) {
34
+ console.log(` ${DIM}${msg}${RESET}`);
35
+ }
36
+
37
+ export function heading(step, total, msg) {
38
+ console.log(`\n${CYAN}[${step}/${total}]${RESET} ${BOLD}${msg}${RESET}`);
39
+ }
40
+
41
+ export function blank() {
42
+ console.log("");
43
+ }
44
+
45
+ // ── Spinner ──────────────────────────────────────────────────────────────
46
+
47
+ const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
48
+
49
+ /**
50
+ * Creates an inline spinner that overwrites the current line.
51
+ *
52
+ * Returns { stop(finalMsg) } — call stop() to clear the spinner
53
+ * and print a final message.
54
+ */
55
+ export function createSpinner(msg) {
56
+ let i = 0;
57
+ const id = setInterval(() => {
58
+ const frame = FRAMES[i % FRAMES.length];
59
+ process.stdout.write(`\r ${CYAN}${frame}${RESET} ${msg}`);
60
+ i++;
61
+ }, 80);
62
+
63
+ return {
64
+ /** Update the spinner message while it's running. */
65
+ update(newMsg) {
66
+ msg = newMsg;
67
+ },
68
+ /** Stop spinner and print final line. */
69
+ stop(finalMsg) {
70
+ clearInterval(id);
71
+ process.stdout.write(`\r\x1b[2K`); // clear line
72
+ if (finalMsg) {
73
+ success(finalMsg);
74
+ }
75
+ },
76
+ /** Stop spinner and print error. */
77
+ fail(failMsg) {
78
+ clearInterval(id);
79
+ process.stdout.write(`\r\x1b[2K`);
80
+ if (failMsg) {
81
+ error(failMsg);
82
+ }
83
+ },
84
+ };
85
+ }
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "create-middag-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Bootstrap a MIDDAG React UI layer in your Moodle or WordPress plugin",
6
6
  "bin": {
7
- "create-middag-ui": "./index.js"
7
+ "create-middag-ui": "cli.js"
8
8
  },
9
9
  "files": [
10
- "index.js"
10
+ "cli.js",
11
+ "lib/",
12
+ "README.md"
11
13
  ],
12
14
  "keywords": [
13
15
  "middag",