mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 08:03:46 +00:00
Refine Foundry UI layout and styling (#235)
* feat: modernize chat UI and rename handoff to task - Remove agent message bubbles, keep user bubbles (right-aligned) - Rename "Handoffs" to "Tasks" with ListChecks icon in sidebar - Move model picker inside composer, add renderFooter to ChatComposer SDK - Make project sections collapsible with hover-only chevrons - Remove divider between chat and composer - Update model picker chevron to flip on open/close - Replace all user-visible "handoff" strings with "task" across frontend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: real org mock data, model picker styling, project icons, task minutes indicator - Replace fake acme/* mock data with real rivet-dev GitHub org repos and PRs - Fix model picker popover: dark gray surface with backdrop blur instead of pure black - Add colored letter icons to project section headers (swap to chevron on hover) - Add "847 min used" indicator in transcript header - Rename browser tab title from OpenHandoff to Foundry - Reduce transcript header title font weight to 500 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: refine Foundry UI — single-line task cards, dark user bubbles, curved panel corners, send icon - Collapse task sidebar cards to single-line layout (title, number, diffs, timestamp) - Dark-themed user message bubbles matching site theme - Curved top-left corner on center chat panel with border line - Subtle focus border on composer input - Replace ArrowUpFromLine with SendHorizonal icon - Tab strip gaps, padding, and divider alignment fixes - Plus button with visible background - Right sidebar header color matching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
20082512a3
commit
e792a720a0
9 changed files with 201 additions and 123 deletions
|
|
@ -4,15 +4,15 @@ export const appTheme: Theme = createDarkTheme({
|
|||
colors: {
|
||||
primary: "#e4e4e7", // zinc-200
|
||||
accent: "#ff4f00", // orange accent (inspector)
|
||||
backgroundPrimary: "#000000", // pure black (inspector --bg)
|
||||
backgroundSecondary: "#0a0a0b", // near-black panels (inspector --bg-panel)
|
||||
backgroundTertiary: "#0a0a0b", // same as panel (border provides separation)
|
||||
backgroundPrimary: "#09090b", // darkest — chat center panel
|
||||
backgroundSecondary: "#0f0f11", // slightly lighter — sidebars
|
||||
backgroundTertiary: "#0c0c0e", // center + right panel headers
|
||||
backgroundInversePrimary: "#fafafa",
|
||||
contentPrimary: "#ffffff", // white (inspector --text)
|
||||
contentSecondary: "#a1a1aa", // zinc-400 (inspector --muted)
|
||||
contentTertiary: "#71717a", // zinc-500
|
||||
contentInversePrimary: "#000000",
|
||||
borderOpaque: "rgba(255, 255, 255, 0.18)", // inspector --border
|
||||
borderTransparent: "rgba(255, 255, 255, 0.14)", // inspector --border-2
|
||||
borderOpaque: "rgba(255, 255, 255, 0.10)", // inspector --border
|
||||
borderTransparent: "rgba(255, 255, 255, 0.07)", // inspector --border-2
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -455,6 +455,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", backgroundColor: "#09090b", borderTopLeftRadius: "12px", borderLeft: "1px solid rgba(255, 255, 255, 0.10)", borderTop: "1px solid rgba(255, 255, 255, 0.10)", overflow: "hidden" }}>
|
||||
<TabStrip
|
||||
handoff={handoff}
|
||||
activeTabId={activeTabId}
|
||||
|
|
@ -510,8 +511,8 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
border: 0,
|
||||
borderRadius: "999px",
|
||||
padding: "10px 18px",
|
||||
background: "#ff4f00",
|
||||
color: "#fff",
|
||||
background: "rgba(255, 255, 255, 0.12)",
|
||||
color: "#e4e4e7",
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
|
|
@ -554,6 +555,7 @@ const TranscriptPanel = memo(function TranscriptPanel({
|
|||
onSetDefaultModel={setDefaultModel}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SPanel>
|
||||
);
|
||||
});
|
||||
|
|
@ -723,7 +725,23 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
handoffWorkbenchClient.getSnapshot.bind(handoffWorkbenchClient),
|
||||
);
|
||||
const handoffs = viewModel.handoffs ?? [];
|
||||
const projects = viewModel.projects ?? [];
|
||||
const rawProjects = viewModel.projects ?? [];
|
||||
const [projectOrder, setProjectOrder] = useState<string[] | null>(null);
|
||||
const projects = useMemo(() => {
|
||||
if (!projectOrder) return rawProjects;
|
||||
const byId = new Map(rawProjects.map((p) => [p.id, p]));
|
||||
const ordered = projectOrder.map((id) => byId.get(id)).filter(Boolean) as typeof rawProjects;
|
||||
for (const p of rawProjects) {
|
||||
if (!projectOrder.includes(p.id)) ordered.push(p);
|
||||
}
|
||||
return ordered;
|
||||
}, [rawProjects, projectOrder]);
|
||||
const reorderProjects = useCallback((fromIndex: number, toIndex: number) => {
|
||||
const ids = projects.map((p) => p.id);
|
||||
const [moved] = ids.splice(fromIndex, 1);
|
||||
ids.splice(toIndex, 0, moved!);
|
||||
setProjectOrder(ids);
|
||||
}, [projects]);
|
||||
const [activeTabIdByHandoff, setActiveTabIdByHandoff] = useState<Record<string, string | null>>({});
|
||||
const [lastAgentTabIdByHandoff, setLastAgentTabIdByHandoff] = useState<Record<string, string | null>>({});
|
||||
const [openDiffsByHandoff, setOpenDiffsByHandoff] = useState<Record<string, string[]>>({});
|
||||
|
|
@ -1027,34 +1045,35 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
>
|
||||
<div
|
||||
style={{
|
||||
width: "min(520px, 100%)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.14)",
|
||||
borderRadius: "18px",
|
||||
background: "#111113",
|
||||
boxShadow: "0 32px 80px rgba(0, 0, 0, 0.45)",
|
||||
padding: "24px",
|
||||
width: "min(440px, 100%)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderRadius: "12px",
|
||||
background: "rgba(24, 24, 27, 0.98)",
|
||||
backdropFilter: "blur(16px)",
|
||||
boxShadow: "0 24px 64px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
|
||||
padding: "28px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "16px",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<div style={{ fontSize: "12px", letterSpacing: "0.08em", textTransform: "uppercase", color: "rgba(255, 255, 255, 0.5)" }}>Onboarding</div>
|
||||
<h2 style={{ margin: 0, fontSize: "24px", lineHeight: 1.1 }}>Give us support for sandbox agent</h2>
|
||||
<p style={{ margin: 0, color: "rgba(255, 255, 255, 0.72)", lineHeight: 1.5 }}>
|
||||
Before you keep going, give us support for sandbox agent and star the repo right here in the app.
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<div style={{ fontSize: "11px", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 600, color: "rgba(255, 255, 255, 0.4)" }}>Welcome to Foundry</div>
|
||||
<h2 style={{ margin: 0, fontSize: "18px", fontWeight: 500, lineHeight: 1.3 }}>Support Sandbox Agent</h2>
|
||||
<p style={{ margin: 0, color: "rgba(255, 255, 255, 0.55)", fontSize: "13px", lineHeight: 1.6 }}>
|
||||
Star the repo to help us grow and stay up to date with new releases.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{starRepoError ? (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "12px",
|
||||
border: "1px solid rgba(255, 110, 110, 0.32)",
|
||||
background: "rgba(255, 110, 110, 0.08)",
|
||||
padding: "12px 14px",
|
||||
color: "#ffb4b4",
|
||||
fontSize: "13px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid rgba(255, 110, 110, 0.24)",
|
||||
background: "rgba(255, 110, 110, 0.06)",
|
||||
padding: "10px 12px",
|
||||
color: "#ff9b9b",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
data-testid="onboarding-star-repo-error"
|
||||
>
|
||||
|
|
@ -1062,18 +1081,20 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissStarRepoPrompt}
|
||||
style={{
|
||||
border: "1px solid rgba(255, 255, 255, 0.14)",
|
||||
borderRadius: "999px",
|
||||
padding: "10px 16px",
|
||||
background: "transparent",
|
||||
color: "#e4e4e7",
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
borderRadius: "6px",
|
||||
padding: "8px 14px",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
cursor: "pointer",
|
||||
fontWeight: 600,
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
transition: "all 160ms ease",
|
||||
}}
|
||||
>
|
||||
Maybe later
|
||||
|
|
@ -1084,16 +1105,18 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
disabled={starRepoPending}
|
||||
style={{
|
||||
border: 0,
|
||||
borderRadius: "999px",
|
||||
padding: "10px 16px",
|
||||
background: starRepoPending ? "#7f5539" : "#ff4f00",
|
||||
color: "#fff",
|
||||
borderRadius: "6px",
|
||||
padding: "8px 14px",
|
||||
background: starRepoPending ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
|
||||
color: "#e4e4e7",
|
||||
cursor: starRepoPending ? "progress" : "pointer",
|
||||
fontWeight: 700,
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
transition: "all 160ms ease",
|
||||
}}
|
||||
data-testid="onboarding-star-repo-submit"
|
||||
>
|
||||
{starRepoPending ? "Starring..." : "Star the sandbox agent repo"}
|
||||
{starRepoPending ? "Starring..." : "Star the repo"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1112,8 +1135,9 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
onMarkUnread={markHandoffUnread}
|
||||
onRenameHandoff={renameHandoff}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
/>
|
||||
<SPanel>
|
||||
<SPanel $style={{ backgroundColor: "#09090b" }}>
|
||||
<ScrollBody>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1148,8 +1172,8 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
border: 0,
|
||||
borderRadius: "999px",
|
||||
padding: "10px 18px",
|
||||
background: viewModel.repos.length > 0 ? "#ff4f00" : "#444",
|
||||
color: "#fff",
|
||||
background: viewModel.repos.length > 0 ? "rgba(255, 255, 255, 0.12)" : "#444",
|
||||
color: "#e4e4e7",
|
||||
cursor: viewModel.repos.length > 0 ? "pointer" : "not-allowed",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
|
|
@ -1178,6 +1202,7 @@ export function MockLayout({ workspaceId, selectedHandoffId, selectedSessionId }
|
|||
onMarkUnread={markHandoffUnread}
|
||||
onRenameHandoff={renameHandoff}
|
||||
onRenameBranch={renameBranch}
|
||||
onReorderProjects={reorderProjects}
|
||||
/>
|
||||
<TranscriptPanel
|
||||
handoff={activeHandoff}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
|
|||
borderTopRightRadius: "16px",
|
||||
...(isUser
|
||||
? {
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#000000",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.10)",
|
||||
color: "#e4e4e7",
|
||||
borderBottomLeftRadius: "16px",
|
||||
borderBottomRightRadius: "4px",
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({
|
|||
})}
|
||||
>
|
||||
<Copy size={11} />
|
||||
{isCopied ? "Copied" : "Copy"}
|
||||
{isCopied ? "Copied" : null}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo, type Ref } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { ChatComposer, type ChatComposerClassNames } from "@sandbox-agent/react";
|
||||
import { ArrowUpFromLine, FileCode, Square, X } from "lucide-react";
|
||||
import { FileCode, SendHorizonal, Square, X } from "lucide-react";
|
||||
|
||||
import { ModelPicker } from "./model-picker";
|
||||
import { PROMPT_TEXTAREA_MAX_HEIGHT, PROMPT_TEXTAREA_MIN_HEIGHT } from "./ui";
|
||||
|
|
@ -45,7 +45,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
borderRadius: "16px",
|
||||
minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`,
|
||||
transition: "border-color 200ms ease",
|
||||
":focus-within": { borderColor: "rgba(255, 255, 255, 0.3)" },
|
||||
":focus-within": { borderColor: "rgba(255, 255, 255, 0.15)" },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}),
|
||||
|
|
@ -82,9 +82,9 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
justifyContent: "center",
|
||||
color: theme.colors.contentPrimary,
|
||||
transition: "background 200ms ease",
|
||||
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.06)" : "#ff4f00",
|
||||
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.12)",
|
||||
":hover": {
|
||||
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.12)" : "#ff6a00",
|
||||
backgroundColor: isRunning ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.20)",
|
||||
},
|
||||
":disabled": {
|
||||
cursor: "not-allowed",
|
||||
|
|
@ -157,7 +157,7 @@ export const PromptComposer = memo(function PromptComposer({
|
|||
allowEmptySubmit={isRunning}
|
||||
submitLabel={isRunning ? "Stop" : "Send"}
|
||||
classNames={composerClassNames}
|
||||
renderSubmitContent={() => (isRunning ? <Square size={16} /> : <ArrowUpFromLine size={16} />)}
|
||||
renderSubmitContent={() => (isRunning ? <Square size={16} /> : <SendHorizonal size={16} />)}
|
||||
renderFooter={() => (
|
||||
<div className={css({ padding: "0 10px 8px" })}>
|
||||
<ModelPicker
|
||||
|
|
|
|||
|
|
@ -135,8 +135,8 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
);
|
||||
|
||||
return (
|
||||
<SPanel>
|
||||
<PanelHeaderBar>
|
||||
<SPanel $style={{ backgroundColor: "#09090b" }}>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
||||
<div className={css({ flex: 1 })} />
|
||||
{!isTerminal ? (
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "4px" })}>
|
||||
|
|
@ -208,12 +208,14 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
) : null}
|
||||
</PanelHeaderBar>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", borderTop: `1px solid rgba(255, 255, 255, 0.10)` }}>
|
||||
<div
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "stretch",
|
||||
gap: "4px",
|
||||
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
|
||||
backgroundColor: theme.colors.backgroundSecondary,
|
||||
backgroundColor: "#09090b",
|
||||
height: "41px",
|
||||
minHeight: "41px",
|
||||
flexShrink: 0,
|
||||
|
|
@ -226,19 +228,21 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
height: "100%",
|
||||
padding: "0 16px",
|
||||
padding: "4px 12px",
|
||||
marginTop: "6px",
|
||||
marginBottom: "6px",
|
||||
marginLeft: "6px",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
whiteSpace: "nowrap",
|
||||
color: rightTab === "changes" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
|
||||
borderBottom: `2px solid ${rightTab === "changes" ? "#ff4f00" : "transparent"}`,
|
||||
marginBottom: "-1px",
|
||||
transitionProperty: "color, border-color",
|
||||
backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
transitionProperty: "color, background-color",
|
||||
transitionDuration: "200ms",
|
||||
transitionTimingFunction: "ease",
|
||||
":hover": { color: "#e4e4e7" },
|
||||
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "changes" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
|
||||
})}
|
||||
>
|
||||
Changes
|
||||
|
|
@ -268,19 +272,20 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
all: "unset",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "0 16px",
|
||||
padding: "4px 12px",
|
||||
marginTop: "6px",
|
||||
marginBottom: "6px",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
whiteSpace: "nowrap",
|
||||
color: rightTab === "files" ? theme.colors.contentPrimary : theme.colors.contentSecondary,
|
||||
borderBottom: `2px solid ${rightTab === "files" ? "#ff4f00" : "transparent"}`,
|
||||
marginBottom: "-1px",
|
||||
transitionProperty: "color, border-color",
|
||||
backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
transitionProperty: "color, background-color",
|
||||
transitionDuration: "200ms",
|
||||
transitionTimingFunction: "ease",
|
||||
":hover": { color: "#e4e4e7" },
|
||||
":hover": { color: "#e4e4e7", backgroundColor: rightTab === "files" ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
|
||||
})}
|
||||
>
|
||||
All Files
|
||||
|
|
@ -360,6 +365,7 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
</div>
|
||||
)}
|
||||
</ScrollBody>
|
||||
</div>
|
||||
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
||||
</SPanel>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useState } from "react";
|
||||
import { memo, useRef, useState } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react";
|
||||
|
|
@ -30,6 +30,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
onMarkUnread,
|
||||
onRenameHandoff,
|
||||
onRenameBranch,
|
||||
onReorderProjects,
|
||||
}: {
|
||||
projects: ProjectSection[];
|
||||
activeId: string;
|
||||
|
|
@ -38,10 +39,13 @@ export const Sidebar = memo(function Sidebar({
|
|||
onMarkUnread: (id: string) => void;
|
||||
onRenameHandoff: (id: string) => void;
|
||||
onRenameBranch: (id: string) => void;
|
||||
onReorderProjects: (fromIndex: number, toIndex: number) => void;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const contextMenu = useContextMenu();
|
||||
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({});
|
||||
const dragIndexRef = useRef<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<SPanel>
|
||||
|
|
@ -53,10 +57,10 @@ export const Sidebar = memo(function Sidebar({
|
|||
display: none !important;
|
||||
}
|
||||
`}</style>
|
||||
<PanelHeaderBar>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "transparent", borderBottom: "none" }}>
|
||||
<LabelSmall
|
||||
color={theme.colors.contentPrimary}
|
||||
$style={{ fontWeight: 600, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
|
||||
$style={{ fontWeight: 500, flex: 1, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }}
|
||||
>
|
||||
<ListChecks size={14} />
|
||||
Tasks
|
||||
|
|
@ -65,17 +69,17 @@ export const Sidebar = memo(function Sidebar({
|
|||
onClick={onCreate}
|
||||
className={css({
|
||||
all: "unset",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: "#ff4f00",
|
||||
color: "#ffffff",
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
color: "#e4e4e7",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "background 200ms ease",
|
||||
":hover": { backgroundColor: "#ff6a00" },
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.20)" },
|
||||
})}
|
||||
>
|
||||
<Plus size={14} />
|
||||
|
|
@ -83,11 +87,48 @@ export const Sidebar = memo(function Sidebar({
|
|||
</PanelHeaderBar>
|
||||
<ScrollBody>
|
||||
<div className={css({ padding: "8px", display: "flex", flexDirection: "column", gap: "4px" })}>
|
||||
{projects.map((project) => {
|
||||
{projects.map((project, projectIndex) => {
|
||||
const isCollapsed = collapsedProjects[project.id] === true;
|
||||
const isDragOver = dragOverIndex === projectIndex && dragIndexRef.current !== projectIndex;
|
||||
|
||||
return (
|
||||
<div key={project.id} className={css({ display: "flex", flexDirection: "column", gap: "4px" })}>
|
||||
<div
|
||||
key={project.id}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
dragIndexRef.current = projectIndex;
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", String(projectIndex));
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setDragOverIndex(projectIndex);
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setDragOverIndex((current) => (current === projectIndex ? null : current));
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
const fromIndex = dragIndexRef.current;
|
||||
if (fromIndex != null && fromIndex !== projectIndex) {
|
||||
onReorderProjects(fromIndex, projectIndex);
|
||||
}
|
||||
dragIndexRef.current = null;
|
||||
setDragOverIndex(null);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
dragIndexRef.current = null;
|
||||
setDragOverIndex(null);
|
||||
}}
|
||||
className={css({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
borderTop: isDragOver ? "2px solid #ff4f00" : "2px solid transparent",
|
||||
transition: "border-color 150ms ease",
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
setCollapsedProjects((current) => ({
|
||||
|
|
@ -102,7 +143,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
justifyContent: "space-between",
|
||||
padding: "10px 8px 4px",
|
||||
gap: "8px",
|
||||
cursor: "pointer",
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
":hover": { opacity: 0.8 },
|
||||
})}
|
||||
|
|
@ -150,9 +191,11 @@ export const Sidebar = memo(function Sidebar({
|
|||
{project.label}
|
||||
</LabelSmall>
|
||||
</div>
|
||||
<LabelXSmall color={theme.colors.contentTertiary}>
|
||||
{formatRelativeAge(project.updatedAtMs)}
|
||||
</LabelXSmall>
|
||||
{isCollapsed ? (
|
||||
<LabelXSmall color={theme.colors.contentTertiary}>
|
||||
{formatRelativeAge(project.updatedAtMs)}
|
||||
</LabelXSmall>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isCollapsed && project.handoffs.map((handoff) => {
|
||||
|
|
@ -177,7 +220,7 @@ export const Sidebar = memo(function Sidebar({
|
|||
])
|
||||
}
|
||||
className={css({
|
||||
padding: "12px",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid transparent",
|
||||
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
|
|
@ -204,35 +247,17 @@ export const Sidebar = memo(function Sidebar({
|
|||
</div>
|
||||
<LabelSmall
|
||||
$style={{
|
||||
fontWeight: 600,
|
||||
flex: 1,
|
||||
fontWeight: hasUnread ? 600 : 400,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
color={isDim ? theme.colors.contentSecondary : theme.colors.contentPrimary}
|
||||
color={hasUnread ? "#ffffff" : theme.colors.contentSecondary}
|
||||
>
|
||||
{handoff.title}
|
||||
</LabelSmall>
|
||||
{hasDiffs ? (
|
||||
<div className={css({ display: "flex", gap: "4px", flexShrink: 0 })}>
|
||||
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
|
||||
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={css({ display: "flex", alignItems: "center", marginTop: "4px", gap: "6px" })}>
|
||||
<LabelXSmall
|
||||
color={theme.colors.contentTertiary}
|
||||
$style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
{handoff.repoName}
|
||||
</LabelXSmall>
|
||||
{handoff.pullRequest != null ? (
|
||||
<span className={css({ display: "inline-flex", alignItems: "center", gap: "4px", flexShrink: 0 })}>
|
||||
<LabelXSmall color={theme.colors.contentSecondary} $style={{ fontWeight: 600 }}>
|
||||
|
|
@ -243,7 +268,13 @@ export const Sidebar = memo(function Sidebar({
|
|||
) : (
|
||||
<GitPullRequestDraft size={11} color={theme.colors.contentTertiary} />
|
||||
)}
|
||||
<LabelXSmall color={theme.colors.contentTertiary} $style={{ marginLeft: "auto", flexShrink: 0 }}>
|
||||
{hasDiffs ? (
|
||||
<div className={css({ display: "flex", gap: "4px", flexShrink: 0, marginLeft: "auto" })}>
|
||||
<span className={css({ fontSize: "11px", color: "#7ee787" })}>+{totalAdded}</span>
|
||||
<span className={css({ fontSize: "11px", color: "#ffa198" })}>-{totalRemoved}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<LabelXSmall color={theme.colors.contentTertiary} $style={{ flexShrink: 0, marginLeft: hasDiffs ? undefined : "auto" }}>
|
||||
{formatRelativeAge(handoff.updatedAtMs)}
|
||||
</LabelXSmall>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,12 +42,18 @@ export const TabStrip = memo(function TabStrip({
|
|||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
[data-tab]:hover [data-tab-close] { opacity: 0.5 !important; }
|
||||
[data-tab]:hover [data-tab-close]:hover { opacity: 1 !important; }
|
||||
`}</style>
|
||||
<div
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "stretch",
|
||||
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
|
||||
backgroundColor: theme.colors.backgroundSecondary,
|
||||
gap: "4px",
|
||||
backgroundColor: "#09090b",
|
||||
paddingLeft: "6px",
|
||||
height: "41px",
|
||||
minHeight: "41px",
|
||||
overflowX: "auto",
|
||||
|
|
@ -79,16 +85,20 @@ export const TabStrip = memo(function TabStrip({
|
|||
...(handoff.tabs.length > 1 ? [{ label: "Close tab", onClick: () => onCloseTab(tab.id) }] : []),
|
||||
])
|
||||
}
|
||||
data-tab
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "0 14px",
|
||||
borderBottom: isActive ? "2px solid #ff4f00" : "2px solid transparent",
|
||||
padding: "4px 12px",
|
||||
marginTop: "6px",
|
||||
marginBottom: "6px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
cursor: "pointer",
|
||||
transition: "color 200ms ease, border-color 200ms ease",
|
||||
transition: "color 200ms ease, background-color 200ms ease",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#e4e4e7" },
|
||||
":hover": { color: "#e4e4e7", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
|
||||
})}
|
||||
>
|
||||
<div
|
||||
|
|
@ -130,7 +140,7 @@ export const TabStrip = memo(function TabStrip({
|
|||
})}
|
||||
/>
|
||||
) : (
|
||||
<LabelXSmall color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} $style={{ fontWeight: 600 }}>
|
||||
<LabelXSmall color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} $style={{ fontWeight: 500 }}>
|
||||
{tab.sessionName}
|
||||
</LabelXSmall>
|
||||
)}
|
||||
|
|
@ -138,7 +148,8 @@ export const TabStrip = memo(function TabStrip({
|
|||
<X
|
||||
size={11}
|
||||
color={theme.colors.contentTertiary}
|
||||
className={css({ cursor: "pointer", opacity: 0.5, ":hover": { opacity: 1 } })}
|
||||
data-tab-close
|
||||
className={css({ cursor: "pointer", opacity: 0 })}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onCloseTab(tab.id);
|
||||
|
|
@ -161,29 +172,34 @@ export const TabStrip = memo(function TabStrip({
|
|||
onCloseDiffTab(path);
|
||||
}
|
||||
}}
|
||||
data-tab
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "0 14px",
|
||||
borderBottom: "2px solid transparent",
|
||||
padding: "4px 12px",
|
||||
marginTop: "6px",
|
||||
marginBottom: "6px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
cursor: "pointer",
|
||||
transition: "color 200ms ease, border-color 200ms ease",
|
||||
transition: "color 200ms ease, background-color 200ms ease",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#e4e4e7" },
|
||||
":hover": { color: "#e4e4e7", backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "rgba(255, 255, 255, 0.04)" },
|
||||
})}
|
||||
>
|
||||
<FileCode size={12} color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary} />
|
||||
<LabelXSmall
|
||||
color={isActive ? theme.colors.contentPrimary : theme.colors.contentSecondary}
|
||||
$style={{ fontWeight: 600, fontFamily: '"IBM Plex Mono", monospace' }}
|
||||
$style={{ fontWeight: 500, fontFamily: '"IBM Plex Mono", monospace' }}
|
||||
>
|
||||
{fileName(path)}
|
||||
</LabelXSmall>
|
||||
<X
|
||||
size={11}
|
||||
color={theme.colors.contentTertiary}
|
||||
className={css({ cursor: "pointer", opacity: 0.5, ":hover": { opacity: 1 } })}
|
||||
data-tab-close
|
||||
className={css({ cursor: "pointer", opacity: 0 })}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onCloseDiffTab(path);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
const [css, theme] = useStyletron();
|
||||
|
||||
return (
|
||||
<PanelHeaderBar>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
||||
{editingField === "title" ? (
|
||||
<input
|
||||
autoFocus
|
||||
|
|
@ -58,7 +58,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
<LabelSmall
|
||||
title="Rename"
|
||||
color={theme.colors.contentPrimary}
|
||||
$style={{ fontWeight: 500, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }}
|
||||
$style={{ fontWeight: 400, whiteSpace: "nowrap", cursor: "pointer", ":hover": { textDecoration: "underline" } }}
|
||||
onClick={() => onStartEditingField("title", handoff.title)}
|
||||
>
|
||||
{handoff.title}
|
||||
|
|
|
|||
|
|
@ -178,8 +178,8 @@ export const Shell = styled("div", ({ $theme }) => ({
|
|||
display: "grid",
|
||||
gap: "1px",
|
||||
height: "100dvh",
|
||||
backgroundColor: $theme.colors.borderOpaque,
|
||||
gridTemplateColumns: "280px minmax(0, 1fr) 380px",
|
||||
backgroundColor: $theme.colors.backgroundSecondary,
|
||||
gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1.5fr) 380px",
|
||||
overflow: "hidden",
|
||||
}));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue