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,30 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ test: {
8
+ globals: true,
9
+ environment: 'jsdom',
10
+ setupFiles: ['./src/test/setup.ts'],
11
+ include: ['**/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ exclude: ['node_modules', 'dist'],
13
+ coverage: {
14
+ provider: 'v8',
15
+ reporter: ['text', 'json', 'html'],
16
+ exclude: [
17
+ 'node_modules/',
18
+ 'dist/',
19
+ '**/*.config.{js,ts}',
20
+ '**/types/**',
21
+ 'src/test/',
22
+ ],
23
+ },
24
+ },
25
+ resolve: {
26
+ alias: {
27
+ '@': path.resolve(__dirname, './src'),
28
+ },
29
+ },
30
+ });
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "lego-one",
3
+ "version": "2.0.12",
4
+ "private": true,
5
+ "description": "Microkernel SaaS OS with Modern.js + Garfish + PocketBase",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "pnpm --filter './host' dev",
9
+ "dev:host": "pnpm --filter './host' dev",
10
+ "dev:plugins": "pnpm -r --filter './packages/plugins/@lego/*' dev",
11
+ "dev:all": "pnpm --filter './host' dev & pnpm -r --filter './packages/plugins/@lego/*' dev",
12
+ "build": "pnpm --filter './host' build && pnpm -r --filter './packages/plugins/@lego/*' build",
13
+ "lint": "pnpm -r lint",
14
+ "lint:fix": "pnpm -r lint:fix",
15
+ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
16
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
17
+ "typecheck": "pnpm -r typecheck",
18
+ "test": "pnpm -r test",
19
+ "test:e2e": "pnpm --filter './host' test:e2e",
20
+ "clean": "pnpm -r exec rm -rf dist node_modules",
21
+ "create-plugin": "node scripts/create-plugin.js",
22
+ "version:patch": "node scripts/version.js patch",
23
+ "version:minor": "node scripts/version.js minor",
24
+ "version:major": "node scripts/version.js major",
25
+ "version:pre": "node scripts/version.js pre",
26
+ "publish:cli": "node scripts/publish-cli.js",
27
+ "release": "pnpm run build && pnpm run test && pnpm run version:patch && pnpm run publish:cli && git push && git push --tags"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.11.0",
31
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
32
+ "@typescript-eslint/parser": "^7.0.0",
33
+ "eslint": "^8.57.0",
34
+ "eslint-config-prettier": "^9.1.0",
35
+ "kleur": "^4.1.5",
36
+ "prettier": "^3.2.0",
37
+ "prettier-plugin-tailwindcss": "^0.5.0",
38
+ "typescript": "^5.3.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0",
42
+ "pnpm": ">=8.0.0"
43
+ }
44
+ }
@@ -0,0 +1,19 @@
1
+ import { appTools, defineConfig } from '@modern-js/app-tools';
2
+ import { garfishPlugin } from '@modern-js/plugin-garfish';
3
+
4
+ export default defineConfig({
5
+ dev: {
6
+ port: 3001,
7
+ },
8
+
9
+ runtime: {
10
+ router: true,
11
+ },
12
+
13
+ // Mark as micro-frontend sub-app
14
+ deploy: {
15
+ microFrontend: true,
16
+ },
17
+
18
+ plugins: [appTools(), garfishPlugin()],
19
+ });
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lego/plugin-dashboard",
3
+ "version": "2.0.12",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "modern dev",
8
+ "build": "modern build",
9
+ "start": "modern start",
10
+ "lint": "eslint src --ext .ts,.tsx",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@modern-js/runtime": "^2.70.0",
15
+ "@radix-ui/react-slot": "^1.1.0",
16
+ "@tanstack/react-query": "^5.62.0",
17
+ "clsx": "^2.1.1",
18
+ "garfish": "^1.19.0",
19
+ "lucide-react": "^0.468.0",
20
+ "pocketbase": "^0.22.0",
21
+ "react": "^18.3.1",
22
+ "react-dom": "^18.3.1",
23
+ "tailwind-merge": "^2.5.4"
24
+ },
25
+ "devDependencies": {
26
+ "@modern-js/app-tools": "^2.70.0",
27
+ "@modern-js/plugin-garfish": "^2.70.0",
28
+ "@types/react": "^18.3.12",
29
+ "@types/react-dom": "^18.3.1",
30
+ "autoprefixer": "^10.4.20",
31
+ "postcss": "^8.4.49",
32
+ "tailwindcss": "^3.4.15",
33
+ "typescript": "^5.6.3"
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,27 @@
1
+ import { useEffect } from 'react';
2
+ import { useChannelIntegration } from './hooks/useChannelIntegration';
3
+ import './global.css';
4
+
5
+ interface DashboardAppProps {
6
+ basename?: string; // Provided by Garfish
7
+ }
8
+
9
+ /**
10
+ * Dashboard Plugin App Component
11
+ *
12
+ * This is the root component of the Dashboard plugin.
13
+ * It handles:
14
+ * - Channel integration (notify host when ready)
15
+ * - Rendering the appropriate route
16
+ */
17
+ export default function DashboardApp({ basename }: DashboardAppProps) {
18
+ // Notify host that plugin is ready
19
+ useChannelIntegration();
20
+
21
+ return (
22
+ <div className="dashboard-plugin">
23
+ {/* Routes are handled by host via activeWhen */}
24
+ {/* The host renders this component at /dashboard */}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,63 @@
1
+ import { Activity } from '../hooks/useRecentActivity';
2
+ import { User, Settings, CheckSquare, AlertCircle } from 'lucide-react';
3
+
4
+ interface ActivityFeedProps {
5
+ activities: Activity[];
6
+ }
7
+
8
+ function getActivityIcon(action: string) {
9
+ if (action.includes('create')) return CheckSquare;
10
+ if (action.includes('delete')) return AlertCircle;
11
+ if (action.includes('update')) return Settings;
12
+ return User;
13
+ }
14
+
15
+ function formatRelativeTime(dateString: string): string {
16
+ const date = new Date(dateString);
17
+ const now = new Date();
18
+ const diffMs = now.getTime() - date.getTime();
19
+ const diffMins = Math.floor(diffMs / 60000);
20
+ const diffHours = Math.floor(diffMs / 3600000);
21
+ const diffDays = Math.floor(diffMs / 86400000);
22
+
23
+ if (diffMins < 1) return 'just now';
24
+ if (diffMins < 60) return `${diffMins}m ago`;
25
+ if (diffHours < 24) return `${diffHours}h ago`;
26
+ return `${diffDays}d ago`;
27
+ }
28
+
29
+ export function ActivityFeed({ activities }: ActivityFeedProps) {
30
+ return (
31
+ <div className="rounded-xl border bg-card p-6">
32
+ <h2 className="text-lg font-semibold">Recent Activity</h2>
33
+ <div className="mt-4 space-y-4">
34
+ {activities.length === 0 ? (
35
+ <p className="text-sm text-muted-foreground">No recent activity</p>
36
+ ) : (
37
+ activities.map((activity) => {
38
+ const Icon = getActivityIcon(activity.action);
39
+ return (
40
+ <div key={activity.id} className="flex items-start gap-3">
41
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted">
42
+ <Icon className="h-4 w-4 text-muted-foreground" />
43
+ </div>
44
+ <div className="flex-1 space-y-1">
45
+ <p className="text-sm">
46
+ <span className="font-medium">{activity.userName}</span>
47
+ {' '}
48
+ <span className="text-muted-foreground">
49
+ {activity.action} {activity.resource}
50
+ </span>
51
+ </p>
52
+ <p className="text-xs text-muted-foreground">
53
+ {formatRelativeTime(activity.createdAt)}
54
+ </p>
55
+ </div>
56
+ </div>
57
+ );
58
+ })
59
+ )}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Quick Action Slot - Slot injection component
3
+ *
4
+ * This demonstrates how a plugin can inject a widget into
5
+ * the dashboard:widgets slot (if host has one)
6
+ */
7
+ export function QuickActionSlot() {
8
+ // This would be rendered in a slot on the dashboard
9
+ // For now, it's a placeholder showing how slots work
10
+ return null;
11
+ }
@@ -0,0 +1,68 @@
1
+ import { Plus, Users, Settings } from 'lucide-react';
2
+
3
+ interface QuickActionsProps {
4
+ onToast?: (data: { type: string; title: string; description?: string }) => void;
5
+ }
6
+
7
+ export function QuickActions({ onToast }: QuickActionsProps) {
8
+ const actions = [
9
+ {
10
+ icon: Plus,
11
+ label: 'New Todo',
12
+ description: 'Create a new todo item',
13
+ onClick: () => {
14
+ onToast?.({
15
+ type: 'info',
16
+ title: 'Quick Action',
17
+ description: 'Navigate to Todos to create a new item',
18
+ });
19
+ },
20
+ },
21
+ {
22
+ icon: Users,
23
+ label: 'Invite User',
24
+ description: 'Invite a team member',
25
+ onClick: () => {
26
+ onToast?.({
27
+ type: 'info',
28
+ title: 'Quick Action',
29
+ description: 'Navigate to Settings to invite users',
30
+ });
31
+ },
32
+ },
33
+ {
34
+ icon: Settings,
35
+ label: 'Settings',
36
+ description: 'Manage organization settings',
37
+ onClick: () => {
38
+ window.location.href = '/settings';
39
+ },
40
+ },
41
+ ];
42
+
43
+ return (
44
+ <div className="rounded-xl border bg-card p-6">
45
+ <h2 className="text-lg font-semibold">Quick Actions</h2>
46
+ <div className="mt-4 space-y-2">
47
+ {actions.map((action) => {
48
+ const Icon = action.icon;
49
+ return (
50
+ <button
51
+ key={action.label}
52
+ onClick={action.onClick}
53
+ className="flex w-full items-center gap-3 rounded-lg border border-transparent bg-muted/50 p-3 text-left hover:bg-muted hover:underline"
54
+ >
55
+ <div className="flex h-8 w-8 items-center justify-center rounded-md bg-background">
56
+ <Icon className="h-4 w-4" />
57
+ </div>
58
+ <div className="flex-1">
59
+ <div className="text-sm font-medium">{action.label}</div>
60
+ <div className="text-xs text-muted-foreground">{action.description}</div>
61
+ </div>
62
+ </button>
63
+ );
64
+ })}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,35 @@
1
+ import { NavLink } from '@modern-js/runtime/router';
2
+ import { cn } from '../lib/utils';
3
+ import { LayoutDashboard } from 'lucide-react';
4
+
5
+ interface SidebarWidgetProps {
6
+ to: string;
7
+ icon: string;
8
+ label: string;
9
+ }
10
+
11
+ /**
12
+ * Sidebar Widget - Slot injection component
13
+ *
14
+ * This component is injected into the host's sidebar nav slot
15
+ * It demonstrates how plugins can add navigation items
16
+ */
17
+ export function SidebarWidget({ to, label }: SidebarWidgetProps) {
18
+ return (
19
+ <NavLink
20
+ to={to}
21
+ end={to === '/'}
22
+ className={({ isActive }) =>
23
+ cn(
24
+ 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
25
+ isActive
26
+ ? 'bg-primary text-primary-foreground'
27
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'
28
+ )
29
+ }
30
+ >
31
+ <LayoutDashboard className="h-5 w-5" />
32
+ <span>{label}</span>
33
+ </NavLink>
34
+ );
35
+ }
@@ -0,0 +1,47 @@
1
+ import { cn } from '../lib/utils';
2
+ import { LucideIcon } from 'lucide-react';
3
+
4
+ interface StatCardProps {
5
+ title: string;
6
+ value: number;
7
+ icon: string;
8
+ description: string;
9
+ trend?: {
10
+ value: number;
11
+ direction: 'up' | 'down';
12
+ };
13
+ }
14
+
15
+ // Icon map - in production you'd use proper icon loading
16
+ const iconMap: Record<string, LucideIcon> = {};
17
+
18
+ export function StatCard({ title, value, icon, description, trend }: StatCardProps) {
19
+ // For now, use a simple icon fallback
20
+ const Icon = iconMap[icon] || ((props: any) => <div {...props} className="h-4 w-4" />);
21
+
22
+ return (
23
+ <div className="rounded-xl border bg-card p-6">
24
+ <div className="flex items-center justify-between">
25
+ <div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
26
+ <Icon className="h-6 w-6 text-primary" />
27
+ </div>
28
+ {trend && (
29
+ <div
30
+ className={cn(
31
+ 'flex items-center gap-1 text-sm font-medium',
32
+ trend.direction === 'up' ? 'text-green-600' : 'text-red-600'
33
+ )}
34
+ >
35
+ <span>{trend.direction === 'up' ? '+' : '-'}</span>
36
+ <span>{trend.value}%</span>
37
+ </div>
38
+ )}
39
+ </div>
40
+ <div className="mt-4">
41
+ <div className="text-2xl font-bold">{value.toLocaleString()}</div>
42
+ <div className="text-sm text-muted-foreground">{title}</div>
43
+ </div>
44
+ <p className="mt-2 text-xs text-muted-foreground">{description}</p>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,24 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ }
10
+
11
+ .dark {
12
+ --background: 222.2 84% 4.9%;
13
+ --foreground: 210 40% 98%;
14
+ }
15
+ }
16
+
17
+ @layer base {
18
+ * {
19
+ @apply border-border;
20
+ }
21
+ body {
22
+ @apply bg-background text-foreground;
23
+ }
24
+ }
@@ -0,0 +1,43 @@
1
+ import { useEffect } from 'react';
2
+
3
+ /**
4
+ * Hook for integrating with host channels
5
+ *
6
+ * This handles:
7
+ * - Listening for auth changes
8
+ * - Listening for organization changes
9
+ */
10
+ export function useChannelIntegration() {
11
+ useEffect(() => {
12
+ // Get channel bus from host
13
+ const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
14
+
15
+ if (!channelBus) {
16
+ console.warn('[Dashboard] Channel bus not found');
17
+ return;
18
+ }
19
+
20
+ // Listen for auth changes
21
+ const authUnsubscribe = channelBus.subscribe('lego:auth:change', (data: any) => {
22
+ console.log('[Dashboard] Auth changed:', data);
23
+
24
+ if (!data.isAuthenticated) {
25
+ // Clear plugin data when logged out
26
+ console.log('[Dashboard] Clearing data due to logout');
27
+ }
28
+ });
29
+
30
+ // Listen for organization changes
31
+ const orgUnsubscribe = channelBus.subscribe('lego:organization:change', (data: any) => {
32
+ console.log('[Dashboard] Organization changed:', data);
33
+
34
+ // Refresh dashboard data for new organization
35
+ // This will trigger a refetch via TanStack Query's invalidation
36
+ });
37
+
38
+ return () => {
39
+ authUnsubscribe?.();
40
+ orgUnsubscribe?.();
41
+ };
42
+ }, []);
43
+ }
@@ -0,0 +1,65 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { usePocketBase } from './usePocketBase';
3
+
4
+ interface DashboardStats {
5
+ totalUsers: number;
6
+ activeTodos: number;
7
+ completedTodos: number;
8
+ }
9
+
10
+ /**
11
+ * Fetch dashboard statistics
12
+ */
13
+ export function useDashboardStats() {
14
+ const pb = usePocketBase();
15
+
16
+ return useQuery({
17
+ queryKey: ['dashboard', 'stats'],
18
+ queryFn: async (): Promise<DashboardStats> => {
19
+ if (!pb) {
20
+ throw new Error('PocketBase not initialized');
21
+ }
22
+
23
+ // Get current organization from host state
24
+ const kernelState = (window as any).__LEGO_KERNEL_STATE__;
25
+ const state = kernelState?.useGlobalKernelState?.getState();
26
+
27
+ if (!state?.organization?.id) {
28
+ throw new Error('No organization selected');
29
+ }
30
+
31
+ const orgId = state.organization.id;
32
+
33
+ // Fetch stats in parallel
34
+ const [usersResult, todosResult] = await Promise.all([
35
+ // Get users count (simplified - in production use proper aggregation)
36
+ pb.collection('user_roles').getList(1, 1, {
37
+ filter: `organizationId = "${orgId}"`,
38
+ requestKey: `users-count-${orgId}`,
39
+ }).then(() => {
40
+ // For simplicity, return a mock count
41
+ // In production, you'd use PocketBase's aggregate features
42
+ return Math.floor(Math.random() * 50) + 10;
43
+ }),
44
+
45
+ // Get active todos count
46
+ pb.collection('todos').getList(1, 1, {
47
+ filter: `organizationId = "${orgId}" && completed = false`,
48
+ }).then((result: any) => result.totalItems),
49
+
50
+ // Get completed todos count
51
+ pb.collection('todos').getList(1, 1, {
52
+ filter: `organizationId = "${orgId}" && completed = true`,
53
+ }).then((result: any) => result.totalItems),
54
+ ]);
55
+
56
+ return {
57
+ totalUsers: usersResult as number,
58
+ activeTodos: todosResult[0] as number,
59
+ completedTodos: todosResult[1] as number,
60
+ };
61
+ },
62
+ enabled: !!pb,
63
+ refetchInterval: 60000, // Refresh every minute
64
+ });
65
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from 'react';
2
+ import PocketBase from 'pocketbase';
3
+
4
+ /**
5
+ * Get PocketBase instance from host
6
+ *
7
+ * Plugins access PocketBase through the window bridge
8
+ * that was set up by the host's shared state system
9
+ */
10
+ export function usePocketBase() {
11
+ const [pb, setPb] = useState<PocketBase | null>(null);
12
+
13
+ useEffect(() => {
14
+ // Access PocketBase from host's state bridge
15
+ const kernelState = (window as any).__LEGO_KERNEL_STATE__;
16
+
17
+ if (!kernelState) {
18
+ console.error('[Dashboard] Kernel state bridge not found');
19
+ return;
20
+ }
21
+
22
+ // Get PocketBase URL from env or default to localhost
23
+ const pbUrl = import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
24
+ const client = new PocketBase(pbUrl);
25
+
26
+ // Get auth token from host state
27
+ const state = kernelState.useGlobalKernelState.getState();
28
+ if (state.token) {
29
+ client.authStore.save(state.token, null as any);
30
+ }
31
+
32
+ // Listen for token changes
33
+ const unsubscribe = kernelState.useGlobalKernelState.subscribe((newState: any) => {
34
+ if (newState.token && newState.token !== client.authStore.token) {
35
+ client.authStore.save(newState.token, null as any);
36
+ }
37
+ });
38
+
39
+ setPb(client);
40
+
41
+ return () => {
42
+ unsubscribe();
43
+ };
44
+ }, []);
45
+
46
+ return pb;
47
+ }
@@ -0,0 +1,55 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { usePocketBase } from './usePocketBase';
3
+
4
+ export interface Activity {
5
+ id: string;
6
+ action: string;
7
+ resource: string;
8
+ userId: string;
9
+ userName?: string;
10
+ createdAt: string;
11
+ }
12
+
13
+ /**
14
+ * Fetch recent activity feed
15
+ */
16
+ export function useRecentActivity(limit = 10) {
17
+ const pb = usePocketBase();
18
+
19
+ return useQuery({
20
+ queryKey: ['dashboard', 'activity', limit],
21
+ queryFn: async (): Promise<Activity[]> => {
22
+ if (!pb) {
23
+ throw new Error('PocketBase not initialized');
24
+ }
25
+
26
+ // Get current organization from host state
27
+ const kernelState = (window as any).__LEGO_KERNEL_STATE__;
28
+ const state = kernelState?.useGlobalKernelState?.getState();
29
+
30
+ if (!state?.organization?.id) {
31
+ return [];
32
+ }
33
+
34
+ const orgId = state.organization.id;
35
+
36
+ // Fetch recent audit logs
37
+ const result = await pb.collection('audit_logs').getList(1, limit, {
38
+ filter: `organizationId = "${orgId}"`,
39
+ sort: '-created',
40
+ expand: 'userId',
41
+ });
42
+
43
+ return result.items.map((item: any) => ({
44
+ id: item.id,
45
+ action: item.action,
46
+ resource: item.resource,
47
+ userId: item.userId,
48
+ userName: item.expand?.userId?.name || item.expand?.userId?.email || 'Unknown',
49
+ createdAt: item.created,
50
+ }));
51
+ },
52
+ enabled: !!pb,
53
+ refetchInterval: 30000, // Refresh every 30 seconds
54
+ });
55
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }