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 +19 -6
- package/lib/detect.js +3 -3
- package/lib/scaffold.js +387 -20
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -31,7 +31,10 @@ import {
|
|
|
31
31
|
scaffoldIndexHtml,
|
|
32
32
|
scaffoldDemoFiles,
|
|
33
33
|
scaffoldPageExamples,
|
|
34
|
-
|
|
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
|
|
139
|
+
// ── Step 8: Scaffold app + page examples (PRO vs FREE) ─────────────────
|
|
137
140
|
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
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
|
-
|
|
167
|
-
"@inertiajs/react":
|
|
168
|
-
"@inertiajs/core":
|
|
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
|
|
248
|
-
export function HelloBlock({
|
|
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
|
|
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">«</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">»</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
|
|
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"));
|