Computer panel: reserve chat space (--computer-width) so the chat condenses, not hides
The absolute panel overlaid the full-width centered chat column, hiding the messages/composer behind it (WorkClaw avoids this by reserving the panel's width as padding on the chat). Introduced a shared --computer-width var, set once on the ChatWorkspace row per ?device= (phone 448px, tablet 67%, full 100%; 0 when closed). Both the panel <aside> width and a new chat- column wrapper's md:pr read it, so they never drift: opening the panel squeezes the centered max-w-3xl column into the left space (re-centered) while the panel overlays the reserved right area. Animated with transition-[padding]/[width] + the signature easing for the 'condense' effect. The header is outside the padded wrapper, so its cluster shift is unaffected. Verified at 1440: composer right clears the panel left edge in both tablet (561<593) and phone (920<992) — nothing hidden. 86 unit + 31 functional E2E green. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
914ffae8f9
commit
21da4dca19
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState, type CSSProperties } from "react";
|
||||||
import { useQueryStates } from "nuqs";
|
import { useQueryStates } from "nuqs";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
@@ -9,7 +9,17 @@ import type { Session } from "@/lib/api/sessions";
|
|||||||
import { useChat } from "@/lib/gateway/use-chat";
|
import { useChat } from "@/lib/gateway/use-chat";
|
||||||
import type { UiMessage } from "@/lib/gateway/transcript";
|
import type { UiMessage } from "@/lib/gateway/transcript";
|
||||||
import { encodeSessionKeyParam } from "@/lib/url/session-key";
|
import { encodeSessionKeyParam } from "@/lib/url/session-key";
|
||||||
import { panelParsers } from "@/lib/url/panel-params";
|
import { panelParsers, type DeviceSize } from "@/lib/url/panel-params";
|
||||||
|
|
||||||
|
/* One source of truth for the Computer panel's width (measured): both the
|
||||||
|
panel <aside> (width) and the chat column (padding-right) read this var,
|
||||||
|
so the chat condenses into the left space instead of hiding behind the
|
||||||
|
panel — WorkClaw's reserve-space model. Collapses to 0 when closed. */
|
||||||
|
const COMPUTER_WIDTH: Record<DeviceSize, string> = {
|
||||||
|
phone: "448px",
|
||||||
|
tablet: "67%",
|
||||||
|
full: "100%",
|
||||||
|
};
|
||||||
import { SlidePanel } from "@/components/ui/SlidePanel";
|
import { SlidePanel } from "@/components/ui/SlidePanel";
|
||||||
import { SessionsColumn } from "@/components/sessions/SessionsColumn";
|
import { SessionsColumn } from "@/components/sessions/SessionsColumn";
|
||||||
import { DevicePanel } from "@/components/computer/DevicePanel";
|
import { DevicePanel } from "@/components/computer/DevicePanel";
|
||||||
@@ -41,9 +51,11 @@ export function ChatWorkspace({
|
|||||||
);
|
);
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
const empty = state.messages.length === 0;
|
const empty = state.messages.length === 0;
|
||||||
const [{ sessions: showSessions }] = useQueryStates(panelParsers, {
|
const [{ sessions: showSessions, app, device }] = useQueryStates(
|
||||||
shallow: true,
|
panelParsers,
|
||||||
});
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
const computerWidth = app !== null ? COMPUTER_WIDTH[device] : "0px";
|
||||||
|
|
||||||
function openSession(session: Session) {
|
function openSession(session: Session) {
|
||||||
router.push(
|
router.push(
|
||||||
@@ -66,7 +78,10 @@ export function ChatWorkspace({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-dvh">
|
<div
|
||||||
|
className="relative flex h-dvh"
|
||||||
|
style={{ "--computer-width": computerWidth } as CSSProperties}
|
||||||
|
>
|
||||||
<SlidePanel open={showSessions} width={208} label="Sessions">
|
<SlidePanel open={showSessions} width={208} label="Sessions">
|
||||||
<SessionsColumn
|
<SessionsColumn
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
@@ -77,7 +92,11 @@ export function ChatWorkspace({
|
|||||||
</SlidePanel>
|
</SlidePanel>
|
||||||
<section className="flex min-w-0 flex-1 flex-col">
|
<section className="flex min-w-0 flex-1 flex-col">
|
||||||
<ChatHeader agent={agent} onNewSession={newSession} />
|
<ChatHeader agent={agent} onNewSession={newSession} />
|
||||||
<div className="mx-auto flex w-full max-w-3xl min-w-0 flex-1 flex-col overflow-hidden px-4">
|
{/* Reserve the panel's width as right-padding so the centered chat
|
||||||
|
column re-centers in the left space and never hides behind the
|
||||||
|
absolute panel (animates in sync with the panel). */}
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col transition-[padding] duration-(--duration-normal) ease-app md:pr-[var(--computer-width)]">
|
||||||
|
<div className="mx-auto flex w-full max-w-3xl min-w-0 flex-1 flex-col overflow-hidden px-4">
|
||||||
{empty ? (
|
{empty ? (
|
||||||
// Empty state: the composer is part of the centered welcome
|
// Empty state: the composer is part of the centered welcome
|
||||||
// stack (avatar → heading → subtitle → composer → chips).
|
// stack (avatar → heading → subtitle → composer → chips).
|
||||||
@@ -116,6 +135,7 @@ export function ChatWorkspace({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<DevicePanel agent={agent} />
|
<DevicePanel agent={agent} />
|
||||||
|
|||||||
@@ -10,31 +10,25 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
import { panelParsers, type AppId, type DeviceSize } from "@/lib/url/panel-params";
|
import { panelParsers, type AppId } from "@/lib/url/panel-params";
|
||||||
import { clawThemeStyle, WallpaperSurface } from "./ClawTheme";
|
import { clawThemeStyle, WallpaperSurface } from "./ClawTheme";
|
||||||
import { HomeScreen } from "./HomeScreen";
|
import { HomeScreen } from "./HomeScreen";
|
||||||
import { AppRouter, appTitle } from "./AppRouter";
|
import { AppRouter, appTitle } from "./AppRouter";
|
||||||
|
|
||||||
/* Device-state sizing + positioning model (measured at 1440). The panel is
|
/* The panel is ALWAYS an absolute, right-anchored overlay (never an in-flow
|
||||||
ALWAYS an absolute, right-anchored overlay (never an in-flow sibling) so
|
sibling). Its width and the chat column's reserved padding both read the
|
||||||
the chat keeps its full width and never reflows — opening the panel just
|
shared --computer-width var (set by ChatWorkspace per ?device=: phone
|
||||||
covers the right N% of the chat. Widths are proportional to the post-rail
|
448px, tablet 67%, full 100%; 0 when closed), so they never drift and the
|
||||||
area: phone 448px, tablet 67% (≈ the measured 847px), full 100% (and in
|
chat condenses into the left space rather than hiding behind the panel.
|
||||||
full the rail collapses to 80px in parallel, giving the panel the room).
|
The card stays w-full inside the px-6 wrapper → 24px inset + 32px radius. */
|
||||||
The card stays w-full inside the px-6 wrapper → 24px inset + 32px radius
|
const OVERLAY =
|
||||||
throughout. */
|
"md:absolute md:top-0 md:right-0 md:z-30 md:h-full md:w-[var(--computer-width)]";
|
||||||
const OVERLAY = "md:absolute md:top-0 md:right-0 md:z-30 md:h-full";
|
|
||||||
const WIDTH: Record<DeviceSize, string> = {
|
|
||||||
phone: "md:w-[448px]",
|
|
||||||
tablet: "md:w-[67%]",
|
|
||||||
full: "md:w-full",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** The right-hand "Computer" slide-out (§7): a per-agent themed screen card
|
/** The right-hand "Computer" slide-out (§7): a per-agent themed screen card
|
||||||
* hosting every sub-app behind ?app=, sized by ?device=. The size toggles
|
* hosting every sub-app behind ?app=. The size toggles and close live in
|
||||||
* and close live in the chat header; this owns the card + its width. */
|
* the chat header; this owns the card. */
|
||||||
export function DevicePanel({ agent }: { agent: Agent }) {
|
export function DevicePanel({ agent }: { agent: Agent }) {
|
||||||
const [{ app, device }, setParams] = useQueryStates(panelParsers, {
|
const [{ app }, setParams] = useQueryStates(panelParsers, {
|
||||||
shallow: true,
|
shallow: true,
|
||||||
});
|
});
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -79,9 +73,7 @@ export function DevicePanel({ agent }: { agent: Agent }) {
|
|||||||
aria-label="Computer"
|
aria-label="Computer"
|
||||||
aria-hidden={!open}
|
aria-hidden={!open}
|
||||||
onTransitionEnd={handleTransitionEnd}
|
onTransitionEnd={handleTransitionEnd}
|
||||||
className={`pointer-events-none overflow-hidden transition-[width] duration-(--duration-normal) ease-app max-md:fixed max-md:inset-0 max-md:z-40 max-md:h-dvh ${OVERLAY} ${
|
className={`pointer-events-none overflow-hidden transition-[width] duration-(--duration-normal) ease-app max-md:fixed max-md:inset-0 max-md:z-40 max-md:h-dvh ${OVERLAY}`}
|
||||||
open ? WIDTH[device] : "md:w-0"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="device-panel-theme"
|
data-testid="device-panel-theme"
|
||||||
|
|||||||
Reference in New Issue
Block a user