create-middag-ui 0.10.3 → 0.12.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/cli.js CHANGED
@@ -33,10 +33,16 @@ import {
33
33
  scaffoldIndexHtml,
34
34
  scaffoldDemoFiles,
35
35
  scaffoldPageExamples,
36
- scaffoldProApp,
37
36
  scaffoldFreeApp,
38
37
  scaffoldFreeAdapters,
39
38
  scaffoldDevShell,
39
+ scaffoldHostEntry,
40
+ scaffoldHostViteConfig,
41
+ scaffoldHostThemeCSS,
42
+ scaffoldFreeRegister,
43
+ scaffoldPageResolver,
44
+ scaffoldRouteHelper,
45
+ scaffoldDemoDirectPage,
40
46
  } from "./lib/scaffold.js";
41
47
  import { runNpmInstall } from "./lib/install.js";
42
48
  import { log, success, heading, blank, info } from "./lib/ui.js";
@@ -116,7 +122,7 @@ if (!dirCreated) {
116
122
 
117
123
  heading(5, TOTAL_STEPS, "Scaffolding config files");
118
124
 
119
- scaffoldPackageJson(targetDir, host, cwd, registryPath);
125
+ scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey);
120
126
  scaffoldTsconfig(targetDir);
121
127
  scaffoldViteConfig(targetDir, host, registryPath);
122
128
  scaffoldEslintConfig(targetDir);
@@ -147,16 +153,44 @@ heading(8, TOTAL_STEPS, `Creating ${isPro ? "PRO" : "FREE"} UI module`);
147
153
 
148
154
  scaffoldPageExamples(targetDir);
149
155
 
156
+ // Shared files (both PRO and FREE)
157
+ scaffoldPageResolver(targetDir);
158
+ scaffoldDemoDirectPage(targetDir);
159
+ scaffoldRouteHelper(targetDir, hostKey);
160
+
150
161
  if (isPro) {
151
- scaffoldProApp(targetDir);
152
- success("PRO: using MockProductShell from @middag-io/react/mock");
162
+ try {
163
+ const pro = await import("./lib/scaffoldPRO.js");
164
+ pro.scaffoldProRegister(targetDir);
165
+ pro.scaffoldProApp(targetDir);
166
+ pro.scaffoldMockNavigation(targetDir);
167
+ pro.scaffoldMockData(targetDir);
168
+ pro.scaffoldMockEntities(targetDir);
169
+ pro.scaffoldMockPageContracts(targetDir);
170
+ pro.scaffoldMockRoutes(targetDir);
171
+ success("PRO: using MockProductShell from @middag-io/react/mock");
172
+ } catch {
173
+ // npm version — PRO file excluded, fall back to FREE
174
+ info("PRO scaffold not available — using FREE path");
175
+ scaffoldFreeRegister(targetDir);
176
+ scaffoldFreeAdapters(targetDir);
177
+ scaffoldDevShell(targetDir);
178
+ scaffoldFreeApp(targetDir);
179
+ success("FREE: generated DevShell + local Inertia adapters");
180
+ }
153
181
  } else {
182
+ scaffoldFreeRegister(targetDir);
154
183
  scaffoldFreeAdapters(targetDir);
155
184
  scaffoldDevShell(targetDir);
156
185
  scaffoldFreeApp(targetDir);
157
186
  success("FREE: generated DevShell + local Inertia adapters");
158
187
  }
159
188
 
189
+ // Host-specific production files (entry, vite config, theme CSS)
190
+ scaffoldHostEntry(targetDir, hostKey);
191
+ scaffoldHostViteConfig(targetDir, hostKey, host);
192
+ scaffoldHostThemeCSS(targetDir, hostKey, host);
193
+
160
194
  // ── Step 9: npm install ──────────────────────────────────────────────────
161
195
 
162
196
  heading(9, TOTAL_STEPS, "Installing dependencies");
@@ -173,6 +207,10 @@ heading(10, TOTAL_STEPS, "Done!");
173
207
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
174
208
  blank();
175
209
 
210
+ // Determine host-specific build script name for display
211
+ const hostBuildScript = hostKey === "wordpress" ? "build:wp" : hostKey === "moodle" ? "build:moodle" : "build:host";
212
+ const hostWatchScript = hostKey === "wordpress" ? "watch:wp" : hostKey === "moodle" ? "watch:moodle" : "watch:host";
213
+
176
214
  if (installOk) {
177
215
  log(`MIDDAG React UI ready in ${dirName}/ (${elapsed}s)\n`);
178
216
  console.log(" Start developing:");
@@ -194,6 +232,11 @@ console.log(" src/pages/settings.ts \u2190 advanced: tabbed_panel + fo
194
232
  console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
195
233
  console.log(" src/app.tsx \u2190 hash-based page router");
196
234
 
235
+ blank();
236
+ console.log(` Production build for ${host.name}:`);
237
+ console.log(` npm run ${hostBuildScript} \u2192 build for ${host.name}`);
238
+ console.log(` npm run ${hostWatchScript} \u2192 rebuild on change`);
239
+
197
240
  blank();
198
241
  console.log(` Integrate with your ${host.name} plugin:`);
199
242
  console.log(" 1. Import { ContractPage } from '@middag-io/react'");
package/lib/detect.js CHANGED
@@ -1,11 +1,19 @@
1
1
  /**
2
2
  * detect.js — Host detection (Moodle / WordPress / Custom).
3
3
  *
4
- * Checks for marker files in the working directory.
4
+ * Detection strategy:
5
+ * - Moodle plugin: cwd has version.php with $plugin->component
6
+ * - WordPress: wp-config.php found in ancestor dirs (plugin/theme is 3+ levels deep)
7
+ * - Moodle root: ancestor has version.php WITHOUT $plugin->component
8
+ *
9
+ * Walks up to MAX_DEPTH ancestor directories from cwd.
5
10
  */
6
11
 
7
- import { existsSync } from "node:fs";
8
- import { join } from "node:path";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join, dirname } from "node:path";
14
+
15
+ /** Max ancestor levels to walk when searching for platform markers. */
16
+ const MAX_DEPTH = 5;
9
17
 
10
18
  export const HOSTS = {
11
19
  moodle: { name: "Moodle", detect: "version.php", port: 5174, headerHeight: 50 },
@@ -14,16 +22,74 @@ export const HOSTS = {
14
22
  };
15
23
 
16
24
  /**
17
- * Detect host platform by checking for marker files.
25
+ * Check if a version.php file contains $plugin->component (Moodle plugin marker).
26
+ *
27
+ * @param {string} filePath - Path to version.php
28
+ * @returns {boolean}
29
+ */
30
+ function isMoodlePluginVersion(filePath) {
31
+ try {
32
+ const content = readFileSync(filePath, "utf-8");
33
+ return /\$plugin\s*->\s*component\s*=/.test(content);
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Check if a version.php file is a Moodle root version.php.
41
+ * Root has $version but NOT $plugin->component.
42
+ *
43
+ * @param {string} filePath - Path to version.php
44
+ * @returns {boolean}
45
+ */
46
+ function isMoodleRootVersion(filePath) {
47
+ try {
48
+ const content = readFileSync(filePath, "utf-8");
49
+ if (/\$plugin\s*->\s*component\s*=/.test(content)) return false;
50
+ // Moodle root has both $version and $release — distinguishes from generic version.php
51
+ return /\$version\s*=/.test(content) && /\$release\s*=/.test(content);
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Detect host platform by checking cwd and ancestor directories.
59
+ *
60
+ * Priority:
61
+ * 1. Moodle plugin — cwd has version.php with $plugin->component
62
+ * 2. WordPress — wp-config.php in any ancestor (up to MAX_DEPTH)
63
+ * 3. Moodle root — version.php without $plugin->component in any ancestor
18
64
  *
19
65
  * @param {string} cwd - Directory to check
20
66
  * @returns {string|null} Host key ('moodle', 'wordpress') or null
21
67
  */
22
68
  export function detectHost(cwd) {
23
- for (const [key, host] of Object.entries(HOSTS)) {
24
- if (host.detect && existsSync(join(cwd, host.detect))) {
25
- return key;
69
+ // 1. Moodle plugin version.php in cwd with $plugin->component
70
+ const cwdVersion = join(cwd, "version.php");
71
+ if (existsSync(cwdVersion) && isMoodlePluginVersion(cwdVersion)) {
72
+ return "moodle";
73
+ }
74
+
75
+ // 2. Walk ancestors for WordPress (wp-config.php) or Moodle root (version.php)
76
+ let dir = cwd;
77
+ for (let i = 0; i < MAX_DEPTH; i++) {
78
+ const parent = dirname(dir);
79
+ if (parent === dir) break;
80
+ dir = parent;
81
+
82
+ // WordPress — wp-config.php
83
+ if (existsSync(join(dir, "wp-config.php"))) {
84
+ return "wordpress";
85
+ }
86
+
87
+ // Moodle root — version.php without $plugin->component
88
+ const versionFile = join(dir, "version.php");
89
+ if (existsSync(versionFile) && isMoodleRootVersion(versionFile)) {
90
+ return "moodle";
26
91
  }
27
92
  }
93
+
28
94
  return null;
29
95
  }
package/lib/scaffold.js CHANGED
@@ -77,8 +77,9 @@ export function createTargetDir(targetDir) {
77
77
  */
78
78
  /**
79
79
  * @param {string} registryPath - "github" (PRO) or "public" (FREE)
80
+ * @param {string} [hostKey] - 'wordpress' | 'moodle' | 'custom' (adds host build scripts)
80
81
  */
81
- export function scaffoldPackageJson(targetDir, host, cwd, registryPath) {
82
+ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey) {
82
83
  const filePath = join(targetDir, "package.json");
83
84
  if (skipIfExists(filePath, "package.json")) return;
84
85
 
@@ -93,19 +94,33 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath) {
93
94
  deps["sonner"] = "^2.0.0";
94
95
  }
95
96
 
97
+ const scripts = {
98
+ dev: "vite",
99
+ build: "vite build",
100
+ typecheck: "tsc --noEmit",
101
+ lint: "eslint .",
102
+ "lint:fix": "eslint . --fix",
103
+ format: 'prettier --write "src/**/*.{ts,tsx,css}"',
104
+ "format:check": 'prettier --check "src/**/*.{ts,tsx,css}"',
105
+ };
106
+
107
+ // Add host-specific build/watch scripts
108
+ if (hostKey === "wordpress") {
109
+ scripts["build:wp"] = "vite build --config vite.config.wordpress.ts";
110
+ scripts["watch:wp"] = "vite build --config vite.config.wordpress.ts --watch";
111
+ } else if (hostKey === "moodle") {
112
+ scripts["build:moodle"] = "vite build --config vite.config.moodle.ts";
113
+ scripts["watch:moodle"] = "vite build --config vite.config.moodle.ts --watch";
114
+ } else if (hostKey === "custom") {
115
+ scripts["build:host"] = "vite build --config vite.config.custom.ts";
116
+ scripts["watch:host"] = "vite build --config vite.config.custom.ts --watch";
117
+ }
118
+
96
119
  const pkg = {
97
120
  name: `${projectName}-ui`,
98
121
  private: true,
99
122
  type: "module",
100
- scripts: {
101
- dev: "vite",
102
- build: "vite build",
103
- typecheck: "tsc --noEmit",
104
- lint: "eslint .",
105
- "lint:fix": "eslint . --fix",
106
- format: 'prettier --write "src/**/*.{ts,tsx,css}"',
107
- "format:check": 'prettier --check "src/**/*.{ts,tsx,css}"',
108
- },
123
+ scripts,
109
124
  dependencies: deps,
110
125
  devDependencies: {
111
126
  "@types/react": "^19.0.0",
@@ -185,6 +200,9 @@ import { resolve } from "path";
185
200
  export default defineConfig({
186
201
  plugins: [react()],
187
202
  server: { port: ${host.port} },
203
+ optimizeDeps: {
204
+ include: ["@middag-io/react", "@middag-io/react/mock"],
205
+ },
188
206
  resolve: {
189
207
  alias: {
190
208
  "@/": resolve(__dirname, "src") + "/",
@@ -294,6 +312,7 @@ export function scaffoldIndexHtml(targetDir) {
294
312
  -->
295
313
  <div id="root" class="middag-root"></div>
296
314
  <div id="middag-portals" class="middag-root"></div>
315
+ <script>window.__MIDDAG_MOCK_NAVIGATE__ = true;</script>
297
316
  <script type="module" src="/src/main.tsx"></script>
298
317
  </body>
299
318
  </html>
@@ -1231,6 +1250,293 @@ export function App() {
1231
1250
  }
1232
1251
  }
1233
1252
 
1253
+ // ── Host-specific production files ─────────────────────────────────────
1254
+
1255
+ /**
1256
+ * Scaffold production entry point: src/entry-{hostKey}.tsx.
1257
+ * Uses real createInertiaApp — no mocks.
1258
+ *
1259
+ * @param {string} targetDir - Absolute path to UI dir
1260
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1261
+ */
1262
+ export function scaffoldHostEntry(targetDir, hostKey) {
1263
+ ensureDir(join(targetDir, "src"));
1264
+
1265
+ const filePath = join(targetDir, "src", `entry-${hostKey}.tsx`);
1266
+ const label = `src/entry-${hostKey}.tsx`;
1267
+ if (skipIfExists(filePath, label)) return;
1268
+
1269
+ // Host-specific setup and post-mount code
1270
+ let setupCode = "";
1271
+ let postMountCode = "";
1272
+
1273
+ if (hostKey === "wordpress") {
1274
+ setupCode = ` document.body.classList.add("middag-active");`;
1275
+ postMountCode = `
1276
+ // Relocate WP admin notices into the product shell content area
1277
+ const noticeContainer = document.createElement("div");
1278
+ noticeContainer.className = "middag-wp-notices";
1279
+ const observer = new MutationObserver(() => {
1280
+ const content = document.querySelector(".product-shell__content");
1281
+ if (!content) return;
1282
+ const notices = document.querySelectorAll(
1283
+ "#wpbody-content > .notice, #wpbody-content > .update-nag, #wpbody-content > .updated, #wpbody-content > .error",
1284
+ );
1285
+ if (notices.length > 0) {
1286
+ notices.forEach((n) => noticeContainer.appendChild(n));
1287
+ if (!noticeContainer.parentElement) {
1288
+ content.prepend(noticeContainer);
1289
+ }
1290
+ }
1291
+ });
1292
+ observer.observe(document.body, { childList: true, subtree: true });`;
1293
+ } else if (hostKey === "moodle") {
1294
+ setupCode = ` document.body.classList.add("middag-active");`;
1295
+ }
1296
+
1297
+ const content = `/**
1298
+ * Production entry point for ${hostKey === "wordpress" ? "WordPress" : hostKey === "moodle" ? "Moodle" : "custom host"}.
1299
+ *
1300
+ * Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" : hostKey === "moodle" ? "Moodle" : "your backend"})
1301
+ * serves the HTML and Inertia page props. This file is the build target
1302
+ * for \`npm run build:${hostKey === "custom" ? "host" : hostKey}\`.
1303
+ *
1304
+ * NOT used by \`npm run dev\` — that uses src/main.tsx with mock adapters.
1305
+ */
1306
+ import { createRoot } from "react-dom/client";
1307
+ import { createInertiaApp } from "@inertiajs/react";
1308
+ import { I18nProvider, ProgressProvider } from "@middag-io/react";
1309
+ import "@middag-io/react/style.css";
1310
+ import "./theme.css";
1311
+ import { registerDefaults } from "./app/register";
1312
+ import { resolvePageComponent } from "./app/page-resolver";
1313
+
1314
+ registerDefaults();
1315
+
1316
+ createInertiaApp({
1317
+ id: "middag-app",
1318
+ resolve: (name) => resolvePageComponent(name),
1319
+ setup({ el, App, props }) {
1320
+ el.classList.add("middag-root");
1321
+ ${setupCode}
1322
+ createRoot(el).render(
1323
+ <ProgressProvider>
1324
+ <App {...props}>
1325
+ {({ Component, props: pageProps, key }) => (
1326
+ <I18nProvider>
1327
+ <Component key={key} {...pageProps} />
1328
+ </I18nProvider>
1329
+ )}
1330
+ </App>
1331
+ </ProgressProvider>,
1332
+ );
1333
+ ${postMountCode}
1334
+ },
1335
+ });
1336
+ `;
1337
+
1338
+ writeFile(filePath, content, label);
1339
+ }
1340
+
1341
+ /**
1342
+ * Scaffold host-specific Vite build config: vite.config.{hostKey}.ts.
1343
+ *
1344
+ * @param {string} targetDir - Absolute path to UI dir
1345
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1346
+ * @param {object} host - HOSTS[hostKey] object
1347
+ */
1348
+ export function scaffoldHostViteConfig(targetDir, hostKey, host) {
1349
+ const filePath = join(targetDir, `vite.config.${hostKey}.ts`);
1350
+ const label = `vite.config.${hostKey}.ts`;
1351
+ if (skipIfExists(filePath, label)) return;
1352
+
1353
+ let outDir, formats, libName, fileName, extraRollup;
1354
+
1355
+ if (hostKey === "wordpress") {
1356
+ outDir = `resolve(__dirname, "../assets/dist")`;
1357
+ formats = `["iife"]`;
1358
+ libName = `"MiddagUI"`;
1359
+ fileName = `() => "app.js"`;
1360
+ extraRollup = `
1361
+ output: {
1362
+ assetFileNames: (assetInfo) => {
1363
+ if (assetInfo.name?.endsWith(".css")) return "style.css";
1364
+ return assetInfo.name || "[name]-[hash][extname]";
1365
+ },
1366
+ },`;
1367
+ } else if (hostKey === "moodle") {
1368
+ outDir = `resolve(__dirname, "../amd/build")`;
1369
+ formats = `["iife"]`;
1370
+ libName = `"MiddagUI"`;
1371
+ fileName = `() => "app.js"`;
1372
+ extraRollup = `
1373
+ output: {
1374
+ assetFileNames: (assetInfo) => {
1375
+ if (assetInfo.name?.endsWith(".css")) return "style.css";
1376
+ return assetInfo.name || "[name]-[hash][extname]";
1377
+ },
1378
+ },`;
1379
+ } else {
1380
+ outDir = `resolve(__dirname, "../dist")`;
1381
+ formats = `["es"]`;
1382
+ libName = `"MiddagUI"`;
1383
+ fileName = `() => "app.js"`;
1384
+ extraRollup = `
1385
+ output: {
1386
+ assetFileNames: (assetInfo) => {
1387
+ if (assetInfo.name?.endsWith(".css")) return "style.css";
1388
+ return assetInfo.name || "[name]-[hash][extname]";
1389
+ },
1390
+ },`;
1391
+ }
1392
+
1393
+ const content = `/**
1394
+ * Vite build config for ${host.name} — production build target.
1395
+ *
1396
+ * Usage:
1397
+ * npm run build:${hostKey === "custom" ? "host" : hostKey} \u2192 single build
1398
+ * npm run watch:${hostKey === "custom" ? "host" : hostKey} \u2192 rebuild on change
1399
+ *
1400
+ * This config builds src/entry-${hostKey}.tsx into a${hostKey === "custom" ? "n ESM" : "n IIFE"} bundle.
1401
+ * The dev server (\`npm run dev\`) uses vite.config.ts instead.
1402
+ */
1403
+ import { defineConfig } from "vite";
1404
+ import react from "@vitejs/plugin-react";
1405
+ import { resolve } from "path";
1406
+
1407
+ export default defineConfig({
1408
+ plugins: [react()],
1409
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
1410
+ resolve: { alias: { "@/": resolve(__dirname, "src") + "/" } },
1411
+ build: {
1412
+ outDir: ${outDir},
1413
+ emptyOutDir: true,
1414
+ lib: {
1415
+ entry: resolve(__dirname, "src/entry-${hostKey}.tsx"),
1416
+ formats: ${formats},
1417
+ name: ${libName},
1418
+ fileName: ${fileName},
1419
+ },
1420
+ cssCodeSplit: false,
1421
+ rollupOptions: {${extraRollup}
1422
+ },
1423
+ },
1424
+ });
1425
+ `;
1426
+
1427
+ writeFile(filePath, content, label);
1428
+ }
1429
+
1430
+ /**
1431
+ * Append host-specific CSS to src/theme.css.
1432
+ * If theme.css doesn't exist yet, it will be created by scaffoldDemoFiles.
1433
+ *
1434
+ * @param {string} targetDir - Absolute path to UI dir
1435
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1436
+ * @param {object} host - HOSTS[hostKey] object
1437
+ */
1438
+ export function scaffoldHostThemeCSS(targetDir, hostKey, host) {
1439
+ const themePath = join(targetDir, "src", "theme.css");
1440
+
1441
+ let hostSection;
1442
+
1443
+ if (hostKey === "wordpress") {
1444
+ hostSection = `
1445
+
1446
+ /* \u2500\u2500 WordPress admin integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1447
+ * Active when MIDDAG mounts inside wp-admin (body.middag-active).
1448
+ */
1449
+
1450
+ body.middag-active #wpbody-content {
1451
+ padding-bottom: 0;
1452
+ }
1453
+
1454
+ body.middag-active [data-slot="sidebar-container"] {
1455
+ left: 160px !important;
1456
+ }
1457
+
1458
+ body.folded.middag-active [data-slot="sidebar-container"] {
1459
+ left: 36px !important;
1460
+ }
1461
+
1462
+ @media screen and (max-width: 782px) {
1463
+ body.middag-active [data-slot="sidebar-container"] {
1464
+ left: 0 !important;
1465
+ }
1466
+ }
1467
+
1468
+ body.middag-active #wpbody-content > .notice,
1469
+ body.middag-active #wpbody-content > .update-nag,
1470
+ body.middag-active #wpbody-content > .updated,
1471
+ body.middag-active #wpbody-content > .error {
1472
+ display: none !important;
1473
+ }
1474
+
1475
+ .middag-wp-notices {
1476
+ padding: 0.75rem 1.5rem 0;
1477
+ }
1478
+
1479
+ .middag-wp-notices .notice {
1480
+ display: block !important;
1481
+ margin: 0 0 0.5rem;
1482
+ border-radius: var(--radius, 0.5rem);
1483
+ }
1484
+
1485
+ body.middag-active #wpfooter {
1486
+ display: none !important;
1487
+ }
1488
+
1489
+ body.middag-active {
1490
+ --host-header-height: 32px;
1491
+ }
1492
+
1493
+ @media screen and (max-width: 782px) {
1494
+ body.middag-active {
1495
+ --host-header-height: 46px;
1496
+ }
1497
+ }`;
1498
+ } else if (hostKey === "moodle") {
1499
+ hostSection = `
1500
+
1501
+ /* \u2500\u2500 Moodle Boost integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1502
+ * Active when MIDDAG mounts inside Moodle admin (body.middag-active).
1503
+ */
1504
+
1505
+ body.middag-active {
1506
+ --host-header-height: 50px;
1507
+ }
1508
+
1509
+ body.middag-active [data-slot="sidebar-container"] {
1510
+ left: 0 !important;
1511
+ }`;
1512
+ } else {
1513
+ hostSection = `
1514
+
1515
+ /* \u2500\u2500 Host integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1516
+ * Set --host-header-height and --host-sidebar-width as needed.
1517
+ */
1518
+
1519
+ :root {
1520
+ --host-header-height: 0px;
1521
+ --host-sidebar-width: 0px;
1522
+ }`;
1523
+ }
1524
+
1525
+ // If theme.css exists, append; otherwise create with host section only
1526
+ if (existsSync(themePath)) {
1527
+ try {
1528
+ const existing = readFileSync(themePath, "utf-8");
1529
+ writeFileSync(themePath, existing + hostSection + "\n");
1530
+ success(`Appended ${host.name} integration CSS to src/theme.css`);
1531
+ } catch (err) {
1532
+ error(`Failed to append to src/theme.css: ${err.message}`);
1533
+ }
1534
+ } else {
1535
+ ensureDir(join(targetDir, "src"));
1536
+ writeFile(themePath, hostSection.trimStart() + "\n", "src/theme.css (host integration)");
1537
+ }
1538
+ }
1539
+
1234
1540
  // ── LEGACY (kept for backward compat, delegates to FREE) ────────────────
1235
1541
 
1236
1542
  /** @deprecated Use scaffoldFreeApp + scaffoldFreeAdapters instead */
@@ -1474,3 +1780,70 @@ export const router = {
1474
1780
  );
1475
1781
  }
1476
1782
  }
1783
+
1784
+ // ── Template reader ─────────────────────────────────────────────────────
1785
+
1786
+ /** Read a template file relative to this script's directory. */
1787
+ function readTemplate(relativePath) {
1788
+ return readFileSync(join(__dirname, relativePath), "utf-8");
1789
+ }
1790
+
1791
+ // ── Shared: register, page-resolver, route helper, demo page ────────────
1792
+
1793
+ /**
1794
+ * Scaffold FREE register: src/app/register.ts — minimal (5 blocks).
1795
+ */
1796
+ export function scaffoldFreeRegister(targetDir) {
1797
+ ensureDir(join(targetDir, "src", "app"));
1798
+ const filePath = join(targetDir, "src", "app", "register.ts");
1799
+ if (skipIfExists(filePath, "src/app/register.ts")) return;
1800
+ writeFile(filePath, readTemplate("templates/shared/register-free.ts"), "src/app/register.ts (FREE)");
1801
+ }
1802
+
1803
+ /**
1804
+ * Scaffold page resolver: src/app/page-resolver.tsx.
1805
+ * Supports Contract: prefix pages (ContractPage) and direct pages (glob).
1806
+ */
1807
+ export function scaffoldPageResolver(targetDir) {
1808
+ ensureDir(join(targetDir, "src", "app"));
1809
+ const filePath = join(targetDir, "src", "app", "page-resolver.tsx");
1810
+ if (skipIfExists(filePath, "src/app/page-resolver.tsx")) return;
1811
+ writeFile(filePath, readTemplate("templates/shared/page-resolver.tsx"), "src/app/page-resolver.tsx");
1812
+ }
1813
+
1814
+ /**
1815
+ * Scaffold route helper: src/lib/routes.ts.
1816
+ * Abstracts host admin URL vs dev mock path.
1817
+ *
1818
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1819
+ */
1820
+ export function scaffoldRouteHelper(targetDir, hostKey) {
1821
+ ensureDir(join(targetDir, "src", "lib"));
1822
+ const filePath = join(targetDir, "src", "lib", "routes.ts");
1823
+ if (skipIfExists(filePath, "src/lib/routes.ts")) return;
1824
+
1825
+ const templateMap = {
1826
+ wordpress: "templates/shared/route-helper-wp.ts",
1827
+ moodle: "templates/shared/route-helper-moodle.ts",
1828
+ custom: "templates/shared/route-helper-custom.ts",
1829
+ };
1830
+ const template = templateMap[hostKey] || templateMap.wordpress;
1831
+
1832
+ try {
1833
+ writeFile(filePath, readTemplate(template), "src/lib/routes.ts");
1834
+ } catch {
1835
+ // Fallback to WP template if host-specific one doesn't exist
1836
+ writeFile(filePath, readTemplate("templates/shared/route-helper-wp.ts"), "src/lib/routes.ts");
1837
+ }
1838
+ }
1839
+
1840
+ /**
1841
+ * Scaffold demo direct page: src/pages/DemoPage.tsx.
1842
+ * Shows the direct page pattern (usePage + custom React).
1843
+ */
1844
+ export function scaffoldDemoDirectPage(targetDir) {
1845
+ ensureDir(join(targetDir, "src", "pages"));
1846
+ const filePath = join(targetDir, "src", "pages", "DemoPage.tsx");
1847
+ if (skipIfExists(filePath, "src/pages/DemoPage.tsx")) return;
1848
+ writeFile(filePath, readTemplate("templates/shared/demo-page.tsx"), "src/pages/DemoPage.tsx");
1849
+ }