create-lego-one 2.0.9 → 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.
- package/dist/index.cjs +145 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +5 -3
- package/template/host/e2e/auth.spec.ts +38 -0
- package/template/host/e2e/layout.spec.ts +38 -0
- package/template/host/modern.config.ts +19 -0
- package/template/host/package.json +71 -0
- package/template/host/playwright.config.ts +34 -0
- package/template/host/postcss.config.mjs +6 -0
- package/template/host/src/App.tsx +6 -0
- package/template/host/src/bootstrap.tsx +74 -0
- package/template/host/src/global.css +59 -0
- package/template/host/src/index.ts +2 -0
- package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
- package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
- package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
- package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
- package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
- package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
- package/template/host/src/kernel/auth/hooks.ts +174 -0
- package/template/host/src/kernel/auth/index.ts +5 -0
- package/template/host/src/kernel/auth/schemas.ts +27 -0
- package/template/host/src/kernel/auth/service.ts +197 -0
- package/template/host/src/kernel/auth/types.ts +36 -0
- package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
- package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
- package/template/host/src/kernel/channels/events.ts +27 -0
- package/template/host/src/kernel/channels/hooks.ts +168 -0
- package/template/host/src/kernel/channels/index.ts +6 -0
- package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
- package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
- package/template/host/src/kernel/channels/types.ts +112 -0
- package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
- package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
- package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
- package/template/host/src/kernel/components/index.ts +32 -0
- package/template/host/src/kernel/components/ui/alert.tsx +58 -0
- package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
- package/template/host/src/kernel/components/ui/badge.tsx +35 -0
- package/template/host/src/kernel/components/ui/button.tsx +50 -0
- package/template/host/src/kernel/components/ui/card.tsx +78 -0
- package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
- package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
- package/template/host/src/kernel/components/ui/index.ts +7 -0
- package/template/host/src/kernel/components/ui/input.tsx +24 -0
- package/template/host/src/kernel/components/ui/label.tsx +21 -0
- package/template/host/src/kernel/components/ui/popover.tsx +28 -0
- package/template/host/src/kernel/components/ui/progress.tsx +25 -0
- package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
- package/template/host/src/kernel/components/ui/select.tsx +155 -0
- package/template/host/src/kernel/components/ui/separator.tsx +28 -0
- package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
- package/template/host/src/kernel/components/ui/switch.tsx +26 -0
- package/template/host/src/kernel/components/ui/table.tsx +116 -0
- package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
- package/template/host/src/kernel/components/ui/toast.tsx +126 -0
- package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
- package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
- package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
- package/template/host/src/kernel/index.ts +48 -0
- package/template/host/src/kernel/lib/cn.ts +1 -0
- package/template/host/src/kernel/lib/utils.ts +36 -0
- package/template/host/src/kernel/plugins/Slot.tsx +41 -0
- package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
- package/template/host/src/kernel/plugins/index.ts +23 -0
- package/template/host/src/kernel/plugins/loader.ts +122 -0
- package/template/host/src/kernel/plugins/schemas.ts +54 -0
- package/template/host/src/kernel/plugins/store.ts +185 -0
- package/template/host/src/kernel/plugins/types.ts +103 -0
- package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
- package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
- package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
- package/template/host/src/kernel/providers/index.ts +3 -0
- package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
- package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
- package/template/host/src/kernel/rbac/hooks.ts +379 -0
- package/template/host/src/kernel/rbac/index.ts +6 -0
- package/template/host/src/kernel/rbac/service.ts +504 -0
- package/template/host/src/kernel/rbac/types.ts +164 -0
- package/template/host/src/kernel/rbac/utils.ts +34 -0
- package/template/host/src/kernel/shared-state/bridge.ts +31 -0
- package/template/host/src/kernel/shared-state/index.ts +3 -0
- package/template/host/src/kernel/shared-state/store.ts +62 -0
- package/template/host/src/kernel/shared-state/types.ts +60 -0
- package/template/host/src/kernel/use-migrations.ts +72 -0
- package/template/host/src/layout/MobileMenu.tsx +61 -0
- package/template/host/src/layout/Shell.tsx +42 -0
- package/template/host/src/layout/Sidebar.tsx +178 -0
- package/template/host/src/layout/Topbar.tsx +50 -0
- package/template/host/src/layout/index.ts +4 -0
- package/template/host/src/lib/pocketbase/client.ts +38 -0
- package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
- package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
- package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
- package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
- package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
- package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
- package/template/host/src/lib/pocketbase/index.ts +5 -0
- package/template/host/src/lib/pocketbase/migrations.ts +44 -0
- package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
- package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
- package/template/host/src/lib/pocketbase/seed.ts +113 -0
- package/template/host/src/lib/pocketbase/types.ts +102 -0
- package/template/host/src/modern.runtime.ts +26 -0
- package/template/host/src/plugins.d.ts +9 -0
- package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
- package/template/host/src/routes/_.tsx +6 -0
- package/template/host/src/routes/dashboard._.tsx +41 -0
- package/template/host/src/routes/index.tsx +93 -0
- package/template/host/src/routes/login.tsx +36 -0
- package/template/host/src/saas.config.ts +52 -0
- package/template/host/src/test/setup.ts +65 -0
- package/template/host/src/test/utils.tsx +69 -0
- package/template/host/src/test/vitest-globals.d.ts +19 -0
- package/template/host/src/vite-env.d.ts +16 -0
- package/template/host/tailwind.config.ts +77 -0
- package/template/host/tsconfig.json +19 -0
- package/template/host/vitest.config.ts +30 -0
- package/template/package.json +44 -0
- package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
- package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
- package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
- package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
- package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
- package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
- package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
- package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
- package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
- package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
- package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
- package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.json +8 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { channelBus } from './ChannelBus';
|
|
3
|
+
import { ChannelName, type ChannelMessage, type ChannelSubscriber } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to a Garfish channel
|
|
7
|
+
*
|
|
8
|
+
* @param channel - The channel name to subscribe to
|
|
9
|
+
* @param callback - The callback function when messages are received
|
|
10
|
+
* @param deps - Dependencies for re-subscription
|
|
11
|
+
*/
|
|
12
|
+
export function useChannel<T extends ChannelMessage>(
|
|
13
|
+
channel: ChannelName,
|
|
14
|
+
callback: ChannelSubscriber<T>,
|
|
15
|
+
deps: React.DependencyList = []
|
|
16
|
+
) {
|
|
17
|
+
const callbackRef = useRef(callback);
|
|
18
|
+
|
|
19
|
+
// Update callback ref without causing re-subscription
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
callbackRef.current = callback;
|
|
22
|
+
}, [callback]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const unsubscribe = channelBus.subscribe<T>(channel, (data) => {
|
|
26
|
+
callbackRef.current(data);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
unsubscribe();
|
|
31
|
+
};
|
|
32
|
+
}, [channel, ...deps]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Publish to a Garfish channel
|
|
37
|
+
*
|
|
38
|
+
* @returns A function to publish messages
|
|
39
|
+
*/
|
|
40
|
+
export function usePublish() {
|
|
41
|
+
return useCallback(<T extends ChannelMessage>(message: T) => {
|
|
42
|
+
channelBus.publish(message);
|
|
43
|
+
}, []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Publish toast notifications
|
|
48
|
+
*
|
|
49
|
+
* @returns A function to publish toast messages
|
|
50
|
+
*/
|
|
51
|
+
export function useToastChannel() {
|
|
52
|
+
const publish = usePublish();
|
|
53
|
+
|
|
54
|
+
return useCallback((data: any) => {
|
|
55
|
+
publish({
|
|
56
|
+
channel: ChannelName.TOAST,
|
|
57
|
+
data: {
|
|
58
|
+
...data,
|
|
59
|
+
id: `toast_${Date.now()}`,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
source: 'plugin',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}, [publish]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Publish navigation events
|
|
69
|
+
*
|
|
70
|
+
* @returns A function to publish navigation messages
|
|
71
|
+
*/
|
|
72
|
+
export function useNavigationChannel() {
|
|
73
|
+
const publish = usePublish();
|
|
74
|
+
|
|
75
|
+
return useCallback((path: string, replace = false) => {
|
|
76
|
+
publish({
|
|
77
|
+
channel: ChannelName.NAVIGATION,
|
|
78
|
+
data: {
|
|
79
|
+
path,
|
|
80
|
+
replace,
|
|
81
|
+
id: `nav_${Date.now()}`,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
source: 'plugin',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}, [publish]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Publish state updates
|
|
91
|
+
*
|
|
92
|
+
* @returns A function to publish state update messages
|
|
93
|
+
*/
|
|
94
|
+
export function useStateUpdateChannel() {
|
|
95
|
+
const publish = usePublish();
|
|
96
|
+
|
|
97
|
+
return useCallback((key: string, value: unknown) => {
|
|
98
|
+
publish({
|
|
99
|
+
channel: ChannelName.STATE_UPDATE,
|
|
100
|
+
data: {
|
|
101
|
+
key,
|
|
102
|
+
value,
|
|
103
|
+
id: `state_${Date.now()}`,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
source: 'plugin',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}, [publish]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Publish plugin ready event
|
|
113
|
+
*
|
|
114
|
+
* Call this when your plugin has finished initializing
|
|
115
|
+
*/
|
|
116
|
+
export function usePluginReady(pluginName: string, version: string) {
|
|
117
|
+
const publish = usePublish();
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
publish({
|
|
121
|
+
channel: ChannelName.PLUGIN_READY,
|
|
122
|
+
data: {
|
|
123
|
+
pluginName,
|
|
124
|
+
version,
|
|
125
|
+
id: `ready_${Date.now()}`,
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
source: pluginName,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}, [pluginName, version, publish]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Publish plugin error event
|
|
135
|
+
*
|
|
136
|
+
* Call this when your plugin encounters an error
|
|
137
|
+
*/
|
|
138
|
+
export function usePublishPluginError() {
|
|
139
|
+
const publish = usePublish();
|
|
140
|
+
|
|
141
|
+
return useCallback((pluginName: string, error: string, stack?: string) => {
|
|
142
|
+
publish({
|
|
143
|
+
channel: ChannelName.PLUGIN_ERROR,
|
|
144
|
+
data: {
|
|
145
|
+
pluginName,
|
|
146
|
+
error,
|
|
147
|
+
stack,
|
|
148
|
+
id: `error_${Date.now()}`,
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
source: pluginName,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}, [publish]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Listen for auth changes
|
|
158
|
+
*/
|
|
159
|
+
export function useAuthChannel(callback: (data: any) => void) {
|
|
160
|
+
return useChannel(ChannelName.AUTH_CHANGE, callback as any, []);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Listen for organization changes
|
|
165
|
+
*/
|
|
166
|
+
export function useOrganizationChannel(callback: (data: any) => void) {
|
|
167
|
+
return useChannel(ChannelName.ORGANIZATION_CHANGE, callback as any, []);
|
|
168
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { toast } from '../../components/ui/use-toast';
|
|
3
|
+
import { channelBus } from '../ChannelBus';
|
|
4
|
+
import { ChannelName, type ChannelMessage } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ToastIntegration - Listens to toast channel events and displays them
|
|
8
|
+
*
|
|
9
|
+
* This component subscribes to the TOAST channel and shows toasts
|
|
10
|
+
* when plugins publish events to it. This demonstrates plugin → host communication.
|
|
11
|
+
*/
|
|
12
|
+
export function ToastIntegration() {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Subscribe to toast channel
|
|
15
|
+
const unsubscribe = channelBus.subscribe(
|
|
16
|
+
ChannelName.TOAST,
|
|
17
|
+
(message: ChannelMessage) => {
|
|
18
|
+
if (message.channel === ChannelName.TOAST) {
|
|
19
|
+
const { type, title, description } = message.data as any;
|
|
20
|
+
|
|
21
|
+
// Map toast type to variant
|
|
22
|
+
const variant = type === 'error' || type === 'warning' ? 'destructive' : 'default';
|
|
23
|
+
|
|
24
|
+
// Show toast
|
|
25
|
+
toast({
|
|
26
|
+
variant,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(`[ToastIntegration] Received toast:`, { type, title, description });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
unsubscribe();
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// This component doesn't render anything
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hook to publish toast events (for plugins to use)
|
|
47
|
+
*/
|
|
48
|
+
export function usePublishToast() {
|
|
49
|
+
return (data: any) => {
|
|
50
|
+
channelBus.publish({
|
|
51
|
+
channel: ChannelName.TOAST,
|
|
52
|
+
data: {
|
|
53
|
+
...data,
|
|
54
|
+
id: `toast_${Date.now()}`,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
source: 'plugin',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useNavigate } from '@modern-js/runtime/router';
|
|
3
|
+
import { channelBus } from './ChannelBus';
|
|
4
|
+
import { ChannelName, type ToastEventData } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper hook for plugins to access kernel channels
|
|
8
|
+
*
|
|
9
|
+
* This hook can be used from any plugin via the window bridge:
|
|
10
|
+
* window.__LEGO_KERNEL_STATE__?.usePluginChannels?.()
|
|
11
|
+
*/
|
|
12
|
+
export function usePluginChannels() {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
// Show toast notification
|
|
16
|
+
const toast = useCallback((data: Omit<ToastEventData, 'id' | 'timestamp' | 'source'>) => {
|
|
17
|
+
channelBus.publish({
|
|
18
|
+
channel: ChannelName.TOAST,
|
|
19
|
+
data: {
|
|
20
|
+
...data,
|
|
21
|
+
id: `toast_${Date.now()}`,
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
source: 'plugin',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// Navigate to a path
|
|
29
|
+
const navigateTo = useCallback((path: string, replace = false) => {
|
|
30
|
+
// Use direct navigation instead of publishing to channel
|
|
31
|
+
// This avoids circular dependencies and works more reliably
|
|
32
|
+
navigate(path, { replace });
|
|
33
|
+
}, [navigate]);
|
|
34
|
+
|
|
35
|
+
// Subscribe to auth changes
|
|
36
|
+
const onAuthChange = useCallback((callback: (data: any) => void) => {
|
|
37
|
+
return channelBus.subscribe(ChannelName.AUTH_CHANGE, callback as any);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Subscribe to organization changes
|
|
41
|
+
const onOrganizationChange = useCallback((callback: (data: any) => void) => {
|
|
42
|
+
return channelBus.subscribe(ChannelName.ORGANIZATION_CHANGE, callback as any);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
toast,
|
|
47
|
+
navigateTo,
|
|
48
|
+
onAuthChange,
|
|
49
|
+
onOrganizationChange,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register plugin channels helper to window bridge
|
|
55
|
+
*/
|
|
56
|
+
export function registerPluginChannels() {
|
|
57
|
+
if (typeof window !== 'undefined') {
|
|
58
|
+
(window as any).__LEGO_PLUGIN_CHANNELS__ = {
|
|
59
|
+
toast: (data: Omit<ToastEventData, 'id' | 'timestamp' | 'source'>) => {
|
|
60
|
+
channelBus.publish({
|
|
61
|
+
channel: ChannelName.TOAST,
|
|
62
|
+
data: {
|
|
63
|
+
...data,
|
|
64
|
+
id: `toast_${Date.now()}`,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
source: 'plugin',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Garfish } from 'garfish';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Channel names for different communication topics
|
|
5
|
+
*/
|
|
6
|
+
export enum ChannelName {
|
|
7
|
+
TOAST = 'lego:toast',
|
|
8
|
+
NAVIGATION = 'lego:navigation',
|
|
9
|
+
STATE_UPDATE = 'lego:state:update',
|
|
10
|
+
PLUGIN_READY = 'lego:plugin:ready',
|
|
11
|
+
PLUGIN_ERROR = 'lego:plugin:error',
|
|
12
|
+
AUTH_CHANGE = 'lego:auth:change',
|
|
13
|
+
ORGANIZATION_CHANGE = 'lego:organization:change',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base event interface
|
|
18
|
+
*/
|
|
19
|
+
export interface ChannelEvent {
|
|
20
|
+
id: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
source?: string; // Plugin name or 'host'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Toast event data
|
|
27
|
+
*/
|
|
28
|
+
export interface ToastEventData extends ChannelEvent {
|
|
29
|
+
type: 'success' | 'error' | 'info' | 'warning';
|
|
30
|
+
title: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
duration?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Navigation event data
|
|
37
|
+
*/
|
|
38
|
+
export interface NavigationEventData extends ChannelEvent {
|
|
39
|
+
path: string;
|
|
40
|
+
replace?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* State update event data
|
|
45
|
+
*/
|
|
46
|
+
export interface StateUpdateEventData extends ChannelEvent {
|
|
47
|
+
key: string;
|
|
48
|
+
value: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Plugin ready event data
|
|
53
|
+
*/
|
|
54
|
+
export interface PluginReadyEventData extends ChannelEvent {
|
|
55
|
+
pluginName: string;
|
|
56
|
+
version: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Plugin error event data
|
|
61
|
+
*/
|
|
62
|
+
export interface PluginErrorEventData extends ChannelEvent {
|
|
63
|
+
pluginName: string;
|
|
64
|
+
error: string;
|
|
65
|
+
stack?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Auth change event data
|
|
70
|
+
*/
|
|
71
|
+
export interface AuthChangeEventData extends ChannelEvent {
|
|
72
|
+
isAuthenticated: boolean;
|
|
73
|
+
userId?: string;
|
|
74
|
+
organizationId?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Organization change event data
|
|
79
|
+
*/
|
|
80
|
+
export interface OrganizationChangeEventData extends ChannelEvent {
|
|
81
|
+
organizationId: string;
|
|
82
|
+
organizationName: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Channel message payload
|
|
87
|
+
*/
|
|
88
|
+
export type ChannelMessage =
|
|
89
|
+
| { channel: ChannelName.TOAST; data: ToastEventData }
|
|
90
|
+
| { channel: ChannelName.NAVIGATION; data: NavigationEventData }
|
|
91
|
+
| { channel: ChannelName.STATE_UPDATE; data: StateUpdateEventData }
|
|
92
|
+
| { channel: ChannelName.PLUGIN_READY; data: PluginReadyEventData }
|
|
93
|
+
| { channel: ChannelName.PLUGIN_ERROR; data: PluginErrorEventData }
|
|
94
|
+
| { channel: ChannelName.AUTH_CHANGE; data: AuthChangeEventData }
|
|
95
|
+
| { channel: ChannelName.ORGANIZATION_CHANGE; data: OrganizationChangeEventData };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Channel subscriber callback
|
|
99
|
+
*/
|
|
100
|
+
export type ChannelSubscriber<T = ChannelMessage> = (data: T) => void | Promise<void>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Channel API
|
|
104
|
+
*/
|
|
105
|
+
export interface ChannelAPI {
|
|
106
|
+
publish: <T extends ChannelMessage>(message: T) => void;
|
|
107
|
+
subscribe: <T extends ChannelMessage>(
|
|
108
|
+
channel: ChannelName,
|
|
109
|
+
callback: ChannelSubscriber<T>
|
|
110
|
+
) => () => void; // Returns unsubscribe function
|
|
111
|
+
unsubscribe: (channel: ChannelName, callback: ChannelSubscriber) => void;
|
|
112
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Badge } from '../ui/badge';
|
|
4
|
+
|
|
5
|
+
describe('Badge Component', () => {
|
|
6
|
+
it('should render children', () => {
|
|
7
|
+
render(<Badge>New</Badge>);
|
|
8
|
+
expect(screen.getByText('New')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should apply default variant classes', () => {
|
|
12
|
+
render(<Badge>Default</Badge>);
|
|
13
|
+
expect(screen.getByText('Default')).toHaveClass('bg-primary');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should apply secondary variant classes', () => {
|
|
17
|
+
render(<Badge variant="secondary">Secondary</Badge>);
|
|
18
|
+
expect(screen.getByText('Secondary')).toHaveClass('bg-secondary');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should apply destructive variant classes', () => {
|
|
22
|
+
render(<Badge variant="destructive">Error</Badge>);
|
|
23
|
+
expect(screen.getByText('Error')).toHaveClass('bg-destructive');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should apply outline variant classes', () => {
|
|
27
|
+
render(<Badge variant="outline">Outline</Badge>);
|
|
28
|
+
expect(screen.getByText('Outline')).toHaveClass('border');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should apply custom className', () => {
|
|
32
|
+
render(<Badge className="custom-class">Custom</Badge>);
|
|
33
|
+
expect(screen.getByText('Custom')).toHaveClass('custom-class');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
|
|
6
|
+
describe('Button Component', () => {
|
|
7
|
+
it('should render children', () => {
|
|
8
|
+
render(<Button>Click me</Button>);
|
|
9
|
+
expect(screen.getByText('Click me')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should call onClick when clicked', async () => {
|
|
13
|
+
const handleClick = vi.fn();
|
|
14
|
+
const user = userEvent.setup();
|
|
15
|
+
|
|
16
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
17
|
+
|
|
18
|
+
await user.click(screen.getByText('Click me'));
|
|
19
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should not call onClick when disabled', async () => {
|
|
23
|
+
const handleClick = vi.fn();
|
|
24
|
+
const user = userEvent.setup();
|
|
25
|
+
|
|
26
|
+
render(
|
|
27
|
+
<Button onClick={handleClick} disabled>
|
|
28
|
+
Click me
|
|
29
|
+
</Button>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await user.click(screen.getByText('Click me'));
|
|
33
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should apply variant classes correctly', () => {
|
|
37
|
+
const { rerender } = render(<Button variant="default">Default</Button>);
|
|
38
|
+
expect(screen.getByText('Default')).toHaveClass('bg-primary');
|
|
39
|
+
|
|
40
|
+
rerender(<Button variant="destructive">Destructive</Button>);
|
|
41
|
+
expect(screen.getByText('Destructive')).toHaveClass('bg-destructive');
|
|
42
|
+
|
|
43
|
+
rerender(<Button variant="outline">Outline</Button>);
|
|
44
|
+
expect(screen.getByText('Outline')).toHaveClass('border-input');
|
|
45
|
+
|
|
46
|
+
rerender(<Button variant="ghost">Ghost</Button>);
|
|
47
|
+
expect(screen.getByText('Ghost')).toHaveClass('hover:bg-accent');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should apply size classes correctly', () => {
|
|
51
|
+
const { rerender } = render(<Button size="default">Default</Button>);
|
|
52
|
+
expect(screen.getByText('Default')).toHaveClass('h-10');
|
|
53
|
+
|
|
54
|
+
rerender(<Button size="sm">Small</Button>);
|
|
55
|
+
expect(screen.getByText('Small')).toHaveClass('h-9');
|
|
56
|
+
|
|
57
|
+
rerender(<Button size="lg">Large</Button>);
|
|
58
|
+
expect(screen.getByText('Large')).toHaveClass('h-11');
|
|
59
|
+
|
|
60
|
+
rerender(<Button size="icon">Icon</Button>);
|
|
61
|
+
expect(screen.getByText('Icon')).toHaveClass('h-10', 'w-10');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Input } from '../ui/input';
|
|
5
|
+
|
|
6
|
+
describe('Input Component', () => {
|
|
7
|
+
it('should render input element', () => {
|
|
8
|
+
render(<Input />);
|
|
9
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should accept and update text input', async () => {
|
|
13
|
+
const user = userEvent.setup();
|
|
14
|
+
render(<Input placeholder="Enter text" />);
|
|
15
|
+
|
|
16
|
+
const input = screen.getByPlaceholderText('Enter text');
|
|
17
|
+
await user.type(input, 'Hello World');
|
|
18
|
+
|
|
19
|
+
expect(input).toHaveValue('Hello World');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should call onChange when value changes', async () => {
|
|
23
|
+
const handleChange = vi.fn();
|
|
24
|
+
const user = userEvent.setup();
|
|
25
|
+
|
|
26
|
+
render(<Input onChange={handleChange} />);
|
|
27
|
+
|
|
28
|
+
await user.type(screen.getByRole('textbox'), 'a');
|
|
29
|
+
|
|
30
|
+
expect(handleChange).toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should be disabled when disabled prop is true', async () => {
|
|
34
|
+
const user = userEvent.setup();
|
|
35
|
+
render(<Input disabled />);
|
|
36
|
+
|
|
37
|
+
const input = screen.getByRole('textbox');
|
|
38
|
+
expect(input).toBeDisabled();
|
|
39
|
+
|
|
40
|
+
await user.type(input, 'test');
|
|
41
|
+
expect(input).toHaveValue('');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should apply custom className', () => {
|
|
45
|
+
render(<Input className="custom-class" />);
|
|
46
|
+
expect(screen.getByRole('textbox')).toHaveClass('custom-class');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should accept all standard input props', () => {
|
|
50
|
+
render(
|
|
51
|
+
<Input
|
|
52
|
+
type="email"
|
|
53
|
+
name="email"
|
|
54
|
+
placeholder="email@example.com"
|
|
55
|
+
required
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const input = screen.getByPlaceholderText('email@example.com');
|
|
60
|
+
expect(input).toHaveAttribute('type', 'email');
|
|
61
|
+
expect(input).toHaveAttribute('name', 'email');
|
|
62
|
+
expect(input).toBeRequired();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export * from './ui/avatar';
|
|
2
|
+
export * from './ui/badge';
|
|
3
|
+
export * from './ui/button';
|
|
4
|
+
export * from './ui/card';
|
|
5
|
+
export * from './ui/input';
|
|
6
|
+
export * from './ui/label';
|
|
7
|
+
export * from './ui/alert';
|
|
8
|
+
export * from './ui/dialog';
|
|
9
|
+
export * from './ui/dropdown-menu';
|
|
10
|
+
export * from './ui/select';
|
|
11
|
+
export * from './ui/switch';
|
|
12
|
+
export * from './ui/tabs';
|
|
13
|
+
export * from './ui/separator';
|
|
14
|
+
export * from './ui/popover';
|
|
15
|
+
export * from './ui/scroll-area';
|
|
16
|
+
export * from './ui/progress';
|
|
17
|
+
export * from './ui/tooltip';
|
|
18
|
+
// Exclude Toast to avoid naming conflict with shared-state Toast type
|
|
19
|
+
export {
|
|
20
|
+
ToastProvider,
|
|
21
|
+
ToastViewport,
|
|
22
|
+
ToastAction,
|
|
23
|
+
ToastClose,
|
|
24
|
+
ToastTitle,
|
|
25
|
+
ToastDescription,
|
|
26
|
+
type ToastProps,
|
|
27
|
+
type ToastActionElement,
|
|
28
|
+
} from './ui/toast';
|
|
29
|
+
export * from './ui/toaster';
|
|
30
|
+
export * from './ui/use-toast';
|
|
31
|
+
export * from './ui/table';
|
|
32
|
+
export * from './ui/skeleton';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
const alertVariants = cva(
|
|
6
|
+
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-background text-foreground',
|
|
11
|
+
destructive:
|
|
12
|
+
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: 'default',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const Alert = React.forwardRef<
|
|
22
|
+
HTMLDivElement,
|
|
23
|
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
24
|
+
>(({ className, variant, ...props }, ref) => (
|
|
25
|
+
<div
|
|
26
|
+
ref={ref}
|
|
27
|
+
role="alert"
|
|
28
|
+
className={cn(alertVariants({ variant }), className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
));
|
|
32
|
+
Alert.displayName = 'Alert';
|
|
33
|
+
|
|
34
|
+
const AlertTitle = React.forwardRef<
|
|
35
|
+
HTMLParagraphElement,
|
|
36
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
37
|
+
>(({ className, ...props }, ref) => (
|
|
38
|
+
<h5
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
));
|
|
44
|
+
AlertTitle.displayName = 'AlertTitle';
|
|
45
|
+
|
|
46
|
+
const AlertDescription = React.forwardRef<
|
|
47
|
+
HTMLParagraphElement,
|
|
48
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
49
|
+
>(({ className, ...props }, ref) => (
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
));
|
|
56
|
+
AlertDescription.displayName = 'AlertDescription';
|
|
57
|
+
|
|
58
|
+
export { Alert, AlertTitle, AlertDescription };
|