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,326 @@
1
+ <script setup lang="ts">
2
+ import type { EditorToolbarItem } from '@nuxt/ui'
3
+ import type { Document, DocumentMetadata, Project } from '~~/shared/types'
4
+ import { detectLanguage, isMarkdownFile } from '~~/shared/utils/language-detection'
5
+
6
+ const props = defineProps<{
7
+ document: Document
8
+ body: string
9
+ metadata: DocumentMetadata
10
+ filePath: string
11
+ saveStatus: 'idle' | 'saving' | 'saved' | 'error'
12
+ isDirty: boolean
13
+ metadataDirty: boolean
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ 'update:body': [value: string]
18
+ 'update:metadata': [updates: Partial<DocumentMetadata>]
19
+ 'save': []
20
+ }>()
21
+
22
+ // Fetch projects for the dropdown
23
+ const { data: projectsData } = await useFetch<{ data: Project[] }>('/api/projects')
24
+ const projects = computed(() => projectsData.value?.data || [])
25
+
26
+ const showMetadata = ref(true)
27
+
28
+ // Persist editor mode preference
29
+ const { editorMode } = usePreferences()
30
+ const isEditorMode = computed({
31
+ get: () => editorMode.value === 'editor',
32
+ set: v => editorMode.value = v ? 'editor' : 'code'
33
+ })
34
+
35
+ // Determine if this is a markdown file (shows toggle) or text file (CodeMirror only)
36
+ const isMarkdown = computed(() =>
37
+ props.document.fileType === 'markdown' || isMarkdownFile(props.filePath)
38
+ )
39
+ const codeLanguage = computed(() => detectLanguage(props.filePath))
40
+ const showModeToggle = computed(() => isMarkdown.value)
41
+
42
+ const toolbarItems: EditorToolbarItem[][] = [
43
+ [
44
+ { kind: 'heading', level: 1, icon: 'i-lucide-heading-1' },
45
+ { kind: 'heading', level: 2, icon: 'i-lucide-heading-2' },
46
+ { kind: 'heading', level: 3, icon: 'i-lucide-heading-3' }
47
+ ],
48
+ [
49
+ { kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
50
+ { kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' },
51
+ { kind: 'mark', mark: 'code', icon: 'i-lucide-code' }
52
+ ],
53
+ [
54
+ { kind: 'bulletList', icon: 'i-lucide-list' },
55
+ { kind: 'orderedList', icon: 'i-lucide-list-ordered' },
56
+ { kind: 'blockquote', icon: 'i-lucide-quote' }
57
+ ],
58
+ [
59
+ { kind: 'link', icon: 'i-lucide-link' },
60
+ { kind: 'codeBlock', icon: 'i-lucide-file-code' },
61
+ { kind: 'horizontalRule', icon: 'i-lucide-minus' }
62
+ ],
63
+ [
64
+ { kind: 'undo', icon: 'i-lucide-undo' },
65
+ { kind: 'redo', icon: 'i-lucide-redo' }
66
+ ]
67
+ ]
68
+
69
+ const saveStatusIcon = computed(() => {
70
+ switch (props.saveStatus) {
71
+ case 'saving': return 'i-lucide-loader-2'
72
+ case 'saved': return 'i-lucide-check'
73
+ case 'error': return 'i-lucide-alert-circle'
74
+ default: return (props.isDirty || props.metadataDirty) ? 'i-lucide-circle-dot' : null
75
+ }
76
+ })
77
+
78
+ const saveStatusText = computed(() => {
79
+ switch (props.saveStatus) {
80
+ case 'saving': return 'Saving...'
81
+ case 'saved': return 'Saved'
82
+ case 'error': return 'Error saving'
83
+ default: return (props.isDirty || props.metadataDirty) ? 'Unsaved changes' : ''
84
+ }
85
+ })
86
+
87
+ const editorContent = computed(() => props.body || undefined)
88
+
89
+ function handleBodyUpdate(value: string) {
90
+ emit('update:body', value)
91
+ }
92
+
93
+ function handleMetadataUpdate(updates: Partial<DocumentMetadata>) {
94
+ emit('update:metadata', updates)
95
+ }
96
+
97
+ function handleKeydown(e: KeyboardEvent) {
98
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
99
+ e.preventDefault()
100
+ emit('save')
101
+ }
102
+ }
103
+
104
+ onMounted(() => window.addEventListener('keydown', handleKeydown))
105
+ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
106
+
107
+ // Share link functionality
108
+ const toast = useToast()
109
+ const shareUrl = computed(() => {
110
+ if (!props.document?.id) return ''
111
+ return `${window.location.origin}/view/${props.document.id}`
112
+ })
113
+
114
+ const linkCopied = ref(false)
115
+ const showShareModal = ref(false)
116
+
117
+ function handleShareClick() {
118
+ if (props.metadata.shared) {
119
+ copyShareLink()
120
+ } else {
121
+ showShareModal.value = true
122
+ }
123
+ }
124
+
125
+ async function copyShareLink() {
126
+ try {
127
+ await navigator.clipboard.writeText(shareUrl.value)
128
+ linkCopied.value = true
129
+ toast.add({ title: 'Link copied!', icon: 'i-lucide-check', color: 'success' })
130
+ setTimeout(() => {
131
+ linkCopied.value = false
132
+ }, 2000)
133
+ } catch {
134
+ toast.add({ title: 'Failed to copy link', icon: 'i-lucide-x', color: 'error' })
135
+ }
136
+ }
137
+
138
+ function setVisibility(shared: boolean, shareType: 'public' | 'private' | null) {
139
+ emit('update:metadata', { shared, shareType: shareType ?? undefined })
140
+ showShareModal.value = false
141
+ if (shared) {
142
+ // Copy link after enabling sharing
143
+ setTimeout(() => copyShareLink(), 100)
144
+ }
145
+ }
146
+ </script>
147
+
148
+ <template>
149
+ <div class="h-full flex flex-col">
150
+ <div class="border-b border-default py-2 flex items-center justify-between">
151
+ <div class="flex items-center gap-2 text-sm text-dimmed min-w-0">
152
+ <UIcon
153
+ name="i-lucide-file-text"
154
+ class="size-4 shrink-0"
155
+ />
156
+ <span class="truncate">{{ filePath }}</span>
157
+ </div>
158
+ <div
159
+ v-if="showModeToggle"
160
+ class="w-40"
161
+ >
162
+ <USwitch
163
+ v-model="isEditorMode"
164
+ unchecked-icon="i-lucide-code"
165
+ checked-icon="i-lucide-pencil"
166
+ :label="isEditorMode ? 'Editor Mode' : 'Code Mode'"
167
+ :ui="{ base: 'data-[state=unchecked]:bg-info', icon: 'group-data-[state=unchecked]:text-info' }"
168
+ />
169
+ </div>
170
+ <div class="flex items-center gap-3">
171
+ <UButton
172
+ :icon="linkCopied ? 'i-lucide-check' : 'i-lucide-share'"
173
+ size="xs"
174
+ variant="ghost"
175
+ class="rounded-full"
176
+ @click="handleShareClick"
177
+ />
178
+ <UButton
179
+ :icon="showMetadata ? 'i-lucide-panel-top-close' : 'i-lucide-panel-top-open'"
180
+ size="xs"
181
+ variant="ghost"
182
+ class="rounded-full"
183
+ @click="showMetadata = !showMetadata"
184
+ />
185
+ <div
186
+ v-if="saveStatusIcon"
187
+ class="flex items-center gap-1 text-sm text-dimmed"
188
+ >
189
+ <UIcon
190
+ :name="saveStatusIcon"
191
+ :class="[
192
+ 'size-4',
193
+ saveStatus === 'saving' && 'animate-spin',
194
+ saveStatus === 'error' && 'text-error'
195
+ ]"
196
+ />
197
+ <span>{{ saveStatusText }}</span>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <EditorDocumentMetadata
203
+ v-if="showMetadata"
204
+ :metadata="metadata"
205
+ :projects="projects"
206
+ @update:metadata="handleMetadataUpdate"
207
+ />
208
+
209
+ <!-- WYSIWYG Editor: markdown files in editor mode -->
210
+ <UEditor
211
+ v-if="isMarkdown && isEditorMode"
212
+ v-slot="{ editor }"
213
+ :model-value="editorContent"
214
+ content-type="markdown"
215
+ placeholder="Start writing..."
216
+ class="markdown-editor w-full min-h-64 flex-1"
217
+ @update:model-value="handleBodyUpdate"
218
+ >
219
+ <UEditorToolbar
220
+ :editor="editor"
221
+ :items="toolbarItems"
222
+ class="border-b border-default"
223
+ />
224
+ </UEditor>
225
+
226
+ <!-- CodeMirror: code mode for markdown OR non-markdown text files -->
227
+ <ClientOnly v-else>
228
+ <EditorCodeEditor
229
+ :model-value="body"
230
+ :language="codeLanguage"
231
+ class="flex-1"
232
+ @update:model-value="handleBodyUpdate"
233
+ />
234
+ <template #fallback>
235
+ <EditorCodeEditorFallback class="flex-1" />
236
+ </template>
237
+ </ClientOnly>
238
+
239
+ <!-- Share visibility modal -->
240
+ <UModal
241
+ v-model:open="showShareModal"
242
+ title="Share Document"
243
+ description="Choose how you want to share this document"
244
+ >
245
+ <template #body>
246
+ <div class="space-y-4">
247
+ <!-- Hidden option -->
248
+ <div
249
+ class="p-4 border border-default rounded-lg cursor-pointer hover:bg-elevated transition-colors"
250
+ :class="{ 'border-primary bg-elevated': !metadata.shared }"
251
+ @click="setVisibility(false, null)"
252
+ >
253
+ <div class="flex items-center gap-3">
254
+ <UIcon
255
+ name="i-lucide-eye-off"
256
+ class="size-5 text-dimmed"
257
+ />
258
+ <div>
259
+ <div class="font-medium">
260
+ Hidden
261
+ </div>
262
+ <div class="text-sm text-dimmed">
263
+ Only you can view this document. Not accessible via link.
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- Private (link only) option -->
270
+ <div
271
+ class="p-4 border border-default rounded-lg cursor-pointer hover:bg-elevated transition-colors"
272
+ :class="{ 'border-primary bg-elevated': metadata.shared && metadata.shareType === 'private' }"
273
+ @click="setVisibility(true, 'private')"
274
+ >
275
+ <div class="flex items-center gap-3">
276
+ <UIcon
277
+ name="i-lucide-link"
278
+ class="size-5 text-dimmed"
279
+ />
280
+ <div>
281
+ <div class="font-medium">
282
+ Private (Link Only)
283
+ </div>
284
+ <div class="text-sm text-dimmed">
285
+ Anyone with the link can view. Not indexed by search engines.
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <!-- Public option -->
292
+ <div
293
+ class="p-4 border border-default rounded-lg cursor-pointer hover:bg-elevated transition-colors"
294
+ :class="{ 'border-primary bg-elevated': metadata.shared && metadata.shareType === 'public' }"
295
+ @click="setVisibility(true, 'public')"
296
+ >
297
+ <div class="flex items-center gap-3">
298
+ <UIcon
299
+ name="i-lucide-globe"
300
+ class="size-5 text-dimmed"
301
+ />
302
+ <div>
303
+ <div class="font-medium">
304
+ Public
305
+ </div>
306
+ <div class="text-sm text-dimmed">
307
+ Visible to everyone. Indexed by search engines.
308
+ </div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </template>
314
+ </UModal>
315
+ </div>
316
+ </template>
317
+
318
+ <style>
319
+ .markdown-editor [data-slot="content"] {
320
+ flex: 1;
321
+ overflow: auto;
322
+ }
323
+ .markdown-editor .tiptap {
324
+ min-height: 100%;
325
+ }
326
+ </style>
@@ -0,0 +1,140 @@
1
+ <script setup lang="ts">
2
+ import type { DocumentMetadata } from '~~/shared/types'
3
+
4
+ const props = defineProps<{
5
+ metadata: DocumentMetadata
6
+ projects: Array<{ id: string, name: string, color: string }>
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ 'update:metadata': [updates: Partial<DocumentMetadata>]
11
+ }>()
12
+
13
+ // Visibility options with descriptions
14
+ const visibilityOptions = [
15
+ {
16
+ label: 'Hidden',
17
+ value: 'hidden',
18
+ icon: 'i-lucide-lock',
19
+ description: 'Hidden behind authentication and never viewable by the public'
20
+ },
21
+ {
22
+ label: 'Private',
23
+ value: 'private',
24
+ icon: 'i-lucide-eye-off',
25
+ description: 'Viewable by anyone with the link, but will not be indexed by search engines'
26
+ },
27
+ {
28
+ label: 'Public',
29
+ value: 'public',
30
+ icon: 'i-lucide-eye',
31
+ description: 'Viewable to the public and indexed by search engines'
32
+ }
33
+ ]
34
+
35
+ // Computed visibility value from shared/shareType
36
+ const visibility = computed(() => {
37
+ if (!props.metadata.shared) return 'hidden'
38
+ return props.metadata.shareType || 'private'
39
+ })
40
+
41
+ const selectedVisibility = computed(() =>
42
+ visibilityOptions.find(o => o.value === visibility.value)
43
+ )
44
+
45
+ // Local tags model for UInputTags
46
+ const localTags = computed({
47
+ get: () => props.metadata.tags || [],
48
+ set: (value: string[]) => emit('update:metadata', { tags: value })
49
+ })
50
+
51
+ function handleProjectChange(projectId: string | undefined) {
52
+ emit('update:metadata', { projectId: projectId || undefined })
53
+ }
54
+
55
+ function handleVisibilityChange(value: string) {
56
+ if (value === 'hidden') {
57
+ emit('update:metadata', { shared: false, shareType: undefined })
58
+ } else {
59
+ emit('update:metadata', { shared: true, shareType: value as 'public' | 'private' })
60
+ }
61
+ }
62
+
63
+ const projectOptions = computed(() => [
64
+ { label: 'None', value: undefined },
65
+ ...props.projects.map(p => ({ label: p.name, value: p.id }))
66
+ ])
67
+ </script>
68
+
69
+ <template>
70
+ <div class="border-b border-default px-4 py-3 bg-elevated/50">
71
+ <div class="flex flex-wrap items-start gap-3">
72
+ <!-- Project -->
73
+ <UFormField
74
+ label="Project"
75
+ class="w-40"
76
+ >
77
+ <USelectMenu
78
+ :model-value="metadata.projectId"
79
+ :items="projectOptions"
80
+ value-key="value"
81
+ placeholder="None"
82
+ class="w-full"
83
+ :search-input="false"
84
+ @update:model-value="handleProjectChange"
85
+ />
86
+ </UFormField>
87
+
88
+ <!-- Visibility -->
89
+ <UFormField
90
+ label="Visibility"
91
+ class="w-44"
92
+ >
93
+ <USelectMenu
94
+ :model-value="visibility"
95
+ :items="visibilityOptions"
96
+ value-key="value"
97
+ class="w-full"
98
+ :search-input="false"
99
+ @update:model-value="handleVisibilityChange"
100
+ >
101
+ <template #leading>
102
+ <UIcon
103
+ v-if="selectedVisibility"
104
+ :name="selectedVisibility.icon"
105
+ class="size-4 text-dimmed"
106
+ />
107
+ </template>
108
+ <template #item="{ item }">
109
+ <div class="flex items-start gap-2 py-0.5">
110
+ <UIcon
111
+ :name="item.icon"
112
+ class="size-4 mt-0.5 shrink-0"
113
+ />
114
+ <div class="min-w-0">
115
+ <div class="font-medium">
116
+ {{ item.label }}
117
+ </div>
118
+ <div class="text-xs text-dimmed truncate">
119
+ {{ item.description }}
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </template>
124
+ </USelectMenu>
125
+ </UFormField>
126
+
127
+ <!-- Tags -->
128
+ <UFormField
129
+ label="Tags"
130
+ class="flex-1 min-w-48"
131
+ >
132
+ <UInputTags
133
+ v-model="localTags"
134
+ placeholder="Add tags..."
135
+ class="w-full"
136
+ />
137
+ </UFormField>
138
+ </div>
139
+ </div>
140
+ </template>
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ import type { EditorToolbarItem } from '@nuxt/ui'
3
+
4
+ const props = defineProps<{
5
+ modelValue: string
6
+ filePath: string | null
7
+ saveStatus: 'idle' | 'saving' | 'saved' | 'error'
8
+ isDirty: boolean
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ 'update:modelValue': [value: string]
13
+ 'save': []
14
+ }>()
15
+
16
+ const toolbarItems: EditorToolbarItem[][] = [
17
+ [
18
+ { kind: 'heading', level: 1, icon: 'i-lucide-heading-1' },
19
+ { kind: 'heading', level: 2, icon: 'i-lucide-heading-2' },
20
+ { kind: 'heading', level: 3, icon: 'i-lucide-heading-3' }
21
+ ],
22
+ [
23
+ { kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
24
+ { kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' },
25
+ { kind: 'mark', mark: 'code', icon: 'i-lucide-code' }
26
+ ],
27
+ [
28
+ { kind: 'bulletList', icon: 'i-lucide-list' },
29
+ { kind: 'orderedList', icon: 'i-lucide-list-ordered' },
30
+ { kind: 'blockquote', icon: 'i-lucide-quote' }
31
+ ],
32
+ [
33
+ { kind: 'link', icon: 'i-lucide-link' },
34
+ { kind: 'codeBlock', icon: 'i-lucide-file-code' },
35
+ { kind: 'horizontalRule', icon: 'i-lucide-minus' }
36
+ ],
37
+ [
38
+ { kind: 'undo', icon: 'i-lucide-undo' },
39
+ { kind: 'redo', icon: 'i-lucide-redo' }
40
+ ]
41
+ ]
42
+
43
+ const saveStatusIcon = computed(() => {
44
+ switch (props.saveStatus) {
45
+ case 'saving':
46
+ return 'i-lucide-loader-2'
47
+ case 'saved':
48
+ return 'i-lucide-check'
49
+ case 'error':
50
+ return 'i-lucide-alert-circle'
51
+ default:
52
+ return props.isDirty ? 'i-lucide-circle-dot' : null
53
+ }
54
+ })
55
+
56
+ const saveStatusText = computed(() => {
57
+ switch (props.saveStatus) {
58
+ case 'saving':
59
+ return 'Saving...'
60
+ case 'saved':
61
+ return 'Saved'
62
+ case 'error':
63
+ return 'Error saving'
64
+ default:
65
+ return props.isDirty ? 'Unsaved changes' : ''
66
+ }
67
+ })
68
+
69
+ // Use undefined instead of empty string for proper TipTap initialization
70
+ const editorContent = computed(() => props.modelValue || undefined)
71
+
72
+ function handleUpdate(value: string) {
73
+ emit('update:modelValue', value)
74
+ }
75
+
76
+ // Handle Cmd+S / Ctrl+S
77
+ function handleKeydown(e: KeyboardEvent) {
78
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
79
+ e.preventDefault()
80
+ emit('save')
81
+ }
82
+ }
83
+
84
+ onMounted(() => {
85
+ window.addEventListener('keydown', handleKeydown)
86
+ })
87
+
88
+ onUnmounted(() => {
89
+ window.removeEventListener('keydown', handleKeydown)
90
+ })
91
+ </script>
92
+
93
+ <template>
94
+ <div class="h-full flex flex-col">
95
+ <div class="border-b border-default px-4 py-2 flex items-center justify-between">
96
+ <div class="flex items-center gap-2 text-sm text-dimmed min-w-0">
97
+ <UIcon
98
+ name="i-lucide-file-text"
99
+ class="size-4 shrink-0"
100
+ />
101
+ <span class="truncate">{{ filePath || 'No file selected' }}</span>
102
+ </div>
103
+ <div
104
+ v-if="saveStatusIcon"
105
+ class="flex items-center gap-1 text-sm text-dimmed"
106
+ >
107
+ <UIcon
108
+ :name="saveStatusIcon"
109
+ :class="[
110
+ 'size-4',
111
+ saveStatus === 'saving' && 'animate-spin',
112
+ saveStatus === 'error' && 'text-error'
113
+ ]"
114
+ />
115
+ <span>{{ saveStatusText }}</span>
116
+ </div>
117
+ </div>
118
+
119
+ <UEditor
120
+ v-slot="{ editor }"
121
+ :model-value="editorContent"
122
+ content-type="markdown"
123
+ placeholder="Start writing..."
124
+ class="markdown-editor w-full min-h-64 flex-1"
125
+ @update:model-value="handleUpdate"
126
+ >
127
+ <UEditorToolbar
128
+ :editor="editor"
129
+ :items="toolbarItems"
130
+ class="border-b border-default"
131
+ />
132
+ </UEditor>
133
+ </div>
134
+ </template>
135
+
136
+ <style>
137
+ /* Make the editor content area fill available height for better click targeting */
138
+ .markdown-editor [data-slot="content"] {
139
+ flex: 1;
140
+ overflow: auto;
141
+ }
142
+
143
+ .markdown-editor .tiptap {
144
+ min-height: 100%;
145
+ }
146
+ </style>