@knotpad/app 0.1.4 → 0.1.6

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 (340) hide show
  1. package/app/(app)/calendar/page.tsx +57 -0
  2. package/app/(app)/error.tsx +35 -0
  3. package/app/(app)/graph/page.tsx +32 -0
  4. package/app/(app)/guide/page.tsx +21 -0
  5. package/app/(app)/kanban/loading.tsx +24 -0
  6. package/app/(app)/kanban/page.tsx +59 -0
  7. package/app/(app)/layout.tsx +122 -0
  8. package/app/(app)/list/loading.tsx +21 -0
  9. package/app/(app)/list/page.tsx +137 -0
  10. package/app/(app)/loading.tsx +18 -0
  11. package/app/(app)/notes/[noteId]/page.tsx +84 -0
  12. package/app/(app)/notes/layout.tsx +30 -0
  13. package/app/(app)/notes/page.tsx +39 -0
  14. package/app/(app)/page.tsx +5 -0
  15. package/app/(app)/settings/agent-token/page.tsx +59 -0
  16. package/app/(app)/settings/backup/page.tsx +49 -0
  17. package/app/(app)/settings/billing/page.tsx +53 -0
  18. package/app/(app)/settings/calendar/page.tsx +41 -0
  19. package/app/(app)/settings/layout.test.tsx +39 -0
  20. package/app/(app)/settings/layout.tsx +71 -0
  21. package/app/(app)/settings/page.tsx +4 -0
  22. package/app/(app)/settings/security/page.tsx +43 -0
  23. package/app/(app)/settings/team/page.tsx +74 -0
  24. package/app/(app)/settings/workspace/page.tsx +27 -0
  25. package/app/(app)/tasks/[taskId]/page.tsx +79 -0
  26. package/app/(auth)/forgot-password/page.tsx +106 -0
  27. package/app/(auth)/guest/page.tsx +56 -0
  28. package/app/(auth)/layout.tsx +13 -0
  29. package/app/(auth)/login/page.tsx +14 -0
  30. package/app/(auth)/register/page.tsx +193 -0
  31. package/app/(auth)/reset-password/page.tsx +138 -0
  32. package/app/api/account/claim/route.tsx +135 -0
  33. package/app/api/admin/backfill-encryption/route.tsx +43 -0
  34. package/app/api/admin/license/route.tsx +42 -0
  35. package/app/api/auth/2fa/route.tsx +148 -0
  36. package/app/api/auth/[...nextauth]/route.tsx +3 -0
  37. package/app/api/auth/change-password/route.tsx +61 -0
  38. package/app/api/auth/check-2fa/route.tsx +19 -0
  39. package/app/api/auth/forgot-password/route.tsx +65 -0
  40. package/app/api/auth/reset-password/route.tsx +52 -0
  41. package/app/api/auth/verify-2fa/route.tsx +88 -0
  42. package/app/api/backup/download/db/route.ts +29 -0
  43. package/app/api/backup/download/notes/route.ts +25 -0
  44. package/app/api/backup/settings/route.ts +92 -0
  45. package/app/api/billing/checkout/route.tsx +81 -0
  46. package/app/api/billing/migrate/route.tsx +163 -0
  47. package/app/api/billing/portal/route.tsx +24 -0
  48. package/app/api/billing/setup-intent/route.tsx +55 -0
  49. package/app/api/billing/status/route.tsx +36 -0
  50. package/app/api/billing/subscribe/route.tsx +85 -0
  51. package/app/api/billing/webhook/route.tsx +199 -0
  52. package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
  53. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
  54. package/app/api/calendar-feeds/events/route.tsx +82 -0
  55. package/app/api/calendar-feeds/route.tsx +52 -0
  56. package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
  57. package/app/api/cron/calendar-feeds/route.tsx +31 -0
  58. package/app/api/cron/stale-tasks/route.tsx +51 -0
  59. package/app/api/cron/sync/route.tsx +34 -0
  60. package/app/api/devices/[deviceId]/route.tsx +25 -0
  61. package/app/api/devices/route.tsx +41 -0
  62. package/app/api/export/route.tsx +40 -0
  63. package/app/api/feedback/route.tsx +54 -0
  64. package/app/api/folders/[folderId]/route.tsx +51 -0
  65. package/app/api/folders/route.tsx +37 -0
  66. package/app/api/graph/route.tsx +242 -0
  67. package/app/api/guest/route.tsx +58 -0
  68. package/app/api/health/route.tsx +10 -0
  69. package/app/api/holidays/countries/route.tsx +14 -0
  70. package/app/api/holidays/route.tsx +49 -0
  71. package/app/api/holidays/states/route.tsx +21 -0
  72. package/app/api/invites/[token]/route.tsx +131 -0
  73. package/app/api/invites/route.tsx +74 -0
  74. package/app/api/mcp/generate-token/route.tsx +55 -0
  75. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
  76. package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
  77. package/app/api/notes/[noteId]/export/route.tsx +45 -0
  78. package/app/api/notes/[noteId]/route.tsx +360 -0
  79. package/app/api/notes/route.tsx +112 -0
  80. package/app/api/notifications/route.tsx +44 -0
  81. package/app/api/register/route.tsx +67 -0
  82. package/app/api/restore/route.tsx +148 -0
  83. package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
  84. package/app/api/sync/conflicts/route.tsx +48 -0
  85. package/app/api/sync/status/route.tsx +49 -0
  86. package/app/api/sync/trigger/route.tsx +15 -0
  87. package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
  88. package/app/api/tasks/[taskId]/route.tsx +259 -0
  89. package/app/api/tasks/bulk/route.tsx +133 -0
  90. package/app/api/tasks/route.tsx +36 -0
  91. package/app/api/workspace/active/route.tsx +39 -0
  92. package/app/api/workspace/create-team/route.tsx +42 -0
  93. package/app/api/workspace/kanban-statuses/route.tsx +71 -0
  94. package/app/api/workspace/members/[memberId]/route.tsx +69 -0
  95. package/app/api/workspace/route.tsx +24 -0
  96. package/app/download/page.tsx +170 -0
  97. package/app/favicon.ico +0 -0
  98. package/app/generated/prisma/client.d.ts +1 -0
  99. package/app/generated/prisma/client.js +5 -0
  100. package/app/generated/prisma/default.d.ts +1 -0
  101. package/app/generated/prisma/default.js +5 -0
  102. package/app/generated/prisma/edge.d.ts +1 -0
  103. package/app/generated/prisma/edge.js +497 -0
  104. package/app/generated/prisma/index-browser.js +523 -0
  105. package/app/generated/prisma/index.d.ts +46376 -0
  106. package/app/generated/prisma/index.js +497 -0
  107. package/app/generated/prisma/package.json +144 -0
  108. package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
  109. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  110. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
  111. package/app/generated/prisma/runtime/client.d.ts +3386 -0
  112. package/app/generated/prisma/runtime/client.js +86 -0
  113. package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
  114. package/app/generated/prisma/runtime/index-browser.js +6 -0
  115. package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
  116. package/app/generated/prisma/schema.prisma +456 -0
  117. package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
  118. package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
  119. package/app/globals.css +54 -0
  120. package/app/invite/[token]/page.tsx +52 -0
  121. package/app/layout.tsx +90 -0
  122. package/app/mcp/route.tsx +430 -0
  123. package/app/opengraph-image.tsx +120 -0
  124. package/app/page.tsx +398 -0
  125. package/app/privacy/page.tsx +69 -0
  126. package/app/robots.tsx +25 -0
  127. package/app/sitemap.tsx +36 -0
  128. package/app/terms/page.tsx +69 -0
  129. package/app/upgrade/page.tsx +75 -0
  130. package/auth.config.ts +33 -0
  131. package/auth.ts +79 -0
  132. package/bin/brief.js +224 -0
  133. package/components/auth/login-form.tsx +302 -0
  134. package/components/auth/password-checklist.tsx +31 -0
  135. package/components/auth/password-input.tsx +36 -0
  136. package/components/auth/switch-account-button.test.tsx +22 -0
  137. package/components/auth/switch-account-button.tsx +19 -0
  138. package/components/auth/two-factor-input.tsx +116 -0
  139. package/components/billing/billing-dashboard.tsx +265 -0
  140. package/components/billing/card-form.tsx +210 -0
  141. package/components/billing/claim-account-form.tsx +99 -0
  142. package/components/branding/app-logo.test.tsx +20 -0
  143. package/components/branding/app-logo.tsx +25 -0
  144. package/components/calendar/calendar-agenda.tsx +150 -0
  145. package/components/calendar/calendar-drag.test.tsx +177 -0
  146. package/components/calendar/calendar-grid.tsx +357 -0
  147. package/components/calendar/calendar-hooks.test.tsx +27 -0
  148. package/components/calendar/calendar-hooks.ts +351 -0
  149. package/components/calendar/calendar-toolbar.test.tsx +68 -0
  150. package/components/calendar/calendar-toolbar.tsx +291 -0
  151. package/components/calendar/calendar-types.ts +148 -0
  152. package/components/calendar/calendar-view.test.tsx +295 -0
  153. package/components/calendar/calendar-view.tsx +307 -0
  154. package/components/calendar/day-detail-popover.tsx +174 -0
  155. package/components/calendar/task-chip.tsx +86 -0
  156. package/components/command/command-palette.test.tsx +33 -0
  157. package/components/command/command-palette.tsx +310 -0
  158. package/components/download-cta.tsx +87 -0
  159. package/components/feedback/feedback-popup.tsx +207 -0
  160. package/components/graph/graph-draw.ts +337 -0
  161. package/components/graph/graph-overlays.tsx +160 -0
  162. package/components/graph/graph-page.test.tsx +131 -0
  163. package/components/graph/graph-page.tsx +263 -0
  164. package/components/graph/graph-types.ts +47 -0
  165. package/components/graph/graph-view.tsx +322 -0
  166. package/components/guide/guide-view.tsx +522 -0
  167. package/components/kanban/kanban-board.test.tsx +128 -0
  168. package/components/kanban/kanban-board.tsx +361 -0
  169. package/components/kanban/kanban-card-menu.tsx +102 -0
  170. package/components/kanban/kanban-card.tsx +227 -0
  171. package/components/kanban/kanban-column.tsx +49 -0
  172. package/components/kanban/kanban-status-context.tsx +28 -0
  173. package/components/landing/calendar-sandbox.test.tsx +15 -0
  174. package/components/landing/calendar-sandbox.tsx +107 -0
  175. package/components/landing/graph-sandbox.test.tsx +27 -0
  176. package/components/landing/graph-sandbox.tsx +80 -0
  177. package/components/landing/kanban-sandbox.test.tsx +24 -0
  178. package/components/landing/kanban-sandbox.tsx +101 -0
  179. package/components/landing/landing-showcase.test.tsx +21 -0
  180. package/components/landing/landing-showcase.tsx +54 -0
  181. package/components/landing/list-sandbox.tsx +86 -0
  182. package/components/landing/mock-workspace.ts +168 -0
  183. package/components/landing/notes-sandbox.test.tsx +14 -0
  184. package/components/landing/notes-sandbox.tsx +88 -0
  185. package/components/layout/app-shell.tsx +83 -0
  186. package/components/layout/backup-scheduler.tsx +122 -0
  187. package/components/layout/bottom-nav.tsx +43 -0
  188. package/components/layout/icon-bar.test.tsx +29 -0
  189. package/components/layout/icon-bar.tsx +118 -0
  190. package/components/layout/mobile-top-bar.tsx +68 -0
  191. package/components/layout/notes-panel-folder.tsx +127 -0
  192. package/components/layout/notes-panel-note-item.tsx +140 -0
  193. package/components/layout/notes-panel-task-tab.tsx +63 -0
  194. package/components/layout/notes-panel-types.ts +44 -0
  195. package/components/layout/notes-panel.tsx +476 -0
  196. package/components/layout/notification-bell.tsx +251 -0
  197. package/components/layout/paywall-screen.tsx +41 -0
  198. package/components/layout/pro-banner.tsx +76 -0
  199. package/components/layout/sw-register.tsx +27 -0
  200. package/components/layout/workspace-switcher.tsx +90 -0
  201. package/components/notes/mobile-bottom-sheet.tsx +99 -0
  202. package/components/notes/note-editor-context-menu.tsx +47 -0
  203. package/components/notes/note-editor-dom.ts +33 -0
  204. package/components/notes/note-editor-dropdowns.tsx +484 -0
  205. package/components/notes/note-editor-hooks.ts +692 -0
  206. package/components/notes/note-editor-keyboard.ts +305 -0
  207. package/components/notes/note-editor-overlay.tsx +90 -0
  208. package/components/notes/note-editor.test.tsx +372 -0
  209. package/components/notes/note-editor.tsx +662 -0
  210. package/components/notes/note-preview-pane.tsx +156 -0
  211. package/components/notes/note-tabs.tsx +120 -0
  212. package/components/notes/note-types.tsx +157 -0
  213. package/components/settings/accept-invite.tsx +108 -0
  214. package/components/settings/agent-token-settings.tsx +369 -0
  215. package/components/settings/backup-restore-settings.test.tsx +25 -0
  216. package/components/settings/backup-restore-settings.tsx +327 -0
  217. package/components/settings/calendar-feeds-settings.tsx +489 -0
  218. package/components/settings/calendar-general-settings.tsx +174 -0
  219. package/components/settings/confirm-danger-action.test.tsx +215 -0
  220. package/components/settings/confirm-danger-action.tsx +65 -0
  221. package/components/settings/security-settings.tsx +252 -0
  222. package/components/settings/settings-guidance.test.tsx +98 -0
  223. package/components/settings/team-settings.tsx +319 -0
  224. package/components/settings/two-factor-auth.tsx +296 -0
  225. package/components/settings/workspace-settings-client.tsx +363 -0
  226. package/components/settings/workspace-settings-form.tsx +73 -0
  227. package/components/sync/conflict-viewer.tsx +247 -0
  228. package/components/sync/sync-indicator.tsx +171 -0
  229. package/components/tasks/snippet-thread.tsx +119 -0
  230. package/components/tasks/status-dot.tsx +47 -0
  231. package/components/tasks/task-badge.tsx +43 -0
  232. package/components/tasks/task-detail.test.tsx +187 -0
  233. package/components/tasks/task-detail.tsx +458 -0
  234. package/components/tasks/task-list-filters.test.tsx +75 -0
  235. package/components/tasks/task-list-filters.tsx +163 -0
  236. package/components/tasks/task-list-types.ts +20 -0
  237. package/components/tasks/task-list.test.tsx +175 -0
  238. package/components/tasks/task-list.tsx +481 -0
  239. package/components/tasks/task-row.tsx +85 -0
  240. package/components/tasks/task-table-row.tsx +259 -0
  241. package/components/ui/skeleton.tsx +3 -0
  242. package/components/ui/toast.test.tsx +42 -0
  243. package/components/ui/toast.tsx +70 -0
  244. package/instrumentation.tsx +23 -0
  245. package/lib/api-error.ts +50 -0
  246. package/lib/backup/backup-runner.test.ts +32 -0
  247. package/lib/backup/backup-runner.ts +19 -0
  248. package/lib/backup/backup-schedule.test.ts +23 -0
  249. package/lib/backup/backup-schedule.ts +55 -0
  250. package/lib/backup/backup-settings.test.ts +30 -0
  251. package/lib/backup/backup-settings.ts +27 -0
  252. package/lib/backup/export-notes-zip.test.ts +26 -0
  253. package/lib/backup/export-notes-zip.ts +82 -0
  254. package/lib/backup/export-workspace-backup.test.ts +17 -0
  255. package/lib/backup/export-workspace-backup.ts +77 -0
  256. package/lib/backup/restore-workspace-from-export.test.ts +18 -0
  257. package/lib/backup/restore-workspace-from-export.ts +183 -0
  258. package/lib/backup/types.ts +14 -0
  259. package/lib/brand-icons.ts +1 -0
  260. package/lib/calendar-feed-crypto.ts +38 -0
  261. package/lib/calendar-feed.ts +239 -0
  262. package/lib/client/online-status.ts +47 -0
  263. package/lib/conflict-resolver.test.ts +57 -0
  264. package/lib/conflict-resolver.ts +240 -0
  265. package/lib/db-init.ts +79 -0
  266. package/lib/email.ts +159 -0
  267. package/lib/encryption.test.ts +41 -0
  268. package/lib/encryption.ts +98 -0
  269. package/lib/extract-snippet.test.ts +123 -0
  270. package/lib/extract-snippet.ts +69 -0
  271. package/lib/kanban-status.ts +55 -0
  272. package/lib/license.ts +21 -0
  273. package/lib/limits.ts +31 -0
  274. package/lib/mcp-auth.test.ts +58 -0
  275. package/lib/mcp-auth.ts +65 -0
  276. package/lib/mcp-contract.test.ts +25 -0
  277. package/lib/mcp-contract.ts +210 -0
  278. package/lib/mcp-handler.ts +31 -0
  279. package/lib/mcp-url.test.ts +12 -0
  280. package/lib/mcp-url.ts +7 -0
  281. package/lib/mentions.test.ts +45 -0
  282. package/lib/mentions.ts +73 -0
  283. package/lib/note-crypto.ts +108 -0
  284. package/lib/note-sync.ts +201 -0
  285. package/lib/note-title.ts +93 -0
  286. package/lib/prisma.ts +193 -0
  287. package/lib/pro-flush.ts +292 -0
  288. package/lib/rate-limit.ts +57 -0
  289. package/lib/stripe.ts +38 -0
  290. package/lib/sync-worker.ts +388 -0
  291. package/lib/task-parser.test.ts +91 -0
  292. package/lib/task-parser.ts +81 -0
  293. package/lib/task-utils.ts +52 -0
  294. package/lib/use-is-electron.ts +19 -0
  295. package/lib/use-is-mobile.ts +22 -0
  296. package/lib/validation/calendar-feed.ts +31 -0
  297. package/lib/validation/note.ts +27 -0
  298. package/lib/validation/task.ts +26 -0
  299. package/lib/view-preferences.test.ts +54 -0
  300. package/lib/view-preferences.ts +28 -0
  301. package/lib/workspace.ts +66 -0
  302. package/next.config.ts +21 -0
  303. package/package.json +49 -3
  304. package/postcss.config.mjs +7 -0
  305. package/prisma/migrations/20260519021916_init/migration.sql +388 -0
  306. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
  307. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
  308. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
  309. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
  310. package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
  311. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
  312. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
  313. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
  314. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
  315. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
  316. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
  317. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
  318. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
  319. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
  320. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
  321. package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
  322. package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
  323. package/prisma/migrations/migration_lock.toml +3 -0
  324. package/prisma/schema.prisma +457 -0
  325. package/public/Logo_icon.svg +1 -0
  326. package/public/file.svg +1 -0
  327. package/public/globe.svg +1 -0
  328. package/public/icon-192.png +0 -0
  329. package/public/icon-512.png +0 -0
  330. package/public/icon.svg +4 -0
  331. package/public/icon_dark.svg +1 -0
  332. package/public/knotpad_icon.svg +1 -0
  333. package/public/knotpad_logo_full.svg +1 -0
  334. package/public/manifest.json +14 -0
  335. package/public/next.svg +1 -0
  336. package/public/sw.js +137 -0
  337. package/public/vercel.svg +1 -0
  338. package/public/window.svg +1 -0
  339. package/tsconfig.json +35 -0
  340. package/brief.js +0 -311
@@ -0,0 +1,662 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import Link from "next/link";
5
+ import {
6
+ Download, CheckCircle2, X,
7
+ } from "lucide-react";
8
+ import { StatusDot } from "@/components/tasks/status-dot";
9
+ import { useKanbanStatuses } from "@/components/kanban/kanban-status-context";
10
+ import { useToast } from "@/components/ui/toast";
11
+ import { loadPreference, savePreference } from "@/lib/view-preferences";
12
+ import { extractTitleFromContent } from "@/lib/note-title";
13
+ import { useIsMobile } from "@/lib/use-is-mobile";
14
+ import { MobileBottomSheet } from "./mobile-bottom-sheet";
15
+
16
+ import type { Task, Props } from "./note-types";
17
+ import { extractTaskTitleFromLine, stripBadgeComments, formatTodayHeader } from "./note-types";
18
+ import { EditorOverlay, getTaskBadgeRows, GUTTER_PX } from "./note-editor-overlay";
19
+ import { PreviewPane } from "./note-preview-pane";
20
+ import {
21
+ SlashPalette, MentionDropdown, NotePickerDropdown, TaskPickerDropdown,
22
+ PlusPickerDropdown, DatePickerDropdown, TemplatePickerDropdown, SoftHintBar,
23
+ PriorityPickerDropdown,
24
+ } from "./note-editor-dropdowns";
25
+ import { EditorContextMenu } from "./note-editor-context-menu";
26
+ import { useNoteSave, useUndoHistory, usePickerState } from "./note-editor-hooks";
27
+ import { useNoteEditorKeyboard } from "./note-editor-keyboard";
28
+
29
+ // ── PickerShell — desktop: inline, mobile: bottom sheet ─────────────────────
30
+ function PickerShell({
31
+ open, onClose, title, isMobile, children,
32
+ }: {
33
+ open: boolean;
34
+ onClose: () => void;
35
+ title?: string;
36
+ isMobile: boolean;
37
+ children: React.ReactNode;
38
+ }) {
39
+ if (!open) return null;
40
+ if (isMobile) {
41
+ return (
42
+ <MobileBottomSheet open={open} onClose={onClose} title={title}>
43
+ {children}
44
+ </MobileBottomSheet>
45
+ );
46
+ }
47
+ return <>{children}</>;
48
+ }
49
+
50
+ type EditorViewMode = "edit" | "split" | "preview";
51
+
52
+ export function NoteEditor({ note, members = [], notesList = [], tasksList = [], highlight }: Props) {
53
+ // ── Local state ─────────────────────────────────────────────────────────
54
+ const [title, setTitle] = useState(note.title);
55
+ const [content, setContent] = useState(stripBadgeComments(note.content));
56
+ const [localTasks, setLocalTasks] = useState<Task[]>(note.tasks);
57
+ const [showLockWarning, setShowLockWarning] = useState(false);
58
+ const [allDoneDismissed, setAllDoneDismissed] = useState(false);
59
+ const [viewMode, setViewMode] = useState<EditorViewMode>(() =>
60
+ loadPreference("brief:notes-view-mode", "edit")
61
+ );
62
+ const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
63
+ const [badgePositions, setBadgePositions] = useState<Record<number, number>>({});
64
+ const userEditedTitle = useRef(false);
65
+ const hadPendingSave = useRef(false);
66
+ const titleRef = useRef<HTMLInputElement>(null);
67
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
68
+ const overlayRef = useRef<HTMLDivElement>(null);
69
+
70
+ // ── Hooks ───────────────────────────────────────────────────────────────
71
+ const kanbanStatuses = useKanbanStatuses();
72
+ const toast = useToast();
73
+ const isMobile = useIsMobile();
74
+ const availableViewModes: { key: EditorViewMode; label: string }[] = isMobile
75
+ ? [
76
+ { key: "edit", label: "Edit only" },
77
+ { key: "preview", label: "Preview only" },
78
+ ]
79
+ : [
80
+ { key: "edit", label: "Edit only" },
81
+ { key: "split", label: "Split view" },
82
+ { key: "preview", label: "Preview only" },
83
+ ];
84
+
85
+ const { historyStack, historyPos, historyTimer, pushHistory, pushHistoryImmediate } =
86
+ useUndoHistory(stripBadgeComments(note.content));
87
+
88
+ const {
89
+ saving, saveError, setSaveError, staleReloaded, setStaleReloaded,
90
+ savePending, scheduleSave,
91
+ } = useNoteSave(note.id, note.version, setLocalTasks, setContent, setTitle);
92
+
93
+ const p = usePickerState(
94
+ content, title, members, notesList, tasksList,
95
+ note.id, textareaRef, scheduleSave, setContent,
96
+ );
97
+ const savedCursor = p.savedCursor;
98
+
99
+ const { handleKeyDown } = useNoteEditorKeyboard({
100
+ content, title, textareaRef, savedCursor: p.savedCursor,
101
+ historyTimer, historyPos, historyStack, pushHistoryImmediate,
102
+ scheduleSave, setContent,
103
+ slashQuery: p.slashQuery, setSlashQuery: p.setSlashQuery,
104
+ slashIndex: p.slashIndex, setSlashIndex: p.setSlashIndex,
105
+ slashSuggestions: p.slashSuggestions,
106
+ executeSlashCommand: p.executeSlashCommand,
107
+ setDropdownPos: p.setDropdownPos,
108
+ mentionQuery: p.mentionQuery, setMentionQuery: p.setMentionQuery,
109
+ mentionIndex: p.mentionIndex, setMentionIndex: p.setMentionIndex,
110
+ mentionSuggestions: p.mentionSuggestions,
111
+ insertMention: p.insertMention,
112
+ taskQuery: p.taskQuery, setTaskQuery: p.setTaskQuery,
113
+ taskIndex: p.taskIndex, setTaskIndex: p.setTaskIndex,
114
+ taskSuggestions: p.taskSuggestions,
115
+ insertTaskRef: p.insertTaskRef,
116
+ priorityQuery: p.priorityQuery, setPriorityQuery: p.setPriorityQuery,
117
+ priorityIndex: p.priorityIndex, setPriorityIndex: p.setPriorityIndex,
118
+ prioritySuggestions: p.prioritySuggestions,
119
+ insertPriority: p.insertPriority,
120
+ plusOpen: p.plusOpen, setPlusOpen: p.setPlusOpen,
121
+ triggerPlusOption: p.triggerPlusOption,
122
+ templateOpen: p.templateOpen, setTemplateOpen: p.setTemplateOpen,
123
+ contextMenu: p.contextMenu, setContextMenu: p.setContextMenu,
124
+ noteQuery: p.noteQuery, setNoteQuery: p.setNoteQuery,
125
+ noteIndex: p.noteIndex, setNoteIndex: p.setNoteIndex,
126
+ noteSuggestions: p.noteSuggestions,
127
+ insertNoteLink: p.insertNoteLink,
128
+ });
129
+
130
+ // ── Mount effects ───────────────────────────────────────────────────────
131
+ useEffect(() => {
132
+ if (note.title && note.title.trim() && note.title.trim() !== "Untitled") {
133
+ userEditedTitle.current = true;
134
+ }
135
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
136
+
137
+ useEffect(() => {
138
+ if (isMobile && viewMode === "split") {
139
+ setViewMode("edit");
140
+ savePreference("brief:notes-view-mode", "edit");
141
+ return;
142
+ }
143
+ savePreference("brief:notes-view-mode", viewMode);
144
+ }, [isMobile, viewMode]);
145
+
146
+ useEffect(() => {
147
+ if (savePending.current) {
148
+ hadPendingSave.current = true;
149
+ return;
150
+ }
151
+ if (hadPendingSave.current && !saving && !saveError) {
152
+ hadPendingSave.current = false;
153
+ setLastSavedAt(Date.now());
154
+ }
155
+ }, [content, localTasks, saveError, savePending, saving, title]);
156
+
157
+ // Scroll to and select highlighted text on deep-link
158
+ useEffect(() => {
159
+ if (!highlight || !textareaRef.current) return;
160
+ const ta = textareaRef.current;
161
+ const needle = highlight.toLowerCase();
162
+ const idx = ta.value.toLowerCase().indexOf(needle);
163
+ if (idx === -1) return;
164
+ ta.focus();
165
+ ta.setSelectionRange(idx, idx + highlight.length);
166
+ const linesBefore = ta.value.slice(0, idx).split("\n").length - 1;
167
+ const lineH = parseInt(getComputedStyle(ta).lineHeight) || 24;
168
+ ta.scrollTop = Math.max(0, linesBefore * lineH - ta.clientHeight / 3);
169
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
170
+
171
+ // Toggle a task checkbox from the preview pane
172
+ function toggleTaskInPreview(checkboxIndex: number) {
173
+ const lines = content.split("\n");
174
+ const taskLinePattern = /^(\s*)((?:[-*]|\d+\.))(\s+)\[([ x])\](.*)$/;
175
+ let taskCount = 0;
176
+ for (let i = 0; i < lines.length; i++) {
177
+ const matchedLine = taskLinePattern.exec(lines[i]);
178
+ if (matchedLine) {
179
+ if (taskCount === checkboxIndex) {
180
+ const [, indent, marker, spacing, checkState, remainder] = matchedLine;
181
+ const wasChecked = checkState === "x";
182
+ lines[i] = `${indent}${marker}${spacing}[${wasChecked ? " " : "x"}]${remainder}`;
183
+ const newContent = lines.join("\n");
184
+ setContent(newContent);
185
+ scheduleSave(title, newContent);
186
+ const rawText = remainder.trim();
187
+ const taskTitle = rawText
188
+ .replace(/\[\[[^\]]*\]\]/g, "").replace(/\(\([^)]*\)\)/g, "")
189
+ .replace(/@[\w-]+/g, "").replace(/<[^>]+>/g, "")
190
+ .replace(/(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})/g, "")
191
+ .replace(/(\d{4}-\d{2}-\d{2})/g, "").replace(/\s*<!--task::[A-Z_]+-->/g, "").trim();
192
+ const matchedTask = tasksList.find((t) => t.title === taskTitle);
193
+ if (matchedTask) {
194
+ fetch(`/api/tasks/${matchedTask.id}`, {
195
+ method: "PATCH", headers: { "Content-Type": "application/json" },
196
+ body: JSON.stringify({ status: wasChecked ? "OPEN" : "DONE" }),
197
+ }).catch(() => {});
198
+ }
199
+ break;
200
+ }
201
+ taskCount++;
202
+ }
203
+ }
204
+ }
205
+
206
+ // Measure badge positions for text wrapping
207
+ useEffect(() => {
208
+ function measureBadgePositions() {
209
+ const overlay = overlayRef.current;
210
+ if (!overlay) return;
211
+ const lines = content.split("\n");
212
+ const positions: Record<number, number> = {};
213
+ lines.forEach((line, i) => {
214
+ const taskTitle = extractTaskTitleFromLine(line);
215
+ if (taskTitle && localTasks.find((t) => t.title === taskTitle)) {
216
+ const spans = overlay.querySelectorAll("span > span");
217
+ for (const span of spans) {
218
+ if (span.textContent === line || span.textContent?.includes(line.slice(0, 20))) {
219
+ const rect = span.getBoundingClientRect();
220
+ const overlayRect = overlay.getBoundingClientRect();
221
+ positions[i] = rect.top - overlayRect.top;
222
+ break;
223
+ }
224
+ }
225
+ if (!(i in positions)) positions[i] = i * 24;
226
+ }
227
+ });
228
+ setBadgePositions(positions);
229
+ }
230
+ measureBadgePositions();
231
+ const ta = textareaRef.current;
232
+ if (!ta) return;
233
+ const resizeObserver = new ResizeObserver(() => measureBadgePositions());
234
+ resizeObserver.observe(ta);
235
+ return () => resizeObserver.disconnect();
236
+ }, [content, localTasks]);
237
+
238
+ // ── Handlers ────────────────────────────────────────────────────────────
239
+ function handleTitleChange(e: React.ChangeEvent<HTMLInputElement>) {
240
+ userEditedTitle.current = true;
241
+ setTitle(e.target.value);
242
+ scheduleSave(e.target.value, content);
243
+ }
244
+
245
+ function handleContentChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
246
+ const composing = (e.nativeEvent as InputEvent).isComposing ?? false;
247
+ const val = e.target.value;
248
+ if (!composing) {
249
+ p.savedCursor.current = { start: e.target.selectionStart, end: e.target.selectionEnd };
250
+ }
251
+ setContent(val);
252
+ if (composing) return;
253
+ pushHistory(val);
254
+ p.detectTriggers(val, e.target.selectionStart);
255
+ let newTitle = title;
256
+ if (!userEditedTitle.current) {
257
+ const extracted = extractTitleFromContent(val);
258
+ if (extracted) { newTitle = extracted; setTitle(newTitle); }
259
+ }
260
+ scheduleSave(newTitle, val);
261
+ }
262
+
263
+ // After every render, restore cursor and auto-grow textarea
264
+ useLayoutEffect(() => {
265
+ const ta = textareaRef.current;
266
+ if (!ta) return;
267
+ ta.style.height = "auto";
268
+ ta.style.height = ta.scrollHeight + "px";
269
+ if (savedCursor.current) {
270
+ ta.setSelectionRange(savedCursor.current.start, savedCursor.current.end);
271
+ savedCursor.current = null;
272
+ }
273
+ }, [content, savedCursor]);
274
+
275
+ const activeStatusKeys = kanbanStatuses
276
+ .filter((s) => s.isVisible && s.key !== "OPEN" && s.key !== "DONE")
277
+ .map((s) => s.key);
278
+ const hasActiveTasks = localTasks.some((t) => activeStatusKeys.includes(t.status));
279
+ const allTasksDone = localTasks.length > 0 && localTasks.every((t) => t.status === "DONE");
280
+ const effectiveViewMode: EditorViewMode = isMobile && viewMode === "split" ? "edit" : viewMode;
281
+ const showEditorPane = effectiveViewMode !== "preview";
282
+ const showPreviewPane = effectiveViewMode !== "edit";
283
+ const saveLabel = saving
284
+ ? "Saving..."
285
+ : saveError
286
+ ? "Save failed"
287
+ : lastSavedAt && !savePending.current
288
+ ? "Saved just now"
289
+ : null;
290
+
291
+ function handleExport() { window.open(`/api/notes/${note.id}/export`, "_blank"); }
292
+
293
+ function handleContextMenu(e: React.MouseEvent<HTMLTextAreaElement>) {
294
+ const ta = e.currentTarget;
295
+ if (ta.selectionStart === ta.selectionEnd) return;
296
+ e.preventDefault();
297
+ const vw = window.innerWidth, vh = window.innerHeight;
298
+ p.setContextMenu({
299
+ top: Math.min(e.clientY, vh - 196), left: Math.min(e.clientX, vw - 192),
300
+ selStart: ta.selectionStart, selEnd: ta.selectionEnd,
301
+ });
302
+ }
303
+
304
+ function copyDeepLink() {
305
+ if (!p.contextMenu) return;
306
+ const { selStart, selEnd } = p.contextMenu;
307
+ const sel = content.slice(selStart, selEnd);
308
+ const url = `${window.location.origin}/notes/${note.id}?q=${encodeURIComponent(sel)}`;
309
+ navigator.clipboard.writeText(url)
310
+ .then(() => toast("Link to selection copied", "success"))
311
+ .catch(() => toast("Couldn't copy link", "error"));
312
+ p.setContextMenu(null);
313
+ }
314
+
315
+ // Badge gutter
316
+ const badgeRows = getTaskBadgeRows(content, localTasks);
317
+ const gutterWidth = badgeRows.length === 0 ? 0 : GUTTER_PX;
318
+
319
+ return (
320
+ <div className="flex flex-1 flex-col overflow-hidden">
321
+ {/* Breadcrumb + status bar */}
322
+ <div className="flex items-center gap-3 border-b border-zinc-800 px-6 py-2.5">
323
+ <div className="flex-1 min-w-0">
324
+ <nav className="flex items-center gap-1 text-xs text-zinc-600">
325
+ <span>Workspace</span><span>/</span>
326
+ {note.folder && (<><span className="truncate">{note.folder.name}</span><span>/</span></>)}
327
+ <span className="text-zinc-400 truncate">{title}</span>
328
+ </nav>
329
+ </div>
330
+ <div className="flex items-center gap-2">
331
+ {hasActiveTasks && <span className="text-xs text-amber-400">Tasks active</span>}
332
+ {saveLabel && <span className="text-xs text-zinc-600">{saveLabel}</span>}
333
+ <div className="inline-flex rounded-md border border-zinc-800 bg-zinc-950 p-1">
334
+ {availableViewModes.map((mode) => (
335
+ <button
336
+ key={mode.key}
337
+ type="button"
338
+ aria-pressed={effectiveViewMode === mode.key}
339
+ onClick={() => setViewMode(mode.key as EditorViewMode)}
340
+ className={`rounded px-2 py-1 text-xs transition-colors ${
341
+ effectiveViewMode === mode.key
342
+ ? "bg-zinc-800 text-zinc-200"
343
+ : "text-zinc-500 hover:bg-zinc-900 hover:text-zinc-300"
344
+ }`}
345
+ >
346
+ {mode.label}
347
+ </button>
348
+ ))}
349
+ </div>
350
+ <button onClick={handleExport} title="Download as .md"
351
+ className="flex items-center gap-1 rounded px-2 py-1 text-xs text-zinc-600 hover:bg-zinc-800 hover:text-zinc-300 transition-colors">
352
+ <Download size={12} /><span>Export</span>
353
+ </button>
354
+ </div>
355
+ </div>
356
+
357
+ {showEditorPane && (
358
+ <div className="border-b border-zinc-800/60 px-6 py-2">
359
+ <div className="flex flex-wrap items-center gap-2 text-xs text-zinc-500">
360
+ <span className="text-zinc-300">Editing help</span>
361
+ <span>/ commands</span>
362
+ <span>@ mentions</span>
363
+ <span>+ insert</span>
364
+ <span>- [ ] tasks</span>
365
+ </div>
366
+ </div>
367
+ )}
368
+
369
+ {hasActiveTasks && showLockWarning && (
370
+ <div className="flex items-center gap-2 bg-amber-950/40 border-b border-amber-800/40 px-6 py-2">
371
+ <span className="text-sm text-amber-300">Tasks are active in this note. Edit anyway?</span>
372
+ <button onClick={() => setShowLockWarning(false)} className="text-xs text-amber-400 underline">Yes, edit</button>
373
+ </div>
374
+ )}
375
+
376
+ {saveError && (
377
+ <div className="flex items-center justify-between gap-2 bg-red-950/40 border-b border-red-800/40 px-6 py-2">
378
+ <span className="text-sm text-red-300">{saveError}</span>
379
+ <button onClick={() => setSaveError(null)} className="text-zinc-500 hover:text-zinc-300"><X size={14} /></button>
380
+ </div>
381
+ )}
382
+
383
+ {staleReloaded && (
384
+ <div className="flex items-center justify-between gap-2 bg-blue-950/40 border-b border-blue-800/40 px-6 py-2">
385
+ <span className="text-sm text-blue-300">This note was edited elsewhere — reloaded the latest version to avoid overwriting it.</span>
386
+ <button onClick={() => setStaleReloaded(false)} className="text-zinc-500 hover:text-zinc-300"><X size={14} /></button>
387
+ </div>
388
+ )}
389
+
390
+ {allTasksDone && !allDoneDismissed && (
391
+ <div className="flex items-center justify-between bg-emerald-950/40 border-b border-emerald-800/40 px-6 py-2">
392
+ <div className="flex items-center gap-2">
393
+ <CheckCircle2 size={14} className="text-emerald-400" />
394
+ <span className="text-sm text-emerald-300">All tasks complete!</span>
395
+ </div>
396
+ <div className="flex items-center gap-3">
397
+ <button onClick={handleExport}
398
+ className="flex items-center gap-1.5 rounded-md bg-emerald-900/60 px-3 py-1 text-xs font-medium text-emerald-300 hover:bg-emerald-900 transition-colors">
399
+ <Download size={12} /> Export .md
400
+ </button>
401
+ <button onClick={() => setAllDoneDismissed(true)} className="text-zinc-600 hover:text-zinc-400 transition-colors">
402
+ <X size={14} />
403
+ </button>
404
+ </div>
405
+ </div>
406
+ )}
407
+
408
+ <div className="flex flex-1 overflow-hidden">
409
+ {/* ── Editor pane ── */}
410
+ {showEditorPane && (
411
+ <div
412
+ className={`flex min-w-0 flex-col overflow-y-auto px-6 py-6 ${
413
+ effectiveViewMode === "split"
414
+ ? "basis-[56%] border-r border-zinc-800"
415
+ : "flex-1"
416
+ }`}
417
+ >
418
+ <div className="mb-1 text-xs font-medium text-zinc-500">{formatTodayHeader()}</div>
419
+ <input ref={titleRef} value={title} onChange={handleTitleChange}
420
+ className="mb-4 w-full bg-transparent text-2xl font-semibold text-zinc-100 outline-none placeholder-zinc-700"
421
+ placeholder="Untitled" />
422
+
423
+ <div className="flex flex-1 gap-0">
424
+ {/* ── Left badge gutter ── */}
425
+ <div className="relative shrink-0 border-r border-zinc-800/60 mr-3 transition-[width]" style={{ width: gutterWidth }}>
426
+ {badgeRows.map(({ lineIndex, task }) => (
427
+ <Link key={lineIndex} href={`/tasks/${task.id}`} title={task.title}
428
+ className="absolute right-1 flex items-center justify-end opacity-80 hover:opacity-100 transition-opacity"
429
+ style={{ top: badgePositions[lineIndex] ?? lineIndex * 24, height: 24 }}>
430
+ <StatusDot status={task.status} />
431
+ </Link>
432
+ ))}
433
+ </div>
434
+
435
+ {/* ── Text editor (overlay + textarea) ── */}
436
+ <div className="relative flex-1">
437
+ <div ref={overlayRef} dir="ltr"
438
+ className="pointer-events-none absolute inset-0 z-10 whitespace-pre-wrap break-words font-mono text-sm leading-6 text-zinc-300 selection:bg-blue-500/30"
439
+ aria-hidden>
440
+ <EditorOverlay content={content} />
441
+ </div>
442
+
443
+ <textarea dir="ltr" ref={textareaRef} value={content}
444
+ onChange={handleContentChange}
445
+ onCompositionEnd={(e) => {
446
+ const ta = e.target as HTMLTextAreaElement;
447
+ p.savedCursor.current = { start: ta.selectionStart, end: ta.selectionEnd };
448
+ setContent(ta.value);
449
+ p.detectTriggers(ta.value, ta.selectionStart);
450
+ let newTitle = title;
451
+ if (!userEditedTitle.current) {
452
+ const extracted = extractTitleFromContent(ta.value);
453
+ if (extracted) { newTitle = extracted; setTitle(newTitle); }
454
+ }
455
+ scheduleSave(newTitle, ta.value);
456
+ }}
457
+ onKeyDown={handleKeyDown}
458
+ onPaste={() => pushHistoryImmediate(content)}
459
+ onFocus={() => { if (hasActiveTasks && note.isLocked) setShowLockWarning(true); }}
460
+ onDragOver={(e) => {
461
+ if (e.dataTransfer.types.includes("application/brief-note-title")) {
462
+ e.preventDefault(); e.dataTransfer.dropEffect = "copy";
463
+ }
464
+ }}
465
+ onDrop={(e) => {
466
+ const droppedTitle = e.dataTransfer.getData("application/brief-note-title");
467
+ if (!droppedTitle) return;
468
+ e.preventDefault();
469
+ const ta = textareaRef.current;
470
+ if (!ta) return;
471
+ let insertPos: number | null = null;
472
+ const d = document as unknown as {
473
+ caretPositionFromPoint?: (x: number, y: number) => { offset: number } | null;
474
+ caretRangeFromPoint?: (x: number, y: number) => { startOffset: number } | null;
475
+ };
476
+ const caret =
477
+ d.caretPositionFromPoint?.(e.clientX, e.clientY) ??
478
+ (d.caretRangeFromPoint
479
+ ? (() => { const r = d.caretRangeFromPoint!(e.clientX, e.clientY); return r ? { offset: r.startOffset } : null; })()
480
+ : null);
481
+ if (caret && typeof caret.offset === "number") {
482
+ insertPos = Math.min(caret.offset, content.length);
483
+ }
484
+ if (insertPos === null) {
485
+ const rect = ta.getBoundingClientRect();
486
+ const relY = e.clientY - rect.top;
487
+ const lineIndex = Math.max(0, Math.floor(relY / 24));
488
+ const lines = content.split("\n");
489
+ const targetLine = Math.min(lineIndex, lines.length - 1);
490
+ insertPos = lines.slice(0, targetLine).reduce((acc, l) => acc + l.length + 1, 0) + lines[targetLine].length;
491
+ }
492
+ const link = ` [[${droppedTitle}]]`;
493
+ const newContent = content.slice(0, insertPos) + link + content.slice(insertPos);
494
+ p.savedCursor.current = { start: insertPos + link.length, end: insertPos + link.length };
495
+ setContent(newContent);
496
+ scheduleSave(title, newContent);
497
+ }}
498
+ onContextMenu={handleContextMenu}
499
+ onBlur={() => {
500
+ setTimeout(() => { p.setMentionQuery(null); p.setSlashQuery(null); p.setNoteQuery(null); p.setTaskQuery(null); p.setPriorityQuery(null); p.setPlusOpen(false); p.setTemplateOpen(false); p.setDropdownPos(null); }, 150);
501
+ setTimeout(() => { p.setSoftHint(null); p.setContextMenu(null); }, 300);
502
+ }}
503
+ className="relative z-20 min-h-[400px] w-full overflow-hidden resize-none bg-transparent font-mono text-sm leading-6 text-transparent outline-none placeholder-zinc-700 caret-zinc-300"
504
+ placeholder={`Write a note…\n\nTip: / for commands · + to insert a link · @name to mention · - [ ] for tasks`}
505
+ spellCheck={false} />
506
+
507
+ {/* ── Dropdowns ── */}
508
+ {/* Desktop pos (undefined on mobile — bottom sheet handles layout) */}
509
+ {p.softHint && p.mentionQuery === null && p.slashQuery === null && (
510
+ <SoftHintBar handle={p.softHint.handle} onAccept={p.applySoftHint} onDismiss={() => p.setSoftHint(null)} />
511
+ )}
512
+
513
+ <PickerShell
514
+ open={p.slashQuery !== null && (isMobile || !!p.dropdownPos)}
515
+ onClose={() => { p.setSlashQuery(null); p.setDropdownPos(null); }}
516
+ title="Commands"
517
+ isMobile={isMobile}
518
+ >
519
+ {p.slashSuggestions.length > 0 ? (
520
+ <SlashPalette
521
+ suggestions={p.slashSuggestions}
522
+ activeIndex={p.slashIndex}
523
+ pos={isMobile ? undefined : p.dropdownPos!}
524
+ onPick={p.executeSlashCommand}
525
+ />
526
+ ) : (
527
+ <div className="rounded-lg border border-zinc-800 bg-zinc-900 px-3 py-2 text-xs text-zinc-500">
528
+ No commands match <span className="text-zinc-300">/{p.slashQuery}</span>. Try `/date`, `/table`, or `/h1`.
529
+ </div>
530
+ )}
531
+ </PickerShell>
532
+
533
+ <PickerShell
534
+ open={p.mentionQuery !== null && p.mentionSuggestions.length > 0 && (isMobile || !!p.dropdownPos)}
535
+ onClose={() => { p.setMentionQuery(null); p.setDropdownPos(null); }}
536
+ title="Mention"
537
+ isMobile={isMobile}
538
+ >
539
+ <MentionDropdown
540
+ suggestions={p.mentionSuggestions}
541
+ activeIndex={p.mentionIndex}
542
+ pos={isMobile ? undefined : p.dropdownPos!}
543
+ onPick={p.insertMention}
544
+ />
545
+ </PickerShell>
546
+
547
+ <PickerShell
548
+ open={isMobile ? !!p.datePickerPos : !!p.datePickerPos}
549
+ onClose={() => p.setDatePickerPos(null)}
550
+ title="Insert date"
551
+ isMobile={isMobile}
552
+ >
553
+ <DatePickerDropdown
554
+ pos={isMobile ? undefined : p.datePickerPos!}
555
+ startDate={p.datePickerStart}
556
+ endDate={p.datePickerEnd}
557
+ onStartChange={p.setDatePickerStart}
558
+ onEndChange={p.setDatePickerEnd}
559
+ onConfirm={p.confirmDatePicker}
560
+ onCancel={() => p.setDatePickerPos(null)}
561
+ />
562
+ </PickerShell>
563
+
564
+ <PickerShell
565
+ open={p.plusOpen && (isMobile || !!p.dropdownPos)}
566
+ onClose={() => { p.setPlusOpen(false); p.setDropdownPos(null); }}
567
+ title="Insert"
568
+ isMobile={isMobile}
569
+ >
570
+ <PlusPickerDropdown
571
+ pos={isMobile ? undefined : p.dropdownPos!}
572
+ onPick={p.triggerPlusOption}
573
+ />
574
+ </PickerShell>
575
+
576
+ <PickerShell
577
+ open={p.taskQuery !== null && (isMobile || !!p.dropdownPos)}
578
+ onClose={() => { p.setTaskQuery(null); p.setDropdownPos(null); }}
579
+ title="Link task"
580
+ isMobile={isMobile}
581
+ >
582
+ <TaskPickerDropdown
583
+ suggestions={p.taskSuggestions}
584
+ activeIndex={p.taskIndex}
585
+ pos={isMobile ? undefined : p.dropdownPos!}
586
+ onPick={p.insertTaskRef}
587
+ />
588
+ </PickerShell>
589
+
590
+ <PickerShell
591
+ open={p.priorityQuery !== null && p.prioritySuggestions.length > 0 && (isMobile || !!p.dropdownPos)}
592
+ onClose={() => { p.setPriorityQuery(null); p.setDropdownPos(null); }}
593
+ title="Set priority"
594
+ isMobile={isMobile}
595
+ >
596
+ <PriorityPickerDropdown
597
+ suggestions={p.prioritySuggestions}
598
+ activeIndex={p.priorityIndex}
599
+ pos={isMobile ? undefined : p.dropdownPos!}
600
+ onPick={p.insertPriority}
601
+ />
602
+ </PickerShell>
603
+
604
+ <PickerShell
605
+ open={p.noteQuery !== null && (isMobile || !!p.dropdownPos)}
606
+ onClose={() => { p.setNoteQuery(null); p.setDropdownPos(null); }}
607
+ title="Link note"
608
+ isMobile={isMobile}
609
+ >
610
+ <NotePickerDropdown
611
+ suggestions={p.noteSuggestions}
612
+ activeIndex={p.noteIndex}
613
+ pos={isMobile ? undefined : p.dropdownPos!}
614
+ onPick={p.insertNoteLink}
615
+ />
616
+ </PickerShell>
617
+
618
+ <PickerShell
619
+ open={p.templateOpen && (isMobile || !!p.dropdownPos)}
620
+ onClose={() => { p.setTemplateOpen(false); p.setDropdownPos(null); }}
621
+ title="Templates"
622
+ isMobile={isMobile}
623
+ >
624
+ <TemplatePickerDropdown
625
+ pos={isMobile ? undefined : p.dropdownPos!}
626
+ onPick={p.insertTemplate}
627
+ />
628
+ </PickerShell>
629
+ </div>
630
+ </div>
631
+ </div>
632
+ )}
633
+
634
+ {/* ── Preview pane ── */}
635
+ {showPreviewPane && (
636
+ <div
637
+ className={
638
+ effectiveViewMode === "preview"
639
+ ? "flex-1 min-w-0 [&>div]:w-full"
640
+ : "basis-[44%] min-w-0"
641
+ }
642
+ >
643
+ <PreviewPane title={title} content={content} notesList={notesList} tasksList={tasksList} onToggleTask={toggleTaskInPreview} />
644
+ </div>
645
+ )}
646
+ </div>
647
+
648
+ {/* Right-click context menu */}
649
+ {p.contextMenu && (
650
+ <EditorContextMenu
651
+ pos={{ top: p.contextMenu.top, left: p.contextMenu.left }}
652
+ menuRef={p.contextMenuRef}
653
+ onBold={() => p.applyContextFormat("bold")}
654
+ onItalic={() => p.applyContextFormat("italic")}
655
+ onStrike={() => p.applyContextFormat("strike")}
656
+ onMakeTask={p.convertSelectionToTask}
657
+ onCopyLink={copyDeepLink}
658
+ />
659
+ )}
660
+ </div>
661
+ );
662
+ }