create-middag-ui 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -33,13 +33,16 @@ import {
33
33
  scaffoldIndexHtml,
34
34
  scaffoldDemoFiles,
35
35
  scaffoldPageExamples,
36
- scaffoldProApp,
37
36
  scaffoldFreeApp,
38
37
  scaffoldFreeAdapters,
39
38
  scaffoldDevShell,
40
39
  scaffoldHostEntry,
41
40
  scaffoldHostViteConfig,
42
41
  scaffoldHostThemeCSS,
42
+ scaffoldFreeRegister,
43
+ scaffoldPageResolver,
44
+ scaffoldRouteHelper,
45
+ scaffoldDemoDirectPage,
43
46
  } from "./lib/scaffold.js";
44
47
  import { runNpmInstall } from "./lib/install.js";
45
48
  import { log, success, heading, blank, info } from "./lib/ui.js";
@@ -150,10 +153,33 @@ heading(8, TOTAL_STEPS, `Creating ${isPro ? "PRO" : "FREE"} UI module`);
150
153
 
151
154
  scaffoldPageExamples(targetDir);
152
155
 
156
+ // Shared files (both PRO and FREE)
157
+ scaffoldPageResolver(targetDir);
158
+ scaffoldDemoDirectPage(targetDir);
159
+ scaffoldRouteHelper(targetDir, hostKey);
160
+
153
161
  if (isPro) {
154
- scaffoldProApp(targetDir);
155
- success("PRO: using MockProductShell from @middag-io/react/mock");
162
+ try {
163
+ const pro = await import("./lib/scaffoldPRO.js");
164
+ pro.scaffoldProRegister(targetDir);
165
+ pro.scaffoldProApp(targetDir);
166
+ pro.scaffoldMockNavigation(targetDir);
167
+ pro.scaffoldMockData(targetDir);
168
+ pro.scaffoldMockEntities(targetDir);
169
+ pro.scaffoldMockPageContracts(targetDir);
170
+ pro.scaffoldMockRoutes(targetDir);
171
+ success("PRO: using MockProductShell from @middag-io/react/mock");
172
+ } catch {
173
+ // npm version — PRO file excluded, fall back to FREE
174
+ info("PRO scaffold not available — using FREE path");
175
+ scaffoldFreeRegister(targetDir);
176
+ scaffoldFreeAdapters(targetDir);
177
+ scaffoldDevShell(targetDir);
178
+ scaffoldFreeApp(targetDir);
179
+ success("FREE: generated DevShell + local Inertia adapters");
180
+ }
156
181
  } else {
182
+ scaffoldFreeRegister(targetDir);
157
183
  scaffoldFreeAdapters(targetDir);
158
184
  scaffoldDevShell(targetDir);
159
185
  scaffoldFreeApp(targetDir);
package/lib/scaffold.js CHANGED
@@ -312,6 +312,7 @@ export function scaffoldIndexHtml(targetDir) {
312
312
  -->
313
313
  <div id="root" class="middag-root"></div>
314
314
  <div id="middag-portals" class="middag-root"></div>
315
+ <script>window.__MIDDAG_MOCK_NAVIGATE__ = true;</script>
315
316
  <script type="module" src="/src/main.tsx"></script>
316
317
  </body>
317
318
  </html>
@@ -1304,39 +1305,31 @@ export function scaffoldHostEntry(targetDir, hostKey) {
1304
1305
  */
1305
1306
  import { createRoot } from "react-dom/client";
1306
1307
  import { createInertiaApp } from "@inertiajs/react";
1307
- import {
1308
- registerDefaults,
1309
- registerShell,
1310
- ContractPage,
1311
- HostProductShell,
1312
- I18nProvider,
1313
- ProgressProvider,
1314
- ptBR,
1315
- } from "@middag-io/react";
1316
- import type { PageContract } from "@middag-io/react";
1308
+ import { I18nProvider, ProgressProvider } from "@middag-io/react";
1317
1309
  import "@middag-io/react/style.css";
1318
1310
  import "./theme.css";
1311
+ import { registerDefaults } from "./app/register";
1312
+ import { resolvePageComponent } from "./app/page-resolver";
1319
1313
 
1320
1314
  registerDefaults();
1321
- registerShell("product", HostProductShell);
1322
1315
 
1323
1316
  createInertiaApp({
1324
1317
  id: "middag-app",
1325
- resolve: () => {
1326
- const Page = ({ contract }: { contract: PageContract }) => (
1327
- <I18nProvider overrides={ptBR}>
1328
- <ProgressProvider>
1329
- <ContractPage contract={contract} />
1330
- </ProgressProvider>
1331
- </I18nProvider>
1332
- );
1333
- Page.displayName = "ContractPageWrapper";
1334
- return Page;
1335
- },
1318
+ resolve: (name) => resolvePageComponent(name),
1336
1319
  setup({ el, App, props }) {
1337
1320
  el.classList.add("middag-root");
1338
1321
  ${setupCode}
1339
- createRoot(el).render(<App {...props} />);
1322
+ createRoot(el).render(
1323
+ <ProgressProvider>
1324
+ <App {...props}>
1325
+ {({ Component, props: pageProps, key }) => (
1326
+ <I18nProvider>
1327
+ <Component key={key} {...pageProps} />
1328
+ </I18nProvider>
1329
+ )}
1330
+ </App>
1331
+ </ProgressProvider>,
1332
+ );
1340
1333
  ${postMountCode}
1341
1334
  },
1342
1335
  });
@@ -1445,7 +1438,7 @@ export default defineConfig({
1445
1438
  export function scaffoldHostThemeCSS(targetDir, hostKey, host) {
1446
1439
  const themePath = join(targetDir, "src", "theme.css");
1447
1440
 
1448
- let hostSection = "";
1441
+ let hostSection;
1449
1442
 
1450
1443
  if (hostKey === "wordpress") {
1451
1444
  hostSection = `
@@ -1787,3 +1780,70 @@ export const router = {
1787
1780
  );
1788
1781
  }
1789
1782
  }
1783
+
1784
+ // ── Template reader ─────────────────────────────────────────────────────
1785
+
1786
+ /** Read a template file relative to this script's directory. */
1787
+ function readTemplate(relativePath) {
1788
+ return readFileSync(join(__dirname, relativePath), "utf-8");
1789
+ }
1790
+
1791
+ // ── Shared: register, page-resolver, route helper, demo page ────────────
1792
+
1793
+ /**
1794
+ * Scaffold FREE register: src/app/register.ts — minimal (5 blocks).
1795
+ */
1796
+ export function scaffoldFreeRegister(targetDir) {
1797
+ ensureDir(join(targetDir, "src", "app"));
1798
+ const filePath = join(targetDir, "src", "app", "register.ts");
1799
+ if (skipIfExists(filePath, "src/app/register.ts")) return;
1800
+ writeFile(filePath, readTemplate("templates/shared/register-free.ts"), "src/app/register.ts (FREE)");
1801
+ }
1802
+
1803
+ /**
1804
+ * Scaffold page resolver: src/app/page-resolver.tsx.
1805
+ * Supports Contract: prefix pages (ContractPage) and direct pages (glob).
1806
+ */
1807
+ export function scaffoldPageResolver(targetDir) {
1808
+ ensureDir(join(targetDir, "src", "app"));
1809
+ const filePath = join(targetDir, "src", "app", "page-resolver.tsx");
1810
+ if (skipIfExists(filePath, "src/app/page-resolver.tsx")) return;
1811
+ writeFile(filePath, readTemplate("templates/shared/page-resolver.tsx"), "src/app/page-resolver.tsx");
1812
+ }
1813
+
1814
+ /**
1815
+ * Scaffold route helper: src/lib/routes.ts.
1816
+ * Abstracts host admin URL vs dev mock path.
1817
+ *
1818
+ * @param {string} hostKey - 'wordpress' | 'moodle' | 'custom'
1819
+ */
1820
+ export function scaffoldRouteHelper(targetDir, hostKey) {
1821
+ ensureDir(join(targetDir, "src", "lib"));
1822
+ const filePath = join(targetDir, "src", "lib", "routes.ts");
1823
+ if (skipIfExists(filePath, "src/lib/routes.ts")) return;
1824
+
1825
+ const templateMap = {
1826
+ wordpress: "templates/shared/route-helper-wp.ts",
1827
+ moodle: "templates/shared/route-helper-moodle.ts",
1828
+ custom: "templates/shared/route-helper-custom.ts",
1829
+ };
1830
+ const template = templateMap[hostKey] || templateMap.wordpress;
1831
+
1832
+ try {
1833
+ writeFile(filePath, readTemplate(template), "src/lib/routes.ts");
1834
+ } catch {
1835
+ // Fallback to WP template if host-specific one doesn't exist
1836
+ writeFile(filePath, readTemplate("templates/shared/route-helper-wp.ts"), "src/lib/routes.ts");
1837
+ }
1838
+ }
1839
+
1840
+ /**
1841
+ * Scaffold demo direct page: src/pages/DemoPage.tsx.
1842
+ * Shows the direct page pattern (usePage + custom React).
1843
+ */
1844
+ export function scaffoldDemoDirectPage(targetDir) {
1845
+ ensureDir(join(targetDir, "src", "pages"));
1846
+ const filePath = join(targetDir, "src", "pages", "DemoPage.tsx");
1847
+ if (skipIfExists(filePath, "src/pages/DemoPage.tsx")) return;
1848
+ writeFile(filePath, readTemplate("templates/shared/demo-page.tsx"), "src/pages/DemoPage.tsx");
1849
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * scaffoldPRO.js — PRO-only scaffold functions for create-middag-ui.
3
+ *
4
+ * NOT published to npm (excluded via .npmignore).
5
+ * Only available when installed from GitHub Packages.
6
+ *
7
+ * Generates the extended mock harness (mock/, src/app/register.ts)
8
+ * with 9+ blocks, extracted navigation/data/entities/routes files,
9
+ * and the slim app.tsx that delegates to mock/.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { success, warn, error } from "./ui.js";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+
19
+ // ── Helpers (duplicated from scaffold.js — not exported there) ──────
20
+
21
+ function readTemplate(relativePath) {
22
+ return readFileSync(join(__dirname, relativePath), "utf-8");
23
+ }
24
+
25
+ function writeFile(filePath, content, label) {
26
+ try {
27
+ writeFileSync(filePath, content);
28
+ success(`Created ${label}`);
29
+ return true;
30
+ } catch (err) {
31
+ error(`Failed to create ${label}: ${err.message}`);
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function ensureDir(dirPath) {
37
+ try {
38
+ mkdirSync(dirPath, { recursive: true });
39
+ return true;
40
+ } catch (err) {
41
+ error(`Failed to create directory ${dirPath}: ${err.message}`);
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function skipIfExists(filePath, label) {
47
+ if (existsSync(filePath)) {
48
+ warn(`${label} already exists — skipping`);
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ // ── 1. PRO register (9+ blocks) ────────────────────────────────────────
55
+
56
+ /**
57
+ * Scaffold `src/app/register.ts` — PRO version with 9+ blocks.
58
+ * Overrides any FREE register that may have been scaffolded.
59
+ *
60
+ * @param {string} targetDir - Absolute path to UI project root
61
+ */
62
+ export function scaffoldProRegister(targetDir) {
63
+ ensureDir(join(targetDir, "src", "app"));
64
+
65
+ const filePath = join(targetDir, "src", "app", "register.ts");
66
+ if (skipIfExists(filePath, "src/app/register.ts")) return;
67
+
68
+ writeFile(
69
+ filePath,
70
+ readTemplate("templates/pro/register-pro.ts"),
71
+ "src/app/register.ts",
72
+ );
73
+ }
74
+
75
+ // ── 2. PRO app (main.tsx + app.tsx) ────────────────────────────────────
76
+
77
+ /**
78
+ * Scaffold `src/main.tsx` and `src/app.tsx` for the PRO path.
79
+ *
80
+ * main.tsx boots the dev server with MockProductShell.
81
+ * app.tsx is a slim shell that delegates to mock/ files.
82
+ *
83
+ * @param {string} targetDir - Absolute path to UI project root
84
+ */
85
+ export function scaffoldProApp(targetDir) {
86
+ ensureDir(join(targetDir, "src"));
87
+
88
+ // ── src/main.tsx ──────────────────────────────────────────────────
89
+ const mainPath = join(targetDir, "src", "main.tsx");
90
+ if (!skipIfExists(mainPath, "src/main.tsx")) {
91
+ writeFile(
92
+ mainPath,
93
+ readTemplate("templates/pro/main.tsx"),
94
+ "src/main.tsx",
95
+ );
96
+ }
97
+
98
+ // ── src/app.tsx ───────────────────────────────────────────────────
99
+ const appPath = join(targetDir, "src", "app.tsx");
100
+ if (!skipIfExists(appPath, "src/app.tsx")) {
101
+ writeFile(
102
+ appPath,
103
+ readTemplate("templates/pro/app.tsx"),
104
+ "src/app.tsx",
105
+ );
106
+ }
107
+ }
108
+
109
+ // ── 3. Mock navigation ─────────────────────────────────────────────────
110
+
111
+ /**
112
+ * Scaffold `mock/navigation.ts` — sidebar structure for dev server.
113
+ *
114
+ * @param {string} targetDir - Absolute path to UI project root
115
+ */
116
+ export function scaffoldMockNavigation(targetDir) {
117
+ ensureDir(join(targetDir, "mock"));
118
+
119
+ const filePath = join(targetDir, "mock", "navigation.ts");
120
+ if (skipIfExists(filePath, "mock/navigation.ts")) return;
121
+
122
+ writeFile(
123
+ filePath,
124
+ readTemplate("templates/pro/mock-navigation.ts"),
125
+ "mock/navigation.ts",
126
+ );
127
+ }
128
+
129
+ // ── 4. Mock data ───────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * Scaffold `mock/data.ts` — synthetic props for dev mode pages.
133
+ *
134
+ * @param {string} targetDir - Absolute path to UI project root
135
+ */
136
+ export function scaffoldMockData(targetDir) {
137
+ ensureDir(join(targetDir, "mock"));
138
+
139
+ const filePath = join(targetDir, "mock", "data.ts");
140
+ if (skipIfExists(filePath, "mock/data.ts")) return;
141
+
142
+ writeFile(
143
+ filePath,
144
+ readTemplate("templates/pro/mock-data.ts"),
145
+ "mock/data.ts",
146
+ );
147
+ }
148
+
149
+ // ── 5. Mock entities ───────────────────────────────────────────────────
150
+
151
+ /**
152
+ * Scaffold `mock/entities.ts` — entity route map for mock contracts.
153
+ *
154
+ * @param {string} targetDir - Absolute path to UI project root
155
+ */
156
+ export function scaffoldMockEntities(targetDir) {
157
+ ensureDir(join(targetDir, "mock"));
158
+
159
+ const filePath = join(targetDir, "mock", "entities.ts");
160
+ if (skipIfExists(filePath, "mock/entities.ts")) return;
161
+
162
+ writeFile(
163
+ filePath,
164
+ readTemplate("templates/pro/mock-entities.ts"),
165
+ "mock/entities.ts",
166
+ );
167
+ }
168
+
169
+ // ── 6. Mock page contracts ────────────────────────────────────────────
170
+
171
+ /**
172
+ * Move page contract examples to `mock/page-contracts/` for PRO path.
173
+ *
174
+ * In PRO, the 3 demo contracts (dashboard, connectors, settings) live
175
+ * in mock/page-contracts/ instead of src/pages/ because they are dev-only
176
+ * mock data, not production React components.
177
+ *
178
+ * scaffoldPageExamples() already created them in src/pages/.
179
+ * This function reads them from there and writes to mock/page-contracts/,
180
+ * then removes the originals from src/pages/.
181
+ *
182
+ * @param {string} targetDir - Absolute path to UI project root
183
+ */
184
+ export function scaffoldMockPageContracts(targetDir) {
185
+ const srcDir = join(targetDir, "src", "pages");
186
+ const destDir = join(targetDir, "mock", "page-contracts");
187
+ ensureDir(destDir);
188
+
189
+ const files = ["dashboard.ts", "connectors.ts", "settings.ts"];
190
+
191
+ for (const file of files) {
192
+ const src = join(srcDir, file);
193
+ const dest = join(destDir, file);
194
+
195
+ if (skipIfExists(dest, `mock/page-contracts/${file}`)) continue;
196
+
197
+ if (existsSync(src)) {
198
+ const content = readFileSync(src, "utf-8");
199
+ writeFile(dest, content, `mock/page-contracts/${file}`);
200
+ // Remove from src/pages/ — PRO keeps contracts in mock/ only
201
+ try {
202
+ unlinkSync(src);
203
+ } catch {
204
+ // Not critical — src/pages/*.ts won't interfere (glob matches .tsx only)
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ // ── 7. Mock routes ─────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Scaffold `mock/routes.tsx` — all dev server route definitions.
214
+ *
215
+ * @param {string} targetDir - Absolute path to UI project root
216
+ */
217
+ export function scaffoldMockRoutes(targetDir) {
218
+ ensureDir(join(targetDir, "mock"));
219
+
220
+ const filePath = join(targetDir, "mock", "routes.tsx");
221
+ if (skipIfExists(filePath, "mock/routes.tsx")) return;
222
+
223
+ writeFile(
224
+ filePath,
225
+ readTemplate("templates/pro/mock-routes.tsx"),
226
+ "mock/routes.tsx",
227
+ );
228
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Dev mock app — shell that never needs editing.
3
+ *
4
+ * To add pages or change navigation, edit files in mock/:
5
+ * mock/routes.tsx — route definitions
6
+ * mock/navigation.ts — sidebar structure
7
+ * mock/data.ts — synthetic page props
8
+ */
9
+ import { useEffect } from "react";
10
+ import { BrowserRouter, Routes, useNavigate } from "react-router";
11
+ import { I18nProvider } from "@middag-io/react";
12
+ import { MockPageProvider, MockI18nProvider, 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
+
17
+ let _navigate: ((to: string) => void) | null = null;
18
+ function NavigateBridge() {
19
+ const navigate = useNavigate();
20
+ useEffect(() => {
21
+ _navigate = (to: string) => navigate(to);
22
+ setMockNavigate((to: string) => navigate(to));
23
+ }, [navigate]);
24
+ return null;
25
+ }
26
+ if (typeof window !== "undefined") {
27
+ (window as any).__MIDDAG_MOCK_NAVIGATE__ = (to: string) => {
28
+ if (_navigate) _navigate(to);
29
+ };
30
+ }
31
+
32
+ export function App() {
33
+ return (
34
+ <MockI18nProvider>
35
+ <MockPageProvider
36
+ value={{
37
+ props: { ...sharedProps, contract: null, navigation: buildNavigation("") },
38
+ url: "/",
39
+ }}
40
+ >
41
+ <I18nProvider>
42
+ <BrowserRouter>
43
+ <NavigateBridge />
44
+ <Routes>{AppRoutes()}</Routes>
45
+ </BrowserRouter>
46
+ </I18nProvider>
47
+ </MockPageProvider>
48
+ </MockI18nProvider>
49
+ );
50
+ }
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { MockProductShell } from "@middag-io/react/mock";
4
+ import { registerShell } from "@middag-io/react";
5
+ import "@middag-io/react/style.css";
6
+ import "./theme.css";
7
+ import "@fontsource-variable/figtree";
8
+ import { registerDefaults } from "./app/register";
9
+ import { App } from "./app";
10
+
11
+ registerDefaults();
12
+ registerShell("product", MockProductShell);
13
+
14
+ createRoot(document.getElementById("root")!).render(
15
+ <StrictMode><App /></StrictMode>,
16
+ );
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Mock data — synthetic props for direct pages in dev mode.
3
+ *
4
+ * Used by mock/routes.tsx to feed Inertia-like props via MockPageProvider.
5
+ * In production, your host platform page controllers provide this data.
6
+ */
7
+
8
+ export const sharedProps = {
9
+ auth: { id: 1, name: "Dev User", email: "dev@localhost", capabilities: [] },
10
+ theme: { appearance: "light" as const, strings: {} as Record<string, string> },
11
+ flash: {},
12
+ locale: "en",
13
+ version: "0.0.0-dev",
14
+ scope: { extension: null, context: "global" },
15
+ };
16
+
17
+ /**
18
+ * Props for the demo direct page (pages/DemoPage.tsx).
19
+ * Add more page props here as you create direct pages.
20
+ */
21
+ export const mockDemoPageProps = {
22
+ title: "Demo Direct Page",
23
+ items: [
24
+ { id: 1, name: "Item One", status: "active" },
25
+ { id: 2, name: "Item Two", status: "pending" },
26
+ { id: 3, name: "Item Three", status: "completed" },
27
+ ],
28
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Entity route map — shared by all mock contracts.
3
+ *
4
+ * Defines navigable entity types and their URL patterns.
5
+ * In production, your host platform builds the same map with admin URLs.
6
+ *
7
+ * Add entity types here as you create domain pages.
8
+ */
9
+ export const mockEntities: Record<string, string> = {
10
+ dashboard: "/",
11
+ connector: "/connectors/{id}",
12
+ setting: "/settings/{id}",
13
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Mock navigation — sidebar structure for dev server.
3
+ *
4
+ * Used by app.tsx routes to simulate host admin navigation.
5
+ * In production, your host platform sends navigation via Inertia shared props.
6
+ */
7
+
8
+ export function buildNavigation(activeKey: string) {
9
+ return {
10
+ sections: [
11
+ {
12
+ key: "overview",
13
+ label: "Overview",
14
+ icon: "home",
15
+ group: "main" as const,
16
+ items: [
17
+ {
18
+ key: "overview.dashboard",
19
+ label: "Dashboard",
20
+ href: "/",
21
+ active: activeKey === "overview.dashboard",
22
+ children: [],
23
+ },
24
+ ],
25
+ },
26
+ {
27
+ key: "integration",
28
+ label: "Integration",
29
+ icon: "plug",
30
+ group: "main" as const,
31
+ items: [
32
+ {
33
+ key: "integration.connectors",
34
+ label: "Connectors",
35
+ href: "/connectors",
36
+ active: activeKey === "integration.connectors",
37
+ children: [],
38
+ },
39
+ ],
40
+ },
41
+ {
42
+ key: "system",
43
+ label: "System",
44
+ icon: "settings",
45
+ group: "system" as const,
46
+ items: [
47
+ {
48
+ key: "system.settings",
49
+ label: "Settings",
50
+ href: "/settings",
51
+ active: activeKey === "system.settings",
52
+ children: [],
53
+ },
54
+ ],
55
+ },
56
+ ],
57
+ activeKey,
58
+ };
59
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Mock routes — all dev server route definitions.
3
+ *
4
+ * Add new pages here. app.tsx renders these without modification.
5
+ */
6
+ import { Route } from "react-router";
7
+ import { type ReactNode } from "react";
8
+ import { ContractPage, resolveShell, EntityRoutesProvider } from "@middag-io/react";
9
+ import { MockPageProvider } from "@middag-io/react/mock";
10
+ import type { PageContract } from "@middag-io/react";
11
+ import { mockEntities } from "./entities";
12
+ import { buildNavigation } from "./navigation";
13
+ import { sharedProps, mockDemoPageProps } from "./data";
14
+ import { dashboardContract } from "./page-contracts/dashboard";
15
+ import { connectorsContract } from "./page-contracts/connectors";
16
+ import { settingsContract } from "./page-contracts/settings";
17
+ import DemoPage from "../src/pages/DemoPage";
18
+
19
+ // Resolve shell at module level (not inside render — avoids react-hooks/static-components)
20
+ const ProductShell = resolveShell("product");
21
+
22
+ // ── Route wrappers ──────────────────────────────────────────────────
23
+
24
+ function MockRoute({ contract, activeKey }: { contract: PageContract; activeKey: string }) {
25
+ return (
26
+ <MockPageProvider
27
+ value={{
28
+ props: { ...sharedProps, contract, navigation: buildNavigation(activeKey) },
29
+ url: window.location.pathname,
30
+ }}
31
+ >
32
+ <ContractPage contract={contract} />
33
+ </MockPageProvider>
34
+ );
35
+ }
36
+
37
+ function MockDirectRoute({
38
+ activeKey,
39
+ pageProps,
40
+ children,
41
+ }: {
42
+ activeKey: string;
43
+ pageProps: Record<string, unknown>;
44
+ children: ReactNode;
45
+ }) {
46
+ return (
47
+ <MockPageProvider
48
+ value={{
49
+ props: { ...sharedProps, ...pageProps, navigation: buildNavigation(activeKey) },
50
+ url: window.location.pathname,
51
+ }}
52
+ >
53
+ <EntityRoutesProvider entities={mockEntities}>
54
+ {ProductShell ? <ProductShell>{children}</ProductShell> : children}
55
+ </EntityRoutesProvider>
56
+ </MockPageProvider>
57
+ );
58
+ }
59
+
60
+ // ── Route list ──────────────────────────────────────────────────────
61
+
62
+ export function AppRoutes() {
63
+ return (
64
+ <>
65
+ {/* Contract pages (rendered by lib ContractPage) */}
66
+ <Route
67
+ path="/"
68
+ element={<MockRoute contract={dashboardContract} activeKey="overview.dashboard" />}
69
+ />
70
+ <Route
71
+ path="/connectors"
72
+ element={<MockRoute contract={connectorsContract} activeKey="integration.connectors" />}
73
+ />
74
+ <Route
75
+ path="/settings"
76
+ element={<MockRoute contract={settingsContract} activeKey="system.settings" />}
77
+ />
78
+
79
+ {/* Direct page (custom React component) */}
80
+ <Route
81
+ path="/demo"
82
+ element={
83
+ <MockDirectRoute activeKey="overview.demo" pageProps={mockDemoPageProps}>
84
+ <DemoPage />
85
+ </MockDirectRoute>
86
+ }
87
+ />
88
+ </>
89
+ );
90
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * register — selective registration for this plugin's UI (PRO).
3
+ *
4
+ * Registers shells, layouts, and blocks this plugin uses.
5
+ * Add or remove registrations as your pages need them.
6
+ *
7
+ * Full catalog: https://docs.middag.io/blocks
8
+ */
9
+
10
+ import {
11
+ registerShell,
12
+ registerLayout,
13
+ registerBlock,
14
+ // Shells
15
+ HostProductShell,
16
+ // Layouts
17
+ StackLayout,
18
+ SplitLayout,
19
+ DashboardLayout,
20
+ // Blocks
21
+ DenseTableBlock,
22
+ MetricCardBlock,
23
+ EmptyStateBlock,
24
+ DetailPanelBlock,
25
+ StatusStripBlock,
26
+ FormPanelBlock,
27
+ TabbedPanelBlock,
28
+ ActivityTimelineBlock,
29
+ WorkflowProgressBlock,
30
+ } from "@middag-io/react";
31
+
32
+ let registered = false;
33
+
34
+ export function registerDefaults(): void {
35
+ if (registered) return;
36
+ registered = true;
37
+
38
+ // Shells
39
+ registerShell("product", HostProductShell);
40
+
41
+ // Layouts
42
+ registerLayout("stack", StackLayout);
43
+ registerLayout("split", SplitLayout);
44
+ registerLayout("dashboard", DashboardLayout);
45
+
46
+ // Blocks — add more as your pages need them
47
+ // See: https://docs.middag.io/blocks for the full catalog
48
+ registerBlock("dense_table", DenseTableBlock);
49
+ registerBlock("metric_card", MetricCardBlock);
50
+ registerBlock("empty_state", EmptyStateBlock);
51
+ registerBlock("detail_panel", DetailPanelBlock);
52
+ registerBlock("status_strip", StatusStripBlock);
53
+ registerBlock("form_panel", FormPanelBlock);
54
+ registerBlock("tabbed_panel", TabbedPanelBlock);
55
+ registerBlock("activity_timeline", ActivityTimelineBlock);
56
+ registerBlock("workflow_progress", WorkflowProgressBlock);
57
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Demo Direct Page — example of a custom React page.
3
+ *
4
+ * Direct pages receive props from Inertia (via usePage) and render
5
+ * custom UI. Use this pattern for complex pages that can't be
6
+ * expressed as a PageContract (workflows, wizards, dashboards).
7
+ *
8
+ * In production, the host page controller calls:
9
+ * InertiaAdapter::render('DemoPage', { title: '...', items: [...] })
10
+ *
11
+ * The page-resolver matches "DemoPage" to this file via import.meta.glob.
12
+ */
13
+ import { usePage } from "@inertiajs/react";
14
+
15
+ interface DemoPageProps {
16
+ title: string;
17
+ items: Array<{ id: number; name: string; status: string }>;
18
+ }
19
+
20
+ export default function DemoPage() {
21
+ const { props } = usePage<DemoPageProps>();
22
+ const { title, items } = props;
23
+
24
+ return (
25
+ <div className="mx-auto max-w-2xl p-8">
26
+ <h1 className="text-foreground text-2xl font-semibold">{title}</h1>
27
+ <p className="text-muted-foreground mt-2 text-sm">
28
+ This is a direct page — it uses usePage() instead of PageContract.
29
+ </p>
30
+ <ul className="mt-6 space-y-2">
31
+ {items?.map((item) => (
32
+ <li
33
+ key={item.id}
34
+ className="bg-card border-border flex items-center justify-between rounded-lg border px-4 py-3"
35
+ >
36
+ <span className="text-foreground text-sm font-medium">{item.name}</span>
37
+ <span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs">
38
+ {item.status}
39
+ </span>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,50 @@
1
+ import { ContractPage } from "@middag-io/react";
2
+ import { usePage } from "@inertiajs/react";
3
+ import type { PageContract, ContractPageProps } from "@middag-io/react";
4
+
5
+ // Direct pages — eager loaded (custom React pages in pages/)
6
+ const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Record<
7
+ string,
8
+ Record<string, unknown>
9
+ >;
10
+
11
+ /**
12
+ * Fallback component — reads PageContract from Inertia props and renders
13
+ * it via the lib's ContractPage. Used when no direct page matches.
14
+ */
15
+ function InertiaContractPage() {
16
+ const { props } = usePage<{
17
+ contract: PageContract;
18
+ help?: ContractPageProps["help"];
19
+ inspector?: ContractPageProps["inspector"];
20
+ }>();
21
+ return <ContractPage contract={props.contract} help={props.help} inspector={props.inspector} />;
22
+ }
23
+
24
+ const contractPageModule = { default: InertiaContractPage };
25
+
26
+ /**
27
+ * Resolve an Inertia page name to a React component module.
28
+ *
29
+ * Resolution order:
30
+ * 1. Direct page — if pages/{name}.tsx exists, use it
31
+ * 2. ContractPage — fallback, reads PageContract JSON from Inertia props
32
+ *
33
+ * This means:
34
+ * - "Dashboard" with pages/Dashboard.tsx → direct React page
35
+ * - "Dashboard" without matching .tsx → ContractPage from props.contract
36
+ * - "Entitlements/Show" with pages/Entitlements/Show.tsx → direct page
37
+ *
38
+ * Direct pages can also use ContractPage internally (hybrid pattern).
39
+ */
40
+ export function resolvePageComponent(name: string): Record<string, unknown> {
41
+ // Direct page: matching .tsx file in pages/
42
+ const path = `../pages/${name}.tsx`;
43
+ const page = directPages[path];
44
+ if (page) {
45
+ return page;
46
+ }
47
+
48
+ // Default: ContractPage renders from Inertia props.contract
49
+ return contractPageModule;
50
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * register — selective registration for this plugin's UI.
3
+ *
4
+ * Registers only the shells, layouts, and blocks this plugin uses.
5
+ * For IIFE bundles (WordPress/Moodle), selective registration avoids
6
+ * pulling in heavy lazy-loaded blocks that bloat the bundle.
7
+ *
8
+ * When adding a new page that needs a block not listed here,
9
+ * add the import + registerBlock call.
10
+ *
11
+ * Full catalog: https://docs.middag.io/blocks
12
+ */
13
+
14
+ import {
15
+ registerShell,
16
+ registerLayout,
17
+ registerBlock,
18
+ // Shells
19
+ HostProductShell,
20
+ // Layouts
21
+ StackLayout,
22
+ SplitLayout,
23
+ DashboardLayout,
24
+ // Blocks
25
+ DenseTableBlock,
26
+ MetricCardBlock,
27
+ EmptyStateBlock,
28
+ DetailPanelBlock,
29
+ FormPanelBlock,
30
+ } from "@middag-io/react";
31
+
32
+ let registered = false;
33
+
34
+ export function registerDefaults(): void {
35
+ if (registered) return;
36
+ registered = true;
37
+
38
+ // Shells
39
+ registerShell("product", HostProductShell);
40
+
41
+ // Layouts
42
+ registerLayout("stack", StackLayout);
43
+ registerLayout("split", SplitLayout);
44
+ registerLayout("dashboard", DashboardLayout);
45
+
46
+ // Blocks — add more as your pages need them
47
+ registerBlock("dense_table", DenseTableBlock);
48
+ registerBlock("metric_card", MetricCardBlock);
49
+ registerBlock("empty_state", EmptyStateBlock);
50
+ registerBlock("detail_panel", DetailPanelBlock);
51
+ registerBlock("form_panel", FormPanelBlock);
52
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Route helper — generates correct URLs for WordPress production vs dev mock.
3
+ *
4
+ * In production (Inertia): /wp-admin/admin.php?page=middag-{slug}
5
+ * In dev mock (BrowserRouter): /{slug} directly
6
+ *
7
+ * Detection: if window.__MIDDAG_MOCK_NAVIGATE__ exists, we're in dev mock.
8
+ */
9
+
10
+ /**
11
+ * Build a URL for a MIDDAG admin page.
12
+ *
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
+ * @param slug - Domain slug (e.g. "entitlements", "organizations")
17
+ * @param path - Optional sub-path (e.g. "/entitlements/1/edit")
18
+ */
19
+ export function route(slug: string, path?: string): string {
20
+ if (typeof window !== "undefined" && "__MIDDAG_MOCK_NAVIGATE__" in window) {
21
+ return path ?? `/${slug}`;
22
+ }
23
+
24
+ const base = `/wp-admin/admin.php?page=middag-${slug}`;
25
+ return path ? `${base}&route=${path}` : base;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-middag-ui",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "Bootstrap a MIDDAG React UI layer in your Moodle or WordPress plugin",
6
6
  "bin": {