create-middag-ui 0.10.2 → 0.11.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
@@ -28,6 +28,8 @@ import {
28
28
  scaffoldPackageJson,
29
29
  scaffoldTsconfig,
30
30
  scaffoldViteConfig,
31
+ scaffoldEslintConfig,
32
+ scaffoldPrettierConfig,
31
33
  scaffoldIndexHtml,
32
34
  scaffoldDemoFiles,
33
35
  scaffoldPageExamples,
@@ -35,6 +37,9 @@ import {
35
37
  scaffoldFreeApp,
36
38
  scaffoldFreeAdapters,
37
39
  scaffoldDevShell,
40
+ scaffoldHostEntry,
41
+ scaffoldHostViteConfig,
42
+ scaffoldHostThemeCSS,
38
43
  } from "./lib/scaffold.js";
39
44
  import { runNpmInstall } from "./lib/install.js";
40
45
  import { log, success, heading, blank, info } from "./lib/ui.js";
@@ -114,9 +119,11 @@ if (!dirCreated) {
114
119
 
115
120
  heading(5, TOTAL_STEPS, "Scaffolding config files");
116
121
 
117
- scaffoldPackageJson(targetDir, host, cwd, registryPath);
122
+ scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey);
118
123
  scaffoldTsconfig(targetDir);
119
124
  scaffoldViteConfig(targetDir, host, registryPath);
125
+ scaffoldEslintConfig(targetDir);
126
+ scaffoldPrettierConfig(targetDir);
120
127
  scaffoldIndexHtml(targetDir);
121
128
 
122
129
  // ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
@@ -145,7 +152,7 @@ scaffoldPageExamples(targetDir);
145
152
 
146
153
  if (isPro) {
147
154
  scaffoldProApp(targetDir);
148
- success("PRO: using MockProductShell + HostAdapter from @middag-io/react/mock");
155
+ success("PRO: using MockProductShell from @middag-io/react/mock");
149
156
  } else {
150
157
  scaffoldFreeAdapters(targetDir);
151
158
  scaffoldDevShell(targetDir);
@@ -153,6 +160,11 @@ if (isPro) {
153
160
  success("FREE: generated DevShell + local Inertia adapters");
154
161
  }
155
162
 
163
+ // Host-specific production files (entry, vite config, theme CSS)
164
+ scaffoldHostEntry(targetDir, hostKey);
165
+ scaffoldHostViteConfig(targetDir, hostKey, host);
166
+ scaffoldHostThemeCSS(targetDir, hostKey, host);
167
+
156
168
  // ── Step 9: npm install ──────────────────────────────────────────────────
157
169
 
158
170
  heading(9, TOTAL_STEPS, "Installing dependencies");
@@ -169,6 +181,10 @@ heading(10, TOTAL_STEPS, "Done!");
169
181
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
170
182
  blank();
171
183
 
184
+ // Determine host-specific build script name for display
185
+ const hostBuildScript = hostKey === "wordpress" ? "build:wp" : hostKey === "moodle" ? "build:moodle" : "build:host";
186
+ const hostWatchScript = hostKey === "wordpress" ? "watch:wp" : hostKey === "moodle" ? "watch:moodle" : "watch:host";
187
+
172
188
  if (installOk) {
173
189
  log(`MIDDAG React UI ready in ${dirName}/ (${elapsed}s)\n`);
174
190
  console.log(" Start developing:");
@@ -190,6 +206,11 @@ console.log(" src/pages/settings.ts \u2190 advanced: tabbed_panel + fo
190
206
  console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
191
207
  console.log(" src/app.tsx \u2190 hash-based page router");
192
208
 
209
+ blank();
210
+ console.log(` Production build for ${host.name}:`);
211
+ console.log(` npm run ${hostBuildScript} \u2192 build for ${host.name}`);
212
+ console.log(` npm run ${hostWatchScript} \u2192 rebuild on change`);
213
+
193
214
  blank();
194
215
  console.log(` Integrate with your ${host.name} plugin:`);
195
216
  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,17 +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
- },
123
+ scripts,
107
124
  dependencies: deps,
108
125
  devDependencies: {
109
126
  "@types/react": "^19.0.0",
@@ -115,6 +132,14 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath) {
115
132
  typescript: "^5.7.0",
116
133
  vite: "^6.0.0",
117
134
  "@vitejs/plugin-react": "^4.0.0",
135
+ "@eslint/js": "^9.0.0",
136
+ "typescript-eslint": "^8.0.0",
137
+ "eslint-plugin-react-hooks": "^5.0.0",
138
+ "eslint-plugin-react-refresh": "^0.5.0",
139
+ "eslint-config-prettier": "^10.0.0",
140
+ eslint: "^9.0.0",
141
+ prettier: "^3.0.0",
142
+ "prettier-plugin-tailwindcss": "^0.6.0",
118
143
  },
119
144
  };
120
145
 
@@ -157,16 +182,9 @@ export function scaffoldViteConfig(targetDir, host, registryPath) {
157
182
  const filePath = join(targetDir, "vite.config.ts");
158
183
  if (skipIfExists(filePath, "vite.config.ts")) return;
159
184
 
160
- const isPro = registryPath === "github";
161
- const adapterComment = isPro
162
- ? "// PRO: Inertia mocks from @middag-io/react/mock"
185
+ const adapterComment = registryPath === "github"
186
+ ? "// PRO: Inertia mocks re-exported from @middag-io/react/mock (same context)"
163
187
  : "// FREE: Inertia mocks from local adapters";
164
- const adapterReact = isPro
165
- ? 'resolve(__dirname, "node_modules/@middag-io/react/mock/adapters/inertia-react.ts")'
166
- : 'resolve(__dirname, "src/adapters/inertia-react.ts")';
167
- const adapterCore = isPro
168
- ? 'resolve(__dirname, "node_modules/@middag-io/react/mock/adapters/inertia-core.ts")'
169
- : 'resolve(__dirname, "src/adapters/inertia-core.ts")';
170
188
 
171
189
  const content = `/**
172
190
  * Vite config \u2014 used by \`npm run dev\` and \`npm run build\`.
@@ -182,12 +200,15 @@ import { resolve } from "path";
182
200
  export default defineConfig({
183
201
  plugins: [react()],
184
202
  server: { port: ${host.port} },
203
+ optimizeDeps: {
204
+ include: ["@middag-io/react", "@middag-io/react/mock"],
205
+ },
185
206
  resolve: {
186
207
  alias: {
187
208
  "@/": resolve(__dirname, "src") + "/",
188
209
  ${adapterComment}
189
- "@inertiajs/react": ${adapterReact},
190
- "@inertiajs/core": ${adapterCore},
210
+ "@inertiajs/react": resolve(__dirname, "src/adapters/inertia-react.ts"),
211
+ "@inertiajs/core": resolve(__dirname, "src/adapters/inertia-core.ts"),
191
212
  },
192
213
  },
193
214
  });
@@ -196,6 +217,70 @@ export default defineConfig({
196
217
  writeFile(filePath, content, "vite.config.ts");
197
218
  }
198
219
 
220
+ /**
221
+ * Scaffold eslint.config.js.
222
+ */
223
+ export function scaffoldEslintConfig(targetDir) {
224
+ const filePath = join(targetDir, "eslint.config.js");
225
+ if (skipIfExists(filePath, "eslint.config.js")) return;
226
+
227
+ writeFile(
228
+ filePath,
229
+ `import js from "@eslint/js";
230
+ import tseslint from "typescript-eslint";
231
+ import reactHooks from "eslint-plugin-react-hooks";
232
+ import reactRefresh from "eslint-plugin-react-refresh";
233
+ import prettierConfig from "eslint-config-prettier";
234
+
235
+ export default tseslint.config(
236
+ { ignores: ["dist/", "node_modules/"] },
237
+ js.configs.recommended,
238
+ ...tseslint.configs.recommended,
239
+ {
240
+ files: ["**/*.{ts,tsx}"],
241
+ plugins: {
242
+ "react-hooks": reactHooks,
243
+ "react-refresh": reactRefresh,
244
+ },
245
+ rules: {
246
+ ...reactHooks.configs.recommended.rules,
247
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
248
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
249
+ "@typescript-eslint/no-explicit-any": "warn",
250
+ },
251
+ },
252
+ prettierConfig,
253
+ );
254
+ `,
255
+ "eslint.config.js",
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Scaffold .prettierrc.
261
+ */
262
+ export function scaffoldPrettierConfig(targetDir) {
263
+ const filePath = join(targetDir, ".prettierrc");
264
+ if (skipIfExists(filePath, ".prettierrc")) return;
265
+
266
+ writeFile(
267
+ filePath,
268
+ JSON.stringify(
269
+ {
270
+ semi: true,
271
+ singleQuote: false,
272
+ tabWidth: 2,
273
+ trailingComma: "all",
274
+ printWidth: 100,
275
+ plugins: ["prettier-plugin-tailwindcss"],
276
+ },
277
+ null,
278
+ 2,
279
+ ) + "\n",
280
+ ".prettierrc",
281
+ );
282
+ }
283
+
199
284
  /**
200
285
  * Scaffold index.html at project root.
201
286
  */
@@ -803,6 +888,30 @@ export const settingsContract: PageContract = {
803
888
  */
804
889
  export function scaffoldProApp(targetDir) {
805
890
  ensureDir(join(targetDir, "src"));
891
+ ensureDir(join(targetDir, "src", "adapters"));
892
+
893
+ // Inertia adapter shims — re-export from pre-built mock bundle
894
+ // so usePage() shares the same MockPageContext as MockPageProvider.
895
+ const adapterReactPath = join(targetDir, "src", "adapters", "inertia-react.ts");
896
+ if (!skipIfExists(adapterReactPath, "src/adapters/inertia-react.ts")) {
897
+ writeFile(adapterReactPath, `/**
898
+ * Mock @inertiajs/react — re-exports from pre-built @middag-io/react/mock.
899
+ *
900
+ * This ensures usePage() reads from the same MockPageContext as
901
+ * MockPageProvider (both live in the pre-built ESM bundle).
902
+ */
903
+ export { usePage, Head, Link, router } from "@middag-io/react/mock";
904
+ `, "src/adapters/inertia-react.ts");
905
+ }
906
+
907
+ const adapterCorePath = join(targetDir, "src", "adapters", "inertia-core.ts");
908
+ if (!skipIfExists(adapterCorePath, "src/adapters/inertia-core.ts")) {
909
+ writeFile(adapterCorePath, `/**
910
+ * Mock @inertiajs/core — re-exports from pre-built @middag-io/react/mock.
911
+ */
912
+ export { router, setMockNavigate } from "@middag-io/react/mock";
913
+ `, "src/adapters/inertia-core.ts");
914
+ }
806
915
 
807
916
  const mainPath = join(targetDir, "src", "main.tsx");
808
917
  if (!skipIfExists(mainPath, "src/main.tsx")) {
@@ -1140,6 +1249,301 @@ export function App() {
1140
1249
  }
1141
1250
  }
1142
1251
 
1252
+ // ── Host-specific production files ─────────────────────────────────────
1253
+
1254
+ /**
1255
+ * Scaffold production entry point: src/entry-{hostKey}.tsx.
1256
+ * Uses real createInertiaApp — no mocks.
1257
+ *
1258
+ * @param {string} targetDir - Absolute path to UI dir
1259
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1260
+ */
1261
+ export function scaffoldHostEntry(targetDir, hostKey) {
1262
+ ensureDir(join(targetDir, "src"));
1263
+
1264
+ const filePath = join(targetDir, "src", `entry-${hostKey}.tsx`);
1265
+ const label = `src/entry-${hostKey}.tsx`;
1266
+ if (skipIfExists(filePath, label)) return;
1267
+
1268
+ // Host-specific setup and post-mount code
1269
+ let setupCode = "";
1270
+ let postMountCode = "";
1271
+
1272
+ if (hostKey === "wordpress") {
1273
+ setupCode = ` document.body.classList.add("middag-active");`;
1274
+ postMountCode = `
1275
+ // Relocate WP admin notices into the product shell content area
1276
+ const noticeContainer = document.createElement("div");
1277
+ noticeContainer.className = "middag-wp-notices";
1278
+ const observer = new MutationObserver(() => {
1279
+ const content = document.querySelector(".product-shell__content");
1280
+ if (!content) return;
1281
+ const notices = document.querySelectorAll(
1282
+ "#wpbody-content > .notice, #wpbody-content > .update-nag, #wpbody-content > .updated, #wpbody-content > .error",
1283
+ );
1284
+ if (notices.length > 0) {
1285
+ notices.forEach((n) => noticeContainer.appendChild(n));
1286
+ if (!noticeContainer.parentElement) {
1287
+ content.prepend(noticeContainer);
1288
+ }
1289
+ }
1290
+ });
1291
+ observer.observe(document.body, { childList: true, subtree: true });`;
1292
+ } else if (hostKey === "moodle") {
1293
+ setupCode = ` document.body.classList.add("middag-active");`;
1294
+ }
1295
+
1296
+ const content = `/**
1297
+ * Production entry point for ${hostKey === "wordpress" ? "WordPress" : hostKey === "moodle" ? "Moodle" : "custom host"}.
1298
+ *
1299
+ * Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" : hostKey === "moodle" ? "Moodle" : "your backend"})
1300
+ * serves the HTML and Inertia page props. This file is the build target
1301
+ * for \`npm run build:${hostKey === "custom" ? "host" : hostKey}\`.
1302
+ *
1303
+ * NOT used by \`npm run dev\` — that uses src/main.tsx with mock adapters.
1304
+ */
1305
+ import { createRoot } from "react-dom/client";
1306
+ import { createInertiaApp } from "@inertiajs/react";
1307
+ import {
1308
+ registerDefaults,
1309
+ registerShell,
1310
+ ContractPage,
1311
+ HostProductShell,
1312
+ I18nProvider,
1313
+ ProgressProvider,
1314
+ ptBR,
1315
+ } from "@middag-io/react";
1316
+ import type { PageContract } from "@middag-io/react";
1317
+ import "@middag-io/react/style.css";
1318
+ import "./theme.css";
1319
+
1320
+ registerDefaults();
1321
+ registerShell("product", HostProductShell);
1322
+
1323
+ createInertiaApp({
1324
+ id: "middag-app",
1325
+ resolve: () => {
1326
+ const Page = ({ contract }: { contract: PageContract }) => (
1327
+ <I18nProvider overrides={ptBR}>
1328
+ <ProgressProvider>
1329
+ <ContractPage contract={contract} />
1330
+ </ProgressProvider>
1331
+ </I18nProvider>
1332
+ );
1333
+ Page.displayName = "ContractPageWrapper";
1334
+ return Page;
1335
+ },
1336
+ setup({ el, App, props }) {
1337
+ el.classList.add("middag-root");
1338
+ ${setupCode}
1339
+ createRoot(el).render(<App {...props} />);
1340
+ ${postMountCode}
1341
+ },
1342
+ });
1343
+ `;
1344
+
1345
+ writeFile(filePath, content, label);
1346
+ }
1347
+
1348
+ /**
1349
+ * Scaffold host-specific Vite build config: vite.config.{hostKey}.ts.
1350
+ *
1351
+ * @param {string} targetDir - Absolute path to UI dir
1352
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1353
+ * @param {object} host - HOSTS[hostKey] object
1354
+ */
1355
+ export function scaffoldHostViteConfig(targetDir, hostKey, host) {
1356
+ const filePath = join(targetDir, `vite.config.${hostKey}.ts`);
1357
+ const label = `vite.config.${hostKey}.ts`;
1358
+ if (skipIfExists(filePath, label)) return;
1359
+
1360
+ let outDir, formats, libName, fileName, extraRollup;
1361
+
1362
+ if (hostKey === "wordpress") {
1363
+ outDir = `resolve(__dirname, "../assets/dist")`;
1364
+ formats = `["iife"]`;
1365
+ libName = `"MiddagUI"`;
1366
+ fileName = `() => "app.js"`;
1367
+ extraRollup = `
1368
+ output: {
1369
+ assetFileNames: (assetInfo) => {
1370
+ if (assetInfo.name?.endsWith(".css")) return "style.css";
1371
+ return assetInfo.name || "[name]-[hash][extname]";
1372
+ },
1373
+ },`;
1374
+ } else if (hostKey === "moodle") {
1375
+ outDir = `resolve(__dirname, "../amd/build")`;
1376
+ formats = `["iife"]`;
1377
+ libName = `"MiddagUI"`;
1378
+ fileName = `() => "app.js"`;
1379
+ extraRollup = `
1380
+ output: {
1381
+ assetFileNames: (assetInfo) => {
1382
+ if (assetInfo.name?.endsWith(".css")) return "style.css";
1383
+ return assetInfo.name || "[name]-[hash][extname]";
1384
+ },
1385
+ },`;
1386
+ } else {
1387
+ outDir = `resolve(__dirname, "../dist")`;
1388
+ formats = `["es"]`;
1389
+ libName = `"MiddagUI"`;
1390
+ fileName = `() => "app.js"`;
1391
+ extraRollup = `
1392
+ output: {
1393
+ assetFileNames: (assetInfo) => {
1394
+ if (assetInfo.name?.endsWith(".css")) return "style.css";
1395
+ return assetInfo.name || "[name]-[hash][extname]";
1396
+ },
1397
+ },`;
1398
+ }
1399
+
1400
+ const content = `/**
1401
+ * Vite build config for ${host.name} — production build target.
1402
+ *
1403
+ * Usage:
1404
+ * npm run build:${hostKey === "custom" ? "host" : hostKey} \u2192 single build
1405
+ * npm run watch:${hostKey === "custom" ? "host" : hostKey} \u2192 rebuild on change
1406
+ *
1407
+ * This config builds src/entry-${hostKey}.tsx into a${hostKey === "custom" ? "n ESM" : "n IIFE"} bundle.
1408
+ * The dev server (\`npm run dev\`) uses vite.config.ts instead.
1409
+ */
1410
+ import { defineConfig } from "vite";
1411
+ import react from "@vitejs/plugin-react";
1412
+ import { resolve } from "path";
1413
+
1414
+ export default defineConfig({
1415
+ plugins: [react()],
1416
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
1417
+ resolve: { alias: { "@/": resolve(__dirname, "src") + "/" } },
1418
+ build: {
1419
+ outDir: ${outDir},
1420
+ emptyOutDir: true,
1421
+ lib: {
1422
+ entry: resolve(__dirname, "src/entry-${hostKey}.tsx"),
1423
+ formats: ${formats},
1424
+ name: ${libName},
1425
+ fileName: ${fileName},
1426
+ },
1427
+ cssCodeSplit: false,
1428
+ rollupOptions: {${extraRollup}
1429
+ },
1430
+ },
1431
+ });
1432
+ `;
1433
+
1434
+ writeFile(filePath, content, label);
1435
+ }
1436
+
1437
+ /**
1438
+ * Append host-specific CSS to src/theme.css.
1439
+ * If theme.css doesn't exist yet, it will be created by scaffoldDemoFiles.
1440
+ *
1441
+ * @param {string} targetDir - Absolute path to UI dir
1442
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1443
+ * @param {object} host - HOSTS[hostKey] object
1444
+ */
1445
+ export function scaffoldHostThemeCSS(targetDir, hostKey, host) {
1446
+ const themePath = join(targetDir, "src", "theme.css");
1447
+
1448
+ let hostSection = "";
1449
+
1450
+ if (hostKey === "wordpress") {
1451
+ hostSection = `
1452
+
1453
+ /* \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
1454
+ * Active when MIDDAG mounts inside wp-admin (body.middag-active).
1455
+ */
1456
+
1457
+ body.middag-active #wpbody-content {
1458
+ padding-bottom: 0;
1459
+ }
1460
+
1461
+ body.middag-active [data-slot="sidebar-container"] {
1462
+ left: 160px !important;
1463
+ }
1464
+
1465
+ body.folded.middag-active [data-slot="sidebar-container"] {
1466
+ left: 36px !important;
1467
+ }
1468
+
1469
+ @media screen and (max-width: 782px) {
1470
+ body.middag-active [data-slot="sidebar-container"] {
1471
+ left: 0 !important;
1472
+ }
1473
+ }
1474
+
1475
+ body.middag-active #wpbody-content > .notice,
1476
+ body.middag-active #wpbody-content > .update-nag,
1477
+ body.middag-active #wpbody-content > .updated,
1478
+ body.middag-active #wpbody-content > .error {
1479
+ display: none !important;
1480
+ }
1481
+
1482
+ .middag-wp-notices {
1483
+ padding: 0.75rem 1.5rem 0;
1484
+ }
1485
+
1486
+ .middag-wp-notices .notice {
1487
+ display: block !important;
1488
+ margin: 0 0 0.5rem;
1489
+ border-radius: var(--radius, 0.5rem);
1490
+ }
1491
+
1492
+ body.middag-active #wpfooter {
1493
+ display: none !important;
1494
+ }
1495
+
1496
+ body.middag-active {
1497
+ --host-header-height: 32px;
1498
+ }
1499
+
1500
+ @media screen and (max-width: 782px) {
1501
+ body.middag-active {
1502
+ --host-header-height: 46px;
1503
+ }
1504
+ }`;
1505
+ } else if (hostKey === "moodle") {
1506
+ hostSection = `
1507
+
1508
+ /* \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
1509
+ * Active when MIDDAG mounts inside Moodle admin (body.middag-active).
1510
+ */
1511
+
1512
+ body.middag-active {
1513
+ --host-header-height: 50px;
1514
+ }
1515
+
1516
+ body.middag-active [data-slot="sidebar-container"] {
1517
+ left: 0 !important;
1518
+ }`;
1519
+ } else {
1520
+ hostSection = `
1521
+
1522
+ /* \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
1523
+ * Set --host-header-height and --host-sidebar-width as needed.
1524
+ */
1525
+
1526
+ :root {
1527
+ --host-header-height: 0px;
1528
+ --host-sidebar-width: 0px;
1529
+ }`;
1530
+ }
1531
+
1532
+ // If theme.css exists, append; otherwise create with host section only
1533
+ if (existsSync(themePath)) {
1534
+ try {
1535
+ const existing = readFileSync(themePath, "utf-8");
1536
+ writeFileSync(themePath, existing + hostSection + "\n");
1537
+ success(`Appended ${host.name} integration CSS to src/theme.css`);
1538
+ } catch (err) {
1539
+ error(`Failed to append to src/theme.css: ${err.message}`);
1540
+ }
1541
+ } else {
1542
+ ensureDir(join(targetDir, "src"));
1543
+ writeFile(themePath, hostSection.trimStart() + "\n", "src/theme.css (host integration)");
1544
+ }
1545
+ }
1546
+
1143
1547
  // ── LEGACY (kept for backward compat, delegates to FREE) ────────────────
1144
1548
 
1145
1549
  /** @deprecated Use scaffoldFreeApp + scaffoldFreeAdapters instead */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-middag-ui",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Bootstrap a MIDDAG React UI layer in your Moodle or WordPress plugin",
6
6
  "bin": {