cognova 0.1.0

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 (205) hide show
  1. package/.env.example +58 -0
  2. package/Claude/CLAUDE.md +92 -0
  3. package/Claude/hooks/lib/__init__.py +1 -0
  4. package/Claude/hooks/lib/hook_client.py +207 -0
  5. package/Claude/hooks/log-event.py +97 -0
  6. package/Claude/hooks/pre-compact.py +46 -0
  7. package/Claude/hooks/session-end.py +26 -0
  8. package/Claude/hooks/session-start.py +35 -0
  9. package/Claude/hooks/stop-extract.py +40 -0
  10. package/Claude/rules/frontmatter.md +54 -0
  11. package/Claude/rules/markdown.md +43 -0
  12. package/Claude/rules/note-organization.md +33 -0
  13. package/Claude/settings.json +54 -0
  14. package/Claude/skills/README.md +136 -0
  15. package/Claude/skills/_lib/__init__.py +1 -0
  16. package/Claude/skills/_lib/api.py +164 -0
  17. package/Claude/skills/_lib/output.py +95 -0
  18. package/Claude/skills/environment/SKILL.md +73 -0
  19. package/Claude/skills/environment/environment.py +239 -0
  20. package/Claude/skills/memory/SKILL.md +153 -0
  21. package/Claude/skills/memory/memory.py +270 -0
  22. package/Claude/skills/project/SKILL.md +105 -0
  23. package/Claude/skills/project/project.py +203 -0
  24. package/Claude/skills/skill-creator/SKILL.md +261 -0
  25. package/Claude/skills/task/SKILL.md +135 -0
  26. package/Claude/skills/task/task.py +310 -0
  27. package/LICENSE +21 -0
  28. package/README.md +176 -0
  29. package/app/app.config.ts +8 -0
  30. package/app/app.vue +39 -0
  31. package/app/assets/css/main.css +10 -0
  32. package/app/components/AppLogo.vue +40 -0
  33. package/app/components/AssistantPanel.client.vue +518 -0
  34. package/app/components/ConfirmModal.vue +84 -0
  35. package/app/components/TemplateMenu.vue +49 -0
  36. package/app/components/agents/AgentActivityChart.client.vue +105 -0
  37. package/app/components/agents/AgentActivityChart.server.vue +25 -0
  38. package/app/components/agents/AgentForm.vue +304 -0
  39. package/app/components/agents/AgentRunModal.vue +154 -0
  40. package/app/components/agents/AgentStatsCards.vue +98 -0
  41. package/app/components/chat/ChatInput.vue +85 -0
  42. package/app/components/chat/ConversationList.vue +78 -0
  43. package/app/components/chat/MessageBubble.vue +81 -0
  44. package/app/components/chat/StreamingMessage.vue +36 -0
  45. package/app/components/chat/ToolCallBlock.vue +77 -0
  46. package/app/components/editor/CodeEditor.client.vue +212 -0
  47. package/app/components/editor/CodeEditorFallback.vue +12 -0
  48. package/app/components/editor/DocumentEditor.vue +326 -0
  49. package/app/components/editor/DocumentMetadata.vue +140 -0
  50. package/app/components/editor/MarkdownEditor.vue +146 -0
  51. package/app/components/files/FileTree.vue +436 -0
  52. package/app/components/hooks/HookActivityChart.client.vue +117 -0
  53. package/app/components/hooks/HookActivityChart.server.vue +25 -0
  54. package/app/components/hooks/HookStatsCards.vue +63 -0
  55. package/app/components/hooks/RecentEventsTable.vue +123 -0
  56. package/app/components/hooks/ToolBreakdownTable.vue +72 -0
  57. package/app/components/search/DashboardSearch.vue +122 -0
  58. package/app/components/tasks/ProjectSelect.vue +35 -0
  59. package/app/components/tasks/TaskCard.vue +182 -0
  60. package/app/components/tasks/TaskDetail.vue +160 -0
  61. package/app/components/tasks/TaskForm.vue +280 -0
  62. package/app/components/tasks/TaskList.vue +69 -0
  63. package/app/components/view/ViewToc.vue +85 -0
  64. package/app/composables/useAgents.ts +153 -0
  65. package/app/composables/useAuth.ts +73 -0
  66. package/app/composables/useChat.ts +298 -0
  67. package/app/composables/useDocument.ts +141 -0
  68. package/app/composables/useEditor.ts +100 -0
  69. package/app/composables/useFileTree.ts +220 -0
  70. package/app/composables/useHookEvents.ts +68 -0
  71. package/app/composables/useMemories.ts +83 -0
  72. package/app/composables/useNotificationBus.ts +154 -0
  73. package/app/composables/usePreferences.ts +131 -0
  74. package/app/composables/useProjects.ts +97 -0
  75. package/app/composables/useSearch.ts +52 -0
  76. package/app/composables/useTasks.ts +201 -0
  77. package/app/composables/useTerminal.ts +135 -0
  78. package/app/layouts/auth.vue +20 -0
  79. package/app/layouts/dashboard.vue +186 -0
  80. package/app/layouts/view.vue +60 -0
  81. package/app/middleware/auth.ts +9 -0
  82. package/app/pages/agents/[id].vue +602 -0
  83. package/app/pages/agents/index.vue +412 -0
  84. package/app/pages/chat.vue +146 -0
  85. package/app/pages/dashboard.vue +80 -0
  86. package/app/pages/docs.vue +131 -0
  87. package/app/pages/hooks.vue +163 -0
  88. package/app/pages/index.vue +249 -0
  89. package/app/pages/login.vue +60 -0
  90. package/app/pages/memories.vue +282 -0
  91. package/app/pages/settings.vue +625 -0
  92. package/app/pages/tasks.vue +312 -0
  93. package/app/pages/view/[uuid].vue +376 -0
  94. package/dist/cli/index.js +2711 -0
  95. package/drizzle.config.ts +10 -0
  96. package/nuxt.config.ts +98 -0
  97. package/package.json +107 -0
  98. package/server/api/agents/[id]/cancel.post.ts +27 -0
  99. package/server/api/agents/[id]/run.post.ts +34 -0
  100. package/server/api/agents/[id]/runs.get.ts +45 -0
  101. package/server/api/agents/[id]/stats.get.ts +94 -0
  102. package/server/api/agents/[id].delete.ts +29 -0
  103. package/server/api/agents/[id].get.ts +25 -0
  104. package/server/api/agents/[id].patch.ts +55 -0
  105. package/server/api/agents/index.get.ts +15 -0
  106. package/server/api/agents/index.post.ts +48 -0
  107. package/server/api/agents/stats.get.ts +86 -0
  108. package/server/api/auth/[...all].ts +5 -0
  109. package/server/api/conversations/[id].delete.ts +16 -0
  110. package/server/api/conversations/[id].get.ts +34 -0
  111. package/server/api/conversations/index.get.ts +17 -0
  112. package/server/api/documents/[id]/index.delete.ts +47 -0
  113. package/server/api/documents/[id]/index.put.ts +102 -0
  114. package/server/api/documents/[id]/public.get.ts +60 -0
  115. package/server/api/documents/[id]/restore.post.ts +65 -0
  116. package/server/api/documents/by-path.post.ts +168 -0
  117. package/server/api/documents/index.get.ts +48 -0
  118. package/server/api/fs/delete.post.ts +41 -0
  119. package/server/api/fs/list.get.ts +99 -0
  120. package/server/api/fs/mkdir.post.ts +44 -0
  121. package/server/api/fs/move.post.ts +68 -0
  122. package/server/api/fs/read.post.ts +48 -0
  123. package/server/api/fs/rename.post.ts +55 -0
  124. package/server/api/fs/write.post.ts +51 -0
  125. package/server/api/health.get.ts +40 -0
  126. package/server/api/home.get.ts +26 -0
  127. package/server/api/hooks/events/index.get.ts +56 -0
  128. package/server/api/hooks/events/index.post.ts +36 -0
  129. package/server/api/hooks/stats.get.ts +99 -0
  130. package/server/api/memory/[id].delete.ts +26 -0
  131. package/server/api/memory/context.get.ts +83 -0
  132. package/server/api/memory/extract.post.ts +42 -0
  133. package/server/api/memory/search.get.ts +70 -0
  134. package/server/api/memory/store.post.ts +31 -0
  135. package/server/api/projects/[id]/index.delete.ts +40 -0
  136. package/server/api/projects/[id]/index.get.ts +25 -0
  137. package/server/api/projects/[id]/index.put.ts +50 -0
  138. package/server/api/projects/index.get.ts +20 -0
  139. package/server/api/projects/index.post.ts +34 -0
  140. package/server/api/secrets/[key].delete.ts +31 -0
  141. package/server/api/secrets/[key].get.ts +30 -0
  142. package/server/api/secrets/[key].put.ts +52 -0
  143. package/server/api/secrets/index.get.ts +20 -0
  144. package/server/api/secrets/index.post.ts +58 -0
  145. package/server/api/tasks/[id]/index.delete.ts +46 -0
  146. package/server/api/tasks/[id]/index.get.ts +24 -0
  147. package/server/api/tasks/[id]/index.put.ts +70 -0
  148. package/server/api/tasks/[id]/restore.post.ts +49 -0
  149. package/server/api/tasks/index.get.ts +53 -0
  150. package/server/api/tasks/index.post.ts +47 -0
  151. package/server/api/tasks/tags.get.ts +21 -0
  152. package/server/api/user/email.patch.ts +56 -0
  153. package/server/db/index.ts +76 -0
  154. package/server/db/migrate.ts +41 -0
  155. package/server/db/schema.ts +345 -0
  156. package/server/db/seed.ts +46 -0
  157. package/server/db/types.ts +28 -0
  158. package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
  159. package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
  160. package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
  161. package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
  162. package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
  163. package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
  164. package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
  165. package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
  166. package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
  167. package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
  168. package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
  169. package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
  170. package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
  171. package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
  172. package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
  173. package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
  174. package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
  175. package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
  176. package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
  177. package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
  178. package/server/drizzle/migrations/meta/_journal.json +76 -0
  179. package/server/middleware/auth.ts +79 -0
  180. package/server/plugins/00.env-validate.ts +38 -0
  181. package/server/plugins/01.api-token.ts +31 -0
  182. package/server/plugins/02.database.ts +54 -0
  183. package/server/plugins/03.file-watcher.ts +65 -0
  184. package/server/plugins/04.cron-agents.ts +26 -0
  185. package/server/routes/_ws/chat.ts +252 -0
  186. package/server/routes/notifications.ts +47 -0
  187. package/server/routes/terminal.ts +98 -0
  188. package/server/services/agent-executor.ts +218 -0
  189. package/server/services/cron-scheduler.ts +78 -0
  190. package/server/services/memory-extractor.ts +120 -0
  191. package/server/utils/agent-cleanup.ts +91 -0
  192. package/server/utils/agent-registry.ts +95 -0
  193. package/server/utils/auth.ts +33 -0
  194. package/server/utils/chat-session-manager.ts +59 -0
  195. package/server/utils/crypto.ts +40 -0
  196. package/server/utils/db-guard.ts +12 -0
  197. package/server/utils/db-state.ts +63 -0
  198. package/server/utils/document-sync.ts +207 -0
  199. package/server/utils/frontmatter.ts +84 -0
  200. package/server/utils/notification-bus.ts +60 -0
  201. package/server/utils/path-validator.ts +55 -0
  202. package/server/utils/pty-manager.ts +130 -0
  203. package/shared/types/index.ts +604 -0
  204. package/shared/utils/language-detection.ts +87 -0
  205. package/tsconfig.json +10 -0
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ import { formatDistanceToNow } from 'date-fns'
3
+ import type { HookEvent } from '~~/shared/types'
4
+
5
+ defineProps<{
6
+ events: HookEvent[]
7
+ loading?: boolean
8
+ }>()
9
+
10
+ function getEventTypeColor(type: string): 'success' | 'warning' | 'error' | 'info' | 'neutral' {
11
+ switch (type) {
12
+ case 'SessionStart': return 'success'
13
+ case 'SessionEnd': return 'neutral'
14
+ case 'PreToolUse': return 'info'
15
+ case 'PostToolUse': return 'success'
16
+ case 'PostToolUseFailure': return 'error'
17
+ case 'UserPromptSubmit': return 'info'
18
+ default: return 'neutral'
19
+ }
20
+ }
21
+
22
+ function formatTime(date: Date | string): string {
23
+ return formatDistanceToNow(new Date(date), { addSuffix: true })
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <UCard>
29
+ <template #header>
30
+ <p class="text-sm font-medium">
31
+ Recent Events
32
+ </p>
33
+ </template>
34
+
35
+ <div
36
+ v-if="loading"
37
+ class="space-y-3"
38
+ >
39
+ <USkeleton
40
+ v-for="i in 10"
41
+ :key="i"
42
+ class="h-10 w-full"
43
+ />
44
+ </div>
45
+
46
+ <div
47
+ v-else-if="events.length === 0"
48
+ class="text-center text-muted py-8"
49
+ >
50
+ No events recorded yet
51
+ </div>
52
+
53
+ <UTable
54
+ v-else
55
+ :data="events"
56
+ :columns="[
57
+ { accessorKey: 'eventType', header: 'Event' },
58
+ { accessorKey: 'toolName', header: 'Tool' },
59
+ { accessorKey: 'blocked', header: 'Status' },
60
+ { accessorKey: 'durationMs', header: 'Duration' },
61
+ { accessorKey: 'createdAt', header: 'Time' }
62
+ ]"
63
+ >
64
+ <template #eventType-cell="{ row }">
65
+ <UBadge
66
+ :color="getEventTypeColor(row.original.eventType)"
67
+ variant="subtle"
68
+ size="sm"
69
+ >
70
+ {{ row.original.eventType }}
71
+ </UBadge>
72
+ </template>
73
+ <template #toolName-cell="{ row }">
74
+ <span
75
+ v-if="row.original.toolName"
76
+ class="font-mono text-sm"
77
+ >
78
+ {{ row.original.toolName }}
79
+ </span>
80
+ <span
81
+ v-else
82
+ class="text-muted"
83
+ >
84
+ -
85
+ </span>
86
+ </template>
87
+ <template #blocked-cell="{ row }">
88
+ <UBadge
89
+ v-if="row.original.blocked"
90
+ color="error"
91
+ variant="subtle"
92
+ size="sm"
93
+ >
94
+ Blocked
95
+ </UBadge>
96
+ <UBadge
97
+ v-else
98
+ color="success"
99
+ variant="subtle"
100
+ size="sm"
101
+ >
102
+ Allowed
103
+ </UBadge>
104
+ </template>
105
+ <template #durationMs-cell="{ row }">
106
+ <span v-if="row.original.durationMs">
107
+ {{ row.original.durationMs }}ms
108
+ </span>
109
+ <span
110
+ v-else
111
+ class="text-muted"
112
+ >
113
+ -
114
+ </span>
115
+ </template>
116
+ <template #createdAt-cell="{ row }">
117
+ <span class="text-muted text-sm">
118
+ {{ formatTime(row.original.createdAt) }}
119
+ </span>
120
+ </template>
121
+ </UTable>
122
+ </UCard>
123
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import type { HookToolBreakdown } from '~~/shared/types'
3
+
4
+ defineProps<{
5
+ data: HookToolBreakdown[]
6
+ loading?: boolean
7
+ }>()
8
+
9
+ function formatDuration(ms: number): string {
10
+ if (ms < 1000) return `${ms}ms`
11
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
12
+ return `${(ms / 60000).toFixed(1)}m`
13
+ }
14
+
15
+ function getBlockRateColor(blocked: number, total: number): string {
16
+ const rate = total > 0 ? (blocked / total) * 100 : 0
17
+ if (rate > 50) return 'text-error'
18
+ if (rate > 25) return 'text-warning'
19
+ return 'text-success'
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <UCard>
25
+ <template #header>
26
+ <p class="text-sm font-medium">
27
+ Tool Usage Breakdown
28
+ </p>
29
+ </template>
30
+
31
+ <div
32
+ v-if="loading"
33
+ class="space-y-3"
34
+ >
35
+ <USkeleton
36
+ v-for="i in 5"
37
+ :key="i"
38
+ class="h-10 w-full"
39
+ />
40
+ </div>
41
+
42
+ <div
43
+ v-else-if="data.length === 0"
44
+ class="text-center text-muted py-8"
45
+ >
46
+ No tool usage data yet
47
+ </div>
48
+
49
+ <UTable
50
+ v-else
51
+ :data="data"
52
+ :columns="[
53
+ { accessorKey: 'toolName', header: 'Tool' },
54
+ { accessorKey: 'total', header: 'Total' },
55
+ { accessorKey: 'blocked', header: 'Blocked' },
56
+ { accessorKey: 'avgDurationMs', header: 'Avg Duration' }
57
+ ]"
58
+ >
59
+ <template #toolName-cell="{ row }">
60
+ <span class="font-mono text-sm">{{ row.original.toolName }}</span>
61
+ </template>
62
+ <template #blocked-cell="{ row }">
63
+ <span :class="getBlockRateColor(row.original.blocked, row.original.total)">
64
+ {{ row.original.blocked }}
65
+ </span>
66
+ </template>
67
+ <template #avgDurationMs-cell="{ row }">
68
+ {{ formatDuration(row.original.avgDurationMs) }}
69
+ </template>
70
+ </UTable>
71
+ </UCard>
72
+ </template>
@@ -0,0 +1,122 @@
1
+ <script setup lang="ts">
2
+ import type { CommandPaletteGroup, CommandPaletteItem } from '@nuxt/ui'
3
+
4
+ const router = useRouter()
5
+ const toast = useToast()
6
+ const { searchTerm, loading, results, reset } = useSearch()
7
+
8
+ const statusIcons: Record<string, string> = {
9
+ todo: 'i-lucide-circle',
10
+ in_progress: 'i-lucide-clock',
11
+ done: 'i-lucide-check-circle',
12
+ blocked: 'i-lucide-alert-circle'
13
+ }
14
+
15
+ const priorityLabels: Record<number, string> = {
16
+ 1: 'Low',
17
+ 2: 'Medium',
18
+ 3: 'High'
19
+ }
20
+
21
+ const navigationItems: CommandPaletteItem[] = [
22
+ { label: 'Dashboard', icon: 'i-lucide-layout-dashboard', to: '/dashboard', kbds: ['G', 'D'] },
23
+ { label: 'Tasks', icon: 'i-lucide-check-square', to: '/tasks', kbds: ['G', 'T'] },
24
+ { label: 'Docs', icon: 'i-lucide-file-text', to: '/docs', kbds: ['G', 'O'] },
25
+ { label: 'Settings', icon: 'i-lucide-settings', to: '/settings', kbds: ['G', 'S'] }
26
+ ]
27
+
28
+ const actionItems: CommandPaletteItem[] = [
29
+ {
30
+ label: 'Create new document',
31
+ icon: 'i-lucide-file-plus',
32
+ onSelect: () => {
33
+ router.push('/docs')
34
+ toast.add({
35
+ title: 'Create a document',
36
+ description: 'Right-click in the file tree to create a new file',
37
+ icon: 'i-lucide-info'
38
+ })
39
+ reset()
40
+ }
41
+ },
42
+ {
43
+ label: 'Add new task',
44
+ icon: 'i-lucide-plus-square',
45
+ onSelect: () => {
46
+ router.push('/tasks?action=new')
47
+ reset()
48
+ }
49
+ }
50
+ ]
51
+
52
+ const groups = computed<CommandPaletteGroup[]>(() => {
53
+ const g: CommandPaletteGroup[] = []
54
+
55
+ // Tasks group (only show if we have results)
56
+ if (results.value.tasks.length) {
57
+ g.push({
58
+ id: 'tasks',
59
+ label: 'Tasks',
60
+ ignoreFilter: true,
61
+ items: results.value.tasks.map(t => ({
62
+ label: t.title,
63
+ icon: statusIcons[t.status] || 'i-lucide-circle',
64
+ suffix: t.project?.name || priorityLabels[t.priority] || '',
65
+ onSelect: () => {
66
+ router.push(`/tasks?selected=${t.id}`)
67
+ reset()
68
+ }
69
+ }))
70
+ })
71
+ }
72
+
73
+ // Documents group (only show if we have results)
74
+ if (results.value.documents.length) {
75
+ g.push({
76
+ id: 'documents',
77
+ label: 'Documents',
78
+ ignoreFilter: true,
79
+ items: results.value.documents.map(d => ({
80
+ label: d.title,
81
+ icon: 'i-lucide-file-text',
82
+ suffix: d.path,
83
+ onSelect: () => {
84
+ router.push(`/docs?path=${encodeURIComponent(d.path)}`)
85
+ reset()
86
+ }
87
+ }))
88
+ })
89
+ }
90
+
91
+ // Navigation group (always visible)
92
+ g.push({
93
+ id: 'navigation',
94
+ label: 'Navigation',
95
+ items: navigationItems
96
+ })
97
+
98
+ // Actions group (filtered based on search term)
99
+ g.push({
100
+ id: 'actions',
101
+ label: 'Actions',
102
+ items: actionItems,
103
+ postFilter: (term: string, items: CommandPaletteItem[]) => {
104
+ if (!term) return items
105
+ const lower = term.toLowerCase()
106
+ return ['new', 'create', 'add'].some(k => lower.includes(k)) ? items : []
107
+ }
108
+ })
109
+
110
+ return g
111
+ })
112
+ </script>
113
+
114
+ <template>
115
+ <UDashboardSearch
116
+ v-model:search-term="searchTerm"
117
+ :groups="groups"
118
+ :loading="loading"
119
+ placeholder="Search tasks, docs, or type a command..."
120
+ :fuse="{ resultLimit: 10 }"
121
+ />
122
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ import type { Project } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ modelValue?: string | null
6
+ projects: Project[]
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ 'update:modelValue': [value: string | null]
11
+ }>()
12
+
13
+ const NONE_VALUE = '__none__'
14
+
15
+ const selectedId = computed({
16
+ get: () => props.modelValue || NONE_VALUE,
17
+ set: value => emit('update:modelValue', value === NONE_VALUE ? null : value)
18
+ })
19
+
20
+ const options = computed(() => [
21
+ { value: NONE_VALUE, label: 'No Project' },
22
+ ...props.projects
23
+ .filter(p => !p.deletedAt)
24
+ .map(p => ({ value: p.id, label: p.name }))
25
+ ])
26
+ </script>
27
+
28
+ <template>
29
+ <USelect
30
+ v-model="selectedId"
31
+ :items="options"
32
+ value-key="value"
33
+ class="w-full"
34
+ />
35
+ </template>
@@ -0,0 +1,182 @@
1
+ <script setup lang="ts">
2
+ import type { Task } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ task: Task
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ toggle: [id: string]
10
+ edit: [task: Task]
11
+ delete: [id: string]
12
+ view: [task: Task]
13
+ }>()
14
+
15
+ const priorityConfig = {
16
+ 1: { label: 'Low', color: 'neutral' as const, icon: 'i-lucide-arrow-down' },
17
+ 2: { label: 'Medium', color: 'warning' as const, icon: 'i-lucide-minus' },
18
+ 3: { label: 'High', color: 'error' as const, icon: 'i-lucide-arrow-up' }
19
+ }
20
+
21
+ const statusConfig = {
22
+ todo: { label: 'Todo', color: 'neutral' as const, icon: 'i-lucide-circle' },
23
+ in_progress: { label: 'In Progress', color: 'info' as const, icon: 'i-lucide-clock' },
24
+ done: { label: 'Done', color: 'success' as const, icon: 'i-lucide-check-circle' },
25
+ blocked: { label: 'Blocked', color: 'error' as const, icon: 'i-lucide-alert-circle' }
26
+ }
27
+
28
+ const priority = computed(() => priorityConfig[props.task.priority as 1 | 2 | 3] || priorityConfig[2])
29
+ const status = computed(() => statusConfig[props.task.status])
30
+ const isDone = computed(() => props.task.status === 'done')
31
+ const hasDescription = computed(() => !!props.task.description?.trim())
32
+
33
+ const isOverdue = computed(() => {
34
+ if (!props.task.dueDate || isDone.value) return false
35
+ return new Date(props.task.dueDate) < new Date()
36
+ })
37
+
38
+ function formatDate(date: Date | string | undefined) {
39
+ if (!date) return null
40
+ const d = new Date(date)
41
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
42
+ }
43
+
44
+ const menuItems = computed(() => [[
45
+ {
46
+ label: 'Edit',
47
+ icon: 'i-lucide-pencil',
48
+ onSelect: () => emit('edit', props.task)
49
+ },
50
+ {
51
+ label: 'Delete',
52
+ icon: 'i-lucide-trash-2',
53
+ color: 'error' as const,
54
+ onSelect: () => emit('delete', props.task.id)
55
+ }
56
+ ]])
57
+ </script>
58
+
59
+ <template>
60
+ <div
61
+ class="group flex items-start gap-3 p-3 rounded-lg border border-default hover:bg-elevated transition-colors"
62
+ :class="{ 'opacity-60': isDone }"
63
+ >
64
+ <!-- Checkbox -->
65
+ <button
66
+ class="mt-0.5 shrink-0"
67
+ @click.stop="emit('toggle', task.id)"
68
+ >
69
+ <UIcon
70
+ :name="isDone ? 'i-lucide-check-circle-2' : 'i-lucide-circle'"
71
+ class="size-5"
72
+ :class="isDone ? 'text-success' : 'text-dimmed hover:text-default'"
73
+ />
74
+ </button>
75
+
76
+ <!-- Content (clickable for details) -->
77
+ <button
78
+ class="flex-1 min-w-0 text-left"
79
+ @click="emit('view', task)"
80
+ >
81
+ <div class="flex items-center gap-2">
82
+ <span
83
+ class="font-medium"
84
+ :class="{ 'line-through text-dimmed': isDone }"
85
+ >
86
+ {{ task.title }}
87
+ </span>
88
+ <UIcon
89
+ v-if="hasDescription"
90
+ name="i-lucide-file-text"
91
+ class="size-3.5 text-dimmed shrink-0"
92
+ />
93
+ </div>
94
+
95
+ <!-- Meta row -->
96
+ <div class="flex items-center flex-wrap gap-2 mt-2">
97
+ <!-- Project badge -->
98
+ <UBadge
99
+ v-if="task.project"
100
+ variant="subtle"
101
+ color="neutral"
102
+ size="xs"
103
+ >
104
+ <span
105
+ class="size-2 rounded-full mr-1"
106
+ :style="{ backgroundColor: task.project.color }"
107
+ />
108
+ {{ task.project.name }}
109
+ </UBadge>
110
+
111
+ <!-- Priority badge -->
112
+ <UBadge
113
+ v-if="task.priority !== 2"
114
+ :color="priority.color"
115
+ variant="subtle"
116
+ size="xs"
117
+ >
118
+ <UIcon
119
+ :name="priority.icon"
120
+ class="size-3 mr-1"
121
+ />
122
+ {{ priority.label }}
123
+ </UBadge>
124
+
125
+ <!-- Status badge (if not todo or done) -->
126
+ <UBadge
127
+ v-if="task.status === 'in_progress' || task.status === 'blocked'"
128
+ :color="status.color"
129
+ variant="subtle"
130
+ size="xs"
131
+ >
132
+ <UIcon
133
+ :name="status.icon"
134
+ class="size-3 mr-1"
135
+ />
136
+ {{ status.label }}
137
+ </UBadge>
138
+
139
+ <!-- Due date -->
140
+ <span
141
+ v-if="task.dueDate"
142
+ class="text-xs flex items-center gap-1"
143
+ :class="isOverdue ? 'text-error' : 'text-dimmed'"
144
+ >
145
+ <UIcon
146
+ name="i-lucide-calendar"
147
+ class="size-3"
148
+ />
149
+ {{ formatDate(task.dueDate) }}
150
+ </span>
151
+
152
+ <!-- Tags -->
153
+ <UBadge
154
+ v-for="tag in task.tags?.slice(0, 3)"
155
+ :key="tag"
156
+ variant="subtle"
157
+ color="neutral"
158
+ size="xs"
159
+ >
160
+ {{ tag }}
161
+ </UBadge>
162
+ <span
163
+ v-if="task.tags && task.tags.length > 3"
164
+ class="text-xs text-dimmed"
165
+ >
166
+ +{{ task.tags.length - 3 }}
167
+ </span>
168
+ </div>
169
+ </button>
170
+
171
+ <!-- Actions -->
172
+ <UDropdownMenu :items="menuItems">
173
+ <UButton
174
+ icon="i-lucide-more-vertical"
175
+ color="neutral"
176
+ variant="ghost"
177
+ size="xs"
178
+ class="opacity-0 group-hover:opacity-100 transition-opacity"
179
+ />
180
+ </UDropdownMenu>
181
+ </div>
182
+ </template>
@@ -0,0 +1,160 @@
1
+ <script setup lang="ts">
2
+ import type { Task } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ task: Task
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ edit: []
10
+ close: []
11
+ toggle: []
12
+ }>()
13
+
14
+ const priorityConfig = {
15
+ 1: { label: 'Low', color: 'neutral' as const },
16
+ 2: { label: 'Medium', color: 'warning' as const },
17
+ 3: { label: 'High', color: 'error' as const }
18
+ }
19
+
20
+ const statusConfig = {
21
+ todo: { label: 'Todo', color: 'neutral' as const },
22
+ in_progress: { label: 'In Progress', color: 'info' as const },
23
+ done: { label: 'Done', color: 'success' as const },
24
+ blocked: { label: 'Blocked', color: 'error' as const }
25
+ }
26
+
27
+ const priority = computed(() => priorityConfig[props.task.priority as 1 | 2 | 3] || priorityConfig[2])
28
+ const status = computed(() => statusConfig[props.task.status])
29
+ const isComplete = computed(() => props.task.status === 'done')
30
+
31
+ function formatDate(date: Date | string | undefined) {
32
+ if (!date) return null
33
+ const d = new Date(date)
34
+ return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <UCard>
40
+ <template #header>
41
+ <div class="flex items-start justify-between gap-4">
42
+ <h3 class="text-lg font-semibold">
43
+ {{ task.title }}
44
+ </h3>
45
+ <UBadge
46
+ :color="status.color"
47
+ variant="subtle"
48
+ >
49
+ {{ status.label }}
50
+ </UBadge>
51
+ </div>
52
+ </template>
53
+
54
+ <div class="space-y-4">
55
+ <!-- Meta info -->
56
+ <div class="flex flex-wrap items-center gap-3 text-sm">
57
+ <div
58
+ v-if="task.project"
59
+ class="flex items-center gap-1.5"
60
+ >
61
+ <span
62
+ class="size-2.5 rounded-full"
63
+ :style="{ backgroundColor: task.project.color }"
64
+ />
65
+ <span class="text-dimmed">{{ task.project.name }}</span>
66
+ </div>
67
+
68
+ <div class="flex items-center gap-1.5">
69
+ <UIcon
70
+ name="i-lucide-flag"
71
+ class="size-4 text-dimmed"
72
+ />
73
+ <UBadge
74
+ :color="priority.color"
75
+ variant="subtle"
76
+ size="xs"
77
+ >
78
+ {{ priority.label }}
79
+ </UBadge>
80
+ </div>
81
+
82
+ <div
83
+ v-if="task.dueDate"
84
+ class="flex items-center gap-1.5"
85
+ >
86
+ <UIcon
87
+ name="i-lucide-calendar"
88
+ class="size-4 text-dimmed"
89
+ />
90
+ <span class="text-dimmed">{{ formatDate(task.dueDate) }}</span>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- Tags -->
95
+ <div
96
+ v-if="task.tags && task.tags.length > 0"
97
+ class="flex flex-wrap gap-1.5"
98
+ >
99
+ <UBadge
100
+ v-for="tag in task.tags"
101
+ :key="tag"
102
+ variant="subtle"
103
+ color="neutral"
104
+ size="sm"
105
+ >
106
+ {{ tag }}
107
+ </UBadge>
108
+ </div>
109
+
110
+ <!-- Description -->
111
+ <div
112
+ v-if="task.description"
113
+ class="pt-2 border-t border-default"
114
+ >
115
+ <div class="prose prose-sm dark:prose-invert max-w-none">
116
+ <MDC :value="task.description" />
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Timestamps & Audit -->
121
+ <div class="pt-2 border-t border-default text-xs text-dimmed space-y-1">
122
+ <p>
123
+ Created: {{ formatDate(task.createdAt) }}
124
+ <span v-if="task.creator"> by {{ task.creator.name }}</span>
125
+ </p>
126
+ <p v-if="task.completedAt">
127
+ Completed: {{ formatDate(task.completedAt) }}
128
+ </p>
129
+ </div>
130
+ </div>
131
+
132
+ <template #footer>
133
+ <div class="flex justify-between">
134
+ <UButton
135
+ :icon="isComplete ? 'i-lucide-rotate-ccw' : 'i-lucide-check'"
136
+ :color="isComplete ? 'neutral' : 'success'"
137
+ :variant="isComplete ? 'ghost' : 'soft'"
138
+ @click="emit('toggle')"
139
+ >
140
+ {{ isComplete ? 'Mark Incomplete' : 'Mark Complete' }}
141
+ </UButton>
142
+ <div class="flex gap-2">
143
+ <UButton
144
+ color="neutral"
145
+ variant="ghost"
146
+ @click="emit('close')"
147
+ >
148
+ Close
149
+ </UButton>
150
+ <UButton
151
+ icon="i-lucide-pencil"
152
+ @click="emit('edit')"
153
+ >
154
+ Edit
155
+ </UButton>
156
+ </div>
157
+ </div>
158
+ </template>
159
+ </UCard>
160
+ </template>