create-lego-one 2.0.12 → 2.0.13
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/dist/index.cjs +34 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/package.json +1 -1
- package/template/nginx.conf +72 -0
- package/template/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
|
@@ -0,0 +1,1518 @@
|
|
|
1
|
+
# Host Kernel Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For AI Implementing This Plan:** This is document 03 of 13. Complete `01-infrastructure-setup.md` and `02-pocketbase-setup.md` first.
|
|
4
|
+
|
|
5
|
+
**Goal:** Initialize the Modern.js host application (the "Kernel") with shared state, layout, routing, Garfish configuration, and base providers.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Host app is the kernel that provides authentication, multitenancy, RBAC, shared services, and layout structure. Plugins are loaded dynamically as sub-apps via Garfish.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Modern.js, Rspack, Garfish, React 18, TypeScript, Zustand, TanStack Query v5, Tailwind CSS, PocketBase
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Completed `01-infrastructure-setup.md`
|
|
16
|
+
- ✅ Completed `02-pocketbase-setup.md`
|
|
17
|
+
- ✅ pnpm workspace configured
|
|
18
|
+
- ✅ PocketBase collections and migrations in place
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Task 1: Initialize Modern.js Host App
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Create: `host/package.json`
|
|
26
|
+
- Create: `host/tsconfig.json`
|
|
27
|
+
- Create: `host/modern.config.ts`
|
|
28
|
+
- Create: `host/.gitignore`
|
|
29
|
+
|
|
30
|
+
### Step 1: Create host package.json
|
|
31
|
+
|
|
32
|
+
**File:** `host/package.json`
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"name": "host",
|
|
37
|
+
"version": "1.0.0",
|
|
38
|
+
"private": true,
|
|
39
|
+
"type": "module",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "modern dev",
|
|
42
|
+
"build": "modern build",
|
|
43
|
+
"start": "modern start",
|
|
44
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@garfish/hooks": "^1.22.0",
|
|
49
|
+
"@modern-js/runtime": "^2.60.0",
|
|
50
|
+
"@modern-js/runtime/garfish": "^2.60.0",
|
|
51
|
+
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
|
52
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
53
|
+
"@tanstack/react-query": "^5.59.0",
|
|
54
|
+
"class-variance-authority": "^0.7.0",
|
|
55
|
+
"clsx": "^2.1.1",
|
|
56
|
+
"garfish": "^1.22.0",
|
|
57
|
+
"lucide-react": "^0.454.0",
|
|
58
|
+
"pocketbase": "^0.21.5",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"tailwind-merge": "^2.5.4",
|
|
62
|
+
"tailwindcss-animate": "^1.0.7",
|
|
63
|
+
"zustand": "^5.0.1"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@modern-js/app-tools": "^2.60.0",
|
|
67
|
+
"@modern-js/plugin-garfish": "^2.60.0",
|
|
68
|
+
"@modern-js/tsconfig": "^2.60.0",
|
|
69
|
+
"@types/react": "^18.3.12",
|
|
70
|
+
"@types/react-dom": "^18.3.1",
|
|
71
|
+
"autoprefixer": "^10.4.20",
|
|
72
|
+
"postcss": "^8.4.49",
|
|
73
|
+
"tailwindcss": "^3.4.15",
|
|
74
|
+
"typescript": "^5.6.3"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Step 2: Create host tsconfig.json
|
|
80
|
+
|
|
81
|
+
**File:** `host/tsconfig.json`
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"extends": "@modern-js/tsconfig/base.json",
|
|
86
|
+
"compilerOptions": {
|
|
87
|
+
"jsx": "react-jsx",
|
|
88
|
+
"strict": true,
|
|
89
|
+
"esModuleInterop": true,
|
|
90
|
+
"skipLibCheck": true,
|
|
91
|
+
"moduleResolution": "bundler",
|
|
92
|
+
"resolveJsonModule": true,
|
|
93
|
+
"isolatedModules": true,
|
|
94
|
+
"paths": {
|
|
95
|
+
"@/*": ["./src/*"],
|
|
96
|
+
"@lego/kernel/*": ["./src/kernel/*"]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"include": ["src"],
|
|
100
|
+
"exclude": ["node_modules", "dist"]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Step 3: Create host modern.config.ts
|
|
105
|
+
|
|
106
|
+
**File:** `host/modern.config.ts`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { appTools, defineConfig } from '@modern-js/app-tools';
|
|
110
|
+
import { garfishPlugin } from '@modern-js/plugin-garfish';
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
// Enable Rspack for high-performance builds
|
|
114
|
+
tools: {
|
|
115
|
+
rspack: (config) => {
|
|
116
|
+
return config;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Enable file-based routing for the host kernel
|
|
121
|
+
runtime: {
|
|
122
|
+
router: true,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Register Garfish plugin for micro-frontend capabilities
|
|
126
|
+
plugins: [appTools(), garfishPlugin()],
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Step 4: Create host .gitignore
|
|
131
|
+
|
|
132
|
+
**File:** `host/.gitignore`
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
node_modules/
|
|
136
|
+
dist/
|
|
137
|
+
.modern/
|
|
138
|
+
*.local
|
|
139
|
+
.DS_Store
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Step 5: Install host dependencies
|
|
143
|
+
|
|
144
|
+
**Run:** From root directory
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pnpm install
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Expected: All dependencies installed successfully.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Task 2: Configure Tailwind CSS for Modern.js
|
|
155
|
+
|
|
156
|
+
**Files:**
|
|
157
|
+
- Create: `host/tailwind.config.ts`
|
|
158
|
+
- Create: `host/postcss.config.js`
|
|
159
|
+
- Create: `host/src/global.css`
|
|
160
|
+
- Create: `host/src/App.tsx`
|
|
161
|
+
|
|
162
|
+
### Step 1: Create Tailwind config
|
|
163
|
+
|
|
164
|
+
**File:** `host/tailwind.config.ts`
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import type { Config } from 'tailwindcss';
|
|
168
|
+
|
|
169
|
+
const config: Config = {
|
|
170
|
+
darkMode: ['class'],
|
|
171
|
+
content: [
|
|
172
|
+
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
|
173
|
+
'../../packages/plugins/@lego/*/src/**/*.{js,ts,jsx,tsx,mdx}',
|
|
174
|
+
],
|
|
175
|
+
theme: {
|
|
176
|
+
container: {
|
|
177
|
+
center: true,
|
|
178
|
+
padding: '2rem',
|
|
179
|
+
screens: {
|
|
180
|
+
'2xl': '1400px',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
extend: {
|
|
184
|
+
colors: {
|
|
185
|
+
border: 'hsl(var(--border))',
|
|
186
|
+
input: 'hsl(var(--input))',
|
|
187
|
+
ring: 'hsl(var(--ring))',
|
|
188
|
+
background: 'hsl(var(--background))',
|
|
189
|
+
foreground: 'hsl(var(--foreground))',
|
|
190
|
+
primary: {
|
|
191
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
192
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
193
|
+
},
|
|
194
|
+
secondary: {
|
|
195
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
196
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
197
|
+
},
|
|
198
|
+
destructive: {
|
|
199
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
200
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
201
|
+
},
|
|
202
|
+
muted: {
|
|
203
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
204
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
205
|
+
},
|
|
206
|
+
accent: {
|
|
207
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
208
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
209
|
+
},
|
|
210
|
+
popover: {
|
|
211
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
212
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
213
|
+
},
|
|
214
|
+
card: {
|
|
215
|
+
DEFAULT: 'hsl(var(--card))',
|
|
216
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
borderRadius: {
|
|
220
|
+
lg: 'var(--radius)',
|
|
221
|
+
md: 'calc(var(--radius) - 2px)',
|
|
222
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
223
|
+
},
|
|
224
|
+
keyframes: {
|
|
225
|
+
'accordion-down': {
|
|
226
|
+
from: { height: '0' },
|
|
227
|
+
to: { height: 'var(--radix-accordion-content-height)' },
|
|
228
|
+
},
|
|
229
|
+
'accordion-up': {
|
|
230
|
+
from: { height: 'var(--radix-accordion-content-height)' },
|
|
231
|
+
to: { height: '0' },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
animation: {
|
|
235
|
+
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
236
|
+
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
plugins: [require('tailwindcss-animate')],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export default config;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Step 2: Create PostCSS config
|
|
247
|
+
|
|
248
|
+
**File:** `host/postcss.config.js`
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
module.exports = {
|
|
252
|
+
plugins: {
|
|
253
|
+
tailwindcss: {},
|
|
254
|
+
autoprefixer: {},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Step 3: Create global CSS with CSS variables
|
|
260
|
+
|
|
261
|
+
**File:** `host/src/global.css`
|
|
262
|
+
|
|
263
|
+
```css
|
|
264
|
+
@tailwind base;
|
|
265
|
+
@tailwind components;
|
|
266
|
+
@tailwind utilities;
|
|
267
|
+
|
|
268
|
+
@layer base {
|
|
269
|
+
:root {
|
|
270
|
+
--background: 0 0% 100%;
|
|
271
|
+
--foreground: 222.2 84% 4.9%;
|
|
272
|
+
--card: 0 0% 100%;
|
|
273
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
274
|
+
--popover: 0 0% 100%;
|
|
275
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
276
|
+
--primary: 221.2 83.2% 53.3%;
|
|
277
|
+
--primary-foreground: 210 40% 98%;
|
|
278
|
+
--secondary: 210 40% 96.1%;
|
|
279
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
280
|
+
--muted: 210 40% 96.1%;
|
|
281
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
282
|
+
--accent: 210 40% 96.1%;
|
|
283
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
284
|
+
--destructive: 0 84.2% 60.2%;
|
|
285
|
+
--destructive-foreground: 210 40% 98%;
|
|
286
|
+
--border: 214.3 31.8% 91.4%;
|
|
287
|
+
--input: 214.3 31.8% 91.4%;
|
|
288
|
+
--ring: 221.2 83.2% 53.3%;
|
|
289
|
+
--radius: 0.5rem;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.dark {
|
|
293
|
+
--background: 222.2 84% 4.9%;
|
|
294
|
+
--foreground: 210 40% 98%;
|
|
295
|
+
--card: 222.2 84% 4.9%;
|
|
296
|
+
--card-foreground: 210 40% 98%;
|
|
297
|
+
--popover: 222.2 84% 4.9%;
|
|
298
|
+
--popover-foreground: 210 40% 98%;
|
|
299
|
+
--primary: 217.2 91.2% 59.8%;
|
|
300
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
301
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
302
|
+
--secondary-foreground: 210 40% 98%;
|
|
303
|
+
--muted: 217.2 32.6% 17.5%;
|
|
304
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
305
|
+
--accent: 217.2 32.6% 17.5%;
|
|
306
|
+
--accent-foreground: 210 40% 98%;
|
|
307
|
+
--destructive: 0 62.8% 30.6%;
|
|
308
|
+
--destructive-foreground: 210 40% 98%;
|
|
309
|
+
--border: 217.2 32.6% 17.5%;
|
|
310
|
+
--input: 217.2 32.6% 17.5%;
|
|
311
|
+
--ring: 224.3 76.3% 48%;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@layer base {
|
|
316
|
+
* {
|
|
317
|
+
@apply border-border;
|
|
318
|
+
}
|
|
319
|
+
body {
|
|
320
|
+
@apply bg-background text-foreground;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Step 4: Create root App component
|
|
326
|
+
|
|
327
|
+
**File:** `host/src/App.tsx`
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { Outlet } from '@modern-js/runtime/router';
|
|
331
|
+
import './global.css';
|
|
332
|
+
|
|
333
|
+
export default function App() {
|
|
334
|
+
return <Outlet />;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Task 3: Create Shared State Bridge
|
|
341
|
+
|
|
342
|
+
**Files:**
|
|
343
|
+
- Create: `host/src/kernel/shared-state/types.ts`
|
|
344
|
+
- Create: `host/src/kernel/shared-state/store.ts`
|
|
345
|
+
- Create: `host/src/kernel/shared-state/bridge.ts`
|
|
346
|
+
- Create: `host/src/kernel/shared-state/index.ts`
|
|
347
|
+
|
|
348
|
+
### Step 1: Create shared state types
|
|
349
|
+
|
|
350
|
+
**File:** `host/src/kernel/shared-state/types.ts`
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import type { RecordAuth } from 'pocketbase';
|
|
354
|
+
|
|
355
|
+
export interface User {
|
|
356
|
+
id: string;
|
|
357
|
+
email: string;
|
|
358
|
+
name: string;
|
|
359
|
+
avatar?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface Organization {
|
|
363
|
+
id: string;
|
|
364
|
+
name: string;
|
|
365
|
+
slug: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface AuthState {
|
|
369
|
+
user: User | null;
|
|
370
|
+
token: string | null;
|
|
371
|
+
isAuthenticated: boolean;
|
|
372
|
+
organization: Organization | null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
376
|
+
|
|
377
|
+
export interface UIState {
|
|
378
|
+
theme: Theme;
|
|
379
|
+
sidebarOpen: boolean;
|
|
380
|
+
mobileMenuOpen: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface Toast {
|
|
384
|
+
id: string;
|
|
385
|
+
title: string;
|
|
386
|
+
description?: string;
|
|
387
|
+
variant?: 'default' | 'destructive' | 'success';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export interface GlobalKernelState extends AuthState, UIState {
|
|
391
|
+
// Auth actions
|
|
392
|
+
setUser: (user: User | null) => void;
|
|
393
|
+
setToken: (token: string | null) => void;
|
|
394
|
+
setOrganization: (org: Organization | null) => void;
|
|
395
|
+
clearAuth: () => void;
|
|
396
|
+
|
|
397
|
+
// UI actions
|
|
398
|
+
setTheme: (theme: Theme) => void;
|
|
399
|
+
toggleSidebar: () => void;
|
|
400
|
+
setSidebarOpen: (open: boolean) => void;
|
|
401
|
+
toggleMobileMenu: () => void;
|
|
402
|
+
setMobileMenuOpen: (open: boolean) => void;
|
|
403
|
+
|
|
404
|
+
// Toast actions
|
|
405
|
+
toasts: Toast[];
|
|
406
|
+
addToast: (toast: Omit<Toast, 'id'>) => void;
|
|
407
|
+
removeToast: (id: string) => void;
|
|
408
|
+
|
|
409
|
+
// Loading state
|
|
410
|
+
isLoading: boolean;
|
|
411
|
+
setIsLoading: (loading: boolean) => void;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Step 2: Create Zustand store
|
|
416
|
+
|
|
417
|
+
**File:** `host/src/kernel/shared-state/store.ts`
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { create } from 'zustand';
|
|
421
|
+
import { devtools, persist } from 'zustand/middleware';
|
|
422
|
+
import type { GlobalKernelState, Theme, User, Organization, Toast } from './types';
|
|
423
|
+
|
|
424
|
+
const generateId = () => Math.random().toString(36).substring(2, 9);
|
|
425
|
+
|
|
426
|
+
export const useGlobalKernelState = create<GlobalKernelState>()(
|
|
427
|
+
devtools(
|
|
428
|
+
persist(
|
|
429
|
+
(set) => ({
|
|
430
|
+
// Initial state
|
|
431
|
+
user: null,
|
|
432
|
+
token: null,
|
|
433
|
+
isAuthenticated: false,
|
|
434
|
+
organization: null,
|
|
435
|
+
theme: 'system',
|
|
436
|
+
sidebarOpen: true,
|
|
437
|
+
mobileMenuOpen: false,
|
|
438
|
+
toasts: [],
|
|
439
|
+
isLoading: false,
|
|
440
|
+
|
|
441
|
+
// Auth actions
|
|
442
|
+
setUser: (user: User | null) => set({ user, isAuthenticated: !!user }),
|
|
443
|
+
setToken: (token: string | null) => set({ token }),
|
|
444
|
+
setOrganization: (org: Organization | null) => set({ organization: org }),
|
|
445
|
+
clearAuth: () => set({
|
|
446
|
+
user: null,
|
|
447
|
+
token: null,
|
|
448
|
+
isAuthenticated: false,
|
|
449
|
+
organization: null,
|
|
450
|
+
}),
|
|
451
|
+
|
|
452
|
+
// UI actions
|
|
453
|
+
setTheme: (theme: Theme) => set({ theme }),
|
|
454
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
455
|
+
setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }),
|
|
456
|
+
toggleMobileMenu: () => set((state) => ({ mobileMenuOpen: !state.mobileMenuOpen })),
|
|
457
|
+
setMobileMenuOpen: (open: boolean) => set({ mobileMenuOpen: open }),
|
|
458
|
+
|
|
459
|
+
// Toast actions
|
|
460
|
+
addToast: (toast: Omit<Toast, 'id'>) => set((state) => ({
|
|
461
|
+
toasts: [...state.toasts, { ...toast, id: generateId() }],
|
|
462
|
+
})),
|
|
463
|
+
removeToast: (id: string) => set((state) => ({
|
|
464
|
+
toasts: state.toasts.filter((t) => t.id !== id),
|
|
465
|
+
})),
|
|
466
|
+
|
|
467
|
+
// Loading actions
|
|
468
|
+
setIsLoading: (isLoading: boolean) => set({ isLoading }),
|
|
469
|
+
}),
|
|
470
|
+
{
|
|
471
|
+
name: 'lego-kernel-state',
|
|
472
|
+
partialize: (state) => ({
|
|
473
|
+
theme: state.theme,
|
|
474
|
+
sidebarOpen: state.sidebarOpen,
|
|
475
|
+
token: state.token,
|
|
476
|
+
}),
|
|
477
|
+
}
|
|
478
|
+
),
|
|
479
|
+
{ name: 'LegoKernelState' }
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Step 3: Create window bridge for plugins
|
|
485
|
+
|
|
486
|
+
**File:** `host/src/kernel/shared-state/bridge.ts`
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { useGlobalKernelState } from './store';
|
|
490
|
+
|
|
491
|
+
// Declare window type for shared state
|
|
492
|
+
declare global {
|
|
493
|
+
interface Window {
|
|
494
|
+
__LEGO_KERNEL_STATE__?: {
|
|
495
|
+
useGlobalKernelState: typeof useGlobalKernelState;
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Register shared state to window for plugin access
|
|
501
|
+
export function registerSharedState() {
|
|
502
|
+
if (typeof window !== 'undefined') {
|
|
503
|
+
window.__LEGO_KERNEL_STATE__ = {
|
|
504
|
+
useGlobalKernelState,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Hook for plugins to access kernel state
|
|
510
|
+
export function useKernelState() {
|
|
511
|
+
return useGlobalKernelState;
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Step 4: Create barrel export
|
|
516
|
+
|
|
517
|
+
**File:** `host/src/kernel/shared-state/index.ts`
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
export * from './types';
|
|
521
|
+
export * from './store';
|
|
522
|
+
export * from './bridge';
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Task 4: Create Base Providers
|
|
528
|
+
|
|
529
|
+
**Files:**
|
|
530
|
+
- Create: `host/src/kernel/providers/PocketBaseProvider.tsx`
|
|
531
|
+
- Create: `host/src/kernel/providers/QueryProvider.tsx`
|
|
532
|
+
- Create: `host/src/kernel/providers/ThemeProvider.tsx`
|
|
533
|
+
- Create: `host/src/kernel/providers/index.ts`
|
|
534
|
+
|
|
535
|
+
### Step 1: Create PocketBase provider
|
|
536
|
+
|
|
537
|
+
**File:** `host/src/kernel/providers/PocketBaseProvider.tsx`
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
541
|
+
import PocketBase from 'pocketbase';
|
|
542
|
+
import { useGlobalKernelState } from '../shared-state';
|
|
543
|
+
|
|
544
|
+
const PocketBaseContext = createContext<PocketBase | null>(null);
|
|
545
|
+
|
|
546
|
+
export function PocketBaseProvider({ children }: { children: React.ReactNode }) {
|
|
547
|
+
const [pb, setPb] = useState<PocketBase | null>(null);
|
|
548
|
+
const { token, clearAuth } = useGlobalKernelState();
|
|
549
|
+
|
|
550
|
+
useEffect(() => {
|
|
551
|
+
// Initialize PocketBase client
|
|
552
|
+
const client = new PocketBase(import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090');
|
|
553
|
+
|
|
554
|
+
// Load stored token
|
|
555
|
+
const storedToken = localStorage.getItem('pocketbase_auth');
|
|
556
|
+
if (storedToken) {
|
|
557
|
+
client.authStore.save(storedToken, null);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
setPb(client);
|
|
561
|
+
|
|
562
|
+
// Set up auth cleanup on token invalidation
|
|
563
|
+
client.authStore.onChange((token, model) => {
|
|
564
|
+
if (!token) {
|
|
565
|
+
clearAuth();
|
|
566
|
+
localStorage.removeItem('pocketbase_auth');
|
|
567
|
+
} else {
|
|
568
|
+
localStorage.setItem('pocketbase_auth', token);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
return () => {
|
|
573
|
+
// Cleanup on unmount
|
|
574
|
+
};
|
|
575
|
+
}, [clearAuth]);
|
|
576
|
+
|
|
577
|
+
// Update auth store when token changes in state
|
|
578
|
+
useEffect(() => {
|
|
579
|
+
if (pb && token && pb.authStore.token !== token) {
|
|
580
|
+
pb.authStore.save(token, null);
|
|
581
|
+
}
|
|
582
|
+
}, [pb, token]);
|
|
583
|
+
|
|
584
|
+
if (!pb) {
|
|
585
|
+
return null; // or a loading spinner
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<PocketBaseContext.Provider value={pb}>
|
|
590
|
+
{children}
|
|
591
|
+
</PocketBaseContext.Provider>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function usePocketBase() {
|
|
596
|
+
const context = useContext(PocketBaseContext);
|
|
597
|
+
if (!context) {
|
|
598
|
+
throw new Error('usePocketBase must be used within PocketBaseProvider');
|
|
599
|
+
}
|
|
600
|
+
return context;
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Step 2: Create TanStack Query provider
|
|
605
|
+
|
|
606
|
+
**File:** `host/src/kernel/providers/QueryProvider.tsx`
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
610
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
611
|
+
import { useState, type ReactNode } from 'react';
|
|
612
|
+
|
|
613
|
+
export function QueryProvider({ children }: { children: ReactNode }) {
|
|
614
|
+
const [queryClient] = useState(
|
|
615
|
+
() =>
|
|
616
|
+
new QueryClient({
|
|
617
|
+
defaultOptions: {
|
|
618
|
+
queries: {
|
|
619
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
620
|
+
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
|
|
621
|
+
retry: 1,
|
|
622
|
+
refetchOnWindowFocus: false,
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<QueryClientProvider client={queryClient}>
|
|
630
|
+
{children}
|
|
631
|
+
{import.meta.env.MODE === 'development' && (
|
|
632
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
633
|
+
)}
|
|
634
|
+
</QueryClientProvider>
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Step 3: Create theme provider
|
|
640
|
+
|
|
641
|
+
**File:** `host/src/kernel/providers/ThemeProvider.tsx`
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
import { useEffect } from 'react';
|
|
645
|
+
import { useGlobalKernelState, type Theme } from '../shared-state';
|
|
646
|
+
|
|
647
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
648
|
+
const { theme } = useGlobalKernelState();
|
|
649
|
+
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
const root = window.document.documentElement;
|
|
652
|
+
root.classList.remove('light', 'dark');
|
|
653
|
+
|
|
654
|
+
let effectiveTheme: 'light' | 'dark' = 'light';
|
|
655
|
+
|
|
656
|
+
if (theme === 'system') {
|
|
657
|
+
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
658
|
+
? 'dark'
|
|
659
|
+
: 'light';
|
|
660
|
+
} else {
|
|
661
|
+
effectiveTheme = theme;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
root.classList.add(effectiveTheme);
|
|
665
|
+
}, [theme]);
|
|
666
|
+
|
|
667
|
+
return <>{children}</>;
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Step 4: Create barrel export
|
|
672
|
+
|
|
673
|
+
**File:** `host/src/kernel/providers/index.ts`
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
export * from './PocketBaseProvider';
|
|
677
|
+
export * from './QueryProvider';
|
|
678
|
+
export * from './ThemeProvider';
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## Task 5: Create UI Utilities
|
|
684
|
+
|
|
685
|
+
**Files:**
|
|
686
|
+
- Create: `host/src/kernel/lib/utils.ts`
|
|
687
|
+
- Create: `host/src/kernel/lib/cn.ts`
|
|
688
|
+
|
|
689
|
+
### Step 1: Create utility functions
|
|
690
|
+
|
|
691
|
+
**File:** `host/src/kernel/lib/utils.ts`
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
695
|
+
import { twMerge } from 'tailwind-merge';
|
|
696
|
+
|
|
697
|
+
export function cn(...inputs: ClassValue[]) {
|
|
698
|
+
return twMerge(clsx(inputs));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function getInitials(name: string): string {
|
|
702
|
+
return name
|
|
703
|
+
.split(' ')
|
|
704
|
+
.map((n) => n[0])
|
|
705
|
+
.join('')
|
|
706
|
+
.toUpperCase()
|
|
707
|
+
.slice(0, 2);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function formatDate(date: string | Date): string {
|
|
711
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
712
|
+
month: 'short',
|
|
713
|
+
day: 'numeric',
|
|
714
|
+
year: 'numeric',
|
|
715
|
+
}).format(new Date(date));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function formatRelativeTime(date: string | Date): string {
|
|
719
|
+
const now = new Date();
|
|
720
|
+
const past = new Date(date);
|
|
721
|
+
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
|
|
722
|
+
|
|
723
|
+
if (diffInSeconds < 60) return 'just now';
|
|
724
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
725
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
726
|
+
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
727
|
+
|
|
728
|
+
return formatDate(date);
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### Step 2: Create barrel export
|
|
733
|
+
|
|
734
|
+
**File:** `host/src/kernel/lib/cn.ts`
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
export { cn } from './utils';
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## Task 6: Create Host Layout Structure
|
|
743
|
+
|
|
744
|
+
**Files:**
|
|
745
|
+
- Create: `host/src/layout/Shell.tsx`
|
|
746
|
+
- Create: `host/src/layout/Sidebar.tsx`
|
|
747
|
+
- Create: `host/src/layout/Topbar.tsx`
|
|
748
|
+
- Create: `host/src/layout/MobileMenu.tsx`
|
|
749
|
+
- Create: `host/src/layout/index.ts`
|
|
750
|
+
|
|
751
|
+
### Step 1: Create shell layout component
|
|
752
|
+
|
|
753
|
+
**File:** `host/src/layout/Shell.tsx`
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
import { Outlet } from '@modern-js/runtime/router';
|
|
757
|
+
import { Sidebar } from './Sidebar';
|
|
758
|
+
import { Topbar } from './Topbar';
|
|
759
|
+
import { MobileMenu } from './MobileMenu';
|
|
760
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
761
|
+
|
|
762
|
+
export function Shell() {
|
|
763
|
+
const { sidebarOpen, mobileMenuOpen } = useGlobalKernelState();
|
|
764
|
+
|
|
765
|
+
return (
|
|
766
|
+
<div className="min-h-screen bg-background">
|
|
767
|
+
{/* Mobile menu overlay */}
|
|
768
|
+
{mobileMenuOpen && (
|
|
769
|
+
<div
|
|
770
|
+
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm lg:hidden"
|
|
771
|
+
aria-hidden="true"
|
|
772
|
+
/>
|
|
773
|
+
)}
|
|
774
|
+
|
|
775
|
+
{/* Sidebar */}
|
|
776
|
+
<Sidebar />
|
|
777
|
+
|
|
778
|
+
{/* Main content area */}
|
|
779
|
+
<div
|
|
780
|
+
className={`transition-all duration-300 lg:pl-64 ${
|
|
781
|
+
sidebarOpen ? 'lg:pl-64' : 'lg:pl-16'
|
|
782
|
+
}`}
|
|
783
|
+
>
|
|
784
|
+
{/* Topbar */}
|
|
785
|
+
<Topbar />
|
|
786
|
+
|
|
787
|
+
{/* Page content */}
|
|
788
|
+
<main className="min-h-[calc(100vh-4rem)] p-6">
|
|
789
|
+
<Outlet />
|
|
790
|
+
</main>
|
|
791
|
+
</div>
|
|
792
|
+
|
|
793
|
+
{/* Mobile menu */}
|
|
794
|
+
<MobileMenu />
|
|
795
|
+
</div>
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Step 2: Create sidebar component
|
|
801
|
+
|
|
802
|
+
**File:** `host/src/layout/Sidebar.tsx`
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
import { Link, NavLink } from '@modern-js/runtime/router';
|
|
806
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
807
|
+
import {
|
|
808
|
+
Home,
|
|
809
|
+
LayoutDashboard,
|
|
810
|
+
Settings,
|
|
811
|
+
ChevronLeft,
|
|
812
|
+
ChevronRight,
|
|
813
|
+
CheckSquare,
|
|
814
|
+
} from 'lucide-react';
|
|
815
|
+
import { cn } from '../kernel/lib/utils';
|
|
816
|
+
|
|
817
|
+
const navItems = [
|
|
818
|
+
{ to: '/', icon: Home, label: 'Home' },
|
|
819
|
+
{ to: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' },
|
|
820
|
+
{ to: '/todos', icon: CheckSquare, label: 'Todos' },
|
|
821
|
+
];
|
|
822
|
+
|
|
823
|
+
export function Sidebar() {
|
|
824
|
+
const { sidebarOpen, toggleSidebar, mobileMenuOpen } = useGlobalKernelState();
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
<>
|
|
828
|
+
{/* Mobile sidebar */}
|
|
829
|
+
<aside
|
|
830
|
+
className={cn(
|
|
831
|
+
'fixed inset-y-0 left-0 z-50 w-64 transform border-r bg-card transition-transform duration-300 lg:hidden',
|
|
832
|
+
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
|
|
833
|
+
)}
|
|
834
|
+
>
|
|
835
|
+
<div className="flex h-16 items-center justify-between border-b px-6">
|
|
836
|
+
<Link to="/" className="flex items-center gap-2 font-semibold">
|
|
837
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
838
|
+
L
|
|
839
|
+
</div>
|
|
840
|
+
<span>Lego-One</span>
|
|
841
|
+
</Link>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<nav className="space-y-1 p-4">
|
|
845
|
+
{navItems.map((item) => (
|
|
846
|
+
<NavLink
|
|
847
|
+
key={item.to}
|
|
848
|
+
to={item.to}
|
|
849
|
+
end={item.to === '/'}
|
|
850
|
+
className={({ isActive }) =>
|
|
851
|
+
cn(
|
|
852
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
853
|
+
isActive
|
|
854
|
+
? 'bg-primary text-primary-foreground'
|
|
855
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
856
|
+
)
|
|
857
|
+
}
|
|
858
|
+
>
|
|
859
|
+
<item.icon className="h-5 w-5" />
|
|
860
|
+
{item.label}
|
|
861
|
+
</NavLink>
|
|
862
|
+
))}
|
|
863
|
+
</nav>
|
|
864
|
+
|
|
865
|
+
<div className="absolute bottom-0 left-0 right-0 border-t p-4">
|
|
866
|
+
<NavLink
|
|
867
|
+
to="/settings"
|
|
868
|
+
className={({ isActive }) =>
|
|
869
|
+
cn(
|
|
870
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
871
|
+
isActive
|
|
872
|
+
? 'bg-primary text-primary-foreground'
|
|
873
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
>
|
|
877
|
+
<Settings className="h-5 w-5" />
|
|
878
|
+
Settings
|
|
879
|
+
</NavLink>
|
|
880
|
+
</div>
|
|
881
|
+
</aside>
|
|
882
|
+
|
|
883
|
+
{/* Desktop sidebar */}
|
|
884
|
+
<aside
|
|
885
|
+
className={cn(
|
|
886
|
+
'fixed inset-y-0 left-0 z-30 hidden border-r bg-card transition-all duration-300 lg:block',
|
|
887
|
+
sidebarOpen ? 'w-64' : 'w-16'
|
|
888
|
+
)}
|
|
889
|
+
>
|
|
890
|
+
<div className="flex h-16 items-center justify-between border-b px-4">
|
|
891
|
+
{sidebarOpen ? (
|
|
892
|
+
<Link to="/" className="flex items-center gap-2 font-semibold">
|
|
893
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
894
|
+
L
|
|
895
|
+
</div>
|
|
896
|
+
<span>Lego-One</span>
|
|
897
|
+
</Link>
|
|
898
|
+
) : (
|
|
899
|
+
<Link to="/" className="flex items-center justify-center">
|
|
900
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
901
|
+
L
|
|
902
|
+
</div>
|
|
903
|
+
</Link>
|
|
904
|
+
)}
|
|
905
|
+
|
|
906
|
+
<button
|
|
907
|
+
onClick={toggleSidebar}
|
|
908
|
+
className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
909
|
+
>
|
|
910
|
+
{sidebarOpen ? (
|
|
911
|
+
<ChevronLeft className="h-5 w-5" />
|
|
912
|
+
) : (
|
|
913
|
+
<ChevronRight className="h-5 w-5" />
|
|
914
|
+
)}
|
|
915
|
+
</button>
|
|
916
|
+
</div>
|
|
917
|
+
|
|
918
|
+
<nav className="space-y-1 p-2">
|
|
919
|
+
{navItems.map((item) => (
|
|
920
|
+
<NavLink
|
|
921
|
+
key={item.to}
|
|
922
|
+
to={item.to}
|
|
923
|
+
end={item.to === '/'}
|
|
924
|
+
className={({ isActive }) =>
|
|
925
|
+
cn(
|
|
926
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
927
|
+
!sidebarOpen && 'justify-center',
|
|
928
|
+
isActive
|
|
929
|
+
? 'bg-primary text-primary-foreground'
|
|
930
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
title={!sidebarOpen ? item.label : undefined}
|
|
934
|
+
>
|
|
935
|
+
<item.icon className="h-5 w-5 flex-shrink-0" />
|
|
936
|
+
{sidebarOpen && <span>{item.label}</span>}
|
|
937
|
+
</NavLink>
|
|
938
|
+
))}
|
|
939
|
+
</nav>
|
|
940
|
+
|
|
941
|
+
<div className="absolute bottom-0 left-0 right-0 border-t p-2">
|
|
942
|
+
<NavLink
|
|
943
|
+
to="/settings"
|
|
944
|
+
className={({ isActive }) =>
|
|
945
|
+
cn(
|
|
946
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
947
|
+
!sidebarOpen && 'justify-center',
|
|
948
|
+
isActive
|
|
949
|
+
? 'bg-primary text-primary-foreground'
|
|
950
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
title={!sidebarOpen ? 'Settings' : undefined}
|
|
954
|
+
>
|
|
955
|
+
<Settings className="h-5 w-5 flex-shrink-0" />
|
|
956
|
+
{sidebarOpen && <span>Settings</span>}
|
|
957
|
+
</NavLink>
|
|
958
|
+
</div>
|
|
959
|
+
</aside>
|
|
960
|
+
</>
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### Step 3: Create topbar component
|
|
966
|
+
|
|
967
|
+
**File:** `host/src/layout/Topbar.tsx`
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
971
|
+
import { Menu, Bell, User } from 'lucide-react';
|
|
972
|
+
import { Link } from '@modern-js/runtime/router';
|
|
973
|
+
import { getInitials } from '../kernel/lib/utils';
|
|
974
|
+
|
|
975
|
+
export function Topbar() {
|
|
976
|
+
const { toggleMobileMenu, user } = useGlobalKernelState();
|
|
977
|
+
|
|
978
|
+
return (
|
|
979
|
+
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-6">
|
|
980
|
+
{/* Mobile menu button */}
|
|
981
|
+
<button
|
|
982
|
+
onClick={toggleMobileMenu}
|
|
983
|
+
className="lg:hidden rounded-lg p-2 text-muted-foreground hover:bg-muted"
|
|
984
|
+
>
|
|
985
|
+
<Menu className="h-5 w-5" />
|
|
986
|
+
</button>
|
|
987
|
+
|
|
988
|
+
{/* Breadcrumb/spacer */}
|
|
989
|
+
<div className="flex-1" />
|
|
990
|
+
|
|
991
|
+
{/* Actions */}
|
|
992
|
+
<div className="flex items-center gap-2">
|
|
993
|
+
<button className="rounded-lg p-2 text-muted-foreground hover:bg-muted">
|
|
994
|
+
<Bell className="h-5 w-5" />
|
|
995
|
+
</button>
|
|
996
|
+
|
|
997
|
+
{user ? (
|
|
998
|
+
<Link
|
|
999
|
+
to="/settings/profile"
|
|
1000
|
+
className="flex h-9 w-9 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-medium"
|
|
1001
|
+
>
|
|
1002
|
+
{getInitials(user.name)}
|
|
1003
|
+
</Link>
|
|
1004
|
+
) : (
|
|
1005
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
1006
|
+
<User className="h-5 w-5" />
|
|
1007
|
+
</div>
|
|
1008
|
+
)}
|
|
1009
|
+
</div>
|
|
1010
|
+
</header>
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Step 4: Create mobile menu component
|
|
1016
|
+
|
|
1017
|
+
**File:** `host/src/layout/MobileMenu.tsx`
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import { NavLink } from '@modern-js/runtime/router';
|
|
1021
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
1022
|
+
import {
|
|
1023
|
+
Home,
|
|
1024
|
+
LayoutDashboard,
|
|
1025
|
+
Settings,
|
|
1026
|
+
CheckSquare,
|
|
1027
|
+
X,
|
|
1028
|
+
} from 'lucide-react';
|
|
1029
|
+
import { cn } from '../kernel/lib/utils';
|
|
1030
|
+
|
|
1031
|
+
const navItems = [
|
|
1032
|
+
{ to: '/', icon: Home, label: 'Home' },
|
|
1033
|
+
{ to: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' },
|
|
1034
|
+
{ to: '/todos', icon: CheckSquare, label: 'Todos' },
|
|
1035
|
+
{ to: '/settings', icon: Settings, label: 'Settings' },
|
|
1036
|
+
];
|
|
1037
|
+
|
|
1038
|
+
export function MobileMenu() {
|
|
1039
|
+
const { mobileMenuOpen, setMobileMenuOpen } = useGlobalKernelState();
|
|
1040
|
+
|
|
1041
|
+
if (!mobileMenuOpen) return null;
|
|
1042
|
+
|
|
1043
|
+
return (
|
|
1044
|
+
<div className="fixed inset-0 z-50 lg:hidden">
|
|
1045
|
+
<div className="fixed inset-y-0 right-0 w-64 border-l bg-background p-6 shadow-lg">
|
|
1046
|
+
<div className="flex items-center justify-between mb-8">
|
|
1047
|
+
<h2 className="text-lg font-semibold">Menu</h2>
|
|
1048
|
+
<button
|
|
1049
|
+
onClick={() => setMobileMenuOpen(false)}
|
|
1050
|
+
className="rounded-lg p-2 text-muted-foreground hover:bg-muted"
|
|
1051
|
+
>
|
|
1052
|
+
<X className="h-5 w-5" />
|
|
1053
|
+
</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
|
|
1056
|
+
<nav className="space-y-1">
|
|
1057
|
+
{navItems.map((item) => (
|
|
1058
|
+
<NavLink
|
|
1059
|
+
key={item.to}
|
|
1060
|
+
to={item.to}
|
|
1061
|
+
end={item.to === '/'}
|
|
1062
|
+
onClick={() => setMobileMenuOpen(false)}
|
|
1063
|
+
className={({ isActive }) =>
|
|
1064
|
+
cn(
|
|
1065
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
1066
|
+
isActive
|
|
1067
|
+
? 'bg-primary text-primary-foreground'
|
|
1068
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
>
|
|
1072
|
+
<item.icon className="h-5 w-5" />
|
|
1073
|
+
{item.label}
|
|
1074
|
+
</NavLink>
|
|
1075
|
+
))}
|
|
1076
|
+
</nav>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Step 5: Create barrel export
|
|
1084
|
+
|
|
1085
|
+
**File:** `host/src/layout/index.ts`
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
export * from './Shell';
|
|
1089
|
+
export * from './Sidebar';
|
|
1090
|
+
export * from './Topbar';
|
|
1091
|
+
export * from './MobileMenu';
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
---
|
|
1095
|
+
|
|
1096
|
+
## Task 7: Configure Garfish and Create Plugin Routes
|
|
1097
|
+
|
|
1098
|
+
**Files:**
|
|
1099
|
+
- Create: `host/src/modern.runtime.ts`
|
|
1100
|
+
- Create: `host/src/routes/_.tsx`
|
|
1101
|
+
- Create: `host/src/routes/index.tsx`
|
|
1102
|
+
- Create: `host/src/routes/dashboard._.tsx`
|
|
1103
|
+
|
|
1104
|
+
### Step 1: Create Modern.js runtime config
|
|
1105
|
+
|
|
1106
|
+
**File:** `host/src/modern.runtime.ts`
|
|
1107
|
+
|
|
1108
|
+
```typescript
|
|
1109
|
+
import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
1110
|
+
|
|
1111
|
+
// Development vs Production configuration
|
|
1112
|
+
const isDev = import.meta.env.MODE === 'development';
|
|
1113
|
+
|
|
1114
|
+
export default defineRuntimeConfig({
|
|
1115
|
+
masterApp: {
|
|
1116
|
+
apps: [
|
|
1117
|
+
{
|
|
1118
|
+
name: '@lego/plugin-dashboard',
|
|
1119
|
+
// Dev: Separate server │ Prod: Bundled via dynamic import
|
|
1120
|
+
entry: isDev
|
|
1121
|
+
? 'http://localhost:3001'
|
|
1122
|
+
: () => import('@lego/plugin-dashboard'),
|
|
1123
|
+
activeWhen: '/dashboard',
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
name: '@lego/plugin-todo',
|
|
1127
|
+
entry: isDev
|
|
1128
|
+
? 'http://localhost:3002'
|
|
1129
|
+
: () => import('@lego/plugin-todo'),
|
|
1130
|
+
activeWhen: '/todos',
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
### Step 2: Create plugin route wrapper
|
|
1138
|
+
|
|
1139
|
+
**File:** `host/src/routes/_.tsx`
|
|
1140
|
+
|
|
1141
|
+
```typescript
|
|
1142
|
+
import { Outlet } from '@modern-js/runtime/router';
|
|
1143
|
+
import { Shell } from '../layout';
|
|
1144
|
+
|
|
1145
|
+
export default function PluginWrapper() {
|
|
1146
|
+
return (
|
|
1147
|
+
<Shell>
|
|
1148
|
+
<Outlet />
|
|
1149
|
+
</Shell>
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### Step 3: Create home/index route
|
|
1155
|
+
|
|
1156
|
+
**File:** `host/src/routes/index.tsx`
|
|
1157
|
+
|
|
1158
|
+
```typescript
|
|
1159
|
+
import { Link } from '@modern-js/runtime/router';
|
|
1160
|
+
import { ArrowRight, LayoutDashboard, CheckSquare, Shield } from 'lucide-react';
|
|
1161
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
1162
|
+
|
|
1163
|
+
export default function HomePage() {
|
|
1164
|
+
const { isAuthenticated, user } = useGlobalKernelState();
|
|
1165
|
+
|
|
1166
|
+
return (
|
|
1167
|
+
<div className="mx-auto max-w-4xl">
|
|
1168
|
+
<div className="space-y-6">
|
|
1169
|
+
{/* Hero */}
|
|
1170
|
+
<div className="rounded-xl border bg-card p-8 text-center">
|
|
1171
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
1172
|
+
Welcome to Lego-One
|
|
1173
|
+
</h1>
|
|
1174
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
1175
|
+
A modular SaaS boilerplate with microkernel architecture
|
|
1176
|
+
</p>
|
|
1177
|
+
|
|
1178
|
+
{isAuthenticated ? (
|
|
1179
|
+
<div className="mt-8">
|
|
1180
|
+
<p className="text-muted-foreground">
|
|
1181
|
+
Welcome back, {user?.name || 'User'}!
|
|
1182
|
+
</p>
|
|
1183
|
+
</div>
|
|
1184
|
+
) : (
|
|
1185
|
+
<div className="mt-8 flex justify-center gap-4">
|
|
1186
|
+
<Link
|
|
1187
|
+
to="/login"
|
|
1188
|
+
className="inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground hover:bg-primary/90"
|
|
1189
|
+
>
|
|
1190
|
+
Sign In
|
|
1191
|
+
<ArrowRight className="h-4 w-4" />
|
|
1192
|
+
</Link>
|
|
1193
|
+
</div>
|
|
1194
|
+
)}
|
|
1195
|
+
</div>
|
|
1196
|
+
|
|
1197
|
+
{/* Features */}
|
|
1198
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
1199
|
+
<div className="rounded-xl border bg-card p-6">
|
|
1200
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
1201
|
+
<LayoutDashboard className="h-6 w-6" />
|
|
1202
|
+
</div>
|
|
1203
|
+
<h3 className="mt-4 text-lg font-semibold">Dashboard Plugin</h3>
|
|
1204
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
1205
|
+
Overview of your account with stats and quick actions
|
|
1206
|
+
</p>
|
|
1207
|
+
<Link
|
|
1208
|
+
to="/dashboard"
|
|
1209
|
+
className="mt-4 inline-flex items-center text-sm font-medium text-primary hover:underline"
|
|
1210
|
+
>
|
|
1211
|
+
View Dashboard
|
|
1212
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
1213
|
+
</Link>
|
|
1214
|
+
</div>
|
|
1215
|
+
|
|
1216
|
+
<div className="rounded-xl border bg-card p-6">
|
|
1217
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
1218
|
+
<CheckSquare className="h-6 w-6" />
|
|
1219
|
+
</div>
|
|
1220
|
+
<h3 className="mt-4 text-lg font-semibold">Todo Plugin</h3>
|
|
1221
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
1222
|
+
Manage your tasks with this example plugin
|
|
1223
|
+
</p>
|
|
1224
|
+
<Link
|
|
1225
|
+
to="/todos"
|
|
1226
|
+
className="mt-4 inline-flex items-center text-sm font-medium text-primary hover:underline"
|
|
1227
|
+
>
|
|
1228
|
+
View Todos
|
|
1229
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
1230
|
+
</Link>
|
|
1231
|
+
</div>
|
|
1232
|
+
|
|
1233
|
+
<div className="rounded-xl border bg-card p-6">
|
|
1234
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
1235
|
+
<Shield className="h-6 w-6" />
|
|
1236
|
+
</div>
|
|
1237
|
+
<h3 className="mt-4 text-lg font-semibold">Multi-Tenancy</h3>
|
|
1238
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
1239
|
+
Built-in support for organizations and RBAC
|
|
1240
|
+
</p>
|
|
1241
|
+
<Link
|
|
1242
|
+
to="/settings"
|
|
1243
|
+
className="mt-4 inline-flex items-center text-sm font-medium text-primary hover:underline"
|
|
1244
|
+
>
|
|
1245
|
+
Manage Settings
|
|
1246
|
+
<ArrowRight className="ml-1 h-4 w-4" />
|
|
1247
|
+
</Link>
|
|
1248
|
+
</div>
|
|
1249
|
+
</div>
|
|
1250
|
+
</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Step 4: Create dashboard plugin route
|
|
1257
|
+
|
|
1258
|
+
**File:** `host/src/routes/dashboard._.tsx`
|
|
1259
|
+
|
|
1260
|
+
```typescript
|
|
1261
|
+
import { useModuleApps } from '@modern-js/runtime/garfish';
|
|
1262
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
1263
|
+
import { Skeleton } from '../kernel/components/ui/skeleton';
|
|
1264
|
+
|
|
1265
|
+
export default function DashboardPluginRoute() {
|
|
1266
|
+
const { isAuthenticated, isLoading } = useGlobalKernelState();
|
|
1267
|
+
const moduleApps = useModuleApps();
|
|
1268
|
+
|
|
1269
|
+
const DashboardApp = moduleApps.find((app) => app.name === '@lego/plugin-dashboard');
|
|
1270
|
+
|
|
1271
|
+
if (isLoading) {
|
|
1272
|
+
return (
|
|
1273
|
+
<div className="space-y-6">
|
|
1274
|
+
<Skeleton className="h-8 w-64" />
|
|
1275
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
1276
|
+
<Skeleton className="h-32" />
|
|
1277
|
+
<Skeleton className="h-32" />
|
|
1278
|
+
<Skeleton className="h-32" />
|
|
1279
|
+
</div>
|
|
1280
|
+
<Skeleton className="h-64" />
|
|
1281
|
+
</div>
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (!isAuthenticated) {
|
|
1286
|
+
return (
|
|
1287
|
+
<div className="text-center">
|
|
1288
|
+
<h1 className="text-2xl font-bold">Authentication Required</h1>
|
|
1289
|
+
<p className="mt-2 text-muted-foreground">
|
|
1290
|
+
Please sign in to access the dashboard.
|
|
1291
|
+
</p>
|
|
1292
|
+
</div>
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (!DashboardApp) {
|
|
1297
|
+
return (
|
|
1298
|
+
<div className="text-center">
|
|
1299
|
+
<h1 className="text-2xl font-bold">Plugin Not Found</h1>
|
|
1300
|
+
<p className="mt-2 text-muted-foreground">
|
|
1301
|
+
The Dashboard plugin could not be loaded.
|
|
1302
|
+
</p>
|
|
1303
|
+
</div>
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// The plugin app will be rendered here by Garfish
|
|
1308
|
+
return DashboardApp.App;
|
|
1309
|
+
}
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
---
|
|
1313
|
+
|
|
1314
|
+
## Task 8: Create Bootstrap and Root Entry
|
|
1315
|
+
|
|
1316
|
+
**Files:**
|
|
1317
|
+
- Create: `host/src/bootstrap.tsx`
|
|
1318
|
+
- Create: `host/src/kernel/index.ts`
|
|
1319
|
+
- Modify: `host/src/App.tsx`
|
|
1320
|
+
|
|
1321
|
+
### Step 1: Create bootstrap file
|
|
1322
|
+
|
|
1323
|
+
**File:** `host/src/bootstrap.tsx`
|
|
1324
|
+
|
|
1325
|
+
```typescript
|
|
1326
|
+
import { StrictMode } from 'react';
|
|
1327
|
+
import { createRoot } from 'react-dom/client';
|
|
1328
|
+
import { BrowserRouter } from '@modern-js/runtime/router';
|
|
1329
|
+
import App from './App';
|
|
1330
|
+
import { registerSharedState } from './kernel/shared-state';
|
|
1331
|
+
import { PocketBaseProvider } from './kernel/providers';
|
|
1332
|
+
import { QueryProvider } from './kernel/providers';
|
|
1333
|
+
import { ThemeProvider } from './kernel/providers';
|
|
1334
|
+
|
|
1335
|
+
// Register shared state bridge for plugins
|
|
1336
|
+
registerSharedState();
|
|
1337
|
+
|
|
1338
|
+
// Create root
|
|
1339
|
+
const container = document.getElementById('root');
|
|
1340
|
+
if (container) {
|
|
1341
|
+
const root = createRoot(container);
|
|
1342
|
+
|
|
1343
|
+
root.render(
|
|
1344
|
+
<StrictMode>
|
|
1345
|
+
<BrowserRouter>
|
|
1346
|
+
<ThemeProvider>
|
|
1347
|
+
<QueryProvider>
|
|
1348
|
+
<PocketBaseProvider>
|
|
1349
|
+
<App />
|
|
1350
|
+
</PocketBaseProvider>
|
|
1351
|
+
</QueryProvider>
|
|
1352
|
+
</ThemeProvider>
|
|
1353
|
+
</BrowserRouter>
|
|
1354
|
+
</StrictMode>
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
### Step 2: Create kernel barrel export
|
|
1360
|
+
|
|
1361
|
+
**File:** `host/src/kernel/index.ts`
|
|
1362
|
+
|
|
1363
|
+
```typescript
|
|
1364
|
+
export * from './shared-state';
|
|
1365
|
+
export * from './providers';
|
|
1366
|
+
export * from './lib/utils';
|
|
1367
|
+
export * from './components';
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
### Step 3: Update App.tsx to use shell layout
|
|
1371
|
+
|
|
1372
|
+
**File:** `host/src/App.tsx`
|
|
1373
|
+
|
|
1374
|
+
```typescript
|
|
1375
|
+
import { Outlet } from '@modern-js/runtime/router';
|
|
1376
|
+
import './global.css';
|
|
1377
|
+
|
|
1378
|
+
export default function App() {
|
|
1379
|
+
return <Outlet />;
|
|
1380
|
+
}
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
### Step 4: Create basic UI components needed
|
|
1384
|
+
|
|
1385
|
+
**File:** `host/src/kernel/components/ui/skeleton.tsx`
|
|
1386
|
+
|
|
1387
|
+
```typescript
|
|
1388
|
+
import { cn } from '../../../lib/utils';
|
|
1389
|
+
|
|
1390
|
+
function Skeleton({
|
|
1391
|
+
className,
|
|
1392
|
+
...props
|
|
1393
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
1394
|
+
return (
|
|
1395
|
+
<div
|
|
1396
|
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
1397
|
+
{...props}
|
|
1398
|
+
/>
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
export { Skeleton };
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
---
|
|
1406
|
+
|
|
1407
|
+
## Task 9: Update Root Entry Point
|
|
1408
|
+
|
|
1409
|
+
**Files:**
|
|
1410
|
+
- Create: `host/src/index.ts`
|
|
1411
|
+
|
|
1412
|
+
### Step 1: Create entry point
|
|
1413
|
+
|
|
1414
|
+
**File:** `host/src/index.ts`
|
|
1415
|
+
|
|
1416
|
+
```typescript
|
|
1417
|
+
import '@modern-js/runtime/garfish';
|
|
1418
|
+
import './bootstrap';
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
---
|
|
1422
|
+
|
|
1423
|
+
## Verification
|
|
1424
|
+
|
|
1425
|
+
### Step 1: Build the host
|
|
1426
|
+
|
|
1427
|
+
**Run:**
|
|
1428
|
+
|
|
1429
|
+
```bash
|
|
1430
|
+
cd host
|
|
1431
|
+
pnpm run build
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Expected: Build completes without errors.
|
|
1435
|
+
|
|
1436
|
+
### Step 2: Start development server
|
|
1437
|
+
|
|
1438
|
+
**Run:**
|
|
1439
|
+
|
|
1440
|
+
```bash
|
|
1441
|
+
cd host
|
|
1442
|
+
pnpm run dev
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
Expected: Server starts on http://localhost:8080
|
|
1446
|
+
|
|
1447
|
+
### Step 3: Verify functionality
|
|
1448
|
+
|
|
1449
|
+
1. Open http://localhost:8080
|
|
1450
|
+
2. Should see the home page with "Welcome to Lego-One"
|
|
1451
|
+
3. Sidebar should be visible with navigation items
|
|
1452
|
+
4. Click menu items to verify routing works
|
|
1453
|
+
5. Open browser console and verify `window.__LEGO_KERNEL_STATE__` exists
|
|
1454
|
+
|
|
1455
|
+
---
|
|
1456
|
+
|
|
1457
|
+
## Summary
|
|
1458
|
+
|
|
1459
|
+
After completing this document, you will have:
|
|
1460
|
+
|
|
1461
|
+
1. ✅ Modern.js host app initialized with Garfish plugin
|
|
1462
|
+
2. ✅ Tailwind CSS configured with Shadcn-compatible theme
|
|
1463
|
+
3. ✅ Shared state bridge via `window.__LEGO_KERNEL_STATE__`
|
|
1464
|
+
4. ✅ Zustand store for auth, UI, and toast state
|
|
1465
|
+
5. ✅ Base providers (PocketBase, TanStack Query, Theme)
|
|
1466
|
+
6. ✅ Layout structure (Sidebar, Topbar, Shell)
|
|
1467
|
+
7. ✅ File-based routing with plugin wrappers
|
|
1468
|
+
8. ✅ Development vs Production plugin loading configuration
|
|
1469
|
+
|
|
1470
|
+
**Next:** `04-auth-system.md` - Implement authentication, login/logout, and session management.
|
|
1471
|
+
|
|
1472
|
+
---
|
|
1473
|
+
|
|
1474
|
+
## Files Created
|
|
1475
|
+
|
|
1476
|
+
```
|
|
1477
|
+
host/
|
|
1478
|
+
├── package.json
|
|
1479
|
+
├── tsconfig.json
|
|
1480
|
+
├── modern.config.ts
|
|
1481
|
+
├── tailwind.config.ts
|
|
1482
|
+
├── postcss.config.js
|
|
1483
|
+
├── .gitignore
|
|
1484
|
+
└── src/
|
|
1485
|
+
├── global.css
|
|
1486
|
+
├── index.ts
|
|
1487
|
+
├── bootstrap.tsx
|
|
1488
|
+
├── App.tsx
|
|
1489
|
+
├── modern.runtime.ts
|
|
1490
|
+
├── kernel/
|
|
1491
|
+
│ ├── index.ts
|
|
1492
|
+
│ ├── shared-state/
|
|
1493
|
+
│ │ ├── types.ts
|
|
1494
|
+
│ │ ├── store.ts
|
|
1495
|
+
│ │ ├── bridge.ts
|
|
1496
|
+
│ │ └── index.ts
|
|
1497
|
+
│ ├── providers/
|
|
1498
|
+
│ │ ├── PocketBaseProvider.tsx
|
|
1499
|
+
│ │ ├── QueryProvider.tsx
|
|
1500
|
+
│ │ ├── ThemeProvider.tsx
|
|
1501
|
+
│ │ └── index.ts
|
|
1502
|
+
│ ├── lib/
|
|
1503
|
+
│ │ ├── utils.ts
|
|
1504
|
+
│ │ └── cn.ts
|
|
1505
|
+
│ └── components/
|
|
1506
|
+
│ └── ui/
|
|
1507
|
+
│ └── skeleton.tsx
|
|
1508
|
+
├── layout/
|
|
1509
|
+
│ ├── Shell.tsx
|
|
1510
|
+
│ ├── Sidebar.tsx
|
|
1511
|
+
│ ├── Topbar.tsx
|
|
1512
|
+
│ ├── MobileMenu.tsx
|
|
1513
|
+
│ └── index.ts
|
|
1514
|
+
└── routes/
|
|
1515
|
+
├── _.tsx
|
|
1516
|
+
├── index.tsx
|
|
1517
|
+
└── dashboard._.tsx
|
|
1518
|
+
```
|