feat: floating bookmarks

add floating bookmarks
This commit is contained in:
2026-06-03 22:19:19 -05:00
parent 6b4c0c43f8
commit 054e7689bd
10 changed files with 263 additions and 12 deletions
@@ -0,0 +1,26 @@
/** Same bookmark ribbon as ReaderToolbar, for rail markers (use with rotate for horizontal). */
interface BookmarkIconProps {
size?: number;
color: string;
className?: string;
}
export function BookmarkIcon({ size = 24, color, className }: BookmarkIconProps) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 24 24"
fill={color}
stroke="rgba(255, 255, 255, 0.9)"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
);
}
@@ -0,0 +1,73 @@
import { useState } from "react";
import { useTranslation } from "react-i18n-lite";
import type { BookmarkRailItem } from "@/utils/bookmarkRailLayout";
import { truncateRailPreview } from "@/utils/bookmarkRailLayout";
import { normalizeHighlightColor } from "@/constants/bookmarkHighlightColors";
import { BookmarkIcon } from "./BookmarkIcon";
interface BookmarkReaderRailProps {
items: BookmarkRailItem[];
onGoToBookmark: (epubCfi: string) => void;
}
function RailMarker({
item,
onGoToBookmark,
}: {
item: BookmarkRailItem;
onGoToBookmark: (epubCfi: string) => void;
}) {
const { t } = useTranslation();
const [hovered, setHovered] = useState(false);
const color = normalizeHighlightColor(item.marker.highlight_color);
const previewText = truncateRailPreview(item.marker.location_text || "");
const notePreview = item.marker.content
? truncateRailPreview(item.marker.content, 80)
: "";
const ariaLabel = previewText
? t("reader.bookmarkRailPreview", { text: previewText })
: t("reader.bookmarkRailGoTo");
return (
<li className="reader-bookmark-marker-wrap">
<button
type="button"
className="reader-bookmark-marker"
onClick={() => onGoToBookmark(item.marker.epub_cfi)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
aria-label={ariaLabel}
title={previewText || undefined}
>
<BookmarkIcon size={28} color={color} className="reader-bookmark-marker-icon" />
</button>
{hovered && previewText && (
<div
className="reader-bookmark-preview"
style={{ borderLeftColor: color }}
role="tooltip"
>
<p className="reader-bookmark-preview-passage">{previewText}</p>
{notePreview && (
<p className="reader-bookmark-preview-note">{notePreview}</p>
)}
</div>
)}
</li>
);
}
export function BookmarkReaderRail({ items, onGoToBookmark }: BookmarkReaderRailProps) {
if (items.length === 0) return null;
return (
<ul className="reader-bookmark-strip" aria-label="Bookmarks">
{items.map((item) => (
<RailMarker key={item.marker.id} item={item} onGoToBookmark={onGoToBookmark} />
))}
</ul>
);
}