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:
Nicholas Kissel 2026-03-11 01:50:36 -07:00 committed by GitHub
parent 20082512a3
commit e792a720a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 201 additions and 123 deletions

View file

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