feat(web): 工作台与创作区 - 收敛彩虹选中色并补中性滑动指示与边框高光
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
1ff24903b0
commit
1066cb3be9
7 files changed
+251
-105
No files matched your search
@@ -5,7 +5,7 @@ import { App, Button, Dropdown, Popover } from "antd";
|
||||
import { AppDrawer } from "@/components/ui/product/app-drawer";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { Reorder, motion, useReducedMotion } from "motion/react";
|
||||
import { Reorder, LayoutGroup, motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowDown, ArrowUp, Brain, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, MoreHorizontal, Music2, Pencil, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, UserRound, WandSparkles, Waves, X } from "lucide-react";
|
||||
|
||||
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
|
||||
@@ -20,7 +20,10 @@ import { CachedResourceImage } from "@/components/cached-resource-image";
|
||||
import { CanvasImagePreview } from "@/components/canvas/canvas-image-preview";
|
||||
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
|
||||
import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button";
|
||||
import { HoverBorderGradient } from "@/components/ui/aceternity/hover-border-gradient";
|
||||
import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface";
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
import { aceternityMotion } from "@/lib/aceternity-motion";
|
||||
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
|
||||
import { ASSET_CATEGORY_LABELS } from "@/lib/asset-category";
|
||||
import { formatShotOrdinal } from "@/lib/shot-label";
|
||||
@@ -482,7 +485,13 @@ export function CreationComposer(props: ComposerProps) {
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const composer = <section className={`creation-chat-composer is-${props.variant}`}>
|
||||
const composer = <HoverBorderGradient as="div" duration={2.2} containerClassName="creation-composer-shell" className="creation-composer-shell-inner">
|
||||
<SpotlightSurface
|
||||
className={`creation-chat-composer is-${props.variant}`}
|
||||
contentClassName="contents"
|
||||
spotlightColor="color-mix(in srgb, var(--user-ink) 12%, transparent)"
|
||||
spotlightRadius={280}
|
||||
>
|
||||
{props.variant === "thread" ? <div className="creation-composer-mode-row"><ModePicker mode={props.mode} onModeChange={props.onModeChange} /></div> : null}
|
||||
<div className="creation-chat-writing-surface">
|
||||
<div className="creation-chat-editor">
|
||||
@@ -600,7 +609,8 @@ export function CreationComposer(props: ComposerProps) {
|
||||
</Button>
|
||||
</footer>
|
||||
<CreationMediaPreviewModal url={previewUrl} type={previewType} onClose={() => setPreviewUrl("")} />
|
||||
</section>;
|
||||
</SpotlightSurface>
|
||||
</HoverBorderGradient>;
|
||||
|
||||
if (!promptOptimizerOpen) return composer;
|
||||
|
||||
@@ -630,18 +640,26 @@ export function CreationModeTabs({ mode, onModeChange, agentActive = false, onAg
|
||||
{ mode: "image", icon: <ImageIcon />, label: "图片" },
|
||||
{ mode: "text", icon: <MessageSquareText />, label: "文本" },
|
||||
];
|
||||
const activeIndex = agentActive ? 3 : items.findIndex((item) => item.mode === mode);
|
||||
const vertical = orientation === "vertical";
|
||||
return <div className="creation-mode-tabs" role="group" aria-label="创作模式" data-active-mode={agentActive ? "agent" : mode} data-orientation={orientation} style={{ gridTemplateColumns: vertical ? "minmax(0, 1fr)" : `repeat(${onAgentSelect ? 4 : 3}, minmax(0, 1fr))` }}>
|
||||
<motion.span aria-hidden className="creation-mode-indicator" initial={false} animate={vertical ? { y: `${activeIndex * 100}%` } : { x: `${activeIndex * 100}%` }} transition={{ duration: reducedMotion ? 0 : .3, ease: [.16, 1, .3, 1] }} style={vertical ? { width: "calc(100% - 8px)", height: `calc((100% - 8px) / ${onAgentSelect ? 4 : 3})`, bottom: "auto" } : { width: onAgentSelect ? "calc((100% - 8px) / 4)" : "calc((100% - 8px) / 3)" }} />
|
||||
const indicator = (pressed: boolean) => pressed ? (
|
||||
<motion.span
|
||||
layoutId={`creation-mode-indicator-${orientation}`}
|
||||
className="creation-mode-indicator"
|
||||
aria-hidden
|
||||
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.dock}
|
||||
/>
|
||||
) : null;
|
||||
return <LayoutGroup id={`creation-mode-tabs-${orientation}`}>
|
||||
<div className="creation-mode-tabs" role="group" aria-label="创作模式" data-active-mode={agentActive ? "agent" : mode} data-orientation={orientation} style={{ gridTemplateColumns: orientation === "vertical" ? "minmax(0, 1fr)" : `repeat(${onAgentSelect ? 4 : 3}, minmax(0, 1fr))` }}>
|
||||
{items.map((item) => (
|
||||
<button key={item.mode} type="button" className="creation-mode-button" data-mode={item.mode} aria-pressed={!agentActive && item.mode === mode} aria-label={`${item.label}生成`} onClick={() => onModeChange(item.mode)}>
|
||||
{indicator(!agentActive && item.mode === mode)}
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
{onAgentSelect ? <button type="button" className="creation-mode-button" data-mode="agent" aria-pressed={agentActive} onClick={onAgentSelect}><Brain /><span>Agent</span><i className="creation-mode-spark" aria-hidden /></button> : null}
|
||||
</div>;
|
||||
{onAgentSelect ? <button type="button" className="creation-mode-button" data-mode="agent" aria-pressed={agentActive} onClick={onAgentSelect}>{indicator(agentActive)}<Brain /><span>Agent</span><i className="creation-mode-spark" aria-hidden /></button> : null}
|
||||
</div>
|
||||
</LayoutGroup>;
|
||||
}
|
||||
|
||||
function ModePicker({ mode, onModeChange }: { mode: CreationMode; onModeChange: (mode: CreationMode) => void }) {
|
||||
@@ -735,22 +753,43 @@ const creationEmptySuggestions: Array<{ mode: CreationMode; icon: typeof Clapper
|
||||
];
|
||||
|
||||
export function CreationEmptySuggest({ onStartPrompt, onOpenLibrary }: { onStartPrompt: (mode: CreationMode, prompt: string) => void; onOpenLibrary: () => void }) {
|
||||
return <div className="creation-empty-suggest" aria-label="快捷创作入口">
|
||||
const reducedMotion = useReducedMotion();
|
||||
const [hovered, setHovered] = useState<string | null>(null);
|
||||
return <LayoutGroup id="creation-empty-suggest">
|
||||
<div className="creation-empty-suggest" aria-label="快捷创作入口">
|
||||
{creationEmptySuggestions.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const start = () => {
|
||||
if (item.openLibrary) onOpenLibrary();
|
||||
else onStartPrompt(item.mode, item.prompt);
|
||||
};
|
||||
return <button key={item.title} type="button" className="suggest-card" onClick={start}>
|
||||
<span className={`library-icon-tile suggest-icon is-${item.mode}`}><Icon size={18} strokeWidth={2} /></span>
|
||||
return <motion.button
|
||||
key={item.title}
|
||||
type="button"
|
||||
className="suggest-card"
|
||||
onClick={start}
|
||||
onHoverStart={() => setHovered(item.title)}
|
||||
onHoverEnd={() => setHovered(null)}
|
||||
whileHover={reducedMotion ? undefined : { y: -2 }}
|
||||
transition={aceternityMotion.spring.surface}
|
||||
>
|
||||
{hovered === item.title ? (
|
||||
<motion.span
|
||||
layoutId="creation-suggest-hover"
|
||||
className="suggest-card-hover"
|
||||
aria-hidden
|
||||
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.surface}
|
||||
/>
|
||||
) : null}
|
||||
<span className="library-icon-tile suggest-icon"><Icon size={18} strokeWidth={2} /></span>
|
||||
<span className="suggest-copy">
|
||||
<strong>{item.title}</strong>
|
||||
<span>{item.hint}</span>
|
||||
</span>
|
||||
</button>;
|
||||
</motion.button>;
|
||||
})}
|
||||
</div>;
|
||||
</div>
|
||||
</LayoutGroup>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user