| author | |
| committer | |
| log | 429c625a8515f9c2e4af28c488fbf32ce7789645 |
| tree | 541399e3f79e605994c29f4681fad6496541c42e |
| parent | 0cfa113be597af3a1b2fe905a58a5b583f8089b8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Chat stays centered while the workspace card fits beside it with 32px to spare. When it does not fit, chat moves left only as far as needed, narrows only after it reaches the left padding, and the card becomes a popover below a 640px chat. The card is lighter: 280px wide, 32px rows, no section labels, no "Project folder" hint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>14 files changed, 123 insertions(+), 40 deletions(-)
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+9-5| ... | ... | @@ -59,7 +59,11 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 59 | 59 | }: BranchToolbarEnvModeSelectorProps) { |
| 60 | 60 | const workspacePath = displayMode === "panel" ? (activeWorktreePath ?? workspaceRoot) : null; |
| 61 | 61 | const workspaceDisplayName = resolveWorkspaceDisplayName(workspacePath); |
| 62 | const workspaceKind = activeWorktreePath ? "Worktree" : "Project folder"; | |
| 62 | // The panel names the workspace kind only when it is not the project folder. | |
| 63 | const workspaceKind = activeWorktreePath ? "Worktree" : null; | |
| 64 | const lockedWorkspaceKind = forceNewWorktree ? "Worktree" : workspaceKind; | |
| 65 | const selectWorkspaceKind = | |
| 66 | effectiveEnvMode === "worktree" && !activeWorktreePath ? "Create" : workspaceKind; | |
| 63 | 67 | const composerFloatingLayerProps = useComposerMenuProps(); |
| 64 | 68 | const showPreviousWorktree = Boolean(previousWorktreeLabel && onUsePreviousWorktree); |
| 65 | 69 | const envModeItems = useMemo( |
| ... | ... | @@ -147,9 +151,9 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 147 | 151 | : (workspaceDisplayName ?? |
| 148 | 152 | resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))} |
| 149 | 153 | </ComposerContextLabel> |
| 150 | {displayMode === "panel" ? ( | |
| 154 | {displayMode === "panel" && lockedWorkspaceKind ? ( | |
| 151 | 155 | <span className="shrink-0 text-3xs font-normal text-muted-foreground/70"> |
| 152 | {forceNewWorktree ? "Worktree" : workspaceKind} | |
| 156 | {lockedWorkspaceKind} | |
| 153 | 157 | </span> |
| 154 | 158 | ) : null} |
| 155 | 159 | </span> |
| ... | ... | @@ -210,9 +214,9 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe |
| 210 | 214 | <ComposerContextLabel displayMode={displayMode}> |
| 211 | 215 | <SelectValue /> |
| 212 | 216 | </ComposerContextLabel> |
| 213 | {displayMode === "panel" ? ( | |
| 217 | {displayMode === "panel" && selectWorkspaceKind ? ( | |
| 214 | 218 | <span className="shrink-0 text-3xs font-normal text-muted-foreground/70"> |
| 215 | {effectiveEnvMode === "worktree" && !activeWorktreePath ? "Create" : workspaceKind} | |
| 219 | {selectWorkspaceKind} | |
| 216 | 220 | </span> |
| 217 | 221 | ) : null} |
| 218 | 222 | </TooltipTrigger> |
apps/web/src/components/GitActionsControl.tsx+1-1| ... | ... | @@ -1880,7 +1880,7 @@ export default function GitActionsControl({ |
| 1880 | 1880 | // the output row expands below. |
| 1881 | 1881 | <GitActionElapsedTime |
| 1882 | 1882 | startedAtMs={gitActionProgress.startedAtMs} |
| 1883 | className="flex h-9 shrink-0 items-center self-start pe-2.5 text-2xs font-normal tabular-nums text-muted-foreground" | |
| 1883 | className="flex h-8 shrink-0 items-center self-start pe-2.5 text-2xs font-normal tabular-nums text-muted-foreground" | |
| 1884 | 1884 | /> |
| 1885 | 1885 | ) : ( |
| 1886 | 1886 | <> |
apps/web/src/components/chat/ChatCanvas.tsx+5-1| ... | ... | @@ -11,7 +11,10 @@ import { ChatCanvasContext } from "./ChatCanvasContext"; |
| 11 | 11 | import { resolveChatCanvasLayout, type ChatCanvasPreview } from "./chatCanvasLayout"; |
| 12 | 12 | import type { PreviewMiniPlayerObstacles } from "../preview/previewMiniPlayerLayout"; |
| 13 | 13 | |
| 14 | /** Owns the available conversation space. Floating cards never reserve it themselves. */ | |
| 14 | /** | |
| 15 | * Owns the available conversation space. Cards only report where they sit; the | |
| 16 | * canvas decides when chat moves over to make room for them. | |
| 17 | */ | |
| 15 | 18 | export function ChatCanvas({ |
| 16 | 19 | composerOverlayElement, |
| 17 | 20 | children, |
| ... | ... | @@ -96,6 +99,7 @@ export function ChatCanvas({ |
| 96 | 99 | const container = { width: measurements.width, height: measurements.height }; |
| 97 | 100 | return { |
| 98 | 101 | container, |
| 102 | lane: { padding: measurements.padding, minChatWidth: measurements.minChatWidth }, | |
| 99 | 103 | layout: resolveChatCanvasLayout({ ...measurements, container, preview, detailsCard }), |
| 100 | 104 | previewKey: preview?.key ?? null, |
| 101 | 105 | reportPreview, |
apps/web/src/components/chat/ChatCanvasContext.ts+1| ... | ... | @@ -4,6 +4,7 @@ import type { PreviewMiniPlayerObstacles } from "../preview/previewMiniPlayerLay |
| 4 | 4 | |
| 5 | 5 | export const ChatCanvasContext = createContext<{ |
| 6 | 6 | container: { width: number; height: number }; |
| 7 | lane: { padding: number; minChatWidth: number }; | |
| 7 | 8 | layout: ReturnType<typeof resolveChatCanvasLayout>; |
| 8 | 9 | previewKey: string | null; |
| 9 | 10 | reportPreview: (preview: ChatCanvasPreview) => void; |
apps/web/src/components/chat/ThreadDetailsCard.tsx+7-2| ... | ... | @@ -29,12 +29,17 @@ export function ThreadDetailsCard({ |
| 29 | 29 | const preferredPlacement = canvas |
| 30 | 30 | ? resolveThreadDetailsCardLayout({ |
| 31 | 31 | container: canvas.container, |
| 32 | chat: canvas.layout.chat, | |
| 32 | lane: canvas.lane, | |
| 33 | 33 | frame: null, |
| 34 | 34 | }) |
| 35 | 35 | : null; |
| 36 | 36 | const placement = canvas |
| 37 | ? resolveThreadDetailsCardLayout({ container: canvas.container, ...canvas.layout }) | |
| 37 | ? resolveThreadDetailsCardLayout({ | |
| 38 | container: canvas.container, | |
| 39 | lane: canvas.lane, | |
| 40 | frame: canvas.layout.frame, | |
| 41 | overlapsDetailsCard: canvas.layout.overlapsDetailsCard, | |
| 42 | }) | |
| 38 | 43 | : null; |
| 39 | 44 | const mode = placement ? "inline" : "popover"; |
| 40 | 45 | const inlineOpen = useRightPanelStore((state) => |
apps/web/src/components/chat/ThreadDetailsControl.tsx+1-1| ... | ... | @@ -56,7 +56,7 @@ export function ThreadDetailsControl({ |
| 56 | 56 | className: cn( |
| 57 | 57 | "relative inline-flex shrink-0 cursor-pointer items-center whitespace-nowrap border outline-none transition-[box-shadow,scale] [&:active:not([aria-haspopup])]:scale-[0.97] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 disabled:pointer-events-none disabled:opacity-64 aria-disabled:cursor-not-allowed aria-disabled:opacity-64 [&_svg]:shrink-0", |
| 58 | 58 | parts[part], |
| 59 | multiline && "h-auto min-h-9 py-0.75 disabled:opacity-100 sm:h-auto", | |
| 59 | multiline && "h-auto min-h-8 py-0.75 disabled:opacity-100 sm:h-auto", | |
| 60 | 60 | tone === "muted" && "text-muted-foreground/70 hover:text-foreground/80", |
| 61 | 61 | tone === "destructive" && |
| 62 | 62 | "text-destructive hover:text-destructive data-pressed:text-destructive", |
apps/web/src/components/chat/ThreadDetailsPanel.tsx+2-2| ... | ... | @@ -126,7 +126,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { |
| 126 | 126 | headingId="thread-details-workspace-heading" |
| 127 | 127 | title="Workspace" |
| 128 | 128 | separated={false} |
| 129 | showHeading={density === "full"} | |
| 129 | showHeading={false} | |
| 130 | 130 | > |
| 131 | 131 | {props.versionMismatch ? ( |
| 132 | 132 | <div className="mx-1 mb-2 flex gap-2 rounded-xl border border-warning/30 bg-warning/6 p-3"> |
| ... | ... | @@ -198,7 +198,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { |
| 198 | 198 | <ThreadDetailsSection |
| 199 | 199 | headingId="thread-details-version-control-heading" |
| 200 | 200 | title="Version Control" |
| 201 | showHeading={density === "full"} | |
| 201 | showHeading={false} | |
| 202 | 202 | separated={density === "full"} |
| 203 | 203 | > |
| 204 | 204 | <div className="flex flex-col"> |
apps/web/src/components/chat/ThreadRelationshipsControl.tsx+2-2| ... | ... | @@ -97,7 +97,7 @@ export function ThreadLineageRowList(props: { |
| 97 | 97 | <button |
| 98 | 98 | type="button" |
| 99 | 99 | onClick={props.onShowMore} |
| 100 | className={`flex h-9 w-full cursor-pointer items-center rounded-lg ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-sm font-medium text-muted-foreground/70 hover:bg-black/[0.055] hover:text-foreground/80 dark:hover:bg-white/[0.075]`} | |
| 100 | className={`flex h-8 w-full cursor-pointer items-center rounded-lg ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-sm font-medium text-muted-foreground/70 hover:bg-black/[0.055] hover:text-foreground/80 dark:hover:bg-white/[0.075]`} | |
| 101 | 101 | > |
| 102 | 102 | <PlusIcon aria-hidden className="size-4 shrink-0" /> |
| 103 | 103 | Show {Math.min(props.hiddenCount, THREAD_LINEAGE_PAGE_COUNT)} more |
| ... | ... | @@ -394,7 +394,7 @@ export function ThreadRelationshipsPanel(props: { |
| 394 | 394 | </> |
| 395 | 395 | ); |
| 396 | 396 | return ( |
| 397 | <li key={threadId} className="group flex h-9 items-center rounded-lg"> | |
| 397 | <li key={threadId} className="group flex h-8 items-center rounded-lg"> | |
| 398 | 398 | {isMergeTarget ? ( |
| 399 | 399 | <div className={THREAD_DETAILS_PANEL_LINK_SPLIT_GROUP_CLASS}> |
| 400 | 400 | <Tooltip> |
apps/web/src/components/chat/chatCanvasLayout.test.ts+45-1| ... | ... | @@ -212,7 +212,12 @@ describe("chat canvas layout", () => { |
| 212 | 212 | }); |
| 213 | 213 | }; |
| 214 | 214 | const density = (result: ReturnType<typeof layout>) => { |
| 215 | const card = resolveThreadDetailsCardLayout({ container, ...result }); | |
| 215 | const card = resolveThreadDetailsCardLayout({ | |
| 216 | container, | |
| 217 | lane: { padding: 20, minChatWidth: 640 }, | |
| 218 | frame: result.frame, | |
| 219 | overlapsDetailsCard: result.overlapsDetailsCard, | |
| 220 | }); | |
| 216 | 221 | return card |
| 217 | 222 | ? resolveThreadDetailsCardDensity(card.height, { full: 327, compact: 182 }) |
| 218 | 223 | : "hidden"; |
| ... | ... | @@ -251,3 +256,42 @@ describe("chat canvas layout", () => { |
| 251 | 256 | expect(result.frame).toEqual({ x: 272, y: 396, width: 1300, height: 400 }); |
| 252 | 257 | }); |
| 253 | 258 | }); |
| 259 | ||
| 260 | describe("workspace card beside chat", () => { | |
| 261 | const withCard = (width: number, player: ChatCanvasPreview | null = null, maxChatWidth = 736) => | |
| 262 | resolveChatCanvasLayout({ | |
| 263 | container: { width, height: 900 }, | |
| 264 | maxChatWidth, | |
| 265 | composerHeight: 180, | |
| 266 | detailsCard: { left: width - 292, right: width - 12, bottom: 400 }, | |
| 267 | preview: player, | |
| 268 | }); | |
| 269 | it("keeps chat centered while the card fits beside it", () => { | |
| 270 | expect(withCard(1384).chat).toEqual({ left: 324, width: 736, insetStart: 0, insetEnd: 0 }); | |
| 271 | }); | |
| 272 | it("moves chat left only as far as the card requires", () => { | |
| 273 | expect(withCard(1383).chat).toEqual({ left: 323, width: 736, insetStart: 0, insetEnd: 1 }); | |
| 274 | expect(withCard(1147).chat).toEqual({ left: 87, width: 736, insetStart: 0, insetEnd: 237 }); | |
| 275 | }); | |
| 276 | it("narrows chat only after it reaches the left padding", () => { | |
| 277 | expect(withCard(1080).chat).toMatchObject({ left: 20, width: 736 }); | |
| 278 | expect(withCard(1000).chat).toEqual({ left: 20, width: 656, insetStart: 0, insetEnd: 304 }); | |
| 279 | expect(withCard(984).chat).toMatchObject({ left: 20, width: 640 }); | |
| 280 | }); | |
| 281 | it("keeps a full-width chat clear of the card", () => { | |
| 282 | expect(withCard(1147, null, 10_000).chat).toEqual({ | |
| 283 | left: 20, | |
| 284 | width: 803, | |
| 285 | insetStart: 0, | |
| 286 | insetEnd: 304, | |
| 287 | }); | |
| 288 | }); | |
| 289 | it("keeps centered chat in place when a new preview opens below the card", () => { | |
| 290 | expect(withCard(1440, preview)).toEqual({ | |
| 291 | chat: { left: 352, width: 736, insetStart: 0, insetEnd: 0 }, | |
| 292 | frame: { x: 1108, y: 688, width: 320, height: 200 }, | |
| 293 | overlapsChat: false, | |
| 294 | overlapsDetailsCard: false, | |
| 295 | }); | |
| 296 | }); | |
| 297 | }); |
apps/web/src/components/chat/chatCanvasLayout.ts+22-4| ... | ... | @@ -19,8 +19,11 @@ export interface ChatCanvasPreview { |
| 19 | 19 | } |
| 20 | 20 | |
| 21 | 21 | const GAP = 12; |
| 22 | // Minimum space between chat and the workspace card. Chat stays centered while | |
| 23 | // the card fits beside it with this much room. | |
| 24 | export const DETAILS_CARD_CLEARANCE = 32; | |
| 22 | 25 | |
| 23 | /** Pure geometry shared by the conversation, composer, and floating preview. */ | |
| 26 | /** Pure geometry shared by the conversation, composer, workspace card, and floating preview. */ | |
| 24 | 27 | export function resolveChatCanvasLayout({ |
| 25 | 28 | container, |
| 26 | 29 | preview, |
| ... | ... | @@ -38,9 +41,24 @@ export function resolveChatCanvasLayout({ |
| 38 | 41 | composerHeight?: number; |
| 39 | 42 | detailsCard?: PreviewMiniPlayerObstacles["detailsCard"]; |
| 40 | 43 | }) { |
| 41 | const normalWidth = Math.max(0, Math.min(maxChatWidth, container.width - padding * 2)); | |
| 42 | const normalLeft = (container.width - normalWidth) / 2; | |
| 43 | let chat = { left: normalLeft, width: normalWidth, insetStart: 0, insetEnd: 0 }; | |
| 44 | const centeredWidth = Math.max(0, Math.min(maxChatWidth, container.width - padding * 2)); | |
| 45 | // A workspace card that does not fit beside the centered chat first moves | |
| 46 | // chat left, only as far as it needs. Chat narrows only after it reaches the | |
| 47 | // left padding. | |
| 48 | const laneRight = detailsCard | |
| 49 | ? detailsCard.left - DETAILS_CARD_CLEARANCE | |
| 50 | : container.width - padding; | |
| 51 | const normalWidth = Math.max(0, Math.min(centeredWidth, laneRight - padding)); | |
| 52 | const normalLeft = Math.max( | |
| 53 | padding, | |
| 54 | Math.min((container.width - normalWidth) / 2, laneRight - normalWidth), | |
| 55 | ); | |
| 56 | let chat = { | |
| 57 | left: normalLeft, | |
| 58 | width: normalWidth, | |
| 59 | insetStart: 0, | |
| 60 | insetEnd: Math.max(0, container.width - normalLeft * 2 - normalWidth), | |
| 61 | }; | |
| 44 | 62 | let frame: PreviewMiniPlayerFrame | null = null; |
| 45 | 63 | let overlapsChat = false; |
| 46 | 64 | if (preview && container.width > 0 && container.height > 0) { |
apps/web/src/components/chat/threadDetailsCardLayout.test.ts+15-13| ... | ... | @@ -4,37 +4,39 @@ import { |
| 4 | 4 | resolveThreadDetailsCardLayout, |
| 5 | 5 | } from "./threadDetailsCardLayout"; |
| 6 | 6 | |
| 7 | const lane = { padding: 20, minChatWidth: 640 }; | |
| 7 | 8 | const resolve = (width: number, height: number, previewY: number | null = null) => |
| 8 | 9 | resolveThreadDetailsCardLayout({ |
| 9 | 10 | container: { width, height }, |
| 10 | chat: { left: (width - 736) / 2, width: 736 }, | |
| 11 | lane, | |
| 11 | 12 | frame: previewY === null ? null : { x: width - 332, y: previewY, width: 320, height: 240 }, |
| 12 | 13 | }); |
| 13 | 14 | |
| 14 | describe("floating details card", () => { | |
| 15 | it("uses the right margin without reserving chat space", () => { | |
| 15 | describe("workspace card", () => { | |
| 16 | it("pins to the top right at a fixed width", () => { | |
| 16 | 17 | expect(resolve(1600, 900)).toEqual({ |
| 17 | x: 1276, | |
| 18 | x: 1308, | |
| 18 | 19 | y: 12, |
| 19 | width: 312, | |
| 20 | width: 280, | |
| 20 | 21 | height: 876, |
| 21 | 22 | }); |
| 22 | expect(resolve(1344, 900)?.width).toBe(280); | |
| 23 | expect(resolve(1344, 900)).toMatchObject({ x: 1052, width: 280 }); | |
| 23 | 24 | }); |
| 24 | it("hides when the margin cannot hold readable controls", () => { | |
| 25 | expect(resolve(1200, 900)).toBeNull(); | |
| 25 | it("hides when a readable chat lane cannot fit beside it", () => { | |
| 26 | expect(resolve(984, 900)).toMatchObject({ x: 692 }); | |
| 27 | expect(resolve(983, 900)).toBeNull(); | |
| 26 | 28 | }); |
| 27 | 29 | it("keeps the card at the top right while the preview is freely dragged vertically", () => { |
| 28 | 30 | for (const y of [12, 170, 250, 400, 648]) { |
| 29 | expect(resolve(1600, 900, y)).toEqual({ x: 1276, y: 12, width: 312, height: 876 }); | |
| 31 | expect(resolve(1600, 900, y)).toEqual({ x: 1308, y: 12, width: 280, height: 876 }); | |
| 30 | 32 | } |
| 31 | 33 | }); |
| 32 | it("keeps width and height independent", () => { | |
| 34 | it("keeps full height while a preview stays clear of the card", () => { | |
| 33 | 35 | expect(resolve(1344, 900, 600)).toMatchObject({ width: 280, height: 876 }); |
| 34 | 36 | expect( |
| 35 | 37 | resolveThreadDetailsCardLayout({ |
| 36 | 38 | container: { width: 1600, height: 900 }, |
| 37 | chat: { left: 432, width: 736 }, | |
| 39 | lane, | |
| 38 | 40 | frame: { x: 12, y: 100, width: 320, height: 240 }, |
| 39 | 41 | })?.height, |
| 40 | 42 | ).toBe(876); |
| ... | ... | @@ -45,14 +47,14 @@ describe("card content fitting", () => { |
| 45 | 47 | const place = (previewY: number, previewHeight = 365, overlapsDetailsCard = false) => |
| 46 | 48 | resolveThreadDetailsCardLayout({ |
| 47 | 49 | container: { width: 1584, height: 988 }, |
| 48 | chat: { left: 424, width: 736 }, | |
| 50 | lane, | |
| 49 | 51 | frame: { x: 1260, y: previewY, width: 240, height: previewHeight }, |
| 50 | 52 | overlapsDetailsCard, |
| 51 | 53 | }); |
| 52 | 54 | it("does not fold in response to a drag while the full card can be kept clear", () => { |
| 53 | 55 | for (const y of [12, 170, 225, 240, 340, 380, 611]) { |
| 54 | 56 | const placement = place(y)!; |
| 55 | expect(placement).toMatchObject({ x: 1260, y: 12, height: 964 }); | |
| 57 | expect(placement).toMatchObject({ x: 1292, y: 12, height: 964 }); | |
| 56 | 58 | expect(resolveThreadDetailsCardDensity(placement.height, { full: 327, compact: 182 })).toBe( |
| 57 | 59 | "full", |
| 58 | 60 | ); |
apps/web/src/components/chat/threadDetailsCardLayout.ts+10-5| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | import type { PreviewMiniPlayerFrame } from "../preview/previewMiniPlayerLayout"; |
| 2 | import { DETAILS_CARD_CLEARANCE } from "./chatCanvasLayout"; | |
| 2 | 3 | |
| 3 | 4 | export function resolveThreadDetailsCardDensity( |
| 4 | 5 | height: number, |
| ... | ... | @@ -9,22 +10,26 @@ export function resolveThreadDetailsCardDensity( |
| 9 | 10 | return "essential"; |
| 10 | 11 | } |
| 11 | 12 | |
| 12 | /** The card uses leftover space. It never changes the conversation's width. */ | |
| 13 | /** | |
| 14 | * The card pins to the top right while a readable chat lane fits beside it. | |
| 15 | * The chat canvas decides whether chat moves over to make room. | |
| 16 | */ | |
| 13 | 17 | export function resolveThreadDetailsCardLayout({ |
| 14 | 18 | container, |
| 15 | chat, | |
| 19 | lane, | |
| 16 | 20 | frame, |
| 17 | 21 | overlapsDetailsCard = false, |
| 18 | 22 | }: { |
| 19 | 23 | container: { width: number; height: number }; |
| 20 | chat: { left: number; width: number }; | |
| 24 | lane: { padding: number; minChatWidth: number }; | |
| 21 | 25 | frame: PreviewMiniPlayerFrame | null; |
| 22 | 26 | overlapsDetailsCard?: boolean; |
| 23 | 27 | }) { |
| 24 | 28 | const gap = 12; |
| 25 | const width = Math.min(312, container.width - chat.left - chat.width - gap * 2); | |
| 26 | if (width < 240) return null; | |
| 29 | // Keep in sync with --thread-details-panel-width, which sizes the popover. | |
| 30 | const width = 280; | |
| 27 | 31 | const x = container.width - width - gap; |
| 32 | if (x - DETAILS_CARD_CLEARANCE - lane.padding < lane.minChatWidth) return null; | |
| 28 | 33 | // Resizing consumes the height above the player. Dragging first tries to |
| 29 | 34 | // clear the full card and folds it only when there is no readable placement. |
| 30 | 35 | const height = |
apps/web/src/components/chat/threadDetailsPanelStyles.ts+2-2| ... | ... | @@ -15,7 +15,7 @@ export const THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS = "gap-2.5 px-2.5 text-left" |
| 15 | 15 | // The row supplies the first tint; the hovered or open segment adds a second tint. |
| 16 | 16 | const THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS} data-popup-open:!bg-black/[0.055] dark:data-popup-open:!bg-white/[0.075]`; |
| 17 | 17 | |
| 18 | const THREAD_DETAILS_PANEL_CONTROL_CLASS = `h-9 min-w-0 rounded-lg border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80`; | |
| 18 | const THREAD_DETAILS_PANEL_CONTROL_CLASS = `h-8 min-w-0 rounded-lg border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80`; | |
| 19 | 19 | const THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_HOVER_SURFACE_CLASS} has-[[data-popup-open]]:bg-black/[0.055] dark:has-[[data-popup-open]]:bg-white/[0.075]`; |
| 20 | 20 | |
| 21 | 21 | export const THREAD_DETAILS_PANEL_ROW_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} w-full justify-start ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`; |
| ... | ... | @@ -32,7 +32,7 @@ export const THREAD_DETAILS_PANEL_LINK_SPLIT_PRIMARY_CLASS = `${THREAD_DETAILS_P |
| 32 | 32 | /** The trailing half of a link split row when it carries a word ("Merge") rather than an icon. */ |
| 33 | 33 | export const THREAD_DETAILS_PANEL_LINK_SPLIT_ACTION_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} shrink-0 justify-center rounded-s-none text-primary ${THREAD_DETAILS_PANEL_SPLIT_BUTTON_SURFACE_CLASS}`; |
| 34 | 34 | |
| 35 | export const THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS = `h-9 w-full min-w-0 justify-start rounded-lg border border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80 sm:h-9 sm:text-[13px]`; | |
| 35 | export const THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS = `h-8 w-full min-w-0 justify-start rounded-lg border border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80 sm:h-8 sm:text-[13px]`; | |
| 36 | 36 | |
| 37 | 37 | export const THREAD_DETAILS_PANEL_ICON_CLASS = "size-4 shrink-0 text-muted-foreground"; |
| 38 | 38 |
apps/web/src/index.css+1-1| ... | ... | @@ -117,7 +117,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 117 | 117 | --workspace-titlebar-control-gap: 0.75rem; |
| 118 | 118 | --workspace-titlebar-scroll-fade-height: 1.5rem; |
| 119 | 119 | --chat-content-max-width: 46rem; |
| 120 | --thread-details-panel-width: 19.5rem; | |
| 120 | --thread-details-panel-width: 17.5rem; | |
| 121 | 121 | /* The prompt on phones: iOS zooms into any input under 16px on focus. */ |
| 122 | 122 | --font-size-prompt-touch: max(var(--font-size-prompt, 1rem), 16px); |
| 123 | 123 | /* Horizontal gutter shared by the page header and the chat column, clear of |