@knotpad/app 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/(app)/calendar/page.tsx +57 -0
- package/app/(app)/error.tsx +35 -0
- package/app/(app)/graph/page.tsx +32 -0
- package/app/(app)/guide/page.tsx +21 -0
- package/app/(app)/kanban/loading.tsx +24 -0
- package/app/(app)/kanban/page.tsx +59 -0
- package/app/(app)/layout.tsx +122 -0
- package/app/(app)/list/loading.tsx +21 -0
- package/app/(app)/list/page.tsx +137 -0
- package/app/(app)/loading.tsx +18 -0
- package/app/(app)/notes/[noteId]/page.tsx +84 -0
- package/app/(app)/notes/layout.tsx +30 -0
- package/app/(app)/notes/page.tsx +39 -0
- package/app/(app)/page.tsx +5 -0
- package/app/(app)/settings/agent-token/page.tsx +59 -0
- package/app/(app)/settings/backup/page.tsx +49 -0
- package/app/(app)/settings/billing/page.tsx +53 -0
- package/app/(app)/settings/calendar/page.tsx +41 -0
- package/app/(app)/settings/layout.test.tsx +39 -0
- package/app/(app)/settings/layout.tsx +71 -0
- package/app/(app)/settings/page.tsx +4 -0
- package/app/(app)/settings/security/page.tsx +43 -0
- package/app/(app)/settings/team/page.tsx +74 -0
- package/app/(app)/settings/workspace/page.tsx +27 -0
- package/app/(app)/tasks/[taskId]/page.tsx +79 -0
- package/app/(auth)/forgot-password/page.tsx +106 -0
- package/app/(auth)/guest/page.tsx +56 -0
- package/app/(auth)/layout.tsx +13 -0
- package/app/(auth)/login/page.tsx +14 -0
- package/app/(auth)/register/page.tsx +193 -0
- package/app/(auth)/reset-password/page.tsx +138 -0
- package/app/api/account/claim/route.tsx +135 -0
- package/app/api/admin/backfill-encryption/route.tsx +43 -0
- package/app/api/admin/license/route.tsx +42 -0
- package/app/api/auth/2fa/route.tsx +148 -0
- package/app/api/auth/[...nextauth]/route.tsx +3 -0
- package/app/api/auth/change-password/route.tsx +61 -0
- package/app/api/auth/check-2fa/route.tsx +19 -0
- package/app/api/auth/forgot-password/route.tsx +65 -0
- package/app/api/auth/reset-password/route.tsx +52 -0
- package/app/api/auth/verify-2fa/route.tsx +88 -0
- package/app/api/backup/download/db/route.ts +29 -0
- package/app/api/backup/download/notes/route.ts +25 -0
- package/app/api/backup/settings/route.ts +92 -0
- package/app/api/billing/checkout/route.tsx +81 -0
- package/app/api/billing/migrate/route.tsx +163 -0
- package/app/api/billing/portal/route.tsx +24 -0
- package/app/api/billing/setup-intent/route.tsx +55 -0
- package/app/api/billing/status/route.tsx +36 -0
- package/app/api/billing/subscribe/route.tsx +85 -0
- package/app/api/billing/webhook/route.tsx +199 -0
- package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
- package/app/api/calendar-feeds/events/route.tsx +82 -0
- package/app/api/calendar-feeds/route.tsx +52 -0
- package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
- package/app/api/cron/calendar-feeds/route.tsx +31 -0
- package/app/api/cron/stale-tasks/route.tsx +51 -0
- package/app/api/cron/sync/route.tsx +34 -0
- package/app/api/devices/[deviceId]/route.tsx +25 -0
- package/app/api/devices/route.tsx +41 -0
- package/app/api/export/route.tsx +40 -0
- package/app/api/feedback/route.tsx +54 -0
- package/app/api/folders/[folderId]/route.tsx +51 -0
- package/app/api/folders/route.tsx +37 -0
- package/app/api/graph/route.tsx +242 -0
- package/app/api/guest/route.tsx +58 -0
- package/app/api/health/route.tsx +10 -0
- package/app/api/holidays/countries/route.tsx +14 -0
- package/app/api/holidays/route.tsx +49 -0
- package/app/api/holidays/states/route.tsx +21 -0
- package/app/api/invites/[token]/route.tsx +131 -0
- package/app/api/invites/route.tsx +74 -0
- package/app/api/mcp/generate-token/route.tsx +55 -0
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
- package/app/api/notes/[noteId]/export/route.tsx +45 -0
- package/app/api/notes/[noteId]/route.tsx +360 -0
- package/app/api/notes/route.tsx +112 -0
- package/app/api/notifications/route.tsx +44 -0
- package/app/api/register/route.tsx +67 -0
- package/app/api/restore/route.tsx +148 -0
- package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
- package/app/api/sync/conflicts/route.tsx +48 -0
- package/app/api/sync/status/route.tsx +49 -0
- package/app/api/sync/trigger/route.tsx +15 -0
- package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
- package/app/api/tasks/[taskId]/route.tsx +259 -0
- package/app/api/tasks/bulk/route.tsx +133 -0
- package/app/api/tasks/route.tsx +36 -0
- package/app/api/workspace/active/route.tsx +39 -0
- package/app/api/workspace/create-team/route.tsx +42 -0
- package/app/api/workspace/kanban-statuses/route.tsx +71 -0
- package/app/api/workspace/members/[memberId]/route.tsx +69 -0
- package/app/api/workspace/route.tsx +24 -0
- package/app/download/page.tsx +170 -0
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +1 -0
- package/app/generated/prisma/client.js +5 -0
- package/app/generated/prisma/default.d.ts +1 -0
- package/app/generated/prisma/default.js +5 -0
- package/app/generated/prisma/edge.d.ts +1 -0
- package/app/generated/prisma/edge.js +497 -0
- package/app/generated/prisma/index-browser.js +523 -0
- package/app/generated/prisma/index.d.ts +46376 -0
- package/app/generated/prisma/index.js +497 -0
- package/app/generated/prisma/package.json +144 -0
- package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/app/generated/prisma/runtime/client.d.ts +3386 -0
- package/app/generated/prisma/runtime/client.js +86 -0
- package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
- package/app/generated/prisma/runtime/index-browser.js +6 -0
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
- package/app/generated/prisma/schema.prisma +456 -0
- package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
- package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
- package/app/globals.css +54 -0
- package/app/invite/[token]/page.tsx +52 -0
- package/app/layout.tsx +90 -0
- package/app/mcp/route.tsx +430 -0
- package/app/opengraph-image.tsx +120 -0
- package/app/page.tsx +398 -0
- package/app/privacy/page.tsx +69 -0
- package/app/robots.tsx +25 -0
- package/app/sitemap.tsx +36 -0
- package/app/terms/page.tsx +69 -0
- package/app/upgrade/page.tsx +75 -0
- package/auth.config.ts +33 -0
- package/auth.ts +79 -0
- package/bin/brief.js +229 -0
- package/components/auth/login-form.tsx +302 -0
- package/components/auth/password-checklist.tsx +31 -0
- package/components/auth/password-input.tsx +36 -0
- package/components/auth/switch-account-button.test.tsx +22 -0
- package/components/auth/switch-account-button.tsx +19 -0
- package/components/auth/two-factor-input.tsx +116 -0
- package/components/billing/billing-dashboard.tsx +265 -0
- package/components/billing/card-form.tsx +210 -0
- package/components/billing/claim-account-form.tsx +99 -0
- package/components/branding/app-logo.test.tsx +20 -0
- package/components/branding/app-logo.tsx +25 -0
- package/components/calendar/calendar-agenda.tsx +150 -0
- package/components/calendar/calendar-drag.test.tsx +177 -0
- package/components/calendar/calendar-grid.tsx +357 -0
- package/components/calendar/calendar-hooks.test.tsx +27 -0
- package/components/calendar/calendar-hooks.ts +351 -0
- package/components/calendar/calendar-toolbar.test.tsx +68 -0
- package/components/calendar/calendar-toolbar.tsx +291 -0
- package/components/calendar/calendar-types.ts +148 -0
- package/components/calendar/calendar-view.test.tsx +295 -0
- package/components/calendar/calendar-view.tsx +307 -0
- package/components/calendar/day-detail-popover.tsx +174 -0
- package/components/calendar/task-chip.tsx +86 -0
- package/components/command/command-palette.test.tsx +33 -0
- package/components/command/command-palette.tsx +310 -0
- package/components/download-cta.tsx +87 -0
- package/components/feedback/feedback-popup.tsx +207 -0
- package/components/graph/graph-draw.ts +337 -0
- package/components/graph/graph-overlays.tsx +160 -0
- package/components/graph/graph-page.test.tsx +131 -0
- package/components/graph/graph-page.tsx +263 -0
- package/components/graph/graph-types.ts +47 -0
- package/components/graph/graph-view.tsx +322 -0
- package/components/guide/guide-view.tsx +522 -0
- package/components/kanban/kanban-board.test.tsx +128 -0
- package/components/kanban/kanban-board.tsx +361 -0
- package/components/kanban/kanban-card-menu.tsx +102 -0
- package/components/kanban/kanban-card.tsx +227 -0
- package/components/kanban/kanban-column.tsx +49 -0
- package/components/kanban/kanban-status-context.tsx +28 -0
- package/components/landing/calendar-sandbox.test.tsx +15 -0
- package/components/landing/calendar-sandbox.tsx +107 -0
- package/components/landing/graph-sandbox.test.tsx +27 -0
- package/components/landing/graph-sandbox.tsx +80 -0
- package/components/landing/kanban-sandbox.test.tsx +24 -0
- package/components/landing/kanban-sandbox.tsx +101 -0
- package/components/landing/landing-showcase.test.tsx +21 -0
- package/components/landing/landing-showcase.tsx +54 -0
- package/components/landing/list-sandbox.tsx +86 -0
- package/components/landing/mock-workspace.ts +168 -0
- package/components/landing/notes-sandbox.test.tsx +14 -0
- package/components/landing/notes-sandbox.tsx +88 -0
- package/components/layout/app-shell.tsx +83 -0
- package/components/layout/backup-scheduler.tsx +122 -0
- package/components/layout/bottom-nav.tsx +43 -0
- package/components/layout/icon-bar.test.tsx +29 -0
- package/components/layout/icon-bar.tsx +118 -0
- package/components/layout/mobile-top-bar.tsx +68 -0
- package/components/layout/notes-panel-folder.tsx +127 -0
- package/components/layout/notes-panel-note-item.tsx +140 -0
- package/components/layout/notes-panel-task-tab.tsx +63 -0
- package/components/layout/notes-panel-types.ts +44 -0
- package/components/layout/notes-panel.tsx +476 -0
- package/components/layout/notification-bell.tsx +251 -0
- package/components/layout/paywall-screen.tsx +41 -0
- package/components/layout/pro-banner.tsx +76 -0
- package/components/layout/sw-register.tsx +27 -0
- package/components/layout/workspace-switcher.tsx +90 -0
- package/components/notes/mobile-bottom-sheet.tsx +99 -0
- package/components/notes/note-editor-context-menu.tsx +47 -0
- package/components/notes/note-editor-dom.ts +33 -0
- package/components/notes/note-editor-dropdowns.tsx +484 -0
- package/components/notes/note-editor-hooks.ts +692 -0
- package/components/notes/note-editor-keyboard.ts +305 -0
- package/components/notes/note-editor-overlay.tsx +90 -0
- package/components/notes/note-editor.test.tsx +372 -0
- package/components/notes/note-editor.tsx +662 -0
- package/components/notes/note-preview-pane.tsx +156 -0
- package/components/notes/note-tabs.tsx +120 -0
- package/components/notes/note-types.tsx +157 -0
- package/components/settings/accept-invite.tsx +108 -0
- package/components/settings/agent-token-settings.tsx +369 -0
- package/components/settings/backup-restore-settings.test.tsx +25 -0
- package/components/settings/backup-restore-settings.tsx +327 -0
- package/components/settings/calendar-feeds-settings.tsx +489 -0
- package/components/settings/calendar-general-settings.tsx +174 -0
- package/components/settings/confirm-danger-action.test.tsx +215 -0
- package/components/settings/confirm-danger-action.tsx +65 -0
- package/components/settings/security-settings.tsx +252 -0
- package/components/settings/settings-guidance.test.tsx +98 -0
- package/components/settings/team-settings.tsx +319 -0
- package/components/settings/two-factor-auth.tsx +296 -0
- package/components/settings/workspace-settings-client.tsx +363 -0
- package/components/settings/workspace-settings-form.tsx +73 -0
- package/components/sync/conflict-viewer.tsx +247 -0
- package/components/sync/sync-indicator.tsx +171 -0
- package/components/tasks/snippet-thread.tsx +119 -0
- package/components/tasks/status-dot.tsx +47 -0
- package/components/tasks/task-badge.tsx +43 -0
- package/components/tasks/task-detail.test.tsx +187 -0
- package/components/tasks/task-detail.tsx +458 -0
- package/components/tasks/task-list-filters.test.tsx +75 -0
- package/components/tasks/task-list-filters.tsx +163 -0
- package/components/tasks/task-list-types.ts +20 -0
- package/components/tasks/task-list.test.tsx +175 -0
- package/components/tasks/task-list.tsx +481 -0
- package/components/tasks/task-row.tsx +85 -0
- package/components/tasks/task-table-row.tsx +259 -0
- package/components/ui/skeleton.tsx +3 -0
- package/components/ui/toast.test.tsx +42 -0
- package/components/ui/toast.tsx +70 -0
- package/instrumentation.tsx +23 -0
- package/lib/api-error.ts +50 -0
- package/lib/backup/backup-runner.test.ts +32 -0
- package/lib/backup/backup-runner.ts +19 -0
- package/lib/backup/backup-schedule.test.ts +23 -0
- package/lib/backup/backup-schedule.ts +55 -0
- package/lib/backup/backup-settings.test.ts +30 -0
- package/lib/backup/backup-settings.ts +27 -0
- package/lib/backup/export-notes-zip.test.ts +26 -0
- package/lib/backup/export-notes-zip.ts +82 -0
- package/lib/backup/export-workspace-backup.test.ts +17 -0
- package/lib/backup/export-workspace-backup.ts +77 -0
- package/lib/backup/restore-workspace-from-export.test.ts +18 -0
- package/lib/backup/restore-workspace-from-export.ts +183 -0
- package/lib/backup/types.ts +14 -0
- package/lib/brand-icons.ts +1 -0
- package/lib/calendar-feed-crypto.ts +38 -0
- package/lib/calendar-feed.ts +239 -0
- package/lib/client/online-status.ts +47 -0
- package/lib/conflict-resolver.test.ts +57 -0
- package/lib/conflict-resolver.ts +240 -0
- package/lib/db-init.ts +79 -0
- package/lib/email.ts +159 -0
- package/lib/encryption.test.ts +41 -0
- package/lib/encryption.ts +98 -0
- package/lib/extract-snippet.test.ts +123 -0
- package/lib/extract-snippet.ts +69 -0
- package/lib/kanban-status.ts +55 -0
- package/lib/license.ts +21 -0
- package/lib/limits.ts +31 -0
- package/lib/mcp-auth.test.ts +58 -0
- package/lib/mcp-auth.ts +65 -0
- package/lib/mcp-contract.test.ts +25 -0
- package/lib/mcp-contract.ts +210 -0
- package/lib/mcp-handler.ts +31 -0
- package/lib/mcp-url.test.ts +12 -0
- package/lib/mcp-url.ts +7 -0
- package/lib/mentions.test.ts +45 -0
- package/lib/mentions.ts +73 -0
- package/lib/note-crypto.ts +108 -0
- package/lib/note-sync.ts +201 -0
- package/lib/note-title.ts +93 -0
- package/lib/prisma.ts +193 -0
- package/lib/pro-flush.ts +292 -0
- package/lib/rate-limit.ts +57 -0
- package/lib/stripe.ts +38 -0
- package/lib/sync-worker.ts +388 -0
- package/lib/task-parser.test.ts +91 -0
- package/lib/task-parser.ts +81 -0
- package/lib/task-utils.ts +52 -0
- package/lib/use-is-electron.ts +19 -0
- package/lib/use-is-mobile.ts +22 -0
- package/lib/validation/calendar-feed.ts +31 -0
- package/lib/validation/note.ts +27 -0
- package/lib/validation/task.ts +26 -0
- package/lib/view-preferences.test.ts +54 -0
- package/lib/view-preferences.ts +28 -0
- package/lib/workspace.ts +66 -0
- package/next.config.ts +21 -0
- package/package.json +54 -3
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260519021916_init/migration.sql +388 -0
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
- package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +457 -0
- package/public/Logo_icon.svg +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon_dark.svg +1 -0
- package/public/knotpad_icon.svg +1 -0
- package/public/knotpad_logo_full.svg +1 -0
- package/public/manifest.json +14 -0
- package/public/next.svg +1 -0
- package/public/sw.js +137 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +35 -0
- package/brief.js +0 -311
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export type MockTaskStatus = "Open" | "In Progress" | "Review" | "Done";
|
|
2
|
+
|
|
3
|
+
export type MockTask = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
status: MockTaskStatus;
|
|
7
|
+
dueDate: string;
|
|
8
|
+
assignee: string;
|
|
9
|
+
noteId: string;
|
|
10
|
+
priority?: "high" | "medium" | "low";
|
|
11
|
+
fileRef?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type MockNote = {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
content: string;
|
|
18
|
+
folder: string;
|
|
19
|
+
references: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MockWorkspace = {
|
|
23
|
+
folder: { id: string; name: string };
|
|
24
|
+
notes: MockNote[];
|
|
25
|
+
tasks: MockTask[];
|
|
26
|
+
busyBlocks: Array<{ id: string; label: string; date: string }>;
|
|
27
|
+
team: Array<{ id: string; name: string; role: string }>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function createMockWorkspace(): MockWorkspace {
|
|
31
|
+
return {
|
|
32
|
+
folder: { id: "folder_launch", name: "Launch" },
|
|
33
|
+
notes: [
|
|
34
|
+
{
|
|
35
|
+
id: "note_launch_page",
|
|
36
|
+
title: "Landing page improvements",
|
|
37
|
+
folder: "Launch",
|
|
38
|
+
references: ["Brand refresh", "Pricing cleanup"],
|
|
39
|
+
content: [
|
|
40
|
+
"## Landing page improvements",
|
|
41
|
+
"",
|
|
42
|
+
"Ship a stronger first impression for Knotpad.",
|
|
43
|
+
"",
|
|
44
|
+
"### Tasks",
|
|
45
|
+
"- [ ] Tighten hero copy @maya !high",
|
|
46
|
+
"- [ ] Build interactive showcase @agent <plan.md>",
|
|
47
|
+
"- [ ] Review pricing language @alex +today",
|
|
48
|
+
"- [x] Design new hero section @maya",
|
|
49
|
+
].join("\n"),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "note_api_auth",
|
|
53
|
+
title: "API authentication flow",
|
|
54
|
+
folder: "Backend",
|
|
55
|
+
references: ["api-spec.md", "security-audit.md"],
|
|
56
|
+
content: [
|
|
57
|
+
"## API authentication flow",
|
|
58
|
+
"",
|
|
59
|
+
"Implement secure JWT-based authentication for the API.",
|
|
60
|
+
"",
|
|
61
|
+
"### Tasks",
|
|
62
|
+
"- [ ] Design token refresh strategy @agent <api-spec.md> !high",
|
|
63
|
+
"- [ ] Implement login endpoint @sarah +tomorrow",
|
|
64
|
+
"- [ ] Add rate limiting @agent !medium",
|
|
65
|
+
"- [ ] Write integration tests @alex +next-week",
|
|
66
|
+
"- [ ] Security review @security-team",
|
|
67
|
+
].join("\n"),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
tasks: [
|
|
71
|
+
{
|
|
72
|
+
id: "task_1",
|
|
73
|
+
title: "Tighten hero copy",
|
|
74
|
+
status: "Open",
|
|
75
|
+
dueDate: "2026-06-14",
|
|
76
|
+
assignee: "maya",
|
|
77
|
+
noteId: "note_launch_page",
|
|
78
|
+
priority: "high",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "task_2",
|
|
82
|
+
title: "Build interactive showcase",
|
|
83
|
+
status: "In Progress",
|
|
84
|
+
dueDate: "2026-06-15",
|
|
85
|
+
assignee: "agent",
|
|
86
|
+
noteId: "note_launch_page",
|
|
87
|
+
priority: "medium",
|
|
88
|
+
fileRef: "plan.md",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "task_3",
|
|
92
|
+
title: "Review pricing language",
|
|
93
|
+
status: "Review",
|
|
94
|
+
dueDate: "2026-06-16",
|
|
95
|
+
assignee: "alex",
|
|
96
|
+
noteId: "note_launch_page",
|
|
97
|
+
priority: "low",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "task_4",
|
|
101
|
+
title: "Design new hero section",
|
|
102
|
+
status: "Done",
|
|
103
|
+
dueDate: "2026-06-13",
|
|
104
|
+
assignee: "maya",
|
|
105
|
+
noteId: "note_launch_page",
|
|
106
|
+
priority: "high",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "task_5",
|
|
110
|
+
title: "Design token refresh strategy",
|
|
111
|
+
status: "Open",
|
|
112
|
+
dueDate: "2026-06-17",
|
|
113
|
+
assignee: "agent",
|
|
114
|
+
noteId: "note_api_auth",
|
|
115
|
+
priority: "high",
|
|
116
|
+
fileRef: "api-spec.md",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "task_6",
|
|
120
|
+
title: "Implement login endpoint",
|
|
121
|
+
status: "In Progress",
|
|
122
|
+
dueDate: "2026-06-18",
|
|
123
|
+
assignee: "sarah",
|
|
124
|
+
noteId: "note_api_auth",
|
|
125
|
+
priority: "high",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "task_7",
|
|
129
|
+
title: "Add rate limiting",
|
|
130
|
+
status: "Open",
|
|
131
|
+
dueDate: "2026-06-19",
|
|
132
|
+
assignee: "agent",
|
|
133
|
+
noteId: "note_api_auth",
|
|
134
|
+
priority: "medium",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "task_8",
|
|
138
|
+
title: "Write integration tests",
|
|
139
|
+
status: "Open",
|
|
140
|
+
dueDate: "2026-06-21",
|
|
141
|
+
assignee: "alex",
|
|
142
|
+
noteId: "note_api_auth",
|
|
143
|
+
priority: "low",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "task_9",
|
|
147
|
+
title: "Security review",
|
|
148
|
+
status: "Review",
|
|
149
|
+
dueDate: "2026-06-20",
|
|
150
|
+
assignee: "security-team",
|
|
151
|
+
noteId: "note_api_auth",
|
|
152
|
+
priority: "high",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
busyBlocks: [
|
|
156
|
+
{ id: "busy_1", label: "Design review", date: "2026-06-14" },
|
|
157
|
+
{ id: "busy_2", label: "Sprint planning", date: "2026-06-16" },
|
|
158
|
+
{ id: "busy_3", label: "Team sync", date: "2026-06-17" },
|
|
159
|
+
{ id: "busy_4", label: "Focus time", date: "2026-06-18" },
|
|
160
|
+
],
|
|
161
|
+
team: [
|
|
162
|
+
{ id: "maya", name: "Maya", role: "Designer" },
|
|
163
|
+
{ id: "alex", name: "Alex", role: "Developer" },
|
|
164
|
+
{ id: "sarah", name: "Sarah", role: "Backend Lead" },
|
|
165
|
+
{ id: "security-team", name: "Security Team", role: "Reviewer" },
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createMockWorkspace } from "@/components/landing/mock-workspace";
|
|
5
|
+
import { NotesSandbox } from "@/components/landing/notes-sandbox";
|
|
6
|
+
|
|
7
|
+
describe("NotesSandbox", () => {
|
|
8
|
+
it("renders note title and folder with syntax-highlighted content", () => {
|
|
9
|
+
render(<NotesSandbox workspace={createMockWorkspace()} />);
|
|
10
|
+
|
|
11
|
+
expect(screen.getByText("Landing page improvements")).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByText("Launch")).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { MockWorkspace } from "@/components/landing/mock-workspace";
|
|
5
|
+
|
|
6
|
+
const STATUS_DOT_COLORS: Record<string, string> = {
|
|
7
|
+
Open: "bg-zinc-500",
|
|
8
|
+
"In Progress": "bg-blue-500",
|
|
9
|
+
Review: "bg-amber-500",
|
|
10
|
+
Done: "bg-emerald-500",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function StatusDot({ status }: { status: string }) {
|
|
14
|
+
const color = STATUS_DOT_COLORS[status] ?? "bg-zinc-500";
|
|
15
|
+
return <span className={`inline-block h-2 w-2 rounded-full ${color}`} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function NotesSandbox({ workspace }: { workspace: MockWorkspace }) {
|
|
19
|
+
const note = workspace.notes[0];
|
|
20
|
+
const tasksByLine = useMemo(() => {
|
|
21
|
+
const lines = note.content.split("\n");
|
|
22
|
+
const map: Record<number, { taskId: string; status: string }> = {};
|
|
23
|
+
lines.forEach((line, i) => {
|
|
24
|
+
const match = line.match(/^\s*-?\s*\[[ x]\]\s+(.+)$/);
|
|
25
|
+
if (match) {
|
|
26
|
+
const title = match[1]
|
|
27
|
+
.replace(/@[\w-]+/g, "")
|
|
28
|
+
.replace(/![\w-]+/g, "")
|
|
29
|
+
.replace(/<[^>]+>/g, "")
|
|
30
|
+
.replace(/\+\w+/g, "")
|
|
31
|
+
.trim();
|
|
32
|
+
const task = workspace.tasks.find(
|
|
33
|
+
(t) => t.title.toLowerCase() === title.toLowerCase()
|
|
34
|
+
);
|
|
35
|
+
if (task) {
|
|
36
|
+
map[i] = { taskId: task.id, status: task.status };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return map;
|
|
41
|
+
}, [note.content, workspace.tasks]);
|
|
42
|
+
|
|
43
|
+
const highlightedContent = useMemo(() => {
|
|
44
|
+
let html = note.content
|
|
45
|
+
.replace(/&/g, "&")
|
|
46
|
+
.replace(/</g, "<")
|
|
47
|
+
.replace(/>/g, ">");
|
|
48
|
+
|
|
49
|
+
html = html.replace(/(@[\w-]+)/g, '<span class="text-blue-400 font-medium">$1</span>');
|
|
50
|
+
html = html.replace(/(!high|!medium|!low)/g, '<span class="text-red-400 font-medium">$1</span>');
|
|
51
|
+
html = html.replace(/(\+\w+)/g, '<span class="text-amber-400 font-medium">$1</span>');
|
|
52
|
+
html = html.replace(/(<[\w.-]+>)/g, '<span class="text-emerald-400 font-medium">$1</span>');
|
|
53
|
+
html = html.replace(/\[x\]/g, '<span class="text-emerald-400">[x]</span>');
|
|
54
|
+
html = html.replace(/\[ \]/g, '<span class="text-zinc-400">[ ]</span>');
|
|
55
|
+
html = html.replace(/\n/g, '<br>');
|
|
56
|
+
|
|
57
|
+
return html;
|
|
58
|
+
}, [note.content]);
|
|
59
|
+
|
|
60
|
+
const lineCount = note.content.split("\n").length;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex h-full flex-col overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950">
|
|
64
|
+
<div className="border-b border-zinc-800 px-4 py-2.5">
|
|
65
|
+
<p className="text-[11px] uppercase tracking-wider text-zinc-600">{note.folder}</p>
|
|
66
|
+
<h3 className="text-sm font-semibold text-zinc-100">{note.title}</h3>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="flex flex-1 overflow-y-auto px-4 py-4">
|
|
70
|
+
<div className="relative shrink-0 border-r border-zinc-800/60 pr-3" style={{ width: 20 }}>
|
|
71
|
+
{Array.from({ length: lineCount }).map((_, i) => {
|
|
72
|
+
const taskInfo = tasksByLine[i];
|
|
73
|
+
return (
|
|
74
|
+
<div key={i} className="flex items-center justify-end" style={{ height: 24 }}>
|
|
75
|
+
{taskInfo && <StatusDot status={taskInfo.status} />}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div
|
|
82
|
+
className="flex-1 pl-3 text-sm leading-6 text-zinc-300 font-mono whitespace-pre-wrap break-words"
|
|
83
|
+
dangerouslySetInnerHTML={{ __html: highlightedContent }}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { useIsMobile } from "@/lib/use-is-mobile";
|
|
6
|
+
import { MobileTopBar } from "./mobile-top-bar";
|
|
7
|
+
import { BottomNav } from "./bottom-nav";
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
panel: React.ReactNode;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function AppShell({ panel, children }: Props) {
|
|
15
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
16
|
+
const [mounted, setMounted] = useState(false);
|
|
17
|
+
const isMobile = useIsMobile();
|
|
18
|
+
const pathname = usePathname();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setDrawerOpen(false);
|
|
22
|
+
}, [pathname]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
// On mobile (after mount) we animate the panel with an inline transform —
|
|
29
|
+
// reliable across Tailwind v4's translate-var handling, and SSR-flash-safe
|
|
30
|
+
// because before mount we fall back to the visibility classes below.
|
|
31
|
+
const useInline = mounted && isMobile;
|
|
32
|
+
const panelStyle = useInline
|
|
33
|
+
? {
|
|
34
|
+
transform: drawerOpen ? "translateX(0)" : "translateX(-100%)",
|
|
35
|
+
opacity: drawerOpen ? 1 : 0,
|
|
36
|
+
}
|
|
37
|
+
: undefined;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex min-w-0 flex-1 min-h-0">
|
|
41
|
+
{/* Backdrop — fades in/out on mobile when the drawer is open */}
|
|
42
|
+
<div
|
|
43
|
+
aria-hidden
|
|
44
|
+
onClick={() => setDrawerOpen(false)}
|
|
45
|
+
className={`fixed inset-0 z-40 bg-black/60 transition-opacity duration-200 md:hidden ${
|
|
46
|
+
drawerOpen ? "opacity-100" : "pointer-events-none opacity-0"
|
|
47
|
+
}`}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
{/*
|
|
51
|
+
Notes panel.
|
|
52
|
+
Desktop: relative sidebar, always visible.
|
|
53
|
+
Mobile: fixed overlay that slides + fades in from the left.
|
|
54
|
+
*/}
|
|
55
|
+
<div
|
|
56
|
+
style={panelStyle}
|
|
57
|
+
aria-hidden={useInline && !drawerOpen}
|
|
58
|
+
className={[
|
|
59
|
+
"fixed inset-y-0 left-0 z-50 flex w-[272px] flex-col transition-[transform,opacity] duration-200 ease-out",
|
|
60
|
+
"md:relative md:inset-y-auto md:left-auto md:z-auto md:w-[200px] md:transition-none",
|
|
61
|
+
useInline
|
|
62
|
+
? drawerOpen
|
|
63
|
+
? "pointer-events-auto"
|
|
64
|
+
: "pointer-events-none"
|
|
65
|
+
: drawerOpen
|
|
66
|
+
? "visible pointer-events-auto"
|
|
67
|
+
: "invisible pointer-events-none md:visible md:pointer-events-auto",
|
|
68
|
+
].join(" ")}
|
|
69
|
+
>
|
|
70
|
+
{panel}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Main area */}
|
|
74
|
+
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
75
|
+
<MobileTopBar onMenuClick={() => setDrawerOpen(true)} />
|
|
76
|
+
<main className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
77
|
+
{children}
|
|
78
|
+
</main>
|
|
79
|
+
<BottomNav />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { shouldRunScheduledBackup } from "@/lib/backup/backup-runner";
|
|
5
|
+
import { useIsElectron } from "@/lib/use-is-electron";
|
|
6
|
+
import type { BackupCadence } from "@/lib/backup/types";
|
|
7
|
+
|
|
8
|
+
type BackupSettingsResponse = {
|
|
9
|
+
workspace?: {
|
|
10
|
+
isCloud: boolean;
|
|
11
|
+
};
|
|
12
|
+
settings?: {
|
|
13
|
+
scheduleEnabled: boolean;
|
|
14
|
+
scheduleCadence: BackupCadence;
|
|
15
|
+
destinationPath: string | null;
|
|
16
|
+
includeMarkdownZip: boolean;
|
|
17
|
+
lastBackupAt: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ElectronBackupFile = {
|
|
22
|
+
name: string;
|
|
23
|
+
bytes: number[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function BackupScheduler() {
|
|
27
|
+
const isElectron = useIsElectron();
|
|
28
|
+
const runningRef = useRef(false);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!isElectron) return;
|
|
32
|
+
|
|
33
|
+
const runIfDue = async () => {
|
|
34
|
+
if (runningRef.current) return;
|
|
35
|
+
runningRef.current = true;
|
|
36
|
+
|
|
37
|
+
let settingsData: BackupSettingsResponse | null = null;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const settingsRes = await fetch("/api/backup/settings", { cache: "no-store" });
|
|
41
|
+
if (!settingsRes.ok) return;
|
|
42
|
+
|
|
43
|
+
settingsData = (await settingsRes.json()) as BackupSettingsResponse;
|
|
44
|
+
const settings = settingsData.settings;
|
|
45
|
+
if (!settings) return;
|
|
46
|
+
|
|
47
|
+
const shouldRun = shouldRunScheduledBackup({
|
|
48
|
+
isElectron,
|
|
49
|
+
isCloudWorkspace: settingsData.workspace?.isCloud ?? false,
|
|
50
|
+
scheduleEnabled: settings.scheduleEnabled,
|
|
51
|
+
destinationPath: settings.destinationPath,
|
|
52
|
+
cadence: settings.scheduleCadence,
|
|
53
|
+
lastBackupAt: settings.lastBackupAt ? new Date(settings.lastBackupAt) : null,
|
|
54
|
+
});
|
|
55
|
+
if (!shouldRun || !settings.destinationPath) return;
|
|
56
|
+
|
|
57
|
+
const files: ElectronBackupFile[] = [];
|
|
58
|
+
|
|
59
|
+
const dbResponse = await fetch("/api/backup/download/db");
|
|
60
|
+
if (!dbResponse.ok) {
|
|
61
|
+
throw new Error("Failed to download database backup.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dbBytes = new Uint8Array(await dbResponse.arrayBuffer());
|
|
65
|
+
files.push({
|
|
66
|
+
name: dbResponse.headers.get("x-brief-filename") ?? "brief-backup.json",
|
|
67
|
+
bytes: Array.from(dbBytes),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (settings.includeMarkdownZip) {
|
|
71
|
+
const notesResponse = await fetch("/api/backup/download/notes");
|
|
72
|
+
if (!notesResponse.ok) {
|
|
73
|
+
throw new Error("Failed to download notes ZIP.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const notesBytes = new Uint8Array(await notesResponse.arrayBuffer());
|
|
77
|
+
files.push({
|
|
78
|
+
name: notesResponse.headers.get("x-brief-filename") ?? "brief-notes.zip",
|
|
79
|
+
bytes: Array.from(notesBytes),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await window.electronAPI.writeBackupFiles(settings.destinationPath, files);
|
|
84
|
+
|
|
85
|
+
await fetch("/api/backup/settings", {
|
|
86
|
+
method: "PUT",
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
...settings,
|
|
90
|
+
lastBackupAt: new Date().toISOString(),
|
|
91
|
+
lastBackupStatus: "success",
|
|
92
|
+
lastBackupError: null,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (settingsData?.settings) {
|
|
97
|
+
await fetch("/api/backup/settings", {
|
|
98
|
+
method: "PUT",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
...settingsData.settings,
|
|
102
|
+
lastBackupStatus: "error",
|
|
103
|
+
lastBackupError:
|
|
104
|
+
error instanceof Error ? error.message : "Scheduled backup failed.",
|
|
105
|
+
}),
|
|
106
|
+
}).catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
runningRef.current = false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
void runIfDue();
|
|
114
|
+
const timer = window.setInterval(() => {
|
|
115
|
+
void runIfDue();
|
|
116
|
+
}, 5 * 60 * 1000);
|
|
117
|
+
|
|
118
|
+
return () => window.clearInterval(timer);
|
|
119
|
+
}, [isElectron]);
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { FileText, List, Kanban, Calendar, Network, Settings } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
const NAV_ITEMS = [
|
|
8
|
+
{ href: "/notes", icon: FileText, label: "Notes" },
|
|
9
|
+
{ href: "/list", icon: List, label: "List" },
|
|
10
|
+
{ href: "/kanban", icon: Kanban, label: "Kanban" },
|
|
11
|
+
{ href: "/calendar", icon: Calendar, label: "Calendar" },
|
|
12
|
+
{ href: "/graph", icon: Network, label: "Graph" },
|
|
13
|
+
{ href: "/settings", icon: Settings, label: "Settings" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function BottomNav() {
|
|
17
|
+
const pathname = usePathname();
|
|
18
|
+
|
|
19
|
+
function isActive(href: string) {
|
|
20
|
+
if (href === "/notes") return pathname === "/" || pathname.startsWith("/notes");
|
|
21
|
+
return pathname.startsWith(href);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<nav className="flex md:hidden items-stretch border-t border-zinc-800 bg-zinc-950" style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)" }}>
|
|
26
|
+
{NAV_ITEMS.map(({ href, icon: Icon, label }) => {
|
|
27
|
+
const active = isActive(href);
|
|
28
|
+
return (
|
|
29
|
+
<Link
|
|
30
|
+
key={href}
|
|
31
|
+
href={href}
|
|
32
|
+
className={`flex flex-1 flex-col items-center justify-center gap-0.5 py-2.5 text-[10px] font-medium transition-colors min-h-[56px] ${
|
|
33
|
+
active ? "text-zinc-100" : "text-zinc-500 hover:text-zinc-300"
|
|
34
|
+
}`}
|
|
35
|
+
>
|
|
36
|
+
<Icon size={20} strokeWidth={active ? 2.5 : 1.75} />
|
|
37
|
+
<span>{label}</span>
|
|
38
|
+
</Link>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</nav>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { IconBar } from "@/components/layout/icon-bar";
|
|
5
|
+
|
|
6
|
+
vi.mock("next/navigation", () => ({
|
|
7
|
+
usePathname: () => "/calendar",
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("next-auth/react", () => ({
|
|
11
|
+
signOut: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@/components/layout/notification-bell", () => ({
|
|
15
|
+
NotificationBell: () => <div>Bell</div>,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/components/command/command-palette", () => ({
|
|
19
|
+
useCommandPalette: () => ({ open: vi.fn() }),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("IconBar", () => {
|
|
23
|
+
it("renders the active module as icon-only navigation", () => {
|
|
24
|
+
render(<IconBar latestNoteId="n1" />);
|
|
25
|
+
|
|
26
|
+
expect(screen.getByLabelText("Calendar")).toBeInTheDocument();
|
|
27
|
+
expect(screen.queryByText("Calendar")).not.toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
FileText,
|
|
7
|
+
Kanban,
|
|
8
|
+
List,
|
|
9
|
+
Calendar,
|
|
10
|
+
Network,
|
|
11
|
+
Settings,
|
|
12
|
+
LogOut,
|
|
13
|
+
Search,
|
|
14
|
+
BookOpen,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { signOut } from "next-auth/react";
|
|
17
|
+
import { AppLogo } from "@/components/branding/app-logo";
|
|
18
|
+
import { NotificationBell } from "@/components/layout/notification-bell";
|
|
19
|
+
import { FeedbackPopup } from "@/components/feedback/feedback-popup";
|
|
20
|
+
import { useCommandPalette } from "@/components/command/command-palette";
|
|
21
|
+
|
|
22
|
+
const staticNavItems = [
|
|
23
|
+
{ href: "/kanban", icon: Kanban, label: "Kanban" },
|
|
24
|
+
{ href: "/list", icon: List, label: "List" },
|
|
25
|
+
{ href: "/calendar", icon: Calendar, label: "Calendar" },
|
|
26
|
+
{ href: "/graph", icon: Network, label: "Graph" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
type IconBarProps = { latestNoteId?: string };
|
|
30
|
+
|
|
31
|
+
export function IconBar({ latestNoteId }: IconBarProps) {
|
|
32
|
+
const pathname = usePathname();
|
|
33
|
+
const palette = useCommandPalette();
|
|
34
|
+
|
|
35
|
+
function isActive(href: string) {
|
|
36
|
+
if (href.startsWith("/notes")) return pathname === "/" || pathname.startsWith("/notes");
|
|
37
|
+
return pathname.startsWith(href);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const notesHref = latestNoteId ? `/notes/${latestNoteId}` : "/notes";
|
|
41
|
+
const navLinkClassName = (active: boolean) =>
|
|
42
|
+
`flex h-10 w-10 self-center items-center justify-center rounded-md transition-colors ring-focus ${
|
|
43
|
+
active
|
|
44
|
+
? "bg-zinc-800 text-zinc-100"
|
|
45
|
+
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
|
|
46
|
+
}`;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<aside className="hidden md:flex w-14 flex-col border-r border-zinc-800 bg-zinc-950 px-2 py-3">
|
|
50
|
+
<nav className="flex flex-1 flex-col items-stretch gap-1">
|
|
51
|
+
<Link
|
|
52
|
+
href={notesHref}
|
|
53
|
+
title="Knotpad"
|
|
54
|
+
aria-label="Knotpad"
|
|
55
|
+
className="mb-2 flex h-10 w-10 self-center items-center justify-center rounded-md transition-colors ring-focus hover:bg-zinc-800"
|
|
56
|
+
>
|
|
57
|
+
<AppLogo variant="icon" className="h-6 w-6 rounded-md" />
|
|
58
|
+
</Link>
|
|
59
|
+
<button
|
|
60
|
+
onClick={palette.open}
|
|
61
|
+
title="Search (⌘K)"
|
|
62
|
+
aria-label="Search"
|
|
63
|
+
className="mb-1 flex h-10 w-10 self-center items-center justify-center rounded-md text-zinc-500 transition-colors ring-focus hover:bg-zinc-800 hover:text-zinc-300"
|
|
64
|
+
>
|
|
65
|
+
<Search size={18} />
|
|
66
|
+
</button>
|
|
67
|
+
<Link
|
|
68
|
+
href={notesHref}
|
|
69
|
+
title="Notes"
|
|
70
|
+
aria-label="Notes"
|
|
71
|
+
className={navLinkClassName(isActive(notesHref))}
|
|
72
|
+
>
|
|
73
|
+
<FileText size={18} />
|
|
74
|
+
</Link>
|
|
75
|
+
{staticNavItems.map(({ href, icon: Icon, label }) => (
|
|
76
|
+
<Link
|
|
77
|
+
key={href}
|
|
78
|
+
href={href}
|
|
79
|
+
title={label}
|
|
80
|
+
aria-label={label}
|
|
81
|
+
className={navLinkClassName(isActive(href))}
|
|
82
|
+
>
|
|
83
|
+
<Icon size={18} />
|
|
84
|
+
</Link>
|
|
85
|
+
))}
|
|
86
|
+
</nav>
|
|
87
|
+
|
|
88
|
+
<div className="flex flex-col items-stretch gap-1">
|
|
89
|
+
<NotificationBell />
|
|
90
|
+
<FeedbackPopup />
|
|
91
|
+
<Link
|
|
92
|
+
href="/guide"
|
|
93
|
+
title="User Guide"
|
|
94
|
+
aria-label="User Guide"
|
|
95
|
+
className={navLinkClassName(isActive("/guide"))}
|
|
96
|
+
>
|
|
97
|
+
<BookOpen size={18} />
|
|
98
|
+
</Link>
|
|
99
|
+
<Link
|
|
100
|
+
href="/settings"
|
|
101
|
+
title="Settings"
|
|
102
|
+
aria-label="Settings"
|
|
103
|
+
className={navLinkClassName(isActive("/settings"))}
|
|
104
|
+
>
|
|
105
|
+
<Settings size={18} />
|
|
106
|
+
</Link>
|
|
107
|
+
<button
|
|
108
|
+
onClick={() => signOut({ callbackUrl: `${window.location.origin}/login` })}
|
|
109
|
+
title="Sign out"
|
|
110
|
+
aria-label="Sign out"
|
|
111
|
+
className="flex h-10 w-10 self-center items-center justify-center rounded-md text-zinc-500 transition-colors ring-focus hover:bg-zinc-800 hover:text-zinc-300"
|
|
112
|
+
>
|
|
113
|
+
<LogOut size={18} />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</aside>
|
|
117
|
+
);
|
|
118
|
+
}
|