create-middag-ui 0.4.0 → 0.4.2
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 +14 -9
- package/lib/scaffold.js +644 -106
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
scaffoldPackageJson,
|
|
29
29
|
scaffoldTsconfig,
|
|
30
30
|
scaffoldViteConfig,
|
|
31
|
+
scaffoldIndexHtml,
|
|
31
32
|
scaffoldDemoFiles,
|
|
32
|
-
|
|
33
|
+
scaffoldPageExamples,
|
|
34
|
+
scaffoldAppFiles,
|
|
33
35
|
} from "./lib/scaffold.js";
|
|
34
36
|
import { runNpmInstall } from "./lib/install.js";
|
|
35
37
|
import { log, success, heading, blank, info } from "./lib/ui.js";
|
|
@@ -112,6 +114,7 @@ heading(5, TOTAL_STEPS, "Scaffolding config files");
|
|
|
112
114
|
scaffoldPackageJson(targetDir, host, cwd);
|
|
113
115
|
scaffoldTsconfig(targetDir);
|
|
114
116
|
scaffoldViteConfig(targetDir, host);
|
|
117
|
+
scaffoldIndexHtml(targetDir);
|
|
115
118
|
|
|
116
119
|
// ── Step 6: Scaffold ~/.npmrc (GitHub path only) ─────────────────────────
|
|
117
120
|
|
|
@@ -130,11 +133,12 @@ heading(7, TOTAL_STEPS, "Creating demo files");
|
|
|
130
133
|
|
|
131
134
|
scaffoldDemoFiles(targetDir);
|
|
132
135
|
|
|
133
|
-
// ── Step 8: Scaffold
|
|
136
|
+
// ── Step 8: Scaffold app files + page examples ─────────────────────────
|
|
134
137
|
|
|
135
|
-
heading(8, TOTAL_STEPS, "Creating
|
|
138
|
+
heading(8, TOTAL_STEPS, "Creating app and page examples");
|
|
136
139
|
|
|
137
|
-
|
|
140
|
+
scaffoldAppFiles(targetDir);
|
|
141
|
+
scaffoldPageExamples(targetDir);
|
|
138
142
|
|
|
139
143
|
// ── Step 9: npm install ──────────────────────────────────────────────────
|
|
140
144
|
|
|
@@ -156,21 +160,22 @@ if (installOk) {
|
|
|
156
160
|
log(`MIDDAG React UI ready in ${dirName}/ (${elapsed}s)\n`);
|
|
157
161
|
console.log(" Start developing:");
|
|
158
162
|
console.log(` cd ${dirName}`);
|
|
159
|
-
console.log(` npm run dev
|
|
163
|
+
console.log(` npm run dev \u2192 dev server at http://localhost:${host.port}`);
|
|
160
164
|
} else {
|
|
161
165
|
log(`Scaffold complete in ${dirName}/ (${elapsed}s) \u2014 install failed\n`);
|
|
162
166
|
console.log(" To retry install:");
|
|
163
167
|
console.log(` cd ${dirName}`);
|
|
164
168
|
console.log(" npm install");
|
|
165
|
-
console.log(` npm run dev
|
|
169
|
+
console.log(` npm run dev \u2192 dev server at http://localhost:${host.port}`);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
blank();
|
|
169
173
|
console.log(" Your scaffold includes:");
|
|
174
|
+
console.log(" src/pages/dashboard.ts \u2190 starter: metric_card + dense_table");
|
|
175
|
+
console.log(" src/pages/connectors.ts \u2190 intermediate: card_grid + status_strip");
|
|
176
|
+
console.log(" src/pages/settings.ts \u2190 advanced: tabbed_panel + form_panel");
|
|
170
177
|
console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
|
|
171
|
-
console.log(" src/
|
|
172
|
-
console.log(" src/contracts.ts \u2190 PageContract type re-export");
|
|
173
|
-
console.log(" mock/hello-contract.ts \u2190 example PageContract with data");
|
|
178
|
+
console.log(" src/app.tsx \u2190 hash-based page router");
|
|
174
179
|
|
|
175
180
|
blank();
|
|
176
181
|
console.log(` Integrate with your ${host.name} plugin:`);
|
package/lib/scaffold.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* scaffold.js — File creation for all scaffolded files.
|
|
3
3
|
*
|
|
4
|
-
* Creates directory structure, config files,
|
|
4
|
+
* Creates directory structure, config files, page examples, app entry,
|
|
5
|
+
* and Inertia mock adapters. Everything lives under src/.
|
|
6
|
+
*
|
|
5
7
|
* Every I/O operation is wrapped with error handling.
|
|
6
8
|
*/
|
|
7
9
|
|
|
@@ -83,16 +85,15 @@ export function scaffoldPackageJson(targetDir, host, cwd) {
|
|
|
83
85
|
private: true,
|
|
84
86
|
type: "module",
|
|
85
87
|
scripts: {
|
|
86
|
-
dev: "vite
|
|
87
|
-
"dev:mock": "vite --config vite.mock.config.ts",
|
|
88
|
+
dev: "vite",
|
|
88
89
|
build: "vite build",
|
|
89
|
-
"build:mock": "vite build --config vite.mock.config.ts",
|
|
90
90
|
typecheck: "tsc --noEmit",
|
|
91
91
|
lint: "eslint .",
|
|
92
92
|
"lint:fix": "eslint . --fix",
|
|
93
93
|
},
|
|
94
94
|
dependencies: {
|
|
95
95
|
"@middag-io/react": `^${getLibVersion()}`,
|
|
96
|
+
"@fontsource-variable/figtree": "^5.0.0",
|
|
96
97
|
},
|
|
97
98
|
devDependencies: {
|
|
98
99
|
"@types/react": "^19.0.0",
|
|
@@ -104,8 +105,6 @@ export function scaffoldPackageJson(targetDir, host, cwd) {
|
|
|
104
105
|
typescript: "^5.7.0",
|
|
105
106
|
vite: "^6.0.0",
|
|
106
107
|
"@vitejs/plugin-react": "^4.0.0",
|
|
107
|
-
tailwindcss: "^4.0.0",
|
|
108
|
-
"@tailwindcss/vite": "^4.0.0",
|
|
109
108
|
},
|
|
110
109
|
};
|
|
111
110
|
|
|
@@ -129,43 +128,89 @@ export function scaffoldTsconfig(targetDir) {
|
|
|
129
128
|
noUnusedLocals: true,
|
|
130
129
|
noUnusedParameters: true,
|
|
131
130
|
skipLibCheck: true,
|
|
132
|
-
paths: { "@/*": ["./src/*"]
|
|
131
|
+
paths: { "@/*": ["./src/*"] },
|
|
133
132
|
baseUrl: ".",
|
|
134
133
|
},
|
|
135
|
-
include: ["src"
|
|
134
|
+
include: ["src"],
|
|
136
135
|
};
|
|
137
136
|
|
|
138
137
|
writeFile(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "tsconfig.json");
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
/**
|
|
142
|
-
* Scaffold vite.
|
|
141
|
+
* Scaffold vite.config.ts.
|
|
143
142
|
*/
|
|
144
143
|
export function scaffoldViteConfig(targetDir, host) {
|
|
145
|
-
const filePath = join(targetDir, "vite.
|
|
146
|
-
if (skipIfExists(filePath, "vite.
|
|
144
|
+
const filePath = join(targetDir, "vite.config.ts");
|
|
145
|
+
if (skipIfExists(filePath, "vite.config.ts")) return;
|
|
147
146
|
|
|
148
|
-
const content =
|
|
147
|
+
const content = `/**
|
|
148
|
+
* Vite config \u2014 used by \`npm run dev\` and \`npm run build\`.
|
|
149
|
+
*
|
|
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.
|
|
154
|
+
*/
|
|
155
|
+
import { defineConfig } from "vite";
|
|
149
156
|
import react from "@vitejs/plugin-react";
|
|
150
|
-
import tailwindcss from "@tailwindcss/vite";
|
|
151
157
|
import { resolve } from "path";
|
|
152
158
|
|
|
153
159
|
export default defineConfig({
|
|
154
|
-
plugins: [react()
|
|
155
|
-
root: "mock",
|
|
160
|
+
plugins: [react()],
|
|
156
161
|
server: { port: ${host.port} },
|
|
157
162
|
resolve: {
|
|
158
163
|
alias: {
|
|
164
|
+
// Path alias \u2014 import from "@/components/..." resolves to src/
|
|
159
165
|
"@/": resolve(__dirname, "src") + "/",
|
|
160
|
-
|
|
161
|
-
"@inertiajs/react": resolve(__dirname, "
|
|
162
|
-
"@inertiajs/core": resolve(__dirname, "
|
|
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"),
|
|
163
169
|
},
|
|
164
170
|
},
|
|
165
171
|
});
|
|
166
172
|
`;
|
|
167
173
|
|
|
168
|
-
writeFile(filePath, content, "vite.
|
|
174
|
+
writeFile(filePath, content, "vite.config.ts");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Scaffold index.html at project root.
|
|
179
|
+
*/
|
|
180
|
+
export function scaffoldIndexHtml(targetDir) {
|
|
181
|
+
const filePath = join(targetDir, "index.html");
|
|
182
|
+
if (skipIfExists(filePath, "index.html")) return;
|
|
183
|
+
|
|
184
|
+
writeFile(
|
|
185
|
+
filePath,
|
|
186
|
+
`<!doctype html>
|
|
187
|
+
<html lang="en">
|
|
188
|
+
<head>
|
|
189
|
+
<meta charset="UTF-8" />
|
|
190
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
191
|
+
<title>MIDDAG React UI</title>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<!--
|
|
195
|
+
DEV ONLY \u2014 This file is used by \`npm run dev\` (Vite dev server).
|
|
196
|
+
In production, your host platform (Moodle/WordPress) serves its own
|
|
197
|
+
HTML and loads the UI via Inertia.js. This file is never deployed.
|
|
198
|
+
|
|
199
|
+
middag-root \u2014 Required class for MIDDAG CSS tokens to apply.
|
|
200
|
+
middag-portals \u2014 Container for floating UI (modals, dropdowns, toasts).
|
|
201
|
+
|
|
202
|
+
To try a theme, add its class to #root:
|
|
203
|
+
class="middag-root theme-ocean"
|
|
204
|
+
See src/theme.css for available themes and how to create your own.
|
|
205
|
+
-->
|
|
206
|
+
<div id="root" class="middag-root"></div>
|
|
207
|
+
<div id="middag-portals" class="middag-root"></div>
|
|
208
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
209
|
+
</body>
|
|
210
|
+
</html>
|
|
211
|
+
`,
|
|
212
|
+
"index.html",
|
|
213
|
+
);
|
|
169
214
|
}
|
|
170
215
|
|
|
171
216
|
// ── Demo files in src/ ──────────────────────────────────────────────────
|
|
@@ -272,68 +317,297 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
272
317
|
"src/contracts.ts",
|
|
273
318
|
);
|
|
274
319
|
}
|
|
320
|
+
|
|
321
|
+
// src/theme.css
|
|
322
|
+
const themePath = join(targetDir, "src", "theme.css");
|
|
323
|
+
if (!skipIfExists(themePath, "src/theme.css")) {
|
|
324
|
+
writeFile(
|
|
325
|
+
themePath,
|
|
326
|
+
`/**
|
|
327
|
+
* Theme customization \u2014 override MIDDAG design tokens here.
|
|
328
|
+
*
|
|
329
|
+
* @middag-io/react uses CSS custom properties for all visual tokens.
|
|
330
|
+
* Override them in :root for global changes, or scope them to a class
|
|
331
|
+
* for switchable themes.
|
|
332
|
+
*
|
|
333
|
+
* All colors use OKLCH color space: oklch(lightness chroma hue)
|
|
334
|
+
* Visual picker: https://oklch.com
|
|
335
|
+
*
|
|
336
|
+
* Import order matters \u2014 this file is loaded AFTER @middag-io/react/style.css
|
|
337
|
+
* so overrides here take precedence.
|
|
338
|
+
*/
|
|
339
|
+
|
|
340
|
+
/* \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
|
|
341
|
+
*
|
|
342
|
+
* Uncomment to change the default appearance globally.
|
|
343
|
+
* Every MIDDAG component reads from these tokens.
|
|
344
|
+
*/
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
:root {
|
|
348
|
+
--primary: oklch(0.45 0.2 260);
|
|
349
|
+
--primary-foreground: oklch(0.98 0 0);
|
|
350
|
+
--accent: oklch(0.95 0.02 260);
|
|
351
|
+
--accent-foreground: oklch(0.21 0.014 286);
|
|
352
|
+
--radius: 0.5rem;
|
|
275
353
|
}
|
|
354
|
+
*/
|
|
276
355
|
|
|
277
|
-
|
|
356
|
+
/* \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
357
|
+
*
|
|
358
|
+
* A complete theme override scoped to a CSS class.
|
|
359
|
+
* Apply with: <div id="root" class="middag-root theme-ocean">
|
|
360
|
+
*
|
|
361
|
+
* This is how MIDDAG themes work \u2014 redefine tokens in a scope.
|
|
362
|
+
* The scoped class overrides :root values for everything inside it.
|
|
363
|
+
*/
|
|
278
364
|
|
|
279
|
-
|
|
280
|
-
|
|
365
|
+
.theme-ocean {
|
|
366
|
+
/* Brand */
|
|
367
|
+
--primary: oklch(0.45 0.18 230);
|
|
368
|
+
--primary-foreground: oklch(0.98 0 0);
|
|
369
|
+
--primary-subtle: oklch(0.92 0.04 230);
|
|
370
|
+
--primary-muted: oklch(0.85 0.06 230);
|
|
371
|
+
|
|
372
|
+
/* Accents */
|
|
373
|
+
--accent: oklch(0.94 0.03 230);
|
|
374
|
+
--accent-foreground: oklch(0.2 0.02 230);
|
|
375
|
+
--info: oklch(0.55 0.14 200);
|
|
376
|
+
--info-foreground: oklch(0.98 0 0);
|
|
377
|
+
|
|
378
|
+
/* Sidebar */
|
|
379
|
+
--sidebar: oklch(0.15 0.03 230);
|
|
380
|
+
--sidebar-foreground: oklch(0.75 0.02 230);
|
|
381
|
+
--sidebar-primary: oklch(0.55 0.15 230);
|
|
382
|
+
--sidebar-primary-foreground: oklch(0.98 0 0);
|
|
383
|
+
--sidebar-accent: oklch(0.22 0.04 230);
|
|
384
|
+
--sidebar-accent-foreground: oklch(0.9 0.005 230);
|
|
385
|
+
--sidebar-hover: oklch(0.19 0.025 230);
|
|
386
|
+
--sidebar-border: oklch(0.25 0.03 230);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* \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
|
|
390
|
+
*
|
|
391
|
+
* Add project-specific styles below. These can target MIDDAG components
|
|
392
|
+
* or your own custom elements.
|
|
393
|
+
*
|
|
394
|
+
* Available token groups:
|
|
395
|
+
* Colors: var(--primary), var(--foreground), var(--background),
|
|
396
|
+
* var(--muted), var(--card), var(--border), var(--destructive),
|
|
397
|
+
* var(--success), var(--warning), var(--info), var(--accent)
|
|
398
|
+
* Spacing: var(--space-1) through var(--space-20) (4px grid)
|
|
399
|
+
* Radius: var(--radius-sm|md|lg|xl|2xl|full)
|
|
400
|
+
* Shadows: var(--shadow-xs|sm|md|lg|xl|2xl)
|
|
401
|
+
* Motion: var(--duration-fast|normal|moderate|slow)
|
|
281
402
|
*/
|
|
282
|
-
export function scaffoldMockFiles(targetDir) {
|
|
283
|
-
const mockDir = join(targetDir, "mock");
|
|
284
|
-
ensureDir(mockDir);
|
|
285
403
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
404
|
+
/* Example: branded page header */
|
|
405
|
+
/*
|
|
406
|
+
.my-page-header {
|
|
407
|
+
background: linear-gradient(135deg, var(--primary) 0%, var(--info) 100%);
|
|
408
|
+
color: var(--primary-foreground);
|
|
409
|
+
padding: var(--space-6) var(--space-8);
|
|
410
|
+
border-radius: var(--radius-lg);
|
|
411
|
+
}
|
|
412
|
+
*/
|
|
413
|
+
`,
|
|
414
|
+
"src/theme.css",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ── Page contract examples ──────────────────────────────────────────────
|
|
292
420
|
|
|
293
421
|
/**
|
|
294
|
-
*
|
|
422
|
+
* Scaffold 3 progressive page contract examples in src/pages/.
|
|
423
|
+
*/
|
|
424
|
+
export function scaffoldPageExamples(targetDir) {
|
|
425
|
+
ensureDir(join(targetDir, "src", "pages"));
|
|
426
|
+
|
|
427
|
+
// ── Starter: dashboard.ts (metric_card + dense_table) ───────────────
|
|
428
|
+
const dashboardPath = join(targetDir, "src", "pages", "dashboard.ts");
|
|
429
|
+
if (!skipIfExists(dashboardPath, "src/pages/dashboard.ts")) {
|
|
430
|
+
writeFile(
|
|
431
|
+
dashboardPath,
|
|
432
|
+
`/**
|
|
433
|
+
* Dashboard page contract \u2014 STARTER example.
|
|
434
|
+
*
|
|
435
|
+
* Demonstrates the "dashboard" layout with two block types:
|
|
436
|
+
* - metric_card (KPI indicators in the metrics region)
|
|
437
|
+
* - dense_table (data table in the content region)
|
|
295
438
|
*
|
|
296
|
-
*
|
|
297
|
-
* Replace this with real data from your server.
|
|
439
|
+
* Layout regions used: metrics, content
|
|
298
440
|
*/
|
|
299
|
-
|
|
441
|
+
import type { PageContract } from "@middag-io/react";
|
|
442
|
+
|
|
443
|
+
export const dashboardContract: PageContract = {
|
|
444
|
+
version: "1",
|
|
300
445
|
shell: "product",
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
{ label: "Hello", href: "/hello" },
|
|
306
|
-
],
|
|
446
|
+
page: {
|
|
447
|
+
key: "dashboard",
|
|
448
|
+
title: "Dashboard",
|
|
449
|
+
breadcrumbs: [{ label: "Home", href: "#/" }],
|
|
307
450
|
},
|
|
308
451
|
layout: {
|
|
309
|
-
template: "
|
|
452
|
+
template: "dashboard",
|
|
310
453
|
regions: {
|
|
311
|
-
|
|
454
|
+
metrics: [
|
|
312
455
|
{
|
|
313
|
-
key: "
|
|
456
|
+
key: "total_users",
|
|
314
457
|
type: "metric_card",
|
|
315
458
|
data: {
|
|
316
|
-
|
|
317
|
-
value: "1",
|
|
318
|
-
|
|
459
|
+
label: "Total Users",
|
|
460
|
+
value: "1,284",
|
|
461
|
+
delta: "+12%",
|
|
462
|
+
deltaDirection: "positive",
|
|
463
|
+
icon: "users",
|
|
319
464
|
},
|
|
320
465
|
},
|
|
321
466
|
{
|
|
322
|
-
key: "
|
|
467
|
+
key: "active_sessions",
|
|
468
|
+
type: "metric_card",
|
|
469
|
+
data: {
|
|
470
|
+
label: "Active Sessions",
|
|
471
|
+
value: "342",
|
|
472
|
+
delta: "+5%",
|
|
473
|
+
deltaDirection: "positive",
|
|
474
|
+
icon: "activity",
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
key: "completion_rate",
|
|
479
|
+
type: "metric_card",
|
|
480
|
+
data: {
|
|
481
|
+
label: "Completion Rate",
|
|
482
|
+
value: "87%",
|
|
483
|
+
delta: "-2%",
|
|
484
|
+
deltaDirection: "negative",
|
|
485
|
+
icon: "chart-line",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
content: [
|
|
490
|
+
{
|
|
491
|
+
key: "recent_activity",
|
|
323
492
|
type: "dense_table",
|
|
493
|
+
title: "Recent Activity",
|
|
494
|
+
data: {
|
|
495
|
+
columns: [
|
|
496
|
+
{ key: "user", label: "User", sortable: true },
|
|
497
|
+
{ key: "action", label: "Action" },
|
|
498
|
+
{ key: "date", label: "Date", sortable: true },
|
|
499
|
+
{ key: "status", label: "Status", variant: "status" },
|
|
500
|
+
],
|
|
501
|
+
rows: [
|
|
502
|
+
{ id: 1, user: "Alice Johnson", action: "Completed module", date: "2024-01-15", status: { label: "Complete", appearance: "success" } },
|
|
503
|
+
{ id: 2, user: "Bob Smith", action: "Started course", date: "2024-01-15", status: { label: "In Progress", appearance: "info" } },
|
|
504
|
+
{ id: 3, user: "Carol Davis", action: "Failed quiz", date: "2024-01-14", status: { label: "Failed", appearance: "danger" } },
|
|
505
|
+
{ id: 4, user: "Dave Wilson", action: "Enrolled", date: "2024-01-14", status: { label: "New", appearance: "neutral" } },
|
|
506
|
+
{ id: 5, user: "Eve Brown", action: "Completed course", date: "2024-01-13", status: { label: "Complete", appearance: "success" } },
|
|
507
|
+
],
|
|
508
|
+
pagination: { page: 1, perPage: 10, total: 5, lastPage: 1 },
|
|
509
|
+
sort: { column: "date", direction: "desc" },
|
|
510
|
+
filters: { available: [], applied: {} },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
`,
|
|
518
|
+
"src/pages/dashboard.ts",
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── Intermediate: connectors.ts (card_grid + status_strip + detail_panel)
|
|
523
|
+
const connectorsPath = join(targetDir, "src", "pages", "connectors.ts");
|
|
524
|
+
if (!skipIfExists(connectorsPath, "src/pages/connectors.ts")) {
|
|
525
|
+
writeFile(
|
|
526
|
+
connectorsPath,
|
|
527
|
+
`/**
|
|
528
|
+
* Connectors page contract \u2014 INTERMEDIATE example.
|
|
529
|
+
*
|
|
530
|
+
* Demonstrates the "split" layout with three block types:
|
|
531
|
+
* - card_grid (connector cards in the main region)
|
|
532
|
+
* - status_strip (health indicators in the aside)
|
|
533
|
+
* - detail_panel (metadata in the aside)
|
|
534
|
+
*
|
|
535
|
+
* Layout regions used: main, aside
|
|
536
|
+
*/
|
|
537
|
+
import type { PageContract } from "@middag-io/react";
|
|
538
|
+
|
|
539
|
+
export const connectorsContract: PageContract = {
|
|
540
|
+
version: "1",
|
|
541
|
+
shell: "product",
|
|
542
|
+
page: {
|
|
543
|
+
key: "connectors",
|
|
544
|
+
title: "Connectors",
|
|
545
|
+
breadcrumbs: [
|
|
546
|
+
{ label: "Home", href: "#/" },
|
|
547
|
+
{ label: "Connectors" },
|
|
548
|
+
],
|
|
549
|
+
actions: [
|
|
550
|
+
{ id: "add", label: "Add Connector", intent: "primary", icon: "plus" },
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
layout: {
|
|
554
|
+
template: "split",
|
|
555
|
+
regions: {
|
|
556
|
+
main: [
|
|
557
|
+
{
|
|
558
|
+
key: "connector_grid",
|
|
559
|
+
type: "card_grid",
|
|
560
|
+
title: "Available Connectors",
|
|
324
561
|
data: {
|
|
325
|
-
|
|
562
|
+
variant: "connector",
|
|
326
563
|
columns: [
|
|
327
|
-
{ key: "
|
|
328
|
-
{ key: "
|
|
329
|
-
{ key: "status", label: "Status" },
|
|
564
|
+
{ key: "name", label: "Name" },
|
|
565
|
+
{ key: "type", label: "Type" },
|
|
566
|
+
{ key: "status", label: "Status", kind: "status" },
|
|
330
567
|
],
|
|
331
568
|
rows: [
|
|
332
|
-
{ id: 1, name: "
|
|
333
|
-
{ id: 2, name: "
|
|
334
|
-
{ id: 3, name: "
|
|
569
|
+
{ id: 1, name: "Moodle LMS", type: "LMS", status: "Active", icon: "graduation-cap", href: "#/connectors" },
|
|
570
|
+
{ id: 2, name: "Google Workspace", type: "SSO", status: "Active", icon: "shield", href: "#/connectors" },
|
|
571
|
+
{ id: 3, name: "Stripe", type: "Payment", status: "Inactive", icon: "credit-card", href: "#/connectors" },
|
|
572
|
+
{ id: 4, name: "Mailchimp", type: "Email", status: "Active", icon: "mail", href: "#/connectors" },
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
aside: [
|
|
578
|
+
{
|
|
579
|
+
key: "connector_health",
|
|
580
|
+
type: "status_strip",
|
|
581
|
+
title: "Health Overview",
|
|
582
|
+
data: {
|
|
583
|
+
score: 75,
|
|
584
|
+
tone: "success",
|
|
585
|
+
items: [
|
|
586
|
+
{ key: "uptime", label: "Uptime", value: "99.9%", appearance: "success" },
|
|
587
|
+
{ key: "sync", label: "Last Sync", value: "2 min ago", appearance: "success" },
|
|
588
|
+
{ key: "errors", label: "Errors (24h)", value: "3", appearance: "warning" },
|
|
589
|
+
{ key: "queue", label: "Queue", value: "0", appearance: "success" },
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
key: "connector_detail",
|
|
595
|
+
type: "detail_panel",
|
|
596
|
+
title: "Selected Connector",
|
|
597
|
+
data: {
|
|
598
|
+
sections: [
|
|
599
|
+
{
|
|
600
|
+
id: "overview",
|
|
601
|
+
title: "Overview",
|
|
602
|
+
fields: [
|
|
603
|
+
{ key: "provider", label: "Provider", value: "Moodle LMS" },
|
|
604
|
+
{ key: "version", label: "API Version", value: "4.3.2" },
|
|
605
|
+
{ key: "connected", label: "Connected Since", value: "2024-01-01", kind: "timestamp" },
|
|
606
|
+
{ key: "status", label: "Status", value: "Active", kind: "status" },
|
|
607
|
+
{ key: "endpoint", label: "Endpoint", value: "https://moodle.example.com/webservice/rest", kind: "code", copyable: true },
|
|
608
|
+
],
|
|
609
|
+
},
|
|
335
610
|
],
|
|
336
|
-
pagination: { page: 1, totalPages: 1, perPage: 10, totalRows: 3 },
|
|
337
611
|
},
|
|
338
612
|
},
|
|
339
613
|
],
|
|
@@ -341,73 +615,325 @@ export const helloContract: PageContract = {
|
|
|
341
615
|
},
|
|
342
616
|
};
|
|
343
617
|
`,
|
|
344
|
-
"
|
|
618
|
+
"src/pages/connectors.ts",
|
|
345
619
|
);
|
|
346
620
|
}
|
|
347
621
|
|
|
348
|
-
//
|
|
349
|
-
const
|
|
350
|
-
if (!skipIfExists(
|
|
622
|
+
// ── Advanced: settings.ts (tabbed_panel + form_panel + link_list) ────
|
|
623
|
+
const settingsPath = join(targetDir, "src", "pages", "settings.ts");
|
|
624
|
+
if (!skipIfExists(settingsPath, "src/pages/settings.ts")) {
|
|
625
|
+
writeFile(
|
|
626
|
+
settingsPath,
|
|
627
|
+
`/**
|
|
628
|
+
* Settings page contract \u2014 ADVANCED example.
|
|
629
|
+
*
|
|
630
|
+
* Demonstrates the "stack" layout with nested block types:
|
|
631
|
+
* - tabbed_panel (tabs that contain other blocks)
|
|
632
|
+
* - form_panel (schema-driven form inside a tab)
|
|
633
|
+
* - link_list (navigation links inside a tab)
|
|
634
|
+
*
|
|
635
|
+
* Layout regions used: content
|
|
636
|
+
*/
|
|
637
|
+
import type { PageContract } from "@middag-io/react";
|
|
638
|
+
|
|
639
|
+
export const settingsContract: PageContract = {
|
|
640
|
+
version: "1",
|
|
641
|
+
shell: "product",
|
|
642
|
+
page: {
|
|
643
|
+
key: "settings",
|
|
644
|
+
title: "Settings",
|
|
645
|
+
breadcrumbs: [
|
|
646
|
+
{ label: "Home", href: "#/" },
|
|
647
|
+
{ label: "Settings" },
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
layout: {
|
|
651
|
+
template: "stack",
|
|
652
|
+
regions: {
|
|
653
|
+
content: [
|
|
654
|
+
{
|
|
655
|
+
key: "settings_tabs",
|
|
656
|
+
type: "tabbed_panel",
|
|
657
|
+
data: {
|
|
658
|
+
defaultTab: "general",
|
|
659
|
+
tabs: [
|
|
660
|
+
{
|
|
661
|
+
key: "general",
|
|
662
|
+
label: "General",
|
|
663
|
+
icon: "settings",
|
|
664
|
+
blocks: [
|
|
665
|
+
{
|
|
666
|
+
key: "general_form",
|
|
667
|
+
type: "form_panel",
|
|
668
|
+
data: {
|
|
669
|
+
action: "/api/settings/general",
|
|
670
|
+
method: "put",
|
|
671
|
+
schema: [
|
|
672
|
+
{
|
|
673
|
+
kind: "section",
|
|
674
|
+
id: "site",
|
|
675
|
+
label: "Site Settings",
|
|
676
|
+
children: [
|
|
677
|
+
{
|
|
678
|
+
kind: "field",
|
|
679
|
+
key: "site_name",
|
|
680
|
+
component: "text",
|
|
681
|
+
props: { label: "Site Name", placeholder: "My Platform", required: true },
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
kind: "field",
|
|
685
|
+
key: "site_url",
|
|
686
|
+
component: "url",
|
|
687
|
+
props: { label: "Site URL", placeholder: "https://example.com" },
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
kind: "field",
|
|
691
|
+
key: "timezone",
|
|
692
|
+
component: "select",
|
|
693
|
+
props: {
|
|
694
|
+
label: "Timezone",
|
|
695
|
+
options: [
|
|
696
|
+
{ value: "UTC", label: "UTC" },
|
|
697
|
+
{ value: "America/Sao_Paulo", label: "S\\u00e3o Paulo (BRT)" },
|
|
698
|
+
{ value: "America/New_York", label: "New York (EST)" },
|
|
699
|
+
{ value: "Europe/London", label: "London (GMT)" },
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
kind: "section",
|
|
707
|
+
id: "features",
|
|
708
|
+
label: "Features",
|
|
709
|
+
children: [
|
|
710
|
+
{
|
|
711
|
+
kind: "field",
|
|
712
|
+
key: "enable_notifications",
|
|
713
|
+
component: "switch",
|
|
714
|
+
props: { label: "Enable Notifications", helpText: "Send email notifications for important events" },
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
kind: "field",
|
|
718
|
+
key: "enable_analytics",
|
|
719
|
+
component: "switch",
|
|
720
|
+
props: { label: "Enable Analytics", helpText: "Track user activity and generate reports" },
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
kind: "field",
|
|
724
|
+
key: "maintenance_mode",
|
|
725
|
+
component: "switch",
|
|
726
|
+
props: { label: "Maintenance Mode", helpText: "Show maintenance page to non-admin users" },
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
values: {
|
|
732
|
+
site_name: "My Platform",
|
|
733
|
+
site_url: "https://example.com",
|
|
734
|
+
timezone: "UTC",
|
|
735
|
+
enable_notifications: true,
|
|
736
|
+
enable_analytics: true,
|
|
737
|
+
maintenance_mode: false,
|
|
738
|
+
},
|
|
739
|
+
errors: {},
|
|
740
|
+
meta: { submitLabel: "Save Changes", cancelHref: "#/" },
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
key: "notifications",
|
|
747
|
+
label: "Notifications",
|
|
748
|
+
icon: "bell",
|
|
749
|
+
blocks: [
|
|
750
|
+
{
|
|
751
|
+
key: "notification_links",
|
|
752
|
+
type: "link_list",
|
|
753
|
+
data: {
|
|
754
|
+
items: [
|
|
755
|
+
{ label: "Email Templates", href: "#/settings", icon: "mail", description: "Customize email notification templates" },
|
|
756
|
+
{ label: "Webhook Endpoints", href: "#/settings", icon: "webhook", description: "Configure webhook delivery endpoints" },
|
|
757
|
+
{ label: "Notification Rules", href: "#/settings", icon: "filter", description: "Set up conditional notification routing" },
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
`,
|
|
771
|
+
"src/pages/settings.ts",
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ── App files (entry point + router + adapters) ─────────────────────────
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Scaffold src/main.tsx, src/app.tsx, and src/adapters/.
|
|
780
|
+
*/
|
|
781
|
+
export function scaffoldAppFiles(targetDir) {
|
|
782
|
+
ensureDir(join(targetDir, "src"));
|
|
783
|
+
ensureDir(join(targetDir, "src", "adapters"));
|
|
784
|
+
|
|
785
|
+
// src/main.tsx — entry point
|
|
786
|
+
const mainPath = join(targetDir, "src", "main.tsx");
|
|
787
|
+
if (!skipIfExists(mainPath, "src/main.tsx")) {
|
|
351
788
|
writeFile(
|
|
352
789
|
mainPath,
|
|
353
|
-
|
|
790
|
+
`/**
|
|
791
|
+
* Entry point \u2014 loaded by index.html during development.
|
|
792
|
+
*
|
|
793
|
+
* In production, your host platform (Moodle/WordPress) loads the UI
|
|
794
|
+
* via Inertia.js and provides its own entry point. This file is
|
|
795
|
+
* only used by the standalone dev server (\`npm run dev\`).
|
|
796
|
+
*
|
|
797
|
+
* Import order matters for CSS:
|
|
798
|
+
* 1. @middag-io/react/style.css \u2014 base tokens + component styles
|
|
799
|
+
* 2. ./theme.css \u2014 your token overrides (wins by cascade)
|
|
800
|
+
* 3. @fontsource-variable/figtree \u2014 font face declarations
|
|
801
|
+
*/
|
|
802
|
+
import { StrictMode } from "react";
|
|
354
803
|
import { createRoot } from "react-dom/client";
|
|
355
|
-
import {
|
|
804
|
+
import { registerDefaults } from "@middag-io/react";
|
|
356
805
|
import "@middag-io/react/style.css";
|
|
357
|
-
import
|
|
806
|
+
import "./theme.css";
|
|
807
|
+
import "@fontsource-variable/figtree";
|
|
808
|
+
import { App } from "./app";
|
|
358
809
|
|
|
359
|
-
// Register all default shells, layouts, and blocks
|
|
810
|
+
// Register all default shells, layouts, and blocks.
|
|
811
|
+
// In production, your host entry point calls this same function.
|
|
360
812
|
registerDefaults();
|
|
361
813
|
|
|
362
814
|
createRoot(document.getElementById("root")!).render(
|
|
363
815
|
<StrictMode>
|
|
364
|
-
<
|
|
816
|
+
<App />
|
|
365
817
|
</StrictMode>,
|
|
366
818
|
);
|
|
367
819
|
`,
|
|
368
|
-
"
|
|
820
|
+
"src/main.tsx",
|
|
369
821
|
);
|
|
370
822
|
}
|
|
371
823
|
|
|
372
|
-
//
|
|
373
|
-
const
|
|
374
|
-
if (!skipIfExists(
|
|
824
|
+
// src/app.tsx — hash-based page router
|
|
825
|
+
const appPath = join(targetDir, "src", "app.tsx");
|
|
826
|
+
if (!skipIfExists(appPath, "src/app.tsx")) {
|
|
375
827
|
writeFile(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
828
|
+
appPath,
|
|
829
|
+
`import { useState, useEffect } from "react";
|
|
830
|
+
import { ContractPage } from "@middag-io/react";
|
|
831
|
+
import type { PageContract } from "@middag-io/react";
|
|
832
|
+
import { dashboardContract } from "./pages/dashboard";
|
|
833
|
+
import { connectorsContract } from "./pages/connectors";
|
|
834
|
+
import { settingsContract } from "./pages/settings";
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Route map \u2014 hash fragment to PageContract.
|
|
838
|
+
*
|
|
839
|
+
* In production, Inertia handles routing. This hash-based approach
|
|
840
|
+
* is only for the standalone dev server. Add more pages here as
|
|
841
|
+
* you build them.
|
|
842
|
+
*/
|
|
843
|
+
const routes: Record<string, PageContract> = {
|
|
844
|
+
"/": dashboardContract,
|
|
845
|
+
"/connectors": connectorsContract,
|
|
846
|
+
"/settings": settingsContract,
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
function getRoute(): string {
|
|
850
|
+
return window.location.hash.replace("#", "") || "/";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function App() {
|
|
854
|
+
const [route, setRoute] = useState(getRoute);
|
|
855
|
+
|
|
856
|
+
useEffect(() => {
|
|
857
|
+
const onHashChange = () => setRoute(getRoute());
|
|
858
|
+
window.addEventListener("hashchange", onHashChange);
|
|
859
|
+
return () => window.removeEventListener("hashchange", onHashChange);
|
|
860
|
+
}, []);
|
|
861
|
+
|
|
862
|
+
const contract = routes[route] || dashboardContract;
|
|
863
|
+
|
|
864
|
+
// Expose contract for usePage() mock adapter
|
|
865
|
+
(window as any).__MIDDAG_MOCK_CONTRACT__ = contract;
|
|
866
|
+
|
|
867
|
+
return <ContractPage contract={contract} />;
|
|
868
|
+
}
|
|
389
869
|
`,
|
|
390
|
-
"
|
|
870
|
+
"src/app.tsx",
|
|
391
871
|
);
|
|
392
872
|
}
|
|
393
873
|
|
|
394
|
-
//
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const inertiaReactPath = join(adaptersDir, "inertia-react.ts");
|
|
399
|
-
if (!skipIfExists(inertiaReactPath, "mock/adapters/inertia-react.ts")) {
|
|
874
|
+
// src/adapters/inertia-react.ts
|
|
875
|
+
const inertiaReactPath = join(targetDir, "src", "adapters", "inertia-react.ts");
|
|
876
|
+
if (!skipIfExists(inertiaReactPath, "src/adapters/inertia-react.ts")) {
|
|
400
877
|
writeFile(
|
|
401
878
|
inertiaReactPath,
|
|
402
879
|
`/**
|
|
403
|
-
* Mock @inertiajs/react \u2014 standalone adapter for
|
|
880
|
+
* Mock @inertiajs/react \u2014 standalone adapter for dev server.
|
|
881
|
+
*
|
|
404
882
|
* Vite alias redirects @inertiajs/react imports here.
|
|
883
|
+
* In production, the real Inertia package handles this.
|
|
405
884
|
*/
|
|
406
|
-
import
|
|
885
|
+
import React from "react";
|
|
407
886
|
import { router } from "./inertia-core";
|
|
408
887
|
|
|
888
|
+
// \u2500\u2500 Navigation \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
889
|
+
|
|
890
|
+
function getActiveKey(): string {
|
|
891
|
+
const hash = window.location.hash.replace("#", "") || "/";
|
|
892
|
+
const map: Record<string, string> = {
|
|
893
|
+
"/": "overview.dashboard",
|
|
894
|
+
"/connectors": "integration.connectors",
|
|
895
|
+
"/settings": "system.settings",
|
|
896
|
+
};
|
|
897
|
+
return map[hash] || "overview.dashboard";
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function buildNavigation() {
|
|
901
|
+
const activeKey = getActiveKey();
|
|
902
|
+
const sections = [
|
|
903
|
+
{
|
|
904
|
+
key: "overview",
|
|
905
|
+
label: "Overview",
|
|
906
|
+
icon: "house",
|
|
907
|
+
group: "main" as const,
|
|
908
|
+
items: [
|
|
909
|
+
{ key: "overview.dashboard", label: "Dashboard", href: "#/", active: activeKey === "overview.dashboard", children: [] },
|
|
910
|
+
],
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
key: "integration",
|
|
914
|
+
label: "Integration",
|
|
915
|
+
icon: "plug",
|
|
916
|
+
group: "main" as const,
|
|
917
|
+
items: [
|
|
918
|
+
{ key: "integration.connectors", label: "Connectors", href: "#/connectors", active: activeKey === "integration.connectors", children: [] },
|
|
919
|
+
],
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
key: "system",
|
|
923
|
+
label: "System",
|
|
924
|
+
icon: "settings",
|
|
925
|
+
group: "system" as const,
|
|
926
|
+
items: [
|
|
927
|
+
{ key: "system.settings", label: "Settings", href: "#/settings", active: activeKey === "system.settings", children: [] },
|
|
928
|
+
],
|
|
929
|
+
},
|
|
930
|
+
];
|
|
931
|
+
return { sections, activeKey };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// \u2500\u2500 usePage \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
935
|
+
|
|
409
936
|
const mockSharedProps = {
|
|
410
|
-
navigation: { sections: [], activeKey: "" },
|
|
411
937
|
auth: { id: 1, name: "Dev User", email: "dev@localhost", capabilities: [] },
|
|
412
938
|
theme: { appearance: "light" as const },
|
|
413
939
|
flash: {},
|
|
@@ -416,15 +942,25 @@ const mockSharedProps = {
|
|
|
416
942
|
};
|
|
417
943
|
|
|
418
944
|
export function usePage<T = Record<string, unknown>>(): { props: T; url: string } {
|
|
419
|
-
|
|
945
|
+
const contract = typeof window !== "undefined" ? (window as any).__MIDDAG_MOCK_CONTRACT__ : undefined;
|
|
946
|
+
return {
|
|
947
|
+
props: { ...mockSharedProps, navigation: buildNavigation(), contract } as T,
|
|
948
|
+
url: window.location.pathname,
|
|
949
|
+
};
|
|
420
950
|
}
|
|
421
951
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
952
|
+
// \u2500\u2500 Head \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
953
|
+
|
|
954
|
+
export function Head({ title, children }: { title?: string; children?: React.ReactNode }) {
|
|
955
|
+
React.useEffect(() => {
|
|
956
|
+
if (title) document.title = title;
|
|
957
|
+
}, [title]);
|
|
958
|
+
return children ? React.createElement("span", { style: { display: "none" } }, children) : null;
|
|
425
959
|
}
|
|
426
960
|
|
|
427
|
-
|
|
961
|
+
// \u2500\u2500 Link \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
962
|
+
|
|
963
|
+
interface MockLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
428
964
|
href?: string;
|
|
429
965
|
method?: string;
|
|
430
966
|
preserveScroll?: boolean;
|
|
@@ -432,7 +968,7 @@ interface MockLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "h
|
|
|
432
968
|
as?: string;
|
|
433
969
|
}
|
|
434
970
|
|
|
435
|
-
export const Link = forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLink(
|
|
971
|
+
export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLink(
|
|
436
972
|
{ href, onClick, children, as: _as, method: _m, preserveScroll: _ps, preserveState: _pst, ...rest },
|
|
437
973
|
ref,
|
|
438
974
|
) {
|
|
@@ -442,23 +978,25 @@ export const Link = forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLi
|
|
|
442
978
|
e.preventDefault();
|
|
443
979
|
if (href) window.location.hash = href;
|
|
444
980
|
};
|
|
445
|
-
return createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
|
|
981
|
+
return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
|
|
446
982
|
});
|
|
447
983
|
|
|
448
984
|
export { router };
|
|
449
985
|
`,
|
|
450
|
-
"
|
|
986
|
+
"src/adapters/inertia-react.ts",
|
|
451
987
|
);
|
|
452
988
|
}
|
|
453
989
|
|
|
454
|
-
//
|
|
455
|
-
const
|
|
456
|
-
if (!skipIfExists(
|
|
990
|
+
// src/adapters/inertia-core.ts
|
|
991
|
+
const inertiaCorePath = join(targetDir, "src", "adapters", "inertia-core.ts");
|
|
992
|
+
if (!skipIfExists(inertiaCorePath, "src/adapters/inertia-core.ts")) {
|
|
457
993
|
writeFile(
|
|
458
|
-
|
|
994
|
+
inertiaCorePath,
|
|
459
995
|
`/**
|
|
460
|
-
* Mock @inertiajs/core \u2014 standalone adapter for
|
|
996
|
+
* Mock @inertiajs/core \u2014 standalone adapter for dev server.
|
|
997
|
+
*
|
|
461
998
|
* Vite alias redirects @inertiajs/core imports here.
|
|
999
|
+
* In production, the real Inertia package handles this.
|
|
462
1000
|
*/
|
|
463
1001
|
export const router = {
|
|
464
1002
|
get: (url: string) => { window.location.hash = url; },
|
|
@@ -471,7 +1009,7 @@ export const router = {
|
|
|
471
1009
|
on: () => () => {},
|
|
472
1010
|
};
|
|
473
1011
|
`,
|
|
474
|
-
"
|
|
1012
|
+
"src/adapters/inertia-core.ts",
|
|
475
1013
|
);
|
|
476
1014
|
}
|
|
477
1015
|
}
|