create-middag-ui 0.11.0 → 0.13.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/scaffold.js CHANGED
@@ -8,9 +8,9 @@
8
8
  */
9
9
 
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { join, basename, dirname } from "node:path";
11
+ import { basename, dirname, join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import { success, warn, error } from "./ui.js";
13
+ import { error, success, warn } from "./ui.js";
14
14
 
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
 
@@ -94,6 +94,13 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
94
94
  deps["sonner"] = "^2.0.0";
95
95
  }
96
96
 
97
+ // Moodle AMD build needs Tailwind Vite plugin for CSS processing
98
+ const moodleDevDeps = {};
99
+ if (hostKey === "moodle") {
100
+ moodleDevDeps["@tailwindcss/vite"] = "^4.0.0";
101
+ moodleDevDeps["tailwindcss"] = "^4.0.0";
102
+ }
103
+
97
104
  const scripts = {
98
105
  dev: "vite",
99
106
  build: "vite build",
@@ -140,6 +147,7 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
140
147
  eslint: "^9.0.0",
141
148
  prettier: "^3.0.0",
142
149
  "prettier-plugin-tailwindcss": "^0.6.0",
150
+ ...moodleDevDeps,
143
151
  },
144
152
  };
145
153
 
@@ -166,7 +174,7 @@ export function scaffoldTsconfig(targetDir) {
166
174
  paths: { "@/*": ["./src/*"] },
167
175
  baseUrl: ".",
168
176
  },
169
- include: ["src"],
177
+ include: ["src", "mock"],
170
178
  };
171
179
 
172
180
  writeFile(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "tsconfig.json");
@@ -312,6 +320,7 @@ export function scaffoldIndexHtml(targetDir) {
312
320
  -->
313
321
  <div id="root" class="middag-root"></div>
314
322
  <div id="middag-portals" class="middag-root"></div>
323
+ <script>window.__MIDDAG_MOCK_NAVIGATE__ = true;</script>
315
324
  <script type="module" src="/src/main.tsx"></script>
316
325
  </body>
317
326
  </html>
@@ -442,6 +451,22 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
442
451
  *
443
452
  * Import order matters \u2014 this file is loaded AFTER @middag-io/react/style.css
444
453
  * so overrides here take precedence.
454
+ *
455
+ * \u2500\u2500 Built-in themes (PRO) \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
456
+ * PRO users: 4 built-in themes are imported in main.tsx:
457
+ * classic (Maia) \u2014 default, warm neutral, shadcn/ui feel
458
+ * enterprise \u2014 Jira/Linear, dense, corporate blue
459
+ * soft \u2014 Notion/Craft, friendly, rose/pink
460
+ * midnight \u2014 GitHub Dark/Vercel, dev-tools
461
+ *
462
+ * Switch via: document.body.classList.add("theme-enterprise")
463
+ * See: https://ui-docs.middag.io/guides/theme#built-in-themes
464
+ *
465
+ * \u2500\u2500 Custom themes (all users) \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
466
+ * Override :root for global changes, or create a .theme-{name} class
467
+ * for a switchable theme. Both Community and PRO users can create
468
+ * unlimited custom themes.
469
+ * See: https://ui-docs.middag.io/guides/theme#custom-themes
445
470
  */
446
471
 
447
472
  /* \u2500\u2500 Global token overrides \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
@@ -460,13 +485,13 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
460
485
  }
461
486
  */
462
487
 
463
- /* \u2500\u2500 Example theme: Ocean \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
488
+ /* \u2500\u2500 Example custom theme: Ocean \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
464
489
  *
465
- * A complete theme override scoped to a CSS class.
466
- * Apply with: <div id="root" class="middag-root theme-ocean">
490
+ * A complete custom theme scoped to a CSS class.
491
+ * Apply with: document.body.classList.add("theme-ocean")
467
492
  *
468
- * This is how MIDDAG themes work \u2014 redefine tokens in a scope.
469
- * The scoped class overrides :root values for everything inside it.
493
+ * Override colors, radius, density, shadows, and layout in one class.
494
+ * Dark mode: use :root.dark .theme-ocean for dark overrides.
470
495
  */
471
496
 
472
497
  .theme-ocean {
@@ -482,6 +507,13 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
482
507
  --info: oklch(0.55 0.14 200);
483
508
  --info-foreground: oklch(0.98 0 0);
484
509
 
510
+ /* Shape \u2014 tighter corners, denser rows */
511
+ --radius: 0.5rem;
512
+ --radius-sm: 4px;
513
+ --radius-lg: 8px;
514
+ --size-table-row: 44px;
515
+ --size-button-md: 34px;
516
+
485
517
  /* Sidebar */
486
518
  --sidebar: oklch(0.15 0.03 230);
487
519
  --sidebar-foreground: oklch(0.75 0.02 230);
@@ -493,6 +525,16 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
493
525
  --sidebar-border: oklch(0.25 0.03 230);
494
526
  }
495
527
 
528
+ /* Dark mode for custom theme */
529
+ :root.dark .theme-ocean,
530
+ [data-theme="dark"] .theme-ocean {
531
+ --primary: oklch(0.65 0.14 230);
532
+ --primary-foreground: oklch(0.1 0.01 230);
533
+ --background: oklch(0.12 0.01 230);
534
+ --sidebar: oklch(0.10 0.02 230);
535
+ --sidebar-foreground: oklch(0.70 0.01 230);
536
+ }
537
+
496
538
  /* \u2500\u2500 Custom project styles \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
497
539
  *
498
540
  * Add project-specific styles below. These can target MIDDAG components
@@ -506,17 +548,9 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
506
548
  * Radius: var(--radius-sm|md|lg|xl|2xl|full)
507
549
  * Shadows: var(--shadow-xs|sm|md|lg|xl|2xl)
508
550
  * Motion: var(--duration-fast|normal|moderate|slow)
551
+ * Sizing: var(--size-button-sm|md|lg), var(--size-table-row),
552
+ * var(--size-input), var(--sidebar-width)
509
553
  */
510
-
511
- /* Example: branded page header */
512
- /*
513
- .my-page-header {
514
- background: linear-gradient(135deg, var(--primary) 0%, var(--info) 100%);
515
- color: var(--primary-foreground);
516
- padding: var(--space-6) var(--space-8);
517
- border-radius: var(--radius-lg);
518
- }
519
- */
520
554
  `,
521
555
  "src/theme.css",
522
556
  );
@@ -634,7 +668,7 @@ export const dashboardContract: PageContract = {
634
668
  `/**
635
669
  * Connectors page contract \u2014 INTERMEDIATE example.
636
670
  *
637
- * Demonstrates the "split" layout with three block types:
671
+ * Demonstrates the "sidebar" layout with three block types:
638
672
  * - card_grid (connector cards in the main region)
639
673
  * - status_strip (health indicators in the aside)
640
674
  * - detail_panel (metadata in the aside)
@@ -658,7 +692,7 @@ export const connectorsContract: PageContract = {
658
692
  ],
659
693
  },
660
694
  layout: {
661
- template: "split",
695
+ template: "sidebar",
662
696
  regions: {
663
697
  main: [
664
698
  {
@@ -1265,7 +1299,8 @@ export function scaffoldHostEntry(targetDir, hostKey) {
1265
1299
  const label = `src/entry-${hostKey}.tsx`;
1266
1300
  if (skipIfExists(filePath, label)) return;
1267
1301
 
1268
- // Host-specific setup and post-mount code
1302
+ // Host-specific imports, setup, and post-mount code
1303
+ let extraImports = "";
1269
1304
  let setupCode = "";
1270
1305
  let postMountCode = "";
1271
1306
 
@@ -1290,7 +1325,20 @@ export function scaffoldHostEntry(targetDir, hostKey) {
1290
1325
  });
1291
1326
  observer.observe(document.body, { childList: true, subtree: true });`;
1292
1327
  } else if (hostKey === "moodle") {
1293
- setupCode = ` document.body.classList.add("middag-active");`;
1328
+ extraImports = `import "./tailwind.css";
1329
+ import "@fontsource-variable/figtree";`;
1330
+ setupCode = ` document.body.classList.add("middag-active");
1331
+
1332
+ // Portal container for Radix UI (modals, popovers, toasts).
1333
+ // Radix portals default to document.body which is outside .middag-root,
1334
+ // meaning scoped Tailwind styles won't apply. This creates a sibling
1335
+ // container with .middag-root so portal content inherits design tokens.
1336
+ if (!document.getElementById("middag-portals")) {
1337
+ const portalContainer = document.createElement("div");
1338
+ portalContainer.id = "middag-portals";
1339
+ portalContainer.classList.add("middag-root");
1340
+ document.body.appendChild(portalContainer);
1341
+ }`;
1294
1342
  }
1295
1343
 
1296
1344
  const content = `/**
@@ -1304,39 +1352,31 @@ export function scaffoldHostEntry(targetDir, hostKey) {
1304
1352
  */
1305
1353
  import { createRoot } from "react-dom/client";
1306
1354
  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";
1355
+ import { I18nProvider, ProgressProvider } from "@middag-io/react";
1317
1356
  import "@middag-io/react/style.css";
1318
1357
  import "./theme.css";
1358
+ ${ extraImports }import { registerDefaults } from "./app/register";
1359
+ import { resolvePageComponent } from "./app/page-resolver";
1319
1360
 
1320
1361
  registerDefaults();
1321
- registerShell("product", HostProductShell);
1322
1362
 
1323
1363
  createInertiaApp({
1324
1364
  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
- },
1365
+ resolve: (name) => resolvePageComponent(name),
1336
1366
  setup({ el, App, props }) {
1337
1367
  el.classList.add("middag-root");
1338
1368
  ${setupCode}
1339
- createRoot(el).render(<App {...props} />);
1369
+ createRoot(el).render(
1370
+ <ProgressProvider>
1371
+ <App {...props}>
1372
+ {({ Component, props: pageProps, key }) => (
1373
+ <I18nProvider>
1374
+ <Component key={key} {...pageProps} />
1375
+ </I18nProvider>
1376
+ )}
1377
+ </App>
1378
+ </ProgressProvider>,
1379
+ );
1340
1380
  ${postMountCode}
1341
1381
  },
1342
1382
  });
@@ -1372,17 +1412,71 @@ export function scaffoldHostViteConfig(targetDir, hostKey, host) {
1372
1412
  },
1373
1413
  },`;
1374
1414
  } else if (hostKey === "moodle") {
1375
- outDir = `resolve(__dirname, "../amd/build")`;
1376
- formats = `["iife"]`;
1377
- libName = `"MiddagUI"`;
1378
- fileName = `() => "app.js"`;
1379
- extraRollup = `
1415
+ // Moodle uses AMD format with vite-plugin-moodle-amd.
1416
+ // Generate a dedicated AMD config instead of the shared lib template.
1417
+ const content = `/**
1418
+ * Vite build config for Moodle — AMD production build.
1419
+ *
1420
+ * Usage:
1421
+ * npm run build:moodle \u2192 AMD chunks to dist/ + amd/
1422
+ * npm run watch:moodle \u2192 rebuild on change
1423
+ *
1424
+ * Output: AMD modules compatible with Moodle's RequireJS loader.
1425
+ * vite-plugin-moodle-amd rewrites chunk paths and copies to amd/.
1426
+ * CSS is copied to styles/middag-app.css (Moodle auto-discovers it).
1427
+ *
1428
+ * The dev server (\`npm run dev\`) uses vite.config.ts instead.
1429
+ */
1430
+ import { defineConfig } from "vite";
1431
+ import react from "@vitejs/plugin-react";
1432
+ import tailwindcss from "@tailwindcss/vite";
1433
+ import moodleAmd from "./plugins/vite-plugin-moodle-amd";
1434
+ import { resolve } from "path";
1435
+
1436
+ export default defineConfig({
1437
+ plugins: [react(), tailwindcss(), moodleAmd()],
1438
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
1439
+ resolve: { alias: { "@/": resolve(__dirname, "src") + "/" } },
1440
+ build: {
1441
+ outDir: resolve(__dirname, "dist"),
1442
+ emptyOutDir: true,
1443
+ minify: "esbuild",
1444
+ cssCodeSplit: false,
1445
+ rollupOptions: {
1446
+ input: { "middag-app": resolve(__dirname, "src/entry-moodle.tsx") },
1447
+ external: ["core/ajax", "core/str", "core/notification", "jquery"],
1380
1448
  output: {
1381
- assetFileNames: (assetInfo) => {
1382
- if (assetInfo.name?.endsWith(".css")) return "style.css";
1383
- return assetInfo.name || "[name]-[hash][extname]";
1449
+ format: "amd",
1450
+ dir: resolve(__dirname, "dist"),
1451
+ entryFileNames: "[name].js",
1452
+ chunkFileNames: "[name].js",
1453
+ manualChunks(id) {
1454
+ if (
1455
+ id.includes("node_modules/react/") ||
1456
+ id.includes("node_modules/react-dom/") ||
1457
+ id.includes("node_modules/scheduler/") ||
1458
+ id.includes("node_modules/@inertiajs/")
1459
+ ) {
1460
+ return "react-vendor-lazy";
1461
+ }
1462
+ if (
1463
+ id.includes("node_modules/@radix-ui/") ||
1464
+ id.includes("node_modules/@base-ui/") ||
1465
+ id.includes("node_modules/class-variance-authority/")
1466
+ ) {
1467
+ return "react-ui-lazy";
1468
+ }
1469
+ if (id.includes("node_modules/@tanstack/")) {
1470
+ return "react-table-lazy";
1471
+ }
1384
1472
  },
1385
- },`;
1473
+ },
1474
+ },
1475
+ },
1476
+ });
1477
+ `;
1478
+ writeFile(filePath, content, label);
1479
+ return;
1386
1480
  } else {
1387
1481
  outDir = `resolve(__dirname, "../dist")`;
1388
1482
  formats = `["es"]`;
@@ -1445,7 +1539,7 @@ export default defineConfig({
1445
1539
  export function scaffoldHostThemeCSS(targetDir, hostKey, host) {
1446
1540
  const themePath = join(targetDir, "src", "theme.css");
1447
1541
 
1448
- let hostSection = "";
1542
+ let hostSection;
1449
1543
 
1450
1544
  if (hostKey === "wordpress") {
1451
1545
  hostSection = `
@@ -1505,8 +1599,18 @@ body.middag-active {
1505
1599
  } else if (hostKey === "moodle") {
1506
1600
  hostSection = `
1507
1601
 
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).
1602
+ /* ── Moodle Boost integration ────────────────────────────────────────────
1603
+ * Active when MIDDAG mounts inside Moodle (body.middag-active).
1604
+ *
1605
+ * CSS Isolation Strategy:
1606
+ *
1607
+ * OUTWARD (MIDDAG → Moodle):
1608
+ * Tailwind utility classes only match elements that carry them.
1609
+ * Known collisions with Bootstrap are neutralized below.
1610
+ *
1611
+ * INWARD (Moodle → MIDDAG):
1612
+ * .middag-root blocks inherited Moodle/Bootstrap styles.
1613
+ * Portals render inside #middag-portals (also .middag-root).
1510
1614
  */
1511
1615
 
1512
1616
  body.middag-active {
@@ -1515,7 +1619,31 @@ body.middag-active {
1515
1619
 
1516
1620
  body.middag-active [data-slot="sidebar-container"] {
1517
1621
  left: 0 !important;
1518
- }`;
1622
+ }
1623
+
1624
+ /* ── Outward isolation: Tailwind ↔ Bootstrap collision fixes ─────────────
1625
+ *
1626
+ * .collapse — Bootstrap uses display:none/block for collapsible fieldsets.
1627
+ * Tailwind emits \`.collapse { visibility: collapse }\` which breaks them.
1628
+ * Neutralize outside the React tree (.middag-root).
1629
+ *
1630
+ * Add new entries here as collisions are discovered.
1631
+ */
1632
+ .collapse:not(.middag-root .collapse, .middag-root.collapse) {
1633
+ visibility: visible !important;
1634
+ }
1635
+
1636
+ /* ── Theme bridge: inherit Moodle Boost primary color ────────────────────
1637
+ *
1638
+ * If Boost defines --bs-primary, map it to --middag-brand so the
1639
+ * MIDDAG UI inherits the institution's brand color automatically.
1640
+ * Uncomment and adjust if your Moodle theme uses a different variable.
1641
+ */
1642
+ /*
1643
+ :root {
1644
+ --middag-brand: var(--bs-primary);
1645
+ }
1646
+ */`;
1519
1647
  } else {
1520
1648
  hostSection = `
1521
1649
 
@@ -1787,3 +1915,136 @@ export const router = {
1787
1915
  );
1788
1916
  }
1789
1917
  }
1918
+
1919
+ // ── Template reader ─────────────────────────────────────────────────────
1920
+
1921
+ /** Read a template file relative to this script's directory. */
1922
+ function readTemplate(relativePath) {
1923
+ return readFileSync(join(__dirname, relativePath), "utf-8");
1924
+ }
1925
+
1926
+ // ── Shared: register, page-resolver, route helper, demo page ────────────
1927
+
1928
+ /**
1929
+ * Scaffold FREE register: src/app/register.ts — minimal (5 blocks).
1930
+ */
1931
+ export function scaffoldFreeRegister(targetDir) {
1932
+ ensureDir(join(targetDir, "src", "app"));
1933
+ const filePath = join(targetDir, "src", "app", "register.ts");
1934
+ if (skipIfExists(filePath, "src/app/register.ts")) return;
1935
+ writeFile(filePath, readTemplate("templates/shared/register-free.ts"), "src/app/register.ts (FREE)");
1936
+ }
1937
+
1938
+ /**
1939
+ * Scaffold page resolver: src/app/page-resolver.tsx.
1940
+ * Supports Contract: prefix pages (ContractPage) and direct pages (glob).
1941
+ */
1942
+ export function scaffoldPageResolver(targetDir) {
1943
+ ensureDir(join(targetDir, "src", "app"));
1944
+ const filePath = join(targetDir, "src", "app", "page-resolver.tsx");
1945
+ if (skipIfExists(filePath, "src/app/page-resolver.tsx")) return;
1946
+ writeFile(filePath, readTemplate("templates/shared/page-resolver.tsx"), "src/app/page-resolver.tsx");
1947
+ }
1948
+
1949
+ /**
1950
+ * Scaffold route helper: src/lib/routes.ts.
1951
+ * Abstracts host admin URL vs dev mock path.
1952
+ *
1953
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1954
+ */
1955
+ /**
1956
+ * @param {string} targetDir
1957
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1958
+ * @param {string} [pluginSlug] - For Moodle: slug portion of frankenstyle (e.g. "middag" from "local_middag")
1959
+ */
1960
+ export function scaffoldRouteHelper(targetDir, hostKey, pluginSlug) {
1961
+ ensureDir(join(targetDir, "src", "lib"));
1962
+ const filePath = join(targetDir, "src", "lib", "routes.ts");
1963
+ if (skipIfExists(filePath, "src/lib/routes.ts")) return;
1964
+
1965
+ const templateMap = {
1966
+ wordpress: "templates/shared/route-helper-wp.ts",
1967
+ moodle: "templates/shared/route-helper-moodle.ts",
1968
+ custom: "templates/shared/route-helper-custom.ts",
1969
+ };
1970
+ const template = templateMap[hostKey] || templateMap.wordpress;
1971
+
1972
+ try {
1973
+ let content = readTemplate(template);
1974
+ if (hostKey === "moodle" && pluginSlug) {
1975
+ // Convert frankenstyle slug to Moodle path: "middag" with type prefix "local" → "local/middag"
1976
+ // pluginSlug is the name part; the full frankenstyle comes from cli.js
1977
+ content = content.replace(/__PLUGIN_PATH__/g, pluginSlug);
1978
+ }
1979
+ writeFile(filePath, content, "src/lib/routes.ts");
1980
+ } catch {
1981
+ // Fallback to WP template if host-specific one doesn't exist
1982
+ writeFile(filePath, readTemplate("templates/shared/route-helper-wp.ts"), "src/lib/routes.ts");
1983
+ }
1984
+ }
1985
+
1986
+ /**
1987
+ * Scaffold demo direct page: src/pages/DemoPage.tsx.
1988
+ * Shows the direct page pattern (usePage + custom React).
1989
+ */
1990
+ export function scaffoldDemoDirectPage(targetDir) {
1991
+ ensureDir(join(targetDir, "src", "pages"));
1992
+ const filePath = join(targetDir, "src", "pages", "DemoPage.tsx");
1993
+ if (skipIfExists(filePath, "src/pages/DemoPage.tsx")) return;
1994
+ writeFile(filePath, readTemplate("templates/shared/demo-page.tsx"), "src/pages/DemoPage.tsx");
1995
+ }
1996
+
1997
+ // ── Moodle-specific scaffold functions ────────────────────────────────────
1998
+
1999
+ /**
2000
+ * Scaffold vite-plugin-moodle-amd: plugins/vite-plugin-moodle-amd.ts.
2001
+ * Converts Rollup AMD output to Moodle's RequireJS format and copies
2002
+ * built files to amd/src/ + amd/build/ + styles/.
2003
+ *
2004
+ * @param {string} targetDir - Absolute path to UI dir
2005
+ * @param {string} pluginPrefix - Moodle frankenstyle (e.g. "local_middag")
2006
+ */
2007
+ export function scaffoldMoodlePlugin(targetDir, pluginPrefix) {
2008
+ ensureDir(join(targetDir, "plugins"));
2009
+ const filePath = join(targetDir, "plugins", "vite-plugin-moodle-amd.ts");
2010
+ if (skipIfExists(filePath, "plugins/vite-plugin-moodle-amd.ts")) return;
2011
+ const content = readTemplate("templates/shared/vite-plugin-moodle-amd.ts")
2012
+ .replace(/__PLUGIN_PREFIX__/g, pluginPrefix);
2013
+ writeFile(filePath, content, "plugins/vite-plugin-moodle-amd.ts");
2014
+ }
2015
+
2016
+ /**
2017
+ * Scaffold Tailwind CSS entry for Moodle AMD build: src/tailwind.css.
2018
+ * Brings in the Tailwind engine so local pages get their utilities compiled.
2019
+ */
2020
+ export function scaffoldMoodleTailwind(targetDir) {
2021
+ ensureDir(join(targetDir, "src"));
2022
+ const filePath = join(targetDir, "src", "tailwind.css");
2023
+ if (skipIfExists(filePath, "src/tailwind.css")) return;
2024
+ writeFile(filePath, readTemplate("templates/shared/tailwind.css"), "src/tailwind.css");
2025
+ }
2026
+
2027
+ /**
2028
+ * Scaffold Moodle AMD adapters: src/lib/moodle/.
2029
+ * Typed wrappers for core/ajax, core/str, core/notification.
2030
+ *
2031
+ * @param {string} targetDir - Absolute path to UI dir
2032
+ * @param {string} pluginPrefix - Moodle frankenstyle (e.g. "local_middag")
2033
+ */
2034
+ export function scaffoldMoodleAdapters(targetDir, pluginPrefix) {
2035
+ ensureDir(join(targetDir, "src", "lib", "moodle"));
2036
+
2037
+ const files = [
2038
+ { template: "templates/shared/moodle-ajax.ts", dest: "src/lib/moodle/ajax.ts" },
2039
+ { template: "templates/shared/moodle-strings.ts", dest: "src/lib/moodle/strings.ts" },
2040
+ { template: "templates/shared/moodle-notification.ts", dest: "src/lib/moodle/notification.ts" },
2041
+ ];
2042
+
2043
+ for (const { template, dest } of files) {
2044
+ const filePath = join(targetDir, dest);
2045
+ if (skipIfExists(filePath, dest)) continue;
2046
+ const content = readTemplate(template)
2047
+ .replace(/__PLUGIN_PREFIX__/g, pluginPrefix);
2048
+ writeFile(filePath, content, dest);
2049
+ }
2050
+ }