97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
import { Check, Sparkles, X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import type { YouthActiveChoice } from './youth-form-projection';
|
|
|
|
export type CreativeChoiceGridProps = {
|
|
choice: YouthActiveChoice;
|
|
disabled?: boolean;
|
|
onSelect: (optionId: string) => void;
|
|
onReject?: () => void;
|
|
};
|
|
|
|
/**
|
|
* A small, keyboard-friendly surface for the one decision the creator needs
|
|
* to make next. The server-provided option label stays visible verbatim (so
|
|
* options such as “你帮我选” remain useful), while the first option gets a
|
|
* gentle recommendation marker.
|
|
*/
|
|
export function CreativeChoiceGrid({
|
|
choice,
|
|
disabled = false,
|
|
onSelect,
|
|
onReject,
|
|
}: CreativeChoiceGridProps) {
|
|
if (choice.options.length === 0) return null;
|
|
|
|
return (
|
|
<section
|
|
data-testid="youth-creation-choice"
|
|
className="rounded-2xl border border-brand/20 bg-brand/[0.035] p-4"
|
|
>
|
|
<div className="flex items-start gap-2.5">
|
|
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-brand/[0.1] text-brand">
|
|
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="text-sm font-semibold text-foreground">{choice.title}</h3>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground">{choice.body}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<fieldset className="mt-3 space-y-2">
|
|
<legend className="sr-only">选择一个创作方向</legend>
|
|
{choice.options.map((option, index) => {
|
|
const descriptionId = `${choice.id}-${option.id}-description`;
|
|
return (
|
|
<button
|
|
key={option.id}
|
|
type="button"
|
|
disabled={disabled}
|
|
aria-describedby={option.description ? descriptionId : undefined}
|
|
className={cn(
|
|
'flex min-h-11 w-full items-center gap-3 rounded-xl border border-border/70 bg-background px-3 text-left transition-colors duration-150',
|
|
'hover:border-brand/35 hover:bg-brand/[0.04]',
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 focus-visible:ring-offset-2',
|
|
'disabled:pointer-events-none disabled:opacity-55',
|
|
)}
|
|
onClick={() => onSelect(option.id)}
|
|
>
|
|
<span className="flex min-w-0 flex-1 flex-col justify-center gap-0.5 py-2">
|
|
<span className="flex flex-wrap items-center gap-2 text-sm font-medium text-foreground">
|
|
<span>{option.label}</span>
|
|
{index === 0 && (
|
|
<span className="rounded-full bg-brand/[0.1] px-2 py-0.5 text-[11px] font-medium text-brand">
|
|
推荐
|
|
</span>
|
|
)}
|
|
</span>
|
|
{option.description && (
|
|
<span
|
|
id={descriptionId}
|
|
className="text-xs leading-5 text-muted-foreground"
|
|
>
|
|
{option.description}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<Check className="h-4 w-4 shrink-0 text-brand/75" aria-hidden="true" />
|
|
</button>
|
|
);
|
|
})}
|
|
</fieldset>
|
|
|
|
{onReject && (
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
className="mt-2 inline-flex min-h-11 items-center gap-1.5 rounded-xl px-2.5 text-xs text-muted-foreground transition-colors hover:bg-background hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-55"
|
|
onClick={onReject}
|
|
>
|
|
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
|
先跳过这题
|
|
</button>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|