@hiai-gg/hiai-docs 0.0.1

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 (216) hide show
  1. package/.all-contributorsrc +18 -0
  2. package/.claude/settings.local.json +61 -0
  3. package/.dockerignore +113 -0
  4. package/.env.example +68 -0
  5. package/.github/FUNDING.yml +5 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +74 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +78 -0
  8. package/.github/dependabot.yml +136 -0
  9. package/.github/pull_request_template.md +96 -0
  10. package/.github/workflows/ci.yml +283 -0
  11. package/AGENTS.md +237 -0
  12. package/CODE_OF_CONDUCT.md +134 -0
  13. package/CONTRIBUTING.md +77 -0
  14. package/Caddyfile +50 -0
  15. package/Dockerfile.backend +60 -0
  16. package/LICENSE +21 -0
  17. package/README.md +284 -0
  18. package/RELEASE_CHECKLIST.md +34 -0
  19. package/SECURITY.md +60 -0
  20. package/backend/package.json +43 -0
  21. package/backend/src/__tests__/auth-helpers.test.ts +51 -0
  22. package/backend/src/__tests__/chunker.test.ts +65 -0
  23. package/backend/src/__tests__/config.test.ts +91 -0
  24. package/backend/src/__tests__/csrf.test.ts +91 -0
  25. package/backend/src/__tests__/embedding.test.ts +48 -0
  26. package/backend/src/__tests__/rate-limit.test.ts +46 -0
  27. package/backend/src/__tests__/routes.test.ts +38 -0
  28. package/backend/src/__tests__/schema.test.ts +31 -0
  29. package/backend/src/__tests__/validation.test.ts +556 -0
  30. package/backend/src/api/middleware/auth.ts +56 -0
  31. package/backend/src/api/middleware/csrf.ts +91 -0
  32. package/backend/src/api/middleware/rate-limit.ts +77 -0
  33. package/backend/src/api/middleware/webhook-verify.ts +22 -0
  34. package/backend/src/api/routes/attachments.ts +280 -0
  35. package/backend/src/api/routes/auth.ts +52 -0
  36. package/backend/src/api/routes/collaboration.ts +121 -0
  37. package/backend/src/api/routes/documents.ts +664 -0
  38. package/backend/src/api/routes/folders.ts +226 -0
  39. package/backend/src/api/routes/search.ts +354 -0
  40. package/backend/src/api/routes/share.ts +512 -0
  41. package/backend/src/api/routes/tags.ts +247 -0
  42. package/backend/src/api/routes/versions.ts +99 -0
  43. package/backend/src/api/routes/webhooks.ts +43 -0
  44. package/backend/src/embedding/chunker.ts +74 -0
  45. package/backend/src/embedding/index.ts +117 -0
  46. package/backend/src/embedding/providers/ollama.ts +63 -0
  47. package/backend/src/embedding/providers/openrouter.ts +71 -0
  48. package/backend/src/embedding/utils.ts +13 -0
  49. package/backend/src/embedding/worker.ts +89 -0
  50. package/backend/src/index.ts +147 -0
  51. package/backend/src/lib/auth-helpers.ts +27 -0
  52. package/backend/src/lib/auth.ts +35 -0
  53. package/backend/src/lib/config.ts +73 -0
  54. package/backend/src/lib/db.ts +7 -0
  55. package/backend/src/lib/embedding-queue.ts +12 -0
  56. package/backend/src/lib/logger.ts +18 -0
  57. package/backend/src/lib/markdown-to-doc.ts +45 -0
  58. package/backend/src/lib/minio.ts +46 -0
  59. package/backend/src/lib/redis.ts +19 -0
  60. package/backend/src/lib/yjs-provider.ts +182 -0
  61. package/backend/tests/integration/_harness.ts +754 -0
  62. package/backend/tests/integration/auth.test.ts +296 -0
  63. package/backend/tests/integration/routes.documents.test.ts +459 -0
  64. package/backend/tests/integration/routes.folders.test.ts +337 -0
  65. package/backend/tests/integration/routes.search.test.ts +322 -0
  66. package/backend/tests/integration/routes.share.test.ts +773 -0
  67. package/backend/tests/integration/routes.tags.test.ts +425 -0
  68. package/backend/tests/integration/routes.versions.test.ts +233 -0
  69. package/backend/tsconfig.json +18 -0
  70. package/docker-compose.yml +218 -0
  71. package/docs/API.md +328 -0
  72. package/docs/ARCHITECTURE.md +75 -0
  73. package/docs/DEPLOYMENT.md +113 -0
  74. package/docs/PRODUCTION_STATUS.md +61 -0
  75. package/docs/openapi.json +385 -0
  76. package/frontend/.svelte-kit.old/ambient.d.ts +230 -0
  77. package/frontend/.svelte-kit.old/env.d.ts +1 -0
  78. package/frontend/.svelte-kit.old/generated/client/app.js +46 -0
  79. package/frontend/.svelte-kit.old/generated/client/matchers.js +1 -0
  80. package/frontend/.svelte-kit.old/generated/client/nodes/0.js +3 -0
  81. package/frontend/.svelte-kit.old/generated/client/nodes/1.js +1 -0
  82. package/frontend/.svelte-kit.old/generated/client/nodes/10.js +3 -0
  83. package/frontend/.svelte-kit.old/generated/client/nodes/2.js +1 -0
  84. package/frontend/.svelte-kit.old/generated/client/nodes/3.js +1 -0
  85. package/frontend/.svelte-kit.old/generated/client/nodes/4.js +1 -0
  86. package/frontend/.svelte-kit.old/generated/client/nodes/5.js +3 -0
  87. package/frontend/.svelte-kit.old/generated/client/nodes/6.js +1 -0
  88. package/frontend/.svelte-kit.old/generated/client/nodes/7.js +3 -0
  89. package/frontend/.svelte-kit.old/generated/client/nodes/8.js +1 -0
  90. package/frontend/.svelte-kit.old/generated/client/nodes/9.js +3 -0
  91. package/frontend/.svelte-kit.old/generated/root.js +3 -0
  92. package/frontend/.svelte-kit.old/generated/root.svelte +80 -0
  93. package/frontend/.svelte-kit.old/generated/server/internal.js +55 -0
  94. package/frontend/.svelte-kit.old/non-ambient.d.ts +59 -0
  95. package/frontend/.svelte-kit.old/tsconfig.json +59 -0
  96. package/frontend/.svelte-kit.old/types/route_meta_data.json +40 -0
  97. package/frontend/.svelte-kit.old/types/src/routes/$types.d.ts +21 -0
  98. package/frontend/.svelte-kit.old/types/src/routes/(app)/$types.d.ts +30 -0
  99. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/$types.d.ts +27 -0
  100. package/frontend/.svelte-kit.old/types/src/routes/(app)/docs/[id]/proxy+page.ts +25 -0
  101. package/frontend/.svelte-kit.old/types/src/routes/api/[...path]/$types.d.ts +10 -0
  102. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/$types.d.ts +27 -0
  103. package/frontend/.svelte-kit.old/types/src/routes/folders/[id]/proxy+page.ts +15 -0
  104. package/frontend/.svelte-kit.old/types/src/routes/login/$types.d.ts +17 -0
  105. package/frontend/.svelte-kit.old/types/src/routes/register/$types.d.ts +17 -0
  106. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/$types.d.ts +20 -0
  107. package/frontend/.svelte-kit.old/types/src/routes/s/[token]/proxy+page.ts +6 -0
  108. package/frontend/.svelte-kit.old/types/src/routes/search/$types.d.ts +19 -0
  109. package/frontend/.svelte-kit.old/types/src/routes/search/proxy+page.ts +26 -0
  110. package/frontend/.svelte-kit.old/types/src/routes/settings/$types.d.ts +17 -0
  111. package/frontend/Dockerfile +44 -0
  112. package/frontend/biome.json +40 -0
  113. package/frontend/components.json +18 -0
  114. package/frontend/messages/en.json +434 -0
  115. package/frontend/package.json +70 -0
  116. package/frontend/project.inlang/settings.json +12 -0
  117. package/frontend/src/app.css +6 -0
  118. package/frontend/src/app.d.ts +13 -0
  119. package/frontend/src/app.html +30 -0
  120. package/frontend/src/hooks.server.ts +10 -0
  121. package/frontend/src/hooks.ts +10 -0
  122. package/frontend/src/lib/api/attachments.ts +45 -0
  123. package/frontend/src/lib/api/client.test.ts +15 -0
  124. package/frontend/src/lib/api/client.ts +57 -0
  125. package/frontend/src/lib/api/documents.ts +83 -0
  126. package/frontend/src/lib/api/folders.ts +180 -0
  127. package/frontend/src/lib/api/search.test.ts +52 -0
  128. package/frontend/src/lib/api/search.ts +128 -0
  129. package/frontend/src/lib/api/settings.ts +95 -0
  130. package/frontend/src/lib/api/share.ts +71 -0
  131. package/frontend/src/lib/api/tags.test.ts +91 -0
  132. package/frontend/src/lib/api/tags.ts +87 -0
  133. package/frontend/src/lib/auth-client.ts +10 -0
  134. package/frontend/src/lib/collaboration.ts +63 -0
  135. package/frontend/src/lib/components/AttachmentUpload.svelte +110 -0
  136. package/frontend/src/lib/components/DatePicker.svelte +322 -0
  137. package/frontend/src/lib/components/DocumentCard.svelte +166 -0
  138. package/frontend/src/lib/components/EmptyState.svelte +49 -0
  139. package/frontend/src/lib/components/FolderCard.svelte +93 -0
  140. package/frontend/src/lib/components/ScrollToTop.svelte +72 -0
  141. package/frontend/src/lib/components/SearchBar.svelte +47 -0
  142. package/frontend/src/lib/components/SearchResult.svelte +115 -0
  143. package/frontend/src/lib/components/SettingsDialog.svelte +271 -0
  144. package/frontend/src/lib/components/ShareDialog.svelte +158 -0
  145. package/frontend/src/lib/components/ShareLink.svelte +98 -0
  146. package/frontend/src/lib/components/TagCreateDialog.svelte +284 -0
  147. package/frontend/src/lib/components/VersionDiff.svelte +55 -0
  148. package/frontend/src/lib/components/VersionHistory.svelte +96 -0
  149. package/frontend/src/lib/components/editor/DocumentTitle.svelte +87 -0
  150. package/frontend/src/lib/components/editor/EditorToolbar.svelte +1367 -0
  151. package/frontend/src/lib/components/editor/HiAiEditor.svelte +531 -0
  152. package/frontend/src/lib/components/editor/LinkDialog.svelte +134 -0
  153. package/frontend/src/lib/components/editor/MarkdownToggle.svelte +88 -0
  154. package/frontend/src/lib/components/editor/editorExtensions.ts +53 -0
  155. package/frontend/src/lib/components/editor/markdown.ts +38 -0
  156. package/frontend/src/lib/components/sidebar/FolderTree.svelte +731 -0
  157. package/frontend/src/lib/components/sidebar/RecentDocs.svelte +311 -0
  158. package/frontend/src/lib/components/sidebar/Sidebar.svelte +156 -0
  159. package/frontend/src/lib/components/sidebar/TagList.svelte +200 -0
  160. package/frontend/src/lib/components/ui/confirm-dialog/ConfirmDialog.svelte +76 -0
  161. package/frontend/src/lib/components/ui/confirm-dialog/index.ts +1 -0
  162. package/frontend/src/lib/stores/tag-store.svelte.ts +56 -0
  163. package/frontend/src/lib/stores/theme.svelte.ts +97 -0
  164. package/frontend/src/lib/svelte.d.ts +6 -0
  165. package/frontend/src/lib/types.ts +44 -0
  166. package/frontend/src/lib/utils/clipboard.ts +17 -0
  167. package/frontend/src/lib/utils/strip-markdown.ts +59 -0
  168. package/frontend/src/lib/utils.ts +33 -0
  169. package/frontend/src/routes/(app)/+layout.svelte +17 -0
  170. package/frontend/src/routes/(app)/+page.server.ts +10 -0
  171. package/frontend/src/routes/(app)/+page.svelte +303 -0
  172. package/frontend/src/routes/(app)/docs/[id]/+page.server.ts +10 -0
  173. package/frontend/src/routes/(app)/docs/[id]/+page.svelte +1108 -0
  174. package/frontend/src/routes/(app)/docs/[id]/+page.ts +24 -0
  175. package/frontend/src/routes/(app)/search/+page.svelte +593 -0
  176. package/frontend/src/routes/(app)/search/+page.ts +25 -0
  177. package/frontend/src/routes/+error.svelte +12 -0
  178. package/frontend/src/routes/+layout.svelte +18 -0
  179. package/frontend/src/routes/+layout.ts +2 -0
  180. package/frontend/src/routes/api/[...path]/+server.ts +111 -0
  181. package/frontend/src/routes/folders/[id]/+page.server.ts +10 -0
  182. package/frontend/src/routes/folders/[id]/+page.svelte +319 -0
  183. package/frontend/src/routes/folders/[id]/+page.ts +14 -0
  184. package/frontend/src/routes/login/+page.svelte +90 -0
  185. package/frontend/src/routes/register/+page.svelte +97 -0
  186. package/frontend/src/routes/s/[token]/+page.svelte +496 -0
  187. package/frontend/src/routes/s/[token]/+page.ts +5 -0
  188. package/frontend/src/routes/settings/+page.svelte +175 -0
  189. package/frontend/static/favicon.png +0 -0
  190. package/frontend/static/logo.png +0 -0
  191. package/frontend/svelte.config.js +15 -0
  192. package/frontend/tsconfig.json +15 -0
  193. package/frontend/vite.config.ts +25 -0
  194. package/init.sql +9 -0
  195. package/logo.png +0 -0
  196. package/package.json +39 -0
  197. package/package.public.json +39 -0
  198. package/packages/db/drizzle.config.ts +10 -0
  199. package/packages/db/package.json +30 -0
  200. package/packages/db/src/client.ts +9 -0
  201. package/packages/db/src/index.ts +2 -0
  202. package/packages/db/src/migrations/0000_nice_bedlam.sql +165 -0
  203. package/packages/db/src/migrations/0001_w2_3_test.sql +5 -0
  204. package/packages/db/src/migrations/0002_rename_content_json.sql +2 -0
  205. package/packages/db/src/migrations/meta/0000_snapshot.json +1331 -0
  206. package/packages/db/src/migrations/meta/0001_snapshot.json +1399 -0
  207. package/packages/db/src/migrations/meta/0002_snapshot.json +1399 -0
  208. package/packages/db/src/migrations/meta/_journal.json +27 -0
  209. package/packages/db/src/schema.ts +378 -0
  210. package/packages/db/tsconfig.json +17 -0
  211. package/scripts/export-openapi.ts +37 -0
  212. package/scripts/health-check.sh +75 -0
  213. package/scripts/migrate.sh +135 -0
  214. package/scripts/prework_backup.sh +25 -0
  215. package/scripts/release.sh +83 -0
  216. package/tsconfig.json +25 -0
@@ -0,0 +1,1108 @@
1
+ <!-- Document editor page -->
2
+ <script lang="ts">
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuTrigger,
9
+ } from "@hiai-gg/hiai-ui/components/ui/dropdown-menu";
10
+ import {
11
+ Check,
12
+ ChevronRight,
13
+ Code,
14
+ Download,
15
+ FileText,
16
+ Folder,
17
+ Loader2,
18
+ MoreHorizontal,
19
+ Pencil,
20
+ Plus,
21
+ Share2,
22
+ Trash2,
23
+ X,
24
+ } from "lucide-svelte";
25
+ import { onDestroy, onMount } from "svelte";
26
+ import { goto } from "$app/navigation";
27
+ import { ApiError } from "$lib/api/client";
28
+ import { deleteDocument, updateDocument } from "$lib/api/documents";
29
+ import { createFolder, listFolders } from "$lib/api/folders";
30
+ import {
31
+ addTagToDocument,
32
+ listTags,
33
+ removeTagFromDocument,
34
+ type Tag,
35
+ } from "$lib/api/tags";
36
+ import DocumentTitle from "$lib/components/editor/DocumentTitle.svelte";
37
+ import type { EditorOutput } from "$lib/components/editor/HiAiEditor.svelte";
38
+ import HiAiEditor from "$lib/components/editor/HiAiEditor.svelte";
39
+ import MarkdownToggle from "$lib/components/editor/MarkdownToggle.svelte";
40
+ import ShareDialog from "$lib/components/ShareDialog.svelte";
41
+ import TagCreateDialog from "$lib/components/TagCreateDialog.svelte";
42
+ import { ConfirmDialog } from "$lib/components/ui/confirm-dialog";
43
+ import * as m from "$lib/paraglide/messages.js";
44
+ import { refreshDocs, refreshTags } from "$lib/stores/tag-store.svelte";
45
+
46
+ const { data } = $props();
47
+
48
+ let title = $state("");
49
+ let content = $state("");
50
+ let contentJson = $state<object | undefined>(undefined);
51
+ $effect(() => {
52
+ title = data.document.title;
53
+ content = data.document.content ?? "";
54
+ contentJson =
55
+ (data.document.contentJson as object | null | undefined) ?? undefined;
56
+ });
57
+ let mode = $state<"wysiwyg" | "markdown">("wysiwyg");
58
+ let saveStatus = $state<"saved" | "saving" | "unsaved">("saved");
59
+ let showMenu = $state(false);
60
+ let loading = $state(true);
61
+ let error = $state<string | null>(null);
62
+ let showShareDialog = $state(false);
63
+ let showCreateTagDialog = $state(false);
64
+ let showDeleteDialog = $state(false);
65
+ let deleteBusy = $state(false);
66
+
67
+ // Tag management
68
+ type DocTag = { id: string; name: string; color: string };
69
+ let tags = $state<DocTag[]>([]);
70
+ let availableTags = $state<DocTag[]>([]);
71
+ let tagsLoading = $state(false);
72
+ let tagBusy = $state(false);
73
+
74
+ // Folder management
75
+ let folders = $state<{ id: string; name: string }[]>([]);
76
+ let foldersLoading = $state(false);
77
+ let currentFolderId = $state<string | null>(null);
78
+ let currentFolderName = $state<string>("");
79
+ let creatingFolder = $state(false);
80
+ let newFolderName = $state("");
81
+
82
+ $effect(() => {
83
+ tags = data.document.tags ?? [];
84
+ });
85
+
86
+ $effect(() => {
87
+ currentFolderId = data.document.folderId ?? null;
88
+ currentFolderName = data.document.folderName ?? "";
89
+ });
90
+
91
+ const assignedTagIds = $derived(new Set(tags.map((t) => t.id)));
92
+ const assignableTags = $derived(
93
+ availableTags.filter((t) => !assignedTagIds.has(t.id)),
94
+ );
95
+
96
+ let errorTimer: ReturnType<typeof setTimeout> | null = null;
97
+
98
+ function setError(msg: string | null) {
99
+ error = msg;
100
+ // Auto-dismiss the banner after 5s so transient errors don't linger
101
+ // forever. Clear any pending timer first to avoid races when several
102
+ // errors fire in quick succession.
103
+ if (errorTimer) {
104
+ clearTimeout(errorTimer);
105
+ errorTimer = null;
106
+ }
107
+ if (msg !== null) {
108
+ errorTimer = setTimeout(() => {
109
+ error = null;
110
+ errorTimer = null;
111
+ }, 5000);
112
+ }
113
+ }
114
+
115
+ // Initialize after mount
116
+ onMount(() => {
117
+ title = data.document.title;
118
+ content = data.document.content;
119
+ contentJson =
120
+ (data.document.contentJson as object | null | undefined) ?? undefined;
121
+ loading = false;
122
+ });
123
+
124
+ onDestroy(() => {
125
+ // Clear any pending auto-dismiss timer so we don't write to a
126
+ // destroyed component's state if the user navigates away mid-delay.
127
+ if (errorTimer) {
128
+ clearTimeout(errorTimer);
129
+ errorTimer = null;
130
+ }
131
+ });
132
+
133
+ // Close dropdown on outside click
134
+ function handleWindowClick(e: MouseEvent) {
135
+ if (showMenu) {
136
+ const target = e.target as HTMLElement;
137
+ if (!target.closest("[data-menu-container]")) {
138
+ showMenu = false;
139
+ }
140
+ }
141
+ }
142
+
143
+ // Auto-save debounce for content.
144
+ // Accepts a EditorOutput (`{ markdown, json }`) from either editor
145
+ // so that edits in the raw-markdown view keep the server-side
146
+ // `contentJson` in sync — the wysiwyg editor reuses that field to avoid
147
+ // re-parsing on every load.
148
+ type ContentUpdate = EditorOutput;
149
+ let contentSaveTimer: ReturnType<typeof setTimeout> | null = null;
150
+
151
+ function debounceContentSave(update: ContentUpdate) {
152
+ content = update.markdown;
153
+ contentJson = update.json;
154
+ saveStatus = "unsaved";
155
+ if (contentSaveTimer) clearTimeout(contentSaveTimer);
156
+ contentSaveTimer = setTimeout(async () => {
157
+ await saveContent(update);
158
+ }, 2000);
159
+ }
160
+
161
+ // Retries with exponential backoff: 2s, 4s, 8s (max 3 attempts).
162
+ // Used when the backend responds with 429 (rate limit) so fast typing
163
+ // doesn't surface a hard error to the user.
164
+ const RATE_LIMIT_BACKOFF_MS = [2000, 4000, 8000];
165
+
166
+ function sleep(ms: number): Promise<void> {
167
+ return new Promise((resolve) => setTimeout(resolve, ms));
168
+ }
169
+
170
+ async function saveContent(update: ContentUpdate) {
171
+ saveStatus = "saving";
172
+ for (let attempt = 0; attempt <= RATE_LIMIT_BACKOFF_MS.length; attempt++) {
173
+ try {
174
+ await updateDocument(data.document.id, {
175
+ content: update.markdown,
176
+ contentJson: update.json,
177
+ });
178
+ saveStatus = "saved";
179
+ return;
180
+ } catch (e) {
181
+ if (
182
+ e instanceof ApiError &&
183
+ e.status === 429 &&
184
+ attempt < RATE_LIMIT_BACKOFF_MS.length
185
+ ) {
186
+ const wait = RATE_LIMIT_BACKOFF_MS[attempt];
187
+ error = "Saving too fast. Waiting before retry...";
188
+ await sleep(wait);
189
+ continue;
190
+ }
191
+ saveStatus = "unsaved";
192
+ error = m.doc_save_content_error();
193
+ return;
194
+ }
195
+ }
196
+ }
197
+
198
+ async function handleTitleUpdate(newTitle: string) {
199
+ title = newTitle;
200
+ saveStatus = "saving";
201
+ try {
202
+ await updateDocument(data.document.id, { title: newTitle });
203
+ saveStatus = "saved";
204
+ refreshDocs();
205
+ } catch (_e) {
206
+ saveStatus = "unsaved";
207
+ error = m.doc_save_title_error();
208
+ }
209
+ }
210
+
211
+ async function handleDelete() {
212
+ showMenu = false;
213
+ showDeleteDialog = true;
214
+ }
215
+
216
+ async function confirmDelete() {
217
+ if (deleteBusy) return;
218
+ deleteBusy = true;
219
+ try {
220
+ await deleteDocument(data.document.id);
221
+ showDeleteDialog = false;
222
+ refreshDocs();
223
+ goto("/");
224
+ } catch (_e) {
225
+ error = m.doc_delete_error();
226
+ } finally {
227
+ deleteBusy = false;
228
+ }
229
+ }
230
+
231
+ function cancelDelete() {
232
+ showDeleteDialog = false;
233
+ }
234
+
235
+ function handleExport() {
236
+ showMenu = false;
237
+ const blob = new Blob([content], { type: "text/markdown" });
238
+ const url = URL.createObjectURL(blob);
239
+ const a = document.createElement("a");
240
+ a.href = url;
241
+ a.download = `${title || m.doc_title_placeholder()}.md`;
242
+ a.click();
243
+ URL.revokeObjectURL(url);
244
+ }
245
+
246
+ function handleShare() {
247
+ showShareDialog = true;
248
+ }
249
+
250
+ async function loadAvailableTags() {
251
+ if (availableTags.length > 0 || tagsLoading) return;
252
+ tagsLoading = true;
253
+ try {
254
+ const all = await listTags();
255
+ availableTags = all.map((t) => ({
256
+ id: t.id,
257
+ name: t.name,
258
+ color: t.color ?? "#888",
259
+ }));
260
+ } catch (_e) {
261
+ setError(m.tags_load_error());
262
+ } finally {
263
+ tagsLoading = false;
264
+ }
265
+ }
266
+
267
+ async function handleAddTag(tagId: string) {
268
+ if (tagBusy) return;
269
+ tagBusy = true;
270
+ const tag = availableTags.find((t) => t.id === tagId);
271
+ try {
272
+ await addTagToDocument(data.document.id, tagId);
273
+ if (tag) tags = [...tags, { id: tag.id, name: tag.name, color: tag.color }];
274
+ availableTags = availableTags.filter((t) => t.id !== tagId);
275
+ } catch (e) {
276
+ console.error("handleAddTag: addTagToDocument failed", e);
277
+ setError(m.tag_add_error());
278
+ } finally {
279
+ tagBusy = false;
280
+ }
281
+ }
282
+
283
+ async function handleTagCreated(newTag: Tag) {
284
+ // Add to available tags (in case it wasn't already there) and assign
285
+ // to this document immediately so the user doesn't need a second click.
286
+ availableTags = [
287
+ ...availableTags.filter((t) => t.id !== newTag.id),
288
+ { id: newTag.id, name: newTag.name, color: newTag.color ?? "#888" },
289
+ ];
290
+ if (!assignedTagIds.has(newTag.id)) {
291
+ await handleAddTag(newTag.id);
292
+ }
293
+ // Notify other components (e.g. sidebar TagList) to reload.
294
+ refreshTags();
295
+ }
296
+
297
+ async function handleRemoveTag(tagId: string) {
298
+ if (tagBusy) return;
299
+ tagBusy = true;
300
+ const tag = tags.find((t) => t.id === tagId);
301
+ try {
302
+ await removeTagFromDocument(data.document.id, tagId);
303
+ tags = tags.filter((t) => t.id !== tagId);
304
+ if (tag) availableTags = [...availableTags, tag];
305
+ } catch (_e) {
306
+ setError(m.tag_remove_error());
307
+ } finally {
308
+ tagBusy = false;
309
+ }
310
+ }
311
+
312
+ // --- Folder management ---
313
+ async function loadFolders() {
314
+ if (folders.length > 0 || foldersLoading) return;
315
+ foldersLoading = true;
316
+ try {
317
+ // listFolders(null) returns a single synthetic root whose children
318
+ // are the user's top-level folders.
319
+ const result = await listFolders(null);
320
+ folders = (result[0]?.children ?? []).map((f) => ({
321
+ id: f.id,
322
+ name: f.name,
323
+ }));
324
+ } catch (_e) {
325
+ setError(m.error_generic());
326
+ } finally {
327
+ foldersLoading = false;
328
+ }
329
+ }
330
+
331
+ async function moveToFolder(folderId: string | null) {
332
+ try {
333
+ await updateDocument(data.document.id, { folderId });
334
+ currentFolderId = folderId;
335
+ currentFolderName = folderId
336
+ ? (folders.find((f) => f.id === folderId)?.name ?? "")
337
+ : "";
338
+ saveStatus = "saved";
339
+ // Keep sidebar folder/doc lists in sync after a move.
340
+ refreshDocs();
341
+ } catch (_e) {
342
+ setError(m.doc_save_content_error());
343
+ }
344
+ }
345
+
346
+ async function handleCreateFolder() {
347
+ const name = newFolderName.trim();
348
+ if (!name) return;
349
+ try {
350
+ const created = await createFolder({ name });
351
+ folders = [...folders, { id: created.id, name: created.name }];
352
+ await moveToFolder(created.id);
353
+ } catch (_e) {
354
+ setError(m.error_generic());
355
+ } finally {
356
+ creatingFolder = false;
357
+ newFolderName = "";
358
+ }
359
+ }
360
+ </script>
361
+
362
+ <svelte:window onclick={handleWindowClick} />
363
+
364
+ <svelte:head>
365
+ <title>{m.doc_page_title({ title: title || m.doc_title_placeholder() })}</title>
366
+ </svelte:head>
367
+
368
+ {#if loading}
369
+ <div class="loading-page">
370
+ <div class="loading-content">
371
+ <div class="skeleton-line skeleton-title"></div>
372
+ <div class="skeleton-line skeleton-short"></div>
373
+ <div class="skeleton-line skeleton-full"></div>
374
+ <div class="skeleton-line skeleton-full"></div>
375
+ <div class="skeleton-line skeleton-medium"></div>
376
+ </div>
377
+ </div>
378
+ {:else}
379
+ <div class="editor-page">
380
+ <!-- Header -->
381
+ <header class="editor-header">
382
+ <!-- Breadcrumb -->
383
+ <nav class="breadcrumb" aria-label={m.aria_breadcrumb()}>
384
+ <a href="/" class="breadcrumb-link">{m.breadcrumb_home()}</a>
385
+ {#if data.document.folderName}
386
+ <ChevronRight size={14} class="breadcrumb-sep" />
387
+ <a href="/folders/{data.document.folderId}" class="breadcrumb-link">
388
+ {data.document.folderName}
389
+ </a>
390
+ {/if}
391
+ <ChevronRight size={14} class="breadcrumb-sep" />
392
+ <span class="breadcrumb-current">{title || m.doc_title_placeholder()}</span>
393
+ </nav>
394
+
395
+ <div class="editor-actions">
396
+ <!-- Save status -->
397
+ <span
398
+ class="save-status"
399
+ class:saved={saveStatus === "saved"}
400
+ class:saving={saveStatus === "saving"}
401
+ class:unsaved={saveStatus === "unsaved"}
402
+ >
403
+ {#if saveStatus === "saved"}
404
+ <Check size={14} /> {m.editor_status_saved()}
405
+ {:else if saveStatus === "saving"}
406
+ <Loader2 size={14} class="animate-spin" /> {m.editor_status_saving()}
407
+ {:else}
408
+ <Pencil size={14} /> {m.editor_status_unsaved()}
409
+ {/if}
410
+ </span>
411
+
412
+ <!-- Mode toggle -->
413
+ <div class="mode-toggle" role="radiogroup" aria-label={m.editor_mode_label()}>
414
+ <button
415
+ class="mode-btn"
416
+ class:active={mode === "wysiwyg"}
417
+ onclick={() => (mode = "wysiwyg")}
418
+ title={m.editor_wysiwyg_title()}
419
+ aria-label={m.editor_wysiwyg_mode_label()}
420
+ role="radio"
421
+ aria-checked={mode === "wysiwyg"}
422
+ >
423
+ <FileText size={16} />
424
+ </button>
425
+ <button
426
+ class="mode-btn"
427
+ class:active={mode === "markdown"}
428
+ onclick={() => (mode = "markdown")}
429
+ title={m.editor_markdown_title()}
430
+ aria-label={m.editor_markdown_mode_label()}
431
+ role="radio"
432
+ aria-checked={mode === "markdown"}
433
+ >
434
+ <Code size={16} />
435
+ </button>
436
+ </div>
437
+
438
+ <!-- Share -->
439
+ <button
440
+ class="action-btn"
441
+ title={m.action_copy_link()}
442
+ aria-label={m.editor_share_label()}
443
+ onclick={handleShare}
444
+ >
445
+ <Share2 size={16} />
446
+ </button>
447
+
448
+ <!-- More menu -->
449
+ <div class="menu-container" data-menu-container>
450
+ <button
451
+ class="action-btn"
452
+ title={m.editor_more_options()}
453
+ aria-label={m.editor_more_options()}
454
+ onclick={() => (showMenu = !showMenu)}
455
+ >
456
+ <MoreHorizontal size={16} />
457
+ </button>
458
+ {#if showMenu}
459
+ <div class="dropdown" role="menu">
460
+ <button
461
+ class="dropdown-item"
462
+ role="menuitem"
463
+ onclick={handleExport}
464
+ >
465
+ <Download size={14} /> {m.editor_export_md()}
466
+ </button>
467
+ <div class="dropdown-divider"></div>
468
+ <button
469
+ class="dropdown-item destructive"
470
+ role="menuitem"
471
+ onclick={handleDelete}
472
+ >
473
+ <Trash2 size={14} /> {m.action_delete()}
474
+ </button>
475
+ </div>
476
+ {/if}
477
+ </div>
478
+ </div>
479
+ </header>
480
+
481
+ <!-- Error banner -->
482
+ {#if error}
483
+ <div class="error-banner" role="alert">
484
+ <span>{error}</span>
485
+ <button class="error-dismiss" onclick={() => setError(null)} aria-label={m.error_dismiss()}>
486
+ &times;
487
+ </button>
488
+ </div>
489
+ {/if}
490
+
491
+ <!-- Editor area -->
492
+ <main class="editor-main">
493
+ <!-- Editable title -->
494
+ <DocumentTitle {title} onUpdate={handleTitleUpdate} />
495
+
496
+ <!-- Tags -->
497
+ <div class="tag-row">
498
+ <!-- Folder selector: shows the current folder (or "No folder") and
499
+ lets the user move the document, clear it to root, or create a
500
+ new folder. -->
501
+ <DropdownMenu onOpenChange={(open) => open && void loadFolders()}>
502
+ <DropdownMenuTrigger>
503
+ {#snippet child({ props })}
504
+ <button
505
+ {...props}
506
+ type="button"
507
+ class="folder-badge"
508
+ title={currentFolderName || "No folder"}
509
+ aria-label="Change folder"
510
+ >
511
+ <Folder size={14} />
512
+ {currentFolderName || "No folder"}
513
+ </button>
514
+ {/snippet}
515
+ </DropdownMenuTrigger>
516
+ <DropdownMenuContent align="start">
517
+ {#if foldersLoading}
518
+ <div class="tag-empty">{m.action_loading()}</div>
519
+ {:else}
520
+ {#each folders as folder (folder.id)}
521
+ <DropdownMenuItem onSelect={() => moveToFolder(folder.id)}>
522
+ <Folder size={14} />
523
+ {folder.name}
524
+ {#if currentFolderId === folder.id}
525
+ <Check size={12} />
526
+ {/if}
527
+ </DropdownMenuItem>
528
+ {/each}
529
+ {/if}
530
+ <DropdownMenuItem
531
+ disabled={currentFolderId === null}
532
+ onSelect={() => moveToFolder(null)}
533
+ >
534
+ Move to root
535
+ </DropdownMenuItem>
536
+ <DropdownMenuSeparator />
537
+ <DropdownMenuItem onSelect={() => { creatingFolder = true; }}>
538
+ <Plus size={14} />
539
+ New folder
540
+ </DropdownMenuItem>
541
+ </DropdownMenuContent>
542
+ </DropdownMenu>
543
+
544
+ {#if creatingFolder}
545
+ <span class="folder-create">
546
+ <input
547
+ type="text"
548
+ bind:value={newFolderName}
549
+ placeholder="Folder name"
550
+ aria-label="New folder name"
551
+ onkeydown={(e) => {
552
+ if (e.key === "Enter") handleCreateFolder();
553
+ if (e.key === "Escape") { creatingFolder = false; newFolderName = ""; }
554
+ }}
555
+ />
556
+ <button type="button" class="folder-create-btn" onclick={handleCreateFolder}>
557
+ {m.action_save()}
558
+ </button>
559
+ <button
560
+ type="button"
561
+ class="folder-create-btn"
562
+ onclick={() => { creatingFolder = false; newFolderName = ""; }}
563
+ >
564
+ {m.action_cancel()}
565
+ </button>
566
+ </span>
567
+ {/if}
568
+ {#each tags as tag (tag.id)}
569
+ <span
570
+ class="tag-badge"
571
+ style="background-color: {tag.color}20; color: {tag.color}; border-color: {tag.color}40"
572
+ >
573
+ {tag.name}
574
+ <button
575
+ type="button"
576
+ class="tag-remove"
577
+ onclick={() => handleRemoveTag(tag.id)}
578
+ disabled={tagBusy}
579
+ aria-label={m.tag_remove_label()}
580
+ title={m.tag_remove_label()}
581
+ >
582
+ <X size={12} />
583
+ </button>
584
+ </span>
585
+ {/each}
586
+ <DropdownMenu onOpenChange={(open) => open && void loadAvailableTags()}>
587
+ <DropdownMenuTrigger>
588
+ {#snippet child({ props })}
589
+ <button
590
+ {...props}
591
+ type="button"
592
+ class="tag-add-btn"
593
+ disabled={tagBusy}
594
+ aria-label={m.tag_add_label()}
595
+ title={m.tag_add_label()}
596
+ >
597
+ {#if tagsLoading}
598
+ <Loader2 size={12} class="animate-spin" />
599
+ {:else}
600
+ <Plus size={12} />
601
+ {/if}
602
+ {m.tag_add_label()}
603
+ </button>
604
+ {/snippet}
605
+ </DropdownMenuTrigger>
606
+ <DropdownMenuContent align="start">
607
+ {#if assignableTags.length === 0}
608
+ <div class="tag-empty">
609
+ {m.tag_no_tags_available()}
610
+ </div>
611
+ {:else}
612
+ {#each assignableTags as tag (tag.id)}
613
+ <DropdownMenuItem
614
+ disabled={tagBusy}
615
+ onSelect={() => handleAddTag(tag.id)}
616
+ >
617
+ <span
618
+ class="tag-swatch"
619
+ style="background-color: {tag.color}"
620
+ ></span>
621
+ {tag.name}
622
+ </DropdownMenuItem>
623
+ {/each}
624
+ {/if}
625
+ <DropdownMenuSeparator />
626
+ <DropdownMenuItem
627
+ disabled={tagBusy}
628
+ onSelect={() => (showCreateTagDialog = true)}
629
+ >
630
+ <Plus class="tag-swatch" />
631
+ {m.tags_create_new()}
632
+ </DropdownMenuItem>
633
+ </DropdownMenuContent>
634
+ </DropdownMenu>
635
+ </div>
636
+
637
+ <!-- Editor -->
638
+ <div class="editor-container">
639
+ {#if mode === "wysiwyg"}
640
+ <HiAiEditor
641
+ {content}
642
+ {contentJson}
643
+ onUpdate={debounceContentSave}
644
+ editable={true}
645
+ documentId={data.document.id}
646
+ />
647
+ {:else}
648
+ <MarkdownToggle {content} onUpdate={debounceContentSave} />
649
+ {/if}
650
+ </div>
651
+ </main>
652
+
653
+ <ShareDialog bind:open={showShareDialog} documentId={data.document.id} documentTitle={title} />
654
+ <TagCreateDialog
655
+ bind:open={showCreateTagDialog}
656
+ mode="create"
657
+ onCreated={handleTagCreated}
658
+ />
659
+ <ConfirmDialog
660
+ bind:open={showDeleteDialog}
661
+ title={m.action_delete()}
662
+ description={m.doc_delete_confirm_hard()}
663
+ confirmLabel={m.action_delete()}
664
+ cancelLabel={m.action_cancel()}
665
+ variant="destructive"
666
+ busy={deleteBusy}
667
+ onConfirm={confirmDelete}
668
+ onCancel={cancelDelete}
669
+ />
670
+ </div>
671
+ {/if}
672
+
673
+ <style>
674
+ /* Loading skeleton */
675
+ .loading-page {
676
+ display: flex;
677
+ align-items: flex-start;
678
+ justify-content: center;
679
+ min-height: 100vh;
680
+ padding: 48px 24px;
681
+ background: var(--background);
682
+ }
683
+
684
+ .loading-content {
685
+ width: 100%;
686
+ max-width: 860px;
687
+ display: flex;
688
+ flex-direction: column;
689
+ gap: 16px;
690
+ }
691
+
692
+ .skeleton-line {
693
+ height: 20px;
694
+ border-radius: 4px;
695
+ background: var(--muted);
696
+ animation: pulse 1.5s ease-in-out infinite;
697
+ }
698
+
699
+ .skeleton-title {
700
+ height: 40px;
701
+ width: 60%;
702
+ }
703
+
704
+ .skeleton-short {
705
+ width: 30%;
706
+ height: 16px;
707
+ }
708
+
709
+ .skeleton-full {
710
+ width: 100%;
711
+ }
712
+
713
+ .skeleton-medium {
714
+ width: 70%;
715
+ }
716
+
717
+ @keyframes pulse {
718
+ 0%,
719
+ 100% {
720
+ opacity: 1;
721
+ }
722
+ 50% {
723
+ opacity: 0.4;
724
+ }
725
+ }
726
+
727
+ /* Editor page layout */
728
+ .editor-page {
729
+ display: flex;
730
+ flex-direction: column;
731
+ min-height: 100vh;
732
+ background: var(--background);
733
+ }
734
+
735
+ .editor-header {
736
+ display: flex;
737
+ align-items: center;
738
+ justify-content: space-between;
739
+ padding: 10px 24px;
740
+ border-bottom: 1px solid var(--border);
741
+ background: var(--card);
742
+ gap: 16px;
743
+ flex-wrap: wrap;
744
+ position: sticky;
745
+ top: 0;
746
+ z-index: 20;
747
+ }
748
+
749
+ .breadcrumb {
750
+ display: flex;
751
+ align-items: center;
752
+ gap: 6px;
753
+ font-size: 13px;
754
+ min-width: 0;
755
+ }
756
+
757
+ .breadcrumb-link {
758
+ color: var(--muted-foreground);
759
+ text-decoration: none;
760
+ transition: color 0.15s;
761
+ }
762
+
763
+ .breadcrumb-link:hover {
764
+ color: var(--foreground);
765
+ }
766
+
767
+ .breadcrumb-current {
768
+ color: var(--foreground);
769
+ font-weight: 500;
770
+ white-space: nowrap;
771
+ overflow: hidden;
772
+ text-overflow: ellipsis;
773
+ max-width: 200px;
774
+ }
775
+
776
+ .editor-actions {
777
+ display: flex;
778
+ align-items: center;
779
+ gap: 8px;
780
+ flex-shrink: 0;
781
+ }
782
+
783
+ .save-status {
784
+ display: inline-flex;
785
+ align-items: center;
786
+ gap: 4px;
787
+ font-size: 12px;
788
+ padding: 4px 10px;
789
+ border-radius: 6px;
790
+ white-space: nowrap;
791
+ }
792
+
793
+ .save-status.saved {
794
+ color: var(--muted-foreground);
795
+ }
796
+
797
+ .save-status.saving {
798
+ color: var(--ring);
799
+ }
800
+
801
+ .save-status.unsaved {
802
+ color: var(--destructive);
803
+ }
804
+
805
+ @keyframes spin {
806
+ from {
807
+ transform: rotate(0deg);
808
+ }
809
+ to {
810
+ transform: rotate(360deg);
811
+ }
812
+ }
813
+
814
+ .mode-toggle {
815
+ display: flex;
816
+ border: 1px solid var(--border);
817
+ border-radius: 6px;
818
+ overflow: hidden;
819
+ }
820
+
821
+ .mode-btn {
822
+ display: inline-flex;
823
+ align-items: center;
824
+ justify-content: center;
825
+ min-width: 44px;
826
+ min-height: 44px;
827
+ border: none;
828
+ background: transparent;
829
+ color: var(--muted-foreground);
830
+ cursor: pointer;
831
+ transition: all 0.15s;
832
+ }
833
+
834
+ .mode-btn:hover {
835
+ background: var(--muted);
836
+ }
837
+
838
+ .mode-btn.active {
839
+ background: var(--primary);
840
+ color: var(--primary-foreground);
841
+ }
842
+
843
+ .action-btn {
844
+ display: inline-flex;
845
+ align-items: center;
846
+ justify-content: center;
847
+ min-width: 44px;
848
+ min-height: 44px;
849
+ border: 1px solid var(--border);
850
+ border-radius: 6px;
851
+ background: transparent;
852
+ color: var(--muted-foreground);
853
+ cursor: pointer;
854
+ transition: all 0.15s;
855
+ }
856
+
857
+ .action-btn:hover {
858
+ background: var(--muted);
859
+ color: var(--foreground);
860
+ }
861
+
862
+ .menu-container {
863
+ position: relative;
864
+ }
865
+
866
+ .dropdown {
867
+ position: absolute;
868
+ right: 0;
869
+ top: 100%;
870
+ margin-top: 4px;
871
+ min-width: 180px;
872
+ border: 1px solid var(--border);
873
+ border-radius: 8px;
874
+ background: var(--card);
875
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
876
+ padding: 4px;
877
+ z-index: 50;
878
+ }
879
+
880
+ .dropdown-item {
881
+ display: flex;
882
+ align-items: center;
883
+ gap: 8px;
884
+ width: 100%;
885
+ padding: 8px 12px;
886
+ border: none;
887
+ background: transparent;
888
+ color: var(--foreground);
889
+ font-size: 13px;
890
+ cursor: pointer;
891
+ border-radius: 6px;
892
+ transition: background 0.15s;
893
+ }
894
+
895
+ .dropdown-item:hover {
896
+ background: var(--muted);
897
+ }
898
+
899
+ .dropdown-item.destructive {
900
+ color: var(--destructive);
901
+ }
902
+
903
+ .dropdown-item.destructive:hover {
904
+ background: color-mix(in srgb, var(--destructive) 10%, transparent);
905
+ }
906
+
907
+ .dropdown-divider {
908
+ height: 1px;
909
+ background: var(--border);
910
+ margin: 4px 0;
911
+ }
912
+
913
+ /* Error banner */
914
+ .error-banner {
915
+ display: flex;
916
+ align-items: center;
917
+ justify-content: space-between;
918
+ padding: 8px 24px;
919
+ background: color-mix(in srgb, var(--destructive) 10%, transparent);
920
+ color: var(--destructive);
921
+ font-size: 13px;
922
+ border-bottom: 1px solid color-mix(in srgb, var(--destructive) 20%, transparent);
923
+ }
924
+
925
+ .error-dismiss {
926
+ background: none;
927
+ border: none;
928
+ color: var(--destructive);
929
+ cursor: pointer;
930
+ font-size: 18px;
931
+ line-height: 1;
932
+ padding: 0 4px;
933
+ }
934
+
935
+ /* Editor main area */
936
+ .editor-main {
937
+ flex: 1;
938
+ display: flex;
939
+ flex-direction: column;
940
+ max-width: 860px;
941
+ width: 100%;
942
+ margin: 0 auto;
943
+ padding: 32px 24px;
944
+ }
945
+
946
+ .tag-row {
947
+ display: flex;
948
+ gap: 6px;
949
+ margin-bottom: 16px;
950
+ flex-wrap: wrap;
951
+ align-items: center;
952
+ }
953
+
954
+ .tag-badge {
955
+ display: inline-flex;
956
+ align-items: center;
957
+ gap: 4px;
958
+ padding: 2px 4px 2px 8px;
959
+ border-radius: 4px;
960
+ font-size: 12px;
961
+ font-weight: 500;
962
+ border: 1px solid;
963
+ }
964
+
965
+ .folder-badge {
966
+ display: inline-flex;
967
+ align-items: center;
968
+ gap: 4px;
969
+ padding: 2px 10px;
970
+ border-radius: 9999px;
971
+ font-size: 0.75rem;
972
+ background: var(--accent);
973
+ color: var(--accent-foreground);
974
+ border: 1px solid var(--border);
975
+ text-decoration: none;
976
+ transition: opacity 0.15s;
977
+ }
978
+
979
+ .folder-badge:hover {
980
+ opacity: 0.8;
981
+ }
982
+
983
+ .tag-remove {
984
+ display: inline-flex;
985
+ align-items: center;
986
+ justify-content: center;
987
+ width: 16px;
988
+ height: 16px;
989
+ padding: 0;
990
+ border: none;
991
+ border-radius: 3px;
992
+ background: transparent;
993
+ color: inherit;
994
+ cursor: pointer;
995
+ opacity: 0.6;
996
+ transition: opacity 0.15s, background 0.15s;
997
+ }
998
+
999
+ .tag-remove:hover {
1000
+ opacity: 1;
1001
+ background: color-mix(in srgb, currentColor 20%, transparent);
1002
+ }
1003
+
1004
+ .tag-remove:disabled {
1005
+ cursor: not-allowed;
1006
+ opacity: 0.3;
1007
+ }
1008
+
1009
+ .tag-add-btn {
1010
+ display: inline-flex;
1011
+ align-items: center;
1012
+ gap: 4px;
1013
+ padding: 2px 8px;
1014
+ border-radius: 4px;
1015
+ font-size: 12px;
1016
+ font-weight: 500;
1017
+ border: 1px dashed var(--border);
1018
+ background: transparent;
1019
+ color: var(--muted-foreground);
1020
+ cursor: pointer;
1021
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
1022
+ }
1023
+
1024
+ .tag-add-btn:hover {
1025
+ background: var(--muted);
1026
+ color: var(--foreground);
1027
+ border-color: var(--muted-foreground);
1028
+ }
1029
+
1030
+ .tag-add-btn:disabled {
1031
+ cursor: not-allowed;
1032
+ opacity: 0.5;
1033
+ }
1034
+
1035
+ .tag-swatch {
1036
+ display: inline-block;
1037
+ width: 8px;
1038
+ height: 8px;
1039
+ border-radius: 50%;
1040
+ flex-shrink: 0;
1041
+ }
1042
+
1043
+ .tag-empty {
1044
+ padding: 8px 12px;
1045
+ font-size: 12px;
1046
+ color: var(--muted-foreground);
1047
+ }
1048
+
1049
+ .editor-container {
1050
+ flex: 1;
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ border: 1px solid var(--border);
1054
+ border-radius: 8px;
1055
+ /* `visible` (not `hidden`) so the editor toolbar popovers — emoji,
1056
+ heading, list, align — are not clipped by the container edge. */
1057
+ overflow: visible;
1058
+ min-height: 500px;
1059
+ background: var(--card);
1060
+ }
1061
+
1062
+ .folder-create {
1063
+ display: inline-flex;
1064
+ align-items: center;
1065
+ gap: 4px;
1066
+ }
1067
+
1068
+ .folder-create input {
1069
+ height: 24px;
1070
+ padding: 0 8px;
1071
+ font-size: 12px;
1072
+ border: 1px solid var(--border);
1073
+ border-radius: 4px;
1074
+ background: var(--background);
1075
+ color: var(--foreground);
1076
+ }
1077
+
1078
+ .folder-create-btn {
1079
+ padding: 2px 8px;
1080
+ font-size: 12px;
1081
+ border: 1px solid var(--border);
1082
+ border-radius: 4px;
1083
+ background: transparent;
1084
+ color: var(--muted-foreground);
1085
+ cursor: pointer;
1086
+ }
1087
+
1088
+ .folder-create-btn:hover {
1089
+ background: var(--muted);
1090
+ color: var(--foreground);
1091
+ }
1092
+
1093
+ /* Mobile responsive */
1094
+ @media (max-width: 640px) {
1095
+ .editor-header {
1096
+ padding: 8px 16px;
1097
+ }
1098
+
1099
+ .breadcrumb {
1100
+ display: none;
1101
+ }
1102
+
1103
+ .editor-main {
1104
+ padding: 20px 16px;
1105
+ }
1106
+
1107
+ }
1108
+ </style>