mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-25 09:14:22 +01:00
feat: responsive design
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ChevronDownIcon,
|
||||
FolderIcon,
|
||||
MessageSquareIcon,
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SettingsControls } from "@/components/SettingsControls";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -18,6 +20,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { useConfig } from "../../../hooks/useConfig";
|
||||
import { projectQueryConfig, useProject } from "../hooks/useProject";
|
||||
import { firstCommandToTitle } from "../services/firstCommandToTitle";
|
||||
@@ -29,6 +36,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
} = useProject(projectId);
|
||||
const { config } = useConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed
|
||||
useEffect(() => {
|
||||
@@ -44,48 +52,77 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<header className="mb-8">
|
||||
<div className="container mx-auto px-2 sm:px-4 py-4 sm:py-8 max-w-6xl">
|
||||
<header className="mb-6 sm:mb-8">
|
||||
<Button asChild variant="ghost" className="mb-4">
|
||||
<Link href="/projects" className="flex items-center gap-2">
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back to Projects
|
||||
<span className="hidden sm:inline">Back to Projects</span>
|
||||
<span className="sm:hidden">Back</span>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderIcon className="w-6 h-6" />
|
||||
<h1 className="text-3xl font-bold">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-2">
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
||||
<FolderIcon className="w-5 h-5 sm:w-6 sm:h-6 flex-shrink-0" />
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold break-words overflow-hidden">
|
||||
{project.meta.projectPath ?? project.claudeProjectPath}
|
||||
</h1>
|
||||
</div>
|
||||
<NewChatModal
|
||||
projectId={projectId}
|
||||
trigger={
|
||||
<Button size="lg" className="gap-2">
|
||||
<PlusIcon className="w-5 h-5" />
|
||||
Start New Chat
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex-shrink-0">
|
||||
<NewChatModal
|
||||
projectId={projectId}
|
||||
trigger={
|
||||
<Button size="lg" className="gap-2 w-full sm:w-auto">
|
||||
<PlusIcon className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
<span className="hidden sm:inline">Start New Chat</span>
|
||||
<span className="sm:hidden">New Chat</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground font-mono text-sm">
|
||||
<p className="text-muted-foreground font-mono text-xs sm:text-sm break-all">
|
||||
History File: {project.claudeProjectPath ?? "unknown"}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-4">
|
||||
Conversation Sessions{" "}
|
||||
{sessions.length > 0 ? `(${sessions.length})` : ""}
|
||||
</h2>
|
||||
|
||||
{/* Filter Controls */}
|
||||
<div className="mb-6 p-4 bg-muted/50 rounded-lg">
|
||||
<SettingsControls onConfigChange={handleConfigChange} />
|
||||
</div>
|
||||
<Collapsible open={isSettingsOpen} onOpenChange={setIsSettingsOpen}>
|
||||
<div className="mb-6">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-between mb-2 h-auto py-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
<span className="font-medium">Filter Settings</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({sessions.length} sessions)
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`w-4 h-4 transition-transform ${
|
||||
isSettingsOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="p-4 bg-muted/50 rounded-lg border">
|
||||
<SettingsControls onConfigChange={handleConfigChange} />
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
{sessions.length === 0 ? (
|
||||
<Card>
|
||||
@@ -117,7 +154,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="break-all overflow-ellipsis line-clamp-2 text-xl">
|
||||
<span className="break-words overflow-ellipsis line-clamp-2 text-lg sm:text-xl">
|
||||
{session.meta.firstCommand !== null
|
||||
? firstCommandToTitle(session.meta.firstCommand)
|
||||
: session.id}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ArrowLeftIcon, LoaderIcon, PauseIcon, XIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
LoaderIcon,
|
||||
MenuIcon,
|
||||
PauseIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -41,6 +47,7 @@ export const SessionPageContent: FC<{
|
||||
|
||||
const [previousConversationLength, setPreviousConversationLength] =
|
||||
useState(0);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自動スクロール処理
|
||||
@@ -62,27 +69,44 @@ export const SessionPageContent: FC<{
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<SessionSidebar currentSessionId={sessionId} projectId={projectId} />
|
||||
<SessionSidebar
|
||||
currentSessionId={sessionId}
|
||||
projectId={projectId}
|
||||
isMobileOpen={isMobileSidebarOpen}
|
||||
onMobileOpenChange={setIsMobileSidebarOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="max-w-none flex-1 overflow-y-auto"
|
||||
>
|
||||
<header className="px-3 py-3 sticky top-0 z-10 bg-background w-full">
|
||||
<Button asChild variant="ghost">
|
||||
<Link
|
||||
href={`/projects/${projectId}`}
|
||||
className="flex items-center gap-2"
|
||||
<header className="px-2 sm:px-3 py-3 sticky top-0 z-10 bg-background w-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="md:hidden"
|
||||
onClick={() => setIsMobileSidebarOpen(true)}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
Back to Session List
|
||||
</Link>
|
||||
</Button>
|
||||
<MenuIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="ghost">
|
||||
<Link
|
||||
href={`/projects/${projectId}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Back to Session List</span>
|
||||
<span className="sm:hidden">Back</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-3xl font-bold break-all overflow-ellipsis line-clamp-2 px-5">
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold break-words overflow-ellipsis line-clamp-2 px-2 sm:px-5">
|
||||
{session.meta.firstCommand !== null
|
||||
? firstCommandToTitle(session.meta.firstCommand)
|
||||
: sessionId}
|
||||
@@ -137,7 +161,7 @@ export const SessionPageContent: FC<{
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="w-full px-20 pb-10 relative z-5">
|
||||
<main className="w-full px-4 sm:px-8 md:px-12 lg:px-16 xl:px-20 pb-10 relative z-5">
|
||||
<ConversationList
|
||||
conversations={conversations}
|
||||
getToolResult={getToolResult}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const AssistantConversationContent: FC<{
|
||||
}> = ({ content, getToolResult }) => {
|
||||
if (content.type === "text") {
|
||||
return (
|
||||
<div className="w-full mx-2 my-6">
|
||||
<div className="w-full mx-1 sm:mx-2 my-4 sm:my-6">
|
||||
<MarkdownContent content={content.text} />
|
||||
</div>
|
||||
);
|
||||
@@ -111,7 +111,7 @@ export const AssistantConversationContent: FC<{
|
||||
<CollapsibleContent>
|
||||
<div className="bg-background rounded border p-2 mt-1">
|
||||
{typeof toolResult.content === "string" ? (
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-line">
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap break-words">
|
||||
{toolResult.content}
|
||||
</pre>
|
||||
) : (
|
||||
@@ -129,7 +129,7 @@ export const AssistantConversationContent: FC<{
|
||||
return (
|
||||
<pre
|
||||
key={item.text}
|
||||
className="text-xs overflow-x-auto whitespace-pre-line"
|
||||
className="text-xs overflow-x-auto whitespace-pre-wrap break-words"
|
||||
>
|
||||
{item.text}
|
||||
</pre>
|
||||
|
||||
@@ -66,7 +66,7 @@ export const ConversationList: FC<ConversationListProps> = ({
|
||||
}`}
|
||||
key={getConversationKey(conversation)}
|
||||
>
|
||||
<div className="w-[85%]">{elm}</div>
|
||||
<div className="w-full max-w-4xl sm:w-[90%] md:w-[85%]">{elm}</div>
|
||||
</li>,
|
||||
];
|
||||
})}
|
||||
|
||||
@@ -78,7 +78,9 @@ export const UserTextContent: FC<{ text: string; id?: string }> = ({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="py-0 px-4">
|
||||
<pre className="text-xs overflow-x-auto">{parsed.stdout}</pre>
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap break-words">
|
||||
{parsed.stdout}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ListTodoIcon, MessageSquareIcon, SettingsIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useProject } from "../../../../hooks/useProject";
|
||||
import { SessionsTab } from "./SessionsTab";
|
||||
@@ -12,7 +13,15 @@ export const SessionSidebar: FC<{
|
||||
currentSessionId: string;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
}> = ({ currentSessionId, projectId, className }) => {
|
||||
isMobileOpen?: boolean;
|
||||
onMobileOpenChange?: (open: boolean) => void;
|
||||
}> = ({
|
||||
currentSessionId,
|
||||
projectId,
|
||||
className,
|
||||
isMobileOpen = false,
|
||||
onMobileOpenChange,
|
||||
}) => {
|
||||
const {
|
||||
data: { sessions },
|
||||
} = useProject(projectId);
|
||||
@@ -51,71 +60,87 @@ export const SessionSidebar: FC<{
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("hidden md:flex h-full", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full border-r border-sidebar-border transition-all duration-300 ease-in-out flex bg-sidebar text-sidebar-foreground",
|
||||
isExpanded ? "w-72 lg:w-80" : "w-12",
|
||||
)}
|
||||
>
|
||||
{/* Vertical Icon Menu - Always Visible */}
|
||||
<div className="w-12 flex flex-col border-r border-sidebar-border bg-sidebar/50">
|
||||
<div className="flex flex-col p-2 space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("sessions")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "sessions" && isExpanded
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
title="Sessions"
|
||||
>
|
||||
<MessageSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
const sidebarContent = (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full border-r border-sidebar-border transition-all duration-300 ease-in-out flex bg-sidebar text-sidebar-foreground",
|
||||
isExpanded ? "w-72 lg:w-80" : "w-12",
|
||||
)}
|
||||
>
|
||||
{/* Vertical Icon Menu - Always Visible */}
|
||||
<div className="w-12 flex flex-col border-r border-sidebar-border bg-sidebar/50">
|
||||
<div className="flex flex-col p-2 space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("sessions")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "sessions" && isExpanded
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
title="Sessions"
|
||||
>
|
||||
<MessageSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("tasks")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "tasks" && isExpanded
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
title="Tasks"
|
||||
>
|
||||
<ListTodoIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("tasks")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "tasks" && isExpanded
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
title="Tasks"
|
||||
>
|
||||
<ListTodoIcon className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("settings")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "settings" && isExpanded
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
title="Settings"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTabClick("settings")}
|
||||
className={cn(
|
||||
"w-8 h-8 flex items-center justify-center rounded-md transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
activeTab === "settings" && isExpanded
|
||||
? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
|
||||
: "text-sidebar-foreground/70",
|
||||
)}
|
||||
title="Settings"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Area - Only shown when expanded */}
|
||||
{isExpanded && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{renderContent()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Area - Only shown when expanded */}
|
||||
{isExpanded && (
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{renderContent()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop sidebar */}
|
||||
<div className={cn("hidden md:flex h-full", className)}>
|
||||
{sidebarContent}
|
||||
</div>
|
||||
|
||||
{/* Mobile sidebar - rendered in dialog */}
|
||||
<div className="md:hidden">
|
||||
<Dialog open={isMobileOpen} onOpenChange={onMobileOpenChange}>
|
||||
<DialogContent className="p-0 max-w-sm w-full h-[90vh] max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="flex-1 overflow-hidden">{sidebarContent}</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user