Archived
fix: cleanup
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
|
||||
interface MarkerPassageActionsProps {
|
||||
onGoToPassage: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function MarkerPassageActions({ onGoToPassage, onDelete }: MarkerPassageActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="annotation-actions">
|
||||
<button type="button" className="btn btn-sm" onClick={onGoToPassage}>
|
||||
{t("annotations.goToPassage")}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm btn-danger" onClick={onDelete}>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CollapsibleMarkerPassage,
|
||||
CollapsibleMarkerThought,
|
||||
} from "@/components/annotations/CollapsibleMarkerText";
|
||||
import { MarkerPassageActions } from "@/components/annotations/MarkerPassageActions";
|
||||
|
||||
interface MarkerThreadsViewProps {
|
||||
ebookIdFilter?: string;
|
||||
@@ -91,22 +92,10 @@ export function MarkerThreadsView({
|
||||
) : (
|
||||
<span className="annotation-kind-badge bookmark-badge">{t("annotations.bookmarkOnly")}</span>
|
||||
)}
|
||||
<div className="annotation-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={() => onGoToPassage(m.ebook_id, m.epub_cfi)}
|
||||
>
|
||||
{t("annotations.goToPassage")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => removeBookmark(m.id)}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</div>
|
||||
<MarkerPassageActions
|
||||
onGoToPassage={() => onGoToPassage(m.ebook_id, m.epub_cfi)}
|
||||
onDelete={() => removeBookmark(m.id)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { BookmarksNotesPage } from "./BookmarksNotesPage";
|
||||
export { MarkerThreadsView } from "./MarkerThreadsView";
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function Layout({
|
||||
children,
|
||||
title,
|
||||
}: LayoutProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const pageTitle = title ?? t("common.appName");
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<header className="app-header">
|
||||
<h1 className="app-title">{pageTitle}</h1>
|
||||
<nav className="app-nav">
|
||||
<a href="/" className="nav-link">{t("annotations.home")}</a>
|
||||
<a href="/bookmarks-notes" className="nav-link">{t("annotations.bookmarksNotes")}</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main className="app-main">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
|
||||
interface SimpleFormPageLayoutProps {
|
||||
title: string;
|
||||
onBack: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function SimpleFormPageLayout({ title, onBack, children }: SimpleFormPageLayoutProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div style={{ maxWidth: 500, margin: "0 auto", padding: 16, minHeight: "100vh", background: "#f8f9fa" }}>
|
||||
<header style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 24, padding: "16px 0", borderBottom: "1px solid #eee" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
style={{ padding: "8px 16px", borderRadius: 6, border: "1px solid #ddd", background: "#fff", cursor: "pointer", fontSize: 14 }}
|
||||
>
|
||||
← {t("common.back")}
|
||||
</button>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 700, color: "#1a1a2e", margin: 0 }}>{title}</h1>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { Layout } from "./Layout";
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import styles from "./FinishedBooksShelf.module.css";
|
||||
|
||||
export interface ShelfBook {
|
||||
interface ShelfBook {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
|
||||
@@ -2,14 +2,14 @@ import { useTranslation } from "react-i18n-lite";
|
||||
import { readingStatusKey } from "@/locales";
|
||||
import styles from "../../pages/Library.module.css";
|
||||
|
||||
export const LIBRARY_STATUS_COLORS: Record<string, { bg: string; text: string }> = {
|
||||
const LIBRARY_STATUS_COLORS: Record<string, { bg: string; text: string }> = {
|
||||
want_to_read: { bg: "#dbeafe", text: "#1d4ed8" },
|
||||
reading: { bg: "#dcfce7", text: "#16a34a" },
|
||||
finished: { bg: "#f3e8ff", text: "#9333ea" },
|
||||
dnf: { bg: "#fef3c7", text: "#b45309" },
|
||||
};
|
||||
|
||||
export interface LibraryBookCardData {
|
||||
interface LibraryBookCardData {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CollapsibleMarkerPassage,
|
||||
CollapsibleMarkerThought,
|
||||
} from "@/components/annotations/CollapsibleMarkerText";
|
||||
import { MarkerPassageActions } from "@/components/annotations/MarkerPassageActions";
|
||||
|
||||
interface BookMarkersPanelProps {
|
||||
ebookId: number;
|
||||
@@ -67,18 +68,10 @@ export function BookMarkersPanel({
|
||||
) : (
|
||||
<span className="annotation-kind-badge bookmark-badge">{t("annotations.bookmarkOnly")}</span>
|
||||
)}
|
||||
<div className="annotation-actions">
|
||||
<button type="button" className="btn btn-sm" onClick={() => onGoToPassage(m)}>
|
||||
{t("annotations.goToPassage")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => removeBookmark(m.id)}
|
||||
>
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</div>
|
||||
<MarkerPassageActions
|
||||
onGoToPassage={() => onGoToPassage(m)}
|
||||
onDelete={() => removeBookmark(m.id)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* EpubReadingView — full-screen EPUB reading powered by react-reader (epub.js).
|
||||
*/
|
||||
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { lazy, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import { EpubView, EpubViewStyle } from "react-reader";
|
||||
@@ -12,6 +12,10 @@ import { useEpubHighlights } from "../../hooks/useEpubHighlights";
|
||||
import { useEpubReader } from "../../hooks/useEpubReader";
|
||||
import { useEpubSelection } from "../../hooks/useEpubSelection";
|
||||
import { useReadingSettings } from "../../hooks/useReadingSettings";
|
||||
import { useReaderOrientationCss } from "../../hooks/useReaderOrientationCss";
|
||||
import { ReaderErrorScreen } from "./ReaderErrorScreen";
|
||||
import { ReaderLoadingScreen } from "./ReaderLoadingScreen";
|
||||
import { ReaderSuspenseShell } from "./ReaderSuspenseShell";
|
||||
import type { EBookDetail } from "../../types/book";
|
||||
import type { EpubTocItem } from "./TableOfContents";
|
||||
import { SelectionPopover } from "./SelectionPopover";
|
||||
@@ -82,17 +86,7 @@ export function EpubReadingView({ book, bookId, initialEpubLocation }: EpubReadi
|
||||
applySettings(settings);
|
||||
}, [settings, applySettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (settings.orientation_lock !== "auto") {
|
||||
root.style.setProperty(
|
||||
"--reader-orientation",
|
||||
settings.orientation_lock === "portrait" ? "portrait" : "landscape",
|
||||
);
|
||||
} else {
|
||||
root.style.removeProperty("--reader-orientation");
|
||||
}
|
||||
}, [settings.orientation_lock]);
|
||||
useReaderOrientationCss(settings.orientation_lock);
|
||||
|
||||
const handleTocChanged = useCallback((toc: EpubTocItem[]) => {
|
||||
setTocItems(toc);
|
||||
@@ -122,34 +116,20 @@ export function EpubReadingView({ book, bookId, initialEpubLocation }: EpubReadi
|
||||
);
|
||||
|
||||
if (epubLoading) {
|
||||
return (
|
||||
<div className="reader-loading">
|
||||
<div className="spinner" />
|
||||
<p>{t("reader.loading")}</p>
|
||||
</div>
|
||||
);
|
||||
return <ReaderLoadingScreen />;
|
||||
}
|
||||
|
||||
if (epubError || !epubUrl) {
|
||||
return (
|
||||
<div className="reader-loading">
|
||||
<p className="reader-error">{epubError ?? t("reader.unableToOpen")}</p>
|
||||
<button type="button" className="back-button" onClick={() => navigate("/")}>
|
||||
{t("reader.backToLibrary")}
|
||||
</button>
|
||||
</div>
|
||||
<ReaderErrorScreen
|
||||
message={epubError ?? t("reader.unableToOpen")}
|
||||
onBack={() => navigate("/")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="reader-loading">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="reader-container" data-theme={settings.theme}>
|
||||
<ReaderSuspenseShell theme={settings.theme}>
|
||||
<ReaderToolbar
|
||||
bookTitle={book.title}
|
||||
chapterTitle={chapterTitle || t("reader.reading")}
|
||||
@@ -235,7 +215,6 @@ export function EpubReadingView({ book, bookId, initialEpubLocation }: EpubReadi
|
||||
›
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</Suspense>
|
||||
</ReaderSuspenseShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
interface PanelCloseButtonProps {
|
||||
className: string;
|
||||
onClick: () => void;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
export function PanelCloseButton({ className, onClick, ariaLabel }: PanelCloseButtonProps) {
|
||||
return (
|
||||
<button type="button" className={className} onClick={onClick} aria-label={ariaLabel}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,17 @@
|
||||
* PdfReadingView — full-screen PDF reading powered by PDF.js.
|
||||
*/
|
||||
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { lazy, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import { useAnnotations } from "@/context/AnnotationsContext";
|
||||
import { useBookmarkRailLayout } from "../../hooks/useBookmarkRailLayout";
|
||||
import { usePdfReader } from "../../hooks/usePdfReader";
|
||||
import { useReadingSettings } from "../../hooks/useReadingSettings";
|
||||
import { useReaderOrientationCss } from "../../hooks/useReaderOrientationCss";
|
||||
import { ReaderErrorScreen } from "./ReaderErrorScreen";
|
||||
import { ReaderLoadingScreen } from "./ReaderLoadingScreen";
|
||||
import { ReaderSuspenseShell } from "./ReaderSuspenseShell";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import { booksApi } from "../../api/books";
|
||||
import type { EBookDetail } from "../../types/book";
|
||||
@@ -96,17 +100,7 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
|
||||
);
|
||||
}, [bookId, pageCount, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (settings.orientation_lock !== "auto") {
|
||||
root.style.setProperty(
|
||||
"--reader-orientation",
|
||||
settings.orientation_lock === "portrait" ? "portrait" : "landscape",
|
||||
);
|
||||
} else {
|
||||
root.style.removeProperty("--reader-orientation");
|
||||
}
|
||||
}, [settings.orientation_lock]);
|
||||
useReaderOrientationCss(settings.orientation_lock);
|
||||
|
||||
const handleTocNavigate = useCallback(
|
||||
(href: string) => {
|
||||
@@ -165,36 +159,17 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
|
||||
}, [bookMarkers, currentPage]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="reader-loading">
|
||||
<div className="spinner" />
|
||||
<p>{t("reader.loading")}</p>
|
||||
</div>
|
||||
);
|
||||
return <ReaderLoadingScreen />;
|
||||
}
|
||||
|
||||
if (error || !pdfDocument) {
|
||||
const message =
|
||||
error === "PDF_PASSWORD" ? t("reader.pdfPassword") : (error ?? t("reader.unableToOpen"));
|
||||
return (
|
||||
<div className="reader-loading">
|
||||
<p className="reader-error">{message}</p>
|
||||
<button type="button" className="back-button" onClick={() => navigate("/")}>
|
||||
{t("reader.backToLibrary")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
return <ReaderErrorScreen message={message} onBack={() => navigate("/")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="reader-loading">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="reader-container" data-theme={settings.theme}>
|
||||
<ReaderSuspenseShell theme={settings.theme}>
|
||||
<ReaderToolbar
|
||||
bookTitle={book.title}
|
||||
chapterTitle={chapterTitle || t("reader.reading")}
|
||||
@@ -270,7 +245,6 @@ export function PdfReadingView({ book, bookId, initialAnchor }: PdfReadingViewPr
|
||||
›
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</Suspense>
|
||||
</ReaderSuspenseShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
|
||||
interface ReaderErrorScreenProps {
|
||||
message: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ReaderErrorScreen({ message, onBack }: ReaderErrorScreenProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="reader-loading">
|
||||
<p className="reader-error">{message}</p>
|
||||
<button type="button" className="back-button" onClick={onBack}>
|
||||
{t("reader.backToLibrary")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
|
||||
interface ReaderLoadingScreenProps {
|
||||
showMessage?: boolean;
|
||||
}
|
||||
|
||||
export function ReaderLoadingScreen({ showMessage = true }: ReaderLoadingScreenProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="reader-loading">
|
||||
<div className="spinner" />
|
||||
{showMessage && <p>{t("reader.loading")}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Suspense, type ReactNode } from "react";
|
||||
import { ReaderLoadingScreen } from "./ReaderLoadingScreen";
|
||||
|
||||
interface ReaderSuspenseShellProps {
|
||||
theme: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ReaderSuspenseShell({ theme, children }: ReaderSuspenseShellProps) {
|
||||
return (
|
||||
<Suspense fallback={<ReaderLoadingScreen showMessage={false} />}>
|
||||
<div className="reader-container" data-theme={theme}>
|
||||
{children}
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
|
||||
interface ReaderTocButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ReaderTocButton({ onClick }: ReaderTocButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="reader-bar-btn"
|
||||
onClick={onClick}
|
||||
aria-label={t("reader.tocAria")}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import type { ReadingProgress } from "../../types/reader";
|
||||
import { ReaderTocButton } from "./ReaderTocButton";
|
||||
|
||||
interface ReaderToolbarProps {
|
||||
bookTitle: string;
|
||||
@@ -47,38 +48,14 @@ export default function ReaderToolbar({
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="reader-bar-btn"
|
||||
onClick={onToggleToc}
|
||||
aria-label={t("reader.tocAria")}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<ReaderTocButton onClick={onToggleToc} />
|
||||
)}
|
||||
<div className="reader-bar-title">
|
||||
<span className="reader-bar-book">{bookTitle}</span>
|
||||
<span className="reader-bar-chapter">{chapterTitle}</span>
|
||||
</div>
|
||||
<div className="reader-bar-actions">
|
||||
{onBack && (
|
||||
<button
|
||||
type="button"
|
||||
className="reader-bar-btn"
|
||||
onClick={onToggleToc}
|
||||
aria-label={t("reader.tocAria")}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{onBack && <ReaderTocButton onClick={onToggleToc} />}
|
||||
{onBookmarkPage && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
ThemePreset,
|
||||
} from "../../types/reader";
|
||||
import type { SettingsPersistMode } from "../../hooks/useReadingSettings";
|
||||
import { PanelCloseButton } from "./PanelCloseButton";
|
||||
|
||||
interface ReadingSettingsPanelProps {
|
||||
format?: "epub" | "pdf";
|
||||
@@ -89,17 +90,11 @@ export default function ReadingSettingsPanel({
|
||||
>
|
||||
<div className="settings-header">
|
||||
<h2 className="settings-title">{t("reader.settingsTitle")}</h2>
|
||||
<button
|
||||
type="button"
|
||||
<PanelCloseButton
|
||||
className="settings-close-btn"
|
||||
onClick={handleClose}
|
||||
aria-label={t("reader.closeSettingsAria")}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
ariaLabel={t("reader.closeSettingsAria")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-body">
|
||||
@@ -124,7 +119,7 @@ export default function ReadingSettingsPanel({
|
||||
{format === "pdf" && onPdfScaleChange && (
|
||||
<section className="settings-section">
|
||||
<h3 className="settings-section-title">
|
||||
{t("reader.pdfZoom", { value: Math.round(pdfScale * 100) })}
|
||||
{t("reader.pdfZoom", { value: String(Math.round(pdfScale * 100)) })}
|
||||
</h3>
|
||||
<input
|
||||
type="range"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { KeyboardEvent } from "react";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import { PanelCloseButton } from "./PanelCloseButton";
|
||||
|
||||
export interface EpubTocItem {
|
||||
label: string;
|
||||
@@ -73,17 +74,11 @@ export default function TableOfContents({
|
||||
<aside className={`toc-drawer ${isOpen ? "toc-drawer--open" : ""}`}>
|
||||
<div className="toc-header">
|
||||
<h2 className="toc-title">{t("reader.tocTitle")}</h2>
|
||||
<button
|
||||
type="button"
|
||||
<PanelCloseButton
|
||||
className="toc-close-btn"
|
||||
onClick={onClose}
|
||||
aria-label={t("reader.closeTocAria")}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
ariaLabel={t("reader.closeTocAria")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav className="toc-list">
|
||||
|
||||
Reference in New Issue
Block a user