mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
feat: integrate Drawer component for mobile filtering in InboxSidebar
- Added a Drawer component to replace the dropdown menu for filtering in the InboxSidebar on mobile devices, enhancing usability. - Implemented state management for the filter drawer, allowing users to select filters and connectors seamlessly. - Updated the UI to accommodate the new Drawer, ensuring a consistent experience across different screen sizes. - Enhanced connector filtering logic to improve item management and display options effectively.
This commit is contained in:
parent
57baeda767
commit
596515f523
9 changed files with 445 additions and 103 deletions
|
|
@ -69,7 +69,7 @@ export function CommentPanel({
|
|||
style={!isMobile && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined}
|
||||
>
|
||||
{hasThreads && (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin">
|
||||
<div className={cn("min-h-0 flex-1 overflow-y-auto scrollbar-thin", isMobile && "pb-24")}>
|
||||
<div className="space-y-4 p-4">
|
||||
{threads.map((thread) => (
|
||||
<CommentThread
|
||||
|
|
@ -106,7 +106,7 @@ export function CommentPanel({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-3">
|
||||
<div className={cn("p-3", isMobile && "fixed bottom-0 left-0 right-0 z-50 bg-card border-t")}>
|
||||
<CommentComposer
|
||||
members={members}
|
||||
membersLoading={membersLoading}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { Drawer, DrawerContent, DrawerHandle, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CommentPanelContainer } from "../comment-panel-container/comment-panel-container";
|
||||
|
|
@ -15,22 +16,39 @@ export function CommentSheet({
|
|||
}: CommentSheetProps) {
|
||||
const isBottomSheet = side === "bottom";
|
||||
|
||||
// Use Drawer for mobile (bottom), Sheet for medium screens (right)
|
||||
if (isBottomSheet) {
|
||||
return (
|
||||
<Drawer open={isOpen} onOpenChange={onOpenChange} shouldScaleBackground={false}>
|
||||
<DrawerContent className="h-[85vh] max-h-[85vh] z-80" overlayClassName="z-80">
|
||||
<DrawerHandle />
|
||||
<DrawerHeader className="px-4 pb-3 pt-2">
|
||||
<DrawerTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<MessageSquare className="size-5" />
|
||||
Comments
|
||||
{commentCount > 0 && (
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
{commentCount}
|
||||
</span>
|
||||
)}
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin">
|
||||
<CommentPanelContainer messageId={messageId} isOpen={true} variant="mobile" />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
// Use Sheet for medium screens (right side)
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side={side}
|
||||
className={cn(
|
||||
"flex flex-col gap-0 overflow-hidden p-0",
|
||||
isBottomSheet ? "h-[85vh] max-h-[85vh] rounded-t-xl" : "h-full w-full max-w-md"
|
||||
)}
|
||||
className={cn("flex flex-col gap-0 overflow-hidden p-0 h-full w-full max-w-md")}
|
||||
>
|
||||
{/* Drag handle indicator - only for bottom sheet */}
|
||||
{isBottomSheet && (
|
||||
<div className="flex justify-center pt-3 pb-1">
|
||||
<div className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
||||
</div>
|
||||
)}
|
||||
<SheetHeader className={cn("flex-shrink-0 border-b px-4", isBottomSheet ? "pb-3" : "py-4")}>
|
||||
<SheetHeader className="flex-shrink-0 px-4 py-4">
|
||||
<SheetTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<MessageSquare className="size-5" />
|
||||
Comments
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ import { createPortal } from "react-dom";
|
|||
import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHandle,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -34,6 +41,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { InboxItem } from "@/hooks/use-inbox";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import type { ConnectorIndexingMetadata } from "@/contracts/types/inbox.types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -56,6 +64,40 @@ function getInitials(name: string | null | undefined, email: string | null | und
|
|||
return "U";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for connector type
|
||||
*/
|
||||
function getConnectorTypeDisplayName(connectorType: string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
GITHUB_CONNECTOR: "GitHub",
|
||||
GOOGLE_CALENDAR_CONNECTOR: "Google Calendar",
|
||||
GOOGLE_GMAIL_CONNECTOR: "Gmail",
|
||||
GOOGLE_DRIVE_CONNECTOR: "Google Drive",
|
||||
LINEAR_CONNECTOR: "Linear",
|
||||
NOTION_CONNECTOR: "Notion",
|
||||
SLACK_CONNECTOR: "Slack",
|
||||
TEAMS_CONNECTOR: "Microsoft Teams",
|
||||
DISCORD_CONNECTOR: "Discord",
|
||||
JIRA_CONNECTOR: "Jira",
|
||||
CONFLUENCE_CONNECTOR: "Confluence",
|
||||
BOOKSTACK_CONNECTOR: "BookStack",
|
||||
CLICKUP_CONNECTOR: "ClickUp",
|
||||
AIRTABLE_CONNECTOR: "Airtable",
|
||||
LUMA_CONNECTOR: "Luma",
|
||||
ELASTICSEARCH_CONNECTOR: "Elasticsearch",
|
||||
WEBCRAWLER_CONNECTOR: "Web Crawler",
|
||||
YOUTUBE_CONNECTOR: "YouTube",
|
||||
CIRCLEBACK_CONNECTOR: "Circleback",
|
||||
MCP_CONNECTOR: "MCP",
|
||||
TAVILY_API: "Tavily",
|
||||
SEARXNG_API: "SearXNG",
|
||||
LINKUP_API: "Linkup",
|
||||
BAIDU_SEARCH_API: "Baidu",
|
||||
};
|
||||
|
||||
return displayNames[connectorType] || connectorType.replace(/_/g, " ").replace(/CONNECTOR|API/gi, "").trim();
|
||||
}
|
||||
|
||||
type InboxTab = "mentions" | "status";
|
||||
type InboxFilter = "all" | "unread";
|
||||
|
||||
|
|
@ -82,14 +124,17 @@ export function InboxSidebar({
|
|||
}: InboxSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<InboxTab>("mentions");
|
||||
const [activeFilter, setActiveFilter] = useState<InboxFilter>("all");
|
||||
const [selectedConnector, setSelectedConnector] = useState<string | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
// Dropdown state for filter menu
|
||||
// Dropdown state for filter menu (desktop only)
|
||||
const [openDropdown, setOpenDropdown] = useState<"filter" | null>(null);
|
||||
// Drawer state for filter menu (mobile only)
|
||||
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
|
||||
const [markingAsReadId, setMarkingAsReadId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -138,23 +183,23 @@ export function InboxSidebar({
|
|||
[inboxItems]
|
||||
);
|
||||
|
||||
// Get unique connectors from status items for filtering
|
||||
const uniqueConnectors = useMemo(() => {
|
||||
const connectorMap = new Map<string, { type: string; name: string }>();
|
||||
// Get unique connector types from status items for filtering
|
||||
const uniqueConnectorTypes = useMemo(() => {
|
||||
const connectorTypes = new Set<string>();
|
||||
|
||||
statusItems
|
||||
.filter((item) => item.type === "connector_indexing")
|
||||
.forEach((item) => {
|
||||
const metadata = item.metadata as ConnectorIndexingMetadata;
|
||||
if (metadata?.connector_type && !connectorMap.has(metadata.connector_type)) {
|
||||
connectorMap.set(metadata.connector_type, {
|
||||
type: metadata.connector_type,
|
||||
name: metadata.connector_name || metadata.connector_type,
|
||||
});
|
||||
if (metadata?.connector_type) {
|
||||
connectorTypes.add(metadata.connector_type);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(connectorMap.values());
|
||||
return Array.from(connectorTypes).map((type) => ({
|
||||
type,
|
||||
displayName: getConnectorTypeDisplayName(type),
|
||||
}));
|
||||
}, [statusItems]);
|
||||
|
||||
// Get items for current tab
|
||||
|
|
@ -358,53 +403,205 @@ export function InboxSidebar({
|
|||
<h2 className="text-lg font-semibold">{t("inbox") || "Inbox"}</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<DropdownMenu
|
||||
open={openDropdown === "filter"}
|
||||
onOpenChange={(isOpen) => setOpenDropdown(isOpen ? "filter" : null)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{/* Mobile: Button that opens bottom drawer */}
|
||||
{isMobile ? (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
onClick={() => setFilterDrawerOpen(true)}
|
||||
>
|
||||
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">{t("filter") || "Filter"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-80">
|
||||
{t("filter") || "Filter"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="w-44 z-80">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal">
|
||||
{t("filter") || "Filter"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveFilter("all")}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Inbox className="h-4 w-4" />
|
||||
<span>{t("all") || "All"}</span>
|
||||
</span>
|
||||
{activeFilter === "all" && <Check className="h-4 w-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveFilter("unread")}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<BellDot className="h-4 w-4" />
|
||||
<span>{t("unread") || "Unread"}</span>
|
||||
</span>
|
||||
{activeFilter === "unread" && <Check className="h-4 w-4" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-80">
|
||||
{t("filter") || "Filter"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Drawer open={filterDrawerOpen} onOpenChange={setFilterDrawerOpen} shouldScaleBackground={false}>
|
||||
<DrawerContent
|
||||
className="max-h-[70vh] z-80"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
<DrawerHeader className="px-4 pb-3 pt-2">
|
||||
<DrawerTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<ListFilter className="size-5" />
|
||||
{t("filter") || "Filter"}
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* Filter section */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground/80 font-medium px-1">
|
||||
{t("filter") || "Filter"}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveFilter("all");
|
||||
setFilterDrawerOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||
activeFilter === "all" ? "bg-primary/10 text-primary" : "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Inbox className="h-4 w-4" />
|
||||
<span>{t("all") || "All"}</span>
|
||||
</span>
|
||||
{activeFilter === "all" && <Check className="h-4 w-4" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveFilter("unread");
|
||||
setFilterDrawerOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||
activeFilter === "unread" ? "bg-primary/10 text-primary" : "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<BellDot className="h-4 w-4" />
|
||||
<span>{t("unread") || "Unread"}</span>
|
||||
</span>
|
||||
{activeFilter === "unread" && <Check className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Connectors section - only for status tab */}
|
||||
{activeTab === "status" && uniqueConnectorTypes.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground/80 font-medium px-1">
|
||||
{t("connectors") || "Connectors"}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedConnector(null);
|
||||
setFilterDrawerOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||
selectedConnector === null ? "bg-primary/10 text-primary" : "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<span>{t("all_connectors") || "All connectors"}</span>
|
||||
{selectedConnector === null && <Check className="h-4 w-4" />}
|
||||
</button>
|
||||
{uniqueConnectorTypes.map((connector) => (
|
||||
<button
|
||||
key={connector.type}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSelectedConnector(connector.type);
|
||||
setFilterDrawerOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||
selectedConnector === connector.type ? "bg-primary/10 text-primary" : "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{getConnectorIcon(connector.type, "h-4 w-4")}
|
||||
<span>{connector.displayName}</span>
|
||||
</span>
|
||||
{selectedConnector === connector.type && <Check className="h-4 w-4" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
) : (
|
||||
/* Desktop: Dropdown menu */
|
||||
<DropdownMenu
|
||||
open={openDropdown === "filter"}
|
||||
onOpenChange={(isOpen) => setOpenDropdown(isOpen ? "filter" : null)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
>
|
||||
<ListFilter className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="sr-only">{t("filter") || "Filter"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-80">
|
||||
{t("filter") || "Filter"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className={cn("z-80", activeTab === "status" ? "w-52" : "w-44")}>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal">
|
||||
{t("filter") || "Filter"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveFilter("all")}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Inbox className="h-4 w-4" />
|
||||
<span>{t("all") || "All"}</span>
|
||||
</span>
|
||||
{activeFilter === "all" && <Check className="h-4 w-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveFilter("unread")}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<BellDot className="h-4 w-4" />
|
||||
<span>{t("unread") || "Unread"}</span>
|
||||
</span>
|
||||
{activeFilter === "unread" && <Check className="h-4 w-4" />}
|
||||
</DropdownMenuItem>
|
||||
{activeTab === "status" && uniqueConnectorTypes.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal mt-2">
|
||||
{t("connectors") || "Connectors"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setSelectedConnector(null)}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span>{t("all_connectors") || "All connectors"}</span>
|
||||
{selectedConnector === null && <Check className="h-4 w-4" />}
|
||||
</DropdownMenuItem>
|
||||
{uniqueConnectorTypes.map((connector) => (
|
||||
<DropdownMenuItem
|
||||
key={connector.type}
|
||||
onClick={() => setSelectedConnector(connector.type)}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{getConnectorIcon(connector.type, "h-4 w-4")}
|
||||
<span>{connector.displayName}</span>
|
||||
</span>
|
||||
{selectedConnector === connector.type && <Check className="h-4 w-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -481,44 +678,6 @@ export function InboxSidebar({
|
|||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Connector filter chips - only show in status tab when there are connectors */}
|
||||
{activeTab === "status" && uniqueConnectors.length > 0 && (
|
||||
<div className="shrink-0 py-2 border-b relative">
|
||||
{/* Left shadow indicator */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-6 bg-gradient-to-r from-background to-transparent pointer-events-none z-10" />
|
||||
{/* Right shadow indicator */}
|
||||
<div className="absolute right-0 top-0 bottom-0 w-6 bg-gradient-to-l from-background to-transparent pointer-events-none z-10" />
|
||||
<div className="flex items-center gap-1.5 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden px-4">
|
||||
<Button
|
||||
variant={selectedConnector === null ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs shrink-0"
|
||||
onClick={() => setSelectedConnector(null)}
|
||||
>
|
||||
{t("all") || "All"}
|
||||
</Button>
|
||||
{uniqueConnectors.map((connector) => (
|
||||
<Tooltip key={connector.type}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={selectedConnector === connector.type ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs shrink-0 gap-1.5"
|
||||
onClick={() => setSelectedConnector(connector.type)}
|
||||
>
|
||||
{getConnectorIcon(connector.type, "h-3.5 w-3.5")}
|
||||
<span className="truncate max-w-20">{connector.name}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="z-80">
|
||||
{connector.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
|
|
|
|||
140
surfsense_web/components/ui/drawer.tsx
Normal file
140
surfsense_web/components/ui/drawer.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Drawer({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Drawer.displayName = "Drawer";
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
overlayClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
||||
overlayClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay className={overlayClassName} />
|
||||
<DrawerPrimitive.Content
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
}
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
function DrawerHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
function DrawerFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
function DrawerHandle({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div className={cn("mx-auto mt-4 h-1.5 w-12 rounded-full bg-muted-foreground/40", className)} {...props} />
|
||||
);
|
||||
}
|
||||
DrawerHandle.displayName = "DrawerHandle";
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
DrawerHandle,
|
||||
};
|
||||
|
||||
|
|
@ -42,13 +42,15 @@ function SheetContent({
|
|||
className,
|
||||
children,
|
||||
side = "right",
|
||||
overlayClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
overlayClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetOverlay className={overlayClassName} />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -706,7 +706,9 @@
|
|||
"no_status_updates_hint": "Document and connector updates will appear here",
|
||||
"filter": "Filter",
|
||||
"all": "All",
|
||||
"unread": "Unread"
|
||||
"unread": "Unread",
|
||||
"connectors": "Connectors",
|
||||
"all_connectors": "All connectors"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "Something went wrong",
|
||||
|
|
|
|||
|
|
@ -691,7 +691,9 @@
|
|||
"no_status_updates_hint": "文档和连接器更新将显示在这里",
|
||||
"filter": "筛选",
|
||||
"all": "全部",
|
||||
"unread": "未读"
|
||||
"unread": "未读",
|
||||
"connectors": "连接器",
|
||||
"all_connectors": "所有连接器"
|
||||
},
|
||||
"errors": {
|
||||
"something_went_wrong": "出错了",
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.2.1",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
|
|
|
|||
18
surfsense_web/pnpm-lock.yaml
generated
18
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -260,6 +260,9 @@ importers:
|
|||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
vaul:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
zod:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
|
|
@ -6384,6 +6387,12 @@ packages:
|
|||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
||||
vaul@1.1.2:
|
||||
resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
|
|
@ -13451,6 +13460,15 @@ snapshots:
|
|||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue