From 098e56a8950efbef79e551e12622ae7c8cd90c03 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sat, 8 Nov 2025 18:52:30 +0000
Subject: feat(extension): Allow writing notes directly in the extension
 (#2104)

* feat(extension): add notes editor to bookmark hoarded screen

Adds the ability to directly add and edit notes for bookmarks in the browser extension's hoarded screen (the page shown after saving a bookmark).

Changes:
- Created Textarea UI component for the browser extension
- Created NoteEditor component that uses useUpdateBookmark hook
- Added Notes section to BookmarkSavedPage, displayed between the header and tags
- Notes auto-save when the user clicks away from the textarea (onBlur)
- Shows saving state and error messages to the user

This brings feature parity with the web app's notes functionality.

* add explicit button

* more fixes

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 .../src/components/NoteEditor.tsx                  | 105 +++++++++++++++++++++
 1 file changed, 105 insertions(+)
 create mode 100644 apps/browser-extension/src/components/NoteEditor.tsx

(limited to 'apps/browser-extension/src/components/NoteEditor.tsx')

diff --git a/apps/browser-extension/src/components/NoteEditor.tsx b/apps/browser-extension/src/components/NoteEditor.tsx
new file mode 100644
index 00000000..15f1515b
--- /dev/null
+++ b/apps/browser-extension/src/components/NoteEditor.tsx
@@ -0,0 +1,105 @@
+import { useEffect, useState } from "react";
+import { Check, Save } from "lucide-react";
+
+import {
+  useAutoRefreshingBookmarkQuery,
+  useUpdateBookmark,
+} from "@karakeep/shared-react/hooks/bookmarks";
+
+import { Button } from "./ui/button";
+import { Textarea } from "./ui/textarea";
+
+export function NoteEditor({ bookmarkId }: { bookmarkId: string }) {
+  const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });
+  const [error, setError] = useState<string | null>(null);
+  const [isSaving, setIsSaving] = useState(false);
+  const [noteValue, setNoteValue] = useState("");
+  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
+
+  // Update local state when bookmark changes, but only if there are no unsaved changes
+  // This prevents overwriting user's edits while they're typing
+  useEffect(() => {
+    if (bookmark && !hasUnsavedChanges) {
+      setNoteValue(bookmark.note ?? "");
+    }
+  }, [bookmark?.note, bookmark, hasUnsavedChanges]);
+
+  const updateBookmarkMutator = useUpdateBookmark({
+    onSuccess: () => {
+      setError(null);
+      setIsSaving(false);
+      setHasUnsavedChanges(false);
+    },
+    onError: (e) => {
+      setError(e.message || "Failed to save note");
+      setIsSaving(false);
+    },
+  });
+
+  const handleSave = () => {
+    if (!bookmark || noteValue === bookmark.note || isSaving) {
+      return;
+    }
+    setIsSaving(true);
+    setError(null);
+    updateBookmarkMutator.mutate({
+      bookmarkId: bookmark.id,
+      note: noteValue,
+    });
+  };
+
+  if (!bookmark) {
+    return null;
+  }
+
+  return (
+    <div className="flex flex-col gap-2">
+      <Textarea
+        className="h-32 w-full overflow-auto rounded bg-background p-2 text-sm text-gray-400 dark:text-gray-300"
+        value={noteValue}
+        placeholder="Write some notes ..."
+        onChange={(e) => {
+          setNoteValue(e.currentTarget.value);
+          setHasUnsavedChanges(e.currentTarget.value !== bookmark.note);
+        }}
+      />
+      <div className="flex items-center justify-between gap-2">
+        <div className="flex-1">
+          {isSaving && <p className="text-xs text-gray-500">Saving note...</p>}
+          {error && <p className="text-xs text-red-500">{error}</p>}
+          {!isSaving && !error && hasUnsavedChanges && (
+            <p className="text-xs text-amber-600 dark:text-amber-500">
+              Unsaved changes
+            </p>
+          )}
+        </div>
+        {hasUnsavedChanges && (
+          <Button
+            onClick={handleSave}
+            disabled={isSaving}
+            size="sm"
+            className="gap-1.5"
+          >
+            {isSaving ? (
+              <>
+                <Save className="h-3.5 w-3.5 animate-pulse" />
+                Saving...
+              </>
+            ) : (
+              <>
+                <Save className="h-3.5 w-3.5" />
+                Save Note
+              </>
+            )}
+          </Button>
+        )}
+        {!hasUnsavedChanges && !isSaving && noteValue && (
+          <div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-500">
+            <Check className="h-3.5 w-3.5" />
+            Saved
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

