@knotpad/app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +167 -0
  2. package/app/(app)/calendar/page.tsx +57 -0
  3. package/app/(app)/error.tsx +35 -0
  4. package/app/(app)/graph/page.tsx +32 -0
  5. package/app/(app)/guide/page.tsx +21 -0
  6. package/app/(app)/kanban/loading.tsx +24 -0
  7. package/app/(app)/kanban/page.tsx +59 -0
  8. package/app/(app)/layout.tsx +122 -0
  9. package/app/(app)/list/loading.tsx +21 -0
  10. package/app/(app)/list/page.tsx +137 -0
  11. package/app/(app)/loading.tsx +18 -0
  12. package/app/(app)/notes/[noteId]/page.tsx +84 -0
  13. package/app/(app)/notes/layout.tsx +30 -0
  14. package/app/(app)/notes/page.tsx +39 -0
  15. package/app/(app)/page.tsx +5 -0
  16. package/app/(app)/settings/agent-token/page.tsx +59 -0
  17. package/app/(app)/settings/backup/page.tsx +49 -0
  18. package/app/(app)/settings/billing/page.tsx +53 -0
  19. package/app/(app)/settings/calendar/page.tsx +41 -0
  20. package/app/(app)/settings/layout.test.tsx +39 -0
  21. package/app/(app)/settings/layout.tsx +71 -0
  22. package/app/(app)/settings/page.tsx +4 -0
  23. package/app/(app)/settings/security/page.tsx +43 -0
  24. package/app/(app)/settings/team/page.tsx +74 -0
  25. package/app/(app)/settings/workspace/page.tsx +27 -0
  26. package/app/(app)/tasks/[taskId]/page.tsx +79 -0
  27. package/app/(auth)/forgot-password/page.tsx +106 -0
  28. package/app/(auth)/guest/page.tsx +56 -0
  29. package/app/(auth)/layout.tsx +13 -0
  30. package/app/(auth)/login/page.tsx +14 -0
  31. package/app/(auth)/register/page.tsx +193 -0
  32. package/app/(auth)/reset-password/page.tsx +138 -0
  33. package/app/api/account/claim/route.tsx +135 -0
  34. package/app/api/admin/backfill-encryption/route.tsx +43 -0
  35. package/app/api/admin/license/route.tsx +42 -0
  36. package/app/api/auth/2fa/route.tsx +148 -0
  37. package/app/api/auth/[...nextauth]/route.tsx +3 -0
  38. package/app/api/auth/change-password/route.tsx +61 -0
  39. package/app/api/auth/check-2fa/route.tsx +19 -0
  40. package/app/api/auth/forgot-password/route.tsx +65 -0
  41. package/app/api/auth/reset-password/route.tsx +52 -0
  42. package/app/api/auth/verify-2fa/route.tsx +88 -0
  43. package/app/api/backup/download/db/route.ts +29 -0
  44. package/app/api/backup/download/notes/route.ts +25 -0
  45. package/app/api/backup/settings/route.ts +92 -0
  46. package/app/api/billing/checkout/route.tsx +81 -0
  47. package/app/api/billing/migrate/route.tsx +163 -0
  48. package/app/api/billing/portal/route.tsx +24 -0
  49. package/app/api/billing/setup-intent/route.tsx +55 -0
  50. package/app/api/billing/status/route.tsx +36 -0
  51. package/app/api/billing/subscribe/route.tsx +85 -0
  52. package/app/api/billing/webhook/route.tsx +199 -0
  53. package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
  54. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
  55. package/app/api/calendar-feeds/events/route.tsx +82 -0
  56. package/app/api/calendar-feeds/route.tsx +52 -0
  57. package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
  58. package/app/api/cron/calendar-feeds/route.tsx +31 -0
  59. package/app/api/cron/stale-tasks/route.tsx +51 -0
  60. package/app/api/cron/sync/route.tsx +34 -0
  61. package/app/api/devices/[deviceId]/route.tsx +25 -0
  62. package/app/api/devices/route.tsx +41 -0
  63. package/app/api/export/route.tsx +40 -0
  64. package/app/api/feedback/route.tsx +54 -0
  65. package/app/api/folders/[folderId]/route.tsx +51 -0
  66. package/app/api/folders/route.tsx +37 -0
  67. package/app/api/graph/route.tsx +242 -0
  68. package/app/api/guest/route.tsx +58 -0
  69. package/app/api/health/route.tsx +10 -0
  70. package/app/api/holidays/countries/route.tsx +14 -0
  71. package/app/api/holidays/route.tsx +49 -0
  72. package/app/api/holidays/states/route.tsx +21 -0
  73. package/app/api/invites/[token]/route.tsx +131 -0
  74. package/app/api/invites/route.tsx +74 -0
  75. package/app/api/mcp/generate-token/route.tsx +55 -0
  76. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
  77. package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
  78. package/app/api/notes/[noteId]/export/route.tsx +45 -0
  79. package/app/api/notes/[noteId]/route.tsx +360 -0
  80. package/app/api/notes/route.tsx +112 -0
  81. package/app/api/notifications/route.tsx +44 -0
  82. package/app/api/register/route.tsx +67 -0
  83. package/app/api/restore/route.tsx +148 -0
  84. package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
  85. package/app/api/sync/conflicts/route.tsx +48 -0
  86. package/app/api/sync/status/route.tsx +49 -0
  87. package/app/api/sync/trigger/route.tsx +15 -0
  88. package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
  89. package/app/api/tasks/[taskId]/route.tsx +259 -0
  90. package/app/api/tasks/bulk/route.tsx +133 -0
  91. package/app/api/tasks/route.tsx +36 -0
  92. package/app/api/workspace/active/route.tsx +39 -0
  93. package/app/api/workspace/create-team/route.tsx +42 -0
  94. package/app/api/workspace/kanban-statuses/route.tsx +71 -0
  95. package/app/api/workspace/members/[memberId]/route.tsx +69 -0
  96. package/app/api/workspace/route.tsx +24 -0
  97. package/app/download/page.tsx +170 -0
  98. package/app/favicon.ico +0 -0
  99. package/app/generated/prisma/client.d.ts +1 -0
  100. package/app/generated/prisma/client.js +5 -0
  101. package/app/generated/prisma/default.d.ts +1 -0
  102. package/app/generated/prisma/default.js +5 -0
  103. package/app/generated/prisma/edge.d.ts +1 -0
  104. package/app/generated/prisma/edge.js +497 -0
  105. package/app/generated/prisma/index-browser.js +523 -0
  106. package/app/generated/prisma/index.d.ts +46376 -0
  107. package/app/generated/prisma/index.js +497 -0
  108. package/app/generated/prisma/package.json +144 -0
  109. package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
  110. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
  112. package/app/generated/prisma/runtime/client.d.ts +3386 -0
  113. package/app/generated/prisma/runtime/client.js +86 -0
  114. package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
  115. package/app/generated/prisma/runtime/index-browser.js +6 -0
  116. package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
  117. package/app/generated/prisma/schema.prisma +456 -0
  118. package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
  119. package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
  120. package/app/globals.css +54 -0
  121. package/app/invite/[token]/page.tsx +52 -0
  122. package/app/layout.tsx +90 -0
  123. package/app/mcp/route.tsx +430 -0
  124. package/app/opengraph-image.tsx +120 -0
  125. package/app/page.tsx +398 -0
  126. package/app/privacy/page.tsx +69 -0
  127. package/app/robots.tsx +25 -0
  128. package/app/sitemap.tsx +36 -0
  129. package/app/terms/page.tsx +69 -0
  130. package/app/upgrade/page.tsx +75 -0
  131. package/auth.config.ts +33 -0
  132. package/auth.ts +79 -0
  133. package/bin/brief.js +224 -0
  134. package/components/auth/login-form.tsx +302 -0
  135. package/components/auth/password-checklist.tsx +31 -0
  136. package/components/auth/password-input.tsx +36 -0
  137. package/components/auth/switch-account-button.test.tsx +22 -0
  138. package/components/auth/switch-account-button.tsx +19 -0
  139. package/components/auth/two-factor-input.tsx +116 -0
  140. package/components/billing/billing-dashboard.tsx +265 -0
  141. package/components/billing/card-form.tsx +210 -0
  142. package/components/billing/claim-account-form.tsx +99 -0
  143. package/components/branding/app-logo.test.tsx +20 -0
  144. package/components/branding/app-logo.tsx +25 -0
  145. package/components/calendar/calendar-agenda.tsx +150 -0
  146. package/components/calendar/calendar-drag.test.tsx +177 -0
  147. package/components/calendar/calendar-grid.tsx +357 -0
  148. package/components/calendar/calendar-hooks.test.tsx +27 -0
  149. package/components/calendar/calendar-hooks.ts +351 -0
  150. package/components/calendar/calendar-toolbar.test.tsx +68 -0
  151. package/components/calendar/calendar-toolbar.tsx +291 -0
  152. package/components/calendar/calendar-types.ts +148 -0
  153. package/components/calendar/calendar-view.test.tsx +295 -0
  154. package/components/calendar/calendar-view.tsx +307 -0
  155. package/components/calendar/day-detail-popover.tsx +174 -0
  156. package/components/calendar/task-chip.tsx +86 -0
  157. package/components/command/command-palette.test.tsx +33 -0
  158. package/components/command/command-palette.tsx +310 -0
  159. package/components/download-cta.tsx +87 -0
  160. package/components/feedback/feedback-popup.tsx +207 -0
  161. package/components/graph/graph-draw.ts +337 -0
  162. package/components/graph/graph-overlays.tsx +160 -0
  163. package/components/graph/graph-page.test.tsx +131 -0
  164. package/components/graph/graph-page.tsx +263 -0
  165. package/components/graph/graph-types.ts +47 -0
  166. package/components/graph/graph-view.tsx +322 -0
  167. package/components/guide/guide-view.tsx +522 -0
  168. package/components/kanban/kanban-board.test.tsx +128 -0
  169. package/components/kanban/kanban-board.tsx +361 -0
  170. package/components/kanban/kanban-card-menu.tsx +102 -0
  171. package/components/kanban/kanban-card.tsx +227 -0
  172. package/components/kanban/kanban-column.tsx +49 -0
  173. package/components/kanban/kanban-status-context.tsx +28 -0
  174. package/components/landing/calendar-sandbox.test.tsx +15 -0
  175. package/components/landing/calendar-sandbox.tsx +107 -0
  176. package/components/landing/graph-sandbox.test.tsx +27 -0
  177. package/components/landing/graph-sandbox.tsx +80 -0
  178. package/components/landing/kanban-sandbox.test.tsx +24 -0
  179. package/components/landing/kanban-sandbox.tsx +101 -0
  180. package/components/landing/landing-showcase.test.tsx +21 -0
  181. package/components/landing/landing-showcase.tsx +54 -0
  182. package/components/landing/list-sandbox.tsx +86 -0
  183. package/components/landing/mock-workspace.ts +168 -0
  184. package/components/landing/notes-sandbox.test.tsx +14 -0
  185. package/components/landing/notes-sandbox.tsx +88 -0
  186. package/components/layout/app-shell.tsx +83 -0
  187. package/components/layout/backup-scheduler.tsx +122 -0
  188. package/components/layout/bottom-nav.tsx +43 -0
  189. package/components/layout/icon-bar.test.tsx +29 -0
  190. package/components/layout/icon-bar.tsx +118 -0
  191. package/components/layout/mobile-top-bar.tsx +68 -0
  192. package/components/layout/notes-panel-folder.tsx +127 -0
  193. package/components/layout/notes-panel-note-item.tsx +140 -0
  194. package/components/layout/notes-panel-task-tab.tsx +63 -0
  195. package/components/layout/notes-panel-types.ts +44 -0
  196. package/components/layout/notes-panel.tsx +476 -0
  197. package/components/layout/notification-bell.tsx +251 -0
  198. package/components/layout/paywall-screen.tsx +41 -0
  199. package/components/layout/pro-banner.tsx +76 -0
  200. package/components/layout/sw-register.tsx +27 -0
  201. package/components/layout/workspace-switcher.tsx +90 -0
  202. package/components/notes/mobile-bottom-sheet.tsx +99 -0
  203. package/components/notes/note-editor-context-menu.tsx +47 -0
  204. package/components/notes/note-editor-dom.ts +33 -0
  205. package/components/notes/note-editor-dropdowns.tsx +484 -0
  206. package/components/notes/note-editor-hooks.ts +692 -0
  207. package/components/notes/note-editor-keyboard.ts +305 -0
  208. package/components/notes/note-editor-overlay.tsx +90 -0
  209. package/components/notes/note-editor.test.tsx +372 -0
  210. package/components/notes/note-editor.tsx +662 -0
  211. package/components/notes/note-preview-pane.tsx +156 -0
  212. package/components/notes/note-tabs.tsx +120 -0
  213. package/components/notes/note-types.tsx +157 -0
  214. package/components/settings/accept-invite.tsx +108 -0
  215. package/components/settings/agent-token-settings.tsx +369 -0
  216. package/components/settings/backup-restore-settings.test.tsx +25 -0
  217. package/components/settings/backup-restore-settings.tsx +327 -0
  218. package/components/settings/calendar-feeds-settings.tsx +489 -0
  219. package/components/settings/calendar-general-settings.tsx +174 -0
  220. package/components/settings/confirm-danger-action.test.tsx +215 -0
  221. package/components/settings/confirm-danger-action.tsx +65 -0
  222. package/components/settings/security-settings.tsx +252 -0
  223. package/components/settings/settings-guidance.test.tsx +98 -0
  224. package/components/settings/team-settings.tsx +319 -0
  225. package/components/settings/two-factor-auth.tsx +296 -0
  226. package/components/settings/workspace-settings-client.tsx +363 -0
  227. package/components/settings/workspace-settings-form.tsx +73 -0
  228. package/components/sync/conflict-viewer.tsx +247 -0
  229. package/components/sync/sync-indicator.tsx +171 -0
  230. package/components/tasks/snippet-thread.tsx +119 -0
  231. package/components/tasks/status-dot.tsx +47 -0
  232. package/components/tasks/task-badge.tsx +43 -0
  233. package/components/tasks/task-detail.test.tsx +187 -0
  234. package/components/tasks/task-detail.tsx +458 -0
  235. package/components/tasks/task-list-filters.test.tsx +75 -0
  236. package/components/tasks/task-list-filters.tsx +163 -0
  237. package/components/tasks/task-list-types.ts +20 -0
  238. package/components/tasks/task-list.test.tsx +175 -0
  239. package/components/tasks/task-list.tsx +481 -0
  240. package/components/tasks/task-row.tsx +85 -0
  241. package/components/tasks/task-table-row.tsx +259 -0
  242. package/components/ui/skeleton.tsx +3 -0
  243. package/components/ui/toast.test.tsx +42 -0
  244. package/components/ui/toast.tsx +70 -0
  245. package/electron/main.ts +251 -0
  246. package/electron/preload.ts +56 -0
  247. package/instrumentation.tsx +23 -0
  248. package/lib/api-error.ts +50 -0
  249. package/lib/backup/backup-runner.test.ts +32 -0
  250. package/lib/backup/backup-runner.ts +19 -0
  251. package/lib/backup/backup-schedule.test.ts +23 -0
  252. package/lib/backup/backup-schedule.ts +55 -0
  253. package/lib/backup/backup-settings.test.ts +30 -0
  254. package/lib/backup/backup-settings.ts +27 -0
  255. package/lib/backup/export-notes-zip.test.ts +26 -0
  256. package/lib/backup/export-notes-zip.ts +82 -0
  257. package/lib/backup/export-workspace-backup.test.ts +17 -0
  258. package/lib/backup/export-workspace-backup.ts +77 -0
  259. package/lib/backup/restore-workspace-from-export.test.ts +18 -0
  260. package/lib/backup/restore-workspace-from-export.ts +183 -0
  261. package/lib/backup/types.ts +14 -0
  262. package/lib/brand-icons.ts +1 -0
  263. package/lib/calendar-feed-crypto.ts +38 -0
  264. package/lib/calendar-feed.ts +239 -0
  265. package/lib/client/online-status.ts +47 -0
  266. package/lib/conflict-resolver.test.ts +57 -0
  267. package/lib/conflict-resolver.ts +240 -0
  268. package/lib/db-init.ts +79 -0
  269. package/lib/email.ts +159 -0
  270. package/lib/encryption.test.ts +41 -0
  271. package/lib/encryption.ts +98 -0
  272. package/lib/extract-snippet.test.ts +123 -0
  273. package/lib/extract-snippet.ts +69 -0
  274. package/lib/kanban-status.ts +55 -0
  275. package/lib/license.ts +21 -0
  276. package/lib/limits.ts +31 -0
  277. package/lib/mcp-auth.test.ts +58 -0
  278. package/lib/mcp-auth.ts +65 -0
  279. package/lib/mcp-contract.test.ts +25 -0
  280. package/lib/mcp-contract.ts +210 -0
  281. package/lib/mcp-handler.ts +31 -0
  282. package/lib/mcp-url.test.ts +12 -0
  283. package/lib/mcp-url.ts +7 -0
  284. package/lib/mentions.test.ts +45 -0
  285. package/lib/mentions.ts +73 -0
  286. package/lib/note-crypto.ts +108 -0
  287. package/lib/note-sync.ts +201 -0
  288. package/lib/note-title.ts +93 -0
  289. package/lib/prisma.ts +193 -0
  290. package/lib/pro-flush.ts +292 -0
  291. package/lib/rate-limit.ts +57 -0
  292. package/lib/stripe.ts +38 -0
  293. package/lib/sync-worker.ts +388 -0
  294. package/lib/task-parser.test.ts +91 -0
  295. package/lib/task-parser.ts +81 -0
  296. package/lib/task-utils.ts +52 -0
  297. package/lib/use-is-electron.ts +19 -0
  298. package/lib/use-is-mobile.ts +22 -0
  299. package/lib/validation/calendar-feed.ts +31 -0
  300. package/lib/validation/note.ts +27 -0
  301. package/lib/validation/task.ts +26 -0
  302. package/lib/view-preferences.test.ts +54 -0
  303. package/lib/view-preferences.ts +28 -0
  304. package/lib/workspace.ts +66 -0
  305. package/next.config.ts +21 -0
  306. package/package.json +99 -0
  307. package/postcss.config.mjs +7 -0
  308. package/prisma/migrations/20260519021916_init/migration.sql +388 -0
  309. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
  310. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
  311. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
  312. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
  313. package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
  314. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
  315. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
  316. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
  317. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
  318. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
  319. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
  320. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
  321. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
  322. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
  323. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
  324. package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
  325. package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
  326. package/prisma/migrations/migration_lock.toml +3 -0
  327. package/prisma/schema.prisma +457 -0
  328. package/public/Logo_icon.svg +1 -0
  329. package/public/file.svg +1 -0
  330. package/public/globe.svg +1 -0
  331. package/public/icon-192.png +0 -0
  332. package/public/icon-512.png +0 -0
  333. package/public/icon.svg +4 -0
  334. package/public/icon_dark.svg +1 -0
  335. package/public/knotpad_icon.svg +1 -0
  336. package/public/knotpad_logo_full.svg +1 -0
  337. package/public/manifest.json +14 -0
  338. package/public/next.svg +1 -0
  339. package/public/sw.js +137 -0
  340. package/public/vercel.svg +1 -0
  341. package/public/window.svg +1 -0
  342. package/tsconfig.json +35 -0
@@ -0,0 +1,291 @@
1
+ "use client";
2
+
3
+ import { ChevronLeft, ChevronRight, CalendarDays, RefreshCw, ChevronDown, Globe, AlertCircle, ChevronDown as ChevronDownIcon } from "lucide-react";
4
+ import { useRef, useEffect } from "react";
5
+ import type { FeedMeta } from "./calendar-types";
6
+ import { FEED_COLORS, countryFlag } from "./calendar-types";
7
+
8
+ export function CalendarToolbar({
9
+ monthLabel,
10
+ filterNoteId,
11
+ filterFolderId,
12
+ onPrevMonth,
13
+ onNextMonth,
14
+ onJumpToToday,
15
+ // Holiday state
16
+ holidaysEnabled,
17
+ holidayModuleEnabled,
18
+ holidaysVisible,
19
+ holidayCountry,
20
+ holidaysLoading,
21
+ holidayError,
22
+ onToggleHolidays,
23
+ // Busy blocks state
24
+ feeds,
25
+ visibleFeedIds,
26
+ busyEnabled,
27
+ busyLoading,
28
+ busyDropdownOpen,
29
+ setBusyDropdownOpen,
30
+ syncingFeedId,
31
+ syncingAll,
32
+ onToggleFeedVisibility,
33
+ onSyncFeed,
34
+ onSyncAllFeeds,
35
+ onToggleBusyBlocks,
36
+ busyLimit,
37
+ onBusyLimitChange,
38
+ // Month dropdown state
39
+ monthDropdownOpen,
40
+ setMonthDropdownOpen,
41
+ currentMonthParam,
42
+ monthJumpOptions,
43
+ onMonthSelect,
44
+ }: {
45
+ monthLabel: string;
46
+ filterNoteId?: string;
47
+ filterFolderId?: string;
48
+ onPrevMonth: () => void;
49
+ onNextMonth: () => void;
50
+ onJumpToToday: () => void;
51
+ holidaysEnabled: boolean;
52
+ holidayModuleEnabled?: boolean;
53
+ holidaysVisible?: boolean;
54
+ holidayCountry: string;
55
+ holidaysLoading: boolean;
56
+ holidayError: boolean;
57
+ onToggleHolidays: () => void;
58
+ feeds: FeedMeta[];
59
+ visibleFeedIds: Set<string>;
60
+ busyEnabled: boolean;
61
+ busyLoading: boolean;
62
+ busyDropdownOpen: boolean;
63
+ setBusyDropdownOpen: (v: boolean | ((prev: boolean) => boolean)) => void;
64
+ syncingFeedId: string | null;
65
+ syncingAll: boolean;
66
+ onToggleFeedVisibility: (feedId: string) => void;
67
+ onSyncFeed: (feedId: string) => void;
68
+ onSyncAllFeeds: () => void;
69
+ onToggleBusyBlocks: () => void;
70
+ busyLimit: number;
71
+ onBusyLimitChange: (n: number) => void;
72
+ monthDropdownOpen: boolean;
73
+ setMonthDropdownOpen: (v: boolean) => void;
74
+ currentMonthParam: string;
75
+ monthJumpOptions: { value: string; label: string }[];
76
+ onMonthSelect: (value: string) => void;
77
+ }) {
78
+ const dropdownRef = useRef<HTMLDivElement>(null);
79
+ const monthDropdownRef = useRef<HTMLDivElement>(null);
80
+ const holidayModuleIsEnabled = holidayModuleEnabled ?? holidaysEnabled;
81
+ const holidayToggleIsVisible = holidaysVisible ?? holidaysEnabled;
82
+
83
+ // Close dropdowns on outside click
84
+ useEffect(() => {
85
+ function handleClickOutside(event: MouseEvent) {
86
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
87
+ setBusyDropdownOpen(false);
88
+ }
89
+ if (monthDropdownRef.current && !monthDropdownRef.current.contains(event.target as Node)) {
90
+ setMonthDropdownOpen(false);
91
+ }
92
+ }
93
+ document.addEventListener('mousedown', handleClickOutside);
94
+ return () => document.removeEventListener('mousedown', handleClickOutside);
95
+ }, [setBusyDropdownOpen, setMonthDropdownOpen]);
96
+
97
+ return (
98
+ <div className="flex flex-wrap items-center justify-between gap-y-2 border-b border-zinc-800 px-4 py-2.5">
99
+ <div className="flex items-center gap-1">
100
+ <button
101
+ onClick={onPrevMonth}
102
+ className="flex h-7 w-7 items-center justify-center rounded text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
103
+ >
104
+ <ChevronLeft size={16} />
105
+ </button>
106
+ <button
107
+ onClick={onNextMonth}
108
+ className="flex h-7 w-7 items-center justify-center rounded text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
109
+ >
110
+ <ChevronRight size={16} />
111
+ </button>
112
+ <div className="relative" ref={monthDropdownRef}>
113
+ <button
114
+ onClick={() => setMonthDropdownOpen(!monthDropdownOpen)}
115
+ className="ml-2 flex items-center gap-1 text-sm font-medium text-zinc-300 hover:text-zinc-200"
116
+ >
117
+ {monthLabel}
118
+ <ChevronDownIcon size={12} className={`transition-transform ${monthDropdownOpen ? 'rotate-180' : ''}`} />
119
+ </button>
120
+ {monthDropdownOpen && (
121
+ <div className="absolute left-0 top-full z-50 mt-1 w-48 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl shadow-black/40 max-h-64 overflow-y-auto">
122
+ {monthJumpOptions.map((option) => (
123
+ <button
124
+ key={option.value}
125
+ onClick={() => onMonthSelect(option.value)}
126
+ className={`block w-full px-3 py-2 text-left text-xs transition-colors ${
127
+ option.value === currentMonthParam
128
+ ? 'bg-zinc-800 text-zinc-200'
129
+ : 'text-zinc-400 hover:bg-zinc-800/60 hover:text-zinc-200'
130
+ }`}
131
+ >
132
+ {option.label}
133
+ </button>
134
+ ))}
135
+ </div>
136
+ )}
137
+ </div>
138
+ {(filterNoteId || filterFolderId) && (
139
+ <span className="ml-3 rounded-full bg-zinc-800 px-2 py-0.5 text-[10px] text-zinc-400">
140
+ Filtered
141
+ </span>
142
+ )}
143
+ </div>
144
+ <div className="flex flex-wrap items-center justify-end gap-2">
145
+ {/* Public holiday toggle */}
146
+ {holidayModuleIsEnabled && (
147
+ <button
148
+ type="button"
149
+ onClick={onToggleHolidays}
150
+ title={holidayToggleIsVisible ? "Hide public holidays" : "Show public holidays"}
151
+ className={`flex items-center gap-1.5 rounded border px-2.5 py-1 text-xs transition-colors ${
152
+ holidayError
153
+ ? "border-red-700/60 bg-red-950/30 text-red-400 hover:bg-red-950/50"
154
+ : holidayToggleIsVisible
155
+ ? "border-amber-700/70 bg-amber-950/40 text-amber-300 hover:bg-amber-950/60"
156
+ : "border-zinc-700 text-zinc-500 hover:border-zinc-600 hover:text-zinc-300"
157
+ }`}
158
+ >
159
+ {holidayError ? (
160
+ <AlertCircle size={12} />
161
+ ) : holidayToggleIsVisible ? (
162
+ <span className="text-[13px] leading-none">{countryFlag(holidayCountry)}</span>
163
+ ) : (
164
+ <Globe size={12} />
165
+ )}
166
+ <span>Holidays</span>
167
+ {holidaysLoading && (
168
+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
169
+ )}
170
+ </button>
171
+ )}
172
+
173
+ {/* External calendar feeds dropdown */}
174
+ <div className="relative" ref={dropdownRef}>
175
+ <button
176
+ type="button"
177
+ onClick={() => setBusyDropdownOpen((v: boolean) => !v)}
178
+ title="Manage connected calendar feeds"
179
+ className="flex items-center gap-1.5 rounded border border-zinc-700 px-2.5 py-1 text-xs text-zinc-300 transition-colors hover:border-zinc-600 hover:bg-zinc-800/60"
180
+ >
181
+ <CalendarDays size={12} />
182
+ <span>Calendar feeds</span>
183
+ <ChevronDown size={10} className={`transition-transform ${busyDropdownOpen ? "rotate-180" : ""}`} />
184
+ </button>
185
+
186
+ {busyDropdownOpen && (
187
+ <div className="absolute right-0 top-full z-50 mt-1.5 w-64 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl shadow-black/40">
188
+ {feeds.length === 0 ? (
189
+ <div className="p-3 text-xs text-zinc-500">
190
+ <p>No calendars connected yet.</p>
191
+ <p className="mt-1 text-zinc-600">Add one in Settings → Calendar.</p>
192
+ </div>
193
+ ) : (
194
+ <>
195
+ <div className="flex items-center justify-between border-b border-zinc-800 px-3 py-2">
196
+ <button
197
+ type="button"
198
+ onClick={onToggleBusyBlocks}
199
+ className="text-xs text-zinc-400 hover:text-zinc-200 transition-colors"
200
+ >
201
+ {feeds.every((f) => visibleFeedIds.has(f.id)) ? "Hide all" : "Show all"}
202
+ </button>
203
+ <button
204
+ type="button"
205
+ onClick={onSyncAllFeeds}
206
+ disabled={syncingAll}
207
+ className="flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors disabled:opacity-50"
208
+ title="Refresh all feeds"
209
+ >
210
+ <RefreshCw size={11} className={syncingAll ? "animate-spin" : ""} />
211
+ <span>{syncingAll ? "Syncing…" : "Sync all"}</span>
212
+ </button>
213
+ </div>
214
+
215
+ <div className="max-h-60 overflow-y-auto py-1">
216
+ {feeds.map((feed) => {
217
+ const fc = (FEED_COLORS as Record<string, { bg: string; text: string }>)[feed.color] ?? FEED_COLORS.sky;
218
+ const visible = visibleFeedIds.has(feed.id);
219
+ return (
220
+ <div
221
+ key={feed.id}
222
+ className="flex items-center gap-2 px-3 py-1.5 hover:bg-zinc-800/60 transition-colors"
223
+ >
224
+ <button
225
+ type="button"
226
+ onClick={() => onToggleFeedVisibility(feed.id)}
227
+ className="flex flex-1 items-center gap-2 min-w-0"
228
+ >
229
+ <span
230
+ className={`h-3 w-3 shrink-0 rounded-full transition-opacity ${fc.bg} ${
231
+ visible ? "opacity-100" : "opacity-30"
232
+ }`}
233
+ />
234
+ <span className={`truncate text-xs ${visible ? "text-zinc-200" : "text-zinc-600"}`}>
235
+ {feed.label}
236
+ </span>
237
+ </button>
238
+
239
+ <button
240
+ type="button"
241
+ onClick={() => onSyncFeed(feed.id)}
242
+ disabled={syncingFeedId === feed.id}
243
+ title={`Refresh ${feed.label}`}
244
+ className="shrink-0 text-zinc-600 hover:text-zinc-300 transition-colors disabled:opacity-50"
245
+ >
246
+ <RefreshCw size={11} className={syncingFeedId === feed.id ? "animate-spin" : ""} />
247
+ </button>
248
+ </div>
249
+ );
250
+ })}
251
+ </div>
252
+
253
+ <div className="flex items-center justify-between border-t border-zinc-800 px-3 py-2">
254
+ <span className="text-xs text-zinc-500">Show per day</span>
255
+ <div className="flex items-center gap-1.5">
256
+ <button
257
+ type="button"
258
+ onClick={() => onBusyLimitChange(busyLimit - 1)}
259
+ disabled={busyLimit <= 1}
260
+ className="flex h-5 w-5 items-center justify-center rounded border border-zinc-700 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200 disabled:opacity-30 disabled:hover:bg-transparent"
261
+ >
262
+
263
+ </button>
264
+ <span className="w-4 text-center text-xs text-zinc-300">{busyLimit}</span>
265
+ <button
266
+ type="button"
267
+ onClick={() => onBusyLimitChange(busyLimit + 1)}
268
+ disabled={busyLimit >= 5}
269
+ className="flex h-5 w-5 items-center justify-center rounded border border-zinc-700 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200 disabled:opacity-30 disabled:hover:bg-transparent"
270
+ >
271
+ +
272
+ </button>
273
+ </div>
274
+ </div>
275
+ </>
276
+ )}
277
+ </div>
278
+ )}
279
+ </div>
280
+
281
+ <button
282
+ type="button"
283
+ onClick={onJumpToToday}
284
+ className="rounded border border-zinc-700 px-2.5 py-1 text-xs text-zinc-400 hover:border-zinc-600 hover:text-zinc-200 transition-colors"
285
+ >
286
+ Today
287
+ </button>
288
+ </div>
289
+ </div>
290
+ );
291
+ }
@@ -0,0 +1,148 @@
1
+ import { FEED_COLORS } from "@/lib/limits";
2
+
3
+ export type CalTask = {
4
+ id: string;
5
+ title: string;
6
+ status: string;
7
+ startDate: string | null;
8
+ dueDate: string | null;
9
+ noteId: string;
10
+ noteTitle: string;
11
+ assigneeName: string | null;
12
+ assigneeType: string;
13
+ claimedByAlias: string | null;
14
+ };
15
+
16
+ export type HolidayEntry = { name: string; is_subject_to_change: boolean };
17
+ export type BusyBlockEntry = { title: string; start: string; end: string; allDay: boolean; feedId: string; feedLabel: string; feedColor: string };
18
+ export type FeedMeta = { id: string; label: string; color: string };
19
+
20
+ export type Props = {
21
+ tasks: CalTask[];
22
+ initialMonth?: string;
23
+ filterNoteId?: string;
24
+ filterFolderId?: string;
25
+ };
26
+
27
+ export const COLOR_MAP: Record<string, string> = {
28
+ "border-zinc-700": "bg-zinc-600",
29
+ "border-red-700": "bg-red-600",
30
+ "border-orange-700": "bg-orange-600",
31
+ "border-amber-700": "bg-amber-600",
32
+ "border-yellow-700": "bg-yellow-600",
33
+ "border-lime-700": "bg-lime-600",
34
+ "border-green-700": "bg-green-600",
35
+ "border-emerald-700": "bg-emerald-600",
36
+ "border-teal-700": "bg-teal-600",
37
+ "border-cyan-700": "bg-cyan-600",
38
+ "border-sky-700": "bg-sky-600",
39
+ "border-blue-700": "bg-blue-600",
40
+ "border-indigo-700": "bg-indigo-600",
41
+ "border-violet-700": "bg-violet-600",
42
+ "border-purple-700": "bg-purple-600",
43
+ "border-fuchsia-700": "bg-fuchsia-600",
44
+ "border-pink-700": "bg-pink-600",
45
+ "border-rose-700": "bg-rose-600",
46
+ };
47
+
48
+ export const DOT_MAP: Record<string, string> = {
49
+ "border-zinc-700": "bg-zinc-500",
50
+ "border-red-700": "bg-red-500",
51
+ "border-orange-700": "bg-orange-500",
52
+ "border-amber-700": "bg-amber-500",
53
+ "border-yellow-700": "bg-yellow-500",
54
+ "border-lime-700": "bg-lime-500",
55
+ "border-green-700": "bg-green-500",
56
+ "border-emerald-700": "bg-emerald-500",
57
+ "border-teal-700": "bg-teal-500",
58
+ "border-cyan-700": "bg-cyan-500",
59
+ "border-sky-700": "bg-sky-500",
60
+ "border-blue-700": "bg-blue-500",
61
+ "border-indigo-700": "bg-indigo-500",
62
+ "border-violet-700": "bg-violet-500",
63
+ "border-purple-700": "bg-purple-500",
64
+ "border-fuchsia-700": "bg-fuchsia-500",
65
+ "border-pink-700": "bg-pink-500",
66
+ "border-rose-700": "bg-rose-500",
67
+ };
68
+
69
+ export const MY_STATES = [
70
+ { code: "FED", name: "Federal (All Territories)", short: "Federal" },
71
+ { code: "KUL", name: "W.P. Kuala Lumpur", short: "KL" },
72
+ { code: "PJY", name: "W.P. Putrajaya", short: "Putrajaya" },
73
+ { code: "LBN", name: "W.P. Labuan", short: "Labuan" },
74
+ { code: "SGR", name: "Selangor", short: "Selangor" },
75
+ { code: "JHR", name: "Johor", short: "Johor" },
76
+ { code: "KDH", name: "Kedah", short: "Kedah" },
77
+ { code: "KLN", name: "Kelantan", short: "Kelantan" },
78
+ { code: "MLK", name: "Melaka", short: "Melaka" },
79
+ { code: "NSN", name: "Negeri Sembilan", short: "N. Sembilan" },
80
+ { code: "PHG", name: "Pahang", short: "Pahang" },
81
+ { code: "PNG", name: "Pulau Pinang", short: "Penang" },
82
+ { code: "PRK", name: "Perak", short: "Perak" },
83
+ { code: "PLS", name: "Perlis", short: "Perlis" },
84
+ { code: "SBH", name: "Sabah", short: "Sabah" },
85
+ { code: "SWK", name: "Sarawak", short: "Sarawak" },
86
+ { code: "TRG", name: "Terengganu", short: "Terengganu" },
87
+ ] as const;
88
+
89
+ export const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
90
+
91
+ export function getOrderedDays(firstDayOfWeek: number): string[] {
92
+ return [...DAYS.slice(firstDayOfWeek), ...DAYS.slice(0, firstDayOfWeek)];
93
+ }
94
+
95
+ /** Look up a feed colour's Tailwind classes, falling back to "sky". */
96
+ export function feedBg(color: string): string {
97
+ const c = (FEED_COLORS as Record<string, { bgLight: string; border: string; text: string }>)[color];
98
+ return c ? `${c.bgLight} ${c.border}` : "bg-sky-600/20 border-sky-500/30";
99
+ }
100
+ export function feedText(color: string): string {
101
+ const c = (FEED_COLORS as Record<string, { text: string }>)[color];
102
+ return c ? c.text : "text-sky-300";
103
+ }
104
+
105
+ export function getStatusColor(kanbanStatuses: { key: string; color: string }[], status: string): string {
106
+ const config = kanbanStatuses.find((s) => s.key === status);
107
+ return config ? (COLOR_MAP[config.color] ?? "bg-zinc-600") : "bg-zinc-600";
108
+ }
109
+
110
+ export function getStatusDot(kanbanStatuses: { key: string; color: string }[], status: string): string {
111
+ const config = kanbanStatuses.find((s) => s.key === status);
112
+ return config ? (DOT_MAP[config.color] ?? "bg-zinc-500") : "bg-zinc-500";
113
+ }
114
+
115
+ export function toYMD(date: Date) {
116
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
117
+ }
118
+
119
+ export function parseYM(s?: string): { year: number; month: number } {
120
+ if (s && /^\d{4}-\d{2}$/.test(s)) {
121
+ const [y, m] = s.split("-").map(Number);
122
+ return { year: y, month: m - 1 };
123
+ }
124
+ const now = new Date();
125
+ return { year: now.getFullYear(), month: now.getMonth() };
126
+ }
127
+
128
+ // Effective date range for a task (with optimistic overrides)
129
+ export function effectiveDates(
130
+ task: CalTask,
131
+ optimistic: Record<string, { startDate?: string | null; dueDate?: string | null }>,
132
+ mode?: "due" | "start"
133
+ ): { start: string | null; end: string | null } {
134
+ const o = optimistic[task.id] ?? {};
135
+ const start = "startDate" in o ? o.startDate : task.startDate;
136
+ const end = "dueDate" in o ? o.dueDate : task.dueDate;
137
+ // Normalise: start defaults to end for single-day tasks
138
+ const s = (start ?? end)?.slice(0, 10) ?? null;
139
+ const e = (end ?? start)?.slice(0, 10) ?? null;
140
+ return { start: s, end: e };
141
+ }
142
+
143
+ export function countryFlag(code: string): string {
144
+ if (!/^[A-Z]{2}$/.test(code)) return "🌍";
145
+ return String.fromCodePoint(...code.split("").map((c) => 0x1f1e6 + c.charCodeAt(0) - 65));
146
+ }
147
+
148
+ export { FEED_COLORS };