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.
- package/.env.example +58 -0
- package/Claude/CLAUDE.md +92 -0
- package/Claude/hooks/lib/__init__.py +1 -0
- package/Claude/hooks/lib/hook_client.py +207 -0
- package/Claude/hooks/log-event.py +97 -0
- package/Claude/hooks/pre-compact.py +46 -0
- package/Claude/hooks/session-end.py +26 -0
- package/Claude/hooks/session-start.py +35 -0
- package/Claude/hooks/stop-extract.py +40 -0
- package/Claude/rules/frontmatter.md +54 -0
- package/Claude/rules/markdown.md +43 -0
- package/Claude/rules/note-organization.md +33 -0
- package/Claude/settings.json +54 -0
- package/Claude/skills/README.md +136 -0
- package/Claude/skills/_lib/__init__.py +1 -0
- package/Claude/skills/_lib/api.py +164 -0
- package/Claude/skills/_lib/output.py +95 -0
- package/Claude/skills/environment/SKILL.md +73 -0
- package/Claude/skills/environment/environment.py +239 -0
- package/Claude/skills/memory/SKILL.md +153 -0
- package/Claude/skills/memory/memory.py +270 -0
- package/Claude/skills/project/SKILL.md +105 -0
- package/Claude/skills/project/project.py +203 -0
- package/Claude/skills/skill-creator/SKILL.md +261 -0
- package/Claude/skills/task/SKILL.md +135 -0
- package/Claude/skills/task/task.py +310 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +39 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/AppLogo.vue +40 -0
- package/app/components/AssistantPanel.client.vue +518 -0
- package/app/components/ConfirmModal.vue +84 -0
- package/app/components/TemplateMenu.vue +49 -0
- package/app/components/agents/AgentActivityChart.client.vue +105 -0
- package/app/components/agents/AgentActivityChart.server.vue +25 -0
- package/app/components/agents/AgentForm.vue +304 -0
- package/app/components/agents/AgentRunModal.vue +154 -0
- package/app/components/agents/AgentStatsCards.vue +98 -0
- package/app/components/chat/ChatInput.vue +85 -0
- package/app/components/chat/ConversationList.vue +78 -0
- package/app/components/chat/MessageBubble.vue +81 -0
- package/app/components/chat/StreamingMessage.vue +36 -0
- package/app/components/chat/ToolCallBlock.vue +77 -0
- package/app/components/editor/CodeEditor.client.vue +212 -0
- package/app/components/editor/CodeEditorFallback.vue +12 -0
- package/app/components/editor/DocumentEditor.vue +326 -0
- package/app/components/editor/DocumentMetadata.vue +140 -0
- package/app/components/editor/MarkdownEditor.vue +146 -0
- package/app/components/files/FileTree.vue +436 -0
- package/app/components/hooks/HookActivityChart.client.vue +117 -0
- package/app/components/hooks/HookActivityChart.server.vue +25 -0
- package/app/components/hooks/HookStatsCards.vue +63 -0
- package/app/components/hooks/RecentEventsTable.vue +123 -0
- package/app/components/hooks/ToolBreakdownTable.vue +72 -0
- package/app/components/search/DashboardSearch.vue +122 -0
- package/app/components/tasks/ProjectSelect.vue +35 -0
- package/app/components/tasks/TaskCard.vue +182 -0
- package/app/components/tasks/TaskDetail.vue +160 -0
- package/app/components/tasks/TaskForm.vue +280 -0
- package/app/components/tasks/TaskList.vue +69 -0
- package/app/components/view/ViewToc.vue +85 -0
- package/app/composables/useAgents.ts +153 -0
- package/app/composables/useAuth.ts +73 -0
- package/app/composables/useChat.ts +298 -0
- package/app/composables/useDocument.ts +141 -0
- package/app/composables/useEditor.ts +100 -0
- package/app/composables/useFileTree.ts +220 -0
- package/app/composables/useHookEvents.ts +68 -0
- package/app/composables/useMemories.ts +83 -0
- package/app/composables/useNotificationBus.ts +154 -0
- package/app/composables/usePreferences.ts +131 -0
- package/app/composables/useProjects.ts +97 -0
- package/app/composables/useSearch.ts +52 -0
- package/app/composables/useTasks.ts +201 -0
- package/app/composables/useTerminal.ts +135 -0
- package/app/layouts/auth.vue +20 -0
- package/app/layouts/dashboard.vue +186 -0
- package/app/layouts/view.vue +60 -0
- package/app/middleware/auth.ts +9 -0
- package/app/pages/agents/[id].vue +602 -0
- package/app/pages/agents/index.vue +412 -0
- package/app/pages/chat.vue +146 -0
- package/app/pages/dashboard.vue +80 -0
- package/app/pages/docs.vue +131 -0
- package/app/pages/hooks.vue +163 -0
- package/app/pages/index.vue +249 -0
- package/app/pages/login.vue +60 -0
- package/app/pages/memories.vue +282 -0
- package/app/pages/settings.vue +625 -0
- package/app/pages/tasks.vue +312 -0
- package/app/pages/view/[uuid].vue +376 -0
- package/dist/cli/index.js +2711 -0
- package/drizzle.config.ts +10 -0
- package/nuxt.config.ts +98 -0
- package/package.json +107 -0
- package/server/api/agents/[id]/cancel.post.ts +27 -0
- package/server/api/agents/[id]/run.post.ts +34 -0
- package/server/api/agents/[id]/runs.get.ts +45 -0
- package/server/api/agents/[id]/stats.get.ts +94 -0
- package/server/api/agents/[id].delete.ts +29 -0
- package/server/api/agents/[id].get.ts +25 -0
- package/server/api/agents/[id].patch.ts +55 -0
- package/server/api/agents/index.get.ts +15 -0
- package/server/api/agents/index.post.ts +48 -0
- package/server/api/agents/stats.get.ts +86 -0
- package/server/api/auth/[...all].ts +5 -0
- package/server/api/conversations/[id].delete.ts +16 -0
- package/server/api/conversations/[id].get.ts +34 -0
- package/server/api/conversations/index.get.ts +17 -0
- package/server/api/documents/[id]/index.delete.ts +47 -0
- package/server/api/documents/[id]/index.put.ts +102 -0
- package/server/api/documents/[id]/public.get.ts +60 -0
- package/server/api/documents/[id]/restore.post.ts +65 -0
- package/server/api/documents/by-path.post.ts +168 -0
- package/server/api/documents/index.get.ts +48 -0
- package/server/api/fs/delete.post.ts +41 -0
- package/server/api/fs/list.get.ts +99 -0
- package/server/api/fs/mkdir.post.ts +44 -0
- package/server/api/fs/move.post.ts +68 -0
- package/server/api/fs/read.post.ts +48 -0
- package/server/api/fs/rename.post.ts +55 -0
- package/server/api/fs/write.post.ts +51 -0
- package/server/api/health.get.ts +40 -0
- package/server/api/home.get.ts +26 -0
- package/server/api/hooks/events/index.get.ts +56 -0
- package/server/api/hooks/events/index.post.ts +36 -0
- package/server/api/hooks/stats.get.ts +99 -0
- package/server/api/memory/[id].delete.ts +26 -0
- package/server/api/memory/context.get.ts +83 -0
- package/server/api/memory/extract.post.ts +42 -0
- package/server/api/memory/search.get.ts +70 -0
- package/server/api/memory/store.post.ts +31 -0
- package/server/api/projects/[id]/index.delete.ts +40 -0
- package/server/api/projects/[id]/index.get.ts +25 -0
- package/server/api/projects/[id]/index.put.ts +50 -0
- package/server/api/projects/index.get.ts +20 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/secrets/[key].delete.ts +31 -0
- package/server/api/secrets/[key].get.ts +30 -0
- package/server/api/secrets/[key].put.ts +52 -0
- package/server/api/secrets/index.get.ts +20 -0
- package/server/api/secrets/index.post.ts +58 -0
- package/server/api/tasks/[id]/index.delete.ts +46 -0
- package/server/api/tasks/[id]/index.get.ts +24 -0
- package/server/api/tasks/[id]/index.put.ts +70 -0
- package/server/api/tasks/[id]/restore.post.ts +49 -0
- package/server/api/tasks/index.get.ts +53 -0
- package/server/api/tasks/index.post.ts +47 -0
- package/server/api/tasks/tags.get.ts +21 -0
- package/server/api/user/email.patch.ts +56 -0
- package/server/db/index.ts +76 -0
- package/server/db/migrate.ts +41 -0
- package/server/db/schema.ts +345 -0
- package/server/db/seed.ts +46 -0
- package/server/db/types.ts +28 -0
- package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
- package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
- package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
- package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
- package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
- package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
- package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
- package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
- package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
- package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
- package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
- package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
- package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
- package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
- package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
- package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
- package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
- package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
- package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
- package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
- package/server/drizzle/migrations/meta/_journal.json +76 -0
- package/server/middleware/auth.ts +79 -0
- package/server/plugins/00.env-validate.ts +38 -0
- package/server/plugins/01.api-token.ts +31 -0
- package/server/plugins/02.database.ts +54 -0
- package/server/plugins/03.file-watcher.ts +65 -0
- package/server/plugins/04.cron-agents.ts +26 -0
- package/server/routes/_ws/chat.ts +252 -0
- package/server/routes/notifications.ts +47 -0
- package/server/routes/terminal.ts +98 -0
- package/server/services/agent-executor.ts +218 -0
- package/server/services/cron-scheduler.ts +78 -0
- package/server/services/memory-extractor.ts +120 -0
- package/server/utils/agent-cleanup.ts +91 -0
- package/server/utils/agent-registry.ts +95 -0
- package/server/utils/auth.ts +33 -0
- package/server/utils/chat-session-manager.ts +59 -0
- package/server/utils/crypto.ts +40 -0
- package/server/utils/db-guard.ts +12 -0
- package/server/utils/db-state.ts +63 -0
- package/server/utils/document-sync.ts +207 -0
- package/server/utils/frontmatter.ts +84 -0
- package/server/utils/notification-bus.ts +60 -0
- package/server/utils/path-validator.ts +55 -0
- package/server/utils/pty-manager.ts +130 -0
- package/shared/types/index.ts +604 -0
- package/shared/utils/language-detection.ts +87 -0
- 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>
|