create-middag-ui 0.12.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/README.md +1 -1
- package/cli.js +53 -16
- package/lib/detect.js +21 -1
- package/lib/scaffold.js +238 -37
- package/lib/templates/pro/app.tsx +8 -8
- package/lib/templates/pro/main.tsx +7 -2
- package/lib/templates/pro/register-pro.ts +24 -6
- package/lib/templates/shared/moodle-ajax.ts +33 -0
- package/lib/templates/shared/moodle-notification.ts +23 -0
- package/lib/templates/shared/moodle-strings.ts +62 -0
- package/lib/templates/shared/page-resolver.tsx +4 -3
- package/lib/templates/shared/register-free.ts +16 -4
- package/lib/templates/shared/route-helper-custom.ts +44 -0
- package/lib/templates/shared/route-helper-moodle.ts +66 -0
- package/lib/templates/shared/route-helper-wp.ts +43 -11
- package/lib/templates/shared/tailwind.css +12 -0
- package/lib/templates/shared/vite-plugin-moodle-amd.ts +122 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ npx @middag-io/react upgrade # Check for updates
|
|
|
86
86
|
|
|
87
87
|
## Documentation
|
|
88
88
|
|
|
89
|
-
- **[Live Demo](https://
|
|
89
|
+
- **[Live Demo](https://ui-demo.middag.io)** — 24 screens showing all block types
|
|
90
90
|
- **[Full Documentation](https://ui-docs.middag.io)** — Getting started, host guides, API reference
|
|
91
91
|
- **[GitHub](https://github.com/middag-io/middag-react)** — Source code and issues
|
|
92
92
|
|
package/cli.js
CHANGED
|
@@ -20,32 +20,35 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { detectHost, HOSTS } from "./lib/detect.js";
|
|
24
|
-
import { ask,
|
|
23
|
+
import { detectHost, detectMoodleComponent, HOSTS } from "./lib/detect.js";
|
|
24
|
+
import { ask, confirm, select } from "./lib/prompts.js";
|
|
25
25
|
import { runTokenFlow } from "./lib/auth.js";
|
|
26
26
|
import {
|
|
27
27
|
createTargetDir,
|
|
28
|
-
|
|
29
|
-
scaffoldTsconfig,
|
|
30
|
-
scaffoldViteConfig,
|
|
31
|
-
scaffoldEslintConfig,
|
|
32
|
-
scaffoldPrettierConfig,
|
|
33
|
-
scaffoldIndexHtml,
|
|
28
|
+
scaffoldDemoDirectPage,
|
|
34
29
|
scaffoldDemoFiles,
|
|
35
|
-
scaffoldPageExamples,
|
|
36
|
-
scaffoldFreeApp,
|
|
37
|
-
scaffoldFreeAdapters,
|
|
38
30
|
scaffoldDevShell,
|
|
31
|
+
scaffoldEslintConfig,
|
|
32
|
+
scaffoldFreeAdapters,
|
|
33
|
+
scaffoldFreeApp,
|
|
34
|
+
scaffoldFreeRegister,
|
|
39
35
|
scaffoldHostEntry,
|
|
40
|
-
scaffoldHostViteConfig,
|
|
41
36
|
scaffoldHostThemeCSS,
|
|
42
|
-
|
|
37
|
+
scaffoldHostViteConfig,
|
|
38
|
+
scaffoldIndexHtml,
|
|
39
|
+
scaffoldMoodleAdapters,
|
|
40
|
+
scaffoldMoodlePlugin,
|
|
41
|
+
scaffoldMoodleTailwind,
|
|
42
|
+
scaffoldPackageJson,
|
|
43
|
+
scaffoldPageExamples,
|
|
43
44
|
scaffoldPageResolver,
|
|
45
|
+
scaffoldPrettierConfig,
|
|
44
46
|
scaffoldRouteHelper,
|
|
45
|
-
|
|
47
|
+
scaffoldTsconfig,
|
|
48
|
+
scaffoldViteConfig,
|
|
46
49
|
} from "./lib/scaffold.js";
|
|
47
50
|
import { runNpmInstall } from "./lib/install.js";
|
|
48
|
-
import {
|
|
51
|
+
import { blank, heading, info, log, success } from "./lib/ui.js";
|
|
49
52
|
|
|
50
53
|
const TOTAL_STEPS = 10;
|
|
51
54
|
const cwd = process.cwd();
|
|
@@ -73,6 +76,20 @@ if (hostKey) {
|
|
|
73
76
|
|
|
74
77
|
const host = HOSTS[hostKey];
|
|
75
78
|
|
|
79
|
+
// Moodle: detect frankenstyle component name (e.g. "local_middag", "mod_assign")
|
|
80
|
+
let moodleComponent = null;
|
|
81
|
+
if (hostKey === "moodle") {
|
|
82
|
+
moodleComponent = detectMoodleComponent(cwd);
|
|
83
|
+
if (moodleComponent) {
|
|
84
|
+
success(`Moodle component: ${ moodleComponent }`);
|
|
85
|
+
} else {
|
|
86
|
+
info("Could not detect Moodle component from version.php");
|
|
87
|
+
const answer = await ask(" Frankenstyle component (e.g. local_yourplugin): ");
|
|
88
|
+
moodleComponent = answer || "local_myplugin";
|
|
89
|
+
success(`Component: ${ moodleComponent }`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
// ── Step 2: Ask directory ────────────────────────────────────────────────
|
|
77
94
|
|
|
78
95
|
heading(2, TOTAL_STEPS, "Target directory");
|
|
@@ -156,7 +173,9 @@ scaffoldPageExamples(targetDir);
|
|
|
156
173
|
// Shared files (both PRO and FREE)
|
|
157
174
|
scaffoldPageResolver(targetDir);
|
|
158
175
|
scaffoldDemoDirectPage(targetDir);
|
|
159
|
-
|
|
176
|
+
// Convert frankenstyle "local_middag" → Moodle path "local/middag"
|
|
177
|
+
const moodlePath = moodleComponent ? moodleComponent.replace("_", "/") : null;
|
|
178
|
+
scaffoldRouteHelper(targetDir, hostKey, moodlePath);
|
|
160
179
|
|
|
161
180
|
if (isPro) {
|
|
162
181
|
try {
|
|
@@ -191,6 +210,14 @@ scaffoldHostEntry(targetDir, hostKey);
|
|
|
191
210
|
scaffoldHostViteConfig(targetDir, hostKey, host);
|
|
192
211
|
scaffoldHostThemeCSS(targetDir, hostKey, host);
|
|
193
212
|
|
|
213
|
+
// Moodle-specific: AMD plugin + Moodle adapters (ajax, strings, notification)
|
|
214
|
+
if (hostKey === "moodle" && moodleComponent) {
|
|
215
|
+
scaffoldMoodlePlugin(targetDir, moodleComponent);
|
|
216
|
+
scaffoldMoodleTailwind(targetDir);
|
|
217
|
+
scaffoldMoodleAdapters(targetDir, moodleComponent);
|
|
218
|
+
success(`Moodle: AMD plugin + Tailwind + adapters for ${ moodleComponent }`);
|
|
219
|
+
}
|
|
220
|
+
|
|
194
221
|
// ── Step 9: npm install ──────────────────────────────────────────────────
|
|
195
222
|
|
|
196
223
|
heading(9, TOTAL_STEPS, "Installing dependencies");
|
|
@@ -237,6 +264,16 @@ console.log(` Production build for ${host.name}:`);
|
|
|
237
264
|
console.log(` npm run ${hostBuildScript} \u2192 build for ${host.name}`);
|
|
238
265
|
console.log(` npm run ${hostWatchScript} \u2192 rebuild on change`);
|
|
239
266
|
|
|
267
|
+
if (hostKey === "moodle" && moodleComponent) {
|
|
268
|
+
blank();
|
|
269
|
+
console.log(` Moodle integration (${ moodleComponent }):`);
|
|
270
|
+
console.log(" AMD chunks \u2192 amd/src/ + amd/build/");
|
|
271
|
+
console.log(" CSS \u2192 styles/middag-app.css");
|
|
272
|
+
console.log(" AMD plugin: plugins/vite-plugin-moodle-amd.ts");
|
|
273
|
+
console.log(" Moodle adapters: src/lib/moodle/ (ajax, strings, notification)");
|
|
274
|
+
console.log(" Tailwind entry: src/tailwind.css");
|
|
275
|
+
}
|
|
276
|
+
|
|
240
277
|
blank();
|
|
241
278
|
console.log(` Integrate with your ${host.name} plugin:`);
|
|
242
279
|
console.log(" 1. Import { ContractPage } from '@middag-io/react'");
|
package/lib/detect.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
-
import {
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
14
|
|
|
15
15
|
/** Max ancestor levels to walk when searching for platform markers. */
|
|
16
16
|
const MAX_DEPTH = 5;
|
|
@@ -54,6 +54,26 @@ function isMoodleRootVersion(filePath) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Extract Moodle plugin frankenstyle component name from version.php.
|
|
59
|
+
* Returns e.g. "local_middag", "mod_assign", or null if not found.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} cwd - Directory to check (should be the Moodle plugin root)
|
|
62
|
+
* @returns {string|null} Frankenstyle component name
|
|
63
|
+
*/
|
|
64
|
+
export function detectMoodleComponent(cwd) {
|
|
65
|
+
const versionFile = join(cwd, "version.php");
|
|
66
|
+
if (!existsSync(versionFile)) return null;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const content = readFileSync(versionFile, "utf-8");
|
|
70
|
+
const match = content.match(/\$plugin\s*->\s*component\s*=\s*['"]([^'"]+)['"]/);
|
|
71
|
+
return match ? match[1] : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
/**
|
|
58
78
|
* Detect host platform by checking cwd and ancestor directories.
|
|
59
79
|
*
|
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 {
|
|
11
|
+
import { basename, dirname, join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { success, warn
|
|
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");
|
|
@@ -443,6 +451,22 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
443
451
|
*
|
|
444
452
|
* Import order matters \u2014 this file is loaded AFTER @middag-io/react/style.css
|
|
445
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
|
|
446
470
|
*/
|
|
447
471
|
|
|
448
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
|
|
@@ -461,13 +485,13 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
461
485
|
}
|
|
462
486
|
*/
|
|
463
487
|
|
|
464
|
-
/* \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
|
|
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
|
|
465
489
|
*
|
|
466
|
-
* A complete theme
|
|
467
|
-
* Apply with:
|
|
490
|
+
* A complete custom theme scoped to a CSS class.
|
|
491
|
+
* Apply with: document.body.classList.add("theme-ocean")
|
|
468
492
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
493
|
+
* Override colors, radius, density, shadows, and layout in one class.
|
|
494
|
+
* Dark mode: use :root.dark .theme-ocean for dark overrides.
|
|
471
495
|
*/
|
|
472
496
|
|
|
473
497
|
.theme-ocean {
|
|
@@ -483,6 +507,13 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
483
507
|
--info: oklch(0.55 0.14 200);
|
|
484
508
|
--info-foreground: oklch(0.98 0 0);
|
|
485
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
|
+
|
|
486
517
|
/* Sidebar */
|
|
487
518
|
--sidebar: oklch(0.15 0.03 230);
|
|
488
519
|
--sidebar-foreground: oklch(0.75 0.02 230);
|
|
@@ -494,6 +525,16 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
494
525
|
--sidebar-border: oklch(0.25 0.03 230);
|
|
495
526
|
}
|
|
496
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
|
+
|
|
497
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
|
|
498
539
|
*
|
|
499
540
|
* Add project-specific styles below. These can target MIDDAG components
|
|
@@ -507,17 +548,9 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
507
548
|
* Radius: var(--radius-sm|md|lg|xl|2xl|full)
|
|
508
549
|
* Shadows: var(--shadow-xs|sm|md|lg|xl|2xl)
|
|
509
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)
|
|
510
553
|
*/
|
|
511
|
-
|
|
512
|
-
/* Example: branded page header */
|
|
513
|
-
/*
|
|
514
|
-
.my-page-header {
|
|
515
|
-
background: linear-gradient(135deg, var(--primary) 0%, var(--info) 100%);
|
|
516
|
-
color: var(--primary-foreground);
|
|
517
|
-
padding: var(--space-6) var(--space-8);
|
|
518
|
-
border-radius: var(--radius-lg);
|
|
519
|
-
}
|
|
520
|
-
*/
|
|
521
554
|
`,
|
|
522
555
|
"src/theme.css",
|
|
523
556
|
);
|
|
@@ -635,7 +668,7 @@ export const dashboardContract: PageContract = {
|
|
|
635
668
|
`/**
|
|
636
669
|
* Connectors page contract \u2014 INTERMEDIATE example.
|
|
637
670
|
*
|
|
638
|
-
* Demonstrates the "
|
|
671
|
+
* Demonstrates the "sidebar" layout with three block types:
|
|
639
672
|
* - card_grid (connector cards in the main region)
|
|
640
673
|
* - status_strip (health indicators in the aside)
|
|
641
674
|
* - detail_panel (metadata in the aside)
|
|
@@ -659,7 +692,7 @@ export const connectorsContract: PageContract = {
|
|
|
659
692
|
],
|
|
660
693
|
},
|
|
661
694
|
layout: {
|
|
662
|
-
template: "
|
|
695
|
+
template: "sidebar",
|
|
663
696
|
regions: {
|
|
664
697
|
main: [
|
|
665
698
|
{
|
|
@@ -1266,7 +1299,8 @@ export function scaffoldHostEntry(targetDir, hostKey) {
|
|
|
1266
1299
|
const label = `src/entry-${hostKey}.tsx`;
|
|
1267
1300
|
if (skipIfExists(filePath, label)) return;
|
|
1268
1301
|
|
|
1269
|
-
// Host-specific setup and post-mount code
|
|
1302
|
+
// Host-specific imports, setup, and post-mount code
|
|
1303
|
+
let extraImports = "";
|
|
1270
1304
|
let setupCode = "";
|
|
1271
1305
|
let postMountCode = "";
|
|
1272
1306
|
|
|
@@ -1291,7 +1325,20 @@ export function scaffoldHostEntry(targetDir, hostKey) {
|
|
|
1291
1325
|
});
|
|
1292
1326
|
observer.observe(document.body, { childList: true, subtree: true });`;
|
|
1293
1327
|
} else if (hostKey === "moodle") {
|
|
1294
|
-
|
|
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
|
+
}`;
|
|
1295
1342
|
}
|
|
1296
1343
|
|
|
1297
1344
|
const content = `/**
|
|
@@ -1308,7 +1355,7 @@ import { createInertiaApp } from "@inertiajs/react";
|
|
|
1308
1355
|
import { I18nProvider, ProgressProvider } from "@middag-io/react";
|
|
1309
1356
|
import "@middag-io/react/style.css";
|
|
1310
1357
|
import "./theme.css";
|
|
1311
|
-
import { registerDefaults } from "./app/register";
|
|
1358
|
+
${ extraImports }import { registerDefaults } from "./app/register";
|
|
1312
1359
|
import { resolvePageComponent } from "./app/page-resolver";
|
|
1313
1360
|
|
|
1314
1361
|
registerDefaults();
|
|
@@ -1365,17 +1412,71 @@ export function scaffoldHostViteConfig(targetDir, hostKey, host) {
|
|
|
1365
1412
|
},
|
|
1366
1413
|
},`;
|
|
1367
1414
|
} else if (hostKey === "moodle") {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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"],
|
|
1373
1448
|
output: {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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
|
+
}
|
|
1377
1472
|
},
|
|
1378
|
-
}
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
},
|
|
1476
|
+
});
|
|
1477
|
+
`;
|
|
1478
|
+
writeFile(filePath, content, label);
|
|
1479
|
+
return;
|
|
1379
1480
|
} else {
|
|
1380
1481
|
outDir = `resolve(__dirname, "../dist")`;
|
|
1381
1482
|
formats = `["es"]`;
|
|
@@ -1498,8 +1599,18 @@ body.middag-active {
|
|
|
1498
1599
|
} else if (hostKey === "moodle") {
|
|
1499
1600
|
hostSection = `
|
|
1500
1601
|
|
|
1501
|
-
/*
|
|
1502
|
-
* Active when MIDDAG mounts inside Moodle
|
|
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).
|
|
1503
1614
|
*/
|
|
1504
1615
|
|
|
1505
1616
|
body.middag-active {
|
|
@@ -1508,7 +1619,31 @@ body.middag-active {
|
|
|
1508
1619
|
|
|
1509
1620
|
body.middag-active [data-slot="sidebar-container"] {
|
|
1510
1621
|
left: 0 !important;
|
|
1511
|
-
}
|
|
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
|
+
*/`;
|
|
1512
1647
|
} else {
|
|
1513
1648
|
hostSection = `
|
|
1514
1649
|
|
|
@@ -1817,7 +1952,12 @@ export function scaffoldPageResolver(targetDir) {
|
|
|
1817
1952
|
*
|
|
1818
1953
|
* @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
|
|
1819
1954
|
*/
|
|
1820
|
-
|
|
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) {
|
|
1821
1961
|
ensureDir(join(targetDir, "src", "lib"));
|
|
1822
1962
|
const filePath = join(targetDir, "src", "lib", "routes.ts");
|
|
1823
1963
|
if (skipIfExists(filePath, "src/lib/routes.ts")) return;
|
|
@@ -1830,7 +1970,13 @@ export function scaffoldRouteHelper(targetDir, hostKey) {
|
|
|
1830
1970
|
const template = templateMap[hostKey] || templateMap.wordpress;
|
|
1831
1971
|
|
|
1832
1972
|
try {
|
|
1833
|
-
|
|
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");
|
|
1834
1980
|
} catch {
|
|
1835
1981
|
// Fallback to WP template if host-specific one doesn't exist
|
|
1836
1982
|
writeFile(filePath, readTemplate("templates/shared/route-helper-wp.ts"), "src/lib/routes.ts");
|
|
@@ -1847,3 +1993,58 @@ export function scaffoldDemoDirectPage(targetDir) {
|
|
|
1847
1993
|
if (skipIfExists(filePath, "src/pages/DemoPage.tsx")) return;
|
|
1848
1994
|
writeFile(filePath, readTemplate("templates/shared/demo-page.tsx"), "src/pages/DemoPage.tsx");
|
|
1849
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
|
+
}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* mock/navigation.ts — sidebar structure
|
|
7
7
|
* mock/data.ts — synthetic page props
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { MockPageProvider,
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
9
|
+
import {useEffect} from "react";
|
|
10
|
+
import {BrowserRouter, Routes, useNavigate} from "react-router";
|
|
11
|
+
import {I18nProvider} from "@middag-io/react";
|
|
12
|
+
import {MockI18nProvider, MockPageProvider, setMockNavigate} from "@middag-io/react/mock";
|
|
13
|
+
import {buildNavigation} from "../mock/navigation";
|
|
14
|
+
import {sharedProps} from "../mock/data";
|
|
15
|
+
import {AppRoutes} from "../mock/routes";
|
|
16
16
|
|
|
17
17
|
let _navigate: ((to: string) => void) | null = null;
|
|
18
18
|
function NavigateBridge() {
|
|
@@ -24,7 +24,7 @@ function NavigateBridge() {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
if (typeof window !== "undefined") {
|
|
27
|
-
(window as
|
|
27
|
+
(window as Record<string, unknown>).__MIDDAG_MOCK_NAVIGATE__ = (to: string) => {
|
|
28
28
|
if (_navigate) _navigate(to);
|
|
29
29
|
};
|
|
30
30
|
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { registerDefaults, registerShell } from "@middag-io/react";
|
|
3
4
|
import { MockProductShell } from "@middag-io/react/mock";
|
|
4
|
-
import { registerShell } from "@middag-io/react";
|
|
5
5
|
import "@middag-io/react/style.css";
|
|
6
|
+
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";
|
|
6
10
|
import "./theme.css";
|
|
7
11
|
import "@fontsource-variable/figtree";
|
|
8
|
-
import { registerDefaults } from "./app/register";
|
|
9
12
|
import { App } from "./app";
|
|
10
13
|
|
|
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.
|
|
11
16
|
registerDefaults();
|
|
12
17
|
registerShell("product", MockProductShell);
|
|
13
18
|
|
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* register — selective registration for this plugin's UI (PRO).
|
|
3
3
|
*
|
|
4
4
|
* Registers shells, layouts, and blocks this plugin uses.
|
|
5
|
+
* All 13 standard blocks (exported from the barrel) are included.
|
|
5
6
|
* Add or remove registrations as your pages need them.
|
|
6
7
|
*
|
|
8
|
+
* Heavy lazy-loaded blocks (chart_panel, kanban_board, flow_editor,
|
|
9
|
+
* form_builder, condition_tree, sentence_builder) are NOT included here.
|
|
10
|
+
* To use them, import via deep path and registerBlock() individually,
|
|
11
|
+
* or call registerDefaults() from @middag-io/react instead.
|
|
12
|
+
*
|
|
7
13
|
* Full catalog: https://docs.middag.io/blocks
|
|
8
14
|
*/
|
|
9
15
|
|
|
@@ -12,12 +18,14 @@ import {
|
|
|
12
18
|
registerLayout,
|
|
13
19
|
registerBlock,
|
|
14
20
|
// Shells
|
|
15
|
-
|
|
21
|
+
ProductShell,
|
|
22
|
+
ImmersiveShell,
|
|
16
23
|
// Layouts
|
|
17
24
|
StackLayout,
|
|
18
|
-
|
|
25
|
+
SidebarLayout,
|
|
19
26
|
DashboardLayout,
|
|
20
|
-
|
|
27
|
+
WizardLayout,
|
|
28
|
+
// Blocks (all 13 standard barrel exports)
|
|
21
29
|
DenseTableBlock,
|
|
22
30
|
MetricCardBlock,
|
|
23
31
|
EmptyStateBlock,
|
|
@@ -27,6 +35,10 @@ import {
|
|
|
27
35
|
TabbedPanelBlock,
|
|
28
36
|
ActivityTimelineBlock,
|
|
29
37
|
WorkflowProgressBlock,
|
|
38
|
+
MarkdownPanelBlock,
|
|
39
|
+
CardGridBlock,
|
|
40
|
+
ActionGridBlock,
|
|
41
|
+
LinkListBlock,
|
|
30
42
|
} from "@middag-io/react";
|
|
31
43
|
|
|
32
44
|
let registered = false;
|
|
@@ -36,14 +48,16 @@ export function registerDefaults(): void {
|
|
|
36
48
|
registered = true;
|
|
37
49
|
|
|
38
50
|
// Shells
|
|
39
|
-
registerShell("product",
|
|
51
|
+
registerShell("product", ProductShell);
|
|
52
|
+
registerShell("immersive", ImmersiveShell);
|
|
40
53
|
|
|
41
54
|
// Layouts
|
|
42
55
|
registerLayout("stack", StackLayout);
|
|
43
|
-
registerLayout("
|
|
56
|
+
registerLayout("sidebar", SidebarLayout);
|
|
44
57
|
registerLayout("dashboard", DashboardLayout);
|
|
58
|
+
registerLayout("wizard", WizardLayout);
|
|
45
59
|
|
|
46
|
-
// Blocks —
|
|
60
|
+
// Blocks — all 13 standard blocks from the barrel
|
|
47
61
|
// See: https://docs.middag.io/blocks for the full catalog
|
|
48
62
|
registerBlock("dense_table", DenseTableBlock);
|
|
49
63
|
registerBlock("metric_card", MetricCardBlock);
|
|
@@ -54,4 +68,8 @@ export function registerDefaults(): void {
|
|
|
54
68
|
registerBlock("tabbed_panel", TabbedPanelBlock);
|
|
55
69
|
registerBlock("activity_timeline", ActivityTimelineBlock);
|
|
56
70
|
registerBlock("workflow_progress", WorkflowProgressBlock);
|
|
71
|
+
registerBlock("markdown_panel", MarkdownPanelBlock);
|
|
72
|
+
registerBlock("card_grid", CardGridBlock);
|
|
73
|
+
registerBlock("action_grid", ActionGridBlock);
|
|
74
|
+
registerBlock("link_list", LinkListBlock);
|
|
57
75
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed adapter for Moodle's core/ajax AMD module.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Ajax.call() with generics for type-safe web service calls.
|
|
5
|
+
* React components use this instead of importing core/ajax directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// @ts-expect-error AMD external — resolved by Moodle's RequireJS at runtime
|
|
9
|
+
import Ajax from "core/ajax";
|
|
10
|
+
|
|
11
|
+
export interface WebServiceCall {
|
|
12
|
+
methodname: string;
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Call a single Moodle web service and return the typed result.
|
|
18
|
+
*/
|
|
19
|
+
export async function callWebService<T = unknown>(
|
|
20
|
+
call: WebServiceCall,
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
const [result] = await Ajax.call([call]);
|
|
23
|
+
return result as T;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Call multiple Moodle web services in a single request.
|
|
28
|
+
*/
|
|
29
|
+
export async function callWebServices(
|
|
30
|
+
calls: WebServiceCall[],
|
|
31
|
+
): Promise<unknown[]> {
|
|
32
|
+
return Ajax.call(calls);
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed adapter for Moodle's core/notification AMD module.
|
|
3
|
+
*
|
|
4
|
+
* Centralises error reporting so React components never import
|
|
5
|
+
* Moodle AMD externals directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// @ts-expect-error AMD external — resolved by Moodle's RequireJS at runtime
|
|
9
|
+
import Notification from "core/notification";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Report an exception to Moodle's notification system.
|
|
13
|
+
*/
|
|
14
|
+
export function reportException(error: Error): void {
|
|
15
|
+
Notification.exception(error);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Show an alert dialog via Moodle's notification system.
|
|
20
|
+
*/
|
|
21
|
+
export function alert(title: string, message: string): void {
|
|
22
|
+
Notification.alert(title, message);
|
|
23
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed adapter for Moodle's core/str AMD module.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the native `get_strings` function with TypeScript types
|
|
5
|
+
* and returns a key→value Record for convenient consumption.
|
|
6
|
+
*
|
|
7
|
+
* Can be used as the asyncResolver for I18nProvider:
|
|
8
|
+
*
|
|
9
|
+
* import { getString } from "@/lib/moodle/strings";
|
|
10
|
+
* <I18nProvider asyncResolver={getString}>
|
|
11
|
+
*
|
|
12
|
+
* Change the default component below to match your plugin's frankenstyle.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// @ts-expect-error AMD external — resolved by Moodle's RequireJS at runtime
|
|
16
|
+
import {get_strings} from "core/str";
|
|
17
|
+
|
|
18
|
+
export interface StringRequest {
|
|
19
|
+
key: string;
|
|
20
|
+
component?: string;
|
|
21
|
+
param?: string | number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── CHANGE THIS to your plugin's frankenstyle name ──
|
|
25
|
+
const DEFAULT_COMPONENT = "__PLUGIN_PREFIX__";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load translated strings from Moodle's string manager.
|
|
29
|
+
*
|
|
30
|
+
* @param requests - Array of string requests (key + optional component/param)
|
|
31
|
+
* @returns Record mapping each key to its translated string
|
|
32
|
+
*/
|
|
33
|
+
export async function getStrings(
|
|
34
|
+
requests: StringRequest[],
|
|
35
|
+
): Promise<Record<string, string>> {
|
|
36
|
+
const moodleRequests = requests.map((r) => ({
|
|
37
|
+
key: r.key,
|
|
38
|
+
component: r.component ?? DEFAULT_COMPONENT,
|
|
39
|
+
param: r.param,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const results: string[] = await get_strings(moodleRequests);
|
|
43
|
+
|
|
44
|
+
const out: Record<string, string> = {};
|
|
45
|
+
results.forEach((translated, index) => {
|
|
46
|
+
out[requests[index].key] = translated;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load a single translated string.
|
|
54
|
+
* Compatible with I18nProvider's AsyncStringResolver signature.
|
|
55
|
+
*/
|
|
56
|
+
export async function getString(
|
|
57
|
+
key: string,
|
|
58
|
+
component = DEFAULT_COMPONENT,
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
const result = await getStrings([{key, component}]);
|
|
61
|
+
return result[key] ?? key;
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import type {ContractPageProps, PageContract} from "@middag-io/react";
|
|
2
|
+
import {ContractPage} from "@middag-io/react";
|
|
3
|
+
import {usePage} from "@inertiajs/react";
|
|
4
4
|
|
|
5
5
|
// Direct pages — eager loaded (custom React pages in pages/)
|
|
6
6
|
const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Record<
|
|
@@ -12,6 +12,7 @@ const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Re
|
|
|
12
12
|
* Fallback component — reads PageContract from Inertia props and renders
|
|
13
13
|
* it via the lib's ContractPage. Used when no direct page matches.
|
|
14
14
|
*/
|
|
15
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
15
16
|
function InertiaContractPage() {
|
|
16
17
|
const { props } = usePage<{
|
|
17
18
|
contract: PageContract;
|
|
@@ -16,17 +16,23 @@ import {
|
|
|
16
16
|
registerLayout,
|
|
17
17
|
registerBlock,
|
|
18
18
|
// Shells
|
|
19
|
-
|
|
19
|
+
ProductShell,
|
|
20
|
+
ImmersiveShell,
|
|
20
21
|
// Layouts
|
|
21
22
|
StackLayout,
|
|
22
|
-
|
|
23
|
+
SidebarLayout,
|
|
23
24
|
DashboardLayout,
|
|
25
|
+
WizardLayout,
|
|
24
26
|
// Blocks
|
|
25
27
|
DenseTableBlock,
|
|
26
28
|
MetricCardBlock,
|
|
27
29
|
EmptyStateBlock,
|
|
28
30
|
DetailPanelBlock,
|
|
29
31
|
FormPanelBlock,
|
|
32
|
+
CardGridBlock,
|
|
33
|
+
StatusStripBlock,
|
|
34
|
+
TabbedPanelBlock,
|
|
35
|
+
LinkListBlock,
|
|
30
36
|
} from "@middag-io/react";
|
|
31
37
|
|
|
32
38
|
let registered = false;
|
|
@@ -36,12 +42,14 @@ export function registerDefaults(): void {
|
|
|
36
42
|
registered = true;
|
|
37
43
|
|
|
38
44
|
// Shells
|
|
39
|
-
registerShell("product",
|
|
45
|
+
registerShell("product", ProductShell);
|
|
46
|
+
registerShell("immersive", ImmersiveShell);
|
|
40
47
|
|
|
41
48
|
// Layouts
|
|
42
49
|
registerLayout("stack", StackLayout);
|
|
43
|
-
registerLayout("
|
|
50
|
+
registerLayout("sidebar", SidebarLayout);
|
|
44
51
|
registerLayout("dashboard", DashboardLayout);
|
|
52
|
+
registerLayout("wizard", WizardLayout);
|
|
45
53
|
|
|
46
54
|
// Blocks — add more as your pages need them
|
|
47
55
|
registerBlock("dense_table", DenseTableBlock);
|
|
@@ -49,4 +57,8 @@ export function registerDefaults(): void {
|
|
|
49
57
|
registerBlock("empty_state", EmptyStateBlock);
|
|
50
58
|
registerBlock("detail_panel", DetailPanelBlock);
|
|
51
59
|
registerBlock("form_panel", FormPanelBlock);
|
|
60
|
+
registerBlock("card_grid", CardGridBlock);
|
|
61
|
+
registerBlock("status_strip", StatusStripBlock);
|
|
62
|
+
registerBlock("tabbed_panel", TabbedPanelBlock);
|
|
63
|
+
registerBlock("link_list", LinkListBlock);
|
|
52
64
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route helper — custom host URL builder.
|
|
3
|
+
*
|
|
4
|
+
* Abstracts away the difference between:
|
|
5
|
+
* - Dev server (mock navigate via react-router)
|
|
6
|
+
* - Production (your host platform's URL scheme)
|
|
7
|
+
*
|
|
8
|
+
* Adjust the production URL pattern below to match your backend routing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
__MIDDAG_MOCK_NAVIGATE__?: (to: string) => void;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a URL for a given path.
|
|
19
|
+
*
|
|
20
|
+
* @param path - Route path (e.g. "/connectors", "/settings/general")
|
|
21
|
+
* @param params - Optional query parameters
|
|
22
|
+
* @returns Full URL string
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* route("/connectors")
|
|
26
|
+
* // dev → navigates via react-router
|
|
27
|
+
* // prod → "/app/connectors"
|
|
28
|
+
*
|
|
29
|
+
* route("/settings", { tab: "general" })
|
|
30
|
+
* // prod → "/app/settings?tab=general"
|
|
31
|
+
*/
|
|
32
|
+
export function route(path: string, params?: Record<string, string>): string {
|
|
33
|
+
// Dev mode — mock navigate (react-router)
|
|
34
|
+
if (typeof window !== "undefined" && window.__MIDDAG_MOCK_NAVIGATE__) {
|
|
35
|
+
const url = path + (params ? "?" + new URLSearchParams(params).toString() : "");
|
|
36
|
+
window.__MIDDAG_MOCK_NAVIGATE__(url);
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Production — adjust base path to match your backend routing
|
|
41
|
+
const base = "/app";
|
|
42
|
+
const query = params ? "?" + new URLSearchParams(params).toString() : "";
|
|
43
|
+
return `${base}${path}${query}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route helper — generates correct URLs for Moodle production vs dev mock.
|
|
3
|
+
*
|
|
4
|
+
* Moodle routing modes:
|
|
5
|
+
*
|
|
6
|
+
* Classic (Moodle ≤ 5.0):
|
|
7
|
+
* /local/{plugin}/index.php/{path}
|
|
8
|
+
*
|
|
9
|
+
* New router (Moodle 5.1+, opt-in):
|
|
10
|
+
* /r.php/{frankenstyle}/{path}
|
|
11
|
+
*
|
|
12
|
+
* Dev mock (BrowserRouter):
|
|
13
|
+
* /{path} directly
|
|
14
|
+
*
|
|
15
|
+
* In production, most navigation URLs come from PHP via Inertia shared props
|
|
16
|
+
* (navigation payload, PageContract actions). Use this helper only when
|
|
17
|
+
* the React side needs to build a URL programmatically (e.g. router.visit).
|
|
18
|
+
*
|
|
19
|
+
* Detection: window.__MIDDAG_MOCK_NAVIGATE__ → dev mock
|
|
20
|
+
* window.__MIDDAG_ROUTE_BASE__ → production base (set by PHP)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Base path set by the Moodle bootstrap. Falls back to classic format. */
|
|
24
|
+
const CLASSIC_BASE = "/__PLUGIN_PATH__/index.php";
|
|
25
|
+
|
|
26
|
+
function getBase(): string {
|
|
27
|
+
if (typeof window !== "undefined") {
|
|
28
|
+
// Dev mock — no base prefix
|
|
29
|
+
if ("__MIDDAG_MOCK_NAVIGATE__" in window) return "";
|
|
30
|
+
|
|
31
|
+
// Production — PHP sets the base in the bootstrap HTML
|
|
32
|
+
// Supports both classic (/local/plugin/index.php) and r.php (/r.php/frankenstyle)
|
|
33
|
+
const custom = (window as Record<string, unknown>).__MIDDAG_ROUTE_BASE__;
|
|
34
|
+
if (typeof custom === "string") return custom;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return CLASSIC_BASE;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a URL for a route within this Moodle plugin.
|
|
42
|
+
*
|
|
43
|
+
* @param path - Route path (e.g. "/admin/dashboard", "/connectors/1")
|
|
44
|
+
* @param params - Optional URL search params (e.g. { courseid: "5" })
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* route("/admin/dashboard")
|
|
48
|
+
* // dev: /admin/dashboard
|
|
49
|
+
* // prod: /local/myplugin/index.php/admin/dashboard
|
|
50
|
+
* // 5.1: /r.php/local_myplugin/admin/dashboard
|
|
51
|
+
*
|
|
52
|
+
* route("/admin/logs", { level: "error" })
|
|
53
|
+
* // → .../admin/logs?level=error
|
|
54
|
+
*/
|
|
55
|
+
export function route(
|
|
56
|
+
path: string,
|
|
57
|
+
params?: Record<string, string>,
|
|
58
|
+
): string {
|
|
59
|
+
const base = getBase();
|
|
60
|
+
const url = `${base}${path}`;
|
|
61
|
+
|
|
62
|
+
if (!params) return url;
|
|
63
|
+
|
|
64
|
+
const search = new URLSearchParams(params).toString();
|
|
65
|
+
return `${url}?${search}`;
|
|
66
|
+
}
|
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route helper — generates correct URLs for WordPress production vs dev mock.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Production (Inertia):
|
|
5
|
+
* /wp-admin/admin.php?page=middag-{slug}
|
|
6
|
+
* /wp-admin/admin.php?page=middag-{slug}&route={path}
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* Dev mock (BrowserRouter):
|
|
9
|
+
* /{path} directly
|
|
10
|
+
*
|
|
11
|
+
* Detection: window.__MIDDAG_MOCK_NAVIGATE__ → dev mock
|
|
8
12
|
*/
|
|
9
13
|
|
|
14
|
+
function isMock(): boolean {
|
|
15
|
+
return typeof window !== "undefined" && "__MIDDAG_MOCK_NAVIGATE__" in window;
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Build a URL for a MIDDAG admin page.
|
|
12
20
|
*
|
|
13
|
-
* Detection is lazy (checked per call) because module evaluation order
|
|
14
|
-
* means the mock flag may not be set when this module first loads.
|
|
15
|
-
*
|
|
16
21
|
* @param slug - Domain slug (e.g. "entitlements", "organizations")
|
|
17
|
-
* @param path - Optional sub-path (e.g. "/entitlements/1/edit")
|
|
22
|
+
* @param path - Optional sub-path for router dispatch (e.g. "/entitlements/1/edit")
|
|
23
|
+
* @param params - Optional extra query params (e.g. { tab: "billing" })
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* route("dashboard")
|
|
27
|
+
* // dev: /dashboard
|
|
28
|
+
* // prod: /wp-admin/admin.php?page=middag-dashboard
|
|
29
|
+
*
|
|
30
|
+
* route("entitlements", "/entitlements/1/edit")
|
|
31
|
+
* // dev: /entitlements/1/edit
|
|
32
|
+
* // prod: /wp-admin/admin.php?page=middag-entitlements&route=/entitlements/1/edit
|
|
33
|
+
*
|
|
34
|
+
* route("entitlements", "/entitlements/1", { tab: "billing" })
|
|
35
|
+
* // dev: /entitlements/1?tab=billing
|
|
36
|
+
* // prod: /wp-admin/admin.php?page=middag-entitlements&route=/entitlements/1&tab=billing
|
|
18
37
|
*/
|
|
19
|
-
export function route(
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
export function route(
|
|
39
|
+
slug: string,
|
|
40
|
+
path?: string,
|
|
41
|
+
params?: Record<string, string>,
|
|
42
|
+
): string {
|
|
43
|
+
if (isMock()) {
|
|
44
|
+
const url = path ?? `/${slug}`;
|
|
45
|
+
if (!params) return url;
|
|
46
|
+
return `${url}?${new URLSearchParams(params).toString()}`;
|
|
22
47
|
}
|
|
23
48
|
|
|
24
49
|
const base = `/wp-admin/admin.php?page=middag-${slug}`;
|
|
25
|
-
|
|
50
|
+
const parts = [base];
|
|
51
|
+
if (path) parts.push(`route=${encodeURIComponent(path)}`);
|
|
52
|
+
if (params) {
|
|
53
|
+
for (const [k, v] of Object.entries(params)) {
|
|
54
|
+
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return parts.join("&");
|
|
26
58
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tailwind CSS entry — processed by @tailwindcss/vite during AMD build.
|
|
3
|
+
*
|
|
4
|
+
* This file brings in the Tailwind engine so that any utility classes
|
|
5
|
+
* used in your local pages (src/pages/) and components (src/blocks/,
|
|
6
|
+
* src/components/) are compiled into the final CSS.
|
|
7
|
+
*
|
|
8
|
+
* The lib's pre-compiled style.css covers all @middag-io/react components.
|
|
9
|
+
* This file covers YOUR custom code. Both are merged into a single
|
|
10
|
+
* styles/middag-app.css by the Moodle build.
|
|
11
|
+
*/
|
|
12
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vite-plugin-moodle-amd — Rewrites Rollup AMD output for Moodle's RequireJS.
|
|
3
|
+
*
|
|
4
|
+
* Rollup's AMD format with code splitting uses relative paths between chunks
|
|
5
|
+
* (e.g. './react-vendor-lazy'). Moodle's RequireJS can resolve these relative
|
|
6
|
+
* to the plugin prefix, but only when the module namespace is correct.
|
|
7
|
+
*
|
|
8
|
+
* This plugin:
|
|
9
|
+
* 1. Rewrites relative inter-chunk paths to absolute `{PLUGIN_PREFIX}/{name}` format
|
|
10
|
+
* 2. Adds named define IDs for better debuggability
|
|
11
|
+
* 3. Copies built JS to amd/src/ (dev) and amd/build/ (prod .min.js)
|
|
12
|
+
* 4. Copies CSS to styles/ so Moodle auto-discovers it
|
|
13
|
+
*
|
|
14
|
+
* Change PLUGIN_PREFIX below to match your plugin's frankenstyle name
|
|
15
|
+
* (e.g. 'local_yourplugin', 'mod_yourmod').
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {Plugin, ResolvedConfig} from "vite";
|
|
19
|
+
import {copyFileSync, existsSync, mkdirSync, readdirSync} from "node:fs";
|
|
20
|
+
import {basename, resolve} from "node:path";
|
|
21
|
+
|
|
22
|
+
// ── CHANGE THIS to your Moodle plugin's frankenstyle name ──
|
|
23
|
+
const PLUGIN_PREFIX = "__PLUGIN_PREFIX__";
|
|
24
|
+
|
|
25
|
+
export default function moodleAmd(): Plugin {
|
|
26
|
+
let config: ResolvedConfig;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
name: "vite-plugin-moodle-amd",
|
|
30
|
+
apply: "build",
|
|
31
|
+
|
|
32
|
+
configResolved(resolved) {
|
|
33
|
+
config = resolved;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Post-process each AMD chunk to rewrite paths.
|
|
38
|
+
*
|
|
39
|
+
* Rollup generates: define(['./react-vendor-lazy', ...], function(...) { ... })
|
|
40
|
+
* We rewrite to: define('PLUGIN_PREFIX/middag-app', ['PLUGIN_PREFIX/react-vendor-lazy', ...], function(...) { ... })
|
|
41
|
+
*/
|
|
42
|
+
generateBundle(_, bundle) {
|
|
43
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
44
|
+
if (chunk.type !== "chunk") continue;
|
|
45
|
+
|
|
46
|
+
let code = chunk.code;
|
|
47
|
+
const moduleName = fileName.replace(/\.js$/, "");
|
|
48
|
+
|
|
49
|
+
// Rewrite relative AMD dependencies to absolute Moodle paths.
|
|
50
|
+
code = code.replace(
|
|
51
|
+
/define\(\[([\s\S]*?)\]/,
|
|
52
|
+
(match, deps: string) => {
|
|
53
|
+
const rewritten = deps.replace(
|
|
54
|
+
/(["'])\.\/([^"']+)\1/g,
|
|
55
|
+
`$1${PLUGIN_PREFIX}/$2$1`,
|
|
56
|
+
);
|
|
57
|
+
return `define([${rewritten}]`;
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Add named module ID.
|
|
62
|
+
if (!code.includes(`define('${PLUGIN_PREFIX}/${moduleName}'`)) {
|
|
63
|
+
code = code.replace(
|
|
64
|
+
/define\(\[/,
|
|
65
|
+
`define('${PLUGIN_PREFIX}/${moduleName}',[`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
chunk.code = code;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* After each build, copy chunks to amd/src/ and amd/build/.
|
|
75
|
+
* Makes `npm run watch:moodle` auto-update both directories.
|
|
76
|
+
*/
|
|
77
|
+
closeBundle() {
|
|
78
|
+
const outDir = config.build.outDir;
|
|
79
|
+
const pluginRoot = resolve(config.root, "..");
|
|
80
|
+
const amdSrc = resolve(pluginRoot, "amd/src");
|
|
81
|
+
const amdBuild = resolve(pluginRoot, "amd/build");
|
|
82
|
+
|
|
83
|
+
if (!existsSync(outDir)) return;
|
|
84
|
+
if (!existsSync(amdSrc)) mkdirSync(amdSrc, {recursive: true});
|
|
85
|
+
if (!existsSync(amdBuild)) mkdirSync(amdBuild, {recursive: true});
|
|
86
|
+
|
|
87
|
+
const jsFiles = readdirSync(outDir).filter((f) => f.endsWith(".js"));
|
|
88
|
+
|
|
89
|
+
for (const file of jsFiles) {
|
|
90
|
+
const src = resolve(outDir, file);
|
|
91
|
+
const name = basename(file, ".js");
|
|
92
|
+
|
|
93
|
+
copyFileSync(src, resolve(amdSrc, file));
|
|
94
|
+
copyFileSync(src, resolve(amdBuild, `${name}.min.js`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (jsFiles.length > 0) {
|
|
98
|
+
console.log(
|
|
99
|
+
`[moodle-amd] ${jsFiles.length} chunk(s) → amd/src/ + amd/build/`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Copy CSS to styles/ so Moodle auto-discovers it.
|
|
104
|
+
const assetsDir = resolve(outDir, "assets");
|
|
105
|
+
if (existsSync(assetsDir)) {
|
|
106
|
+
const stylesDir = resolve(pluginRoot, "styles");
|
|
107
|
+
if (!existsSync(stylesDir)) mkdirSync(stylesDir, {recursive: true});
|
|
108
|
+
|
|
109
|
+
const cssFiles = readdirSync(assetsDir).filter((f) =>
|
|
110
|
+
f.endsWith(".css"),
|
|
111
|
+
);
|
|
112
|
+
for (const file of cssFiles) {
|
|
113
|
+
copyFileSync(
|
|
114
|
+
resolve(assetsDir, file),
|
|
115
|
+
resolve(stylesDir, "middag-app.css"),
|
|
116
|
+
);
|
|
117
|
+
console.log(`[moodle-amd] ${file} → styles/middag-app.css`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|