mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 23:01:36 +00:00
Polish Foundry desktop UI: billing redesign, sidebar hover menu, org switching fix
- Redesign billing page with task-hours pricing model (Free: 8h, Pro: 200h/seat) - Add bulk hour purchase packages and Stripe payment management - Remove Usage nav section, add upgrade CTA in Members for free plan - Fix gear icon to open menu on hover with debounced timers - Fix org switching in workspace flyout (portal outside-click detection) - Fix tab strip padding when sidebar is collapsed - Update website components and Tauri config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f6656a90af
commit
ed6e6f6fa5
24 changed files with 1746 additions and 1028 deletions
|
|
@ -108,6 +108,16 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
const contextMenu = useContextMenu();
|
||||
const changedPaths = useMemo(() => new Set(task.fileChanges.map((file) => file.path)), [task.fileChanges]);
|
||||
const isTerminal = task.status === "archived";
|
||||
const [compact, setCompact] = useState(false);
|
||||
const headerRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (!node) return;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setCompact(entry.contentRect.width < 400);
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
}, []);
|
||||
const pullRequestUrl = task.pullRequest != null ? `https://github.com/${task.repoName}/pull/${task.pullRequest.number}` : null;
|
||||
|
||||
const copyFilePath = useCallback(async (path: string) => {
|
||||
|
|
@ -137,121 +147,128 @@ export const RightSidebar = memo(function RightSidebar({
|
|||
);
|
||||
|
||||
return (
|
||||
<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" })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (pullRequestUrl) {
|
||||
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
<SPanel $style={{ backgroundColor: "#09090b", minWidth: 0 }}>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", overflow: "hidden" }}>
|
||||
<div ref={headerRef} className={css({ display: "flex", alignItems: "center", flex: 1, minWidth: 0, justifyContent: "flex-end", gap: "2px" })}>
|
||||
{!isTerminal ? (
|
||||
<div className={css({ display: "flex", alignItems: "center", gap: "2px", flexShrink: 1, minWidth: 0 })}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (pullRequestUrl) {
|
||||
window.open(pullRequestUrl, "_blank", "noopener,noreferrer");
|
||||
return;
|
||||
}
|
||||
|
||||
onPublishPr();
|
||||
onPublishPr();
|
||||
}}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: compact ? "4px 6px" : "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
color: theme.colors.contentSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: compact ? "4px 6px" : "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
color: theme.colors.contentSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>Push</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={onArchive}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
padding: compact ? "4px 6px" : "4px 10px",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
color: theme.colors.contentSecondary,
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: theme.colors.contentPrimary },
|
||||
})}
|
||||
>
|
||||
<Archive size={12} style={{ flexShrink: 0 }} />
|
||||
{!compact && <span>Archive</span>}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{onToggleSidebar ? (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggleSidebar}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
||||
}}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
color: "#e4e4e7",
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
color: "#71717a",
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
>
|
||||
<GitPullRequest size={12} style={{ flexShrink: 0 }} />
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{pullRequestUrl ? "Open PR" : "Publish PR"}</span>
|
||||
</button>
|
||||
<button
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
color: "#e4e4e7",
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
||||
})}
|
||||
>
|
||||
<ArrowUpFromLine size={12} style={{ flexShrink: 0 }} />{" "}
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Push</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onArchive}
|
||||
className={css({
|
||||
appearance: "none",
|
||||
WebkitAppearance: "none",
|
||||
background: "none",
|
||||
border: "none",
|
||||
margin: "0",
|
||||
boxSizing: "border-box",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "12px",
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
color: "#e4e4e7",
|
||||
cursor: "pointer",
|
||||
transition: "all 200ms ease",
|
||||
":hover": { backgroundColor: "rgba(255, 255, 255, 0.06)", color: "#ffffff" },
|
||||
})}
|
||||
>
|
||||
<Archive size={12} style={{ flexShrink: 0 }} />{" "}
|
||||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>Archive</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{onToggleSidebar ? (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggleSidebar}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") onToggleSidebar();
|
||||
}}
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
color: "#71717a",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
>
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PanelHeaderBar>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,26 @@
|
|||
import { memo, useRef, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall, LabelXSmall } from "baseui/typography";
|
||||
import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, PanelLeft, Plus } from "lucide-react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
CloudUpload,
|
||||
CreditCard,
|
||||
GitPullRequestDraft,
|
||||
ListChecks,
|
||||
LogOut,
|
||||
PanelLeft,
|
||||
Plus,
|
||||
Settings,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
|
||||
import { formatRelativeAge, type Task, type ProjectSection } from "./view-model";
|
||||
import { ContextMenuOverlay, TaskIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui";
|
||||
import { activeMockOrganization, eligibleOrganizations, useMockAppClient, useMockAppSnapshot } from "../../lib/mock-app";
|
||||
|
||||
const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];
|
||||
|
||||
|
|
@ -400,7 +416,343 @@ export const Sidebar = memo(function Sidebar({
|
|||
})}
|
||||
</div>
|
||||
</ScrollBody>
|
||||
<SidebarFooter />
|
||||
{contextMenu.menu ? <ContextMenuOverlay menu={contextMenu.menu} onClose={contextMenu.close} /> : null}
|
||||
</SPanel>
|
||||
);
|
||||
});
|
||||
|
||||
const menuButtonStyle = (highlight: boolean) =>
|
||||
({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
background: highlight ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
color: "rgba(255, 255, 255, 0.75)",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
fontWeight: 400 as const,
|
||||
textAlign: "left" as const,
|
||||
transition: "background 120ms ease, color 120ms ease",
|
||||
}) satisfies React.CSSProperties;
|
||||
|
||||
function SidebarFooter() {
|
||||
const [css] = useStyletron();
|
||||
const navigate = useNavigate();
|
||||
const client = useMockAppClient();
|
||||
const snapshot = useMockAppSnapshot();
|
||||
const organization = activeMockOrganization(snapshot);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [workspaceFlyoutOpen, setWorkspaceFlyoutOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const flyoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const workspaceTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const flyoutRef = useRef<HTMLDivElement>(null);
|
||||
const [flyoutPos, setFlyoutPos] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (workspaceFlyoutOpen && workspaceTriggerRef.current) {
|
||||
const rect = workspaceTriggerRef.current.getBoundingClientRect();
|
||||
setFlyoutPos({ top: rect.top, left: rect.right + 4 });
|
||||
}
|
||||
}, [workspaceFlyoutOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleClick(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
const inContainer = containerRef.current?.contains(target);
|
||||
const inFlyout = flyoutRef.current?.contains(target);
|
||||
if (!inContainer && !inFlyout) {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [open]);
|
||||
|
||||
const switchToOrg = useCallback(
|
||||
(org: (typeof snapshot.organizations)[number]) => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
void (async () => {
|
||||
await client.selectOrganization(org.id);
|
||||
await navigate({ to: `/workspaces/${org.workspaceId}` as never });
|
||||
})();
|
||||
},
|
||||
[client, navigate],
|
||||
);
|
||||
|
||||
const openFlyout = useCallback(() => {
|
||||
if (flyoutTimerRef.current) clearTimeout(flyoutTimerRef.current);
|
||||
setWorkspaceFlyoutOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
flyoutTimerRef.current = setTimeout(() => setWorkspaceFlyoutOpen(false), 150);
|
||||
}, []);
|
||||
|
||||
const menuItems: Array<{ icon: React.ReactNode; label: string; danger?: boolean; onClick: () => void }> = [];
|
||||
|
||||
if (organization) {
|
||||
menuItems.push(
|
||||
{
|
||||
icon: <Settings size={14} />,
|
||||
label: "Settings",
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void navigate({ to: "/organizations/$organizationId/settings" as never, params: { organizationId: organization.id } as never });
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <CreditCard size={14} />,
|
||||
label: "Billing",
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void navigate({ to: "/organizations/$organizationId/billing" as never, params: { organizationId: organization.id } as never });
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menuItems.push(
|
||||
{
|
||||
icon: <User size={14} />,
|
||||
label: "Account",
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void navigate({ to: "/account" as never });
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <LogOut size={14} />,
|
||||
label: "Sign Out",
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
setOpen(false);
|
||||
void (async () => {
|
||||
await client.signOut();
|
||||
await navigate({ to: "/signin" });
|
||||
})();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const popoverStyle = css({
|
||||
borderRadius: "10px",
|
||||
border: "1px solid rgba(255, 255, 255, 0.10)",
|
||||
backgroundColor: "#18181b",
|
||||
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)",
|
||||
padding: "4px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseEnter={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = setTimeout(() => setOpen(true), 300);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}, 200);
|
||||
}}
|
||||
className={css({ position: "relative", flexShrink: 0 })}
|
||||
>
|
||||
{open ? (
|
||||
<div
|
||||
className={css({
|
||||
position: "absolute",
|
||||
bottom: "100%",
|
||||
left: "8px",
|
||||
right: "8px",
|
||||
marginBottom: "4px",
|
||||
zIndex: 9999,
|
||||
})}
|
||||
>
|
||||
<div className={popoverStyle}>
|
||||
{/* Workspace flyout trigger */}
|
||||
{organization ? (
|
||||
<div ref={workspaceTriggerRef} onMouseEnter={openFlyout} onMouseLeave={closeFlyout}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWorkspaceFlyoutOpen((prev) => !prev)}
|
||||
className={css({
|
||||
...menuButtonStyle(workspaceFlyoutOpen),
|
||||
fontWeight: 500,
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: "#ffffff",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
background: `linear-gradient(135deg, ${projectIconColor(organization.settings.displayName)}, ${projectIconColor(organization.settings.displayName + "x")})`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "9px",
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{organization.settings.displayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||
{organization.settings.displayName}
|
||||
</span>
|
||||
<ChevronRight size={12} className={css({ flexShrink: 0, color: "rgba(255, 255, 255, 0.35)" })} />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Workspace flyout portal */}
|
||||
{workspaceFlyoutOpen && organization && flyoutPos
|
||||
? createPortal(
|
||||
<div
|
||||
ref={flyoutRef}
|
||||
className={css({
|
||||
position: "fixed",
|
||||
top: `${flyoutPos.top}px`,
|
||||
left: `${flyoutPos.left}px`,
|
||||
minWidth: "200px",
|
||||
zIndex: 10000,
|
||||
})}
|
||||
onMouseEnter={() => {
|
||||
openFlyout();
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
closeFlyout();
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}, 200);
|
||||
}}
|
||||
>
|
||||
<div className={popoverStyle}>
|
||||
{eligibleOrganizations(snapshot).map((org) => {
|
||||
const isActive = organization.id === org.id;
|
||||
return (
|
||||
<button
|
||||
key={org.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isActive) switchToOrg(org);
|
||||
else {
|
||||
setOpen(false);
|
||||
setWorkspaceFlyoutOpen(false);
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
...menuButtonStyle(isActive),
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
color: isActive ? "#ffffff" : "rgba(255, 255, 255, 0.65)",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: "#ffffff",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "4px",
|
||||
background: `linear-gradient(135deg, ${projectIconColor(org.settings.displayName)}, ${projectIconColor(org.settings.displayName + "x")})`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "9px",
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{org.settings.displayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
<span className={css({ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" })}>
|
||||
{org.settings.displayName}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
onClick={item.onClick}
|
||||
className={css({
|
||||
...menuButtonStyle(false),
|
||||
color: item.danger ? "#ffa198" : "rgba(255, 255, 255, 0.75)",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: item.danger ? "#ff6b6b" : "#ffffff",
|
||||
},
|
||||
})}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={css({ padding: "8px" })}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
||||
setOpen((prev) => {
|
||||
if (prev) setWorkspaceFlyoutOpen(false);
|
||||
return !prev;
|
||||
});
|
||||
}}
|
||||
className={css({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "28px",
|
||||
height: "28px",
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
background: open ? "rgba(255, 255, 255, 0.06)" : "transparent",
|
||||
color: open ? "#ffffff" : "#71717a",
|
||||
cursor: "pointer",
|
||||
transition: "all 160ms ease",
|
||||
":hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
color: "#a1a1aa",
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const TabStrip = memo(function TabStrip({
|
|||
onCloseTab,
|
||||
onCloseDiffTab,
|
||||
onAddTab,
|
||||
sidebarCollapsed,
|
||||
}: {
|
||||
task: Task;
|
||||
activeTabId: string | null;
|
||||
|
|
@ -36,8 +37,10 @@ export const TabStrip = memo(function TabStrip({
|
|||
onCloseTab: (tabId: string) => void;
|
||||
onCloseDiffTab: (path: string) => void;
|
||||
onAddTab: () => void;
|
||||
sidebarCollapsed?: boolean;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||
const contextMenu = useContextMenu();
|
||||
|
||||
return (
|
||||
|
|
@ -53,7 +56,7 @@ export const TabStrip = memo(function TabStrip({
|
|||
borderBottom: `1px solid ${theme.colors.borderOpaque}`,
|
||||
gap: "4px",
|
||||
backgroundColor: "#09090b",
|
||||
paddingLeft: "6px",
|
||||
paddingLeft: sidebarCollapsed ? "14px" : "6px",
|
||||
height: "41px",
|
||||
minHeight: "41px",
|
||||
overflowX: "auto",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo } from "react";
|
||||
import { useStyletron } from "baseui";
|
||||
import { LabelSmall } from "baseui/typography";
|
||||
import { Clock, MailOpen } from "lucide-react";
|
||||
import { Clock, MailOpen, PanelLeft, PanelRight } from "lucide-react";
|
||||
|
||||
import { PanelHeaderBar } from "./ui";
|
||||
import { type AgentTab, type Task } from "./view-model";
|
||||
|
|
@ -16,6 +16,12 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
onCommitEditingField,
|
||||
onCancelEditingField,
|
||||
onSetActiveTabUnread,
|
||||
sidebarCollapsed,
|
||||
onToggleSidebar,
|
||||
onSidebarPeekStart,
|
||||
onSidebarPeekEnd,
|
||||
rightSidebarCollapsed,
|
||||
onToggleRightSidebar,
|
||||
}: {
|
||||
task: Task;
|
||||
activeTab: AgentTab | null | undefined;
|
||||
|
|
@ -26,11 +32,40 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
onCommitEditingField: (field: "title" | "branch") => void;
|
||||
onCancelEditingField: () => void;
|
||||
onSetActiveTabUnread: (unread: boolean) => void;
|
||||
sidebarCollapsed?: boolean;
|
||||
onToggleSidebar?: () => void;
|
||||
onSidebarPeekStart?: () => void;
|
||||
onSidebarPeekEnd?: () => void;
|
||||
rightSidebarCollapsed?: boolean;
|
||||
onToggleRightSidebar?: () => void;
|
||||
}) {
|
||||
const [css, theme] = useStyletron();
|
||||
const isDesktop = !!import.meta.env.VITE_DESKTOP;
|
||||
const needsTrafficLightInset = isDesktop && sidebarCollapsed;
|
||||
|
||||
return (
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none" }}>
|
||||
<PanelHeaderBar $style={{ backgroundColor: "#0f0f11", borderBottom: "none", paddingLeft: needsTrafficLightInset ? "74px" : "14px" }}>
|
||||
{sidebarCollapsed && onToggleSidebar ? (
|
||||
<div
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#71717a",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
onClick={onToggleSidebar}
|
||||
onMouseEnter={onSidebarPeekStart}
|
||||
onMouseLeave={onSidebarPeekEnd}
|
||||
>
|
||||
<PanelLeft size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
{editingField === "title" ? (
|
||||
<input
|
||||
autoFocus
|
||||
|
|
@ -170,6 +205,25 @@ export const TranscriptHeader = memo(function TranscriptHeader({
|
|||
<span className={css({ "@media screen and (max-width: 768px)": { display: "none" } })}>{activeTab.unread ? "Mark read" : "Mark unread"}</span>
|
||||
</button>
|
||||
) : null}
|
||||
{rightSidebarCollapsed && onToggleRightSidebar ? (
|
||||
<div
|
||||
className={css({
|
||||
width: "26px",
|
||||
height: "26px",
|
||||
borderRadius: "6px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
color: "#71717a",
|
||||
flexShrink: 0,
|
||||
":hover": { color: "#a1a1aa", backgroundColor: "rgba(255, 255, 255, 0.06)" },
|
||||
})}
|
||||
onClick={onToggleRightSidebar}
|
||||
>
|
||||
<PanelRight size={14} />
|
||||
</div>
|
||||
) : null}
|
||||
</PanelHeaderBar>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue