create-lego-one 2.0.12 → 2.0.14

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.
Files changed (78) hide show
  1. package/dist/index.cjs +150 -15
  2. package/dist/index.cjs.map +1 -1
  3. package/package.json +1 -1
  4. package/template/.cursor/rules/rules.mdc +639 -0
  5. package/template/.dockerignore +58 -0
  6. package/template/.env.example +18 -0
  7. package/template/.eslintignore +5 -0
  8. package/template/.eslintrc.js +28 -0
  9. package/template/.prettierignore +6 -0
  10. package/template/.prettierrc +11 -0
  11. package/template/CLAUDE.md +634 -0
  12. package/template/Dockerfile +67 -0
  13. package/template/PROMPT.md +457 -0
  14. package/template/README.md +325 -0
  15. package/template/docker-compose.yml +48 -0
  16. package/template/docker-entrypoint.sh +23 -0
  17. package/template/docs/checkpoints/.template.md +64 -0
  18. package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
  19. package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
  20. package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
  21. package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
  22. package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
  23. package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
  24. package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
  25. package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
  26. package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
  27. package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
  28. package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
  29. package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
  30. package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
  31. package/template/docs/framework/plans/00-index.md +164 -0
  32. package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
  33. package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
  34. package/template/docs/framework/plans/03-host-kernel.md +1518 -0
  35. package/template/docs/framework/plans/04-auth-system.md +1466 -0
  36. package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
  37. package/template/docs/framework/plans/06-ui-components.md +1478 -0
  38. package/template/docs/framework/plans/07-communication-system.md +1106 -0
  39. package/template/docs/framework/plans/08-plugin-system.md +1179 -0
  40. package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
  41. package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
  42. package/template/docs/framework/plans/11-testing.md +935 -0
  43. package/template/docs/framework/plans/12-deployment.md +896 -0
  44. package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
  45. package/template/docs/framework/research/00-modernjs-audit.md +488 -0
  46. package/template/docs/framework/research/01-system-blueprint.md +721 -0
  47. package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
  48. package/template/docs/framework/research/03-host-setup.md +714 -0
  49. package/template/docs/framework/research/04-plugin-architecture.md +645 -0
  50. package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
  51. package/template/docs/framework/research/06-cli-strategy.md +615 -0
  52. package/template/docs/framework/research/07-deployment.md +629 -0
  53. package/template/docs/framework/research/README.md +282 -0
  54. package/template/docs/framework/setup/00-index.md +210 -0
  55. package/template/docs/framework/setup/01-framework-structure.md +308 -0
  56. package/template/docs/framework/setup/02-development-workflow.md +405 -0
  57. package/template/docs/framework/setup/03-environment-setup.md +215 -0
  58. package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
  59. package/template/docs/framework/setup/05-plugin-system.md +620 -0
  60. package/template/docs/framework/setup/06-communication-patterns.md +451 -0
  61. package/template/docs/framework/setup/07-plugin-development.md +582 -0
  62. package/template/docs/framework/setup/08-component-library.md +658 -0
  63. package/template/docs/framework/setup/09-data-integration.md +609 -0
  64. package/template/docs/framework/setup/10-auth-rbac.md +497 -0
  65. package/template/docs/framework/setup/11-hooks-api.md +393 -0
  66. package/template/docs/framework/setup/12-components-api.md +665 -0
  67. package/template/docs/framework/setup/13-deployment-guide.md +566 -0
  68. package/template/docs/framework/setup/README.md +548 -0
  69. package/template/host/package.json +1 -1
  70. package/template/nginx.conf +72 -0
  71. package/template/package.json +1 -1
  72. package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
  73. package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
  74. package/template/pocketbase/CHANGELOG.md +911 -0
  75. package/template/pocketbase/LICENSE.md +17 -0
  76. package/template/scripts/create-plugin.js +221 -0
  77. package/template/scripts/deploy.sh +56 -0
  78. 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
+ ```