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,671 @@
|
|
|
1
|
+
# Slot Injection Pattern: UI Extension System
|
|
2
|
+
|
|
3
|
+
**Project:** Lego-One (Modern.js SaaS OS)
|
|
4
|
+
**Document:** 05 - Slot Injection Pattern
|
|
5
|
+
**Status:** Research Phase
|
|
6
|
+
|
|
7
|
+
## Executive Summary
|
|
8
|
+
|
|
9
|
+
This document explains the **Slot Injection Pattern** - how plugins can inject UI components into the Host's layout (Sidebar, Topbar) without modifying the Host's source code. This enables plugins to extend the host UI in a controlled, declarative way.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. The Slot Injection Problem
|
|
14
|
+
|
|
15
|
+
### 1.1 Use Case Example
|
|
16
|
+
|
|
17
|
+
**Scenario:** The "Stripe Plugin" needs to:
|
|
18
|
+
1. Add a "Billing" link to the Host's Sidebar navigation
|
|
19
|
+
2. Show a "Credit Balance" badge in the Host's Topbar
|
|
20
|
+
3. Display a subscription warning banner when credits are low
|
|
21
|
+
|
|
22
|
+
**Challenge:** The plugin should be able to do this **without**:
|
|
23
|
+
- Directly importing/modifying Host components
|
|
24
|
+
- Creating tight coupling between plugin and host
|
|
25
|
+
- Breaking when the Host layout changes
|
|
26
|
+
|
|
27
|
+
### 1.2 Solution: Slot Registry Pattern
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
31
|
+
│ Slot Injection Flow │
|
|
32
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
33
|
+
│ │
|
|
34
|
+
│ 1. PLUGIN INITIALIZATION │
|
|
35
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
36
|
+
│ │ Plugin reads its plugin.config.ts │ │
|
|
37
|
+
│ │ - Defines which slots to inject │ │
|
|
38
|
+
│ │ - Provides component for each slot │ │
|
|
39
|
+
│ │ - Specifies order and visibility rules │ │
|
|
40
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
41
|
+
│ │ │
|
|
42
|
+
│ ▼ │
|
|
43
|
+
│ 2. SLOT REGISTRATION │
|
|
44
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
45
|
+
│ │ Plugin calls window.__LEGO_SLOT_REGISTRY__.register() │ │
|
|
46
|
+
│ │ - Registers component with global registry │ │
|
|
47
|
+
│ │ - Stores slot name, component, order, visibility │ │
|
|
48
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
49
|
+
│ │ │
|
|
50
|
+
│ ▼ │
|
|
51
|
+
│ 3. HOST RENDERING │
|
|
52
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
53
|
+
│ │ Host's Sidebar/Topbar components query the registry │ │
|
|
54
|
+
│ │ - Get all items for a specific slot │ │
|
|
55
|
+
│ │ - Sort by order │ │
|
|
56
|
+
│ │ - Filter by visibility │ │
|
|
57
|
+
│ │ - Render each component │ │
|
|
58
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
59
|
+
│ │ │
|
|
60
|
+
│ ▼ │
|
|
61
|
+
│ 4. RESULT │
|
|
62
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
63
|
+
│ │ Plugin UI appears in host layout seamlessly │ │
|
|
64
|
+
│ │ - No host code modified │ │
|
|
65
|
+
│ │ - Multiple plugins can inject to same slot │ │
|
|
66
|
+
│ │ - Plugins control their own UI │ │
|
|
67
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
68
|
+
│ │
|
|
69
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 2. Available Slots
|
|
75
|
+
|
|
76
|
+
### 2.1 Standard Slot Locations
|
|
77
|
+
|
|
78
|
+
| Slot Name | Location | Description |
|
|
79
|
+
|-----------|----------|-------------|
|
|
80
|
+
| `sidebar:nav` | Sidebar navigation list | Navigation links |
|
|
81
|
+
| `sidebar:header` | Sidebar top section | Branding, logo |
|
|
82
|
+
| `sidebar:footer` | Sidebar bottom section | Version info, help |
|
|
83
|
+
| `topbar:actions` | Topbar right side | Action buttons |
|
|
84
|
+
| `topbar:menu` | Topbar dropdown menu | Menu items |
|
|
85
|
+
| `topbar:search` | Topbar search area | Search components |
|
|
86
|
+
| `dashboard:widgets` | Dashboard grid | Dashboard widgets |
|
|
87
|
+
| `layout:banner` | Above main content | Notification banners |
|
|
88
|
+
|
|
89
|
+
### 2.2 Slot Item Interface
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
interface SlotItem {
|
|
93
|
+
// Unique identifier for this slot item
|
|
94
|
+
id: string;
|
|
95
|
+
|
|
96
|
+
// Plugin name (for debugging/unregistration)
|
|
97
|
+
pluginName: string;
|
|
98
|
+
|
|
99
|
+
// React component to render
|
|
100
|
+
component: React.ComponentType;
|
|
101
|
+
|
|
102
|
+
// Sort order (lower = higher priority)
|
|
103
|
+
order: number;
|
|
104
|
+
|
|
105
|
+
// Visibility check function
|
|
106
|
+
isVisible: () => boolean;
|
|
107
|
+
|
|
108
|
+
// Cleanup function (called on unregister)
|
|
109
|
+
onUnmount?: () => void;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 3. Implementation
|
|
116
|
+
|
|
117
|
+
### 3.1 Host: Slot Registry
|
|
118
|
+
|
|
119
|
+
**File:** `host/src/kernel/slot-registry.ts`
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { reactive } from '@modern-js/runtime';
|
|
123
|
+
|
|
124
|
+
export type SlotName =
|
|
125
|
+
| 'sidebar:nav'
|
|
126
|
+
| 'sidebar:header'
|
|
127
|
+
| 'sidebar:footer'
|
|
128
|
+
| 'topbar:actions'
|
|
129
|
+
| 'topbar:menu'
|
|
130
|
+
| 'topbar:search'
|
|
131
|
+
| 'dashboard:widgets'
|
|
132
|
+
| 'layout:banner';
|
|
133
|
+
|
|
134
|
+
export interface SlotItem {
|
|
135
|
+
id: string;
|
|
136
|
+
pluginName: string;
|
|
137
|
+
component: React.ComponentType;
|
|
138
|
+
order: number;
|
|
139
|
+
isVisible: () => boolean;
|
|
140
|
+
onUnmount?: () => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface SlotRegistry {
|
|
144
|
+
slots: Record<SlotName, SlotItem[]>;
|
|
145
|
+
register: (slot: SlotName, item: Omit<SlotItem, 'id'>) => string;
|
|
146
|
+
unregister: (slot: SlotName, id: string) => void;
|
|
147
|
+
getItems: (slot: SlotName) => SlotItem[];
|
|
148
|
+
clearPlugin: (pluginName: string) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Global singleton for slot registration
|
|
152
|
+
export const slotRegistry: SlotRegistry = reactive({
|
|
153
|
+
slots: {
|
|
154
|
+
'sidebar:nav': [],
|
|
155
|
+
'sidebar:header': [],
|
|
156
|
+
'sidebar:footer': [],
|
|
157
|
+
'topbar:actions': [],
|
|
158
|
+
'topbar:menu': [],
|
|
159
|
+
'topbar:search': [],
|
|
160
|
+
'dashboard:widgets': [],
|
|
161
|
+
'layout:banner': [],
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
register(slot: SlotName, item: Omit<SlotItem, 'id'>) {
|
|
165
|
+
const id = `${slot}-${item.pluginName}-${Date.now()}`;
|
|
166
|
+
const slotItem: SlotItem = { ...item, id };
|
|
167
|
+
|
|
168
|
+
if (!this.slots[slot]) {
|
|
169
|
+
this.slots[slot] = [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.slots[slot].push(slotItem);
|
|
173
|
+
|
|
174
|
+
// Sort by order (lower first)
|
|
175
|
+
this.slots[slot].sort((a, b) => a.order - b.order);
|
|
176
|
+
|
|
177
|
+
console.log(`[SlotRegistry] Registered "${id}" to "${slot}"`);
|
|
178
|
+
return id;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
unregister(slot: SlotName, id: string) {
|
|
182
|
+
const items = this.slots[slot];
|
|
183
|
+
if (!items) return;
|
|
184
|
+
|
|
185
|
+
const item = items.find((i) => i.id === id);
|
|
186
|
+
if (item?.onUnmount) {
|
|
187
|
+
item.onUnmount();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.slots[slot] = items.filter((i) => i.id !== id);
|
|
191
|
+
console.log(`[SlotRegistry] Unregistered "${id}" from "${slot}"`);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
getItems(slot: SlotName) {
|
|
195
|
+
const items = this.slots[slot] || [];
|
|
196
|
+
// Filter by visibility
|
|
197
|
+
return items.filter((item) => item.isVisible());
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
clearPlugin(pluginName: string) {
|
|
201
|
+
for (const slot of Object.keys(this.slots) as SlotName[]) {
|
|
202
|
+
const items = this.slots[slot];
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
if (item.pluginName === pluginName) {
|
|
205
|
+
this.unregister(slot, item.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Register to window for plugin access
|
|
213
|
+
export function initSlotRegistry() {
|
|
214
|
+
(window as any).__LEGO_SLOT_REGISTRY__ = slotRegistry;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 3.2 Host: Initialize Registry
|
|
219
|
+
|
|
220
|
+
**File:** `host/src/bootstrap.tsx`
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { initSlotRegistry } from './kernel/slot-registry';
|
|
224
|
+
import { registerSharedState } from './kernel/shared-state-bridge';
|
|
225
|
+
|
|
226
|
+
// Initialize systems before app starts
|
|
227
|
+
initSlotRegistry();
|
|
228
|
+
registerSharedState();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 3.3 Host: Sidebar Component
|
|
232
|
+
|
|
233
|
+
**File:** `host/src/layout/Sidebar.tsx`
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { Link, useLocation } from '@modern-js/runtime/router';
|
|
237
|
+
import { cn } from '@/lib/utils';
|
|
238
|
+
import { slotRegistry } from '@/kernel/slot-registry';
|
|
239
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
240
|
+
import { Separator } from '@/components/ui/separator';
|
|
241
|
+
|
|
242
|
+
export function Sidebar() {
|
|
243
|
+
const location = useLocation();
|
|
244
|
+
|
|
245
|
+
// Get registered navigation items
|
|
246
|
+
const navItems = slotRegistry.getItems('sidebar:nav');
|
|
247
|
+
const headerItems = slotRegistry.getItems('sidebar:header');
|
|
248
|
+
const footerItems = slotRegistry.getItems('sidebar:footer');
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div className="flex flex-col h-full border-r bg-muted/40 w-64">
|
|
252
|
+
{/* Header Slot */}
|
|
253
|
+
{headerItems.length > 0 && (
|
|
254
|
+
<div className="p-4 space-y-2">
|
|
255
|
+
{headerItems.map((item) => (
|
|
256
|
+
<item.component key={item.id} />
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
<Separator />
|
|
262
|
+
|
|
263
|
+
{/* Navigation */}
|
|
264
|
+
<ScrollArea className="flex-1 px-3 py-2">
|
|
265
|
+
<div className="space-y-1">
|
|
266
|
+
{/* Default nav items */}
|
|
267
|
+
<NavLink to="/dashboard" icon="Layout">
|
|
268
|
+
Dashboard
|
|
269
|
+
</NavLink>
|
|
270
|
+
<NavLink to="/apps" icon="Grid3x3">
|
|
271
|
+
Apps
|
|
272
|
+
</NavLink>
|
|
273
|
+
<NavLink to="/settings" icon="Settings">
|
|
274
|
+
Settings
|
|
275
|
+
</NavLink>
|
|
276
|
+
|
|
277
|
+
{/* Plugin-injected nav items */}
|
|
278
|
+
{navItems.map((item) => (
|
|
279
|
+
<item.component key={item.id} />
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
</ScrollArea>
|
|
283
|
+
|
|
284
|
+
<Separator />
|
|
285
|
+
|
|
286
|
+
{/* Footer Slot */}
|
|
287
|
+
{footerItems.length > 0 && (
|
|
288
|
+
<div className="p-4 space-y-2">
|
|
289
|
+
{footerItems.map((item) => (
|
|
290
|
+
<item.component key={item.id} />
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function NavLink({
|
|
299
|
+
to,
|
|
300
|
+
icon,
|
|
301
|
+
children,
|
|
302
|
+
}: {
|
|
303
|
+
to: string;
|
|
304
|
+
icon: string;
|
|
305
|
+
children: React.ReactNode;
|
|
306
|
+
}) {
|
|
307
|
+
const location = useLocation();
|
|
308
|
+
const isActive = location.pathname === to || location.pathname.startsWith(to + '/');
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<Link
|
|
312
|
+
to={to}
|
|
313
|
+
className={cn(
|
|
314
|
+
'flex items-center gap-3 px-3 py-2 text-sm rounded-md transition-colors',
|
|
315
|
+
isActive
|
|
316
|
+
? 'bg-primary text-primary-foreground'
|
|
317
|
+
: 'hover:bg-accent hover:text-accent-foreground'
|
|
318
|
+
)}
|
|
319
|
+
>
|
|
320
|
+
{/* Icon would be rendered here */}
|
|
321
|
+
<span>{icon}</span>
|
|
322
|
+
<span>{children}</span>
|
|
323
|
+
</Link>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 3.4 Host: Topbar Component
|
|
329
|
+
|
|
330
|
+
**File:** `host/src/layout/Topbar.tsx`
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { slotRegistry } from '@/kernel/slot-registry';
|
|
334
|
+
import { Button } from '@/components/ui/button';
|
|
335
|
+
import {
|
|
336
|
+
DropdownMenu,
|
|
337
|
+
DropdownMenuContent,
|
|
338
|
+
DropdownMenuItem,
|
|
339
|
+
DropdownMenuTrigger,
|
|
340
|
+
} from '@/components/ui/dropdown-menu';
|
|
341
|
+
import { MoreVertical } from 'lucide-react';
|
|
342
|
+
|
|
343
|
+
export function Topbar() {
|
|
344
|
+
// Get registered topbar items
|
|
345
|
+
const actionItems = slotRegistry.getItems('topbar:actions');
|
|
346
|
+
const menuItems = slotRegistry.getItems('topbar:menu');
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<header className="h-14 border-b flex items-center justify-between px-6">
|
|
350
|
+
<div className="flex items-center gap-4">
|
|
351
|
+
{/* Breadcrumb or page title would go here */}
|
|
352
|
+
<h1 className="font-semibold">Lego-One</h1>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div className="flex items-center gap-2">
|
|
356
|
+
{/* Plugin-injected action buttons */}
|
|
357
|
+
{actionItems.map((item) => (
|
|
358
|
+
<item.component key={item.id} />
|
|
359
|
+
))}
|
|
360
|
+
|
|
361
|
+
{/* User menu */}
|
|
362
|
+
<DropdownMenu>
|
|
363
|
+
<DropdownMenuTrigger asChild>
|
|
364
|
+
<Button variant="ghost" size="icon">
|
|
365
|
+
<MoreVertical className="h-4 w-4" />
|
|
366
|
+
</Button>
|
|
367
|
+
</DropdownMenuTrigger>
|
|
368
|
+
<DropdownMenuContent align="end">
|
|
369
|
+
<DropdownMenuItem>Profile</DropdownMenuItem>
|
|
370
|
+
<DropdownMenuItem>Settings</DropdownMenuItem>
|
|
371
|
+
|
|
372
|
+
{/* Plugin-injected menu items */}
|
|
373
|
+
{menuItems.map((item) => (
|
|
374
|
+
<item.component key={item.id} />
|
|
375
|
+
))}
|
|
376
|
+
|
|
377
|
+
<DropdownMenuItem>Logout</DropdownMenuItem>
|
|
378
|
+
</DropdownMenuContent>
|
|
379
|
+
</DropdownMenu>
|
|
380
|
+
</div>
|
|
381
|
+
</header>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 4. Plugin: Slot Injection
|
|
389
|
+
|
|
390
|
+
### 4.1 Plugin Config: Define Slots
|
|
391
|
+
|
|
392
|
+
**File:** `packages/plugins/@lego/plugin-stripe/plugin.config.ts`
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { definePluginConfig } from '@lego/kernel/plugin-config';
|
|
396
|
+
|
|
397
|
+
export default definePluginConfig({
|
|
398
|
+
name: '@lego/plugin-stripe',
|
|
399
|
+
version: '1.0.0',
|
|
400
|
+
displayName: 'Stripe Integration',
|
|
401
|
+
description: 'Payment processing via Stripe',
|
|
402
|
+
|
|
403
|
+
// Define slot injections
|
|
404
|
+
slots: {
|
|
405
|
+
'sidebar:nav': {
|
|
406
|
+
component: './src/components/slots/SidebarBillingLink',
|
|
407
|
+
order: 50,
|
|
408
|
+
isVisible: () => {
|
|
409
|
+
const state = (window as any).__LEGO_KERNEL_STATE__;
|
|
410
|
+
const user = state?.useGlobalKernelState.getState().currentUser;
|
|
411
|
+
return user?.role === 'admin' || user?.role === 'billing';
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
'topbar:actions': {
|
|
416
|
+
component: './src/components/slots/CreditBalanceBadge',
|
|
417
|
+
order: 10,
|
|
418
|
+
isVisible: () => true, // Always visible
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
'topbar:menu': {
|
|
422
|
+
component: './src/components/slots/SubscriptionMenuItem',
|
|
423
|
+
order: 100,
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 4.2 Plugin: Sidebar Link Component
|
|
430
|
+
|
|
431
|
+
**File:** `packages/plugins/@lego/plugin-stripe/src/components/slots/SidebarBillingLink.tsx`
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
import { Link } from '@modern-js/runtime/router';
|
|
435
|
+
import { CreditCard } from 'lucide-react';
|
|
436
|
+
import { cn } from '@/lib/utils';
|
|
437
|
+
|
|
438
|
+
export function SidebarBillingLink() {
|
|
439
|
+
return (
|
|
440
|
+
<Link
|
|
441
|
+
to="/billing"
|
|
442
|
+
className={cn(
|
|
443
|
+
'flex items-center gap-3 px-3 py-2 text-sm rounded-md',
|
|
444
|
+
'transition-colors hover:bg-accent hover:text-accent-foreground'
|
|
445
|
+
)}
|
|
446
|
+
>
|
|
447
|
+
<CreditCard className="h-4 w-4" />
|
|
448
|
+
<span>Billing</span>
|
|
449
|
+
</Link>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export default SidebarBillingLink;
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### 4.3 Plugin: Credit Badge Component
|
|
457
|
+
|
|
458
|
+
**File:** `packages/plugins/@lego/plugin-stripe/src/components/slots/CreditBalanceBadge.tsx`
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { useQuery } from '@tanstack/react-query';
|
|
462
|
+
import { Badge } from '@/components/ui/badge';
|
|
463
|
+
import { CreditCard } from 'lucide-react';
|
|
464
|
+
|
|
465
|
+
export function CreditBalanceBadge() {
|
|
466
|
+
const { data: balance } = useQuery({
|
|
467
|
+
queryKey: ['stripe', 'balance'],
|
|
468
|
+
queryFn: async () => {
|
|
469
|
+
const res = await fetch('/api/stripe/balance');
|
|
470
|
+
return res.json();
|
|
471
|
+
},
|
|
472
|
+
refetchInterval: 60000, // Refresh every minute
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const credits = balance?.credits || 0;
|
|
476
|
+
const isLow = credits < 10;
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<Badge
|
|
480
|
+
variant={isLow ? 'destructive' : 'secondary'}
|
|
481
|
+
className="flex items-center gap-1"
|
|
482
|
+
>
|
|
483
|
+
<CreditCard className="h-3 w-3" />
|
|
484
|
+
<span>{credits} credits</span>
|
|
485
|
+
</Badge>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export default CreditBalanceBadge;
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 4.4 Plugin: Menu Item Component
|
|
493
|
+
|
|
494
|
+
**File:** `packages/plugins/@lego/plugin-stripe/src/components/slots/SubscriptionMenuItem.tsx`
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
|
498
|
+
import { useNavigate } from '@modern-js/runtime/router';
|
|
499
|
+
|
|
500
|
+
export function SubscriptionMenuItem() {
|
|
501
|
+
const navigate = useNavigate();
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<DropdownMenuItem onClick={() => navigate('/subscription')}>
|
|
505
|
+
Manage Subscription
|
|
506
|
+
</DropdownMenuItem>
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export default SubscriptionMenuItem;
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### 4.5 Plugin: Register Slots on Load
|
|
514
|
+
|
|
515
|
+
**File:** `packages/plugins/@lego/plugin-stripe/src/App.tsx`
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { useEffect } from 'react';
|
|
519
|
+
import { BrowserRouter, Routes, Route } from '@modern-js/runtime/router';
|
|
520
|
+
import pluginConfig from '../../plugin.config';
|
|
521
|
+
import { registerPluginSlots } from './lib/slots';
|
|
522
|
+
|
|
523
|
+
export default function PluginApp({ basename }: { basename: string }) {
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
// Register all slots defined in plugin.config.ts
|
|
526
|
+
registerPluginSlots(pluginConfig.slots, '@lego/plugin-stripe');
|
|
527
|
+
}, []);
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<BrowserRouter basename={basename}>
|
|
531
|
+
<Routes>
|
|
532
|
+
{/* Plugin routes */}
|
|
533
|
+
</Routes>
|
|
534
|
+
</BrowserRouter>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### 4.6 Plugin: Slot Registration Helper
|
|
540
|
+
|
|
541
|
+
**File:** `packages/plugins/@lego/plugin-stripe/src/lib/slots.ts`
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
interface SlotConfig {
|
|
545
|
+
component: string;
|
|
546
|
+
order: number;
|
|
547
|
+
isVisible?: () => boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
interface SlotsConfig {
|
|
551
|
+
[slotName: string]: SlotConfig;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function registerPluginSlots(
|
|
555
|
+
slots: SlotsConfig,
|
|
556
|
+
pluginName: string
|
|
557
|
+
) {
|
|
558
|
+
const registry = (window as any).__LEGO_SLOT_REGISTRY__;
|
|
559
|
+
|
|
560
|
+
if (!registry) {
|
|
561
|
+
console.warn('[Slots] Registry not found. Host not initialized?');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (const [slotName, slotConfig] of Object.entries(slots)) {
|
|
566
|
+
try {
|
|
567
|
+
const module = await import(/* @vite-ignore */ slotConfig.component);
|
|
568
|
+
const Component = module.default;
|
|
569
|
+
|
|
570
|
+
registry.register(slotName, {
|
|
571
|
+
pluginName,
|
|
572
|
+
component: Component,
|
|
573
|
+
order: slotConfig.order,
|
|
574
|
+
isVisible: slotConfig.isVisible || (() => true),
|
|
575
|
+
onUnmount: () => {
|
|
576
|
+
console.log(`[Slots] Unmounted ${slotName} for ${pluginName}`);
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error(`[Slots] Failed to register "${slotName}":`, error);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## 5. Advanced Patterns
|
|
589
|
+
|
|
590
|
+
### 5.1 Dynamic Slot Items
|
|
591
|
+
|
|
592
|
+
Slots can be registered/unregistered dynamically:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// Show a notification banner only when needed
|
|
596
|
+
function showLowCreditsBanner() {
|
|
597
|
+
const registry = (window as any).__LEGO_SLOT_REGISTRY__;
|
|
598
|
+
|
|
599
|
+
const id = registry.register('layout:banner', {
|
|
600
|
+
pluginName: '@lego/plugin-stripe',
|
|
601
|
+
component: LowCreditsBanner,
|
|
602
|
+
order: 1,
|
|
603
|
+
isVisible: () => true,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Unregister after user dismisses
|
|
607
|
+
return () => registry.unregister('layout:banner', id);
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### 5.2 Slot Data Passing
|
|
612
|
+
|
|
613
|
+
Pass data from host to slot components via context:
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// Host provides context
|
|
617
|
+
<SlotDataProvider value={{ currentUser, theme }}>
|
|
618
|
+
<Sidebar />
|
|
619
|
+
</SlotDataProvider>
|
|
620
|
+
|
|
621
|
+
// Slot component consumes context
|
|
622
|
+
function SidebarBillingLink() {
|
|
623
|
+
const { currentUser } = useSlotDataContext();
|
|
624
|
+
// ...
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 5.3 Conditional Rendering
|
|
629
|
+
|
|
630
|
+
Use visibility rules for complex conditions:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
isVisible: () => {
|
|
634
|
+
const state = window.__LEGO_KERNEL_STATE__;
|
|
635
|
+
const user = state?.useGlobalKernelState.getState().currentUser;
|
|
636
|
+
|
|
637
|
+
// Only show for admins with active subscription
|
|
638
|
+
return (
|
|
639
|
+
user?.role === 'admin' &&
|
|
640
|
+
user?.subscription?.status === 'active'
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## 6. Slot Best Practices
|
|
648
|
+
|
|
649
|
+
| Practice | Description |
|
|
650
|
+
|----------|-------------|
|
|
651
|
+
| **Use Semantic Names** | `sidebar:nav` not `sidebar:left` |
|
|
652
|
+
| **Order Predictably** | Use increments of 10 for easy reordering |
|
|
653
|
+
| **Cleanup Resources** | Use `onUnmount` for subscriptions |
|
|
654
|
+
| **Handle Missing Registry** | Gracefully degrade if host not ready |
|
|
655
|
+
| **Type Safety** | Export slot component types |
|
|
656
|
+
| **Avoid Side Effects** | Slot components should be pure |
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## 7. Next Steps
|
|
661
|
+
|
|
662
|
+
1. **`06-cli-strategy.md`**: CLI tool for project scaffolding
|
|
663
|
+
2. **`07-deployment.md`**: Deploy host and plugins to production
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## References
|
|
668
|
+
|
|
669
|
+
- [Radix UI Primitives](https://www.radix-ui.com/)
|
|
670
|
+
- [TanStack Query](https://tanstack.com/query/latest)
|
|
671
|
+
- [Modern.js Runtime](https://modernjs.dev/guides/topic-detail/runtime/)
|