create-middag-ui 0.17.0 → 0.19.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
@@ -29,15 +29,17 @@ import {
29
29
  scaffoldDemoFiles,
30
30
  scaffoldDevShell,
31
31
  scaffoldEslintConfig,
32
+ scaffoldGitignore,
32
33
  scaffoldFreeAdapters,
33
34
  scaffoldFreeApp,
34
35
  scaffoldFreeRegister,
35
- scaffoldProAdapters,
36
36
  scaffoldHostEntry,
37
37
  scaffoldHostThemeCSS,
38
38
  scaffoldHostViteConfig,
39
39
  scaffoldIndexHtml,
40
40
  scaffoldMoodleAdapters,
41
+ scaffoldMoodleAdminShell,
42
+ scaffoldMoodlePageResolver,
41
43
  scaffoldMoodlePlugin,
42
44
  scaffoldMoodleTailwind,
43
45
  scaffoldPackageJson,
@@ -166,11 +168,24 @@ if (!dirCreated) {
166
168
 
167
169
  heading(5, TOTAL_STEPS, "Scaffolding config files");
168
170
 
169
- scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey);
171
+ const isPro = registryPath === "github";
172
+
173
+ // PRO only: @middag-io/licensing is a build-time delivery manifest, not a
174
+ // runtime gate. Not every PRO product needs it, so make it opt-in (default no).
175
+ const withLicensing =
176
+ isPro && !nonInteractive
177
+ ? await confirm(
178
+ "Include @middag-io/licensing (build-time delivery manifest)?",
179
+ false,
180
+ )
181
+ : false;
182
+
183
+ scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey, withLicensing);
170
184
  scaffoldTsconfig(targetDir);
171
185
  scaffoldViteConfig(targetDir, host, registryPath);
172
186
  scaffoldEslintConfig(targetDir);
173
187
  scaffoldPrettierConfig(targetDir);
188
+ scaffoldGitignore(targetDir);
174
189
  scaffoldIndexHtml(targetDir);
175
190
 
176
191
  // ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
@@ -192,13 +207,16 @@ scaffoldDemoFiles(targetDir);
192
207
 
193
208
  // ── Step 8: Scaffold app + page examples (PRO vs FREE) ─────────────────
194
209
 
195
- const isPro = registryPath === "github";
196
210
  heading(8, TOTAL_STEPS, `Creating ${isPro ? "PRO" : "FREE"} UI module`);
197
211
 
198
212
  scaffoldPageExamples(targetDir);
199
213
 
200
- // Shared files (both PRO and FREE)
201
- scaffoldPageResolver(targetDir);
214
+ // Page resolver — Moodle gets AMD version (RequireJS + extension globs)
215
+ if (hostKey === "moodle") {
216
+ scaffoldMoodlePageResolver(targetDir);
217
+ } else {
218
+ scaffoldPageResolver(targetDir);
219
+ }
202
220
  scaffoldDemoDirectPage(targetDir);
203
221
  // Convert frankenstyle "local_middag" → Moodle path "local/middag"
204
222
  const moodlePath = moodleComponent ? moodleComponent.replace("_", "/") : null;
@@ -214,10 +232,7 @@ if (isPro) {
214
232
  pro.scaffoldMockEntities(targetDir);
215
233
  pro.scaffoldMockPageContracts(targetDir);
216
234
  pro.scaffoldMockRoutes(targetDir);
217
- // PRO Inertia adapters re-export from @middag-io/react/mock so usePage()
218
- // shares the same React context as MockPageProvider (no context mismatch).
219
- scaffoldProAdapters(targetDir);
220
- success("PRO: using MockProductShell from @middag-io/react/mock");
235
+ success("PRO: host-sim dev harness inherited from @middag-io/react-demo");
221
236
  } catch {
222
237
  // npm version — PRO file excluded, fall back to FREE
223
238
  info("PRO scaffold not available — using FREE path");
@@ -237,7 +252,7 @@ if (isPro) {
237
252
 
238
253
  // Host-specific production files (entry, vite config, theme CSS)
239
254
  scaffoldHostEntry(targetDir, hostKey);
240
- scaffoldHostViteConfig(targetDir, hostKey, host);
255
+ scaffoldHostViteConfig(targetDir, hostKey, host, withLicensing);
241
256
  scaffoldHostThemeCSS(targetDir, hostKey, host);
242
257
 
243
258
  // Moodle-specific: AMD plugin + Moodle adapters (ajax, strings, notification)
@@ -245,7 +260,8 @@ if (hostKey === "moodle" && moodleComponent) {
245
260
  scaffoldMoodlePlugin(targetDir, moodleComponent);
246
261
  scaffoldMoodleTailwind(targetDir);
247
262
  scaffoldMoodleAdapters(targetDir, moodleComponent);
248
- success(`Moodle: AMD plugin + Tailwind + adapters for ${ moodleComponent }`);
263
+ scaffoldMoodleAdminShell(targetDir);
264
+ success(`Moodle: AMD plugin + Tailwind + adapters + AdminShell for ${ moodleComponent }`);
249
265
  }
250
266
 
251
267
  // ── Step 9: npm install ──────────────────────────────────────────────────
package/lib/scaffold.js CHANGED
@@ -91,7 +91,7 @@ export function createTargetDir(targetDir) {
91
91
  * @param {string} registryPath - "github" (PRO) or "public" (FREE)
92
92
  * @param {string} [hostKey] - 'wordpress' | 'moodle' | 'custom' (adds host build scripts)
93
93
  */
94
- export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey) {
94
+ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey, withLicensing = false) {
95
95
  const filePath = join(targetDir, "package.json");
96
96
  if (skipIfExists(filePath, "package.json")) return;
97
97
 
@@ -103,7 +103,12 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
103
103
  "react-router": "^7.0.0",
104
104
  };
105
105
  if (isPro) {
106
+ deps["@middag-io/react-pro"] = `^${getLibVersion()}`;
106
107
  deps["sonner"] = "^2.0.0";
108
+ // Opt-in: build-time delivery manifest, not every PRO product needs it.
109
+ if (withLicensing) {
110
+ deps["@middag-io/licensing"] = "^0.1.0";
111
+ }
107
112
  }
108
113
 
109
114
  // Moodle AMD build needs Tailwind Vite plugin for CSS processing
@@ -113,6 +118,13 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
113
118
  moodleDevDeps["tailwindcss"] = "^4.0.0";
114
119
  }
115
120
 
121
+ // PRO: dev harness (host-sim shell, Inertia mocks, i18n) inherited at dev time
122
+ // from @middag-io/react-demo. Dev-only — production entry-*.tsx never imports it.
123
+ const proDevDeps = {};
124
+ if (isPro) {
125
+ proDevDeps["@middag-io/react-demo"] = `^${getLibVersion()}`;
126
+ }
127
+
116
128
  const scripts = {
117
129
  dev: "vite",
118
130
  build: "vite build",
@@ -138,6 +150,7 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
138
150
  const pkg = {
139
151
  name: `${projectName}-ui`,
140
152
  private: true,
153
+ version: "0.1.0",
141
154
  type: "module",
142
155
  scripts,
143
156
  dependencies: deps,
@@ -160,6 +173,7 @@ export function scaffoldPackageJson(targetDir, host, cwd, registryPath, hostKey)
160
173
  prettier: "^3.0.0",
161
174
  "prettier-plugin-tailwindcss": "^0.6.0",
162
175
  ...moodleDevDeps,
176
+ ...proDevDeps,
163
177
  },
164
178
  };
165
179
 
@@ -190,6 +204,11 @@ export function scaffoldTsconfig(targetDir) {
190
204
  };
191
205
 
192
206
  writeFile(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "tsconfig.json");
207
+
208
+ const envFilePath = join(targetDir, "src", "vite-env.d.ts");
209
+ if (!skipIfExists(envFilePath, "src/vite-env.d.ts")) {
210
+ writeFile(envFilePath, `/// <reference types="vite/client" />\n`, "src/vite-env.d.ts");
211
+ }
193
212
  }
194
213
 
195
214
  /**
@@ -202,9 +221,19 @@ export function scaffoldViteConfig(targetDir, host, registryPath) {
202
221
  const filePath = join(targetDir, "vite.config.ts");
203
222
  if (skipIfExists(filePath, "vite.config.ts")) return;
204
223
 
205
- const adapterComment = registryPath === "github"
206
- ? "// PRO: Inertia mocks re-exported from @middag-io/react/mock (same context)"
207
- : "// FREE: Inertia mocks from local adapters";
224
+ const optimizeInclude = registryPath === "github"
225
+ ? '["@middag-io/react", "@middag-io/react-pro", "@middag-io/react-demo"]'
226
+ : '["@middag-io/react"]';
227
+
228
+ // PRO inherits the dev harness (host-sim shell + Inertia mocks) from
229
+ // @middag-io/react-demo, so @inertiajs/* alias to it. FREE: local adapters.
230
+ const inertiaAlias = registryPath === "github"
231
+ ? '// PRO: Inertia mocks inherited from @middag-io/react-demo (dev harness)\n' +
232
+ ' "@inertiajs/react": "@middag-io/react-demo",\n' +
233
+ ' "@inertiajs/core": "@middag-io/react-demo",'
234
+ : '// FREE: Inertia mocks from local self-contained adapters\n' +
235
+ ' "@inertiajs/react": resolve(__dirname, "mock/adapters/inertia-react.ts"),\n' +
236
+ ' "@inertiajs/core": resolve(__dirname, "mock/adapters/inertia-core.ts"),';
208
237
 
209
238
  const content = `/**
210
239
  * Vite config \u2014 used by \`npm run dev\` and \`npm run build\`.
@@ -221,15 +250,13 @@ export default defineConfig({
221
250
  plugins: [react()],
222
251
  server: { port: ${host.port} },
223
252
  optimizeDeps: {
224
- include: ["@middag-io/react", "@middag-io/react/mock"],
253
+ include: ${optimizeInclude},
225
254
  },
226
255
  resolve: {
227
256
  alias: {
228
257
  "@/": resolve(__dirname, "src") + "/",
229
258
  "@mock/": resolve(__dirname, "mock") + "/",
230
- ${adapterComment}
231
- "@inertiajs/react": resolve(__dirname, "mock/adapters/inertia-react.ts"),
232
- "@inertiajs/core": resolve(__dirname, "mock/adapters/inertia-core.ts"),
259
+ ${inertiaAlias}
233
260
  },
234
261
  },
235
262
  });
@@ -245,6 +272,9 @@ export function scaffoldEslintConfig(targetDir) {
245
272
  const filePath = join(targetDir, "eslint.config.js");
246
273
  if (skipIfExists(filePath, "eslint.config.js")) return;
247
274
 
275
+ const ignores = ["dist/", "node_modules/", "scripts/"];
276
+ const ignoresStr = ignores.map((i) => `"${i}"`).join(", ");
277
+
248
278
  writeFile(
249
279
  filePath,
250
280
  `import js from "@eslint/js";
@@ -254,7 +284,7 @@ import reactRefresh from "eslint-plugin-react-refresh";
254
284
  import prettierConfig from "eslint-config-prettier";
255
285
 
256
286
  export default tseslint.config(
257
- { ignores: ["dist/", "node_modules/"] },
287
+ { ignores: [${ignoresStr}] },
258
288
  js.configs.recommended,
259
289
  ...tseslint.configs.recommended,
260
290
  {
@@ -264,7 +294,8 @@ export default tseslint.config(
264
294
  "react-refresh": reactRefresh,
265
295
  },
266
296
  rules: {
267
- ...reactHooks.configs.recommended.rules,
297
+ "react-hooks/rules-of-hooks": "error",
298
+ "react-hooks/exhaustive-deps": "warn",
268
299
  "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
269
300
  "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
270
301
  "@typescript-eslint/no-explicit-any": "warn",
@@ -294,6 +325,7 @@ export function scaffoldPrettierConfig(targetDir) {
294
325
  trailingComma: "all",
295
326
  printWidth: 100,
296
327
  plugins: ["prettier-plugin-tailwindcss"],
328
+ tailwindFunctions: ["cn", "cva"],
297
329
  },
298
330
  null,
299
331
  2,
@@ -302,6 +334,25 @@ export function scaffoldPrettierConfig(targetDir) {
302
334
  );
303
335
  }
304
336
 
337
+ /**
338
+ * Scaffold .gitignore with UI-specific dist patterns.
339
+ */
340
+ export function scaffoldGitignore(targetDir) {
341
+ const filePath = join(targetDir, ".gitignore");
342
+ if (skipIfExists(filePath, ".gitignore")) return;
343
+
344
+ const lines = [
345
+ "node_modules/",
346
+ "dist/",
347
+ "dist-master/",
348
+ "dist-mock/",
349
+ "dist-lib/",
350
+ "*.log",
351
+ ".vite/",
352
+ ];
353
+ writeFile(filePath, lines.join("\n") + "\n", ".gitignore");
354
+ }
355
+
305
356
  /**
306
357
  * Scaffold index.html at project root.
307
358
  */
@@ -927,132 +978,6 @@ export const settingsContract: PageContract = {
927
978
  }
928
979
  }
929
980
 
930
- // ── App files — PRO path (GitHub Packages) ─────────────────────────────
931
-
932
- /**
933
- * Scaffold PRO app: src/main.tsx + src/app.tsx.
934
- * Uses mock barrel from @middag-io/react/mock. No local adapters/shell.
935
- */
936
- export function scaffoldProApp(targetDir) {
937
- ensureDir(join(targetDir, "src"));
938
- ensureDir(join(targetDir, "mock", "adapters"));
939
-
940
- // Inertia adapter shims — re-export from pre-built mock bundle
941
- // so usePage() shares the same MockPageContext as MockPageProvider.
942
- const adapterReactPath = join(targetDir, "mock", "adapters", "inertia-react.ts");
943
- if (!skipIfExists(adapterReactPath, "mock/adapters/inertia-react.ts")) {
944
- writeFile(adapterReactPath, `/**
945
- * Mock @inertiajs/react — re-exports from pre-built @middag-io/react/mock.
946
- *
947
- * This ensures usePage() reads from the same MockPageContext as
948
- * MockPageProvider (both live in the pre-built ESM bundle).
949
- */
950
- export { usePage, Head, Link, router } from "@middag-io/react/mock";
951
- `, "mock/adapters/inertia-react.ts");
952
- }
953
-
954
- const adapterCorePath = join(targetDir, "mock", "adapters", "inertia-core.ts");
955
- if (!skipIfExists(adapterCorePath, "mock/adapters/inertia-core.ts")) {
956
- writeFile(adapterCorePath, `/**
957
- * Mock @inertiajs/core — re-exports from pre-built @middag-io/react/mock.
958
- */
959
- export { router, setMockNavigate } from "@middag-io/react/mock";
960
- `, "mock/adapters/inertia-core.ts");
961
- }
962
-
963
- const mainPath = join(targetDir, "src", "main.tsx");
964
- if (!skipIfExists(mainPath, "src/main.tsx")) {
965
- writeFile(mainPath, `import { StrictMode } from "react";
966
- import { createRoot } from "react-dom/client";
967
- import { registerDefaults, registerShell } from "@middag-io/react";
968
- import { MockProductShell } from "@middag-io/react/mock";
969
- import "@middag-io/react/style.css";
970
- import "./theme.css";
971
- import "@fontsource-variable/figtree";
972
- import { App } from "./app";
973
-
974
- registerDefaults();
975
- registerShell("product", MockProductShell);
976
-
977
- createRoot(document.getElementById("root")!).render(
978
- <StrictMode><App /></StrictMode>,
979
- );
980
- `, "src/main.tsx");
981
- }
982
-
983
- const appPath = join(targetDir, "src", "app.tsx");
984
- if (!skipIfExists(appPath, "src/app.tsx")) {
985
- writeFile(appPath, `import { useEffect } from "react";
986
- import { BrowserRouter, Routes, Route, useNavigate } from "react-router";
987
- import { ContractPage, I18nProvider } from "@middag-io/react";
988
- import { MockPageProvider, MockI18nProvider } from "@middag-io/react/mock";
989
- import type { PageContract } from "@middag-io/react";
990
- import { dashboardContract } from "./pages/dashboard";
991
- import { connectorsContract } from "./pages/connectors";
992
- import { settingsContract } from "./pages/settings";
993
-
994
- let _navigate: ((to: string) => void) | null = null;
995
- function NavigateBridge() {
996
- const navigate = useNavigate();
997
- useEffect(() => { _navigate = (to: string) => navigate(to); }, [navigate]);
998
- return null;
999
- }
1000
- if (typeof window !== "undefined") {
1001
- (window as any).__MIDDAG_MOCK_NAVIGATE__ = (to: string) => { if (_navigate) _navigate(to); };
1002
- }
1003
-
1004
- const sharedProps = {
1005
- auth: { id: 1, name: "Dev User", email: "dev@localhost", capabilities: [] },
1006
- theme: { appearance: "light" as const, strings: {} as Record<string, string> },
1007
- flash: {},
1008
- locale: "en",
1009
- version: "0.0.0-dev",
1010
- scope: { extension: null, context: "global" },
1011
- };
1012
-
1013
- function buildNavigation(activeKey: string) {
1014
- return {
1015
- tree: [
1016
- { key: "overview.dashboard", label: "Dashboard", icon: "home", href: "/", children: [] },
1017
- { key: "integration.connectors", label: "Connectors", icon: "plug", href: "/connectors", children: [] },
1018
- ],
1019
- footer: [
1020
- { key: "system.settings", label: "Settings", icon: "settings", href: "/settings", children: [] },
1021
- ],
1022
- activeKey,
1023
- };
1024
- }
1025
-
1026
- function MockRoute({ contract, activeKey }: { contract: PageContract; activeKey: string }) {
1027
- return (
1028
- <MockPageProvider value={{ props: { ...sharedProps, contract, navigation: buildNavigation(activeKey) }, url: window.location.pathname }}>
1029
- <ContractPage contract={contract} />
1030
- </MockPageProvider>
1031
- );
1032
- }
1033
-
1034
- export function App() {
1035
- return (
1036
- <MockI18nProvider>
1037
- <MockPageProvider value={{ props: { ...sharedProps, contract: null, navigation: buildNavigation("") }, url: "/" }}>
1038
- <I18nProvider>
1039
- <BrowserRouter>
1040
- <NavigateBridge />
1041
- <Routes>
1042
- <Route path="/" element={<MockRoute contract={dashboardContract} activeKey="overview.dashboard" />} />
1043
- <Route path="/connectors" element={<MockRoute contract={connectorsContract} activeKey="integration.connectors" />} />
1044
- <Route path="/settings" element={<MockRoute contract={settingsContract} activeKey="system.settings" />} />
1045
- </Routes>
1046
- </BrowserRouter>
1047
- </I18nProvider>
1048
- </MockPageProvider>
1049
- </MockI18nProvider>
1050
- );
1051
- }
1052
- `, "src/app.tsx");
1053
- }
1054
- }
1055
-
1056
981
  /**
1057
982
  * Scaffold FREE adapters: mock/adapters/ with react-router.
1058
983
  */
@@ -1132,39 +1057,6 @@ export { router };
1132
1057
  }
1133
1058
  }
1134
1059
 
1135
- /**
1136
- * Scaffold PRO adapters: thin re-exports from @middag-io/react/mock.
1137
- *
1138
- * PRO uses MockPageProvider from the lib, so usePage() must read from the
1139
- * same React context. These adapters delegate to the lib mock instead of
1140
- * defining their own context (which would cause context mismatch).
1141
- */
1142
- export function scaffoldProAdapters(targetDir) {
1143
- ensureDir(join(targetDir, "mock", "adapters"));
1144
-
1145
- const corePath = join(targetDir, "mock", "adapters", "inertia-core.ts");
1146
- if (!skipIfExists(corePath, "mock/adapters/inertia-core.ts")) {
1147
- writeFile(corePath, `/**
1148
- * Mock @inertiajs/core — PRO re-export from @middag-io/react/mock.
1149
- * Shares the same navigate function as MockProductShell.
1150
- */
1151
- export { router, setMockNavigate } from "@middag-io/react/mock";
1152
- `, "mock/adapters/inertia-core.ts (PRO)");
1153
- }
1154
-
1155
- const reactPath = join(targetDir, "mock", "adapters", "inertia-react.ts");
1156
- if (!skipIfExists(reactPath, "mock/adapters/inertia-react.ts")) {
1157
- writeFile(reactPath, `/**
1158
- * Mock @inertiajs/react — PRO re-export from @middag-io/react/mock.
1159
- * Shares the same React context as MockPageProvider so usePage()
1160
- * returns the data set by the mock shell.
1161
- */
1162
- export { usePage, Head, Link } from "@middag-io/react/mock";
1163
- export { router } from "./inertia-core";
1164
- `, "mock/adapters/inertia-react.ts (PRO)");
1165
- }
1166
- }
1167
-
1168
1060
  /**
1169
1061
  * Scaffold FREE DevShell: src/shells/DevShell.tsx.
1170
1062
  */
@@ -1375,26 +1267,91 @@ export function scaffoldHostEntry(targetDir, hostKey) {
1375
1267
  });
1376
1268
  observer.observe(document.body, { childList: true, subtree: true });`;
1377
1269
  } else if (hostKey === "moodle") {
1378
- extraImports = `import "./tailwind.css";
1379
- import "@fontsource-variable/figtree";`;
1380
- setupCode = ` document.body.classList.add("middag-active");
1270
+ // Moodle uses a dedicated content block — see below.
1271
+ }
1272
+
1273
+ // Moodle: fully separate entry (asyncResolver, full provider stack, theme init, AdminShell)
1274
+ if (hostKey === "moodle") {
1275
+ const moodleContent = `/**
1276
+ * Production entry point for Moodle.
1277
+ *
1278
+ * AMD module loaded by Moodle's RequireJS via js_call_amd().
1279
+ * Uses real createInertiaApp — Moodle serves the HTML and page props.
1280
+ * This is the build target for \`npm run build\`.
1281
+ *
1282
+ * NOT used by \`npm run dev\` — that uses src/main.tsx with mock adapters.
1283
+ */
1284
+ import "./tailwind.css";
1285
+ import "@fontsource-variable/figtree";
1286
+
1287
+ import { createRoot } from "react-dom/client";
1288
+ import { createInertiaApp } from "@inertiajs/react";
1289
+ import {
1290
+ I18nProvider,
1291
+ ProgressProvider,
1292
+ AuthProvider,
1293
+ FlashProvider,
1294
+ ScopeProvider,
1295
+ setAppearance,
1296
+ getStoredAppearance,
1297
+ getEffectiveTheme,
1298
+ onSystemThemeChange,
1299
+ } from "@middag-io/react";
1300
+ import "@middag-io/react/style.css";
1301
+ import { getString } from "./lib/moodle/strings";
1302
+ import { registerDefaults } from "./app/register";
1303
+ import { resolvePageComponent } from "./app/page-resolver";
1304
+
1305
+ registerDefaults();
1306
+ setAppearance(getStoredAppearance());
1307
+ onSystemThemeChange();
1308
+
1309
+ createInertiaApp({
1310
+ id: "middag-app",
1311
+ resolve: (name) => resolvePageComponent(name),
1312
+ setup({ el, App, props }) {
1313
+ el.classList.add("middag-root");
1314
+ el.classList.add("middag-active");
1315
+ el.setAttribute("data-theme", getEffectiveTheme(getStoredAppearance()));
1381
1316
 
1382
1317
  // Portal container for Radix UI (modals, popovers, toasts).
1383
1318
  // Radix portals default to document.body which is outside .middag-root,
1384
- // meaning scoped Tailwind styles won't apply. This creates a sibling
1385
- // container with .middag-root so portal content inherits design tokens.
1319
+ // so this sibling container ensures portal content inherits design tokens.
1386
1320
  if (!document.getElementById("middag-portals")) {
1387
- const portalContainer = document.createElement("div");
1388
- portalContainer.id = "middag-portals";
1389
- portalContainer.classList.add("middag-root");
1390
- document.body.appendChild(portalContainer);
1391
- }`;
1321
+ const portals = document.createElement("div");
1322
+ portals.id = "middag-portals";
1323
+ portals.classList.add("middag-root");
1324
+ document.body.appendChild(portals);
1325
+ }
1326
+
1327
+ createRoot(el).render(
1328
+ <ProgressProvider>
1329
+ <App {...props}>
1330
+ {({ Component, props: pageProps, key }) => (
1331
+ <AuthProvider>
1332
+ <ScopeProvider>
1333
+ <FlashProvider>
1334
+ <I18nProvider asyncResolver={getString}>
1335
+ <Component key={key} {...pageProps} />
1336
+ </I18nProvider>
1337
+ </FlashProvider>
1338
+ </ScopeProvider>
1339
+ </AuthProvider>
1340
+ )}
1341
+ </App>
1342
+ </ProgressProvider>,
1343
+ );
1344
+ },
1345
+ });
1346
+ `;
1347
+ writeFile(filePath, moodleContent, label);
1348
+ return;
1392
1349
  }
1393
1350
 
1394
1351
  const content = `/**
1395
- * Production entry point for ${hostKey === "wordpress" ? "WordPress" : hostKey === "moodle" ? "Moodle" : "custom host"}.
1352
+ * Production entry point for ${hostKey === "wordpress" ? "WordPress" : "custom host"}.
1396
1353
  *
1397
- * Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" : hostKey === "moodle" ? "Moodle" : "your backend"})
1354
+ * Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" : "your backend"})
1398
1355
  * serves the HTML and Inertia page props. This file is the build target
1399
1356
  * for \`npm run build:${hostKey === "custom" ? "host" : hostKey}\`.
1400
1357
  *
@@ -1441,13 +1398,20 @@ ${postMountCode}
1441
1398
  * @param {string} targetDir - Absolute path to UI dir
1442
1399
  * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1443
1400
  * @param {object} host - HOSTS[hostKey] object
1401
+ * @param {boolean} withLicensing - Whether to emit @middag-io/licensing delivery manifest plugin
1444
1402
  */
1445
- export function scaffoldHostViteConfig(targetDir, hostKey, host) {
1403
+ export function scaffoldHostViteConfig(targetDir, hostKey, host, withLicensing = false) {
1446
1404
  const filePath = join(targetDir, `vite.config.${hostKey}.ts`);
1447
1405
  const label = `vite.config.${hostKey}.ts`;
1448
1406
  if (skipIfExists(filePath, label)) return;
1449
1407
 
1450
1408
  let outDir, formats, libName, fileName, extraRollup;
1409
+ const licensingImport = withLicensing
1410
+ ? `import { licensingPlugin } from "@middag-io/licensing/build";\n`
1411
+ : "";
1412
+ const licensingPluginEntry = withLicensing
1413
+ ? `,\n licensingPlugin({\n product: process.env.MIDDAG_PRODUCT ?? "middag-ui",\n release: process.env.MIDDAG_RELEASE ?? "local",\n buildId: process.env.MIDDAG_BUILD_ID ?? "local",\n modules: [],\n })`
1414
+ : "";
1451
1415
 
1452
1416
  if (hostKey === "wordpress") {
1453
1417
  outDir = `resolve(__dirname, "../assets/dist")`;
@@ -1481,10 +1445,11 @@ import { defineConfig } from "vite";
1481
1445
  import react from "@vitejs/plugin-react";
1482
1446
  import tailwindcss from "@tailwindcss/vite";
1483
1447
  import moodleAmd from "./plugins/vite-plugin-moodle-amd";
1448
+ ${licensingImport.trimEnd()}
1484
1449
  import { resolve } from "path";
1485
1450
 
1486
1451
  export default defineConfig({
1487
- plugins: [react(), tailwindcss(), moodleAmd()],
1452
+ plugins: [react(), tailwindcss(), moodleAmd()${licensingPluginEntry}],
1488
1453
  define: { "process.env.NODE_ENV": JSON.stringify("production") },
1489
1454
  resolve: { alias: { "@/": resolve(__dirname, "src") + "/" } },
1490
1455
  build: {
@@ -1553,10 +1518,11 @@ export default defineConfig({
1553
1518
  */
1554
1519
  import { defineConfig } from "vite";
1555
1520
  import react from "@vitejs/plugin-react";
1521
+ ${licensingImport.trimEnd()}
1556
1522
  import { resolve } from "path";
1557
1523
 
1558
1524
  export default defineConfig({
1559
- plugins: [react()],
1525
+ plugins: [react()${licensingPluginEntry}],
1560
1526
  define: { "process.env.NODE_ENV": JSON.stringify("production") },
1561
1527
  resolve: { alias: { "@/": resolve(__dirname, "src") + "/" } },
1562
1528
  build: {
@@ -1996,6 +1962,37 @@ export function scaffoldPageResolver(targetDir) {
1996
1962
  writeFile(filePath, readTemplate("templates/shared/page-resolver.tsx"), "src/app/page-resolver.tsx");
1997
1963
  }
1998
1964
 
1965
+ /**
1966
+ * Scaffold Moodle AMD page resolver: src/app/page-resolver.tsx.
1967
+ *
1968
+ * Overwrites the generic resolver with a Moodle-specific version that supports:
1969
+ * - Eager core pages (../extensions/core/pages/**)
1970
+ * - Lazy non-core extension pages (separate AMD chunks)
1971
+ * - External plugin pages via RequireJS (frankenstyle prefix)
1972
+ *
1973
+ * @param {string} targetDir - Absolute path to UI project root
1974
+ */
1975
+ export function scaffoldMoodlePageResolver(targetDir) {
1976
+ ensureDir(join(targetDir, "src", "app"));
1977
+ const filePath = join(targetDir, "src", "app", "page-resolver.tsx");
1978
+ writeFile(filePath, readTemplate("templates/shared/page-resolver-moodle.tsx"), "src/app/page-resolver.tsx (Moodle AMD)");
1979
+ }
1980
+
1981
+ /**
1982
+ * Scaffold Moodle admin shell: src/shells/AdminShell.tsx.
1983
+ *
1984
+ * Moodle-specific shell that supports the admin_tabs shared prop.
1985
+ * Registered as 'admin' shell in src/entry-moodle.tsx.
1986
+ *
1987
+ * @param {string} targetDir - Absolute path to UI project root
1988
+ */
1989
+ export function scaffoldMoodleAdminShell(targetDir) {
1990
+ ensureDir(join(targetDir, "src", "shells"));
1991
+ const filePath = join(targetDir, "src", "shells", "AdminShell.tsx");
1992
+ if (skipIfExists(filePath, "src/shells/AdminShell.tsx")) return;
1993
+ writeFile(filePath, readTemplate("templates/shared/moodle-admin-shell.tsx"), "src/shells/AdminShell.tsx");
1994
+ }
1995
+
1999
1996
  /**
2000
1997
  * Scaffold route helper: src/lib/routes.ts.
2001
1998
  * Abstracts host admin URL vs dev mock path.
@@ -5,11 +5,15 @@
5
5
  * mock/routes.tsx — route definitions
6
6
  * mock/navigation.ts — sidebar structure
7
7
  * mock/data.ts — synthetic page props
8
+ *
9
+ * The dev harness (host-sim shell, Inertia mocks, i18n) is inherited from
10
+ * @middag-io/react-demo — not generated locally. Vite aliases @inertiajs/*
11
+ * to it (see vite.config.ts).
8
12
  */
9
13
  import {useEffect} from "react";
10
14
  import {BrowserRouter, Routes, useNavigate} from "react-router";
11
15
  import {I18nProvider} from "@middag-io/react";
12
- import {MockI18nProvider, MockPageProvider, setMockNavigate} from "@middag-io/react/mock";
16
+ import {MockI18nProvider, MockPageProvider, setMockNavigate} from "@middag-io/react-demo";
13
17
  import {buildNavigation} from "../mock/navigation";
14
18
  import {sharedProps} from "../mock/data";
15
19
  import {AppRoutes} from "../mock/routes";
@@ -1,19 +1,24 @@
1
1
  import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { registerDefaults, registerShell } from "@middag-io/react";
4
- import { MockProductShell } from "@middag-io/react/mock";
4
+ import { registerProDefaults } from "@middag-io/react-pro/runtime";
5
+ import { MockProductShell } from "@middag-io/react-demo";
5
6
  import "@middag-io/react/style.css";
6
7
  import "@middag-io/react/themes/classic.css";
7
- import "@middag-io/react/themes/enterprise.css";
8
- import "@middag-io/react/themes/soft.css";
9
- import "@middag-io/react/themes/midnight.css";
8
+ import "@middag-io/react-pro/themes/enterprise.css";
9
+ import "@middag-io/react-pro/themes/soft.css";
10
+ import "@middag-io/react-pro/themes/midnight.css";
10
11
  import "./theme.css";
11
12
  import "@fontsource-variable/figtree";
12
13
  import { App } from "./app";
13
14
 
14
- // Dev mode: register all blocks from the lib (19 blocks + fields + icons + cells).
15
- // In production, entry-*.tsx uses the selective register from ./app/register.
15
+ // Dev mode: register the free engine defaults (12 standard blocks + fields +
16
+ // icons + cells) plus the premium runtime (the 7 heavy blocks). Then override
17
+ // the "product" shell with the host-sim MockProductShell inherited from
18
+ // @middag-io/react-demo (Moodle/WP chrome, host switcher, theme/locale toggles).
19
+ // Dev-only — in production, entry-*.tsx uses the selective register from ./app/register.
16
20
  registerDefaults();
21
+ registerProDefaults();
17
22
  registerShell("product", MockProductShell);
18
23
 
19
24
  createRoot(document.getElementById("root")!).render(
@@ -6,7 +6,7 @@
6
6
  import { Route } from "react-router";
7
7
  import { type ReactNode } from "react";
8
8
  import { ContractPage, resolveShell, EntityRoutesProvider } from "@middag-io/react";
9
- import { MockPageProvider } from "@middag-io/react/mock";
9
+ import { MockPageProvider } from "@middag-io/react-demo";
10
10
  import type { PageContract } from "@middag-io/react";
11
11
  import { mockEntities } from "./entities";
12
12
  import { buildNavigation } from "./navigation";
@@ -0,0 +1,108 @@
1
+ /**
2
+ * AdminShell — Moodle-specific admin shell.
3
+ *
4
+ * Extends ProductShell architecture with an optional admin tab bar
5
+ * rendered when the `admin_tabs` Inertia shared prop is present.
6
+ *
7
+ * Registered as 'admin' shell in src/entry-moodle.tsx.
8
+ * Edit this file to customise the admin sidebar header, tab bar, or layout.
9
+ */
10
+ import { useCallback, type ReactElement } from "react";
11
+ import { Link, usePage } from "@inertiajs/react";
12
+ import {
13
+ Toaster,
14
+ Sidebar,
15
+ SidebarHeader,
16
+ SidebarProvider,
17
+ useSidebar,
18
+ SidebarNav,
19
+ PageHeader,
20
+ NavErrorBoundary,
21
+ type ShellProps,
22
+ type SharedProps,
23
+ type PageMeta,
24
+ type AdminTabsProps,
25
+ } from "@middag-io/react";
26
+ import { Tabs, TabsList, TabsTrigger } from "@middag-io/react/reui/tabs";
27
+
28
+ // ── Inner shell (must be inside SidebarProvider) ──────────────────────────────
29
+
30
+ function AdminShellInner({ children }: ShellProps): ReactElement {
31
+ const { setOpenMobile } = useSidebar();
32
+ const { props } = usePage<SharedProps>();
33
+
34
+ const page: PageMeta = (props as SharedProps & { contract?: { page?: PageMeta } }).contract
35
+ ?.page ?? { key: "unknown", title: "", breadcrumbs: [], actions: [] };
36
+
37
+ const adminTabs = (props as SharedProps & { admin_tabs?: AdminTabsProps }).admin_tabs;
38
+
39
+ const handleMobileMenuClick = useCallback(() => {
40
+ setOpenMobile(true);
41
+ }, [setOpenMobile]);
42
+
43
+ return (
44
+ <>
45
+ <Sidebar
46
+ aria-label="Navegação"
47
+ collapsible="icon"
48
+ className="border-sidebar-border bg-sidebar border-r"
49
+ >
50
+ <SidebarHeader className="border-sidebar-border border-b px-4 py-3">
51
+ {/* Replace with your plugin name or logo */}
52
+ <p className="text-sidebar-foreground text-sm font-semibold">Admin</p>
53
+ </SidebarHeader>
54
+
55
+ <NavErrorBoundary>
56
+ <SidebarNav />
57
+ </NavErrorBoundary>
58
+ </Sidebar>
59
+
60
+ <div className="admin-shell__content flex min-h-screen flex-1 flex-col overflow-hidden">
61
+ <NavErrorBoundary>
62
+ <PageHeader page={page} onMobileMenuClick={handleMobileMenuClick} />
63
+ </NavErrorBoundary>
64
+
65
+ {adminTabs && (
66
+ <div className="border-b px-6">
67
+ <Tabs value={adminTabs.active}>
68
+ <TabsList variant="line" className="w-full justify-start">
69
+ {adminTabs.items.map((tab) => (
70
+ <TabsTrigger key={tab.key} value={tab.key} asChild>
71
+ <Link href={tab.href} preserveState>
72
+ {tab.label}
73
+ </Link>
74
+ </TabsTrigger>
75
+ ))}
76
+ </TabsList>
77
+ </Tabs>
78
+ </div>
79
+ )}
80
+
81
+ <main className="flex-1 overflow-auto p-6" aria-live="polite" aria-busy="false">
82
+ {children}
83
+ </main>
84
+ </div>
85
+
86
+ <Toaster position="bottom-right" richColors />
87
+ </>
88
+ );
89
+ }
90
+
91
+ // ── AdminShell — public export ──────────────────────────────────────────────
92
+
93
+ export function AdminShell({ children }: ShellProps): ReactElement {
94
+ return (
95
+ <SidebarProvider
96
+ defaultOpen={true}
97
+ style={
98
+ {
99
+ "--sidebar-width": "var(--sidebar-width)",
100
+ "--sidebar-width-icon": "var(--sidebar-width-collapsed)",
101
+ } as React.CSSProperties
102
+ }
103
+ className="bg-background text-foreground flex min-h-screen"
104
+ >
105
+ <AdminShellInner>{children}</AdminShellInner>
106
+ </SidebarProvider>
107
+ );
108
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * page-resolver — Moodle AMD page resolver.
3
+ *
4
+ * Three resolution modes:
5
+ *
6
+ * 1. Core pages (no prefix): "admin/Dashboard" resolves to
7
+ * "../extensions/core/pages/admin/Dashboard.tsx" via eager glob.
8
+ * Bundled into middag-app.js.
9
+ *
10
+ * 2. Extension pages (extension prefix): "ecommerce/pages/Index" resolves
11
+ * to "../extensions/ecommerce/pages/Index.tsx" via lazy glob.
12
+ * Each non-core extension gets its own AMD chunk.
13
+ *
14
+ * 3. External plugin pages (frankenstyle prefix): "local_yourplugin/admin/Index"
15
+ * loads the AMD module via RequireJS. Any Moodle plugin can register
16
+ * React pages in the MIDDAG shell using this mechanism.
17
+ */
18
+
19
+ // Core pages — eager (bundled into middag-app.js)
20
+ const corePages = import.meta.glob("../extensions/core/pages/**/*.tsx", { eager: true }) as Record<
21
+ string,
22
+ Record<string, unknown>
23
+ >;
24
+
25
+ // Non-core extension pages — lazy (separate AMD chunks)
26
+ const extensionPages = import.meta.glob(
27
+ ["../extensions/*/pages/**/*.tsx", "!../extensions/core/**"],
28
+ { eager: false },
29
+ ) as Record<string, () => Promise<Record<string, unknown>>>;
30
+
31
+ /**
32
+ * Frankenstyle pattern: type_name (e.g. local_yourplugin, mod_assign).
33
+ * A name like "local_yourplugin/admin/Index" is external.
34
+ * A name like "admin/Dashboard" is local (no underscore in first segment).
35
+ */
36
+ function isExternalPage(name: string): boolean {
37
+ const firstSegment = name.split("/")[0];
38
+ return /^[a-z]+_[a-z]/.test(firstSegment);
39
+ }
40
+
41
+ /**
42
+ * Load an external plugin page via RequireJS (Moodle AMD).
43
+ *
44
+ * The plugin must build its page as an AMD module that exports a
45
+ * default React component. The module ID matches the page name directly
46
+ * (e.g. "local_yourplugin/admin/Index").
47
+ */
48
+ function loadExternalPage(name: string): Promise<Record<string, unknown>> {
49
+ return new Promise((resolve, reject) => {
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- RequireJS global
51
+ const req = (window as any).require;
52
+ if (!req) {
53
+ reject(new Error(`RequireJS not available. Cannot load external page: ${name}`));
54
+ return;
55
+ }
56
+ req(
57
+ [name],
58
+ (mod: Record<string, unknown>) => resolve(mod),
59
+ (err: Error) => reject(new Error(`Failed to load external page "${name}": ${err.message}`)),
60
+ );
61
+ });
62
+ }
63
+
64
+ export function resolvePageComponent(name: string) {
65
+ if (isExternalPage(name)) {
66
+ return loadExternalPage(name).then((mod) => mod.default ?? mod);
67
+ }
68
+
69
+ // Try core pages first (eager/sync)
70
+ const corePath = `../extensions/core/pages/${name}.tsx`;
71
+ const coreMod = corePages[corePath];
72
+ if (coreMod) {
73
+ return coreMod.default ?? coreMod;
74
+ }
75
+
76
+ // Try non-core extension pages (lazy/async)
77
+ const extPath = `../extensions/${name}.tsx`;
78
+ const extLoader = extensionPages[extPath];
79
+ if (extLoader) {
80
+ return extLoader().then((mod) => mod.default ?? mod);
81
+ }
82
+
83
+ throw new Error(
84
+ `Page not found: ${name}. Available: ${[...Object.keys(corePages), ...Object.keys(extensionPages)].join(", ")}`,
85
+ );
86
+ }
@@ -1,12 +1,14 @@
1
1
  /**
2
- * register — selective registration for this plugin's UI.
2
+ * register — registration for this plugin's UI.
3
3
  *
4
- * Registers only the shells, layouts, blocks, cell renderers, form fields,
5
- * and icons this plugin uses. For IIFE bundles (WordPress/Moodle), selective
6
- * registration avoids pulling in heavy lazy-loaded blocks that bloat the bundle.
4
+ * Registers all 13 standard blocks plus shells, layouts, cell renderers, form
5
+ * fields and icons. Every block in @middag-io/react is free to use — there is no
6
+ * tiered block gating. The 6 heavy lazy-loaded blocks (chart_panel, kanban_board,
7
+ * flow_editor, form_builder, condition_tree, sentence_builder) are NOT registered
8
+ * here to keep the bundle lean; deep-import + registerBlock() them when a page
9
+ * needs one, or call registerDefaults() from @middag-io/react to register all 19.
7
10
  *
8
- * When adding a new page that needs a block not listed here,
9
- * add the import + registerBlock call.
11
+ * For lean IIFE bundles (WordPress/Moodle), trim the blocks you don't use.
10
12
  *
11
13
  * Full catalog: https://docs.middag.io/blocks
12
14
  */
@@ -31,10 +33,14 @@ import {
31
33
  MetricCardBlock,
32
34
  EmptyStateBlock,
33
35
  DetailPanelBlock,
34
- FormPanelBlock,
35
- CardGridBlock,
36
36
  StatusStripBlock,
37
+ FormPanelBlock,
37
38
  TabbedPanelBlock,
39
+ ActivityTimelineBlock,
40
+ WorkflowProgressBlock,
41
+ MarkdownPanelBlock,
42
+ CardGridBlock,
43
+ ActionGridBlock,
38
44
  LinkListBlock,
39
45
  } from "@middag-io/react";
40
46
 
@@ -59,10 +65,14 @@ export function registerDefaults(): void {
59
65
  registerBlock("metric_card", MetricCardBlock);
60
66
  registerBlock("empty_state", EmptyStateBlock);
61
67
  registerBlock("detail_panel", DetailPanelBlock);
62
- registerBlock("form_panel", FormPanelBlock);
63
- registerBlock("card_grid", CardGridBlock);
64
68
  registerBlock("status_strip", StatusStripBlock);
69
+ registerBlock("form_panel", FormPanelBlock);
65
70
  registerBlock("tabbed_panel", TabbedPanelBlock);
71
+ registerBlock("activity_timeline", ActivityTimelineBlock);
72
+ registerBlock("workflow_progress", WorkflowProgressBlock);
73
+ registerBlock("markdown_panel", MarkdownPanelBlock);
74
+ registerBlock("card_grid", CardGridBlock);
75
+ registerBlock("action_grid", ActionGridBlock);
66
76
  registerBlock("link_list", LinkListBlock);
67
77
 
68
78
  // Cell renderers (status, timestamp, link, boolean, etc.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-middag-ui",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "Bootstrap a MIDDAG React UI layer in your Moodle or WordPress plugin",
6
6
  "bin": {