From e9d76dae64152fea172d91f947b7d262ad9818f3 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:25:09 +0800 Subject: [PATCH] fix(web): address UI polish review feedback --- apps/web/tests/models-settings.e2e.ts | 2 +- .../models-settings/configured.expected.md | 4 +- .../queue-actions/editing.expected.md | 1 + .../queue-actions/layout.expected.md | 2 +- .../snapshots/queue-actions/ui.expected.md | 1 + .../src/client/PopupSelectView.module.css | 5 +- .../src/client/chat/MessageIconActions.tsx | 28 +++++-- .../src/client/chat/message-chrome.ts | 39 ---------- .../src/client/skeleton/InputBar.module.css | 11 +-- .../tests/chat-branch-tails.spec.tsx | 74 +++++++++++++++---- .../ui-conversation/tests/input-bar.spec.tsx | 1 - .../ui-goal/src/client/GoalBar.module.css | 2 +- .../client/ui-goal/src/client/GoalBar.tsx | 2 +- .../client/ui-goal/tests/goalbar.spec.tsx | 2 +- .../src/client/ModelsSection.module.css | 2 + packages/client/ui-primitives/src/Tooltip.tsx | 32 +++++--- .../client/ui-primitives/src/clipboard.ts | 5 +- .../client/ui-primitives/src/icons/index.tsx | 27 +++---- packages/client/ui-primitives/src/index.ts | 1 + .../client/ui-primitives/tests/icons.spec.tsx | 10 ++- .../ui-primitives/tests/tooltip.spec.tsx | 31 ++++++++ .../src/client/PlanReviewPanel.module.css | 4 - .../src/client/QuestionComposer.module.css | 4 - 23 files changed, 174 insertions(+), 116 deletions(-) diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts index c46127c9db..1d9117dc85 100644 --- a/apps/web/tests/models-settings.e2e.ts +++ b/apps/web/tests/models-settings.e2e.ts @@ -58,7 +58,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => { await dialog.getByText('填入各提供方的 API 密钥即可使用其模型。').waitFor({ timeout: 10_000 }) // The dormant pi-ai adapter contributes its whole installed catalog; no // provider is configured yet, so the page is one add button. - const add = dialog.getByRole('button', { name: '+ 添加提供方' }) + const add = dialog.getByRole('button', { name: '添加提供方' }) await add.waitFor({ timeout: 10_000 }) // The button enables once the dormant catalog lands in the join. await expect.poll(async () => add.isEnabled(), { timeout: 10_000 }).toBe(true) diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md index 251352ee00..07f96cb7ae 100644 --- a/apps/web/tests/snapshots/models-settings/configured.expected.md +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -17,4 +17,6 @@ - text: minimax-cn - button "编辑" - button "删除" - - button "+ 添加提供方" + - button "添加提供方": + - img + - text: 添加提供方 diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md index 8df2ea2940..ff9ce89731 100644 --- a/apps/web/tests/snapshots/queue-actions/editing.expected.md +++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md @@ -30,6 +30,7 @@ - textbox "Edit queued message": Edited queue item - button "Save queued message": - img + - tooltip "Save queued message" - button "Cancel editing": - img - textbox "Message the agent" diff --git a/apps/web/tests/snapshots/queue-actions/layout.expected.md b/apps/web/tests/snapshots/queue-actions/layout.expected.md index 08aea1a327..beabaa955a 100644 --- a/apps/web/tests/snapshots/queue-actions/layout.expected.md +++ b/apps/web/tests/snapshots/queue-actions/layout.expected.md @@ -23,7 +23,7 @@ - paragraph: partial - status: Deep diving... - region "To-dos": - - button "To-dos 1/2 tasks · 1 in progress" + - button "To-dos 1 completed · 1 in progress" - img - text: Ongoing Goal Keep the composer context panels aligned - button "Pause goal": diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md index 7afdf3c8b4..3cf1fc3743 100644 --- a/apps/web/tests/snapshots/queue-actions/ui.expected.md +++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md @@ -21,6 +21,7 @@ - text: Edited queue item - button "Edit queued message": - img + - tooltip "Edit queued message" - button "Remove queued message": - img - button "Steer queued message": diff --git a/packages/client/ui-command/src/client/PopupSelectView.module.css b/packages/client/ui-command/src/client/PopupSelectView.module.css index e9a803cd4b..cd457cc376 100644 --- a/packages/client/ui-command/src/client/PopupSelectView.module.css +++ b/packages/client/ui-command/src/client/PopupSelectView.module.css @@ -54,9 +54,8 @@ } .label { - /* Grows (detail stays right-aligned) but never shrinks: when the row runs - out of width the detail alone truncates, keeping the name readable. */ - flex: 1 0 auto; + flex: 1 1 auto; + min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx b/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx index 92a074eef0..b12ffcaf64 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx +++ b/packages/client/ui-conversation/src/client/chat/MessageIconActions.tsx @@ -1,12 +1,12 @@ // Shared IconActions chrome for user, steering, and assistant messages: copy // live, optional branch wiring, and an optional date-aware clock. -import { useCallback, useId, useState } from 'react' +import { useCallback, useEffect, useId, useRef, useState } from 'react' import { - IconBranchOutline16, IconCheckOutline16, IconCopyOutline16, Tooltip, + IconBranchOutline16, IconCheckOutline16, IconCopyOutline16, Tooltip, writeClipboard, } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatViewSlotProps } from '../contract/slots.ts' -import { formatMessageClock, writeClipboard } from './message-chrome.ts' +import { formatMessageClock } from './message-chrome.ts' import { useCalendarDay } from './use-calendar-day.ts' import css from './MessageIconActions.module.css' @@ -42,11 +42,27 @@ export function MessageIconActions({ // Same success chrome as CodeBlock: a short check swap after the write, // gated so re-clicks during the window neither re-copy nor stack timers. const [copied, setCopied] = useState(false) + const copyPending = useRef(false) + const copyTimer = useRef | null>(null) + const copyEpoch = useRef(0) + useEffect(() => () => { + copyEpoch.current += 1 + copyPending.current = false + if (copyTimer.current !== null) clearTimeout(copyTimer.current) + }, []) const onCopy = useCallback(() => { - if (copied) return - void writeClipboard(text).then(() => { + if (copied || copyPending.current) return + const epoch = copyEpoch.current + copyPending.current = true + void writeClipboard(text).then((ok) => { + if (epoch !== copyEpoch.current) return + copyPending.current = false + if (!ok) return setCopied(true) - window.setTimeout(() => { setCopied(false) }, 1000) + copyTimer.current = window.setTimeout(() => { + copyTimer.current = null + setCopied(false) + }, 1000) }) }, [copied, text]) const clockEl = time === undefined ? null : ( diff --git a/packages/client/ui-conversation/src/client/chat/message-chrome.ts b/packages/client/ui-conversation/src/client/chat/message-chrome.ts index 67b625e71d..8e3f0eb6d7 100644 --- a/packages/client/ui-conversation/src/client/chat/message-chrome.ts +++ b/packages/client/ui-conversation/src/client/chat/message-chrome.ts @@ -6,45 +6,6 @@ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots' /** The date-template share of the conversation dictionary the clock consumes. */ export type ClockTranslate = Translate<'clock.md' | 'clock.ymd'> -/** - * Best-effort clipboard write; rejections stay swallowed (no success chrome). - * @param text - Plain text to place on the clipboard. - */ -export async function writeClipboard(text: string): Promise { - // lib.dom types clipboard non-optional, but insecure contexts omit it — - // that runtime gap is exactly what this guard detects. - /* oxlint-disable-next-line typescript/no-unnecessary-condition */ - if (navigator.clipboard?.writeText) { - try { - await navigator.clipboard.writeText(text) - } catch { - // Denied permissions / iframe policy. - } - return - } - // execCommand('copy') is the only clipboard fallback where the async API - // is missing (insecure contexts); deprecated but deliberately retained. - /* oxlint-disable typescript/no-deprecated */ - const exec = typeof document.execCommand === 'function' - ? document.execCommand.bind(document) - : undefined - if (exec === undefined) return - const el = document.createElement('textarea') - el.value = text - el.setAttribute('readonly', '') - el.style.position = 'fixed' - el.style.left = '-9999px' - document.body.appendChild(el) - el.select() - try { - exec('copy') - } catch { - // Clipboard unavailable; the button stays idle. - } - /* oxlint-enable typescript/no-deprecated */ - el.remove() -} - function pad2(n: number): string { return String(n).padStart(2, '0') } diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 5baf1ab548..6ab387c3eb 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -11,17 +11,18 @@ /* Floating capsule input (figma Input_Bottom 75:8208): card floats above the viewport bottom inside the centered message column; textarea on top, action row below, one primary circle button bottom-right. Input width rides the - column (--dsh-composer-card-max-width = chat content + 16px is a cap, not a - fixed size — layout rule: the box shrinks with the center column keeping - its clearance). Hero variant = the same card centered in the empty state; - the transition between the two is a position move of one component. */ + column (--dsh-composer-card-max-width = chat content + 32px, 16px per side, + is a cap, not a fixed size — layout rule: the box shrinks with the center + column keeping its clearance). Hero variant = the same card centered in the + empty state; the transition between the two is a position move of one + component. */ .root { display: flex; flex-direction: column; align-items: center; /* Side pads ride the shared clearance (figma Input_Bottom drew L32/R32/B8; - the sides narrowed with the chat+16 width axis); the bottom gradient mask + the sides narrow with the shared width axis); the bottom gradient mask is owned by the chat scroller. No top pad: the composer stack's gap owns the space above; error/status strips still carry their own margin. */ padding: 0 var(--dsh-composer-side-clearance) 8px; diff --git a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx index 72f56befe0..f3d4542687 100644 --- a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx @@ -102,16 +102,10 @@ describe('MessageItem arms', () => { expect(screen.getByRole('tooltip').textContent).toBe('仅可从已完成轮次的最后一条消息分支') }) - it('user copy stays quiet when execCommand throws or is absent', () => { + it('user copy never claims success when the host rejects the write', async () => { Object.defineProperty(navigator, 'clipboard', { configurable: true, - value: undefined, - }) - Object.defineProperty(document, 'execCommand', { - configurable: true, - value: () => { - throw new Error('denied') - }, + value: { writeText: vi.fn().mockRejectedValue(new Error('denied')) }, }) render( { />, ) fireEvent.click(screen.getByRole('button', { name: '复制' })) - - Object.defineProperty(document, 'execCommand', { - configurable: true, - value: undefined, + await act(async () => { + await Promise.resolve() + await Promise.resolve() }) - fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() + expect(screen.queryByRole('button', { name: '复制成功' })).toBeNull() }) it('copy swaps to the check success chrome, gates re-clicks, and reverts after a second', async () => { @@ -145,7 +139,10 @@ describe('MessageItem arms', () => { }} />, ) - fireEvent.click(screen.getByRole('button', { name: '复制' })) + const copy = screen.getByRole('button', { name: '复制' }) + fireEvent.click(copy) + fireEvent.click(copy) + expect(writeText).toHaveBeenCalledTimes(1) // Two microtask ticks: writeClipboard's own await, then the .then that // lands the success chrome. await act(async () => { @@ -159,6 +156,53 @@ describe('MessageItem arms', () => { expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() }) + it('clears copy feedback work when the message unmounts', async () => { + vi.useFakeTimers() + let finishWrite!: () => void + const writeText = vi.fn(() => new Promise((resolve) => { finishWrite = resolve })) + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }) + const view = render( + , + ) + fireEvent.click(screen.getByRole('button', { name: '复制' })) + view.unmount() + await act(async () => { + finishWrite() + await Promise.resolve() + await Promise.resolve() + }) + expect(vi.getTimerCount()).toBe(0) + + const mounted = render( + , + ) + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText: vi.fn().mockResolvedValue(undefined) }, + }) + fireEvent.click(screen.getByRole('button', { name: '复制' })) + await act(async () => { + await Promise.resolve() + await Promise.resolve() + }) + expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy() + mounted.unmount() + expect(vi.getTimerCount()).toBe(0) + }) + it('consumed steering renders copy and branch actions without a badge', () => { const writeText = vi.fn().mockResolvedValue(undefined) Object.defineProperty(navigator, 'clipboard', { @@ -531,6 +575,6 @@ describe('small branch tails', () => { : undefined} />, ) - expect(view.container.textContent).toBe('1 turns · 1 steps|Input 0 tok · Output 10 tok') + expect(view.container.textContent).toBe('1 turns · 1 steps| Input 0 tok · Output 10 tok') }) }) diff --git a/packages/client/ui-conversation/tests/input-bar.spec.tsx b/packages/client/ui-conversation/tests/input-bar.spec.tsx index 7c6f8e4815..13a152f371 100644 --- a/packages/client/ui-conversation/tests/input-bar.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.spec.tsx @@ -143,7 +143,6 @@ function bench(over?: BenchOptions) { } const view = render() const textarea = view.container.querySelector('textarea')! - // aria-label (not role name): title carries the same label and would double-match. const stopping = over?.running === true && over.subagent === undefined const button = view.container.querySelector( `button[aria-label="${stopping ? '停止生成' : '发送消息'}"]`, diff --git a/packages/client/ui-goal/src/client/GoalBar.module.css b/packages/client/ui-goal/src/client/GoalBar.module.css index cbd6349d88..ffc3149cce 100644 --- a/packages/client/ui-goal/src/client/GoalBar.module.css +++ b/packages/client/ui-goal/src/client/GoalBar.module.css @@ -31,7 +31,7 @@ background: var(--dsw-specific-tip); } -.sparkle { +.goalGlyph { display: inline-flex; flex: none; color: var(--dsw-alias-label-tertiary); diff --git a/packages/client/ui-goal/src/client/GoalBar.tsx b/packages/client/ui-goal/src/client/GoalBar.tsx index ceeee27b07..f7a9050597 100644 --- a/packages/client/ui-goal/src/client/GoalBar.tsx +++ b/packages/client/ui-goal/src/client/GoalBar.tsx @@ -127,7 +127,7 @@ export function GoalBar({ goal, onEdit, onPause, onResume, onClear, t }: GoalBar return (
- + {t(PHASE_LABELS[goal.phase])} {goal.objective} {actionError !== null && {actionError}} diff --git a/packages/client/ui-goal/tests/goalbar.spec.tsx b/packages/client/ui-goal/tests/goalbar.spec.tsx index efed54e19e..931c591000 100644 --- a/packages/client/ui-goal/tests/goalbar.spec.tsx +++ b/packages/client/ui-goal/tests/goalbar.spec.tsx @@ -52,7 +52,7 @@ describe('GoalBar', () => { expect(complete.container.firstChild).toBeNull() }) - it('active goal: sparkle, "进行中的目标", truncated objective, edit and clear actions', () => { + it('active goal: goal glyph, "进行中的目标", truncated objective, edit and clear actions', () => { const actions = makeActions() render() expect(screen.getByText('进行中的目标')).toBeTruthy() diff --git a/packages/client/ui-models/src/client/ModelsSection.module.css b/packages/client/ui-models/src/client/ModelsSection.module.css index 299e4694c1..193c291524 100644 --- a/packages/client/ui-models/src/client/ModelsSection.module.css +++ b/packages/client/ui-models/src/client/ModelsSection.module.css @@ -277,6 +277,8 @@ .selectInput { appearance: none; padding-right: 32px; + /* Data-URI SVGs cannot resolve CSS variables; #81858C is the caption gray + shared by both themes. */ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; diff --git a/packages/client/ui-primitives/src/Tooltip.tsx b/packages/client/ui-primitives/src/Tooltip.tsx index 9204f1efdc..449d4fe717 100644 --- a/packages/client/ui-primitives/src/Tooltip.tsx +++ b/packages/client/ui-primitives/src/Tooltip.tsx @@ -47,19 +47,27 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, const [pos, setPos] = useState<{ x: number; y: number } | null>(null) const bubble = useRef(null) // Horizontal viewport clamp: fixed positioning knows nothing about edges, so - // a centered bubble near the right edge would clip. Measured after paint and - // written straight to the style (no state), so it cannot re-trigger itself. - // EDGE_MARGIN keeps the bubble 12px off the viewport edges instead of flush. + // a centered bubble near the right edge would clip. Each measurement resets + // the base position before applying a direct style offset, allowing a shorter + // label or wider viewport to release a previous clamp without another render. useLayoutEffect(() => { - const el = bubble.current - if (el === null || pos === null) return - const EDGE_MARGIN = 12 - const r = el.getBoundingClientRect() - let dx = 0 - if (r.right > window.innerWidth - EDGE_MARGIN) dx = window.innerWidth - EDGE_MARGIN - r.right - if (r.left + dx < EDGE_MARGIN) dx = EDGE_MARGIN - r.left - if (dx !== 0) el.style.left = `${pos.x + dx}px` - }, [pos]) + if (pos === null) return + const clamp = () => { + const el = bubble.current + /* v8 ignore next -- pos is set only while the bubble is mounted. */ + if (el === null) return + const EDGE_MARGIN = 12 + el.style.left = `${pos.x}px` + const r = el.getBoundingClientRect() + let dx = 0 + if (r.right > window.innerWidth - EDGE_MARGIN) dx = window.innerWidth - EDGE_MARGIN - r.right + if (r.left + dx < EDGE_MARGIN) dx = EDGE_MARGIN - r.left + el.style.left = `${pos.x + dx}px` + } + clamp() + window.addEventListener('resize', clamp) + return () => { window.removeEventListener('resize', clamp) } + }, [label, pos]) const showTimer = useRef | null>(null) // Hover and focus are independent triggers: the bubble hides only after // BOTH clear (hovering away from a focused anchor must not drop it). diff --git a/packages/client/ui-primitives/src/clipboard.ts b/packages/client/ui-primitives/src/clipboard.ts index 39a0eb3e76..0ef84a4123 100644 --- a/packages/client/ui-primitives/src/clipboard.ts +++ b/packages/client/ui-primitives/src/clipboard.ts @@ -1,6 +1,5 @@ -// Package-internal clipboard write, shared by every copy control in this -// package (CodeBlock's code copy, TerminalBlock's output copy). Not part of the -// public surface: consumers get the components, not the host detection. +// Host clipboard write shared by Web UI copy controls. Success feedback stays +// with each control; this seam only reports whether the host accepted a write. /** * Write text to the host clipboard, preferring the async Clipboard API and diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx index eee124d402..71e647a3e9 100644 --- a/packages/client/ui-primitives/src/icons/index.tsx +++ b/packages/client/ui-primitives/src/icons/index.tsx @@ -726,23 +726,16 @@ export const IconListPenOutline16 = ({ size = 16, className }: IconProps) => ( /** ic_ds_goal_outline_16 (goal strip leading glyph: dartboard with a landed arrow) */ export const IconGoalOutline16 = ({ size = 16, className }: IconProps) => ( - - - - - - - - - - + + + ) diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index 79bae60c36..feecb95d3e 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -20,6 +20,7 @@ export { FishLogo } from './FishLogo.tsx' export { BrandWordmark } from './BrandWordmark.tsx' export { Tooltip } from './Tooltip.tsx' export type { TooltipSide } from './Tooltip.tsx' +export { writeClipboard } from './clipboard.ts' export { JsonTree } from './JsonTree.tsx' export type { JsonTreeProps, JsonTreeLabels } from './JsonTree.tsx' export { TerminalBlock, DEFAULT_TERMINAL_MAX_LINES } from './TerminalBlock.tsx' diff --git a/packages/client/ui-primitives/tests/icons.spec.tsx b/packages/client/ui-primitives/tests/icons.spec.tsx index c12f09821a..9877b7df1f 100644 --- a/packages/client/ui-primitives/tests/icons.spec.tsx +++ b/packages/client/ui-primitives/tests/icons.spec.tsx @@ -2,7 +2,9 @@ import { cleanup, render } from '@testing-library/react' import { afterEach, describe, expect, it } from 'vitest' import * as primitives from '@deepseek-ai/dsh-client-ui-primitives' -import { IconApiOutline14, IconArchiveOutline20, IconFolderClose16, IconSendOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { + IconApiOutline14, IconArchiveOutline20, IconFolderClose16, IconGoalOutline16, IconSendOutline16, +} from '@deepseek-ai/dsh-client-ui-primitives' afterEach(cleanup) @@ -44,6 +46,12 @@ describe('ic_ds_ icon set', () => { const archive = render() expect(archive.container.querySelector('svg')!.getAttribute('width')).toBe('20') }) + + it('renders reusable goal glyphs without document-global ids', () => { + const { container } = render(<>) + expect(container.querySelector('[id]')).toBeNull() + expect(container.querySelector('[clip-path]')).toBeNull() + }) }) describe('FishLogo', () => { diff --git a/packages/client/ui-primitives/tests/tooltip.spec.tsx b/packages/client/ui-primitives/tests/tooltip.spec.tsx index 01088db4bd..72b33ce12c 100644 --- a/packages/client/ui-primitives/tests/tooltip.spec.tsx +++ b/packages/client/ui-primitives/tests/tooltip.spec.tsx @@ -92,6 +92,37 @@ describe('Tooltip', () => { } }) + it('reclamps after label and viewport width changes', () => { + const originalWidth = window.innerWidth + const spy = vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (this: Element) { + if (this.getAttribute('role') !== 'tooltip') return rect(900, 1000) + return this.textContent === 'Wide' ? rect(900, 1100) : rect(850, 950) + }) + try { + const view = render( + + + , + ) + fireEvent.mouseEnter(screen.getByText('anchor')) + expect(screen.getByRole('tooltip').style.left).toBe('862px') + + view.rerender( + + + , + ) + expect(screen.getByRole('tooltip').style.left).toBe('950px') + + Object.defineProperty(window, 'innerWidth', { configurable: true, value: 900 }) + fireEvent(window, new Event('resize')) + expect(screen.getByRole('tooltip').style.left).toBe('888px') + } finally { + Object.defineProperty(window, 'innerWidth', { configurable: true, value: originalWidth }) + spy.mockRestore() + } + }) + it('clamps a bubble past the left viewport edge back inside', () => { const spy = vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect(-20, 80)) try { diff --git a/packages/client/ui-question/src/client/PlanReviewPanel.module.css b/packages/client/ui-question/src/client/PlanReviewPanel.module.css index 8da6d10c00..a9b5310d40 100644 --- a/packages/client/ui-question/src/client/PlanReviewPanel.module.css +++ b/packages/client/ui-question/src/client/PlanReviewPanel.module.css @@ -108,10 +108,6 @@ } @media (max-width: 720px) { - .frame { - padding: 6px 10px 10px; - } - .card { border-radius: 16px; } diff --git a/packages/client/ui-question/src/client/QuestionComposer.module.css b/packages/client/ui-question/src/client/QuestionComposer.module.css index 33e4a14f5b..c0b83182d2 100644 --- a/packages/client/ui-question/src/client/QuestionComposer.module.css +++ b/packages/client/ui-question/src/client/QuestionComposer.module.css @@ -367,10 +367,6 @@ } @media (max-width: 720px) { - .frame { - padding: 6px 10px 10px; - } - .card { border-radius: 16px; }