mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Message actions still fade in on hover and focus, but their row now keeps a stable height so transcript content does not jump when the pointer enters or leaves a message. Constraint: Preserve the existing hover-only visibility and copy/fork interaction model. Rejected: Keep animating height from zero | it causes the visible transcript reflow this change is fixing. Confidence: high Scope-risk: narrow Tested: cd desktop && /Users/nanmi/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node ./node_modules/typescript/bin/tsc --noEmit Tested: git diff --check -- desktop/src/components/chat/MessageActionBar.tsx desktop/src/components/chat/MessageList.test.tsx Not-tested: Focused Vitest could not complete in this local shell because the Vitest/esbuild startup process stalled or reported esbuild service stopped.
84 lines
3.4 KiB
TypeScript
84 lines
3.4 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-2 flex h-7 w-full opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto 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>
|
|
)
|
|
}
|