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,1179 @@
|
|
|
1
|
+
# Plugin System Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For AI Implementing This Plan:** This is document 08 of 13. Complete documents 01-07 first.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement complete plugin architecture with slot injection system, plugin configuration, dynamic loading, and enable/disable functionality via config and admin UI.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Plugins register slots that can inject content into host layout locations (sidebar, topbar, etc.). Plugin config file controls enabled/disabled state. Host manages plugin lifecycle.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Modern.js plugin system, Garfish dynamic loading, React context, Zustand, Zod validation
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Completed `01-infrastructure-setup.md`
|
|
16
|
+
- ✅ Completed `02-pocketbase-setup.md`
|
|
17
|
+
- ✅ Completed `03-host-kernel.md`
|
|
18
|
+
- ✅ Completed `04-auth-system.md`
|
|
19
|
+
- ✅ Completed `05-multitenancy-rbac.md`
|
|
20
|
+
- ✅ Completed `06-ui-components.md`
|
|
21
|
+
- ✅ Completed `07-communication-system.md`
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Task 1: Create Plugin Types and Interfaces
|
|
26
|
+
|
|
27
|
+
**Files:**
|
|
28
|
+
- Create: `host/src/kernel/plugins/types.ts`
|
|
29
|
+
- Create: `host/src/kernel/plugins/schemas.ts`
|
|
30
|
+
|
|
31
|
+
### Step 1: Create plugin types
|
|
32
|
+
|
|
33
|
+
**File:** `host/src/kernel/plugins/types.ts`
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import type { ReactNode } from 'react';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Slot names where plugins can inject content
|
|
40
|
+
*/
|
|
41
|
+
export enum SlotName {
|
|
42
|
+
SIDEBAR_TOP = 'sidebar:top',
|
|
43
|
+
SIDEBAR_NAV = 'sidebar:nav',
|
|
44
|
+
SIDEBAR_BOTTOM = 'sidebar:bottom',
|
|
45
|
+
TOPBAR_LEFT = 'topbar:left',
|
|
46
|
+
TOPBAR_RIGHT = 'topbar:right',
|
|
47
|
+
TOPBAR_CENTER = 'topbar:center',
|
|
48
|
+
SETTINGS_MENU = 'settings:menu',
|
|
49
|
+
DASHBOARD_WIDGETS = 'dashboard:widgets',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Slot injection configuration
|
|
54
|
+
*/
|
|
55
|
+
export interface SlotInjection {
|
|
56
|
+
slot: SlotName;
|
|
57
|
+
component: ReactNode | React.ComponentType;
|
|
58
|
+
order?: number; // Lower = higher priority
|
|
59
|
+
props?: Record<string, unknown>;
|
|
60
|
+
condition?: () => boolean; // Conditional rendering
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Plugin manifest metadata
|
|
65
|
+
*/
|
|
66
|
+
export interface PluginManifest {
|
|
67
|
+
name: string; // e.g., '@lego/plugin-dashboard'
|
|
68
|
+
version: string;
|
|
69
|
+
displayName: string;
|
|
70
|
+
description: string;
|
|
71
|
+
author?: string;
|
|
72
|
+
icon?: string;
|
|
73
|
+
permissions?: string[]; // Required permissions
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Plugin configuration
|
|
78
|
+
*/
|
|
79
|
+
export interface PluginConfig {
|
|
80
|
+
manifest: PluginManifest;
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
slots: SlotInjection[];
|
|
83
|
+
routes?: RouteConfig[];
|
|
84
|
+
settings?: PluginSetting[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Plugin route configuration
|
|
89
|
+
*/
|
|
90
|
+
export interface RouteConfig {
|
|
91
|
+
path: string;
|
|
92
|
+
component: React.ComponentType;
|
|
93
|
+
protected?: boolean;
|
|
94
|
+
permissions?: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Plugin setting for admin UI
|
|
99
|
+
*/
|
|
100
|
+
export interface PluginSetting {
|
|
101
|
+
key: string;
|
|
102
|
+
type: 'boolean' | 'string' | 'number' | 'select';
|
|
103
|
+
label: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
defaultValue?: unknown;
|
|
106
|
+
options?: { label: string; value: string }[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Plugin state
|
|
111
|
+
*/
|
|
112
|
+
export interface PluginState {
|
|
113
|
+
id: string;
|
|
114
|
+
manifest: PluginManifest;
|
|
115
|
+
enabled: boolean;
|
|
116
|
+
loaded: boolean;
|
|
117
|
+
error?: string;
|
|
118
|
+
settings: Record<string, unknown>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Plugin registry
|
|
123
|
+
*/
|
|
124
|
+
export interface PluginRegistry {
|
|
125
|
+
plugins: Map<string, PluginConfig>;
|
|
126
|
+
enabled: Set<string>;
|
|
127
|
+
slots: Map<SlotName, SlotInjection[]>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Plugin info from host config
|
|
132
|
+
*/
|
|
133
|
+
export interface HostPluginInfo {
|
|
134
|
+
name: string;
|
|
135
|
+
enabled: boolean;
|
|
136
|
+
entry: string | (() => Promise<unknown>);
|
|
137
|
+
activeWhen: string;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Step 2: Create plugin config schemas
|
|
142
|
+
|
|
143
|
+
**File:** `host/src/kernel/plugins/schemas.ts`
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { z } from 'zod';
|
|
147
|
+
import { SlotName } from './types';
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Slot injection schema
|
|
151
|
+
*/
|
|
152
|
+
export const slotInjectionSchema = z.object({
|
|
153
|
+
slot: z.nativeEnum(SlotName),
|
|
154
|
+
component: z.any(), // React component
|
|
155
|
+
order: z.number().optional().default(100),
|
|
156
|
+
props: z.record(z.any()).optional(),
|
|
157
|
+
condition: z.function().optional(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Plugin manifest schema
|
|
162
|
+
*/
|
|
163
|
+
export const pluginManifestSchema = z.object({
|
|
164
|
+
name: z.string().regex(/^@lego\/plugin-/, 'Plugin name must start with @lego/plugin-'),
|
|
165
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver'),
|
|
166
|
+
displayName: z.string().min(1).max(50),
|
|
167
|
+
description: z.string().max(500),
|
|
168
|
+
author: z.string().optional(),
|
|
169
|
+
icon: z.string().optional(),
|
|
170
|
+
permissions: z.array(z.string()).optional(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Plugin config schema
|
|
175
|
+
*/
|
|
176
|
+
export const pluginConfigSchema = z.object({
|
|
177
|
+
manifest: pluginManifestSchema,
|
|
178
|
+
enabled: z.boolean().default(true),
|
|
179
|
+
slots: z.array(slotInjectionSchema).default([]),
|
|
180
|
+
routes: z.array(z.object({
|
|
181
|
+
path: z.string(),
|
|
182
|
+
component: z.any(),
|
|
183
|
+
protected: z.boolean().optional(),
|
|
184
|
+
permissions: z.array(z.string()).optional(),
|
|
185
|
+
})).optional(),
|
|
186
|
+
settings: z.array(z.object({
|
|
187
|
+
key: z.string(),
|
|
188
|
+
type: z.enum(['boolean', 'string', 'number', 'select']),
|
|
189
|
+
label: z.string(),
|
|
190
|
+
description: z.string().optional(),
|
|
191
|
+
defaultValue: z.any(),
|
|
192
|
+
options: z.array(z.object({
|
|
193
|
+
label: z.string(),
|
|
194
|
+
value: z.string(),
|
|
195
|
+
})).optional(),
|
|
196
|
+
})).optional(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export type PluginConfigInput = z.infer<typeof pluginConfigSchema>;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Task 2: Create Plugin Config File Structure
|
|
205
|
+
|
|
206
|
+
**Files:**
|
|
207
|
+
- Create: `host/src/saas.config.ts`
|
|
208
|
+
|
|
209
|
+
### Step 1: Create SaaS config file
|
|
210
|
+
|
|
211
|
+
**File:** `host/src/saas.config.ts`
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { z } from 'zod';
|
|
215
|
+
import { pluginConfigSchema, type PluginConfig } from './kernel/plugins/schemas';
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* SaaS configuration file
|
|
219
|
+
*
|
|
220
|
+
* This file controls which plugins are enabled and their configuration.
|
|
221
|
+
* Admin can override this via UI (stored in PocketBase).
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
const saasConfigSchema = z.object({
|
|
225
|
+
plugins: z.array(z.object({
|
|
226
|
+
name: z.string(),
|
|
227
|
+
enabled: z.boolean(),
|
|
228
|
+
})),
|
|
229
|
+
settings: z.record(z.any()).optional(),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export type SaaSConfig = z.infer<typeof saasConfigSchema>;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Default SaaS configuration
|
|
236
|
+
* Modify this to enable/disable plugins by default
|
|
237
|
+
*/
|
|
238
|
+
export const saasConfig: SaaSConfig = {
|
|
239
|
+
plugins: [
|
|
240
|
+
{
|
|
241
|
+
name: '@lego/plugin-dashboard',
|
|
242
|
+
enabled: true,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: '@lego/plugin-todo',
|
|
246
|
+
enabled: true,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
settings: {
|
|
250
|
+
// Global settings
|
|
251
|
+
allowPublicSignup: false,
|
|
252
|
+
defaultOrganizationRequired: true,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validate config on load
|
|
258
|
+
*/
|
|
259
|
+
export function validateConfig(config: unknown): SaaSConfig {
|
|
260
|
+
return saasConfigSchema.parse(config);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Export for type safety
|
|
264
|
+
export default saasConfig;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Task 3: Create Plugin Store (Zustand)
|
|
270
|
+
|
|
271
|
+
**Files:**
|
|
272
|
+
- Create: `host/src/kernel/plugins/store.ts`
|
|
273
|
+
|
|
274
|
+
### Step 1: Create plugin store
|
|
275
|
+
|
|
276
|
+
**File:** `host/src/kernel/plugins/store.ts`
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { create } from 'zustand';
|
|
280
|
+
import { devtools, persist } from 'zustand/middleware';
|
|
281
|
+
import type { PluginState, PluginConfig } from './types';
|
|
282
|
+
import { saasConfig } from '../../saas.config';
|
|
283
|
+
|
|
284
|
+
interface PluginStore {
|
|
285
|
+
// State
|
|
286
|
+
plugins: Record<string, PluginState>;
|
|
287
|
+
isLoading: boolean;
|
|
288
|
+
|
|
289
|
+
// Actions
|
|
290
|
+
registerPlugin: (config: PluginConfig) => void;
|
|
291
|
+
unregisterPlugin: (name: string) => void;
|
|
292
|
+
enablePlugin: (name: string) => void;
|
|
293
|
+
disablePlugin: (name: string) => void;
|
|
294
|
+
updatePluginSettings: (name: string, settings: Record<string, unknown>) => void;
|
|
295
|
+
setPluginLoaded: (name: string, loaded: boolean) => void;
|
|
296
|
+
setPluginError: (name: string, error: string) => void;
|
|
297
|
+
initializeFromConfig: () => void;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get default plugin settings from manifest
|
|
302
|
+
*/
|
|
303
|
+
function getDefaultSettings(config: PluginConfig): Record<string, unknown> {
|
|
304
|
+
const settings: Record<string, unknown> = {};
|
|
305
|
+
|
|
306
|
+
if (config.settings) {
|
|
307
|
+
for (const setting of config.settings) {
|
|
308
|
+
if (setting.defaultValue !== undefined) {
|
|
309
|
+
settings[setting.key] = setting.defaultValue;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return settings;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Create plugin store
|
|
319
|
+
*/
|
|
320
|
+
export const usePluginStore = create<PluginStore>()(
|
|
321
|
+
devtools(
|
|
322
|
+
persist(
|
|
323
|
+
(set, get) => ({
|
|
324
|
+
plugins: {},
|
|
325
|
+
isLoading: false,
|
|
326
|
+
|
|
327
|
+
registerPlugin: (config) => {
|
|
328
|
+
const { name, enabled } = config.manifest;
|
|
329
|
+
|
|
330
|
+
set((state) => ({
|
|
331
|
+
plugins: {
|
|
332
|
+
...state.plugins,
|
|
333
|
+
[name]: {
|
|
334
|
+
id: name,
|
|
335
|
+
manifest: config.manifest,
|
|
336
|
+
enabled,
|
|
337
|
+
loaded: false,
|
|
338
|
+
settings: getDefaultSettings(config),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
}));
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
unregisterPlugin: (name) => {
|
|
345
|
+
set((state) => {
|
|
346
|
+
const plugins = { ...state.plugins };
|
|
347
|
+
delete plugins[name];
|
|
348
|
+
return { plugins };
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
enablePlugin: (name) => {
|
|
353
|
+
set((state) => ({
|
|
354
|
+
plugins: {
|
|
355
|
+
...state.plugins,
|
|
356
|
+
[name]: {
|
|
357
|
+
...state.plugins[name],
|
|
358
|
+
enabled: true,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
disablePlugin: (name) => {
|
|
365
|
+
set((state) => ({
|
|
366
|
+
plugins: {
|
|
367
|
+
...state.plugins,
|
|
368
|
+
[name]: {
|
|
369
|
+
...state.plugins[name],
|
|
370
|
+
enabled: false,
|
|
371
|
+
loaded: false,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
}));
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
updatePluginSettings: (name, settings) => {
|
|
378
|
+
set((state) => ({
|
|
379
|
+
plugins: {
|
|
380
|
+
...state.plugins,
|
|
381
|
+
[name]: {
|
|
382
|
+
...state.plugins[name],
|
|
383
|
+
settings: {
|
|
384
|
+
...state.plugins[name].settings,
|
|
385
|
+
...settings,
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
}));
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
setPluginLoaded: (name, loaded) => {
|
|
393
|
+
set((state) => ({
|
|
394
|
+
plugins: {
|
|
395
|
+
...state.plugins,
|
|
396
|
+
[name]: {
|
|
397
|
+
...state.plugins[name],
|
|
398
|
+
loaded,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
}));
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
setPluginError: (name, error) => {
|
|
405
|
+
set((state) => ({
|
|
406
|
+
plugins: {
|
|
407
|
+
...state.plugins,
|
|
408
|
+
[name]: {
|
|
409
|
+
...state.plugins[name],
|
|
410
|
+
error,
|
|
411
|
+
loaded: false,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
}));
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
initializeFromConfig: () => {
|
|
418
|
+
const { plugins } = saasConfig;
|
|
419
|
+
|
|
420
|
+
set((state) => {
|
|
421
|
+
const newPlugins = { ...state.plugins };
|
|
422
|
+
|
|
423
|
+
for (const plugin of plugins) {
|
|
424
|
+
const existing = newPlugins[plugin.name];
|
|
425
|
+
|
|
426
|
+
if (!existing) {
|
|
427
|
+
// Create placeholder for plugin that will be loaded
|
|
428
|
+
newPlugins[plugin.name] = {
|
|
429
|
+
id: plugin.name,
|
|
430
|
+
manifest: {
|
|
431
|
+
name: plugin.name,
|
|
432
|
+
version: '0.0.0',
|
|
433
|
+
displayName: plugin.name,
|
|
434
|
+
description: '',
|
|
435
|
+
},
|
|
436
|
+
enabled: plugin.enabled,
|
|
437
|
+
loaded: false,
|
|
438
|
+
settings: {},
|
|
439
|
+
};
|
|
440
|
+
} else {
|
|
441
|
+
// Update enabled state from config
|
|
442
|
+
newPlugins[plugin.name] = {
|
|
443
|
+
...existing,
|
|
444
|
+
enabled: plugin.enabled,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { plugins: newPlugins };
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
{
|
|
454
|
+
name: 'lego-plugin-store',
|
|
455
|
+
partialize: (state) => ({
|
|
456
|
+
plugins: state.plugins,
|
|
457
|
+
}),
|
|
458
|
+
}
|
|
459
|
+
),
|
|
460
|
+
{ name: 'LegoPluginStore' }
|
|
461
|
+
)
|
|
462
|
+
);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Task 4: Create Plugin Loader Service
|
|
468
|
+
|
|
469
|
+
**Files:**
|
|
470
|
+
- Create: `host/src/kernel/plugins/loader.ts`
|
|
471
|
+
|
|
472
|
+
### Step 1: Create plugin loader
|
|
473
|
+
|
|
474
|
+
**File:** `host/src/kernel/plugins/loader.ts`
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import Garfish from 'garfish';
|
|
478
|
+
import type { PluginConfig, PluginManifest } from './types';
|
|
479
|
+
import { usePluginStore } from './store';
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Plugin loader - handles dynamic loading of plugins
|
|
483
|
+
*/
|
|
484
|
+
class PluginLoader {
|
|
485
|
+
private garfishInstance: typeof Garfish | null = null;
|
|
486
|
+
private loadedPlugins = new Map<string, PluginConfig>();
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Initialize loader with Garfish instance
|
|
490
|
+
*/
|
|
491
|
+
initialize(garfish: typeof Garfish): void {
|
|
492
|
+
this.garfishInstance = garfish;
|
|
493
|
+
console.log('[PluginLoader] Initialized');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Load a plugin dynamically
|
|
498
|
+
*/
|
|
499
|
+
async loadPlugin(entry: string | (() => Promise<unknown>)): Promise<PluginConfig | null> {
|
|
500
|
+
if (!this.garfishInstance) {
|
|
501
|
+
console.error('[PluginLoader] Garfish not initialized');
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
let module: any;
|
|
507
|
+
|
|
508
|
+
if (typeof entry === 'function') {
|
|
509
|
+
// Dynamic import (production)
|
|
510
|
+
module = await entry();
|
|
511
|
+
} else {
|
|
512
|
+
// URL load (development)
|
|
513
|
+
// In dev, plugins run on separate servers
|
|
514
|
+
// We just track them, actual loading is handled by Garfish
|
|
515
|
+
console.log(`[PluginLoader] Dev mode: Plugin at ${entry}`);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Extract plugin config from module
|
|
520
|
+
const config = module.default?.config || module.config;
|
|
521
|
+
|
|
522
|
+
if (!config) {
|
|
523
|
+
console.error('[PluginLoader] Plugin has no config export');
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Validate config
|
|
528
|
+
this.loadedPlugins.set(config.manifest.name, config);
|
|
529
|
+
|
|
530
|
+
console.log(`[PluginLoader] Loaded plugin: ${config.manifest.name}`);
|
|
531
|
+
|
|
532
|
+
return config;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error('[PluginLoader] Failed to load plugin:', error);
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Load all configured plugins
|
|
541
|
+
*/
|
|
542
|
+
async loadPlugins(plugins: Array<{
|
|
543
|
+
name: string;
|
|
544
|
+
entry: string | (() => Promise<unknown>);
|
|
545
|
+
}>): Promise<void> {
|
|
546
|
+
const store = usePluginStore.getState();
|
|
547
|
+
|
|
548
|
+
for (const plugin of plugins) {
|
|
549
|
+
const state = store.plugins[plugin.name];
|
|
550
|
+
|
|
551
|
+
if (!state || !state.enabled) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const config = await this.loadPlugin(plugin.entry);
|
|
557
|
+
|
|
558
|
+
if (config) {
|
|
559
|
+
store.registerPlugin(config);
|
|
560
|
+
store.setPluginLoaded(plugin.name, true);
|
|
561
|
+
}
|
|
562
|
+
} catch (error: any) {
|
|
563
|
+
store.setPluginError(plugin.name, error.message);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Get loaded plugin config
|
|
570
|
+
*/
|
|
571
|
+
getPluginConfig(name: string): PluginConfig | undefined {
|
|
572
|
+
return this.loadedPlugins.get(name);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get all loaded plugins
|
|
577
|
+
*/
|
|
578
|
+
getAllPlugins(): PluginConfig[] {
|
|
579
|
+
return Array.from(this.loadedPlugins.values());
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Check if plugin is loaded
|
|
584
|
+
*/
|
|
585
|
+
isLoaded(name: string): boolean {
|
|
586
|
+
return this.loadedPlugins.has(name);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Export singleton
|
|
591
|
+
export const pluginLoader = new PluginLoader();
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Initialize plugin loader (call from bootstrap)
|
|
595
|
+
*/
|
|
596
|
+
export function initializePluginLoader(garfish: typeof Garfish): void {
|
|
597
|
+
pluginLoader.initialize(garfish);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## Task 5: Create Slot System
|
|
604
|
+
|
|
605
|
+
**Files:**
|
|
606
|
+
- Create: `host/src/kernel/plugins/Slot.tsx`
|
|
607
|
+
- Create: `host/src/kernel/plugins/SlotProvider.tsx`
|
|
608
|
+
|
|
609
|
+
### Step 1: Create Slot component
|
|
610
|
+
|
|
611
|
+
**File:** `host/src/kernel/plugins/Slot.tsx`
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
import { useContext } from 'react';
|
|
615
|
+
import { SlotContext } from './SlotProvider';
|
|
616
|
+
import type { SlotName } from './types';
|
|
617
|
+
|
|
618
|
+
interface SlotProps {
|
|
619
|
+
name: SlotName;
|
|
620
|
+
children?: React.ReactNode; // Fallback content
|
|
621
|
+
className?: string;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Slot - Component that renders injected content from plugins
|
|
626
|
+
*
|
|
627
|
+
* Plugins can register content to be rendered in specific slots
|
|
628
|
+
* throughout the host layout (sidebar, topbar, etc.)
|
|
629
|
+
*/
|
|
630
|
+
export function Slot({ name, children, className }: SlotProps) {
|
|
631
|
+
const { getSlotContent } = useContext(SlotContext);
|
|
632
|
+
|
|
633
|
+
const content = getSlotContent(name);
|
|
634
|
+
|
|
635
|
+
if (!content || content.length === 0) {
|
|
636
|
+
return <>{children}</>;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<div className={className}>
|
|
641
|
+
{content.map((item, index) => (
|
|
642
|
+
<div key={`${item.slot}-${index}`} className="slot-item">
|
|
643
|
+
{item.condition === undefined || item.condition() ? (
|
|
644
|
+
<item.component {...(item.props || {})} />
|
|
645
|
+
) : null}
|
|
646
|
+
</div>
|
|
647
|
+
))}
|
|
648
|
+
{children}
|
|
649
|
+
</div>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Step 2: Create Slot Provider
|
|
655
|
+
|
|
656
|
+
**File:** `host/src/kernel/plugins/SlotProvider.tsx`
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
import { createContext, useContext, ReactNode } from 'react';
|
|
660
|
+
import type { SlotInjection, SlotName } from './types';
|
|
661
|
+
import { usePluginStore } from './store';
|
|
662
|
+
|
|
663
|
+
interface SlotContextValue {
|
|
664
|
+
getSlotContent: (name: SlotName) => SlotInjection[];
|
|
665
|
+
registerSlot: (name: SlotName, injection: SlotInjection) => void;
|
|
666
|
+
unregisterSlot: (name: SlotName, component: ReactNode) => void;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const SlotContext = createContext<SlotContextValue | null>(null);
|
|
670
|
+
|
|
671
|
+
interface SlotProviderProps {
|
|
672
|
+
children: ReactNode;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* SlotProvider - Provides slot injection system to the app
|
|
677
|
+
*
|
|
678
|
+
* This provider:
|
|
679
|
+
* 1. Collects slot injections from all enabled plugins
|
|
680
|
+
* 2. Sorts them by order
|
|
681
|
+
* 3. Provides getSlotContent function for Slot components
|
|
682
|
+
*/
|
|
683
|
+
export function SlotProvider({ children }: SlotProviderProps) {
|
|
684
|
+
const plugins = usePluginStore((state) => state.plugins);
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Get all content for a slot from enabled plugins
|
|
688
|
+
*/
|
|
689
|
+
const getSlotContent = (name: SlotName): SlotInjection[] => {
|
|
690
|
+
const injections: SlotInjection[] = [];
|
|
691
|
+
|
|
692
|
+
// Collect injections from all enabled plugins
|
|
693
|
+
for (const plugin of Object.values(plugins)) {
|
|
694
|
+
if (!plugin.enabled || !plugin.loaded) continue;
|
|
695
|
+
|
|
696
|
+
const config = pluginLoader.getPluginConfig(plugin.id);
|
|
697
|
+
if (!config) continue;
|
|
698
|
+
|
|
699
|
+
for (const slot of config.slots) {
|
|
700
|
+
if (slot.slot === name) {
|
|
701
|
+
injections.push(slot);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Sort by order (lower = higher priority = first)
|
|
707
|
+
return injections.sort((a, b) => (a.order || 100) - (b.order || 100));
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Register a slot injection (for dynamic registration)
|
|
712
|
+
*/
|
|
713
|
+
const registerSlot = (name: SlotName, injection: SlotInjection) => {
|
|
714
|
+
// This would be used for runtime slot registration
|
|
715
|
+
// For now, slots are registered via plugin config
|
|
716
|
+
console.log('[SlotProvider] Register slot:', name);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Unregister a slot injection
|
|
721
|
+
*/
|
|
722
|
+
const unregisterSlot = (name: SlotName, component: ReactNode) => {
|
|
723
|
+
console.log('[SlotProvider] Unregister slot:', name);
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
return (
|
|
727
|
+
<SlotContext.Provider value={{ getSlotContent, registerSlot, unregisterSlot }}>
|
|
728
|
+
{children}
|
|
729
|
+
</SlotContext.Provider>
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Hook to access slot context
|
|
735
|
+
*/
|
|
736
|
+
export function useSlots() {
|
|
737
|
+
const context = useContext(SlotContext);
|
|
738
|
+
if (!context) {
|
|
739
|
+
throw new Error('useSlots must be used within SlotProvider');
|
|
740
|
+
}
|
|
741
|
+
return context;
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## Task 6: Update Layout to Use Slots
|
|
748
|
+
|
|
749
|
+
**Files:**
|
|
750
|
+
- Modify: `host/src/layout/Sidebar.tsx`
|
|
751
|
+
- Modify: `host/src/layout/Topbar.tsx`
|
|
752
|
+
|
|
753
|
+
### Step 1: Update sidebar with slots
|
|
754
|
+
|
|
755
|
+
**File:** `host/src/layout/Sidebar.tsx`
|
|
756
|
+
|
|
757
|
+
Add imports and update component:
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
import { Link, NavLink } from '@modern-js/runtime/router';
|
|
761
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
762
|
+
import { Slot } from '../kernel/plugins/Slot';
|
|
763
|
+
import {
|
|
764
|
+
Home,
|
|
765
|
+
LayoutDashboard,
|
|
766
|
+
Settings,
|
|
767
|
+
ChevronLeft,
|
|
768
|
+
ChevronRight,
|
|
769
|
+
CheckSquare,
|
|
770
|
+
} from 'lucide-react';
|
|
771
|
+
import { cn } from '../kernel/lib/utils';
|
|
772
|
+
|
|
773
|
+
const navItems = [
|
|
774
|
+
{ to: '/', icon: Home, label: 'Home' },
|
|
775
|
+
{ to: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' },
|
|
776
|
+
{ to: '/todos', icon: CheckSquare, label: 'Todos' },
|
|
777
|
+
];
|
|
778
|
+
|
|
779
|
+
export function Sidebar() {
|
|
780
|
+
const { sidebarOpen, toggleSidebar, mobileMenuOpen } = useGlobalKernelState();
|
|
781
|
+
|
|
782
|
+
return (
|
|
783
|
+
<>
|
|
784
|
+
{/* Mobile sidebar */}
|
|
785
|
+
<aside
|
|
786
|
+
className={cn(
|
|
787
|
+
'fixed inset-y-0 left-0 z-50 w-64 transform border-r bg-card transition-transform duration-300 lg:hidden',
|
|
788
|
+
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
|
|
789
|
+
)}
|
|
790
|
+
>
|
|
791
|
+
{/* Slot: sidebar top */}
|
|
792
|
+
<Slot name="sidebar:top" className="border-b p-4" />
|
|
793
|
+
|
|
794
|
+
<div className="flex h-16 items-center justify-between border-b px-6">
|
|
795
|
+
<Link to="/" className="flex items-center gap-2 font-semibold">
|
|
796
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
797
|
+
L
|
|
798
|
+
</div>
|
|
799
|
+
<span>Lego-One</span>
|
|
800
|
+
</Link>
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
{/* Slot: sidebar nav (before nav items) */}
|
|
804
|
+
<Slot name="sidebar:nav" />
|
|
805
|
+
|
|
806
|
+
<nav className="space-y-1 p-4">
|
|
807
|
+
{navItems.map((item) => (
|
|
808
|
+
<NavLink
|
|
809
|
+
key={item.to}
|
|
810
|
+
to={item.to}
|
|
811
|
+
end={item.to === '/'}
|
|
812
|
+
className={({ isActive }) =>
|
|
813
|
+
cn(
|
|
814
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
815
|
+
isActive
|
|
816
|
+
? 'bg-primary text-primary-foreground'
|
|
817
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
>
|
|
821
|
+
<item.icon className="h-5 w-5" />
|
|
822
|
+
{item.label}
|
|
823
|
+
</NavLink>
|
|
824
|
+
))}
|
|
825
|
+
</nav>
|
|
826
|
+
|
|
827
|
+
{/* Slot: sidebar bottom */}
|
|
828
|
+
<Slot name="sidebar:bottom" className="absolute bottom-0 left-0 right-0 border-t p-4" />
|
|
829
|
+
|
|
830
|
+
<div className="absolute bottom-0 left-0 right-0 border-t p-4">
|
|
831
|
+
<NavLink
|
|
832
|
+
to="/settings"
|
|
833
|
+
className={({ isActive }) =>
|
|
834
|
+
cn(
|
|
835
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
836
|
+
isActive
|
|
837
|
+
? 'bg-primary text-primary-foreground'
|
|
838
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
>
|
|
842
|
+
<Settings className="h-5 w-5" />
|
|
843
|
+
Settings
|
|
844
|
+
</NavLink>
|
|
845
|
+
</div>
|
|
846
|
+
</aside>
|
|
847
|
+
|
|
848
|
+
{/* Desktop sidebar */}
|
|
849
|
+
<aside
|
|
850
|
+
className={cn(
|
|
851
|
+
'fixed inset-y-0 left-0 z-30 hidden border-r bg-card transition-all duration-300 lg:block',
|
|
852
|
+
sidebarOpen ? 'w-64' : 'w-16'
|
|
853
|
+
)}
|
|
854
|
+
>
|
|
855
|
+
{/* Slot: sidebar top (desktop) */}
|
|
856
|
+
<Slot name="sidebar:top" className="border-b p-4" />
|
|
857
|
+
|
|
858
|
+
<div className="flex h-16 items-center justify-between border-b px-4">
|
|
859
|
+
{sidebarOpen ? (
|
|
860
|
+
<Link to="/" className="flex items-center gap-2 font-semibold">
|
|
861
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
862
|
+
L
|
|
863
|
+
</div>
|
|
864
|
+
<span>Lego-One</span>
|
|
865
|
+
</Link>
|
|
866
|
+
) : (
|
|
867
|
+
<Link to="/" className="flex items-center justify-center">
|
|
868
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
869
|
+
L
|
|
870
|
+
</div>
|
|
871
|
+
</Link>
|
|
872
|
+
)}
|
|
873
|
+
|
|
874
|
+
<button
|
|
875
|
+
onClick={toggleSidebar}
|
|
876
|
+
className="rounded-lg p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
877
|
+
>
|
|
878
|
+
{sidebarOpen ? (
|
|
879
|
+
<ChevronLeft className="h-5 w-5" />
|
|
880
|
+
) : (
|
|
881
|
+
<ChevronRight className="h-5 w-5" />
|
|
882
|
+
)}
|
|
883
|
+
</button>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
{/* Slot: sidebar nav (desktop) */}
|
|
887
|
+
<Slot name="sidebar:nav" />
|
|
888
|
+
|
|
889
|
+
<nav className="space-y-1 p-2">
|
|
890
|
+
{navItems.map((item) => (
|
|
891
|
+
<NavLink
|
|
892
|
+
key={item.to}
|
|
893
|
+
to={item.to}
|
|
894
|
+
end={item.to === '/'}
|
|
895
|
+
className={({ isActive }) =>
|
|
896
|
+
cn(
|
|
897
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
898
|
+
!sidebarOpen && 'justify-center',
|
|
899
|
+
isActive
|
|
900
|
+
? 'bg-primary text-primary-foreground'
|
|
901
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
902
|
+
)
|
|
903
|
+
}
|
|
904
|
+
title={!sidebarOpen ? item.label : undefined}
|
|
905
|
+
>
|
|
906
|
+
<item.icon className="h-5 w-5 flex-shrink-0" />
|
|
907
|
+
{sidebarOpen && <span>{item.label}</span>}
|
|
908
|
+
</NavLink>
|
|
909
|
+
))}
|
|
910
|
+
</nav>
|
|
911
|
+
|
|
912
|
+
{/* Slot: sidebar bottom (desktop) */}
|
|
913
|
+
<Slot name="sidebar:bottom" className="absolute bottom-0 left-0 right-0 border-t p-2" />
|
|
914
|
+
|
|
915
|
+
<div className="absolute bottom-0 left-0 right-0 border-t p-2">
|
|
916
|
+
<NavLink
|
|
917
|
+
to="/settings"
|
|
918
|
+
className={({ isActive }) =>
|
|
919
|
+
cn(
|
|
920
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
921
|
+
!sidebarOpen && 'justify-center',
|
|
922
|
+
isActive
|
|
923
|
+
? 'bg-primary text-primary-foreground'
|
|
924
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
title={!sidebarOpen ? 'Settings' : undefined}
|
|
928
|
+
>
|
|
929
|
+
<Settings className="h-5 w-5 flex-shrink-0" />
|
|
930
|
+
{sidebarOpen && <span>Settings</span>}
|
|
931
|
+
</NavLink>
|
|
932
|
+
</div>
|
|
933
|
+
</aside>
|
|
934
|
+
</>
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### Step 2: Update topbar with slots
|
|
940
|
+
|
|
941
|
+
**File:** `host/src/layout/Topbar.tsx`
|
|
942
|
+
|
|
943
|
+
```typescript
|
|
944
|
+
import { useGlobalKernelState } from '../kernel/shared-state';
|
|
945
|
+
import { Menu, Bell } from 'lucide-react';
|
|
946
|
+
import { LogoutButton } from '../kernel/auth/components/LogoutButton';
|
|
947
|
+
import { useAuth } from '../kernel/auth/hooks';
|
|
948
|
+
import { OrganizationSelector } from '../kernel/rbac/components/OrganizationSelector';
|
|
949
|
+
import { Slot } from '../kernel/plugins/Slot';
|
|
950
|
+
|
|
951
|
+
export function Topbar() {
|
|
952
|
+
const { toggleMobileMenu } = useGlobalKernelState();
|
|
953
|
+
const { isAuthenticated } = useAuth();
|
|
954
|
+
|
|
955
|
+
return (
|
|
956
|
+
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-6">
|
|
957
|
+
{/* Mobile menu button */}
|
|
958
|
+
<button
|
|
959
|
+
onClick={toggleMobileMenu}
|
|
960
|
+
className="lg:hidden rounded-lg p-2 text-muted-foreground hover:bg-muted"
|
|
961
|
+
>
|
|
962
|
+
<Menu className="h-5 w-5" />
|
|
963
|
+
</button>
|
|
964
|
+
|
|
965
|
+
{/* Slot: topbar left */}
|
|
966
|
+
<Slot name="topbar:left" className="flex items-center gap-2" />
|
|
967
|
+
|
|
968
|
+
{/* Organization selector */}
|
|
969
|
+
{isAuthenticated && <OrganizationSelector />}
|
|
970
|
+
|
|
971
|
+
{/* Slot: topbar center */}
|
|
972
|
+
<Slot name="topbar:center" className="flex-1" />
|
|
973
|
+
|
|
974
|
+
{/* Breadcrumb/spacer */}
|
|
975
|
+
<div className="flex-1" />
|
|
976
|
+
|
|
977
|
+
{/* Slot: topbar right */}
|
|
978
|
+
<Slot name="topbar:right" className="flex items-center gap-2" />
|
|
979
|
+
|
|
980
|
+
{/* Actions */}
|
|
981
|
+
<div className="flex items-center gap-2">
|
|
982
|
+
{isAuthenticated && (
|
|
983
|
+
<button className="rounded-lg p-2 text-muted-foreground hover:bg-muted">
|
|
984
|
+
<Bell className="h-5 w-5" />
|
|
985
|
+
</button>
|
|
986
|
+
)}
|
|
987
|
+
|
|
988
|
+
{isAuthenticated && <LogoutButton />}
|
|
989
|
+
</div>
|
|
990
|
+
</header>
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
---
|
|
996
|
+
|
|
997
|
+
## Task 7: Create Plugin Barrel Export
|
|
998
|
+
|
|
999
|
+
**Files:**
|
|
1000
|
+
- Create: `host/src/kernel/plugins/index.ts`
|
|
1001
|
+
|
|
1002
|
+
### Step 1: Create barrel export
|
|
1003
|
+
|
|
1004
|
+
**File:** `host/src/kernel/plugins/index.ts`
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
export * from './types';
|
|
1008
|
+
export * from './schemas';
|
|
1009
|
+
export * from './store';
|
|
1010
|
+
export * from './loader';
|
|
1011
|
+
export * from './Slot';
|
|
1012
|
+
export * from './SlotProvider';
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## Task 8: Update Bootstrap for Plugin System
|
|
1018
|
+
|
|
1019
|
+
**Files:**
|
|
1020
|
+
- Modify: `host/src/bootstrap.tsx`
|
|
1021
|
+
|
|
1022
|
+
### Step 1: Add SlotProvider to bootstrap
|
|
1023
|
+
|
|
1024
|
+
**File:** `host/src/bootstrap.tsx`
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
import { StrictMode } from 'react';
|
|
1028
|
+
import { createRoot } from 'react-dom/client';
|
|
1029
|
+
import { BrowserRouter } from '@modern-js/runtime/router';
|
|
1030
|
+
import App from './App';
|
|
1031
|
+
import { registerSharedState } from './kernel/shared-state';
|
|
1032
|
+
import { PocketBaseProvider } from './kernel/providers';
|
|
1033
|
+
import { QueryProvider } from './kernel/providers';
|
|
1034
|
+
import { ThemeProvider } from './kernel/providers';
|
|
1035
|
+
import { Toaster } from './kernel/components/ui/toaster';
|
|
1036
|
+
import { ChannelProvider, initializeChannelBus } from './kernel/channels';
|
|
1037
|
+
import { SlotProvider, usePluginStore } from './kernel/plugins';
|
|
1038
|
+
import { pluginLoader, initializePluginLoader } from './kernel/plugins/loader';
|
|
1039
|
+
import Garfish from 'garfish';
|
|
1040
|
+
|
|
1041
|
+
// Register shared state bridge for plugins
|
|
1042
|
+
registerSharedState();
|
|
1043
|
+
|
|
1044
|
+
// Initialize Garfish channel bus
|
|
1045
|
+
initializeChannelBus(Garfish);
|
|
1046
|
+
|
|
1047
|
+
// Initialize plugin loader
|
|
1048
|
+
initializePluginLoader(Garfish);
|
|
1049
|
+
|
|
1050
|
+
// Initialize plugin store from config
|
|
1051
|
+
function initializePlugins() {
|
|
1052
|
+
const store = usePluginStore.getState();
|
|
1053
|
+
store.initializeFromConfig();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Create root
|
|
1057
|
+
const container = document.getElementById('root');
|
|
1058
|
+
if (container) {
|
|
1059
|
+
const root = createRoot(container);
|
|
1060
|
+
|
|
1061
|
+
initializePlugins();
|
|
1062
|
+
|
|
1063
|
+
root.render(
|
|
1064
|
+
<StrictMode>
|
|
1065
|
+
<BrowserRouter>
|
|
1066
|
+
<ThemeProvider>
|
|
1067
|
+
<QueryProvider>
|
|
1068
|
+
<PocketBaseProvider>
|
|
1069
|
+
<SlotProvider>
|
|
1070
|
+
<ChannelProvider />
|
|
1071
|
+
<App />
|
|
1072
|
+
<Toaster />
|
|
1073
|
+
</SlotProvider>
|
|
1074
|
+
</PocketBaseProvider>
|
|
1075
|
+
</QueryProvider>
|
|
1076
|
+
</ThemeProvider>
|
|
1077
|
+
</BrowserRouter>
|
|
1078
|
+
</StrictMode>
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
---
|
|
1084
|
+
|
|
1085
|
+
## Task 9: Update Kernel Exports
|
|
1086
|
+
|
|
1087
|
+
**Files:**
|
|
1088
|
+
- Modify: `host/src/kernel/index.ts`
|
|
1089
|
+
|
|
1090
|
+
### Step 1: Add plugins to exports
|
|
1091
|
+
|
|
1092
|
+
**File:** `host/src/kernel/index.ts`
|
|
1093
|
+
|
|
1094
|
+
```typescript
|
|
1095
|
+
export * from './shared-state';
|
|
1096
|
+
export * from './providers';
|
|
1097
|
+
export * from './lib/utils';
|
|
1098
|
+
export * from './components';
|
|
1099
|
+
export * from './channels';
|
|
1100
|
+
export * from './auth';
|
|
1101
|
+
export * from './rbac';
|
|
1102
|
+
export * from './plugins';
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
---
|
|
1106
|
+
|
|
1107
|
+
## Verification
|
|
1108
|
+
|
|
1109
|
+
### Step 1: Build the host
|
|
1110
|
+
|
|
1111
|
+
**Run:**
|
|
1112
|
+
|
|
1113
|
+
```bash
|
|
1114
|
+
cd host
|
|
1115
|
+
pnpm run build
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
Expected: Build completes without errors.
|
|
1119
|
+
|
|
1120
|
+
### Step 2: Start development server
|
|
1121
|
+
|
|
1122
|
+
**Run:**
|
|
1123
|
+
|
|
1124
|
+
```bash
|
|
1125
|
+
cd host
|
|
1126
|
+
pnpm run dev
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
Expected: Server starts on http://localhost:8080
|
|
1130
|
+
|
|
1131
|
+
### Step 3: Verify plugin system
|
|
1132
|
+
|
|
1133
|
+
1. Login to the app
|
|
1134
|
+
2. Check browser console for plugin initialization messages
|
|
1135
|
+
3. Verify plugin store is initialized (check localStorage)
|
|
1136
|
+
4. Slot components should render without errors (empty until plugins register content)
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
## Summary
|
|
1141
|
+
|
|
1142
|
+
After completing this document, you will have:
|
|
1143
|
+
|
|
1144
|
+
1. ✅ Complete plugin type system with manifests, configs, and slots
|
|
1145
|
+
2. ✅ SaaS config file for enabling/disabling plugins
|
|
1146
|
+
3. ✅ Zustand plugin store for state management
|
|
1147
|
+
4. ✅ Plugin loader service for dynamic plugin loading
|
|
1148
|
+
5. ✅ Slot system with Slot components and provider
|
|
1149
|
+
6. ✅ Updated layout with slot injection points
|
|
1150
|
+
7. ✅ Plugin registry and lifecycle management
|
|
1151
|
+
8. ✅ Foundation for admin UI plugin management
|
|
1152
|
+
|
|
1153
|
+
**Next:** `09-dashboard-plugin.md` - Implement the Dashboard plugin with stats, recent activity, and quick actions.
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1157
|
+
## Files Created
|
|
1158
|
+
|
|
1159
|
+
```
|
|
1160
|
+
host/
|
|
1161
|
+
└── src/
|
|
1162
|
+
├── kernel/
|
|
1163
|
+
│ └── plugins/
|
|
1164
|
+
│ ├── types.ts
|
|
1165
|
+
│ ├── schemas.ts
|
|
1166
|
+
│ ├── store.ts
|
|
1167
|
+
│ ├── loader.ts
|
|
1168
|
+
│ ├── Slot.tsx
|
|
1169
|
+
│ ├── SlotProvider.tsx
|
|
1170
|
+
│ └── index.ts
|
|
1171
|
+
├── saas.config.ts
|
|
1172
|
+
└── bootstrap.tsx (modified)
|
|
1173
|
+
|
|
1174
|
+
layout/
|
|
1175
|
+
├── Sidebar.tsx (modified)
|
|
1176
|
+
└── Topbar.tsx (modified)
|
|
1177
|
+
|
|
1178
|
+
kernel/index.ts (modified)
|
|
1179
|
+
```
|