create-middag-ui 0.1.4 → 0.2.1

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