create-lego-one 2.0.10 → 2.0.12

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 (171) hide show
  1. package/dist/index.cjs +145 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/package.json +5 -3
  4. package/template/host/e2e/auth.spec.ts +38 -0
  5. package/template/host/e2e/layout.spec.ts +38 -0
  6. package/template/host/modern.config.ts +19 -0
  7. package/template/host/package.json +71 -0
  8. package/template/host/playwright.config.ts +34 -0
  9. package/template/host/postcss.config.mjs +6 -0
  10. package/template/host/src/App.tsx +6 -0
  11. package/template/host/src/bootstrap.tsx +74 -0
  12. package/template/host/src/global.css +59 -0
  13. package/template/host/src/index.ts +2 -0
  14. package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
  15. package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
  16. package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
  17. package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
  18. package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
  19. package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
  20. package/template/host/src/kernel/auth/hooks.ts +174 -0
  21. package/template/host/src/kernel/auth/index.ts +5 -0
  22. package/template/host/src/kernel/auth/schemas.ts +27 -0
  23. package/template/host/src/kernel/auth/service.ts +197 -0
  24. package/template/host/src/kernel/auth/types.ts +36 -0
  25. package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
  26. package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
  27. package/template/host/src/kernel/channels/events.ts +27 -0
  28. package/template/host/src/kernel/channels/hooks.ts +168 -0
  29. package/template/host/src/kernel/channels/index.ts +6 -0
  30. package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
  31. package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
  32. package/template/host/src/kernel/channels/types.ts +112 -0
  33. package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
  34. package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
  35. package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
  36. package/template/host/src/kernel/components/index.ts +32 -0
  37. package/template/host/src/kernel/components/ui/alert.tsx +58 -0
  38. package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
  39. package/template/host/src/kernel/components/ui/badge.tsx +35 -0
  40. package/template/host/src/kernel/components/ui/button.tsx +50 -0
  41. package/template/host/src/kernel/components/ui/card.tsx +78 -0
  42. package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
  43. package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
  44. package/template/host/src/kernel/components/ui/index.ts +7 -0
  45. package/template/host/src/kernel/components/ui/input.tsx +24 -0
  46. package/template/host/src/kernel/components/ui/label.tsx +21 -0
  47. package/template/host/src/kernel/components/ui/popover.tsx +28 -0
  48. package/template/host/src/kernel/components/ui/progress.tsx +25 -0
  49. package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
  50. package/template/host/src/kernel/components/ui/select.tsx +155 -0
  51. package/template/host/src/kernel/components/ui/separator.tsx +28 -0
  52. package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
  53. package/template/host/src/kernel/components/ui/switch.tsx +26 -0
  54. package/template/host/src/kernel/components/ui/table.tsx +116 -0
  55. package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
  56. package/template/host/src/kernel/components/ui/toast.tsx +126 -0
  57. package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
  58. package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
  59. package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
  60. package/template/host/src/kernel/index.ts +48 -0
  61. package/template/host/src/kernel/lib/cn.ts +1 -0
  62. package/template/host/src/kernel/lib/utils.ts +36 -0
  63. package/template/host/src/kernel/plugins/Slot.tsx +41 -0
  64. package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
  65. package/template/host/src/kernel/plugins/index.ts +23 -0
  66. package/template/host/src/kernel/plugins/loader.ts +122 -0
  67. package/template/host/src/kernel/plugins/schemas.ts +54 -0
  68. package/template/host/src/kernel/plugins/store.ts +185 -0
  69. package/template/host/src/kernel/plugins/types.ts +103 -0
  70. package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
  71. package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
  72. package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
  73. package/template/host/src/kernel/providers/index.ts +3 -0
  74. package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
  75. package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
  76. package/template/host/src/kernel/rbac/hooks.ts +379 -0
  77. package/template/host/src/kernel/rbac/index.ts +6 -0
  78. package/template/host/src/kernel/rbac/service.ts +504 -0
  79. package/template/host/src/kernel/rbac/types.ts +164 -0
  80. package/template/host/src/kernel/rbac/utils.ts +34 -0
  81. package/template/host/src/kernel/shared-state/bridge.ts +31 -0
  82. package/template/host/src/kernel/shared-state/index.ts +3 -0
  83. package/template/host/src/kernel/shared-state/store.ts +62 -0
  84. package/template/host/src/kernel/shared-state/types.ts +60 -0
  85. package/template/host/src/kernel/use-migrations.ts +72 -0
  86. package/template/host/src/layout/MobileMenu.tsx +61 -0
  87. package/template/host/src/layout/Shell.tsx +42 -0
  88. package/template/host/src/layout/Sidebar.tsx +178 -0
  89. package/template/host/src/layout/Topbar.tsx +50 -0
  90. package/template/host/src/layout/index.ts +4 -0
  91. package/template/host/src/lib/pocketbase/client.ts +38 -0
  92. package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
  93. package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
  94. package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
  95. package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
  96. package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
  97. package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
  98. package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
  99. package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
  100. package/template/host/src/lib/pocketbase/index.ts +5 -0
  101. package/template/host/src/lib/pocketbase/migrations.ts +44 -0
  102. package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
  103. package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
  104. package/template/host/src/lib/pocketbase/seed.ts +113 -0
  105. package/template/host/src/lib/pocketbase/types.ts +102 -0
  106. package/template/host/src/modern.runtime.ts +26 -0
  107. package/template/host/src/plugins.d.ts +9 -0
  108. package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
  109. package/template/host/src/routes/_.tsx +6 -0
  110. package/template/host/src/routes/dashboard._.tsx +41 -0
  111. package/template/host/src/routes/index.tsx +93 -0
  112. package/template/host/src/routes/login.tsx +36 -0
  113. package/template/host/src/saas.config.ts +52 -0
  114. package/template/host/src/test/setup.ts +65 -0
  115. package/template/host/src/test/utils.tsx +69 -0
  116. package/template/host/src/test/vitest-globals.d.ts +19 -0
  117. package/template/host/src/vite-env.d.ts +16 -0
  118. package/template/host/tailwind.config.ts +77 -0
  119. package/template/host/tsconfig.json +19 -0
  120. package/template/host/vitest.config.ts +30 -0
  121. package/template/package.json +44 -0
  122. package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
  123. package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
  124. package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
  125. package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
  126. package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
  127. package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
  128. package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
  129. package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
  130. package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
  131. package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
  132. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
  133. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
  134. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
  135. package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
  136. package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
  137. package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
  138. package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
  139. package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
  140. package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
  141. package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
  142. package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
  143. package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
  144. package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
  145. package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
  146. package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
  147. package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
  148. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
  149. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
  150. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
  151. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
  152. package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
  153. package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
  154. package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
  155. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
  156. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
  157. package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
  158. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
  159. package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
  160. package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
  161. package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
  162. package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
  163. package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
  164. package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
  165. package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
  166. package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
  167. package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
  168. package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
  169. package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
  170. package/template/pnpm-workspace.yaml +4 -0
  171. package/template/tsconfig.json +8 -0
@@ -0,0 +1,183 @@
1
+ import * as React from 'react';
2
+
3
+ const TOAST_LIMIT = 1;
4
+ const TOAST_REMOVE_DELAY = 1000000;
5
+
6
+ type ToasterToast = {
7
+ id: string;
8
+ title?: React.ReactNode;
9
+ description?: React.ReactNode;
10
+ action?: React.ReactNode;
11
+ variant?: 'default' | 'destructive';
12
+ open?: boolean;
13
+ };
14
+
15
+ const actionTypes = {
16
+ ADD_TOAST: 'ADD_TOAST',
17
+ UPDATE_TOAST: 'UPDATE_TOAST',
18
+ DISMISS_TOAST: 'DISMISS_TOAST',
19
+ REMOVE_TOAST: 'REMOVE_TOAST',
20
+ } as const;
21
+
22
+ let count = 0;
23
+
24
+ function genId() {
25
+ count = (count + 1) % Number.MAX_SAFE_INTEGER;
26
+ return count.toString();
27
+ }
28
+
29
+ type ActionType = typeof actionTypes;
30
+
31
+ type Action =
32
+ | {
33
+ type: ActionType['ADD_TOAST'];
34
+ toast: ToasterToast;
35
+ }
36
+ | {
37
+ type: ActionType['UPDATE_TOAST'];
38
+ toast: Partial<ToasterToast>;
39
+ }
40
+ | {
41
+ type: ActionType['DISMISS_TOAST'];
42
+ toastId?: ToasterToast['id'];
43
+ }
44
+ | {
45
+ type: ActionType['REMOVE_TOAST'];
46
+ toastId?: ToasterToast['id'];
47
+ };
48
+
49
+ interface State {
50
+ toasts: ToasterToast[];
51
+ }
52
+
53
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
54
+
55
+ const addToRemoveQueue = (toastId: string) => {
56
+ if (toastTimeouts.has(toastId)) {
57
+ return;
58
+ }
59
+
60
+ const timeout = setTimeout(() => {
61
+ toastTimeouts.delete(toastId);
62
+ dispatch({
63
+ type: 'REMOVE_TOAST',
64
+ toastId: toastId,
65
+ });
66
+ }, TOAST_REMOVE_DELAY);
67
+
68
+ toastTimeouts.set(toastId, timeout);
69
+ };
70
+
71
+ export const reducer = (state: State, action: Action): State => {
72
+ switch (action.type) {
73
+ case 'ADD_TOAST':
74
+ return {
75
+ ...state,
76
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
77
+ };
78
+
79
+ case 'UPDATE_TOAST':
80
+ return {
81
+ ...state,
82
+ toasts: state.toasts.map((t) =>
83
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
84
+ ),
85
+ };
86
+
87
+ case 'DISMISS_TOAST': {
88
+ const { toastId } = action;
89
+
90
+ if (toastId) {
91
+ addToRemoveQueue(toastId);
92
+ } else {
93
+ state.toasts.forEach((toast) => {
94
+ addToRemoveQueue(toast.id);
95
+ });
96
+ }
97
+
98
+ return {
99
+ ...state,
100
+ toasts: state.toasts.map((t) =>
101
+ t.id === toastId || toastId === undefined
102
+ ? {
103
+ ...t,
104
+ open: false,
105
+ }
106
+ : t
107
+ ),
108
+ };
109
+ }
110
+ case 'REMOVE_TOAST':
111
+ if (action.toastId === undefined) {
112
+ return {
113
+ ...state,
114
+ toasts: [],
115
+ };
116
+ }
117
+ return {
118
+ ...state,
119
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
120
+ };
121
+ }
122
+ };
123
+
124
+ const listeners: Array<(state: State) => void> = [];
125
+
126
+ let memoryState: State = { toasts: [] };
127
+
128
+ function dispatch(action: Action) {
129
+ memoryState = reducer(memoryState, action);
130
+ listeners.forEach((listener) => {
131
+ listener(memoryState);
132
+ });
133
+ }
134
+
135
+ type Toast = Omit<ToasterToast, 'id'>;
136
+
137
+ function toast({ ...props }: Toast) {
138
+ const id = genId();
139
+
140
+ const update = (props: ToasterToast) =>
141
+ dispatch({
142
+ type: 'UPDATE_TOAST',
143
+ toast: { ...props, id },
144
+ });
145
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
146
+
147
+ dispatch({
148
+ type: 'ADD_TOAST',
149
+ toast: {
150
+ ...props,
151
+ id,
152
+ open: true,
153
+ },
154
+ });
155
+
156
+ return {
157
+ id: id,
158
+ dismiss,
159
+ update,
160
+ };
161
+ }
162
+
163
+ function useToast() {
164
+ const [state, setState] = React.useState<State>(memoryState);
165
+
166
+ React.useEffect(() => {
167
+ listeners.push(setState);
168
+ return () => {
169
+ const index = listeners.indexOf(setState);
170
+ if (index > -1) {
171
+ listeners.splice(index, 1);
172
+ }
173
+ };
174
+ }, [state]);
175
+
176
+ return {
177
+ ...state,
178
+ toast,
179
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
180
+ };
181
+ }
182
+
183
+ export { useToast, toast };
@@ -0,0 +1,48 @@
1
+ // Re-export all kernel modules
2
+ export * from './shared-state';
3
+ export * from './providers';
4
+ export * from './lib/utils';
5
+
6
+ // UI Components
7
+ export * from './components/index';
8
+
9
+ // Channels
10
+ export * from './channels';
11
+
12
+ // Plugins
13
+ export * from './plugins';
14
+
15
+ // Auth
16
+ export type { LoginCredentials, RegisterData, AuthResponse, AuthError } from './auth/types';
17
+ export { AuthService } from './auth/service';
18
+ export { useAuth, useCurrentUser, useRequireAuth } from './auth/hooks';
19
+ export { ProtectedRoute } from './auth/ProtectedRoute';
20
+
21
+ // RBAC
22
+ export type {
23
+ Organization,
24
+ CreateOrganizationData,
25
+ UpdateOrganizationData,
26
+ Role,
27
+ CreateRoleData,
28
+ UpdateRoleData,
29
+ Permission,
30
+ UserRole,
31
+ AssignRoleData,
32
+ UserWithRoles,
33
+ CreateUserRequest,
34
+ UpdateUserRequest,
35
+ PermissionCheck,
36
+ ResourceType,
37
+ ActionType,
38
+ } from './rbac/types';
39
+ export { RBACService } from './rbac/service';
40
+ export {
41
+ useCurrentOrganization,
42
+ useOrganizations,
43
+ useOrganizationRoles,
44
+ useHasPermission,
45
+ useRequirePermission,
46
+ } from './rbac/hooks';
47
+ export { PermissionGate } from './rbac/components/PermissionGate';
48
+ export { OrganizationSelector } from './rbac/components/OrganizationSelector';
@@ -0,0 +1 @@
1
+ export { cn } from './utils';
@@ -0,0 +1,36 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export function getInitials(name: string): string {
9
+ return name
10
+ .split(' ')
11
+ .map((n) => n[0])
12
+ .join('')
13
+ .toUpperCase()
14
+ .slice(0, 2);
15
+ }
16
+
17
+ export function formatDate(date: string | Date): string {
18
+ return new Intl.DateTimeFormat('en-US', {
19
+ month: 'short',
20
+ day: 'numeric',
21
+ year: 'numeric',
22
+ }).format(new Date(date));
23
+ }
24
+
25
+ export function formatRelativeTime(date: string | Date): string {
26
+ const now = new Date();
27
+ const past = new Date(date);
28
+ const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
29
+
30
+ if (diffInSeconds < 60) return 'just now';
31
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
32
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
33
+ if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
34
+
35
+ return formatDate(date);
36
+ }
@@ -0,0 +1,41 @@
1
+ import { useSlots } from './SlotProvider';
2
+ import type { SlotName, SlotInjection } from './types';
3
+
4
+ interface SlotProps {
5
+ name: SlotName;
6
+ children?: React.ReactNode; // Fallback content
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * Slot - Component that renders injected content from plugins
12
+ *
13
+ * Plugins can register content to be rendered in specific slots
14
+ * throughout the host layout (sidebar, topbar, etc.)
15
+ */
16
+ export function Slot({ name, children, className }: SlotProps) {
17
+ const { getSlotContent } = useSlots();
18
+
19
+ const content = getSlotContent(name);
20
+
21
+ if (!content || content.length === 0) {
22
+ return <>{children}</>;
23
+ }
24
+
25
+ return (
26
+ <div className={className}>
27
+ {content.map((item: SlotInjection, index: number) => (
28
+ <div key={`${item.slot}-${index}`} className="slot-item">
29
+ {item.condition === undefined || item.condition() ? (
30
+ typeof item.component === 'function' ? (
31
+ <item.component {...(item.props || {})} />
32
+ ) : (
33
+ item.component
34
+ )
35
+ ) : null}
36
+ </div>
37
+ ))}
38
+ {children}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,88 @@
1
+ import { createContext, useContext, ReactNode } from 'react';
2
+ import type { SlotInjection, SlotName } from './types';
3
+ import { usePluginStore } from './store';
4
+ import { pluginLoader } from './loader';
5
+
6
+ interface SlotContextValue {
7
+ getSlotContent: (name: SlotName) => SlotInjection[];
8
+ registerSlot: (name: SlotName, injection: SlotInjection) => void;
9
+ unregisterSlot: (name: SlotName, component: ReactNode) => void;
10
+ }
11
+
12
+ const SlotContext = createContext<SlotContextValue | null>(null);
13
+
14
+ interface SlotProviderProps {
15
+ children: ReactNode;
16
+ }
17
+
18
+ /**
19
+ * SlotProvider - Provides slot injection system to the app
20
+ *
21
+ * This provider:
22
+ * 1. Collects slot injections from all enabled plugins
23
+ * 2. Sorts them by order
24
+ * 3. Provides getSlotContent function for Slot components
25
+ */
26
+ export function SlotProvider({ children }: SlotProviderProps) {
27
+ const plugins = usePluginStore((state) => state.plugins);
28
+
29
+ /**
30
+ * Get all content for a slot from enabled plugins
31
+ */
32
+ const getSlotContent = (name: SlotName): SlotInjection[] => {
33
+ const injections: SlotInjection[] = [];
34
+
35
+ // Collect injections from all enabled plugins
36
+ for (const plugin of Object.values(plugins)) {
37
+ if (!plugin.enabled || !plugin.loaded) continue;
38
+
39
+ const config = pluginLoader.getPluginConfig(plugin.id);
40
+ if (!config) continue;
41
+
42
+ for (const slot of config.slots) {
43
+ if (slot.slot === name) {
44
+ injections.push(slot);
45
+ }
46
+ }
47
+ }
48
+
49
+ // Sort by order (lower = higher priority = first)
50
+ return injections.sort((a, b) => (a.order || 100) - (b.order || 100));
51
+ };
52
+
53
+ /**
54
+ * Register a slot injection (for dynamic registration)
55
+ */
56
+ const registerSlot = (name: SlotName, injection: SlotInjection) => {
57
+ // This would be used for runtime slot registration
58
+ // For now, slots are registered via plugin config
59
+ console.log('[SlotProvider] Register slot:', name);
60
+ };
61
+
62
+ /**
63
+ * Unregister a slot injection
64
+ */
65
+ const unregisterSlot = (name: SlotName, component: ReactNode) => {
66
+ console.log('[SlotProvider] Unregister slot:', name);
67
+ };
68
+
69
+ return (
70
+ <SlotContext.Provider value={{ getSlotContent, registerSlot, unregisterSlot }}>
71
+ {children}
72
+ </SlotContext.Provider>
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Hook to access slot context
78
+ */
79
+ export function useSlots() {
80
+ const context = useContext(SlotContext);
81
+ if (!context) {
82
+ throw new Error('useSlots must be used within SlotProvider');
83
+ }
84
+ return context;
85
+ }
86
+
87
+ // Export SlotContext for use in Slot component
88
+ export { SlotContext };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Plugin System - Barrel Export
3
+ *
4
+ * Exports all plugin system functionality for use throughout the host application
5
+ * and for plugin developers.
6
+ */
7
+
8
+ // Types and interfaces
9
+ export * from './types';
10
+
11
+ // Zod schemas
12
+ export * from './schemas';
13
+
14
+ // Zustand store
15
+ export * from './store';
16
+
17
+ // Plugin loader
18
+ export * from './loader';
19
+
20
+ // Slot components
21
+ export * from './Slot';
22
+ export * from './SlotProvider';
23
+ export { useSlots } from './SlotProvider';
@@ -0,0 +1,122 @@
1
+ import Garfish from 'garfish';
2
+ import type { PluginConfig, PluginManifest } from './types';
3
+ import { usePluginStore } from './store';
4
+
5
+ /**
6
+ * Plugin loader - handles dynamic loading of plugins
7
+ */
8
+ class PluginLoader {
9
+ private garfishInstance: typeof Garfish | null = null;
10
+ private loadedPlugins = new Map<string, PluginConfig>();
11
+
12
+ /**
13
+ * Initialize loader with Garfish instance
14
+ */
15
+ initialize(garfish: typeof Garfish): void {
16
+ this.garfishInstance = garfish;
17
+ console.log('[PluginLoader] Initialized');
18
+ }
19
+
20
+ /**
21
+ * Load a plugin dynamically
22
+ */
23
+ async loadPlugin(entry: string | (() => Promise<unknown>)): Promise<PluginConfig | null> {
24
+ if (!this.garfishInstance) {
25
+ console.error('[PluginLoader] Garfish not initialized');
26
+ return null;
27
+ }
28
+
29
+ try {
30
+ let module: any;
31
+
32
+ if (typeof entry === 'function') {
33
+ // Dynamic import (production)
34
+ module = await entry();
35
+ } else {
36
+ // URL load (development)
37
+ // In dev, plugins run on separate servers
38
+ // We just track them, actual loading is handled by Garfish
39
+ console.log(`[PluginLoader] Dev mode: Plugin at ${entry}`);
40
+ return null;
41
+ }
42
+
43
+ // Extract plugin config from module
44
+ const config = module.default?.config || module.config;
45
+
46
+ if (!config) {
47
+ console.error('[PluginLoader] Plugin has no config export');
48
+ return null;
49
+ }
50
+
51
+ // Validate config
52
+ this.loadedPlugins.set(config.manifest.name, config);
53
+
54
+ console.log(`[PluginLoader] Loaded plugin: ${config.manifest.name}`);
55
+
56
+ return config;
57
+ } catch (error) {
58
+ console.error('[PluginLoader] Failed to load plugin:', error);
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Load all configured plugins
65
+ */
66
+ async loadPlugins(plugins: Array<{
67
+ name: string;
68
+ entry: string | (() => Promise<unknown>);
69
+ }>): Promise<void> {
70
+ const store = usePluginStore.getState();
71
+
72
+ for (const plugin of plugins) {
73
+ const state = store.plugins[plugin.name];
74
+
75
+ if (!state || !state.enabled) {
76
+ continue;
77
+ }
78
+
79
+ try {
80
+ const config = await this.loadPlugin(plugin.entry);
81
+
82
+ if (config) {
83
+ store.registerPlugin(config);
84
+ store.setPluginLoaded(plugin.name, true);
85
+ }
86
+ } catch (error: any) {
87
+ store.setPluginError(plugin.name, error.message);
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Get loaded plugin config
94
+ */
95
+ getPluginConfig(name: string): PluginConfig | undefined {
96
+ return this.loadedPlugins.get(name);
97
+ }
98
+
99
+ /**
100
+ * Get all loaded plugins
101
+ */
102
+ getAllPlugins(): PluginConfig[] {
103
+ return Array.from(this.loadedPlugins.values());
104
+ }
105
+
106
+ /**
107
+ * Check if plugin is loaded
108
+ */
109
+ isLoaded(name: string): boolean {
110
+ return this.loadedPlugins.has(name);
111
+ }
112
+ }
113
+
114
+ // Export singleton
115
+ export const pluginLoader = new PluginLoader();
116
+
117
+ /**
118
+ * Initialize plugin loader (call from bootstrap)
119
+ */
120
+ export function initializePluginLoader(garfish: typeof Garfish): void {
121
+ pluginLoader.initialize(garfish);
122
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ import { SlotName } from './types';
3
+
4
+ /**
5
+ * Slot injection schema
6
+ */
7
+ export const slotInjectionSchema = z.object({
8
+ slot: z.nativeEnum(SlotName),
9
+ component: z.any(), // React component
10
+ order: z.number().optional().default(100),
11
+ props: z.record(z.any()).optional(),
12
+ condition: z.function().optional(),
13
+ });
14
+
15
+ /**
16
+ * Plugin manifest schema
17
+ */
18
+ export const pluginManifestSchema = z.object({
19
+ name: z.string().regex(/^@lego\/plugin-/, 'Plugin name must start with @lego/plugin-'),
20
+ version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver'),
21
+ displayName: z.string().min(1).max(50),
22
+ description: z.string().max(500),
23
+ author: z.string().optional(),
24
+ icon: z.string().optional(),
25
+ permissions: z.array(z.string()).optional(),
26
+ });
27
+
28
+ /**
29
+ * Plugin config schema
30
+ */
31
+ export const pluginConfigSchema = z.object({
32
+ manifest: pluginManifestSchema,
33
+ enabled: z.boolean().default(true),
34
+ slots: z.array(slotInjectionSchema).default([]),
35
+ routes: z.array(z.object({
36
+ path: z.string(),
37
+ component: z.any(),
38
+ protected: z.boolean().optional(),
39
+ permissions: z.array(z.string()).optional(),
40
+ })).optional(),
41
+ settings: z.array(z.object({
42
+ key: z.string(),
43
+ type: z.enum(['boolean', 'string', 'number', 'select']),
44
+ label: z.string(),
45
+ description: z.string().optional(),
46
+ defaultValue: z.any(),
47
+ options: z.array(z.object({
48
+ label: z.string(),
49
+ value: z.string(),
50
+ })).optional(),
51
+ })).optional(),
52
+ });
53
+
54
+ export type PluginConfigInput = z.infer<typeof pluginConfigSchema>;