mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 19:59:20 +02:00
Compare commits
2 Commits
feat/cli-v
...
agent/j/e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3faadd9df | ||
|
|
4f76f15e01 |
@@ -26,6 +26,7 @@ import {
|
||||
Archive,
|
||||
BookCheck,
|
||||
ListChecks,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import type { InboxItem } from "@multica/core/types";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from "@multica/ui/components/ui/dropdown-menu";
|
||||
import { useIsMobile } from "@multica/ui/hooks/use-mobile";
|
||||
import { InboxListItem, timeAgo } from "./inbox-list-item";
|
||||
import { typeLabels } from "./inbox-detail-label";
|
||||
|
||||
@@ -70,6 +72,7 @@ export function InboxPage() {
|
||||
id: "multica_inbox_layout",
|
||||
});
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const selected = items.find((i) => (i.issue_id ?? i.id) === selectedKey) ?? null;
|
||||
const unreadCount = items.filter((i) => !i.read).length;
|
||||
|
||||
@@ -127,6 +130,166 @@ export function InboxPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// -- Shared sub-components --------------------------------------------------
|
||||
|
||||
const listHeader = (
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-sm font-semibold">Inbox</h1>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-auto">
|
||||
<DropdownMenuItem onClick={handleMarkAllRead}>
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
Mark all as read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleArchiveAll}>
|
||||
<Archive className="h-4 w-4" />
|
||||
Archive all
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleArchiveAllRead}>
|
||||
<BookCheck className="h-4 w-4" />
|
||||
Archive all read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleArchiveCompleted}>
|
||||
<ListChecks className="h-4 w-4" />
|
||||
Archive completed
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
const listBody = items.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
|
||||
<Inbox className="mb-3 h-8 w-8 text-muted-foreground/50" />
|
||||
<p className="text-sm">No notifications</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{items.map((item) => (
|
||||
<InboxListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={(item.issue_id ?? item.id) === selectedKey}
|
||||
onClick={() => handleSelect(item)}
|
||||
onArchive={() => handleArchive(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const detailContent = selected?.issue_id ? (
|
||||
<IssueDetail
|
||||
key={selected.id}
|
||||
issueId={selected.issue_id}
|
||||
defaultSidebarOpen={false}
|
||||
layoutId="multica_inbox_issue_detail_layout"
|
||||
highlightCommentId={selected.details?.comment_id ?? undefined}
|
||||
onDelete={() => {
|
||||
handleArchive(selected.id);
|
||||
}}
|
||||
/>
|
||||
) : selected ? (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold">{selected.title}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{typeLabels[selected.type]} · {timeAgo(selected.created_at)}
|
||||
</p>
|
||||
{selected.body && (
|
||||
<div className="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-foreground/80">
|
||||
{selected.body}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleArchive(selected.id)}
|
||||
>
|
||||
<Archive className="mr-1.5 h-3.5 w-3.5" />
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// -- Mobile layout: list / detail toggle -----------------------------------
|
||||
|
||||
if (isMobile) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col min-h-0">
|
||||
<div className="flex h-12 shrink-0 items-center border-b px-4">
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto space-y-1 p-2">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3 px-4 py-2.5">
|
||||
<Skeleton className="h-7 w-7 shrink-0 rounded-full" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile: show detail full-screen when an item is selected
|
||||
if (selected) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col min-h-0">
|
||||
<div className="flex h-12 shrink-0 items-center border-b px-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedKey("")}
|
||||
className="gap-1.5 text-muted-foreground"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Inbox
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
{detailContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile: full-screen list
|
||||
return (
|
||||
<div className="flex flex-1 flex-col min-h-0">
|
||||
{listHeader}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
{listBody}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -- Desktop layout: resizable two-panel -----------------------------------
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ResizablePanelGroup orientation="horizontal" className="flex-1 min-h-0" defaultLayout={defaultLayout} onLayoutChanged={onLayoutChanged}>
|
||||
@@ -162,111 +325,17 @@ export function InboxPage() {
|
||||
return (
|
||||
<ResizablePanelGroup orientation="horizontal" className="flex-1 min-h-0" defaultLayout={defaultLayout} onLayoutChanged={onLayoutChanged}>
|
||||
<ResizablePanel id="list" defaultSize={320} minSize={240} maxSize={480} groupResizeBehavior="preserve-pixel-size">
|
||||
{/* Left column -- inbox list */}
|
||||
<div className="flex flex-col border-r h-full">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-sm font-semibold">Inbox</h1>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-auto">
|
||||
<DropdownMenuItem onClick={handleMarkAllRead}>
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
Mark all as read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleArchiveAll}>
|
||||
<Archive className="h-4 w-4" />
|
||||
Archive all
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleArchiveAllRead}>
|
||||
<BookCheck className="h-4 w-4" />
|
||||
Archive all read
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleArchiveCompleted}>
|
||||
<ListChecks className="h-4 w-4" />
|
||||
Archive completed
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{listHeader}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
{items.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
|
||||
<Inbox className="mb-3 h-8 w-8 text-muted-foreground/50" />
|
||||
<p className="text-sm">No notifications</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{items.map((item) => (
|
||||
<InboxListItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={(item.issue_id ?? item.id) === selectedKey}
|
||||
onClick={() => handleSelect(item)}
|
||||
onArchive={() => handleArchive(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{listBody}
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel id="detail" minSize="40%">
|
||||
{/* Right column -- detail */}
|
||||
<div className="flex flex-col min-h-0 h-full">
|
||||
{selected?.issue_id ? (
|
||||
<IssueDetail
|
||||
key={selected.id}
|
||||
issueId={selected.issue_id}
|
||||
defaultSidebarOpen={false}
|
||||
layoutId="multica_inbox_issue_detail_layout"
|
||||
highlightCommentId={selected.details?.comment_id ?? undefined}
|
||||
onDelete={() => {
|
||||
handleArchive(selected.id);
|
||||
}}
|
||||
/>
|
||||
) : selected ? (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold">{selected.title}</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{typeLabels[selected.type]} · {timeAgo(selected.created_at)}
|
||||
</p>
|
||||
{selected.body && (
|
||||
<div className="mt-4 whitespace-pre-wrap text-sm leading-relaxed text-foreground/80">
|
||||
{selected.body}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleArchive(selected.id)}
|
||||
>
|
||||
<Archive className="mr-1.5 h-3.5 w-3.5" />
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
{detailContent ?? (
|
||||
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||
<Inbox className="mb-3 h-10 w-10 text-muted-foreground/30" />
|
||||
<p className="text-sm">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { SidebarProvider, SidebarInset } from "@multica/ui/components/ui/sidebar";
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@multica/ui/components/ui/sidebar";
|
||||
import { ModalRegistry } from "../modals/registry";
|
||||
import { AppSidebar } from "./app-sidebar";
|
||||
import { DashboardGuard } from "./dashboard-guard";
|
||||
@@ -34,6 +34,9 @@ export function DashboardLayout({
|
||||
<SidebarProvider className="h-svh">
|
||||
<AppSidebar searchSlot={searchSlot} />
|
||||
<SidebarInset className="overflow-hidden">
|
||||
<div className="flex h-10 shrink-0 items-center border-b px-2 md:hidden">
|
||||
<SidebarTrigger />
|
||||
</div>
|
||||
{children}
|
||||
<ModalRegistry />
|
||||
</SidebarInset>
|
||||
|
||||
Reference in New Issue
Block a user