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,121 @@
1
+ import { useState } from 'react';
2
+ import { CheckSquare, Square, Trash2, Edit2, Calendar } from 'lucide-react';
3
+ import { TodoWithOwner } from '../types';
4
+ import { useUpdateTodo } from '../hooks/useUpdateTodo';
5
+ import { useDeleteTodo } from '../hooks/useDeleteTodo';
6
+ import { cn } from '../lib/utils';
7
+ import { formatRelativeTime } from '../lib/utils';
8
+
9
+ interface TodoItemProps {
10
+ todo: TodoWithOwner;
11
+ }
12
+
13
+ const priorityColors = {
14
+ low: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
15
+ medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
16
+ high: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
17
+ };
18
+
19
+ export function TodoItem({ todo }: TodoItemProps) {
20
+ const updateTodo = useUpdateTodo();
21
+ const deleteTodo = useDeleteTodo();
22
+ const [isEditing, setIsEditing] = useState(false);
23
+
24
+ const handleToggleComplete = () => {
25
+ updateTodo.mutate({
26
+ id: todo.id,
27
+ data: { completed: !todo.completed },
28
+ });
29
+ };
30
+
31
+ const handleDelete = () => {
32
+ if (confirm('Are you sure you want to delete this todo?')) {
33
+ deleteTodo.mutate(todo.id);
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div
39
+ className={cn(
40
+ 'group rounded-lg border p-4 transition-colors hover:bg-muted/50',
41
+ todo.completed && 'opacity-60'
42
+ )}
43
+ >
44
+ <div className="flex items-start gap-3">
45
+ {/* Checkbox */}
46
+ <button
47
+ onClick={handleToggleComplete}
48
+ disabled={updateTodo.isPending}
49
+ className="mt-0.5 shrink-0"
50
+ >
51
+ {todo.completed ? (
52
+ <CheckSquare className="h-5 w-5 text-primary" />
53
+ ) : (
54
+ <Square className="h-5 w-5 text-muted-foreground" />
55
+ )}
56
+ </button>
57
+
58
+ {/* Content */}
59
+ <div className="flex-1 space-y-1">
60
+ <div className="flex items-start justify-between gap-2">
61
+ <h4
62
+ className={cn(
63
+ 'font-medium',
64
+ todo.completed && 'line-through text-muted-foreground'
65
+ )}
66
+ >
67
+ {todo.title}
68
+ </h4>
69
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
70
+ <button
71
+ onClick={() => setIsEditing(true)}
72
+ className="rounded p-1 hover:bg-muted"
73
+ >
74
+ <Edit2 className="h-4 w-4" />
75
+ </button>
76
+ <button
77
+ onClick={handleDelete}
78
+ disabled={deleteTodo.isPending}
79
+ className="rounded p-1 hover:bg-destructive hover:text-destructive-foreground"
80
+ >
81
+ <Trash2 className="h-4 w-4" />
82
+ </button>
83
+ </div>
84
+ </div>
85
+
86
+ {todo.description && (
87
+ <p className="text-sm text-muted-foreground">{todo.description}</p>
88
+ )}
89
+
90
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
91
+ <span className={cn('rounded-full px-2 py-0.5', priorityColors[todo.priority])}>
92
+ {todo.priority}
93
+ </span>
94
+ {todo.dueDate && (
95
+ <span className="flex items-center gap-1">
96
+ <Calendar className="h-3 w-3" />
97
+ {formatRelativeTime(todo.dueDate)}
98
+ </span>
99
+ )}
100
+ {todo.ownerName && (
101
+ <span>Created by {todo.ownerName}</span>
102
+ )}
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ {/* Edit dialog would go here - for now using inline state */}
108
+ {isEditing && (
109
+ <div className="mt-4 pt-4 border-t">
110
+ <p className="text-sm text-muted-foreground">Edit functionality coming soon</p>
111
+ <button
112
+ onClick={() => setIsEditing(false)}
113
+ className="mt-2 text-sm text-primary hover:underline"
114
+ >
115
+ Close
116
+ </button>
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,41 @@
1
+ import { TodoWithOwner } from '../types';
2
+ import { TodoItem } from './TodoItem';
3
+
4
+ interface TodoListProps {
5
+ todos: TodoWithOwner[];
6
+ isLoading?: boolean;
7
+ }
8
+
9
+ export function TodoList({ todos, isLoading }: TodoListProps) {
10
+ if (isLoading) {
11
+ return (
12
+ <div className="space-y-3">
13
+ {[1, 2, 3, 4, 5].map((i) => (
14
+ <div key={i} className="h-20 animate-pulse rounded-lg bg-muted" />
15
+ ))}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ if (todos.length === 0) {
21
+ return (
22
+ <div className="flex flex-col items-center justify-center py-12 text-center">
23
+ <div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
24
+ <span className="text-2xl">📝</span>
25
+ </div>
26
+ <h3 className="mt-4 text-lg font-semibold">No todos found</h3>
27
+ <p className="mt-2 text-sm text-muted-foreground">
28
+ Create your first todo to get started
29
+ </p>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ return (
35
+ <div className="space-y-3">
36
+ {todos.map((todo) => (
37
+ <TodoItem key={todo.id} todo={todo} />
38
+ ))}
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,6 @@
1
+ export * from './TodoForm';
2
+ export * from './TodoItem';
3
+ export * from './TodoList';
4
+ export * from './TodoDialog';
5
+ export * from './TodoFilters';
6
+ export * from './SidebarWidget';
@@ -0,0 +1,59 @@
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
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 221.2 83.2% 53.3%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 221.2 83.2% 53.3%;
26
+ --radius: 0.5rem;
27
+ }
28
+
29
+ .dark {
30
+ --background: 222.2 84% 4.9%;
31
+ --foreground: 210 40% 98%;
32
+ --card: 222.2 84% 4.9%;
33
+ --card-foreground: 210 40% 98%;
34
+ --popover: 222.2 84% 4.9%;
35
+ --popover-foreground: 210 40% 98%;
36
+ --primary: 217.2 91.2% 59.8%;
37
+ --primary-foreground: 222.2 47.4% 11.2%;
38
+ --secondary: 217.2 32.6% 17.5%;
39
+ --secondary-foreground: 210 40% 98%;
40
+ --muted: 217.2 32.6% 17.5%;
41
+ --muted-foreground: 215 20.2% 65.1%;
42
+ --accent: 217.2 32.6% 17.5%;
43
+ --accent-foreground: 210 40% 98%;
44
+ --destructive: 0 62.8% 30.6%;
45
+ --destructive-foreground: 210 40% 98%;
46
+ --border: 217.2 32.6% 17.5%;
47
+ --input: 217.2 32.6% 17.5%;
48
+ --ring: 224.3 76.3% 48%;
49
+ }
50
+ }
51
+
52
+ @layer base {
53
+ * {
54
+ @apply border-border;
55
+ }
56
+ body {
57
+ @apply bg-background text-foreground;
58
+ }
59
+ }
@@ -0,0 +1,62 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { usePocketBase } from './usePocketBase';
3
+ import type { TodoFormData } from '../types';
4
+
5
+ export function useCreateTodo() {
6
+ const pb = usePocketBase();
7
+ const queryClient = useQueryClient();
8
+
9
+ return useMutation({
10
+ mutationFn: async (data: TodoFormData) => {
11
+ if (!pb) {
12
+ throw new Error('PocketBase not initialized');
13
+ }
14
+
15
+ const kernelState = (window as any).__LEGO_KERNEL_STATE__;
16
+ const state = kernelState?.useGlobalKernelState?.getState();
17
+
18
+ if (!state?.organization?.id) {
19
+ throw new Error('No organization selected');
20
+ }
21
+
22
+ const result = await pb.collection('todos').create({
23
+ title: data.title,
24
+ description: data.description || '',
25
+ completed: false,
26
+ priority: data.priority,
27
+ dueDate: data.dueDate || null,
28
+ organizationId: state.organization.id,
29
+ ownerId: state.user?.id,
30
+ });
31
+
32
+ return result;
33
+ },
34
+ onSuccess: () => {
35
+ queryClient.invalidateQueries({ queryKey: ['todos'] });
36
+ const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
37
+ if (channelBus) {
38
+ channelBus.publish('lego:toast', {
39
+ channel: 'lego:toast',
40
+ data: {
41
+ type: 'success',
42
+ title: 'Todo created',
43
+ description: 'Your todo has been created successfully',
44
+ },
45
+ });
46
+ }
47
+ },
48
+ onError: (error: any) => {
49
+ const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
50
+ if (channelBus) {
51
+ channelBus.publish('lego:toast', {
52
+ channel: 'lego:toast',
53
+ data: {
54
+ type: 'error',
55
+ title: 'Failed to create todo',
56
+ description: error.message || 'An error occurred',
57
+ },
58
+ });
59
+ }
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,46 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { usePocketBase } from './usePocketBase';
3
+
4
+ export function useDeleteTodo() {
5
+ const pb = usePocketBase();
6
+ const queryClient = useQueryClient();
7
+
8
+ return useMutation({
9
+ mutationFn: async (id: string) => {
10
+ if (!pb) {
11
+ throw new Error('PocketBase not initialized');
12
+ }
13
+
14
+ await pb.collection('todos').delete(id);
15
+
16
+ return id;
17
+ },
18
+ onSuccess: () => {
19
+ queryClient.invalidateQueries({ queryKey: ['todos'] });
20
+ const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
21
+ if (channelBus) {
22
+ channelBus.publish('lego:toast', {
23
+ channel: 'lego:toast',
24
+ data: {
25
+ type: 'success',
26
+ title: 'Todo deleted',
27
+ description: 'Your todo has been deleted',
28
+ },
29
+ });
30
+ }
31
+ },
32
+ onError: (error: any) => {
33
+ const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
34
+ if (channelBus) {
35
+ channelBus.publish('lego:toast', {
36
+ channel: 'lego:toast',
37
+ data: {
38
+ type: 'error',
39
+ title: 'Failed to delete todo',
40
+ description: error.message || 'An error occurred',
41
+ },
42
+ });
43
+ }
44
+ },
45
+ });
46
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from 'react';
2
+ import PocketBase from 'pocketbase';
3
+
4
+ export function usePocketBase() {
5
+ const [pb, setPb] = useState<PocketBase | null>(null);
6
+
7
+ useEffect(() => {
8
+ const kernelState = (window as any).__LEGO_KERNEL_STATE__;
9
+
10
+ if (!kernelState) {
11
+ console.error('[Todo] Kernel state bridge not found');
12
+ return;
13
+ }
14
+
15
+ const pbUrl = import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
16
+ const client = new PocketBase(pbUrl);
17
+
18
+ // Sync auth token
19
+ const state = kernelState.useGlobalKernelState.getState();
20
+ if (state.token) {
21
+ client.authStore.save(state.token, null as any);
22
+ }
23
+
24
+ const unsubscribe = kernelState.useGlobalKernelState.subscribe((newState: any) => {
25
+ if (newState.token && newState.token !== client.authStore.token) {
26
+ client.authStore.save(newState.token, null as any);
27
+ }
28
+ });
29
+
30
+ setPb(client);
31
+
32
+ return () => {
33
+ unsubscribe();
34
+ };
35
+ }, []);
36
+
37
+ return pb;
38
+ }
@@ -0,0 +1,64 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { usePocketBase } from './usePocketBase';
3
+ import type { TodoWithOwner, TodoFilters } from '../types';
4
+
5
+ export function useTodos(filters: TodoFilters = {}) {
6
+ const pb = usePocketBase();
7
+
8
+ return useQuery({
9
+ queryKey: ['todos', filters],
10
+ queryFn: async (): Promise<TodoWithOwner[]> => {
11
+ if (!pb) {
12
+ throw new Error('PocketBase not initialized');
13
+ }
14
+
15
+ const kernelState = (window as any).__LEGO_KERNEL_STATE__;
16
+ const state = kernelState?.useGlobalKernelState?.getState();
17
+
18
+ if (!state?.organization?.id) {
19
+ return [];
20
+ }
21
+
22
+ const orgId = state.organization.id;
23
+
24
+ // Build filter
25
+ let filterParts: string[] = [`organizationId = "${orgId}"`];
26
+
27
+ if (filters.status === 'active') {
28
+ filterParts.push('completed = false');
29
+ } else if (filters.status === 'completed') {
30
+ filterParts.push('completed = true');
31
+ }
32
+
33
+ if (filters.priority && filters.priority !== 'all') {
34
+ filterParts.push(`priority = "${filters.priority}"`);
35
+ }
36
+
37
+ if (filters.search) {
38
+ filterParts.push(`title ~ "${filters.search}"`);
39
+ }
40
+
41
+ const result = await pb.collection('todos').getList(1, 50, {
42
+ filter: filterParts.join(' && '),
43
+ sort: filters.status === 'completed' ? '-updated,-created' : '-created',
44
+ expand: 'ownerId',
45
+ });
46
+
47
+ return result.items.map((item: any) => ({
48
+ id: item.id,
49
+ title: item.title,
50
+ description: item.description,
51
+ completed: item.completed,
52
+ priority: item.priority,
53
+ dueDate: item.dueDate,
54
+ organizationId: item.organizationId,
55
+ ownerId: item.ownerId,
56
+ created: item.created,
57
+ updated: item.updated,
58
+ ownerName: item.expand?.ownerId?.name || item.expand?.ownerId?.email,
59
+ ownerEmail: item.expand?.ownerId?.email,
60
+ }));
61
+ },
62
+ enabled: !!pb,
63
+ });
64
+ }
@@ -0,0 +1,35 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { usePocketBase } from './usePocketBase';
3
+
4
+ export function useUpdateTodo() {
5
+ const pb = usePocketBase();
6
+ const queryClient = useQueryClient();
7
+
8
+ return useMutation({
9
+ mutationFn: async ({ id, data }: { id: string; data: Partial<any> }) => {
10
+ if (!pb) {
11
+ throw new Error('PocketBase not initialized');
12
+ }
13
+
14
+ const result = await pb.collection('todos').update(id, data);
15
+
16
+ return result;
17
+ },
18
+ onSuccess: () => {
19
+ queryClient.invalidateQueries({ queryKey: ['todos'] });
20
+ },
21
+ onError: (error: any) => {
22
+ const channelBus = (window as any).__LEGO_CHANNEL_BUS__;
23
+ if (channelBus) {
24
+ channelBus.publish('lego:toast', {
25
+ channel: 'lego:toast',
26
+ data: {
27
+ type: 'error',
28
+ title: 'Failed to update todo',
29
+ description: error.message || 'An error occurred',
30
+ },
31
+ });
32
+ }
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,5 @@
1
+ import '@modern-js/runtime/plugins';
2
+ import './global.css';
3
+ import { App } from './App';
4
+
5
+ export { App };
@@ -0,0 +1,20 @@
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
+ }
7
+
8
+ export function formatRelativeTime(dateString: string): string {
9
+ const date = new Date(dateString);
10
+ const now = new Date();
11
+ const diffMs = now.getTime() - date.getTime();
12
+ const diffMins = Math.floor(diffMs / 60000);
13
+ const diffHours = Math.floor(diffMs / 3600000);
14
+ const diffDays = Math.floor(diffMs / 86400000);
15
+
16
+ if (diffMins < 1) return 'just now';
17
+ if (diffMins < 60) return `${diffMins}m ago`;
18
+ if (diffHours < 24) return `${diffHours}h ago`;
19
+ return `${diffDays}d ago`;
20
+ }
@@ -0,0 +1,89 @@
1
+ import { useState } from 'react';
2
+ import { Plus } from 'lucide-react';
3
+ import { TodoList } from '../components/TodoList';
4
+ import { TodoDialog } from '../components/TodoDialog';
5
+ import { TodoFilters } from '../components/TodoFilters';
6
+ import { useTodos } from '../hooks/useTodos';
7
+ import { useCreateTodo } from '../hooks/useCreateTodo';
8
+ import { TodoFilters as TodoFiltersType } from '../types';
9
+ import { todoFormSchema, type TodoFormInput } from '../schemas';
10
+
11
+ export function TodoPage() {
12
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
13
+ const [filters, setFilters] = useState<TodoFiltersType>({
14
+ status: 'all',
15
+ priority: 'all',
16
+ search: '',
17
+ });
18
+
19
+ const { data: todos = [], isLoading } = useTodos(filters);
20
+ const createTodo = useCreateTodo();
21
+
22
+ const handleCreateTodo = (data: TodoFormInput) => {
23
+ createTodo.mutate(data, {
24
+ onSuccess: () => {
25
+ setIsDialogOpen(false);
26
+ },
27
+ });
28
+ };
29
+
30
+ const filteredTodos = todos.filter((todo) => {
31
+ // Status filter
32
+ if (filters.status === 'active' && todo.completed) return false;
33
+ if (filters.status === 'completed' && !todo.completed) return false;
34
+
35
+ // Priority filter
36
+ if (filters.priority !== 'all' && todo.priority !== filters.priority) {
37
+ return false;
38
+ }
39
+
40
+ // Search filter
41
+ if (filters.search) {
42
+ const searchLower = filters.search.toLowerCase();
43
+ const matchesTitle = todo.title.toLowerCase().includes(searchLower);
44
+ const matchesDescription = todo.description?.toLowerCase().includes(searchLower);
45
+ if (!matchesTitle && !matchesDescription) return false;
46
+ }
47
+
48
+ return true;
49
+ });
50
+
51
+ return (
52
+ <div className="container mx-auto max-w-4xl py-8 px-4">
53
+ {/* Header */}
54
+ <div className="mb-8">
55
+ <h1 className="text-3xl font-bold tracking-tight">Todos</h1>
56
+ <p className="mt-2 text-muted-foreground">
57
+ Manage your tasks and stay organized
58
+ </p>
59
+ </div>
60
+
61
+ {/* Actions */}
62
+ <div className="mb-6 flex items-center justify-between">
63
+ <TodoFilters
64
+ filters={filters}
65
+ onFiltersChange={setFilters}
66
+ todoCount={filteredTodos.length}
67
+ />
68
+ <button
69
+ onClick={() => setIsDialogOpen(true)}
70
+ className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
71
+ >
72
+ <Plus className="h-4 w-4" />
73
+ New Todo
74
+ </button>
75
+ </div>
76
+
77
+ {/* Todo List */}
78
+ <TodoList todos={filteredTodos} isLoading={isLoading} />
79
+
80
+ {/* Create Dialog */}
81
+ <TodoDialog
82
+ open={isDialogOpen}
83
+ onOpenChange={setIsDialogOpen}
84
+ onSubmit={handleCreateTodo}
85
+ isLoading={createTodo.isPending}
86
+ />
87
+ </div>
88
+ );
89
+ }