diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-11-23 11:02:27 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-23 11:02:27 +0000 |
| commit | de5ebbc4422b458c685a653b8b8fbaac6e6af5f4 (patch) | |
| tree | 59db718004d9de9f0df0ae61dcd6c08e2d0f9efd /apps | |
| parent | 48ab8a194974e026104bb2566cf8abd693ba51c1 (diff) | |
| download | karakeep-de5ebbc4422b458c685a653b8b8fbaac6e6af5f4.tar.zst | |
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>
Diffstat (limited to 'apps')
3 files changed, 105 insertions, 48 deletions
diff --git a/apps/web/components/dashboard/highlights/HighlightCard.tsx b/apps/web/components/dashboard/highlights/HighlightCard.tsx index 1bba0b47..51421e0f 100644 --- a/apps/web/components/dashboard/highlights/HighlightCard.tsx +++ b/apps/web/components/dashboard/highlights/HighlightCard.tsx @@ -44,16 +44,24 @@ export default function HighlightCard({ }); }; - const Wrapper = ({ children }: { children: React.ReactNode }) => + const Wrapper = ({ + className, + children, + }: { + className?: string; + children: React.ReactNode; + }) => clickable ? ( - <button onClick={onBookmarkClick}>{children}</button> + <button className={className} onClick={onBookmarkClick}> + {children} + </button> ) : ( - <div>{children}</div> + <div className={className}>{children}</div> ); return ( <div className={cn("flex items-center justify-between", className)}> - <Wrapper> + <Wrapper className="flex flex-col gap-2 text-left"> <blockquote cite={highlight.bookmarkId} className={cn( @@ -63,6 +71,11 @@ export default function HighlightCard({ > <p>{highlight.text}</p> </blockquote> + {highlight.note && ( + <span className="text-sm text-muted-foreground"> + {highlight.note} + </span> + )} </Wrapper> {!readOnly && ( <div className="flex gap-2"> 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> diff --git a/apps/web/components/dashboard/preview/ReaderView.tsx b/apps/web/components/dashboard/preview/ReaderView.tsx index 1974626a..f2f843ee 100644 --- a/apps/web/components/dashboard/preview/ReaderView.tsx +++ b/apps/web/components/dashboard/preview/ReaderView.tsx @@ -105,6 +105,7 @@ export default function ReaderView({ updateHighlight({ highlightId: h.id, color: h.color, + note: h.note, }) } onHighlight={(h) => @@ -114,7 +115,7 @@ export default function ReaderView({ color: h.color, bookmarkId, text: h.text, - note: null, + note: h.note ?? null, }) } /> |
