create-middag-ui 0.8.0 → 0.10.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
@@ -31,7 +31,10 @@ import {
31
31
  scaffoldIndexHtml,
32
32
  scaffoldDemoFiles,
33
33
  scaffoldPageExamples,
34
- scaffoldAppFiles,
34
+ scaffoldProApp,
35
+ scaffoldFreeApp,
36
+ scaffoldFreeAdapters,
37
+ scaffoldDevShell,
35
38
  } from "./lib/scaffold.js";
36
39
  import { runNpmInstall } from "./lib/install.js";
37
40
  import { log, success, heading, blank, info } from "./lib/ui.js";
@@ -111,9 +114,9 @@ if (!dirCreated) {
111
114
 
112
115
  heading(5, TOTAL_STEPS, "Scaffolding config files");
113
116
 
114
- scaffoldPackageJson(targetDir, host, cwd);
117
+ scaffoldPackageJson(targetDir, host, cwd, registryPath);
115
118
  scaffoldTsconfig(targetDir);
116
- scaffoldViteConfig(targetDir, host);
119
+ scaffoldViteConfig(targetDir, host, registryPath);
117
120
  scaffoldIndexHtml(targetDir);
118
121
 
119
122
  // ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
@@ -133,13 +136,23 @@ heading(7, TOTAL_STEPS, "Creating demo files");
133
136
 
134
137
  scaffoldDemoFiles(targetDir);
135
138
 
136
- // ── Step 8: Scaffold app files + page examples ─────────────────────────
139
+ // ── Step 8: Scaffold app + page examples (PRO vs FREE) ─────────────────
137
140
 
138
- heading(8, TOTAL_STEPS, "Creating app and page examples");
141
+ const isPro = registryPath === "github";
142
+ heading(8, TOTAL_STEPS, `Creating ${isPro ? "PRO" : "FREE"} UI module`);
139
143
 
140
- scaffoldAppFiles(targetDir);
141
144
  scaffoldPageExamples(targetDir);
142
145
 
146
+ if (isPro) {
147
+ scaffoldProApp(targetDir);
148
+ success("PRO: using MockProductShell + HostAdapter from @middag-io/react/mock");
149
+ } else {
150
+ scaffoldFreeAdapters(targetDir);
151
+ scaffoldDevShell(targetDir);
152
+ scaffoldFreeApp(targetDir);
153
+ success("FREE: generated DevShell + local Inertia adapters");
154
+ }
155
+
143
156
  // ── Step 9: npm install ──────────────────────────────────────────────────
144
157
 
145
158
  heading(9, TOTAL_STEPS, "Installing dependencies");
package/lib/detect.js CHANGED
@@ -8,9 +8,9 @@ import { existsSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
 
10
10
  export const HOSTS = {
11
- moodle: { name: "Moodle", detect: "version.php", port: 5174 },
12
- wordpress: { name: "WordPress", detect: "wp-config.php", port: 5175 },
13
- custom: { name: "Custom", detect: null, port: 5176 },
11
+ moodle: { name: "Moodle", detect: "version.php", port: 5174, headerHeight: 50 },
12
+ wordpress: { name: "WordPress", detect: "wp-config.php", port: 5175, headerHeight: 32 },
13
+ custom: { name: "Custom", detect: null, port: 5176, headerHeight: 0 },
14
14
  };
15
15
 
16
16
  /**
package/lib/scaffold.js CHANGED
@@ -75,11 +75,24 @@ export function createTargetDir(targetDir) {
75
75
  /**
76
76
  * Scaffold package.json.
77
77
  */
78
- export function scaffoldPackageJson(targetDir, host, cwd) {
78
+ /**
79
+ * @param {string} registryPath - "github" (PRO) or "public" (FREE)
80
+ */
81
+ export function scaffoldPackageJson(targetDir, host, cwd, registryPath) {
79
82
  const filePath = join(targetDir, "package.json");
80
83
  if (skipIfExists(filePath, "package.json")) return;
81
84
 
85
+ const isPro = registryPath === "github";
82
86
  const projectName = basename(cwd) || "project";
87
+ const deps = {
88
+ "@middag-io/react": `^${getLibVersion()}`,
89
+ "@fontsource-variable/figtree": "^5.0.0",
90
+ "react-router": "^7.0.0",
91
+ };
92
+ if (isPro) {
93
+ deps["sonner"] = "^2.0.0";
94
+ }
95
+
83
96
  const pkg = {
84
97
  name: `${projectName}-ui`,
85
98
  private: true,
@@ -91,10 +104,7 @@ export function scaffoldPackageJson(targetDir, host, cwd) {
91
104
  lint: "eslint .",
92
105
  "lint:fix": "eslint . --fix",
93
106
  },
94
- dependencies: {
95
- "@middag-io/react": `^${getLibVersion()}`,
96
- "@fontsource-variable/figtree": "^5.0.0",
97
- },
107
+ dependencies: deps,
98
108
  devDependencies: {
99
109
  "@types/react": "^19.0.0",
100
110
  "@types/react-dom": "^19.0.0",
@@ -140,17 +150,30 @@ export function scaffoldTsconfig(targetDir) {
140
150
  /**
141
151
  * Scaffold vite.config.ts.
142
152
  */
143
- export function scaffoldViteConfig(targetDir, host) {
153
+ /**
154
+ * @param {string} registryPath - "github" (PRO) or "public" (FREE)
155
+ */
156
+ export function scaffoldViteConfig(targetDir, host, registryPath) {
144
157
  const filePath = join(targetDir, "vite.config.ts");
145
158
  if (skipIfExists(filePath, "vite.config.ts")) return;
146
159
 
160
+ const isPro = registryPath === "github";
161
+ const adapterComment = isPro
162
+ ? "// PRO: Inertia mocks from @middag-io/react/mock"
163
+ : "// FREE: Inertia mocks from local adapters";
164
+ const adapterReact = isPro
165
+ ? 'resolve(__dirname, "node_modules/@middag-io/react/mock/adapters/inertia-react.ts")'
166
+ : 'resolve(__dirname, "src/adapters/inertia-react.ts")';
167
+ const adapterCore = isPro
168
+ ? 'resolve(__dirname, "node_modules/@middag-io/react/mock/adapters/inertia-core.ts")'
169
+ : 'resolve(__dirname, "src/adapters/inertia-core.ts")';
170
+
147
171
  const content = `/**
148
172
  * Vite config \u2014 used by \`npm run dev\` and \`npm run build\`.
149
173
  *
150
- * The Inertia aliases below redirect @inertiajs/* imports to local
151
- * mock adapters so the dev server works standalone (no Moodle/WP).
152
- * In production, the real @inertiajs packages handle routing and
153
- * page resolution \u2014 these aliases have no effect.
174
+ * Inertia aliases redirect @inertiajs/* to mock adapters so the
175
+ * dev server works standalone. In production, the real @inertiajs
176
+ * packages handle routing and page resolution.
154
177
  */
155
178
  import { defineConfig } from "vite";
156
179
  import react from "@vitejs/plugin-react";
@@ -161,11 +184,10 @@ export default defineConfig({
161
184
  server: { port: ${host.port} },
162
185
  resolve: {
163
186
  alias: {
164
- // Path alias \u2014 import from "@/components/..." resolves to src/
165
187
  "@/": resolve(__dirname, "src") + "/",
166
- // Mock Inertia for standalone dev \u2014 see src/adapters/ for implementation
167
- "@inertiajs/react": resolve(__dirname, "src/adapters/inertia-react.ts"),
168
- "@inertiajs/core": resolve(__dirname, "src/adapters/inertia-core.ts"),
188
+ ${adapterComment}
189
+ "@inertiajs/react": ${adapterReact},
190
+ "@inertiajs/core": ${adapterCore},
169
191
  },
170
192
  },
171
193
  });
@@ -244,15 +266,15 @@ export interface HelloBlockData {
244
266
  name: string;
245
267
  }
246
268
 
247
- /** Custom block component. Receives data from the PageContract. */
248
- export function HelloBlock({ data }: BlockProps<HelloBlockData>) {
269
+ /** Custom block component. Receives block descriptor from the PageContract. */
270
+ export function HelloBlock({ block }: BlockProps<HelloBlockData>) {
249
271
  return (
250
272
  <div className="rounded-lg border bg-card p-6 text-card-foreground">
251
273
  <h2 className="text-lg font-semibold text-foreground">
252
- {data.greeting}
274
+ {block.data.greeting}
253
275
  </h2>
254
276
  <p className="mt-2 text-muted-foreground">
255
- Welcome, {data.name}! This is a custom block.
277
+ Welcome, {block.data.name}! This is a custom block.
256
278
  </p>
257
279
  <p className="mt-4 text-sm text-muted-foreground">
258
280
  Edit this file at <code className="text-xs bg-muted px-1 py-0.5 rounded">src/blocks/hello-block.tsx</code>
@@ -773,11 +795,356 @@ export const settingsContract: PageContract = {
773
795
  }
774
796
  }
775
797
 
776
- // ── App files (entry point + router + adapters) ─────────────────────────
798
+ // ── App files PRO path (GitHub Packages) ─────────────────────────────
799
+
800
+ /**
801
+ * Scaffold PRO app: src/main.tsx + src/app.tsx.
802
+ * Uses mock barrel from @middag-io/react/mock. No local adapters/shell.
803
+ */
804
+ export function scaffoldProApp(targetDir) {
805
+ ensureDir(join(targetDir, "src"));
806
+
807
+ const mainPath = join(targetDir, "src", "main.tsx");
808
+ if (!skipIfExists(mainPath, "src/main.tsx")) {
809
+ writeFile(mainPath, `import { StrictMode } from "react";
810
+ import { createRoot } from "react-dom/client";
811
+ import { registerDefaults, registerShell } from "@middag-io/react";
812
+ import { MockProductShell } from "@middag-io/react/mock";
813
+ import "@middag-io/react/style.css";
814
+ import "./theme.css";
815
+ import "@fontsource-variable/figtree";
816
+ import { App } from "./app";
817
+
818
+ registerDefaults();
819
+ registerShell("product", MockProductShell);
820
+
821
+ createRoot(document.getElementById("root")!).render(
822
+ <StrictMode><App /></StrictMode>,
823
+ );
824
+ `, "src/main.tsx");
825
+ }
826
+
827
+ const appPath = join(targetDir, "src", "app.tsx");
828
+ if (!skipIfExists(appPath, "src/app.tsx")) {
829
+ writeFile(appPath, `import { useEffect } from "react";
830
+ import { BrowserRouter, Routes, Route, useNavigate } from "react-router";
831
+ import { ContractPage, I18nProvider } from "@middag-io/react";
832
+ import { HostAdapter, MockPageProvider, MockI18nProvider } from "@middag-io/react/mock";
833
+ import type { PageContract } from "@middag-io/react";
834
+ import { dashboardContract } from "./pages/dashboard";
835
+ import { connectorsContract } from "./pages/connectors";
836
+ import { settingsContract } from "./pages/settings";
837
+
838
+ let _navigate: ((to: string) => void) | null = null;
839
+ function NavigateBridge() {
840
+ const navigate = useNavigate();
841
+ useEffect(() => { _navigate = (to: string) => navigate(to); }, [navigate]);
842
+ return null;
843
+ }
844
+ if (typeof window !== "undefined") {
845
+ (window as any).__MIDDAG_MOCK_NAVIGATE__ = (to: string) => { if (_navigate) _navigate(to); };
846
+ }
847
+
848
+ const sharedProps = {
849
+ auth: { id: 1, name: "Dev User", email: "dev@localhost", capabilities: [] },
850
+ theme: { appearance: "light" as const, strings: {} as Record<string, string> },
851
+ flash: {},
852
+ locale: "en",
853
+ version: "0.0.0-dev",
854
+ scope: { extension: null, context: "global" },
855
+ };
856
+
857
+ function buildNavigation(activeKey: string) {
858
+ return {
859
+ sections: [
860
+ { key: "overview", label: "Overview", icon: "home", group: "main" as const, items: [{ key: "overview.dashboard", label: "Dashboard", href: "/", active: activeKey === "overview.dashboard", children: [] }] },
861
+ { key: "integration", label: "Integration", icon: "plug", group: "main" as const, items: [{ key: "integration.connectors", label: "Connectors", href: "/connectors", active: activeKey === "integration.connectors", children: [] }] },
862
+ { key: "system", label: "System", icon: "settings", group: "system" as const, items: [{ key: "system.settings", label: "Settings", href: "/settings", active: activeKey === "system.settings", children: [] }] },
863
+ ],
864
+ activeKey,
865
+ };
866
+ }
867
+
868
+ function MockRoute({ contract, activeKey }: { contract: PageContract; activeKey: string }) {
869
+ return (
870
+ <MockPageProvider value={{ props: { ...sharedProps, contract, navigation: buildNavigation(activeKey) }, url: window.location.pathname }}>
871
+ <ContractPage contract={contract} />
872
+ </MockPageProvider>
873
+ );
874
+ }
875
+
876
+ export function App() {
877
+ return (
878
+ <MockI18nProvider>
879
+ <MockPageProvider value={{ props: { ...sharedProps, contract: null, navigation: buildNavigation("") }, url: "/" }}>
880
+ <I18nProvider>
881
+ <BrowserRouter>
882
+ <NavigateBridge />
883
+ <HostAdapter>
884
+ <Routes>
885
+ <Route path="/" element={<MockRoute contract={dashboardContract} activeKey="overview.dashboard" />} />
886
+ <Route path="/connectors" element={<MockRoute contract={connectorsContract} activeKey="integration.connectors" />} />
887
+ <Route path="/settings" element={<MockRoute contract={settingsContract} activeKey="system.settings" />} />
888
+ </Routes>
889
+ </HostAdapter>
890
+ </BrowserRouter>
891
+ </I18nProvider>
892
+ </MockPageProvider>
893
+ </MockI18nProvider>
894
+ );
895
+ }
896
+ `, "src/app.tsx");
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Scaffold FREE adapters: src/adapters/ with react-router.
902
+ */
903
+ export function scaffoldFreeAdapters(targetDir) {
904
+ ensureDir(join(targetDir, "src", "adapters"));
905
+
906
+ const corePath = join(targetDir, "src", "adapters", "inertia-core.ts");
907
+ if (!skipIfExists(corePath, "src/adapters/inertia-core.ts")) {
908
+ writeFile(corePath, `/**
909
+ * Mock @inertiajs/core for standalone dev server.
910
+ * Vite alias redirects @inertiajs/core here.
911
+ */
912
+ let _navigate: ((to: string) => void) | null = null;
913
+
914
+ export function setMockNavigate(fn: (to: string) => void) { _navigate = fn; }
915
+
916
+ export const router = {
917
+ get: (url: string) => { _navigate ? _navigate(url) : console.log("[mock] GET", url); },
918
+ post: (url: string) => { console.log("[mock] POST", url); },
919
+ put: (url: string) => { console.log("[mock] PUT", url); },
920
+ patch: (url: string) => { console.log("[mock] PATCH", url); },
921
+ delete: (url: string) => { console.log("[mock] DELETE", url); },
922
+ reload: () => { window.location.reload(); },
923
+ visit: (url: string) => { _navigate ? _navigate(url) : console.log("[mock] VISIT", url); },
924
+ on: () => () => {},
925
+ };
926
+ `, "src/adapters/inertia-core.ts");
927
+ }
928
+
929
+ const reactPath = join(targetDir, "src", "adapters", "inertia-react.ts");
930
+ if (!skipIfExists(reactPath, "src/adapters/inertia-react.ts")) {
931
+ writeFile(reactPath, `/**
932
+ * Mock @inertiajs/react for standalone dev server (FREE).
933
+ * Context-based usePage + react-router Link.
934
+ */
935
+ import React from "react";
936
+ import { useNavigate } from "react-router";
937
+ import { router } from "./inertia-core";
938
+
939
+ const PageContext = React.createContext<{ props: Record<string, unknown>; url: string }>({ props: {}, url: "/" });
940
+
941
+ export function PageProvider({ value, children }: {
942
+ value: { props: Record<string, unknown>; url: string };
943
+ children: React.ReactNode;
944
+ }) {
945
+ return React.createElement(PageContext.Provider, { value }, children);
946
+ }
947
+
948
+ export function usePage<T = Record<string, unknown>>(): { props: T; url: string } {
949
+ return React.useContext(PageContext) as { props: T; url: string };
950
+ }
951
+
952
+ export function Head({ title, children }: { title?: string; children?: React.ReactNode }) {
953
+ React.useEffect(() => { if (title) document.title = title; }, [title]);
954
+ return children ? React.createElement("span", { style: { display: "none" } }, children) : null;
955
+ }
956
+
957
+ interface MockLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
958
+ href?: string; method?: string; preserveScroll?: boolean; preserveState?: boolean; as?: string;
959
+ }
960
+
961
+ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLink(
962
+ { href, onClick, children, as: _as, method: _m, preserveScroll: _ps, preserveState: _pst, ...rest }, ref,
963
+ ) {
964
+ const navigate = useNavigate();
965
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
966
+ if (onClick) onClick(e);
967
+ if (e.defaultPrevented) return;
968
+ e.preventDefault();
969
+ if (href) navigate(href);
970
+ };
971
+ return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
972
+ });
973
+
974
+ export { router };
975
+ `, "src/adapters/inertia-react.ts");
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Scaffold FREE DevShell: src/shells/DevShell.tsx.
981
+ */
982
+ export function scaffoldDevShell(targetDir) {
983
+ ensureDir(join(targetDir, "src", "shells"));
984
+
985
+ const shellPath = join(targetDir, "src", "shells", "DevShell.tsx");
986
+ if (skipIfExists(shellPath, "src/shells/DevShell.tsx")) return;
987
+
988
+ writeFile(shellPath, `/**
989
+ * DevShell \u2014 custom shell for the standalone dev server (FREE).
990
+ * Offcanvas sidebar (fully hidden when collapsed) + footer controls.
991
+ * Registered as "product" shell override in main.tsx.
992
+ */
993
+ import { useCallback, type ReactElement } from "react";
994
+ import { usePage } from "@inertiajs/react";
995
+ import {
996
+ Sidebar, SidebarFooter, SidebarHeader, SidebarProvider,
997
+ SidebarNav, PageHeader, NavErrorBoundary, AppearanceToggle, useSidebar,
998
+ } from "@middag-io/react";
999
+ import type { ShellProps, SharedProps, PageMeta } from "@middag-io/react";
1000
+
1001
+ function DevShellInner({ children }: ShellProps): ReactElement {
1002
+ const { toggleSidebar, open } = useSidebar();
1003
+ const { props } = usePage<SharedProps>();
1004
+ const page: PageMeta = (props as SharedProps & { contract?: { page?: PageMeta } }).contract?.page ?? { key: "unknown", title: "", breadcrumbs: [], actions: [] };
1005
+ const handleMenuToggle = useCallback(() => { toggleSidebar(); }, [toggleSidebar]);
1006
+
1007
+ return (
1008
+ <>
1009
+ <Sidebar aria-label="Navigation" collapsible="offcanvas" className="border-sidebar-border bg-sidebar border-r">
1010
+ <SidebarHeader className="border-sidebar-border border-b px-4 py-3">
1011
+ <p className="text-sidebar-foreground text-sm font-semibold">MIDDAG</p>
1012
+ </SidebarHeader>
1013
+ <NavErrorBoundary><SidebarNav /></NavErrorBoundary>
1014
+ <SidebarFooter className="border-sidebar-border mt-auto border-t px-3 py-2">
1015
+ <div className="flex items-center justify-between">
1016
+ <button onClick={handleMenuToggle} className="text-muted-foreground hover:bg-sidebar-hover hover:text-sidebar-foreground flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1.5 text-xs transition-colors" type="button">
1017
+ <span className="text-base leading-none">&laquo;</span><span>Collapse</span>
1018
+ </button>
1019
+ <AppearanceToggle />
1020
+ </div>
1021
+ </SidebarFooter>
1022
+ </Sidebar>
1023
+
1024
+ <div className="flex flex-1 flex-col overflow-hidden">
1025
+ <NavErrorBoundary><PageHeader page={page} onMobileMenuClick={handleMenuToggle} /></NavErrorBoundary>
1026
+ <div className="flex min-h-0 flex-1">
1027
+ <main className="min-w-0 flex-1 overflow-auto" aria-live="polite" aria-busy="false">{children}</main>
1028
+ </div>
1029
+ </div>
1030
+
1031
+ {!open && (
1032
+ <div className="fixed bottom-3 left-3 z-30 flex items-center gap-1.5">
1033
+ <button onClick={handleMenuToggle} className="bg-sidebar border-sidebar-border text-muted-foreground hover:text-sidebar-foreground hover:bg-sidebar-hover flex h-9 w-9 items-center justify-center rounded-lg border shadow-md transition-colors" aria-label="Open navigation" type="button">
1034
+ <span className="text-base leading-none">&raquo;</span>
1035
+ </button>
1036
+ <div className="bg-sidebar border-sidebar-border rounded-lg border shadow-md"><AppearanceToggle /></div>
1037
+ </div>
1038
+ )}
1039
+ </>
1040
+ );
1041
+ }
1042
+
1043
+ export function DevShell({ children }: ShellProps): ReactElement {
1044
+ return (
1045
+ <SidebarProvider defaultOpen={true} style={{ "--sidebar-width": "260px" } as React.CSSProperties} className="bg-background text-foreground flex min-h-0 flex-1">
1046
+ <DevShellInner>{children}</DevShellInner>
1047
+ </SidebarProvider>
1048
+ );
1049
+ }
1050
+ `, "src/shells/DevShell.tsx");
1051
+ }
777
1052
 
778
1053
  /**
779
- * Scaffold src/main.tsx, src/app.tsx, and src/adapters/.
1054
+ * Scaffold FREE app: src/main.tsx + src/app.tsx.
780
1055
  */
1056
+ export function scaffoldFreeApp(targetDir) {
1057
+ ensureDir(join(targetDir, "src"));
1058
+
1059
+ const mainPath = join(targetDir, "src", "main.tsx");
1060
+ if (!skipIfExists(mainPath, "src/main.tsx")) {
1061
+ writeFile(mainPath, `import { StrictMode } from "react";
1062
+ import { createRoot } from "react-dom/client";
1063
+ import { registerDefaults, registerShell } from "@middag-io/react";
1064
+ import "@middag-io/react/style.css";
1065
+ import "./theme.css";
1066
+ import "@fontsource-variable/figtree";
1067
+ import { DevShell } from "./shells/DevShell";
1068
+ import { App } from "./app";
1069
+
1070
+ registerDefaults();
1071
+ registerShell("product", DevShell);
1072
+
1073
+ createRoot(document.getElementById("root")!).render(
1074
+ <StrictMode><App /></StrictMode>,
1075
+ );
1076
+ `, "src/main.tsx");
1077
+ }
1078
+
1079
+ const appPath = join(targetDir, "src", "app.tsx");
1080
+ if (!skipIfExists(appPath, "src/app.tsx")) {
1081
+ writeFile(appPath, `import { useEffect } from "react";
1082
+ import { BrowserRouter, Routes, Route, useNavigate } from "react-router";
1083
+ import { ContractPage, I18nProvider } from "@middag-io/react";
1084
+ import type { PageContract } from "@middag-io/react";
1085
+ import { PageProvider } from "./adapters/inertia-react";
1086
+ import { setMockNavigate } from "./adapters/inertia-core";
1087
+ import { dashboardContract } from "./pages/dashboard";
1088
+ import { connectorsContract } from "./pages/connectors";
1089
+ import { settingsContract } from "./pages/settings";
1090
+
1091
+ function NavigateBridge() {
1092
+ const navigate = useNavigate();
1093
+ useEffect(() => { setMockNavigate((to: string) => navigate(to)); }, [navigate]);
1094
+ return null;
1095
+ }
1096
+
1097
+ const sharedProps = {
1098
+ auth: { id: 1, name: "Dev User", email: "dev@localhost", capabilities: [] },
1099
+ theme: { appearance: "light" as const, strings: {} as Record<string, string> },
1100
+ flash: {},
1101
+ locale: "en",
1102
+ version: "0.0.0-dev",
1103
+ scope: { extension: null, context: "global" },
1104
+ };
1105
+
1106
+ function buildNavigation(activeKey: string) {
1107
+ return {
1108
+ sections: [
1109
+ { key: "overview", label: "Overview", icon: "home", group: "main" as const, items: [{ key: "overview.dashboard", label: "Dashboard", href: "/", active: activeKey === "overview.dashboard", children: [] }] },
1110
+ { key: "integration", label: "Integration", icon: "plug", group: "main" as const, items: [{ key: "integration.connectors", label: "Connectors", href: "/connectors", active: activeKey === "integration.connectors", children: [] }] },
1111
+ { key: "system", label: "System", icon: "settings", group: "system" as const, items: [{ key: "system.settings", label: "Settings", href: "/settings", active: activeKey === "system.settings", children: [] }] },
1112
+ ],
1113
+ activeKey,
1114
+ };
1115
+ }
1116
+
1117
+ function MockRoute({ contract, activeKey }: { contract: PageContract; activeKey: string }) {
1118
+ return (
1119
+ <PageProvider value={{ props: { ...sharedProps, contract, navigation: buildNavigation(activeKey) }, url: window.location.pathname }}>
1120
+ <ContractPage contract={contract} />
1121
+ </PageProvider>
1122
+ );
1123
+ }
1124
+
1125
+ export function App() {
1126
+ return (
1127
+ <PageProvider value={{ props: { ...sharedProps, contract: null, navigation: buildNavigation("") }, url: "/" }}>
1128
+ <I18nProvider>
1129
+ <BrowserRouter>
1130
+ <NavigateBridge />
1131
+ <Routes>
1132
+ <Route path="/" element={<MockRoute contract={dashboardContract} activeKey="overview.dashboard" />} />
1133
+ <Route path="/connectors" element={<MockRoute contract={connectorsContract} activeKey="integration.connectors" />} />
1134
+ <Route path="/settings" element={<MockRoute contract={settingsContract} activeKey="system.settings" />} />
1135
+ </Routes>
1136
+ </BrowserRouter>
1137
+ </I18nProvider>
1138
+ </PageProvider>
1139
+ );
1140
+ }
1141
+ `, "src/app.tsx");
1142
+ }
1143
+ }
1144
+
1145
+ // ── LEGACY (kept for backward compat, delegates to FREE) ────────────────
1146
+
1147
+ /** @deprecated Use scaffoldFreeApp + scaffoldFreeAdapters instead */
781
1148
  export function scaffoldAppFiles(targetDir) {
782
1149
  ensureDir(join(targetDir, "src"));
783
1150
  ensureDir(join(targetDir, "src", "adapters"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-middag-ui",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Bootstrap a MIDDAG React UI layer in your Moodle or WordPress plugin",
6
6
  "bin": {