From 30212c0d68da440d63cf49ef63d84edb5e629172 Mon Sep 17 00:00:00 2001 From: Peter vogel Date: Mon, 12 Jan 2026 23:49:55 +0100 Subject: [PATCH 1/3] feat: responsive layouts for phone and tablet --- src-tauri/tauri.conf.json | 4 +- src/App.tsx | 608 ++++++++++++++++++++++++++++++---- src/components/DebugPanel.tsx | 9 +- src/components/Sidebar.tsx | 2 + src/components/TabBar.tsx | 34 ++ src/components/TabletNav.tsx | 35 ++ src/components/icons.tsx | 82 +++++ src/hooks/useLayoutMode.ts | 32 ++ src/styles/base.css | 1 + src/styles/compact.css | 293 ++++++++++++++++ src/styles/sidebar.css | 8 + src/styles/tabbar.css | 58 ++++ 12 files changed, 1106 insertions(+), 60 deletions(-) create mode 100644 src/components/TabBar.tsx create mode 100644 src/components/TabletNav.tsx create mode 100644 src/components/icons.tsx create mode 100644 src/hooks/useLayoutMode.ts create mode 100644 src/styles/compact.css create mode 100644 src/styles/tabbar.css diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7f4d75615..e3427cf48 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,8 +16,8 @@ "title": "CodexMonitor", "width": 1200, "height": 700, - "minWidth": 1200, - "minHeight": 700, + "minWidth": 360, + "minHeight": 600, "titleBarStyle": "Overlay", "hiddenTitle": true, "transparent": true, diff --git a/src/App.tsx b/src/App.tsx index 2f428ea9e..a980bb97b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,8 @@ import "./styles/diff-viewer.css"; import "./styles/debug.css"; import "./styles/plan.css"; import "./styles/about.css"; +import "./styles/tabbar.css"; +import "./styles/compact.css"; import { Sidebar } from "./components/Sidebar"; import { Home } from "./components/Home"; import { MainHeader } from "./components/MainHeader"; @@ -24,6 +26,8 @@ import { GitDiffViewer } from "./components/GitDiffViewer"; import { DebugPanel } from "./components/DebugPanel"; import { PlanPanel } from "./components/PlanPanel"; import { AboutView } from "./components/AboutView"; +import { TabBar } from "./components/TabBar"; +import { TabletNav } from "./components/TabletNav"; import { useWorkspaces } from "./hooks/useWorkspaces"; import { useThreads } from "./hooks/useThreads"; import { useWindowDrag } from "./hooks/useWindowDrag"; @@ -37,6 +41,7 @@ import { useDebugLog } from "./hooks/useDebugLog"; import { useWorkspaceRefreshOnFocus } from "./hooks/useWorkspaceRefreshOnFocus"; import { useWorkspaceRestore } from "./hooks/useWorkspaceRestore"; import { useResizablePanels } from "./hooks/useResizablePanels"; +import { useLayoutMode } from "./hooks/useLayoutMode"; import type { AccessMode, QueuedMessage } from "./types"; function useWindowLabel() { @@ -61,10 +66,18 @@ function MainApp() { planPanelHeight, onPlanPanelResizeStart, } = useResizablePanels(); + const layoutMode = useLayoutMode(); + const isCompact = layoutMode !== "desktop"; + const isTablet = layoutMode === "tablet"; + const isPhone = layoutMode === "phone"; const [centerMode, setCenterMode] = useState<"chat" | "diff">("chat"); const [selectedDiffPath, setSelectedDiffPath] = useState(null); const [gitPanelMode, setGitPanelMode] = useState<"diff" | "log">("diff"); const [accessMode, setAccessMode] = useState("current"); + const [activeTab, setActiveTab] = useState< + "projects" | "codex" | "git" | "log" + >("codex"); + const tabletTab = activeTab === "projects" ? "codex" : activeTab; const [queuedByThread, setQueuedByThread] = useState< Record >({}); @@ -97,11 +110,14 @@ function MainApp() { const { status: gitStatus, refresh: refreshGitStatus } = useGitStatus(activeWorkspace); + const compactTab = isTablet ? tabletTab : activeTab; + const shouldLoadDiffs = + centerMode === "diff" || (isCompact && compactTab === "git"); const { diffs: gitDiffs, isLoading: isDiffLoading, error: diffError, - } = useGitDiffs(activeWorkspace, gitStatus.files, centerMode === "diff"); + } = useGitDiffs(activeWorkspace, gitStatus.files, shouldLoadDiffs); const { entries: gitLogEntries, total: gitLogTotal, @@ -182,6 +198,24 @@ function MainApp() { ? queuedByThread[activeThreadId] ?? [] : []; + useEffect(() => { + if (!isPhone) { + return; + } + if (!activeWorkspace && activeTab !== "projects") { + setActiveTab("projects"); + } + }, [activeTab, activeWorkspace, isPhone]); + + useEffect(() => { + if (!isTablet) { + return; + } + if (activeTab === "projects") { + setActiveTab("codex"); + } + }, [activeTab, isTablet]); + useWindowDrag("titlebar"); useWorkspaceRestore({ workspaces, @@ -196,9 +230,24 @@ function MainApp() { }); async function handleAddWorkspace() { - const workspace = await addWorkspace(); - if (workspace) { - setActiveThreadId(null, workspace.id); + try { + const workspace = await addWorkspace(); + if (workspace) { + setActiveThreadId(null, workspace.id); + if (isCompact) { + setActiveTab("codex"); + } + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + addDebugEntry({ + id: `${Date.now()}-client-add-workspace-error`, + timestamp: Date.now(), + source: "error", + label: "workspace/add error", + payload: message, + }); + alert(`Failed to add workspace.\n\n${message}`); } } @@ -211,6 +260,9 @@ function MainApp() { }); } setActiveWorkspaceId(workspaceId); + if (isCompact) { + setActiveTab("codex"); + } } function exitDiffView() { @@ -225,12 +277,18 @@ function MainApp() { await connectWorkspace(workspace); } await startThreadForWorkspace(workspace.id); + if (isCompact) { + setActiveTab("codex"); + } } function handleSelectDiff(path: string) { setSelectedDiffPath(path); setCenterMode("diff"); setGitPanelMode("diff"); + if (isCompact) { + setActiveTab("git"); + } } async function handleSend(text: string) { @@ -306,56 +364,74 @@ function MainApp() { sendUserMessage, ]); - return ( -
-
- { - exitDiffView(); - setActiveWorkspaceId(null); - }} - onSelectWorkspace={(workspaceId) => { - exitDiffView(); - selectWorkspace(workspaceId); - }} - onConnectWorkspace={connectWorkspace} - onAddAgent={handleAddAgent} - onToggleWorkspaceCollapse={(workspaceId, collapsed) => { - const target = workspaces.find((entry) => entry.id === workspaceId); - if (!target) { - return; - } - void updateWorkspaceSettings(workspaceId, { - ...target.settings, - sidebarCollapsed: collapsed, - }); - }} - onSelectThread={(workspaceId, threadId) => { - exitDiffView(); - selectWorkspace(workspaceId); - setActiveThreadId(threadId, workspaceId); - }} - onDeleteThread={(workspaceId, threadId) => { - removeThread(workspaceId, threadId); - }} - /> + const handleDebugClick = () => { + if (isCompact) { + setActiveTab("log"); + return; + } + setDebugOpen((prev) => !prev); + }; + + const showComposer = !isCompact + ? centerMode === "chat" + : (isTablet ? tabletTab : activeTab) === "codex"; + const showGitDetail = Boolean(selectedDiffPath) && isPhone; + const appClassName = `app ${isCompact ? "layout-compact" : "layout-desktop"}${ + isPhone ? " layout-phone" : "" + }${isTablet ? " layout-tablet" : ""}`; + + const sidebarNode = ( + { + exitDiffView(); + setActiveWorkspaceId(null); + if (isCompact) { + setActiveTab("projects"); + } + }} + onSelectWorkspace={(workspaceId) => { + exitDiffView(); + selectWorkspace(workspaceId); + }} + onConnectWorkspace={async (workspace) => { + await connectWorkspace(workspace); + if (isCompact) { + setActiveTab("codex"); + } + }} + onAddAgent={handleAddAgent} + onToggleWorkspaceCollapse={(workspaceId, collapsed) => { + const target = workspaces.find((entry) => entry.id === workspaceId); + if (!target) { + return; + } + void updateWorkspaceSettings(workspaceId, { + ...target.settings, + sidebarCollapsed: collapsed, + }); + }} + onSelectThread={(workspaceId, threadId) => { + exitDiffView(); + selectWorkspace(workspaceId); + setActiveThreadId(threadId, workspaceId); + }} + onDeleteThread={(workspaceId, threadId) => { + removeThread(workspaceId, threadId); + }} + /> + ); + + const desktopLayout = ( + <> + {sidebarNode}
setDebugOpen((prev) => !prev)} + onClick={handleDebugClick} aria-label="Debug" > @@ -495,7 +571,7 @@ function MainApp() {
- {centerMode === "chat" && ( + {showComposer && ( )} + + ); + + const tabletLayout = ( + <> + +
{sidebarNode}
+
+
+ + {showHome && ( + {}} + /> + )} + {activeWorkspace && !showHome && ( + <> +
+
+ +
+
+ {hasDebugAlerts && ( + + )} +
+
+ {tabletTab === "codex" && ( + <> +
+ +
+ {showComposer && ( + { + if (prefillDraft?.id === id) { + setPrefillDraft(null); + } + }} + onEditQueued={(item) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== item.id, + ), + })); + setPrefillDraft(item); + }} + onDeleteQueued={(id) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== id, + ), + })); + }} + models={models} + selectedModelId={selectedModelId} + onSelectModel={setSelectedModelId} + reasoningOptions={reasoningOptions} + selectedEffort={selectedEffort} + onSelectEffort={setSelectedEffort} + accessMode={accessMode} + onSelectAccessMode={setAccessMode} + skills={skills} + /> + )} + + )} + {tabletTab === "git" && ( +
+ +
+ +
+
+ )} + {tabletTab === "log" && ( + + )} + + )} +
+ + ); + + const phoneLayout = ( +
+ + {activeTab === "projects" &&
{sidebarNode}
} + {activeTab === "codex" && ( +
+ {activeWorkspace ? ( + <> +
+
+ +
+
+ {hasDebugAlerts && ( + + )} +
+
+
+ +
+ {showComposer && ( + { + if (prefillDraft?.id === id) { + setPrefillDraft(null); + } + }} + onEditQueued={(item) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== item.id, + ), + })); + setPrefillDraft(item); + }} + onDeleteQueued={(id) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== id, + ), + })); + }} + models={models} + selectedModelId={selectedModelId} + onSelectModel={setSelectedModelId} + reasoningOptions={reasoningOptions} + selectedEffort={selectedEffort} + onSelectEffort={setSelectedEffort} + accessMode={accessMode} + onSelectAccessMode={setAccessMode} + skills={skills} + /> + )} + + ) : ( +
+

No workspace selected

+

Choose a project to start chatting.

+ +
+ )} +
+ )} + {activeTab === "git" && ( +
+ {!activeWorkspace && ( +
+

No workspace selected

+

Select a project to inspect diffs.

+ +
+ )} + {activeWorkspace && showGitDetail && ( + <> +
+ + Diff +
+
+ +
+ + )} + {activeWorkspace && !showGitDetail && ( + <> +
+
+ +
+
+
+
+ +
+ {!isPhone && ( +
+ +
+ )} +
+ + )} +
+ )} + {activeTab === "log" && ( +
+ +
+ )} + +
+ ); + + return ( +
+
+ {isPhone ? phoneLayout : isTablet ? tabletLayout : desktopLayout}
); } diff --git a/src/components/DebugPanel.tsx b/src/components/DebugPanel.tsx index c037246ba..57b0cea4b 100644 --- a/src/components/DebugPanel.tsx +++ b/src/components/DebugPanel.tsx @@ -5,6 +5,7 @@ type DebugPanelProps = { isOpen: boolean; onClear: () => void; onCopy: () => void; + variant?: "dock" | "full"; }; function formatPayload(payload: unknown) { @@ -26,13 +27,17 @@ export function DebugPanel({ isOpen, onClear, onCopy, + variant = "dock", }: DebugPanelProps) { - if (!isOpen) { + const isVisible = variant === "full" || isOpen; + if (!isVisible) { return null; } return ( -
+
Debug
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 219e9907b..84233ddd2 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,4 +1,5 @@ import type { RateLimitSnapshot, ThreadSummary, WorkspaceInfo } from "../types"; +import { IconProjects } from "./icons"; import { useState } from "react"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { LogicalPosition } from "@tauri-apps/api/dpi"; @@ -118,6 +119,7 @@ export function Sidebar({ data-tauri-drag-region="false" aria-label="Open home" > + Projects
diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx new file mode 100644 index 000000000..c183a542b --- /dev/null +++ b/src/components/TabBar.tsx @@ -0,0 +1,34 @@ +import { IconCodex, IconGit, IconLog, IconProjects } from "./icons"; + +type TabKey = "projects" | "codex" | "git" | "log"; + +type TabBarProps = { + activeTab: TabKey; + onSelect: (tab: TabKey) => void; +}; + +const tabs: { id: TabKey; label: string; icon: JSX.Element }[] = [ + { id: "projects", label: "Projects", icon: }, + { id: "codex", label: "Codex", icon: }, + { id: "git", label: "Git", icon: }, + { id: "log", label: "Log", icon: }, +]; + +export function TabBar({ activeTab, onSelect }: TabBarProps) { + return ( + + ); +} diff --git a/src/components/TabletNav.tsx b/src/components/TabletNav.tsx new file mode 100644 index 000000000..a5501616a --- /dev/null +++ b/src/components/TabletNav.tsx @@ -0,0 +1,35 @@ +import { IconCodex, IconGit, IconLog } from "./icons"; + +type TabletNavTab = "codex" | "git" | "log"; + +type TabletNavProps = { + activeTab: TabletNavTab; + onSelect: (tab: TabletNavTab) => void; +}; + +const tabs: { id: TabletNavTab; label: string; icon: JSX.Element }[] = [ + { id: "codex", label: "Codex", icon: }, + { id: "git", label: "Git", icon: }, + { id: "log", label: "Log", icon: }, +]; + +export function TabletNav({ activeTab, onSelect }: TabletNavProps) { + return ( + + ); +} diff --git a/src/components/icons.tsx b/src/components/icons.tsx new file mode 100644 index 000000000..87366ccf2 --- /dev/null +++ b/src/components/icons.tsx @@ -0,0 +1,82 @@ +type IconProps = { + className?: string; +}; + +export function IconProjects({ className }: IconProps) { + return ( + + + + ); +} + +export function IconCodex({ className }: IconProps) { + return ( + + + + + + ); +} + +export function IconGit({ className }: IconProps) { + return ( + + + + + + + + ); +} + +export function IconLog({ className }: IconProps) { + return ( + + + + + + + + + ); +} diff --git a/src/hooks/useLayoutMode.ts b/src/hooks/useLayoutMode.ts new file mode 100644 index 000000000..386c82b79 --- /dev/null +++ b/src/hooks/useLayoutMode.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react"; + +export type LayoutMode = "desktop" | "tablet" | "phone"; + +const TABLET_MAX_WIDTH = 1100; +const PHONE_MAX_WIDTH = 520; + +function getLayoutMode(width: number): LayoutMode { + if (width <= PHONE_MAX_WIDTH) { + return "phone"; + } + if (width <= TABLET_MAX_WIDTH) { + return "tablet"; + } + return "desktop"; +} + +export function useLayoutMode() { + const [mode, setMode] = useState(() => + getLayoutMode(window.innerWidth), + ); + + useEffect(() => { + function handleResize() { + setMode(getLayoutMode(window.innerWidth)); + } + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return mode; +} diff --git a/src/styles/base.css b/src/styles/base.css index 00e5374b9..6e7120d49 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -54,6 +54,7 @@ --text-accent: rgba(164, 195, 255, 0.7); --shadow-accent: rgba(92, 168, 255, 0.28); --select-caret: rgba(255, 255, 255, 0.6); + --tabbar-height: 56px; } @media (prefers-color-scheme: light) { diff --git a/src/styles/compact.css b/src/styles/compact.css new file mode 100644 index 000000000..996812614 --- /dev/null +++ b/src/styles/compact.css @@ -0,0 +1,293 @@ +.app.layout-compact .sidebar-resizer, +.app.layout-compact .right-panel-resizer, +.app.layout-compact .right-panel, +.app.layout-compact .right-panel-divider { + display: none; +} + +.app.layout-phone { + display: flex; + flex-direction: column; + grid-template-columns: none; + grid-template-rows: none; +} + +.app.layout-phone .drag-strip { + height: 22px; +} + +.compact-shell { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + position: relative; +} + +.compact-panel { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.compact-panel .sidebar { + border-right: none; + padding: 32px 16px 16px; + flex: 1; + min-height: 0; +} + +.app.layout-phone .sidebar { + flex-direction: column; + align-items: stretch; + overflow-x: hidden; +} + +.compact-topbar { + padding: 10px 16px 8px; +} + +.compact-content { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.app.layout-phone .messages-full { + padding: 12px 16px 16px; +} + +.app.layout-phone .composer { + padding: 12px 16px 18px; +} + +.compact-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + color: var(--text-muted); + text-align: center; + padding: 0 24px; +} + +.compact-empty h3 { + margin: 0; + font-size: 16px; + color: var(--text-strong); +} + +.compact-empty p { + margin: 0; + font-size: 13px; +} + +.compact-git { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; + min-height: 0; +} + +.compact-git-list { + flex: 1; + min-height: 0; + overflow: hidden; +} + +.compact-git-viewer { + flex: 1; + min-height: 0; + overflow: hidden; +} + +.compact-git-viewer .diff-viewer { + height: 100%; +} + +.compact-git-back { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid var(--border-subtle); + background: var(--surface-topbar); +} + +.compact-git-back button { + border: none; + background: transparent; + color: var(--text-strong); + font-size: 14px; + cursor: pointer; +} + +.debug-panel.full { + height: 100%; + border-top: none; +} + +.debug-panel.full .debug-list { + flex: 1; +} + +@media (max-width: 960px) { + .app.layout-compact .sidebar-resizer { + display: none; + } +} + +.app.layout-tablet { + display: grid; + grid-template-columns: var(--tablet-nav-width, 72px) var(--sidebar-width, 280px) minmax(0, 1fr); + grid-template-rows: 1fr; + --tablet-nav-width: 72px; +} + +.tablet-nav { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 10px; + padding: 36px 8px 16px; + background: var(--surface-topbar); + border-right: 1px solid var(--border-subtle); + min-height: 0; + height: 100%; +} + +.tablet-nav-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.tablet-nav-item { + border: 1px solid transparent; + background: transparent; + color: var(--text-subtle); + border-radius: 12px; + padding: 10px 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + transition: all 0.15s ease; +} + +.tablet-nav-item:hover { + color: var(--text-stronger); + background: var(--surface-hover); +} + +.tablet-nav-item.active { + color: var(--text-strong); + background: var(--surface-control-hover); + border-color: var(--border-accent-soft); +} + +.tablet-nav-label { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.tablet-nav-icon { + width: 20px; + height: 20px; +} + +.tablet-projects { + border-right: 1px solid var(--border-subtle); + min-height: 0; + height: 100%; + overflow: hidden; +} + +.tablet-projects .sidebar { + flex-direction: column; + align-items: stretch; + overflow-x: hidden; + height: 100%; + min-height: 0; +} + +.tablet-main { + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; + height: 100%; +} + +.tablet-topbar { + padding: 10px 20px 8px; +} + +.tablet-content { + flex: 1; + min-height: 0; +} + +.app.layout-tablet .messages-full { + padding: 12px 20px 16px; +} + +.app.layout-tablet .composer { + padding: 12px 20px 18px; +} + +.tablet-git { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; + min-height: 0; +} + +.tablet-git-viewer { + flex: 1; + min-height: 0; + overflow: hidden; +} + +.tablet-git-viewer .diff-viewer { + height: 100%; +} + +.projects-resizer { + position: absolute; + top: 0; + bottom: 0; + left: calc(var(--tablet-nav-width, 72px) + var(--sidebar-width, 280px) - 4px); + width: 8px; + cursor: col-resize; + z-index: 3; +} + +.projects-resizer::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 3px; + width: 1px; + background: var(--border-strong); + opacity: 0; + transition: opacity 0.15s ease; +} + +.projects-resizer:hover::after { + opacity: 1; +} diff --git a/src/styles/sidebar.css b/src/styles/sidebar.css index 796bab8ee..c28ab822f 100644 --- a/src/styles/sidebar.css +++ b/src/styles/sidebar.css @@ -24,6 +24,9 @@ font-size: 20px; font-weight: 600; margin-top: 6px; + display: inline-flex; + align-items: center; + gap: 8px; } .subtitle-button { @@ -36,6 +39,11 @@ -webkit-app-region: no-drag; } +.sidebar-nav-icon { + width: 16px; + height: 16px; +} + .sidebar-body { flex: 1; min-height: 0; diff --git a/src/styles/tabbar.css b/src/styles/tabbar.css new file mode 100644 index 000000000..e8e03f5d8 --- /dev/null +++ b/src/styles/tabbar.css @@ -0,0 +1,58 @@ +.tabbar { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); + border-top: 1px solid var(--border-subtle); + background: var(--surface-topbar); + backdrop-filter: blur(24px) saturate(1.2); + -webkit-app-region: no-drag; + min-height: var(--tabbar-height, 56px); +} + +.tabbar-item { + border: 1px solid transparent; + background: transparent; + color: var(--text-subtle); + padding: 8px 6px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.01em; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + cursor: pointer; + transition: all 0.15s ease; +} + +.tabbar-item:hover { + color: var(--text-stronger); + background: var(--surface-hover); +} + +.tabbar-item.active { + color: var(--text-strong); + background: var(--surface-control-hover); + border-color: var(--border-accent-soft); + box-shadow: 0 0 0 1px rgba(100, 200, 255, 0.2); +} + +.tabbar-label { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.tabbar-icon { + width: 18px; + height: 18px; +} + +@media (min-width: 521px) { + .tabbar { + display: none; + } +} From 637950e4e4d1e650557330dde9993fb41cd5d45c Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Tue, 13 Jan 2026 07:32:16 +0100 Subject: [PATCH 2/3] refactor: streamline layouts and switch to lucide icons --- package-lock.json | 10 + package.json | 1 + src/App.tsx | 363 ++++-------------- src/components/GitDiffPanel.tsx | 6 +- src/components/Sidebar.tsx | 4 +- src/components/TabBar.tsx | 13 +- src/components/TabletNav.tsx | 11 +- src/components/icons.tsx | 82 ---- src/styles/compact-base.css | 54 +++ src/styles/compact-phone.css | 88 +++++ .../{compact.css => compact-tablet.css} | 144 ------- src/styles/diff.css | 8 + 12 files changed, 261 insertions(+), 523 deletions(-) delete mode 100644 src/components/icons.tsx create mode 100644 src/styles/compact-base.css create mode 100644 src/styles/compact-phone.css rename src/styles/{compact.css => compact-tablet.css} (53%) diff --git a/package-lock.json b/package-lock.json index 17f981b23..f4732e5da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-opener": "^2", + "lucide-react": "^0.562.0", "prismjs": "^1.30.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -2050,6 +2051,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", diff --git a/package.json b/package.json index 93bce31c0..67371d0b0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-opener": "^2", + "lucide-react": "^0.562.0", "prismjs": "^1.30.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src/App.tsx b/src/App.tsx index 042270ae8..3f1a3e35b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,7 +14,9 @@ import "./styles/debug.css"; import "./styles/plan.css"; import "./styles/about.css"; import "./styles/tabbar.css"; -import "./styles/compact.css"; +import "./styles/compact-base.css"; +import "./styles/compact-phone.css"; +import "./styles/compact-tablet.css"; import { Sidebar } from "./components/Sidebar"; import { Home } from "./components/Home"; import { MainHeader } from "./components/MainHeader"; @@ -28,6 +30,7 @@ import { PlanPanel } from "./components/PlanPanel"; import { AboutView } from "./components/AboutView"; import { TabBar } from "./components/TabBar"; import { TabletNav } from "./components/TabletNav"; +import { TerminalSquare } from "lucide-react"; import { useWorkspaces } from "./hooks/useWorkspaces"; import { useThreads } from "./hooks/useThreads"; import { useWindowDrag } from "./hooks/useWindowDrag"; @@ -433,6 +436,73 @@ function MainApp() { /> ); + const messagesNode = ( + + ); + + const composerNode = showComposer ? ( + { + if (prefillDraft?.id === id) { + setPrefillDraft(null); + } + }} + onEditQueued={(item) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== item.id, + ), + })); + setPrefillDraft(item); + }} + onDeleteQueued={(id) => { + if (!activeThreadId) { + return; + } + setQueuedByThread((prev) => ({ + ...prev, + [activeThreadId]: (prev[activeThreadId] ?? []).filter( + (entry) => entry.id !== id, + ), + })); + }} + models={models} + selectedModelId={selectedModelId} + onSelectModel={setSelectedModelId} + reasoningOptions={reasoningOptions} + selectedEffort={selectedEffort} + onSelectEffort={setSelectedEffort} + accessMode={accessMode} + onSelectAccessMode={setAccessMode} + skills={skills} + /> + ) : null; + + const debugButton = hasDebugAlerts ? ( + + ) : null; + const desktopLayout = ( <> {sidebarNode} @@ -475,39 +545,7 @@ function MainApp() { />
- {hasDebugAlerts && ( - - )} + {debugButton}
) : ( - + messagesNode )}
@@ -575,59 +606,7 @@ function MainApp() {
- {showComposer && ( - { - if (prefillDraft?.id === id) { - setPrefillDraft(null); - } - }} - onEditQueued={(item) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== item.id, - ), - })); - setPrefillDraft(item); - }} - onDeleteQueued={(id) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== id, - ), - })); - }} - models={models} - selectedModelId={selectedModelId} - onSelectModel={setSelectedModelId} - reasoningOptions={reasoningOptions} - selectedEffort={selectedEffort} - onSelectEffort={setSelectedEffort} - accessMode={accessMode} - onSelectAccessMode={setAccessMode} - skills={skills} - /> - )} + {composerNode}
- {hasDebugAlerts && ( - - )} + {debugButton}
{tabletTab === "codex" && ( <>
- + {messagesNode}
- {showComposer && ( - { - if (prefillDraft?.id === id) { - setPrefillDraft(null); - } - }} - onEditQueued={(item) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== item.id, - ), - })); - setPrefillDraft(item); - }} - onDeleteQueued={(id) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== id, - ), - })); - }} - models={models} - selectedModelId={selectedModelId} - onSelectModel={setSelectedModelId} - reasoningOptions={reasoningOptions} - selectedEffort={selectedEffort} - onSelectEffort={setSelectedEffort} - accessMode={accessMode} - onSelectAccessMode={setAccessMode} - skills={skills} - /> - )} + {composerNode} )} {tabletTab === "git" && ( @@ -840,104 +728,13 @@ function MainApp() { />
- {hasDebugAlerts && ( - - )} + {debugButton}
- + {messagesNode}
- {showComposer && ( - { - if (prefillDraft?.id === id) { - setPrefillDraft(null); - } - }} - onEditQueued={(item) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== item.id, - ), - })); - setPrefillDraft(item); - }} - onDeleteQueued={(id) => { - if (!activeThreadId) { - return; - } - setQueuedByThread((prev) => ({ - ...prev, - [activeThreadId]: (prev[activeThreadId] ?? []).filter( - (entry) => entry.id !== id, - ), - })); - }} - models={models} - selectedModelId={selectedModelId} - onSelectModel={setSelectedModelId} - reasoningOptions={reasoningOptions} - selectedEffort={selectedEffort} - onSelectEffort={setSelectedEffort} - accessMode={accessMode} - onSelectAccessMode={setAccessMode} - skills={skills} - /> - )} + {composerNode} ) : (
diff --git a/src/components/GitDiffPanel.tsx b/src/components/GitDiffPanel.tsx index 231413e5f..cdd89f6d7 100644 --- a/src/components/GitDiffPanel.tsx +++ b/src/components/GitDiffPanel.tsx @@ -4,6 +4,7 @@ import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { LogicalPosition } from "@tauri-apps/api/dpi"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { openUrl } from "@tauri-apps/plugin-opener"; +import { GitBranch } from "lucide-react"; type GitDiffPanelProps = { mode: "diff" | "log"; @@ -151,7 +152,10 @@ export function GitDiffPanel({ return (