@knotpad/app 0.1.0 → 0.1.2

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 (341) hide show
  1. package/bin/brief.js +165 -78
  2. package/package.json +6 -86
  3. package/app/(app)/calendar/page.tsx +0 -57
  4. package/app/(app)/error.tsx +0 -35
  5. package/app/(app)/graph/page.tsx +0 -32
  6. package/app/(app)/guide/page.tsx +0 -21
  7. package/app/(app)/kanban/loading.tsx +0 -24
  8. package/app/(app)/kanban/page.tsx +0 -59
  9. package/app/(app)/layout.tsx +0 -122
  10. package/app/(app)/list/loading.tsx +0 -21
  11. package/app/(app)/list/page.tsx +0 -137
  12. package/app/(app)/loading.tsx +0 -18
  13. package/app/(app)/notes/[noteId]/page.tsx +0 -84
  14. package/app/(app)/notes/layout.tsx +0 -30
  15. package/app/(app)/notes/page.tsx +0 -39
  16. package/app/(app)/page.tsx +0 -5
  17. package/app/(app)/settings/agent-token/page.tsx +0 -59
  18. package/app/(app)/settings/backup/page.tsx +0 -49
  19. package/app/(app)/settings/billing/page.tsx +0 -53
  20. package/app/(app)/settings/calendar/page.tsx +0 -41
  21. package/app/(app)/settings/layout.test.tsx +0 -39
  22. package/app/(app)/settings/layout.tsx +0 -71
  23. package/app/(app)/settings/page.tsx +0 -4
  24. package/app/(app)/settings/security/page.tsx +0 -43
  25. package/app/(app)/settings/team/page.tsx +0 -74
  26. package/app/(app)/settings/workspace/page.tsx +0 -27
  27. package/app/(app)/tasks/[taskId]/page.tsx +0 -79
  28. package/app/(auth)/forgot-password/page.tsx +0 -106
  29. package/app/(auth)/guest/page.tsx +0 -56
  30. package/app/(auth)/layout.tsx +0 -13
  31. package/app/(auth)/login/page.tsx +0 -14
  32. package/app/(auth)/register/page.tsx +0 -193
  33. package/app/(auth)/reset-password/page.tsx +0 -138
  34. package/app/api/account/claim/route.tsx +0 -135
  35. package/app/api/admin/backfill-encryption/route.tsx +0 -43
  36. package/app/api/admin/license/route.tsx +0 -42
  37. package/app/api/auth/2fa/route.tsx +0 -148
  38. package/app/api/auth/[...nextauth]/route.tsx +0 -3
  39. package/app/api/auth/change-password/route.tsx +0 -61
  40. package/app/api/auth/check-2fa/route.tsx +0 -19
  41. package/app/api/auth/forgot-password/route.tsx +0 -65
  42. package/app/api/auth/reset-password/route.tsx +0 -52
  43. package/app/api/auth/verify-2fa/route.tsx +0 -88
  44. package/app/api/backup/download/db/route.ts +0 -29
  45. package/app/api/backup/download/notes/route.ts +0 -25
  46. package/app/api/backup/settings/route.ts +0 -92
  47. package/app/api/billing/checkout/route.tsx +0 -81
  48. package/app/api/billing/migrate/route.tsx +0 -163
  49. package/app/api/billing/portal/route.tsx +0 -24
  50. package/app/api/billing/setup-intent/route.tsx +0 -55
  51. package/app/api/billing/status/route.tsx +0 -36
  52. package/app/api/billing/subscribe/route.tsx +0 -85
  53. package/app/api/billing/webhook/route.tsx +0 -199
  54. package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
  55. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
  56. package/app/api/calendar-feeds/events/route.tsx +0 -82
  57. package/app/api/calendar-feeds/route.tsx +0 -52
  58. package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
  59. package/app/api/cron/calendar-feeds/route.tsx +0 -31
  60. package/app/api/cron/stale-tasks/route.tsx +0 -51
  61. package/app/api/cron/sync/route.tsx +0 -34
  62. package/app/api/devices/[deviceId]/route.tsx +0 -25
  63. package/app/api/devices/route.tsx +0 -41
  64. package/app/api/export/route.tsx +0 -40
  65. package/app/api/feedback/route.tsx +0 -54
  66. package/app/api/folders/[folderId]/route.tsx +0 -51
  67. package/app/api/folders/route.tsx +0 -37
  68. package/app/api/graph/route.tsx +0 -242
  69. package/app/api/guest/route.tsx +0 -58
  70. package/app/api/health/route.tsx +0 -10
  71. package/app/api/holidays/countries/route.tsx +0 -14
  72. package/app/api/holidays/route.tsx +0 -49
  73. package/app/api/holidays/states/route.tsx +0 -21
  74. package/app/api/invites/[token]/route.tsx +0 -131
  75. package/app/api/invites/route.tsx +0 -74
  76. package/app/api/mcp/generate-token/route.tsx +0 -55
  77. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
  78. package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
  79. package/app/api/notes/[noteId]/export/route.tsx +0 -45
  80. package/app/api/notes/[noteId]/route.tsx +0 -360
  81. package/app/api/notes/route.tsx +0 -112
  82. package/app/api/notifications/route.tsx +0 -44
  83. package/app/api/register/route.tsx +0 -67
  84. package/app/api/restore/route.tsx +0 -148
  85. package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
  86. package/app/api/sync/conflicts/route.tsx +0 -48
  87. package/app/api/sync/status/route.tsx +0 -49
  88. package/app/api/sync/trigger/route.tsx +0 -15
  89. package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
  90. package/app/api/tasks/[taskId]/route.tsx +0 -259
  91. package/app/api/tasks/bulk/route.tsx +0 -133
  92. package/app/api/tasks/route.tsx +0 -36
  93. package/app/api/workspace/active/route.tsx +0 -39
  94. package/app/api/workspace/create-team/route.tsx +0 -42
  95. package/app/api/workspace/kanban-statuses/route.tsx +0 -71
  96. package/app/api/workspace/members/[memberId]/route.tsx +0 -69
  97. package/app/api/workspace/route.tsx +0 -24
  98. package/app/download/page.tsx +0 -170
  99. package/app/favicon.ico +0 -0
  100. package/app/generated/prisma/client.d.ts +0 -1
  101. package/app/generated/prisma/client.js +0 -5
  102. package/app/generated/prisma/default.d.ts +0 -1
  103. package/app/generated/prisma/default.js +0 -5
  104. package/app/generated/prisma/edge.d.ts +0 -1
  105. package/app/generated/prisma/edge.js +0 -497
  106. package/app/generated/prisma/index-browser.js +0 -523
  107. package/app/generated/prisma/index.d.ts +0 -46376
  108. package/app/generated/prisma/index.js +0 -497
  109. package/app/generated/prisma/package.json +0 -144
  110. package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  112. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  113. package/app/generated/prisma/runtime/client.d.ts +0 -3386
  114. package/app/generated/prisma/runtime/client.js +0 -86
  115. package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
  116. package/app/generated/prisma/runtime/index-browser.js +0 -6
  117. package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  118. package/app/generated/prisma/schema.prisma +0 -456
  119. package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  120. package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
  121. package/app/globals.css +0 -54
  122. package/app/invite/[token]/page.tsx +0 -52
  123. package/app/layout.tsx +0 -90
  124. package/app/mcp/route.tsx +0 -430
  125. package/app/opengraph-image.tsx +0 -120
  126. package/app/page.tsx +0 -398
  127. package/app/privacy/page.tsx +0 -69
  128. package/app/robots.tsx +0 -25
  129. package/app/sitemap.tsx +0 -36
  130. package/app/terms/page.tsx +0 -69
  131. package/app/upgrade/page.tsx +0 -75
  132. package/auth.config.ts +0 -33
  133. package/auth.ts +0 -79
  134. package/components/auth/login-form.tsx +0 -302
  135. package/components/auth/password-checklist.tsx +0 -31
  136. package/components/auth/password-input.tsx +0 -36
  137. package/components/auth/switch-account-button.test.tsx +0 -22
  138. package/components/auth/switch-account-button.tsx +0 -19
  139. package/components/auth/two-factor-input.tsx +0 -116
  140. package/components/billing/billing-dashboard.tsx +0 -265
  141. package/components/billing/card-form.tsx +0 -210
  142. package/components/billing/claim-account-form.tsx +0 -99
  143. package/components/branding/app-logo.test.tsx +0 -20
  144. package/components/branding/app-logo.tsx +0 -25
  145. package/components/calendar/calendar-agenda.tsx +0 -150
  146. package/components/calendar/calendar-drag.test.tsx +0 -177
  147. package/components/calendar/calendar-grid.tsx +0 -357
  148. package/components/calendar/calendar-hooks.test.tsx +0 -27
  149. package/components/calendar/calendar-hooks.ts +0 -351
  150. package/components/calendar/calendar-toolbar.test.tsx +0 -68
  151. package/components/calendar/calendar-toolbar.tsx +0 -291
  152. package/components/calendar/calendar-types.ts +0 -148
  153. package/components/calendar/calendar-view.test.tsx +0 -295
  154. package/components/calendar/calendar-view.tsx +0 -307
  155. package/components/calendar/day-detail-popover.tsx +0 -174
  156. package/components/calendar/task-chip.tsx +0 -86
  157. package/components/command/command-palette.test.tsx +0 -33
  158. package/components/command/command-palette.tsx +0 -310
  159. package/components/download-cta.tsx +0 -87
  160. package/components/feedback/feedback-popup.tsx +0 -207
  161. package/components/graph/graph-draw.ts +0 -337
  162. package/components/graph/graph-overlays.tsx +0 -160
  163. package/components/graph/graph-page.test.tsx +0 -131
  164. package/components/graph/graph-page.tsx +0 -263
  165. package/components/graph/graph-types.ts +0 -47
  166. package/components/graph/graph-view.tsx +0 -322
  167. package/components/guide/guide-view.tsx +0 -522
  168. package/components/kanban/kanban-board.test.tsx +0 -128
  169. package/components/kanban/kanban-board.tsx +0 -361
  170. package/components/kanban/kanban-card-menu.tsx +0 -102
  171. package/components/kanban/kanban-card.tsx +0 -227
  172. package/components/kanban/kanban-column.tsx +0 -49
  173. package/components/kanban/kanban-status-context.tsx +0 -28
  174. package/components/landing/calendar-sandbox.test.tsx +0 -15
  175. package/components/landing/calendar-sandbox.tsx +0 -107
  176. package/components/landing/graph-sandbox.test.tsx +0 -27
  177. package/components/landing/graph-sandbox.tsx +0 -80
  178. package/components/landing/kanban-sandbox.test.tsx +0 -24
  179. package/components/landing/kanban-sandbox.tsx +0 -101
  180. package/components/landing/landing-showcase.test.tsx +0 -21
  181. package/components/landing/landing-showcase.tsx +0 -54
  182. package/components/landing/list-sandbox.tsx +0 -86
  183. package/components/landing/mock-workspace.ts +0 -168
  184. package/components/landing/notes-sandbox.test.tsx +0 -14
  185. package/components/landing/notes-sandbox.tsx +0 -88
  186. package/components/layout/app-shell.tsx +0 -83
  187. package/components/layout/backup-scheduler.tsx +0 -122
  188. package/components/layout/bottom-nav.tsx +0 -43
  189. package/components/layout/icon-bar.test.tsx +0 -29
  190. package/components/layout/icon-bar.tsx +0 -118
  191. package/components/layout/mobile-top-bar.tsx +0 -68
  192. package/components/layout/notes-panel-folder.tsx +0 -127
  193. package/components/layout/notes-panel-note-item.tsx +0 -140
  194. package/components/layout/notes-panel-task-tab.tsx +0 -63
  195. package/components/layout/notes-panel-types.ts +0 -44
  196. package/components/layout/notes-panel.tsx +0 -476
  197. package/components/layout/notification-bell.tsx +0 -251
  198. package/components/layout/paywall-screen.tsx +0 -41
  199. package/components/layout/pro-banner.tsx +0 -76
  200. package/components/layout/sw-register.tsx +0 -27
  201. package/components/layout/workspace-switcher.tsx +0 -90
  202. package/components/notes/mobile-bottom-sheet.tsx +0 -99
  203. package/components/notes/note-editor-context-menu.tsx +0 -47
  204. package/components/notes/note-editor-dom.ts +0 -33
  205. package/components/notes/note-editor-dropdowns.tsx +0 -484
  206. package/components/notes/note-editor-hooks.ts +0 -692
  207. package/components/notes/note-editor-keyboard.ts +0 -305
  208. package/components/notes/note-editor-overlay.tsx +0 -90
  209. package/components/notes/note-editor.test.tsx +0 -372
  210. package/components/notes/note-editor.tsx +0 -662
  211. package/components/notes/note-preview-pane.tsx +0 -156
  212. package/components/notes/note-tabs.tsx +0 -120
  213. package/components/notes/note-types.tsx +0 -157
  214. package/components/settings/accept-invite.tsx +0 -108
  215. package/components/settings/agent-token-settings.tsx +0 -369
  216. package/components/settings/backup-restore-settings.test.tsx +0 -25
  217. package/components/settings/backup-restore-settings.tsx +0 -327
  218. package/components/settings/calendar-feeds-settings.tsx +0 -489
  219. package/components/settings/calendar-general-settings.tsx +0 -174
  220. package/components/settings/confirm-danger-action.test.tsx +0 -215
  221. package/components/settings/confirm-danger-action.tsx +0 -65
  222. package/components/settings/security-settings.tsx +0 -252
  223. package/components/settings/settings-guidance.test.tsx +0 -98
  224. package/components/settings/team-settings.tsx +0 -319
  225. package/components/settings/two-factor-auth.tsx +0 -296
  226. package/components/settings/workspace-settings-client.tsx +0 -363
  227. package/components/settings/workspace-settings-form.tsx +0 -73
  228. package/components/sync/conflict-viewer.tsx +0 -247
  229. package/components/sync/sync-indicator.tsx +0 -171
  230. package/components/tasks/snippet-thread.tsx +0 -119
  231. package/components/tasks/status-dot.tsx +0 -47
  232. package/components/tasks/task-badge.tsx +0 -43
  233. package/components/tasks/task-detail.test.tsx +0 -187
  234. package/components/tasks/task-detail.tsx +0 -458
  235. package/components/tasks/task-list-filters.test.tsx +0 -75
  236. package/components/tasks/task-list-filters.tsx +0 -163
  237. package/components/tasks/task-list-types.ts +0 -20
  238. package/components/tasks/task-list.test.tsx +0 -175
  239. package/components/tasks/task-list.tsx +0 -481
  240. package/components/tasks/task-row.tsx +0 -85
  241. package/components/tasks/task-table-row.tsx +0 -259
  242. package/components/ui/skeleton.tsx +0 -3
  243. package/components/ui/toast.test.tsx +0 -42
  244. package/components/ui/toast.tsx +0 -70
  245. package/electron/main.ts +0 -251
  246. package/electron/preload.ts +0 -56
  247. package/instrumentation.tsx +0 -23
  248. package/lib/api-error.ts +0 -50
  249. package/lib/backup/backup-runner.test.ts +0 -32
  250. package/lib/backup/backup-runner.ts +0 -19
  251. package/lib/backup/backup-schedule.test.ts +0 -23
  252. package/lib/backup/backup-schedule.ts +0 -55
  253. package/lib/backup/backup-settings.test.ts +0 -30
  254. package/lib/backup/backup-settings.ts +0 -27
  255. package/lib/backup/export-notes-zip.test.ts +0 -26
  256. package/lib/backup/export-notes-zip.ts +0 -82
  257. package/lib/backup/export-workspace-backup.test.ts +0 -17
  258. package/lib/backup/export-workspace-backup.ts +0 -77
  259. package/lib/backup/restore-workspace-from-export.test.ts +0 -18
  260. package/lib/backup/restore-workspace-from-export.ts +0 -183
  261. package/lib/backup/types.ts +0 -14
  262. package/lib/brand-icons.ts +0 -1
  263. package/lib/calendar-feed-crypto.ts +0 -38
  264. package/lib/calendar-feed.ts +0 -239
  265. package/lib/client/online-status.ts +0 -47
  266. package/lib/conflict-resolver.test.ts +0 -57
  267. package/lib/conflict-resolver.ts +0 -240
  268. package/lib/db-init.ts +0 -79
  269. package/lib/email.ts +0 -159
  270. package/lib/encryption.test.ts +0 -41
  271. package/lib/encryption.ts +0 -98
  272. package/lib/extract-snippet.test.ts +0 -123
  273. package/lib/extract-snippet.ts +0 -69
  274. package/lib/kanban-status.ts +0 -55
  275. package/lib/license.ts +0 -21
  276. package/lib/limits.ts +0 -31
  277. package/lib/mcp-auth.test.ts +0 -58
  278. package/lib/mcp-auth.ts +0 -65
  279. package/lib/mcp-contract.test.ts +0 -25
  280. package/lib/mcp-contract.ts +0 -210
  281. package/lib/mcp-handler.ts +0 -31
  282. package/lib/mcp-url.test.ts +0 -12
  283. package/lib/mcp-url.ts +0 -7
  284. package/lib/mentions.test.ts +0 -45
  285. package/lib/mentions.ts +0 -73
  286. package/lib/note-crypto.ts +0 -108
  287. package/lib/note-sync.ts +0 -201
  288. package/lib/note-title.ts +0 -93
  289. package/lib/prisma.ts +0 -193
  290. package/lib/pro-flush.ts +0 -292
  291. package/lib/rate-limit.ts +0 -57
  292. package/lib/stripe.ts +0 -38
  293. package/lib/sync-worker.ts +0 -388
  294. package/lib/task-parser.test.ts +0 -91
  295. package/lib/task-parser.ts +0 -81
  296. package/lib/task-utils.ts +0 -52
  297. package/lib/use-is-electron.ts +0 -19
  298. package/lib/use-is-mobile.ts +0 -22
  299. package/lib/validation/calendar-feed.ts +0 -31
  300. package/lib/validation/note.ts +0 -27
  301. package/lib/validation/task.ts +0 -26
  302. package/lib/view-preferences.test.ts +0 -54
  303. package/lib/view-preferences.ts +0 -28
  304. package/lib/workspace.ts +0 -66
  305. package/next.config.ts +0 -21
  306. package/postcss.config.mjs +0 -7
  307. package/prisma/migrations/20260519021916_init/migration.sql +0 -388
  308. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
  309. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
  310. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
  311. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
  312. package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
  313. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
  314. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
  315. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
  316. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
  317. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
  318. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
  319. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
  320. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
  321. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
  322. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
  323. package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
  324. package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
  325. package/prisma/migrations/migration_lock.toml +0 -3
  326. package/prisma/schema.prisma +0 -457
  327. package/public/Logo_icon.svg +0 -1
  328. package/public/file.svg +0 -1
  329. package/public/globe.svg +0 -1
  330. package/public/icon-192.png +0 -0
  331. package/public/icon-512.png +0 -0
  332. package/public/icon.svg +0 -4
  333. package/public/icon_dark.svg +0 -1
  334. package/public/knotpad_icon.svg +0 -1
  335. package/public/knotpad_logo_full.svg +0 -1
  336. package/public/manifest.json +0 -14
  337. package/public/next.svg +0 -1
  338. package/public/sw.js +0 -137
  339. package/public/vercel.svg +0 -1
  340. package/public/window.svg +0 -1
  341. package/tsconfig.json +0 -35
@@ -1,662 +0,0 @@
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
- }