Files
Genarrative/src/components/common/CopyFeedbackButton.tsx
T
kdletters 06bf03a28c 继续收口平台空态与动作按钮
作品架异步状态切换复用 PlatformAsyncStatePanel
复制反馈动作外观改为组合 PlatformActionButton
结果页与调试面板空态继续收口到 PlatformEmptyState
暗色私聊与工坊按钮改为复用 PlatformActionButton
更新 PlatformUiKit 收口文档与团队决策记录
2026-06-11 01:41:15 +08:00

158 lines
3.9 KiB
TypeScript

import { Check, Copy } from 'lucide-react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import {
type PlatformActionButtonSize,
type PlatformActionButtonShape,
type PlatformActionButtonSurface,
type PlatformActionButtonTone,
} from './platformActionButtonModel';
import { PlatformActionButton } from './PlatformActionButton';
import {
getPlatformPillBadgeClassName,
type PlatformPillBadgeSize,
type PlatformPillBadgeTone,
} from './platformPillBadgeModel';
import type { CopyFeedbackState } from './useCopyFeedback';
export type CopyFeedbackButtonActionAppearance = 'plain' | 'pill';
type CopyFeedbackButtonProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'children'
> & {
state: CopyFeedbackState;
idleLabel: ReactNode;
copiedLabel?: ReactNode;
failedLabel?: ReactNode;
idleIcon?: ReactNode;
copiedIcon?: ReactNode;
failedIcon?: ReactNode;
showIcon?: boolean;
showLabel?: boolean;
labelClassName?: string;
accessibleLabel?: string;
actionSurface?: PlatformActionButtonSurface;
actionTone?: PlatformActionButtonTone;
actionSize?: PlatformActionButtonSize;
actionShape?: PlatformActionButtonShape;
actionFullWidth?: boolean;
actionAppearance?: CopyFeedbackButtonActionAppearance;
actionPillTone?: PlatformPillBadgeTone;
actionPillSize?: PlatformPillBadgeSize;
};
function resolveCopyFeedbackLabel(
state: CopyFeedbackState,
idleLabel: ReactNode,
copiedLabel: ReactNode,
failedLabel: ReactNode,
) {
if (state === 'copied') {
return copiedLabel;
}
if (state === 'failed') {
return failedLabel;
}
return idleLabel;
}
/**
* 统一复制反馈按钮。
* useCopyFeedback 负责复制状态,这里只收口按钮里的图标、文案和可访问名称。
*/
export function CopyFeedbackButton({
state,
idleLabel,
copiedLabel = '已复制',
failedLabel = '复制失败',
idleIcon = <Copy className="h-4 w-4" />,
copiedIcon = <Check className="h-4 w-4" />,
failedIcon,
showIcon = true,
showLabel = true,
labelClassName,
accessibleLabel: accessibleLabelOverride,
actionSurface,
actionTone = 'primary',
actionSize = 'sm',
actionShape = 'default',
actionFullWidth = false,
actionAppearance = 'plain',
actionPillTone = 'neutral',
actionPillSize = 'xs',
className,
'aria-label': ariaLabel,
title,
...buttonProps
}: CopyFeedbackButtonProps) {
const label = resolveCopyFeedbackLabel(
state,
idleLabel,
copiedLabel,
failedLabel,
);
const icon =
state === 'copied'
? copiedIcon
: state === 'failed'
? (failedIcon ?? idleIcon)
: idleIcon;
const accessibleLabel =
accessibleLabelOverride ??
(typeof label === 'string'
? label
: typeof idleLabel === 'string'
? idleLabel
: undefined);
const resolvedAriaLabel = ariaLabel ?? accessibleLabel;
const resolvedTitle =
title ?? (typeof accessibleLabel === 'string' ? accessibleLabel : undefined);
const content = (
<>
{showIcon ? icon : null}
{showLabel ? <span className={labelClassName}>{label}</span> : null}
</>
);
if (actionSurface) {
return (
<PlatformActionButton
surface={actionSurface}
tone={actionTone}
size={actionSize}
shape={actionShape}
fullWidth={actionFullWidth}
className={className}
{...buttonProps}
aria-label={resolvedAriaLabel}
title={resolvedTitle}
>
{content}
</PlatformActionButton>
);
}
return (
<button
type="button"
className={[
actionAppearance === 'pill'
? getPlatformPillBadgeClassName({
tone: actionPillTone,
size: actionPillSize,
})
: null,
className,
]
.filter(Boolean)
.join(' ')}
{...buttonProps}
aria-label={resolvedAriaLabel}
title={resolvedTitle}
>
{content}
</button>
);
}