@knotpad/app 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +224 -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 +49 -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-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +4 -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,484 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Brackets, Parentheses, AtSign, CalendarDays, Flag,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import type { Member, NoteRef, TaskRef, SlashCommandId, PriorityLevel } from "./note-types";
|
|
8
|
+
import { SLASH_COMMANDS, TEMPLATES, PRIORITY_LEVELS } from "./note-types";
|
|
9
|
+
|
|
10
|
+
// Shared type for optional position — when absent, renders in bottom-sheet mode.
|
|
11
|
+
type DropdownPos = { top: number; left: number };
|
|
12
|
+
|
|
13
|
+
// Touch helpers: distinguish a tap (select) from a scroll-drag (let it scroll).
|
|
14
|
+
// onTouchStart records the start position without preventDefault, so native
|
|
15
|
+
// scrolling still works; onTouchEnd only fires the pick if the touch didn't move.
|
|
16
|
+
const TAP_MOVE_THRESHOLD = 10;
|
|
17
|
+
|
|
18
|
+
function useTapHandlers<T>(onPick: (value: T) => void) {
|
|
19
|
+
const startPos = useRef<{ x: number; y: number } | null>(null);
|
|
20
|
+
|
|
21
|
+
return (value: T) => ({
|
|
22
|
+
onMouseDown: (e: React.MouseEvent) => { e.preventDefault(); onPick(value); },
|
|
23
|
+
onTouchStart: (e: React.TouchEvent) => {
|
|
24
|
+
const t = e.touches[0];
|
|
25
|
+
startPos.current = { x: t.clientX, y: t.clientY };
|
|
26
|
+
},
|
|
27
|
+
onTouchMove: (e: React.TouchEvent) => {
|
|
28
|
+
if (!startPos.current) return;
|
|
29
|
+
const t = e.touches[0];
|
|
30
|
+
const dx = Math.abs(t.clientX - startPos.current.x);
|
|
31
|
+
const dy = Math.abs(t.clientY - startPos.current.y);
|
|
32
|
+
if (dx > TAP_MOVE_THRESHOLD || dy > TAP_MOVE_THRESHOLD) startPos.current = null;
|
|
33
|
+
},
|
|
34
|
+
onTouchEnd: (e: React.TouchEvent) => {
|
|
35
|
+
if (!startPos.current) return;
|
|
36
|
+
startPos.current = null;
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
onPick(value);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Slash command palette ────────────────────────────────────────────────────
|
|
44
|
+
export function SlashPalette({
|
|
45
|
+
suggestions,
|
|
46
|
+
activeIndex,
|
|
47
|
+
pos,
|
|
48
|
+
onPick,
|
|
49
|
+
}: {
|
|
50
|
+
suggestions: typeof SLASH_COMMANDS;
|
|
51
|
+
activeIndex: number;
|
|
52
|
+
pos?: DropdownPos;
|
|
53
|
+
onPick: (id: SlashCommandId) => void;
|
|
54
|
+
}) {
|
|
55
|
+
const tapHandlers = useTapHandlers(onPick);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={pos
|
|
60
|
+
? "absolute z-30 w-72 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
61
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
62
|
+
}
|
|
63
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
64
|
+
>
|
|
65
|
+
<div className="border-b border-zinc-800 px-3 py-1.5">
|
|
66
|
+
<span className="text-xs text-zinc-600">Commands · ↑↓ navigate · Enter to insert</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="max-h-64 overflow-y-auto">
|
|
69
|
+
{suggestions.map((cmd, idx) => (
|
|
70
|
+
<button
|
|
71
|
+
key={cmd.id}
|
|
72
|
+
{...tapHandlers(cmd.id)}
|
|
73
|
+
className={`flex w-full items-center gap-3 px-3 py-2 text-sm text-left transition-colors ${
|
|
74
|
+
idx === activeIndex ? "bg-zinc-700 text-zinc-100" : "text-zinc-300 hover:bg-zinc-800"
|
|
75
|
+
}`}
|
|
76
|
+
>
|
|
77
|
+
<span className="flex w-6 shrink-0 items-center justify-center text-xs font-mono text-zinc-500">
|
|
78
|
+
{cmd.icon}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="font-medium">{cmd.label}</span>
|
|
81
|
+
<span className="ml-auto text-xs text-zinc-600 truncate">{cmd.hint}</span>
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── @mention dropdown ────────────────────────────────────────────────────────
|
|
90
|
+
export function MentionDropdown({
|
|
91
|
+
suggestions,
|
|
92
|
+
activeIndex,
|
|
93
|
+
pos,
|
|
94
|
+
onPick,
|
|
95
|
+
}: {
|
|
96
|
+
suggestions: Member[];
|
|
97
|
+
activeIndex: number;
|
|
98
|
+
pos?: DropdownPos;
|
|
99
|
+
onPick: (handle: string) => void;
|
|
100
|
+
}) {
|
|
101
|
+
const tapHandlers = useTapHandlers(onPick);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
className={pos
|
|
106
|
+
? "absolute z-30 w-64 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
107
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
108
|
+
}
|
|
109
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
110
|
+
>
|
|
111
|
+
<div className="max-h-64 overflow-y-auto">
|
|
112
|
+
{suggestions.map((s, idx) => (
|
|
113
|
+
<button
|
|
114
|
+
key={s.handle}
|
|
115
|
+
{...tapHandlers(s.handle)}
|
|
116
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
|
|
117
|
+
idx === activeIndex ? "bg-zinc-700 text-zinc-100" : "text-zinc-300 hover:bg-zinc-800"
|
|
118
|
+
}`}
|
|
119
|
+
>
|
|
120
|
+
<span className={s.isAgent ? "text-blue-400" : "text-zinc-400"}>{s.handle}</span>
|
|
121
|
+
{s.isAgent ? (
|
|
122
|
+
<span className="text-xs text-zinc-600 ml-auto">agent</span>
|
|
123
|
+
) : (
|
|
124
|
+
<span className="text-xs text-zinc-500 truncate">{s.label}</span>
|
|
125
|
+
)}
|
|
126
|
+
</button>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── !priority picker ─────────────────────────────────────────────────────────
|
|
134
|
+
export function PriorityPickerDropdown({
|
|
135
|
+
suggestions,
|
|
136
|
+
activeIndex,
|
|
137
|
+
pos,
|
|
138
|
+
onPick,
|
|
139
|
+
}: {
|
|
140
|
+
suggestions: typeof PRIORITY_LEVELS;
|
|
141
|
+
activeIndex: number;
|
|
142
|
+
pos?: DropdownPos;
|
|
143
|
+
onPick: (level: PriorityLevel) => void;
|
|
144
|
+
}) {
|
|
145
|
+
const tapHandlers = useTapHandlers(onPick);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
className={pos
|
|
150
|
+
? "absolute z-30 w-52 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
151
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
152
|
+
}
|
|
153
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
154
|
+
>
|
|
155
|
+
<div className="border-b border-zinc-800 px-3 py-1.5">
|
|
156
|
+
<span className="text-xs text-zinc-600">! Set priority · ↑↓ navigate · Enter to insert</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="max-h-64 overflow-y-auto">
|
|
159
|
+
{suggestions.map((level, idx) => (
|
|
160
|
+
<button
|
|
161
|
+
key={level.id}
|
|
162
|
+
{...tapHandlers(level.id)}
|
|
163
|
+
className={`flex w-full items-center gap-3 px-3 py-2 text-sm text-left transition-colors ${
|
|
164
|
+
idx === activeIndex ? "bg-zinc-700 text-zinc-100" : "text-zinc-300 hover:bg-zinc-800"
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
<Flag size={14} className={level.color} />
|
|
168
|
+
<span>{level.label}</span>
|
|
169
|
+
</button>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── [[note]] link picker ─────────────────────────────────────────────────────
|
|
177
|
+
export function NotePickerDropdown({
|
|
178
|
+
suggestions,
|
|
179
|
+
activeIndex,
|
|
180
|
+
pos,
|
|
181
|
+
onPick,
|
|
182
|
+
}: {
|
|
183
|
+
suggestions: NoteRef[];
|
|
184
|
+
activeIndex: number;
|
|
185
|
+
pos?: DropdownPos;
|
|
186
|
+
onPick: (n: NoteRef) => void;
|
|
187
|
+
}) {
|
|
188
|
+
const tapHandlers = useTapHandlers(onPick);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div
|
|
192
|
+
className={pos
|
|
193
|
+
? "absolute z-30 w-72 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
194
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
195
|
+
}
|
|
196
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
197
|
+
>
|
|
198
|
+
<div className="border-b border-zinc-800 px-3 py-1.5">
|
|
199
|
+
<span className="text-xs text-zinc-600">[[ Link note · ↑↓ navigate · Enter to insert</span>
|
|
200
|
+
</div>
|
|
201
|
+
{suggestions.length === 0 ? (
|
|
202
|
+
<p className="px-3 py-2 text-xs text-zinc-600">No notes found</p>
|
|
203
|
+
) : (
|
|
204
|
+
<div className="max-h-64 overflow-y-auto">
|
|
205
|
+
{suggestions.map((n, idx) => (
|
|
206
|
+
<button
|
|
207
|
+
key={n.id}
|
|
208
|
+
{...tapHandlers(n)}
|
|
209
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
|
|
210
|
+
idx === activeIndex ? "bg-zinc-700 text-zinc-100" : "text-zinc-300 hover:bg-zinc-800"
|
|
211
|
+
}`}
|
|
212
|
+
>
|
|
213
|
+
<span className="text-violet-400">[[</span>
|
|
214
|
+
<span className="flex-1 truncate">{n.title}</span>
|
|
215
|
+
<span className="text-violet-400">]]</span>
|
|
216
|
+
</button>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── ((task)) reference picker ────────────────────────────────────────────────
|
|
225
|
+
export function TaskPickerDropdown({
|
|
226
|
+
suggestions,
|
|
227
|
+
activeIndex,
|
|
228
|
+
pos,
|
|
229
|
+
onPick,
|
|
230
|
+
}: {
|
|
231
|
+
suggestions: TaskRef[];
|
|
232
|
+
activeIndex: number;
|
|
233
|
+
pos?: DropdownPos;
|
|
234
|
+
onPick: (t: TaskRef) => void;
|
|
235
|
+
}) {
|
|
236
|
+
const tapHandlers = useTapHandlers(onPick);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
className={pos
|
|
241
|
+
? "absolute z-30 w-72 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
242
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
243
|
+
}
|
|
244
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
245
|
+
>
|
|
246
|
+
<div className="border-b border-zinc-800 px-3 py-1.5">
|
|
247
|
+
<span className="text-xs text-zinc-600">(( Link task · ↑↓ navigate · Enter to insert</span>
|
|
248
|
+
</div>
|
|
249
|
+
{suggestions.length === 0 ? (
|
|
250
|
+
<p className="px-3 py-2 text-xs text-zinc-600">No tasks found</p>
|
|
251
|
+
) : (
|
|
252
|
+
<div className="max-h-64 overflow-y-auto">
|
|
253
|
+
{suggestions.slice(0, 8).map((t, idx) => (
|
|
254
|
+
<button
|
|
255
|
+
key={t.id}
|
|
256
|
+
{...tapHandlers(t)}
|
|
257
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
|
|
258
|
+
idx === activeIndex ? "bg-zinc-700 text-zinc-100" : "text-zinc-300 hover:bg-zinc-800"
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
261
|
+
<span className="text-amber-400 shrink-0">((</span>
|
|
262
|
+
<span className="flex-1 truncate">{t.title}</span>
|
|
263
|
+
<span className="text-xs text-zinc-600 shrink-0 truncate max-w-[80px]">{t.noteTitle}</span>
|
|
264
|
+
<span className="text-amber-400 shrink-0">))</span>
|
|
265
|
+
</button>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── + quick-pick (note / task / person / date / priority) ────────────────────
|
|
274
|
+
export function PlusPickerDropdown({
|
|
275
|
+
pos,
|
|
276
|
+
onPick,
|
|
277
|
+
}: {
|
|
278
|
+
pos?: DropdownPos;
|
|
279
|
+
onPick: (option: "note" | "task" | "person" | "date" | "priority") => void;
|
|
280
|
+
}) {
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
className={pos
|
|
284
|
+
? "absolute z-30 w-52 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
285
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
286
|
+
}
|
|
287
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
288
|
+
>
|
|
289
|
+
<div className="border-b border-zinc-800 px-3 py-1.5">
|
|
290
|
+
<span className="text-xs text-zinc-600">Insert · press 1 / 2 / 3 / 4 / 5</span>
|
|
291
|
+
</div>
|
|
292
|
+
{([
|
|
293
|
+
{ key: "1", label: "Link note", icon: <Brackets size={14} />, option: "note" as const },
|
|
294
|
+
{ key: "2", label: "Link task", icon: <Parentheses size={14} />, option: "task" as const },
|
|
295
|
+
{ key: "3", label: "Mention", icon: <AtSign size={14} />, option: "person" as const },
|
|
296
|
+
{ key: "4", label: "Insert date", icon: <CalendarDays size={14} />, option: "date" as const },
|
|
297
|
+
{ key: "5", label: "Priority", icon: <Flag size={14} />, option: "priority" as const },
|
|
298
|
+
]).map(({ key, label, icon, option }) => (
|
|
299
|
+
<button
|
|
300
|
+
key={key}
|
|
301
|
+
onMouseDown={(e) => { e.preventDefault(); onPick(option); }}
|
|
302
|
+
onTouchStart={(e) => { e.preventDefault(); onPick(option); }}
|
|
303
|
+
className="flex w-full items-center gap-3 px-3 py-2 text-sm text-left text-zinc-300 hover:bg-zinc-800 transition-colors"
|
|
304
|
+
>
|
|
305
|
+
<span className="flex w-6 shrink-0 items-center justify-center text-zinc-400">{icon}</span>
|
|
306
|
+
<span>{label}</span>
|
|
307
|
+
<span className="ml-auto text-xs text-zinc-700">{key}</span>
|
|
308
|
+
</button>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Inline date / date-range picker ──────────────────────────────────────────
|
|
315
|
+
export function DatePickerDropdown({
|
|
316
|
+
pos,
|
|
317
|
+
startDate,
|
|
318
|
+
endDate,
|
|
319
|
+
onStartChange,
|
|
320
|
+
onEndChange,
|
|
321
|
+
onConfirm,
|
|
322
|
+
onCancel,
|
|
323
|
+
}: {
|
|
324
|
+
pos?: DropdownPos;
|
|
325
|
+
startDate: string;
|
|
326
|
+
endDate: string;
|
|
327
|
+
onStartChange: (v: string) => void;
|
|
328
|
+
onEndChange: (v: string) => void;
|
|
329
|
+
onConfirm: () => void;
|
|
330
|
+
onCancel: () => void;
|
|
331
|
+
}) {
|
|
332
|
+
return (
|
|
333
|
+
<div
|
|
334
|
+
className={pos
|
|
335
|
+
? "absolute z-30 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl p-3 flex flex-col gap-3 w-64"
|
|
336
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 p-4 flex flex-col gap-4"
|
|
337
|
+
}
|
|
338
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
339
|
+
>
|
|
340
|
+
<span className={pos ? "text-xs font-medium text-zinc-400" : "text-sm font-medium text-zinc-400"}>
|
|
341
|
+
Pick a date or range
|
|
342
|
+
</span>
|
|
343
|
+
|
|
344
|
+
<div className="flex flex-col gap-1.5">
|
|
345
|
+
<label className="text-[10px] text-zinc-600 uppercase tracking-wider">Start</label>
|
|
346
|
+
<input
|
|
347
|
+
autoFocus
|
|
348
|
+
type="date"
|
|
349
|
+
value={startDate}
|
|
350
|
+
onChange={(e) => {
|
|
351
|
+
onStartChange(e.target.value);
|
|
352
|
+
if (endDate && e.target.value > endDate) onEndChange("");
|
|
353
|
+
}}
|
|
354
|
+
onKeyDown={(e) => {
|
|
355
|
+
if (e.key === "Enter") onConfirm();
|
|
356
|
+
if (e.key === "Escape") onCancel();
|
|
357
|
+
}}
|
|
358
|
+
className={pos
|
|
359
|
+
? "rounded bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100 outline-none ring-1 ring-zinc-700 focus:ring-blue-500 [color-scheme:dark] w-full"
|
|
360
|
+
: "rounded bg-zinc-800 px-3 py-3 text-base text-zinc-100 outline-none ring-1 ring-zinc-700 focus:ring-blue-500 [color-scheme:dark] w-full"
|
|
361
|
+
}
|
|
362
|
+
/>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<div className="flex flex-col gap-1.5">
|
|
366
|
+
<label className="text-[10px] text-zinc-600 uppercase tracking-wider">
|
|
367
|
+
End <span className="text-zinc-700 normal-case">(optional — makes a range)</span>
|
|
368
|
+
</label>
|
|
369
|
+
<input
|
|
370
|
+
type="date"
|
|
371
|
+
value={endDate}
|
|
372
|
+
min={startDate}
|
|
373
|
+
onChange={(e) => onEndChange(e.target.value)}
|
|
374
|
+
onKeyDown={(e) => {
|
|
375
|
+
if (e.key === "Enter") onConfirm();
|
|
376
|
+
if (e.key === "Escape") onCancel();
|
|
377
|
+
}}
|
|
378
|
+
className={pos
|
|
379
|
+
? "rounded bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100 outline-none ring-1 ring-zinc-700 focus:ring-blue-500 [color-scheme:dark] w-full"
|
|
380
|
+
: "rounded bg-zinc-800 px-3 py-3 text-base text-zinc-100 outline-none ring-1 ring-zinc-700 focus:ring-blue-500 [color-scheme:dark] w-full"
|
|
381
|
+
}
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Preview */}
|
|
386
|
+
<p className={pos ? "text-[11px] text-zinc-500 font-mono" : "text-xs text-zinc-500 font-mono"}>
|
|
387
|
+
{startDate
|
|
388
|
+
? endDate && endDate !== startDate
|
|
389
|
+
? `${startDate}..${endDate}`
|
|
390
|
+
: startDate
|
|
391
|
+
: "—"}
|
|
392
|
+
</p>
|
|
393
|
+
|
|
394
|
+
<div className={pos ? "flex gap-2 justify-end pt-0.5" : "flex gap-3 pt-1"}>
|
|
395
|
+
<button
|
|
396
|
+
onMouseDown={(e) => { e.preventDefault(); onCancel(); }}
|
|
397
|
+
onTouchStart={(e) => { e.preventDefault(); onCancel(); }}
|
|
398
|
+
className={pos
|
|
399
|
+
? "text-xs text-zinc-600 hover:text-zinc-400"
|
|
400
|
+
: "flex-1 rounded bg-zinc-800 py-2.5 text-sm text-zinc-400 hover:bg-zinc-700 transition-colors"
|
|
401
|
+
}
|
|
402
|
+
>Cancel</button>
|
|
403
|
+
<button
|
|
404
|
+
onMouseDown={(e) => { e.preventDefault(); onConfirm(); }}
|
|
405
|
+
onTouchStart={(e) => { e.preventDefault(); onConfirm(); }}
|
|
406
|
+
disabled={!startDate}
|
|
407
|
+
className={pos
|
|
408
|
+
? "rounded bg-blue-600 px-2.5 py-1 text-xs text-white hover:bg-blue-500 disabled:opacity-40"
|
|
409
|
+
: "flex-1 rounded bg-blue-600 py-2.5 text-sm text-white hover:bg-blue-500 disabled:opacity-40 transition-colors"
|
|
410
|
+
}
|
|
411
|
+
>Insert</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── Template sub-picker ──────────────────────────────────────────────────────
|
|
418
|
+
export function TemplatePickerDropdown({
|
|
419
|
+
pos,
|
|
420
|
+
onPick,
|
|
421
|
+
}: {
|
|
422
|
+
pos?: DropdownPos;
|
|
423
|
+
onPick: (content: string) => void;
|
|
424
|
+
}) {
|
|
425
|
+
const tapHandlers = useTapHandlers(onPick);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div
|
|
429
|
+
className={pos
|
|
430
|
+
? "absolute z-30 w-56 rounded-lg border border-zinc-700 bg-zinc-900 shadow-xl overflow-hidden"
|
|
431
|
+
: "w-full rounded-lg border border-zinc-700 bg-zinc-900 overflow-hidden"
|
|
432
|
+
}
|
|
433
|
+
style={pos ? { top: pos.top, left: pos.left } : undefined}
|
|
434
|
+
>
|
|
435
|
+
<div className="border-b border-zinc-800 px-3 py-1.5">
|
|
436
|
+
<span className="text-xs text-zinc-600">Templates · click to insert</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div className="max-h-64 overflow-y-auto">
|
|
439
|
+
{TEMPLATES.map((t) => (
|
|
440
|
+
<button
|
|
441
|
+
key={t.id}
|
|
442
|
+
{...tapHandlers(t.content)}
|
|
443
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-left text-zinc-300 hover:bg-zinc-800 transition-colors"
|
|
444
|
+
>
|
|
445
|
+
<span className="text-zinc-500">📋</span>
|
|
446
|
+
<span>{t.label}</span>
|
|
447
|
+
</button>
|
|
448
|
+
))}
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── "Did you mean @name?" soft hint bar ──────────────────────────────────────
|
|
455
|
+
export function SoftHintBar({
|
|
456
|
+
handle,
|
|
457
|
+
onAccept,
|
|
458
|
+
onDismiss,
|
|
459
|
+
}: {
|
|
460
|
+
handle: string;
|
|
461
|
+
onAccept: () => void;
|
|
462
|
+
onDismiss: () => void;
|
|
463
|
+
}) {
|
|
464
|
+
return (
|
|
465
|
+
<div className="absolute z-30 bottom-2 left-0 flex items-center gap-2 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-xs shadow-lg">
|
|
466
|
+
<span className="text-zinc-500">Did you mean</span>
|
|
467
|
+
<button
|
|
468
|
+
onMouseDown={(e) => { e.preventDefault(); onAccept(); }}
|
|
469
|
+
onTouchStart={(e) => { e.preventDefault(); onAccept(); }}
|
|
470
|
+
className="text-blue-400 font-medium hover:text-blue-300"
|
|
471
|
+
>
|
|
472
|
+
{handle}
|
|
473
|
+
</button>
|
|
474
|
+
<span className="text-zinc-500">?</span>
|
|
475
|
+
<button
|
|
476
|
+
onMouseDown={(e) => { e.preventDefault(); onDismiss(); }}
|
|
477
|
+
onTouchStart={(e) => { e.preventDefault(); onDismiss(); }}
|
|
478
|
+
className="ml-1 text-zinc-600 hover:text-zinc-400"
|
|
479
|
+
>
|
|
480
|
+
✕
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|