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 +27 -11
- package/lib/scaffold.js +181 -184
- package/lib/templates/pro/app.tsx +5 -1
- package/lib/templates/pro/main.tsx +11 -6
- package/lib/templates/pro/mock-routes.tsx +1 -1
- package/lib/templates/shared/moodle-admin-shell.tsx +108 -0
- package/lib/templates/shared/page-resolver-moodle.tsx +86 -0
- package/lib/templates/shared/register-free.ts +20 -10
- package/package.json +1 -1
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
|
-
|
|
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
|
-
//
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
206
|
-
? "
|
|
207
|
-
: "
|
|
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:
|
|
253
|
+
include: ${optimizeInclude},
|
|
225
254
|
},
|
|
226
255
|
resolve: {
|
|
227
256
|
alias: {
|
|
228
257
|
"@/": resolve(__dirname, "src") + "/",
|
|
229
258
|
"@mock/": resolve(__dirname, "mock") + "/",
|
|
230
|
-
${
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
//
|
|
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
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
document.body.appendChild(
|
|
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" :
|
|
1352
|
+
* Production entry point for ${hostKey === "wordpress" ? "WordPress" : "custom host"}.
|
|
1396
1353
|
*
|
|
1397
|
-
* Uses real createInertiaApp — the host platform (${hostKey === "wordpress" ? "WP" :
|
|
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
|
|
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 {
|
|
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
|
|
15
|
-
//
|
|
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
|
|
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 —
|
|
2
|
+
* register — registration for this plugin's UI.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* and icons
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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.)
|