feat: responsive design

This commit is contained in:
d-kimsuon
2025-09-03 18:16:08 +09:00
parent 9d7028cce8
commit 35329882f9
6 changed files with 191 additions and 103 deletions

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>,
];
})}

View File

@@ -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>
);

View File

@@ -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>
</>
);
};