From de5ebbc4422b458c685a653b8b8fbaac6e6af5f4 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sun, 23 Nov 2025 11:02:27 +0000
Subject: feat: Add notes feature to highlights (#2154)

* feat: Add note-taking functionality to highlights

This commit adds the ability to add and edit notes on highlights, allowing users to capture their thoughts alongside highlighted text.

Changes:
- Updated zUpdateHighlightSchema to include optional note field
- Modified Highlight.update() method to persist notes to database
- Added note button (MessageSquare icon) beside color picker in highlight menu
- Implemented note dialog with textarea for adding/editing notes
- Updated Highlight interface to include optional note field
- Modified ReaderView to pass notes through create and update operations
- Enhanced HighlightCard to display notes below highlighted text

The note button appears alongside the color picker for both new and existing highlights. Notes are displayed in a smaller, muted font below the highlighted text in the HighlightCard component.

* refactor: Convert highlight UI to inline form

Changed the highlight creation/editing interface from a modal dialog to an inline form for a more streamlined user experience.

Changes:
- Replaced ColorPickerMenu and Dialog with a unified HighlightForm component
- Form now displays color selector and note textarea together inline
- Added labels for "Color" and "Note" sections
- Textarea is now optional and inline (placeholder: "Add a note (optional)...")
- Consolidated Save and Cancel buttons in the form
- Delete button appears only for existing highlights
- Removed MessageSquare icon - notes are now always visible in the form
- Simplified state management by removing separate dialog state

The form appears in a popover when text is selected or an existing highlight is clicked, showing both color options and note input in a single 320px wide form.

* fixes

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 .../dashboard/preview/BookmarkHtmlHighlighter.tsx  | 129 ++++++++++++++-------
 1 file changed, 86 insertions(+), 43 deletions(-)

(limited to 'apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx')

diff --git a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx
index e0f20ea2..63098ac0 100644
--- a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx
+++ b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
 import { ActionButton } from "@/components/ui/action-button";
 import { Button } from "@/components/ui/button";
 import { Popover, PopoverContent } from "@/components/ui/popover";
+import { Textarea } from "@/components/ui/textarea";
 import { cn } from "@/lib/utils";
 import { PopoverAnchor } from "@radix-ui/react-popover";
 import { Check, Trash2 } from "lucide-react";
@@ -13,23 +14,38 @@ import {
 
 import { HIGHLIGHT_COLOR_MAP } from "./highlights";
 
-interface ColorPickerMenuProps {
+interface HighlightFormProps {
   position: { x: number; y: number } | null;
-  onColorSelect: (color: ZHighlightColor) => void;
-  onDelete?: () => void;
   selectedHighlight: Highlight | null;
   onClose: () => void;
+  onSave: (color: ZHighlightColor, note: string | null) => void;
+  onDelete?: () => void;
   isMobile: boolean;
 }
 
-const ColorPickerMenu: React.FC<ColorPickerMenuProps> = ({
+const HighlightForm: React.FC<HighlightFormProps> = ({
   position,
-  onColorSelect,
-  onDelete,
   selectedHighlight,
   onClose,
+  onSave,
+  onDelete,
   isMobile,
 }) => {
+  const [selectedColor, setSelectedColor] = useState<ZHighlightColor>(
+    selectedHighlight?.color || "yellow",
+  );
+  const [noteText, setNoteText] = useState(selectedHighlight?.note || "");
+
+  // Update state when selectedHighlight changes
+  useEffect(() => {
+    setSelectedColor(selectedHighlight?.color || "yellow");
+    setNoteText(selectedHighlight?.note || "");
+  }, [selectedHighlight]);
+
+  const handleSave = () => {
+    onSave(selectedColor, noteText || null);
+  };
+
   return (
     <Popover
       open={position !== null}
@@ -48,35 +64,59 @@ const ColorPickerMenu: React.FC<ColorPickerMenuProps> = ({
       />
       <PopoverContent
         side={isMobile ? "bottom" : "top"}
-        className="flex w-fit items-center gap-1 p-2"
+        className="w-80 space-y-3 p-3"
       >
-        {SUPPORTED_HIGHLIGHT_COLORS.map((color) => (
-          <Button
-            size="none"
-            key={color}
-            onClick={() => onColorSelect(color)}
-            variant="none"
-            className={cn(
-              `size-8 rounded-full hover:border focus-visible:ring-0`,
-              HIGHLIGHT_COLOR_MAP.bg[color],
-            )}
-          >
-            {selectedHighlight?.color === color && (
-              <Check className="size-5 text-gray-600" />
-            )}
-          </Button>
-        ))}
-        {selectedHighlight && (
-          <ActionButton
-            loading={false}
-            size="none"
-            className="size-8 rounded-full"
-            onClick={onDelete}
-            variant="ghost"
-          >
-            <Trash2 className="size-5 text-destructive" />
-          </ActionButton>
-        )}
+        <div>
+          <label className="mb-2 block text-sm font-medium">Color</label>
+          <div className="flex items-center gap-1">
+            {SUPPORTED_HIGHLIGHT_COLORS.map((color) => (
+              <Button
+                size="none"
+                key={color}
+                onClick={() => setSelectedColor(color)}
+                variant="none"
+                className={cn(
+                  `size-8 rounded-full hover:border focus-visible:ring-0`,
+                  HIGHLIGHT_COLOR_MAP.bg[color],
+                )}
+              >
+                {selectedColor === color && (
+                  <Check className="size-5 text-gray-600" />
+                )}
+              </Button>
+            ))}
+          </div>
+        </div>
+        <div>
+          <label className="mb-2 block text-sm font-medium">Note</label>
+          <Textarea
+            placeholder="Add a note (optional)..."
+            value={noteText}
+            onChange={(e) => setNoteText(e.target.value)}
+            className="min-h-[80px] text-sm"
+          />
+        </div>
+        <div className="flex items-center justify-between gap-2">
+          <div className="flex gap-2">
+            <Button onClick={handleSave} size="sm">
+              Save
+            </Button>
+            <Button onClick={onClose} variant="outline" size="sm">
+              Cancel
+            </Button>
+          </div>
+          {selectedHighlight && onDelete && (
+            <ActionButton
+              loading={false}
+              size="sm"
+              onClick={onDelete}
+              variant="ghost"
+              title="Delete highlight"
+            >
+              <Trash2 className="size-4 text-destructive" />
+            </ActionButton>
+          )}
+        </div>
       </PopoverContent>
     </Popover>
   );
@@ -88,6 +128,7 @@ export interface Highlight {
   endOffset: number;
   color: ZHighlightColor;
   text: string | null;
+  note?: string | null;
 }
 
 interface HTMLHighlighterProps {
@@ -221,18 +262,20 @@ function BookmarkHTMLHighlighter({
     setPendingHighlight(createHighlightFromRange(range, "yellow"));
   };
 
-  const handleColorSelect = (color: ZHighlightColor) => {
+  const handleSave = (color: ZHighlightColor, note: string | null) => {
     if (pendingHighlight) {
       pendingHighlight.color = color;
+      pendingHighlight.note = note;
       onHighlight?.(pendingHighlight);
     } else if (selectedHighlight) {
       selectedHighlight.color = color;
+      selectedHighlight.note = note;
       onUpdateHighlight?.(selectedHighlight);
     }
-    closeColorPicker();
+    closeForm();
   };
 
-  const closeColorPicker = () => {
+  const closeForm = () => {
     setMenuPosition(null);
     setPendingHighlight(null);
     setSelectedHighlight(null);
@@ -242,7 +285,7 @@ function BookmarkHTMLHighlighter({
   const handleDelete = () => {
     if (selectedHighlight && onDeleteHighlight) {
       onDeleteHighlight(selectedHighlight);
-      closeColorPicker();
+      closeForm();
     }
   };
 
@@ -355,12 +398,12 @@ function BookmarkHTMLHighlighter({
         className={className}
         style={style}
       />
-      <ColorPickerMenu
+      <HighlightForm
         position={menuPosition}
-        onColorSelect={handleColorSelect}
-        onDelete={handleDelete}
-        selectedHighlight={selectedHighlight}
-        onClose={closeColorPicker}
+        selectedHighlight={selectedHighlight || pendingHighlight}
+        onClose={closeForm}
+        onSave={handleSave}
+        onDelete={selectedHighlight ? handleDelete : undefined}
         isMobile={isMobile}
       />
     </div>
-- 
cgit v1.2.3-70-g09d2

