create-middag-ui 0.4.1 → 0.5.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 +14 -9
- package/lib/scaffold.js +638 -119
- 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,10 +85,8 @@ 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",
|
|
@@ -128,42 +128,89 @@ export function scaffoldTsconfig(targetDir) {
|
|
|
128
128
|
noUnusedLocals: true,
|
|
129
129
|
noUnusedParameters: true,
|
|
130
130
|
skipLibCheck: true,
|
|
131
|
-
paths: { "@/*": ["./src/*"]
|
|
131
|
+
paths: { "@/*": ["./src/*"] },
|
|
132
132
|
baseUrl: ".",
|
|
133
133
|
},
|
|
134
|
-
include: ["src"
|
|
134
|
+
include: ["src"],
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
writeFile(filePath, JSON.stringify(tsconfig, null, 2) + "\n", "tsconfig.json");
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Scaffold vite.
|
|
141
|
+
* Scaffold vite.config.ts.
|
|
142
142
|
*/
|
|
143
143
|
export function scaffoldViteConfig(targetDir, host) {
|
|
144
|
-
const filePath = join(targetDir, "vite.
|
|
145
|
-
if (skipIfExists(filePath, "vite.
|
|
144
|
+
const filePath = join(targetDir, "vite.config.ts");
|
|
145
|
+
if (skipIfExists(filePath, "vite.config.ts")) return;
|
|
146
146
|
|
|
147
|
-
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";
|
|
148
156
|
import react from "@vitejs/plugin-react";
|
|
149
157
|
import { resolve } from "path";
|
|
150
158
|
|
|
151
159
|
export default defineConfig({
|
|
152
160
|
plugins: [react()],
|
|
153
|
-
root: "mock",
|
|
154
161
|
server: { port: ${host.port} },
|
|
155
162
|
resolve: {
|
|
156
163
|
alias: {
|
|
164
|
+
// Path alias \u2014 import from "@/components/..." resolves to src/
|
|
157
165
|
"@/": resolve(__dirname, "src") + "/",
|
|
158
|
-
|
|
159
|
-
"@inertiajs/react": resolve(__dirname, "
|
|
160
|
-
"@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"),
|
|
161
169
|
},
|
|
162
170
|
},
|
|
163
171
|
});
|
|
164
172
|
`;
|
|
165
173
|
|
|
166
|
-
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
|
+
);
|
|
167
214
|
}
|
|
168
215
|
|
|
169
216
|
// ── Demo files in src/ ──────────────────────────────────────────────────
|
|
@@ -270,70 +317,197 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
|
|
|
270
317
|
"src/contracts.ts",
|
|
271
318
|
);
|
|
272
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;
|
|
273
353
|
}
|
|
354
|
+
*/
|
|
274
355
|
|
|
275
|
-
|
|
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
|
+
*/
|
|
276
364
|
|
|
277
|
-
|
|
278
|
-
|
|
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)
|
|
279
402
|
*/
|
|
280
|
-
export function scaffoldMockFiles(targetDir) {
|
|
281
|
-
const mockDir = join(targetDir, "mock");
|
|
282
|
-
ensureDir(mockDir);
|
|
283
403
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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 ──────────────────────────────────────────────
|
|
290
420
|
|
|
291
421
|
/**
|
|
292
|
-
*
|
|
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)
|
|
293
438
|
*
|
|
294
|
-
*
|
|
295
|
-
* Replace this with real data from your server.
|
|
439
|
+
* Layout regions used: metrics, content
|
|
296
440
|
*/
|
|
297
|
-
|
|
441
|
+
import type { PageContract } from "@middag-io/react";
|
|
442
|
+
|
|
443
|
+
export const dashboardContract: PageContract = {
|
|
298
444
|
version: "1",
|
|
299
445
|
shell: "product",
|
|
300
446
|
page: {
|
|
301
|
-
key: "
|
|
302
|
-
title: "
|
|
303
|
-
breadcrumbs: [
|
|
304
|
-
{ label: "Home", href: "/" },
|
|
305
|
-
{ label: "Hello", href: "/hello" },
|
|
306
|
-
],
|
|
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: [
|
|
455
|
+
{
|
|
456
|
+
key: "total_users",
|
|
457
|
+
type: "metric_card",
|
|
458
|
+
data: {
|
|
459
|
+
label: "Total Users",
|
|
460
|
+
value: "1,284",
|
|
461
|
+
delta: "+12%",
|
|
462
|
+
deltaDirection: "positive",
|
|
463
|
+
icon: "users",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
312
466
|
{
|
|
313
|
-
key: "
|
|
467
|
+
key: "active_sessions",
|
|
314
468
|
type: "metric_card",
|
|
315
469
|
data: {
|
|
316
|
-
|
|
317
|
-
value: "
|
|
318
|
-
|
|
470
|
+
label: "Active Sessions",
|
|
471
|
+
value: "342",
|
|
472
|
+
delta: "+5%",
|
|
473
|
+
deltaDirection: "positive",
|
|
474
|
+
icon: "activity",
|
|
319
475
|
},
|
|
320
476
|
},
|
|
321
477
|
{
|
|
322
|
-
key: "
|
|
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",
|
|
324
494
|
data: {
|
|
325
|
-
title: "Example Data",
|
|
326
495
|
columns: [
|
|
327
|
-
{ key: "
|
|
328
|
-
{ key: "
|
|
329
|
-
{ key: "
|
|
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" },
|
|
330
500
|
],
|
|
331
501
|
rows: [
|
|
332
|
-
{ id: 1,
|
|
333
|
-
{ id: 2,
|
|
334
|
-
{ id: 3,
|
|
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" } },
|
|
335
507
|
],
|
|
336
|
-
pagination: { page: 1,
|
|
508
|
+
pagination: { page: 1, perPage: 10, total: 5, lastPage: 1 },
|
|
509
|
+
sort: { column: "date", direction: "desc" },
|
|
510
|
+
filters: { available: [], applied: {} },
|
|
337
511
|
},
|
|
338
512
|
},
|
|
339
513
|
],
|
|
@@ -341,91 +515,425 @@ export const helloContract: PageContract = {
|
|
|
341
515
|
},
|
|
342
516
|
};
|
|
343
517
|
`,
|
|
344
|
-
"
|
|
518
|
+
"src/pages/dashboard.ts",
|
|
345
519
|
);
|
|
346
520
|
}
|
|
347
521
|
|
|
348
|
-
//
|
|
349
|
-
const
|
|
350
|
-
if (!skipIfExists(
|
|
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",
|
|
561
|
+
data: {
|
|
562
|
+
variant: "connector",
|
|
563
|
+
columns: [
|
|
564
|
+
{ key: "name", label: "Name" },
|
|
565
|
+
{ key: "type", label: "Type" },
|
|
566
|
+
{ key: "status", label: "Status", kind: "status" },
|
|
567
|
+
],
|
|
568
|
+
rows: [
|
|
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
|
+
},
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
`,
|
|
618
|
+
"src/pages/connectors.ts",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
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";
|
|
806
|
+
import "./theme.css";
|
|
357
807
|
import "@fontsource-variable/figtree";
|
|
358
|
-
import {
|
|
808
|
+
import { App } from "./app";
|
|
359
809
|
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// 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.
|
|
364
812
|
registerDefaults();
|
|
365
813
|
|
|
366
814
|
createRoot(document.getElementById("root")!).render(
|
|
367
815
|
<StrictMode>
|
|
368
|
-
<
|
|
816
|
+
<App />
|
|
369
817
|
</StrictMode>,
|
|
370
818
|
);
|
|
371
819
|
`,
|
|
372
|
-
"
|
|
820
|
+
"src/main.tsx",
|
|
373
821
|
);
|
|
374
822
|
}
|
|
375
823
|
|
|
376
|
-
//
|
|
377
|
-
const
|
|
378
|
-
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")) {
|
|
379
827
|
writeFile(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
}
|
|
394
869
|
`,
|
|
395
|
-
"
|
|
870
|
+
"src/app.tsx",
|
|
396
871
|
);
|
|
397
872
|
}
|
|
398
873
|
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const inertiaReactPath = join(adaptersDir, "inertia-react.ts");
|
|
404
|
-
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")) {
|
|
405
877
|
writeFile(
|
|
406
878
|
inertiaReactPath,
|
|
407
879
|
`/**
|
|
408
|
-
* Mock @inertiajs/react \u2014 standalone adapter for
|
|
880
|
+
* Mock @inertiajs/react \u2014 standalone adapter for dev server.
|
|
881
|
+
*
|
|
409
882
|
* Vite alias redirects @inertiajs/react imports here.
|
|
883
|
+
* In production, the real Inertia package handles this.
|
|
410
884
|
*/
|
|
411
|
-
import
|
|
885
|
+
import React from "react";
|
|
412
886
|
import { router } from "./inertia-core";
|
|
413
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
|
+
|
|
414
936
|
const mockSharedProps = {
|
|
415
|
-
navigation: {
|
|
416
|
-
sections: [
|
|
417
|
-
{
|
|
418
|
-
key: "home",
|
|
419
|
-
label: "Home",
|
|
420
|
-
icon: "house",
|
|
421
|
-
group: "main",
|
|
422
|
-
items: [
|
|
423
|
-
{ key: "home.dashboard", label: "Dashboard", href: "/", active: true, children: [] },
|
|
424
|
-
],
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
activeKey: "home.dashboard",
|
|
428
|
-
},
|
|
429
937
|
auth: { id: 1, name: "Dev User", email: "dev@localhost", capabilities: [] },
|
|
430
938
|
theme: { appearance: "light" as const },
|
|
431
939
|
flash: {},
|
|
@@ -435,15 +943,24 @@ const mockSharedProps = {
|
|
|
435
943
|
|
|
436
944
|
export function usePage<T = Record<string, unknown>>(): { props: T; url: string } {
|
|
437
945
|
const contract = typeof window !== "undefined" ? (window as any).__MIDDAG_MOCK_CONTRACT__ : undefined;
|
|
438
|
-
return {
|
|
946
|
+
return {
|
|
947
|
+
props: { ...mockSharedProps, navigation: buildNavigation(), contract } as T,
|
|
948
|
+
url: window.location.pathname,
|
|
949
|
+
};
|
|
439
950
|
}
|
|
440
951
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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;
|
|
444
959
|
}
|
|
445
960
|
|
|
446
|
-
|
|
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"> {
|
|
447
964
|
href?: string;
|
|
448
965
|
method?: string;
|
|
449
966
|
preserveScroll?: boolean;
|
|
@@ -451,7 +968,7 @@ interface MockLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "h
|
|
|
451
968
|
as?: string;
|
|
452
969
|
}
|
|
453
970
|
|
|
454
|
-
export const Link = forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLink(
|
|
971
|
+
export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLink(
|
|
455
972
|
{ href, onClick, children, as: _as, method: _m, preserveScroll: _ps, preserveState: _pst, ...rest },
|
|
456
973
|
ref,
|
|
457
974
|
) {
|
|
@@ -461,23 +978,25 @@ export const Link = forwardRef<HTMLAnchorElement, MockLinkProps>(function MockLi
|
|
|
461
978
|
e.preventDefault();
|
|
462
979
|
if (href) window.location.hash = href;
|
|
463
980
|
};
|
|
464
|
-
return createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
|
|
981
|
+
return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
|
|
465
982
|
});
|
|
466
983
|
|
|
467
984
|
export { router };
|
|
468
985
|
`,
|
|
469
|
-
"
|
|
986
|
+
"src/adapters/inertia-react.ts",
|
|
470
987
|
);
|
|
471
988
|
}
|
|
472
989
|
|
|
473
|
-
//
|
|
474
|
-
const
|
|
475
|
-
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")) {
|
|
476
993
|
writeFile(
|
|
477
|
-
|
|
994
|
+
inertiaCorePath,
|
|
478
995
|
`/**
|
|
479
|
-
* Mock @inertiajs/core \u2014 standalone adapter for
|
|
996
|
+
* Mock @inertiajs/core \u2014 standalone adapter for dev server.
|
|
997
|
+
*
|
|
480
998
|
* Vite alias redirects @inertiajs/core imports here.
|
|
999
|
+
* In production, the real Inertia package handles this.
|
|
481
1000
|
*/
|
|
482
1001
|
export const router = {
|
|
483
1002
|
get: (url: string) => { window.location.hash = url; },
|
|
@@ -490,7 +1009,7 @@ export const router = {
|
|
|
490
1009
|
on: () => () => {},
|
|
491
1010
|
};
|
|
492
1011
|
`,
|
|
493
|
-
"
|
|
1012
|
+
"src/adapters/inertia-core.ts",
|
|
494
1013
|
);
|
|
495
1014
|
}
|
|
496
1015
|
}
|