mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The previous fix moved the hover group onto the message shell, but the action row still occupied layout space while invisible. That invisible row kept expanding the shell hit area, so moving the pointer into the empty metadata strip could still reveal copy and timestamp controls. Collapse the action row to zero height by default and expand it only while the message shell is hovered or focused. Keep message content spacing separate from the action row so hidden controls do not create a hover target. Constraint: Copy/fork controls must remain reachable after the bubble reveals them. Rejected: Use display:none for the action row | would remove the keyboard focus path entirely. Confidence: high Scope-risk: narrow Directive: Keep hidden message metadata rows out of normal hover hit-testing; do not reintroduce invisible layout space around chat bubbles. Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run -t "keeps user actions anchored" Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run Tested: cd desktop && bun run test src/components/chat/UserMessage.test.tsx --run Tested: bun run check:desktop Tested: git diff --check Not-tested: Live Tauri native hover smoke Related: #642
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import { Check, Copy, GitFork } from 'lucide-react'
|
|
import { useTranslation } from '../../i18n'
|
|
import { useSettingsStore } from '../../stores/settingsStore'
|
|
import { formatExactMessageTimestamp, formatMessageTimestamp } from '../../lib/formatMessageTimestamp'
|
|
import { CopyButton } from '../shared/CopyButton'
|
|
|
|
export type MessageBranchAction = {
|
|
label: string
|
|
loading?: boolean
|
|
onBranch: () => void
|
|
}
|
|
|
|
type Props = {
|
|
copyText?: string
|
|
copyLabel: string
|
|
branchAction?: MessageBranchAction
|
|
align?: 'start' | 'end'
|
|
timestamp?: number
|
|
}
|
|
|
|
export function MessageActionBar({
|
|
copyText,
|
|
copyLabel,
|
|
branchAction,
|
|
align = 'start',
|
|
timestamp,
|
|
}: Props) {
|
|
const t = useTranslation()
|
|
const locale = useSettingsStore((state) => state.locale)
|
|
const hasCopy = Boolean(copyText?.trim())
|
|
const timeLabel = typeof timestamp === 'number'
|
|
? formatMessageTimestamp(timestamp, t, locale)
|
|
: ''
|
|
const exactTimeLabel = typeof timestamp === 'number'
|
|
? formatExactMessageTimestamp(timestamp, locale)
|
|
: ''
|
|
|
|
if (!hasCopy && !branchAction) return null
|
|
|
|
return (
|
|
<div
|
|
data-message-actions
|
|
data-align={align}
|
|
className={`pointer-events-none mt-0 flex h-0 w-full overflow-hidden opacity-0 transition-[height,margin-top,opacity] duration-150 group-hover:pointer-events-auto group-hover:mt-2 group-hover:h-7 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:mt-2 group-focus-within:h-7 group-focus-within:opacity-100 ${
|
|
align === 'end' ? 'justify-end' : 'justify-start'
|
|
}`}
|
|
>
|
|
<div className="flex min-h-7 items-center gap-1.5">
|
|
{hasCopy ? (
|
|
<CopyButton
|
|
text={copyText!}
|
|
label={copyLabel}
|
|
displayLabel={<Copy size={13} strokeWidth={2.2} aria-hidden="true" />}
|
|
displayCopiedLabel={<Check size={13} strokeWidth={2.4} aria-hidden="true" />}
|
|
onPointerUp={(event) => event.currentTarget.blur()}
|
|
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-transparent text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/30 hover:bg-[var(--color-surface-container-low)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
|
|
/>
|
|
) : null}
|
|
{branchAction ? (
|
|
<button
|
|
type="button"
|
|
onClick={branchAction.onBranch}
|
|
disabled={branchAction.loading}
|
|
aria-label={branchAction.label}
|
|
title={branchAction.label}
|
|
onPointerUp={(event) => event.currentTarget.blur()}
|
|
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-transparent text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/30 hover:bg-[var(--color-surface-container-low)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-wait disabled:opacity-60"
|
|
>
|
|
<GitFork size={13} strokeWidth={2.2} aria-hidden="true" />
|
|
</button>
|
|
) : null}
|
|
{timeLabel ? (
|
|
<span
|
|
className="ml-1 inline-flex items-center text-[11px] font-medium tabular-nums text-[var(--color-text-tertiary)]"
|
|
title={exactTimeLabel || timeLabel}
|
|
>
|
|
{timeLabel}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|