diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml
index 58e667e4e9..ed1b61e098 100644
--- a/frontend/editor/public/locales/en-US/translation.toml
+++ b/frontend/editor/public/locales/en-US/translation.toml
@@ -11328,12 +11328,15 @@ searchPlaceholder = "Search attachments"
title = "Attachments"
[viewer.bookmarks]
+addBookmark = "Add bookmark"
bookmarkTitle = "Bookmark title"
bookmarkTitleRequired = "Bookmark title is required"
closeSidebar = "Close bookmarks sidebar"
collapseAll = "Collapse all bookmarks"
+empty = "No bookmarks in this document"
expandAll = "Expand all bookmarks"
searchPlaceholder = "Search bookmarks"
+title = "Bookmarks"
[viewer.comments]
addComment = "Add comment"
@@ -11355,10 +11358,12 @@ locateAnnotation = "Locate in document"
moreActions = "More actions"
nComments_one = "{{count}} comment"
nComments_other = "{{count}} comments"
+noMatch = "No comments match your search"
pageLabel = "Page {{page}}"
placingHint = "Click a page to place… (cancel)"
removeCommentOnly = "Remove comment only"
saveReply = "Save reply"
+searchPlaceholder = "Search comments"
title = "Comments"
typeComment = "Comment"
typeInsertText = "Insert Text"
@@ -11381,6 +11386,7 @@ unsavedDesc = "You have unsaved changes"
closeSidebar = "Close layers sidebar"
hideAll = "Hide all layers"
showAll = "Show all layers"
+title = "Layers"
[viewer.link]
delete = "Delete link"
diff --git a/frontend/editor/src/core/components/viewer/AttachmentSidebar.tsx b/frontend/editor/src/core/components/viewer/AttachmentSidebar.tsx
index 4dcc61fe2b..fe0bd8414b 100644
--- a/frontend/editor/src/core/components/viewer/AttachmentSidebar.tsx
+++ b/frontend/editor/src/core/components/viewer/AttachmentSidebar.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
-import { Box, ScrollArea, Text, Loader, Stack, TextInput } from "@mantine/core";
+import { Text, Loader, Stack } from "@mantine/core";
import LocalIcon from "@app/components/shared/LocalIcon";
import { Button } from "@app/ui/Button";
import { ActionIcon } from "@app/ui/ActionIcon";
@@ -9,7 +9,7 @@ import { PdfAttachmentObject } from "@embedpdf/models";
import AttachmentIcon from "@mui/icons-material/AttachmentRounded";
import DownloadIcon from "@mui/icons-material/DownloadRounded";
import { useTranslation } from "react-i18next";
-import "@app/components/viewer/SidebarBase.css";
+import { SidebarBase } from "@app/components/viewer/SidebarBase";
import "@app/components/viewer/AttachmentSidebar.css";
interface AttachmentSidebarProps {
@@ -20,8 +20,6 @@ interface AttachmentSidebarProps {
preloadCacheKeys?: string[];
}
-const SIDEBAR_WIDTH = "15rem";
-
interface AttachmentCacheEntry {
status: "idle" | "loading" | "success" | "error";
attachments: PdfAttachmentObject[] | null;
@@ -360,179 +358,129 @@ export const AttachmentSidebar = ({
const showNoDocument = attachmentSupport && !documentCacheKey;
return (
- }
+ rightOffset={`${(thumbnailVisible ? 15 : 0) + (bookmarkVisible ? 15 : 0)}rem`}
+ visible={visible}
+ onClose={toggleAttachmentSidebar}
+ closeLabel={t(
+ "viewer.attachments.closeSidebar",
+ "Close attachments sidebar",
+ )}
+ searchTerm={searchTerm}
+ searchPlaceholder={t(
+ "viewer.attachments.searchPlaceholder",
+ "Search attachments",
+ )}
+ onSearchChange={setSearchTerm}
>
-
-
-
-
-
-
- {t("viewer.attachments.title", "Attachments")}
+ {!attachmentSupport && (
+
+
+ {t(
+ "viewer.attachments.noSupport",
+ "Attachment support is unavailable for this viewer.",
+ )}
-
-
+
+ {t(
+ "viewer.attachments.noDocument",
+ "Open a PDF to view its attachments.",
)}
- title={t("viewer.attachments.close", "Close attachments")}
+
+
+ )}
+
+ {attachmentSupport && documentCacheKey && currentError && (
+
+
+ {currentError}
+
+
-
+
-
-
+
+ )}
-
- setSearchTerm(event.currentTarget.value)}
- leftSection={
-
- }
- size="xs"
- />
-
-
-
-
- {!attachmentSupport && (
-
-
- {t(
- "viewer.attachments.noSupport",
- "Attachment support is unavailable for this viewer.",
- )}
-
-
- )}
+
+
+ {t("viewer.attachments.loading", "Loading attachments...")}
+
+
+ )}
- {attachmentSupport && showNoDocument && (
-
-
- {t(
- "viewer.attachments.noDocument",
- "Open a PDF to view its attachments.",
- )}
-
-
- )}
+ {showEmptyState && (
+
+
+
+ {t("viewer.attachments.empty", "No attachments in this document")}
+
+ }
+ >
+ {t("viewer.attachments.addAttachment", "Add attachment")}
+
+
+ )}
- {attachmentSupport && documentCacheKey && currentError && (
-
-
- {currentError}
-
-
-
-
-
- )}
+ {showAttachmentList && (
+ <>
+
+ }
+ style={{ marginBottom: "var(--space-xs)" }}
+ >
+ {t("viewer.attachments.addAttachment", "Add attachment")}
+
+
+ {renderAttachments(filteredAttachments)}
+
+ >
+ )}
- {attachmentSupport && documentCacheKey && isLocalLoading && (
-
-
-
- {t("viewer.attachments.loading", "Loading attachments...")}
-
-
- )}
-
- {showEmptyState && (
-
-
-
- {t(
- "viewer.attachments.empty",
- "No attachments in this document",
- )}
-
-
- }
- >
- {t("viewer.attachments.addAttachment", "Add attachment")}
-
-
- )}
-
- {showAttachmentList && (
- <>
-
- }
- style={{ marginBottom: "var(--space-xs)" }}
- >
- {t("viewer.attachments.addAttachment", "Add attachment")}
-
-
- {renderAttachments(filteredAttachments)}
-
- >
- )}
-
- {showSearchEmpty && (
-
-
- {t(
- "viewer.attachments.noMatch",
- "No attachments match your search",
- )}
-
-
- )}
-
-
-
+ {showSearchEmpty && (
+
+
+ {t(
+ "viewer.attachments.noMatch",
+ "No attachments match your search",
+ )}
+
+
+ )}
+
);
};
diff --git a/frontend/editor/src/core/components/viewer/BookmarkSidebar.tsx b/frontend/editor/src/core/components/viewer/BookmarkSidebar.tsx
index 2acd3e309d..f331ab5176 100644
--- a/frontend/editor/src/core/components/viewer/BookmarkSidebar.tsx
+++ b/frontend/editor/src/core/components/viewer/BookmarkSidebar.tsx
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
import {
Box,
- ScrollArea,
Text,
Loader,
Stack,
@@ -21,7 +20,7 @@ import apiClient from "@app/services/apiClient";
import { PdfBookmarkObject, PdfActionType } from "@embedpdf/models";
import { useTranslation } from "react-i18next";
import BookmarksIcon from "@mui/icons-material/BookmarksRounded";
-import "@app/components/viewer/SidebarBase.css";
+import { SidebarBase } from "@app/components/viewer/SidebarBase";
import "@app/components/viewer/BookmarkSidebar.css";
interface BookmarkSidebarProps {
@@ -31,8 +30,6 @@ interface BookmarkSidebarProps {
preloadCacheKeys?: string[];
}
-const SIDEBAR_WIDTH = "15rem";
-
type BookmarkNode = PdfBookmarkObject & { id: string };
type BookmarkCacheStatus = "idle" | "loading" | "success" | "error";
@@ -666,285 +663,208 @@ export const BookmarkSidebar = ({
if (!visible) {
return null;
}
-
- return (
-
-
-
-
-
-
-
- Bookmarks
-
-
-
- {bookmarkSupport && bookmarksWithIds.length > 0 && (
- <>
- {Object.values(expanded).some((val) => val === false) ? (
-
-
-
- ) : (
-
-
-
- )}
- >
- )}
+ const expandCollapseActions =
+ bookmarkSupport && bookmarksWithIds.length > 0 ? (
+ <>
+ {Object.values(expanded).some((val) => val === false) ? (
-
+
-
-
+ ) : (
+
+
+
+ )}
+ >
+ ) : null;
-
- setSearchTerm(event.currentTarget.value)}
- leftSection={
-
- }
- size="xs"
- />
-
+ return (
+ }
+ rightOffset={`${thumbnailVisible ? 15 : 0}rem`}
+ visible={visible}
+ onClose={toggleBookmarkSidebar}
+ closeLabel={t("viewer.bookmarks.closeSidebar", "Close bookmarks sidebar")}
+ headerActions={expandCollapseActions}
+ searchTerm={searchTerm}
+ searchPlaceholder={t(
+ "viewer.bookmarks.searchPlaceholder",
+ "Search bookmarks",
+ )}
+ onSearchChange={setSearchTerm}
+ >
+ {bookmarkSupport && showNoDocument && (
+
+
+ Open a PDF to view its bookmarks.
+
+
+ )}
-
-
- {!bookmarkSupport && (
-
-
- Bookmark support is unavailable for this viewer.
-
-
- )}
+ {bookmarkSupport && documentCacheKey && currentError && (
+
+
+ {currentError}
+
+
+
+ )}
- {bookmarkSupport && showNoDocument && (
-
-
- Open a PDF to view its bookmarks.
-
-
- )}
+ {bookmarkSupport && documentCacheKey && isLocalLoading && (
+
+
+
+ Loading bookmarks...
+
+
+ )}
+ {showEmptyState && !isAddingBookmark && (
+
+
+
+ {t("viewer.bookmarks.empty", "No bookmarks in this document")}
+
+ }
+ >
+ {t("viewer.bookmarks.addBookmark", "Add bookmark")}
+
+
+ )}
- {bookmarkSupport && documentCacheKey && currentError && (
-
-
- {currentError}
-
-
-
- )}
-
- {bookmarkSupport && documentCacheKey && isLocalLoading && (
-
-
-
- Loading bookmarks...
-
-
- )}
- {showEmptyState && !isAddingBookmark && (
-
-
-
- No bookmarks in this document
-
-
- }
- >
- Add bookmark
-
-
- )}
-
- {isAddingBookmark && (
-
-
-
- Add bookmark
-
- setNewBookmarkTitle(e.currentTarget.value)}
- autoFocus
- disabled={isSavingBookmark}
- />
-
- setNewBookmarkPage(typeof v === "number" ? v : 1)
- }
- disabled={isSavingBookmark}
- />
- {addBookmarkError && (
-
- {addBookmarkError}
-
- )}
-
-
-
-
-
-
- )}
- {showBookmarkList && (
- <>
- {!isAddingBookmark && (
-
- }
- style={{ marginBottom: "var(--space-xs)" }}
- >
- Add bookmark
-
+ {isAddingBookmark && (
+
+
+
+ {t("viewer.bookmarks.addBookmark", "Add bookmark")}
+
+
- {renderBookmarks(filteredBookmarks)}
-
- >
- )}
-
- {showSearchEmpty && (
-
-
- No bookmarks match your search
+ aria-label={t("viewer.bookmarks.bookmarkTitle", "Bookmark title")}
+ value={newBookmarkTitle}
+ onChange={(e) => setNewBookmarkTitle(e.currentTarget.value)}
+ autoFocus
+ disabled={isSavingBookmark}
+ />
+
+ setNewBookmarkPage(typeof v === "number" ? v : 1)
+ }
+ disabled={isSavingBookmark}
+ />
+ {addBookmarkError && (
+
+ {addBookmarkError}
-
- )}
+ )}
+
+
+
+
+
-
+ )}
+ {showBookmarkList && (
+ <>
+ {!isAddingBookmark && (
+
+ }
+ style={{ marginBottom: "var(--space-xs)" }}
+ >
+ {t("viewer.bookmarks.addBookmark", "Add bookmark")}
+
+ )}
+
+ {renderBookmarks(filteredBookmarks)}
+
+ >
+ )}
+ {showSearchEmpty && (
+
+
+ No bookmarks match your search
+
+
+ )}
{bookmarkSupport && documentCacheKey && (
)}
-
+
);
};
diff --git a/frontend/editor/src/core/components/viewer/CommentsSidebar.tsx b/frontend/editor/src/core/components/viewer/CommentsSidebar.tsx
index 3bbc5c8f53..d2ca4e9436 100644
--- a/frontend/editor/src/core/components/viewer/CommentsSidebar.tsx
+++ b/frontend/editor/src/core/components/viewer/CommentsSidebar.tsx
@@ -1,7 +1,6 @@
import { useMemo, useState, useCallback, useEffect, useRef } from "react";
import {
Box,
- ScrollArea,
Text,
Textarea,
Stack,
@@ -36,8 +35,7 @@ import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useAnnotation as useAnnotationContext } from "@app/contexts/AnnotationContext";
import LocalIcon from "@app/components/shared/LocalIcon";
import { compareEntriesByVisualOrder } from "@app/components/viewer/commentsSidebarOrder";
-
-const SIDEBAR_WIDTH = "18rem";
+import { SidebarBase } from "@app/components/viewer/SidebarBase";
/** PDF subtypes that are inherently standalone comment annotations (not linked to other annotations). */
const STANDALONE_COMMENT_SUBTYPES = new Set([
@@ -455,18 +453,60 @@ export function CommentsSidebar({
return ids;
}, [state]);
+ const [searchTerm, setSearchTerm] = useState("");
+
+ const filteredByPage = useMemo(() => {
+ const query = searchTerm.trim().toLowerCase();
+ if (!query) {
+ return byPage;
+ }
+ const result: Record = {};
+ for (const [pageStr, entries] of Object.entries(byPage)) {
+ const matching = entries.filter((entry) => {
+ const ann = entry.annotation.object;
+ const contents = (ann.contents || "").toLowerCase();
+ const author = (ann.author || "").toLowerCase();
+ const replies = entry.replies || [];
+ const replyMatch = replies.some(
+ (r) =>
+ (r.object.contents || "").toLowerCase().includes(query) ||
+ (r.object.author || "").toLowerCase().includes(query),
+ );
+ return contents.includes(query) || author.includes(query) || replyMatch;
+ });
+ if (matching.length > 0) {
+ result[Number(pageStr)] = matching;
+ }
+ }
+ return result;
+ }, [byPage, searchTerm]);
+
const pageNumbers = useMemo(
+ () =>
+ Object.keys(filteredByPage)
+ .map(Number)
+ .sort((a, b) => a - b),
+ [filteredByPage],
+ );
+
+ const totalCount = useMemo(
() =>
Object.keys(byPage)
.map(Number)
- .sort((a, b) => a - b),
+ .reduce((sum, p) => sum + (byPage[p]?.length ?? 0), 0),
[byPage],
);
- const totalCount = useMemo(
- () => pageNumbers.reduce((sum, p) => sum + (byPage[p]?.length ?? 0), 0),
- [pageNumbers, byPage],
+
+ const totalFilteredCount = useMemo(
+ () =>
+ pageNumbers.reduce((sum, p) => sum + (filteredByPage[p]?.length ?? 0), 0),
+ [pageNumbers, filteredByPage],
);
+ const isSearchActive = searchTerm.trim().length > 0;
+ const showSearchEmpty =
+ isSearchActive && totalCount > 0 && totalFilteredCount === 0;
+
const handleContentsChange = useCallback(
(pageIndex: number, annotationId: string, value: string) => {
setDraftContents((prev) => ({
@@ -670,189 +710,165 @@ export function CommentsSidebar({
if (!visible) return null;
- return (
-
-
-
-
- {t("viewer.comments.title", "Comments")}
-
- {totalCount > 0 && (
-
-
-
-
-
-
-
-
- )}
- {toggleCommentsSidebar && (
+ const commentsHeaderActions =
+ totalCount > 0 ? (
+
+
-
+
+
+
+
+ ) : null;
+
+ return (
+ <>
+
}
+ rightOffset={rightOffset}
+ visible={visible}
+ onClose={toggleCommentsSidebar}
+ closeLabel={t("viewer.comments.closeSidebar", "Close comments sidebar")}
+ headerActions={commentsHeaderActions}
+ searchTerm={searchTerm}
+ searchPlaceholder={t(
+ "viewer.comments.searchPlaceholder",
+ "Search comments",
)}
-
-
-
- {totalCount === 0 ? (
-
-
-
+ onSearchChange={setSearchTerm}
+ viewportRef={scrollViewportRef}
+ >
+ {totalCount === 0 ? (
+
+
+
+ {t(
+ "viewer.comments.hint",
+ "Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.",
+ )}
+
+ {isPlacingComment ? (
+
+ }
+ >
{t(
- "viewer.comments.hint",
- "Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.",
+ "viewer.comments.placingHint",
+ "Click a page to place… (cancel)",
)}
-
- {isPlacingComment ? (
-
- }
- >
+
+ ) : (
+
+ }
+ >
+ {t("viewer.comments.addComment", "Add comment")}
+
+ )}
+
+ ) : (
+ <>
+ {isPlacingComment ? (
+
+ }
+ style={{ paddingInline: 6 }}
+ >
+ {t(
+ "viewer.comments.placingHint",
+ "Click a page to place… (cancel)",
+ )}
+
+ ) : (
+
+ }
+ style={{
+ paddingInline: 6,
+ marginBottom: "var(--space-xs)",
+ }}
+ >
+ {t("viewer.comments.addComment", "Add comment")}
+
+ )}
+ {showSearchEmpty ? (
+
+
{t(
- "viewer.comments.placingHint",
- "Click a page to place… (cancel)",
+ "viewer.comments.noMatch",
+ "No comments match your search",
)}
-
- ) : (
-
- }
- >
- {t("viewer.comments.addComment", "Add comment")}
-
- )}
-
- ) : (
- <>
- {isPlacingComment ? (
-
- }
- style={{ paddingInline: 6 }}
- >
- {t(
- "viewer.comments.placingHint",
- "Click a page to place… (cancel)",
- )}
-
- ) : (
-
- }
- style={{ paddingInline: 6 }}
- >
- {t("viewer.comments.addComment", "Add comment")}
-
- )}
- {pageNumbers.map((pageIndex) => {
- const entries = byPage[pageIndex] ?? [];
+
+
+ ) : (
+ pageNumbers.map((pageIndex) => {
+ const entries = filteredByPage[pageIndex] ?? [];
const pageNum = pageIndex + 1;
return (
@@ -1291,11 +1307,11 @@ export function CommentsSidebar({
);
- })}
- >
- )}
-
-
+ })
+ )}
+ >
+ )}
+
-
+ >
);
}
diff --git a/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx
index 39d751790f..c56d86662c 100644
--- a/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx
+++ b/frontend/editor/src/core/components/viewer/EmbedPdfViewer.tsx
@@ -1132,7 +1132,7 @@ const EmbedPdfViewerContent = ({
]);
const sidebarWidthRem = 15;
- const commentsSidebarWidthRem = 18;
+ const commentsSidebarWidthRem = 15;
const totalRightMargin =
(isThumbnailSidebarVisible ? sidebarWidthRem : 0) +
(isBookmarkSidebarVisible ? sidebarWidthRem : 0) +
diff --git a/frontend/editor/src/core/components/viewer/LayerSidebar.tsx b/frontend/editor/src/core/components/viewer/LayerSidebar.tsx
index 533bb7959b..b7fa108360 100644
--- a/frontend/editor/src/core/components/viewer/LayerSidebar.tsx
+++ b/frontend/editor/src/core/components/viewer/LayerSidebar.tsx
@@ -1,21 +1,12 @@
import { useState, useEffect, useCallback, useRef } from "react";
-import {
- Box,
- ScrollArea,
- Text,
- Checkbox,
- Stack,
- Loader,
- Tooltip,
-} from "@mantine/core";
+import { Text, Checkbox, Stack, Loader, Tooltip } from "@mantine/core";
import LayersIcon from "@mui/icons-material/Layers";
import { ActionIcon } from "@app/ui/ActionIcon";
import VisibilityIcon from "@mui/icons-material/Visibility";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
-import LocalIcon from "@app/components/shared/LocalIcon";
import { useTranslation } from "react-i18next";
import { useViewer } from "@app/contexts/ViewerContext";
-import "@app/components/viewer/SidebarBase.css";
+import { SidebarBase } from "@app/components/viewer/SidebarBase";
import "@app/components/viewer/LayerSidebar.css";
import {
readPdfLayers,
@@ -40,8 +31,6 @@ interface LayerSidebarProps {
onLayersDetected?: (hasLayers: boolean) => void;
}
-const SIDEBAR_WIDTH = "15rem";
-
type LoadStatus = "idle" | "loading" | "ready" | "no-layers" | "error";
export function LayerSidebar({
@@ -318,116 +307,91 @@ export function LayerSidebar({
const allVisible = leafIds.every((id) => visibility[id] !== false);
const allHidden = leafIds.every((id) => visibility[id] === false);
- return (
-
- {/* Header */}
-
-
-
-
-
-
- Layers
-
- {isApplying && }
-
-
- {status === "ready" && leafIds.length > 0 && (
- <>
-
-
-
-
-
-
- >
- )}
+ const layerHeaderActions = (
+ <>
+ {isApplying &&
}
+ {status === "ready" && leafIds.length > 0 && (
+ <>
-
+
+
+
+
+ >
+ )}
+ >
+ );
+
+ return (
+
}
+ rightOffset={`${rightOffset}rem`}
+ visible={visible}
+ onClose={toggleLayerSidebar}
+ closeLabel={t("viewer.layers.closeSidebar", "Close layers sidebar")}
+ headerActions={layerHeaderActions}
+ >
+ {status === "idle" && (
+
+
+ Open a PDF to view its layers.
+
-
+ )}
- {/* Content */}
-
-
- {status === "idle" && (
-
-
- Open a PDF to view its layers.
-
-
- )}
+ {status === "loading" && (
+
+
+
+ Loading layers...
+
+
+ )}
- {status === "loading" && (
-
-
-
- Loading layers...
-
-
- )}
+ {status === "error" && (
+
+
+ {loadError ?? "Failed to load layers."}
+
+
+ )}
- {status === "error" && (
-
-
- {loadError ?? "Failed to load layers."}
-
-
- )}
+ {status === "no-layers" && (
+
+
+ This document has no layers.
+
+
+ )}
- {status === "no-layers" && (
-
-
- This document has no layers.
-
-
- )}
-
- {status === "ready" && layers.length > 0 && (
-
- {layers.map((layer) => renderLayer({ ...layer, depth: 0 }))}
-
- )}
-
-
-
+ {status === "ready" && layers.length > 0 && (
+
+ {layers.map((layer) => renderLayer({ ...layer, depth: 0 }))}
+
+ )}
+
);
}
diff --git a/frontend/editor/src/core/components/viewer/SidebarBase.tsx b/frontend/editor/src/core/components/viewer/SidebarBase.tsx
new file mode 100644
index 0000000000..7cdcc34d97
--- /dev/null
+++ b/frontend/editor/src/core/components/viewer/SidebarBase.tsx
@@ -0,0 +1,121 @@
+import type { ReactNode } from "react";
+import { Box, ScrollArea, Text, TextInput } from "@mantine/core";
+import LocalIcon from "@app/components/shared/LocalIcon";
+import { ActionIcon } from "@app/ui/ActionIcon";
+import "@app/components/viewer/SidebarBase.css";
+
+export const SIDEBAR_WIDTH = "15rem";
+
+export interface SidebarBaseProps {
+ /** Sidebar title string or React element. */
+ title: ReactNode;
+ /** Header icon (ReactNode or string icon name for LocalIcon). */
+ icon: ReactNode;
+ /** Right offset position string (e.g. "15rem" or "0rem"). */
+ rightOffset?: string;
+ /** Sidebar visibility flag. */
+ visible?: boolean;
+ /** Additional CSS class names. */
+ className?: string;
+ /** Callback fired when user clicks the header close button. */
+ onClose?: () => void;
+ /** Accessible label for the close button. */
+ closeLabel?: string;
+ /** Extra buttons/elements to render in the header right actions area. */
+ headerActions?: ReactNode;
+ /** Current search input term. */
+ searchTerm?: string;
+ /** Search input placeholder text. */
+ searchPlaceholder?: string;
+ /** Callback fired when search query changes. */
+ onSearchChange?: (value: string) => void;
+ /** Optional ref for the ScrollArea viewport element. */
+ viewportRef?: React.Ref
;
+ /** Sidebar content children. */
+ children: ReactNode;
+}
+
+export function SidebarBase({
+ title,
+ icon,
+ rightOffset = "0rem",
+ visible = true,
+ className = "",
+ onClose,
+ closeLabel = "Close sidebar",
+ headerActions,
+ searchTerm,
+ searchPlaceholder,
+ onSearchChange,
+ viewportRef,
+ children,
+}: SidebarBaseProps) {
+ if (!visible) {
+ return null;
+ }
+
+ const renderIcon =
+ typeof icon === "string" ? (
+
+ ) : (
+ icon
+ );
+
+ return (
+
+
+
+ {renderIcon}
+
+ {title}
+
+
+
+ {headerActions}
+ {onClose && (
+
+
+
+ )}
+
+
+
+ {onSearchChange !== undefined && (
+
+ onSearchChange(e.currentTarget.value)}
+ leftSection={
+
+ }
+ size="xs"
+ />
+
+ )}
+
+
+
+ {children}
+
+
+
+ );
+}