Skip to content
Closed
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
103 changes: 68 additions & 35 deletions crates/agent-gateway/web/src/components/hub/HubChrome.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import type { ReactNode } from "react";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useLocale } from "../../i18n";
import { cn } from "../../lib/shared/utils";
import { PanelLeft } from "../icons";
import { Button } from "../ui/button";

export function HubBackdrop(props: { tone?: "amber" | "violet" | "neutral" }) {
const { tone = "neutral" } = props;
// macOS frosted-glass: subtle monochrome wash; tone provides only the faintest temperature shift.
// Dark: the canvas drops below --background (--hub-canvas) so background-tinted glass reads
// elevated, and the halos stay a whisper above the canvas instead of a gray fog.
// Cool monochrome wash; tone only shifts temperature slightly.
// Dark canvas sits below --background so glass panels read elevated.
const haloClass =
tone === "amber"
? "bg-[radial-gradient(circle_at_top_left,hsl(0_0%_100%/0.85),transparent_60%)] dark:bg-[radial-gradient(circle_at_top_left,hsl(222_18%_14%/0.55),transparent_60%)]"
? "bg-[radial-gradient(circle_at_top_left,hsl(40_30%_98%/0.9),transparent_58%)] dark:bg-[radial-gradient(circle_at_top_left,hsl(36_18%_14%/0.45),transparent_58%)]"
: tone === "violet"
? "bg-[radial-gradient(circle_at_top_left,hsl(220_18%_98%/0.85),transparent_60%)] dark:bg-[radial-gradient(circle_at_top_left,hsl(224_20%_14%/0.55),transparent_60%)]"
: "bg-[radial-gradient(circle_at_top_left,hsl(0_0%_100%/0.8),transparent_60%)] dark:bg-[radial-gradient(circle_at_top_left,hsl(224_18%_14%/0.5),transparent_60%)]";
? "bg-[radial-gradient(circle_at_top_left,hsl(250_28%_98%/0.9),transparent_58%)] dark:bg-[radial-gradient(circle_at_top_left,hsl(250_18%_14%/0.45),transparent_58%)]"
: "bg-[radial-gradient(circle_at_top_left,hsl(240_12%_98%/0.88),transparent_58%)] dark:bg-[radial-gradient(circle_at_top_left,hsl(240_12%_14%/0.4),transparent_58%)]";
return (
<>
<div className="pointer-events-none absolute inset-0 bg-[hsl(var(--hub-canvas))]" />
Expand All @@ -26,7 +25,7 @@ export function HubBackdrop(props: { tone?: "amber" | "violet" | "neutral" }) {
/>
<div
className={cn(
"pointer-events-none absolute -right-24 bottom-0 h-[360px] w-[360px] rounded-full opacity-60 blur-3xl",
"pointer-events-none absolute -right-24 bottom-0 h-[360px] w-[360px] rounded-full opacity-55 blur-3xl",
haloClass,
)}
/>
Expand All @@ -45,36 +44,40 @@ export function HubHeader(props: {
}) {
const { icon, title, subtitle, actions, sidebarOpen, onOpenSidebar } = props;
const { t } = useLocale();
// Web has no macOS title-bar overlay; always show the open-sidebar control when closed.
const showSidebarButton = !sidebarOpen;
return (
<div className="hub-header relative z-10 px-5 pt-6 pb-3 sm:px-6 lg:px-8 xl:px-10">
<div className="hub-header relative z-10 px-5 pb-3 pt-5 sm:px-6 lg:px-8 xl:px-10">
{showSidebarButton ? (
<Button
type="button"
variant="ghost"
size="icon"
onClick={onOpenSidebar}
title={t("tooltip.openSidebar")}
className="absolute left-3 top-5 h-9 w-9 rounded-lg text-muted-foreground hover:bg-background/70 hover:text-foreground"
className="absolute left-3 top-4 h-9 w-9 rounded-full text-muted-foreground hover:bg-background/70 hover:text-foreground"
>
<PanelLeft className="h-4.5 w-4.5" />
</Button>
) : null}
<div
className={cn(
"mx-auto flex w-full max-w-[1320px] items-center gap-4",
"mx-auto flex w-full max-w-[1320px] items-center gap-3.5",
showSidebarButton && "pl-11 lg:pl-0",
)}
>
<div className="hub-header-icon flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border border-border/40 bg-background/70 text-foreground/80 shadow-[0_1px_0_rgba(255,255,255,0.55)_inset] dark:border-white/[0.08] dark:bg-white/[0.05] dark:shadow-[0_1px_0_rgba(255,255,255,0.05)_inset] backdrop-blur-xl">
<div className="hub-header-icon flex h-11 w-11 shrink-0 items-center justify-center rounded-[14px] text-foreground/80">
{icon}
</div>
<div className="min-w-0 flex-1">
<h1 className="text-[21px] font-semibold leading-tight tracking-tight text-foreground">
<h1 className="text-[calc(22px*var(--zone-font-scale,1))] font-semibold leading-[1.15] tracking-[-0.022em] text-foreground">
{title}
</h1>
{subtitle ? (
<p className="mt-0.5 truncate text-[12px] text-muted-foreground" title={subtitle}>
<p
className="mt-0.5 truncate text-[12.5px] leading-snug tracking-[-0.005em] text-muted-foreground"
title={subtitle}
>
{subtitle}
</p>
) : null}
Expand All @@ -92,31 +95,61 @@ export function GlassPanel(props: {
className?: string;
}) {
const { children, tone = "default", active = false, className } = props;
const toneClass = (() => {
switch (tone) {
case "muted":
return "border-border/40 bg-muted/40";
case "error":
return "border-destructive/30 bg-destructive/5";
case "amber":
case "violet":
case "neutral":
return active
? "border-border/55 bg-background/80 shadow-[0_1px_0_rgba(255,255,255,0.6)_inset,0_6px_22px_-14px_rgba(15,23,42,0.18)] dark:border-white/[0.09] dark:bg-white/[0.06] dark:shadow-[0_1px_0_rgba(255,255,255,0.06)_inset,0_6px_22px_-14px_rgba(0,0,0,0.55)]"
: "border-border/40 bg-background/60";
default:
return "border-border/40 bg-background/60";
}
})();
return (
<div
className={cn(
"hub-glass-panel rounded-2xl border px-4 py-3.5 backdrop-blur-xl",
toneClass,
className,
)}
data-tone={tone}
data-active={active || undefined}
className={cn("hub-glass-panel", className)}
>
{children}
</div>
);
}

/** Status strip under HubHeader (Skills enabled / MCP ready). */
export function HubStatusBanner(props: {
ready?: boolean;
children: ReactNode;
className?: string;
}) {
const { ready = false, children, className } = props;
return (
<div
data-ready={ready || undefined}
className={cn("hub-status-banner hub-panel-enter", className)}
>
{children}
</div>
);
}

/** Segmented tab track (Installed / Store / Import). */
export function HubSegmentedTrack(props: { children: ReactNode; className?: string }) {
return <div className={cn("hub-segmented-track", props.className)}>{props.children}</div>;
}

export function HubSegmentButton(
props: ButtonHTMLAttributes<HTMLButtonElement> & {
active?: boolean;
icon?: ReactNode;
count?: number | null;
},
) {
const { active, icon, count, className, children, type = "button", ...rest } = props;
return (
<button
type={type}
data-active={active || undefined}
className={cn("hub-segment-button", className)}
{...rest}
>
{icon}
<span>{children}</span>
{count != null && count > 0 ? (
<span className="hub-segment-count" data-active={active || undefined}>
{count}
</span>
) : null}
</button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export function RightDockChooser(props: RightDockChooserProps) {
onClick={tool.onClick}
disabled={tool.disabled}
title={tool.titleAttr}
className="group flex items-center gap-3 rounded-lg border border-border/60 bg-background px-3.5 py-3 text-left text-sm text-foreground transition-all hover:border-border hover:bg-muted/60 hover:shadow-sm disabled:pointer-events-none disabled:opacity-50"
className="group flex items-center gap-3 rounded-2xl border border-border/50 bg-background/70 px-3.5 py-3 text-left text-sm text-foreground shadow-[0_1px_0_rgba(255,255,255,0.45)_inset] backdrop-blur-md transition-[background-color,border-color,transform,box-shadow] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-border hover:bg-background/90 hover:shadow-sm active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 motion-reduce:active:scale-100 dark:border-white/[0.08] dark:bg-white/[0.04] dark:shadow-[0_1px_0_rgba(255,255,255,0.06)_inset]"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted/80 text-muted-foreground transition-colors group-hover:bg-muted group-hover:text-foreground">
{tool.icon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -742,22 +742,18 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel
data-state={isOpen ? "open" : "closed"}
data-project-tools-resizing={isResizing ? "true" : undefined}
className={cn(
"project-tools-panel zone-font-scale fixed inset-x-0 bottom-0 z-40 flex h-[min(72vh,34rem)] min-h-0 w-full shrink-0 flex-col overflow-hidden bg-background shadow-2xl transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none md:relative md:inset-auto md:z-10 md:h-full md:overflow-visible md:shadow-none",
// Desktop: single drawer (track width + panel slide). Do NOT fade
// opacity first then collapse width — that reads as a double close.
// Mobile: bottom sheet via translate-y only.
"project-tools-panel zone-font-scale fixed inset-x-0 bottom-0 z-40 h-[min(72vh,34rem)] min-h-0 w-full shrink-0 overflow-hidden border-t shadow-[0_-12px_40px_-18px_rgba(15,23,42,0.28)] motion-reduce:transition-none md:relative md:inset-auto md:z-10 md:h-full md:border-l md:border-t-0 md:shadow-none",
isOpen
? "pointer-events-auto translate-y-0 border-t border-border opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-l md:border-t-0"
: "pointer-events-none translate-y-full border-t border-transparent opacity-0 md:translate-x-3 md:translate-y-0 md:border-l-0 md:border-t-0",
? "pointer-events-auto translate-y-0 md:translate-y-0"
: "pointer-events-none translate-y-full border-transparent md:translate-y-0",
effectiveWidthCollapsed ? "md:w-0" : "md:w-[var(--project-tools-panel-width)]",
)}
style={{ ...panelStyle, "--zone-font-scale": fontScale } as CSSProperties}
>
<div
className={cn(
"project-tools-panel-inner flex h-full min-h-0 w-full flex-col transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none md:w-[var(--project-tools-panel-width)] md:min-w-[var(--project-tools-panel-width)]",
isOpen
? "translate-y-0 opacity-100 md:translate-x-0"
: "translate-y-3 opacity-0 md:translate-x-2 md:translate-y-0",
)}
>
<div className="project-tools-panel-inner flex h-full min-h-0 w-full flex-col md:w-[var(--project-tools-panel-width)] md:min-w-[var(--project-tools-panel-width)]">
{effectiveShouldRenderContent ? (
<>
<div className="project-tools-panel-handle" aria-hidden="true" />
Expand All @@ -780,7 +776,7 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel
)}
/>
</button>
<div className="project-tools-panel-header flex h-11 shrink-0 items-center gap-2 border-b border-border px-3">
<div className="project-tools-panel-header flex h-11 shrink-0 items-center gap-2 px-3">
<div
className="project-tools-panel-tabs-shell flex min-w-0 flex-1 flex-col justify-center gap-1"
onWheel={handleTabsWheel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,12 @@ export function useRightDockPanelWidth(options: UseRightDockPanelWidthOptions) {
setWidthCollapsed(true);
return;
}
// Collapse track width immediately so CSS can animate width + panel slide
// together. Only delay unmounting content until the drawer motion finishes
// (must match --project-tools-motion-duration).
setWidthCollapsed(true);
const timer = window.setTimeout(() => {
setShouldRenderContent(false);
setWidthCollapsed(true);
}, 220);
return () => window.clearTimeout(timer);
}, [collapseImmediately, isOpen]);
Expand Down
Loading
Loading