aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-11-23 11:02:27 +0000
committerGitHub <noreply@github.com>2025-11-23 11:02:27 +0000
commitde5ebbc4422b458c685a653b8b8fbaac6e6af5f4 (patch)
tree59db718004d9de9f0df0ae61dcd6c08e2d0f9efd /apps/web/components
parent48ab8a194974e026104bb2566cf8abd693ba51c1 (diff)
downloadkarakeep-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/web/components')
-rw-r--r--apps/web/components/dashboard/highlights/HighlightCard.tsx21
-rw-r--r--apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx129
-rw-r--r--apps/web/components/dashboard/preview/ReaderView.tsx3
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,
})
}
/>