Panel app-header system + Settings hero (P1, capture-verified)
The panel app header now matches WorkClaw's measured pattern (dark captures
agent-settings / files-explorer / edit-profile):
- App TOP screen: the header lives on the app surface (sticky), showing the
app name (text-lg font-semibold) + a size-7 rounded-full close-X → home,
at px-5 pt-4 pb-3 — replacing the old px-8 pt-6 chevron+small-title. The
home screen keeps its dot + "{agent}'s Computer" header.
- Sub-screens (AppShell SubViewHeader): back-chevron PILL + text-lg
font-semibold title at px-5 pt-4 pb-3 (was text-xs px-3 pt-3 pb-2).
- Settings hero: 96px squircle avatar + text-xl name + text-sm role (matches
the capture; reviewer's 126px was wrong). New Avatar `settings` size.
p3-panel E2E updated (home nav is now the "Close" button). typecheck/lint/
86 unit/31 E2E/6 visual all green; no visual baseline change.
Known follow-up (P2): Settings/Files sub-screens still stack the top header +
the back-header (pre-existing) — to be unified per-app.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
db3dcff932
commit
b61073ce05
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronLeft } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useQueryStates } from "nuqs";
|
import { useQueryStates } from "nuqs";
|
||||||
import {
|
import {
|
||||||
useRef,
|
useRef,
|
||||||
@@ -92,32 +92,41 @@ export function DevicePanel({ agent }: { agent: Agent }) {
|
|||||||
>
|
>
|
||||||
{mounted && (
|
{mounted && (
|
||||||
<WallpaperSurface>
|
<WallpaperSurface>
|
||||||
<header className="relative flex shrink-0 items-center gap-2 px-8 pt-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Computer home"
|
|
||||||
onClick={() => openApp("home")}
|
|
||||||
className="flex items-center gap-1.5 text-xs font-medium text-white/90 transition-colors hover:text-white"
|
|
||||||
>
|
|
||||||
{onHome ? (
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className="size-2 rounded-full bg-rose-500"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronLeft aria-hidden size={16} />
|
|
||||||
)}
|
|
||||||
{onHome ? `${agent.name}'s Computer` : appTitle(app)}
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
{onHome ? (
|
{onHome ? (
|
||||||
<HomeScreen agentName={agent.name} onOpen={openApp} />
|
<>
|
||||||
|
<header className="relative flex shrink-0 items-center gap-2 px-8 pt-6">
|
||||||
|
<span className="flex items-center gap-1.5 text-xs font-medium text-white/90">
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="size-2 rounded-full bg-rose-500"
|
||||||
|
/>
|
||||||
|
{`${agent.name}'s Computer`}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<HomeScreen agentName={agent.name} onOpen={openApp} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
key={app}
|
key={app}
|
||||||
style={zoom}
|
style={zoom}
|
||||||
className="app-shell-anim-launch relative flex-1 overflow-y-auto bg-background/85"
|
className="app-shell-anim-launch relative flex-1 overflow-y-auto bg-background/85"
|
||||||
>
|
>
|
||||||
|
{/* Top app header lives on the app surface (sticky), like the
|
||||||
|
reference: app name + close-X (→ home). Sub-screens render
|
||||||
|
their own back-header via AppShell. */}
|
||||||
|
<header className="sticky top-0 z-10 flex shrink-0 items-center gap-2 bg-background/85 px-5 pt-4 pb-3 backdrop-blur-md">
|
||||||
|
<span className="flex-1 truncate text-lg font-semibold">
|
||||||
|
{appTitle(app)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={() => openApp("home")}
|
||||||
|
className="flex size-7 items-center justify-center rounded-full text-neutral-300 transition-colors hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
<X aria-hidden size={16} />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
<AppRouter app={app} agent={agent} />
|
<AppRouter app={app} agent={agent} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ export function SubViewHeader({
|
|||||||
action?: ReactNode;
|
action?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 px-3 pt-3 pb-2">
|
<header className="flex items-center gap-2 px-5 pt-4 pb-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
className="-my-1 -ml-2 flex min-w-0 items-center gap-1.5 rounded-full py-1.5 pl-2 pr-3 text-left transition-colors hover:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<ChevronLeft aria-hidden size={14} />
|
<ChevronLeft aria-hidden size={18} className="shrink-0" />
|
||||||
{title}
|
<span className="truncate text-lg font-semibold">{title}</span>
|
||||||
</button>
|
</button>
|
||||||
{action != null && <span className="ml-auto">{action}</span>}
|
<div className="flex-1" />
|
||||||
</div>
|
{action != null && <span>{action}</span>}
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,10 +150,10 @@ export default function SettingsApp({ agent }: { agent: Agent }) {
|
|||||||
const agentsMode = settings.data?.access_policy.agents.mode ?? "any";
|
const agentsMode = settings.data?.access_policy.agents.mode ?? "any";
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-3">
|
<div className="flex flex-col gap-3 p-3">
|
||||||
<div className="flex flex-col items-center gap-1 py-2">
|
<div className="flex flex-col items-center px-6 pt-7 pb-5">
|
||||||
<Avatar name={live.name} accent={live.accent} size="lg" shape="squircle" />
|
<Avatar name={live.name} accent={live.accent} size="settings" shape="squircle" />
|
||||||
<p className="text-sm font-medium">{live.name}</p>
|
<h1 className="mt-3 text-xl font-semibold tracking-tight">{live.name}</h1>
|
||||||
<p className="text-xs text-muted-foreground">{live.job_title}</p>
|
<p className="mt-0.5 text-sm text-muted-foreground">{live.job_title}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-divider-subtle overflow-hidden rounded-xl bg-surface-warm text-xs">
|
<div className="divide-y divide-divider-subtle overflow-hidden rounded-xl bg-surface-warm text-xs">
|
||||||
<div className="flex justify-between px-3 py-2.5">
|
<div className="flex justify-between px-3 py-2.5">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ interface AvatarProps {
|
|||||||
name: string;
|
name: string;
|
||||||
/** Per-agent accent hex; defaults to the brand coral. */
|
/** Per-agent accent hex; defaults to the brand coral. */
|
||||||
accent?: string;
|
accent?: string;
|
||||||
size?: "sm" | "md" | "lg" | "rail" | "chat" | "header" | "welcome";
|
size?: "sm" | "md" | "lg" | "rail" | "chat" | "header" | "settings" | "welcome";
|
||||||
/** Circle (people) or 17px-radius squircle (claws, measured). */
|
/** Circle (people) or 17px-radius squircle (claws, measured). */
|
||||||
shape?: "circle" | "squircle";
|
shape?: "circle" | "squircle";
|
||||||
/** Green presence dot overlay (rail + chat headers). */
|
/** Green presence dot overlay (rail + chat headers). */
|
||||||
@@ -16,6 +16,7 @@ const SIZES = {
|
|||||||
rail: "size-[58px] text-xl" /* 58px claw rail tile */,
|
rail: "size-[58px] text-xl" /* 58px claw rail tile */,
|
||||||
chat: "size-[50px] text-base" /* assistant message avatar */,
|
chat: "size-[50px] text-base" /* assistant message avatar */,
|
||||||
header: "size-[58px] text-2xl" /* chat-header identity */,
|
header: "size-[58px] text-2xl" /* chat-header identity */,
|
||||||
|
settings: "size-24 text-3xl" /* 96px Settings hero (measured) */,
|
||||||
welcome: "size-[126px] text-4xl" /* empty-state hero */,
|
welcome: "size-[126px] text-4xl" /* empty-state hero */,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ const SIZES = {
|
|||||||
const SQUIRCLE_RADIUS: Partial<Record<keyof typeof SIZES, string>> = {
|
const SQUIRCLE_RADIUS: Partial<Record<keyof typeof SIZES, string>> = {
|
||||||
rail: "rounded-[20px]",
|
rail: "rounded-[20px]",
|
||||||
header: "rounded-[20px]",
|
header: "rounded-[20px]",
|
||||||
|
settings: "rounded-[34px]",
|
||||||
welcome: "rounded-[40px]",
|
welcome: "rounded-[40px]",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -121,13 +121,13 @@ test("every panel app is reachable and the deep link cold-loads", async ({
|
|||||||
["Skills", /Scout's Skills|Installed skills/],
|
["Skills", /Scout's Skills|Installed skills/],
|
||||||
];
|
];
|
||||||
for (const [tile, marker] of stops) {
|
for (const [tile, marker] of stops) {
|
||||||
await panel.getByRole("button", { name: "Computer home" }).click();
|
await panel.getByRole("button", { name: "Close" }).click();
|
||||||
await panel.getByRole("button", { name: tile, exact: true }).click();
|
await panel.getByRole("button", { name: tile, exact: true }).click();
|
||||||
await expect(panel.getByText(marker).first()).toBeVisible();
|
await expect(panel.getByText(marker).first()).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline keys connect from the directory (P4): Notion via API key.
|
// Inline keys connect from the directory (P4): Notion via API key.
|
||||||
await panel.getByRole("button", { name: "Computer home" }).click();
|
await panel.getByRole("button", { name: "Close" }).click();
|
||||||
await panel.getByRole("button", { name: "Add Apps", exact: true }).click();
|
await panel.getByRole("button", { name: "Add Apps", exact: true }).click();
|
||||||
await panel.getByRole("button", { name: "Connect Notion" }).click();
|
await panel.getByRole("button", { name: "Connect Notion" }).click();
|
||||||
await panel.getByLabel("Notion API key").fill("secret_notion_key");
|
await panel.getByLabel("Notion API key").fill("secret_notion_key");
|
||||||
|
|||||||
Reference in New Issue
Block a user