Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 191 additions & 7 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3909,21 +3909,205 @@ tr.insights-row-clickable:hover {
min-height: 0;
flex: 1;
}
.workbench-info-column {
.wb {
display: grid;
grid-template-columns: minmax(0, 1fr) 0px;
flex: 1;
min-height: 0;
position: relative;
transition: grid-template-columns 320ms var(--ease-drawer);
}
.wb[data-drawer="open"] {
grid-template-columns: minmax(0, 1fr) min(420px, 42%);
}
.wb-thread {
position: relative;
min-width: 0;
}
.bench-pill {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 20;
display: inline-flex;
align-items: center;
gap: 8px;
height: 36px;
max-width: min(440px, calc(100% - 120px));
padding: 0 10px 0 6px;
border: 0;
border-radius: var(--r-full);
background: color-mix(in srgb, var(--surface) 88%, transparent);
backdrop-filter: blur(12px);
box-shadow: var(--floating);
font-size: 13.5px;
color: var(--ink);
cursor: pointer;
transition:
box-shadow 150ms ease-out,
transform 160ms var(--ease-out);
}
.bench-pill:hover {
box-shadow: var(--overlay);
}
.bench-pill:active {
transform: translateX(-50%) scale(0.97);
}
.bench-pill b {
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bench-pill-status {
color: var(--ink-3);
white-space: nowrap;
}
.drawer-cell {
min-width: 0;
overflow: hidden;
position: relative;
}
.drawer:focus {
outline: none;
}
.drawer {
position: absolute;
top: 8px;
right: 8px;
bottom: 8px;
width: calc(min(420px, 42vw) - 8px);
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: var(--overlay);
display: flex;
flex-direction: column;
width: 18rem;
flex-shrink: 0;
min-height: 0;
opacity: 0;
transform: translateX(24px);
transition:
opacity 220ms var(--ease-out),
transform 320ms var(--ease-drawer);
pointer-events: none;
}
.wb[data-drawer="open"] .drawer {
opacity: 1;
transform: none;
pointer-events: auto;
}
.drawer-scrim {
display: none;
}
.drawer-head {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 12px 12px 20px;
}
.drawer-head > div {
flex: 1;
min-width: 0;
}
.drawer-head h2 {
margin: 0;
font-size: 16px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.drawer-head p {
margin: 2px 0 0;
font-size: 13px;
color: var(--ink-3);
}
.drawer-tabs {
display: flex;
gap: 2px;
padding: 0 16px;
border-bottom: 1px solid var(--line);
overflow-x: auto;
scrollbar-width: none;
flex: 0 0 auto;
mask: linear-gradient(90deg, #000 calc(100% - 24px), transparent);
}
.drawer-tabs::-webkit-scrollbar {
display: none;
}
.drawer-tabs button {
flex: 0 0 auto;
height: 40px;
padding: 0 8px;
border: 0;
background: none;
cursor: pointer;
font-size: 13px;
font-weight: 700;
color: var(--ink-3);
box-shadow: inset 0 -2px 0 transparent;
transition:
color 120ms ease,
box-shadow 150ms ease-out;
}
.drawer-tabs button:hover {
color: var(--ink);
}
.drawer-tabs button.active {
color: var(--ink);
box-shadow: inset 0 -2px 0 var(--ink);
}
.drawer-tabs button:last-child {
margin-right: 24px;
}
.drawer-body {
flex: 1;
overflow-y: auto;
padding: 1rem;
border-right: 1px solid var(--border);
padding: 20px;
}
@media (max-width: 1100px) {
.workbench-info-column {
@media (max-width: 860px) {
.wb[data-drawer="open"] {
grid-template-columns: minmax(0, 1fr) 0px;
}
.drawer-cell {
position: absolute;
inset: 0;
overflow: visible;
pointer-events: none;
z-index: 31;
}
.drawer {
width: auto;
left: 8px;
}
.wb[data-drawer="open"] .drawer-cell {
pointer-events: auto;
}
.drawer-scrim {
display: block;
position: absolute;
inset: 0;
background: rgb(0 0 0 / 24%);
opacity: 0;
pointer-events: none;
transition: opacity 200ms ease-out;
}
.wb[data-drawer="open"] .drawer-scrim {
opacity: 1;
pointer-events: auto;
}
}
@media (max-width: 640px) {
.bench-pill-status {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.wb,
.drawer {
transition: opacity 150ms ease-out;
}
}
.workbench-main {
display: flex;
flex-direction: column;
Expand Down
92 changes: 92 additions & 0 deletions apps/web/src/bench/bench-drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Button } from "@corbits/react-ui";
import { X } from "@/lib/icons";
import { useEffect, useRef, useState, type ReactNode } from "react";

export const DRAWER_TABS = [
"Information",
"Artifacts",
"Tools",
"Grants",
"Insights",
"Members",
"Workflows",
] as const;

export type DrawerTab = (typeof DRAWER_TABS)[number];

/** The floating card in the grid column that pushes the thread left. Each
* tab's body arrives as a prop keyed by tab name; a tab without one shows a
* short empty state until its own ticket lands. */
export function BenchDrawer({
open,
title,
subtitle,
onClose,
tabs,
}: {
readonly open: boolean;
readonly title: string;
readonly subtitle: string;
readonly onClose: () => void;
readonly tabs: Partial<Record<DrawerTab, ReactNode>>;
}) {
const [tab, setTab] = useState<DrawerTab>("Information");
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!open) return;
// preventScroll: focusing must never shift the thread's scroll position.
ref.current?.focus({ preventScroll: true });
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);

const body = tabs[tab];
return (
<div className="drawer-cell">
<div className="drawer-scrim" onClick={onClose} aria-hidden="true" />
<div
id="bench-drawer"
ref={ref}
className="drawer"
role="dialog"
aria-label={`${title} details`}
aria-hidden={!open}
inert={!open}
tabIndex={-1}
>
<div className="drawer-head">
<div>
<h2>{title}</h2>
<p>{subtitle}</p>
</div>
<Button variant="ghost" size="sm" aria-label="Close drawer" onClick={onClose}>
<X size={16} aria-hidden="true" />
</Button>
</div>
<div className="drawer-tabs" role="tablist">
{DRAWER_TABS.map((name) => (
<button
key={name}
type="button"
role="tab"
aria-selected={name === tab}
className={name === tab ? "active" : undefined}
onClick={() => setTab(name)}
>
{name}
</button>
))}
</div>
<div className="drawer-body" role="tabpanel">
{body ?? (
<p className="workbench-info-empty-note">{tab} is coming to this drawer soon.</p>
)}
</div>
</div>
</div>
);
}
35 changes: 35 additions & 0 deletions apps/web/src/bench/bench-pill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CaretDown } from "@/lib/icons";
import { IdentityAvatar } from "@/chat/avatar";

/** Frosted pill centered over the thread: the bench's worker, its name and
* live status. Toggles the bench drawer. */
export function BenchPill({
benchName,
worker,
status,
open,
onToggle,
}: {
readonly benchName: string;
readonly worker: { readonly id: string; readonly name: string } | undefined;
readonly status: string;
readonly open: boolean;
readonly onToggle: () => void;
}) {
return (
<button
type="button"
className="bench-pill"
aria-expanded={open}
aria-controls="bench-drawer"
onClick={onToggle}
>
{worker === undefined ? null : (
<IdentityAvatar kind="agent" name={worker.name} principalId={worker.id} />
)}
<b>{benchName}</b>
<span className="bench-pill-status">{status}</span>
<CaretDown size={14} aria-hidden="true" />
</button>
);
}
Loading
Loading