Files
xll-mini-program/src/common/VariantSwitcher.tsx
王冕 a619938e0c Initial commit: 小羚羚小程序 Axhub Make workspace.
Include xll-miniapp prototype, PRD resources, annotation directory sync, agent skills, and cloud publishing setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 15:32:30 +08:00

864 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
*本项目Variant Switcher 组件 (重构版)
*
* 核心特性:
* - 零依赖:不依赖任何外部 CSS 框架或图标库
* - 轻量化 UI使用图标入口替代重型控制条
* - 丰富信息:支持标题和描述
* - 全局面板:支持页面级统一管理和跳转
* - 隐形控制:支持快捷键显隐入口
* - 自动全局入口:当有比选组件时自动显示全局入口按钮
*/
import React, { useState, useEffect, useCallback, CSSProperties, useRef } from 'react';
import { createPortal } from 'react-dom';
// --- 类型定义 ---
export interface VariantItem {
/** 唯一标识,若不提供则使用索引 */
key?: string;
/** 渲染内容 */
content: React.ReactNode;
/** 方案标题 */
title: string;
/** 方案一句话描述 */
description: string;
/** 方案详细说明文档Markdown 格式) */
markdown?: string;
}
export interface VariantAPI {
id: string;
/** 比选方案的中文名称,用于在全局面板中显示 */
name: string;
currentIndex: number;
totalVariants: number;
isDecided: boolean;
variants: VariantItem[]; // 暴露方案详情供全局面板使用
select: (index: number) => void;
confirm: () => void;
reset: () => void;
focus: () => void; // 聚焦到该组件(滚动)
}
declare global {
interface Window {
AXHUB_VARIANT_MANAGER?: VariantManager;
}
}
type Listener = () => void;
export interface VariantManager {
register: (id: string, api: VariantAPI) => void;
unregister: (id: string) => void;
instances: Record<string, VariantAPI>;
subscribe: (listener: Listener) => () => void;
notify: () => void;
setVisibility: (visible: boolean) => void;
isVisible: boolean;
}
export interface VariantSwitcherProps {
id?: string;
/** 比选方案的中文名称,显示在全局面板中(如"头部设计"、"登录页布局" */
name?: string;
/** 方案列表 */
variants: VariantItem[];
defaultIndex?: number;
onConfirm?: (index: number, item: VariantItem) => void;
onReset?: () => void;
style?: CSSProperties;
className?: string;
}
// --- 图标定义 ---
const Icons = {
Switcher: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
),
Check: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
Close: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
),
// 比选图标:两个重叠的卡片,表示多个方案比选
VariantCompare: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{/* 底层卡片 */}
<rect x="4" y="6" width="12" height="10" rx="2" opacity="0.4" />
{/* 顶层卡片(偏移) */}
<rect x="8" y="4" width="12" height="10" rx="2" />
</svg>
),
Target: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="6"></circle>
<circle cx="12" cy="12" r="2"></circle>
</svg>
),
Exit: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
),
Doc: () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
),
Back: () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
)
};
// --- 主题配置 ---
const THEME_COLOR = '#008F5D';
const THEME_COLOR_BG = 'rgba(0, 143, 93, 0.1)';
// --- 内置 Markdown 渲染器(零依赖) ---
const MarkdownViewer: React.FC<{ content: string }> = ({ content }) => {
const parseInlineStyles = (text: string): React.ReactNode => {
// 处理行内代码 `code`
const parts = text.split(/(`[^`]+`)/g);
return parts.map((part, i) => {
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code key={i} style={{
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '13px',
fontFamily: 'Monaco, Consolas, monospace',
color: '#e83e8c'
}}>
{part.slice(1, -1)}
</code>
);
}
// 处理加粗 **text**
const boldParts = part.split(/(\*\*[^*]+\*\*)/g);
return boldParts.map((bp, j) => {
if (bp.startsWith('**') && bp.endsWith('**')) {
return <strong key={`${i}-${j}`}>{bp.slice(2, -2)}</strong>;
}
return bp;
});
});
};
const parseLine = (line: string, index: number): React.ReactNode => {
// 标题
if (line.startsWith('### ')) {
return <h3 key={index} style={{ fontSize: '16px', fontWeight: 600, color: '#333', marginTop: '20px', marginBottom: '8px' }}>{parseInlineStyles(line.slice(4))}</h3>;
}
if (line.startsWith('## ')) {
return <h2 key={index} style={{ fontSize: '18px', fontWeight: 600, color: '#333', marginTop: '24px', marginBottom: '12px', paddingBottom: '8px', borderBottom: '1px solid #eee' }}>{parseInlineStyles(line.slice(3))}</h2>;
}
if (line.startsWith('# ')) {
return <h1 key={index} style={{ fontSize: '22px', fontWeight: 600, color: '#333', marginBottom: '16px' }}>{parseInlineStyles(line.slice(2))}</h1>;
}
// 列表
if (line.startsWith('- ')) {
return (
<li key={index} style={{ marginLeft: '16px', paddingLeft: '8px', color: '#555', marginBottom: '6px', listStyle: 'disc' }}>
{parseInlineStyles(line.slice(2))}
</li>
);
}
if (/^\d+\.\s/.test(line)) {
const match = line.match(/^(\d+)\.\s(.*)$/);
if (match) {
return (
<li key={index} style={{ marginLeft: '16px', paddingLeft: '8px', color: '#555', marginBottom: '6px', listStyle: 'decimal' }}>
{parseInlineStyles(match[2])}
</li>
);
}
}
// 引用
if (line.startsWith('> ')) {
return (
<blockquote key={index} style={{ marginLeft: 0, paddingLeft: '12px', borderLeft: `3px solid ${THEME_COLOR}`, color: '#666', fontStyle: 'italic', margin: '12px 0' }}>
{parseInlineStyles(line.slice(2))}
</blockquote>
);
}
// 空行
if (line.trim() === '') {
return <div key={index} style={{ height: '12px' }} />;
}
// 普通段落
return <p key={index} style={{ color: '#555', lineHeight: 1.7, marginBottom: '12px' }}>{parseInlineStyles(line)}</p>;
};
const lines = content.split('\n');
return (
<div style={{ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', fontSize: '14px' }}>
{lines.map((line, i) => parseLine(line, i))}
</div>
);
};
// --- 全局管理器实现 ---
const listeners: Listener[] = [];
let globalVisible = true;
function initGlobalManager(): VariantManager {
if (!window.AXHUB_VARIANT_MANAGER) {
window.AXHUB_VARIANT_MANAGER = {
instances: {},
isVisible: true,
register(id, api) {
this.instances[id] = api;
this.notify();
},
unregister(id) {
delete this.instances[id];
this.notify();
},
subscribe(listener) {
listeners.push(listener);
return () => {
const idx = listeners.indexOf(listener);
if (idx > -1) listeners.splice(idx, 1);
};
},
notify() {
this.isVisible = globalVisible;
listeners.forEach(fn => fn());
},
setVisibility(visible) {
globalVisible = visible;
this.notify();
}
};
}
return window.AXHUB_VARIANT_MANAGER;
}
// --- Hooks ---
/** 获取所有注册的实例及全局可见性 */
function useVariantManager() {
const [state, setState] = useState<{
instances: Record<string, VariantAPI>;
isVisible: boolean;
}>({ instances: {}, isVisible: true });
useEffect(() => {
const manager = initGlobalManager();
const update = () => setState({
instances: { ...manager.instances },
isVisible: manager.isVisible
});
update();
return manager.subscribe(update);
}, []);
return state;
}
// --- 样式定义 ---
const STYLES = {
container: {
position: 'relative' as const,
width: '100%',
height: '100%',
},
triggerBtn: {
position: 'absolute' as const,
top: '4px',
right: '4px',
zIndex: 9001,
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(255, 255, 255, 0.95)',
border: '1px solid rgba(0, 0, 0, 0.08)',
borderRadius: '2px',
color: '#666',
cursor: 'pointer',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
transition: 'all 0.2s',
},
popover: {
position: 'absolute' as const,
top: '32px',
right: '0px',
width: '260px',
backgroundColor: '#fff',
borderRadius: '2px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
border: '1px solid rgba(0, 0, 0, 0.08)',
padding: '4px',
zIndex: 9999,
display: 'flex',
flexDirection: 'column' as const,
gap: '2px',
opacity: 0,
transform: 'translateY(-4px)',
pointerEvents: 'none' as const,
transition: 'all 0.15s ease-out',
},
popoverVisible: {
opacity: 1,
transform: 'translateY(0)',
pointerEvents: 'auto' as const,
},
variantCard: {
display: 'flex',
flexDirection: 'column' as const,
padding: '8px 10px',
borderRadius: '0',
cursor: 'pointer',
border: 'none',
transition: 'background 0.2s',
textAlign: 'left' as const,
background: 'transparent',
},
variantCardActive: {
background: THEME_COLOR_BG,
border: 'none',
},
variantTitle: {
fontSize: '13px',
fontWeight: 500,
color: '#333',
marginBottom: '2px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
variantDesc: {
fontSize: '12px',
color: '#888',
lineHeight: '1.4',
},
globalTrigger: {
position: 'fixed' as const,
bottom: '24px',
right: '24px',
zIndex: 99999,
width: '32px',
height: '32px',
borderRadius: '16px',
backgroundColor: '#fff',
color: '#555',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.12)',
cursor: 'pointer',
border: '1px solid rgba(0,0,0,0.05)',
transition: 'transform 0.2s, opacity 0.2s',
},
globalPanel: {
position: 'fixed' as const,
top: 0,
right: 0,
bottom: 0,
width: '300px',
backgroundColor: '#fff',
boxShadow: '-4px 0 24px rgba(0,0,0,0.08)',
zIndex: 100000,
padding: '0',
display: 'flex',
flexDirection: 'column' as const,
transform: 'translateX(100%)',
transition: 'transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)',
},
globalPanelVisible: {
transform: 'translateX(0)',
},
globalPanelHeader: {
padding: '16px',
borderBottom: '1px solid #f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: '15px',
fontWeight: 600,
color: '#333',
},
globalPanelContent: {
flex: 1,
overflowY: 'auto' as const,
padding: '16px',
},
globalPanelFooter: {
padding: '12px 16px',
borderTop: '1px solid #f5f5f5',
display: 'flex',
justifyContent: 'center',
},
nodeGroup: {
marginBottom: '16px',
border: '1px solid #eee',
borderRadius: '0',
overflow: 'hidden',
},
nodeHeader: {
padding: '6px 10px',
backgroundColor: '#fafafa',
borderBottom: '1px solid #eee',
fontSize: '12px',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
color: '#666',
},
exitBtn: {
display: 'flex',
alignItems: 'center',
gap: '6px',
background: 'none',
border: 'none',
color: '#999',
fontSize: '12px',
cursor: 'pointer',
padding: '8px',
borderRadius: '0',
transition: 'all 0.2s',
}
};
// --- 全局入口组件(单例) ---
let globalControlMountRef = { current: false };
/** 全局入口控制组件 */
const GlobalVariantControl: React.FC = () => {
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [docView, setDocView] = useState<{ title: string; content: string } | null>(null);
const { instances: allInstances, isVisible } = useVariantManager();
// 键盘快捷键监听
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === '.') {
e.preventDefault();
initGlobalManager().setVisibility(!isVisible);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isVisible]);
const instancesList = Object.values(allInstances);
// 按 id 字母顺序排序,保持稳定的显示顺序
const sortedInstances = [...instancesList].sort((a, b) => a.id.localeCompare(b.id));
// 如果没有实例或不可见,则不渲染
if (!isVisible || sortedInstances.length === 0) {
return null;
}
return (
<>
{/* 全局悬浮球 */}
<button
style={STYLES.globalTrigger}
onClick={() => setIsPanelOpen(true)}
title="方案比选 (Ctrl + .)"
>
<Icons.VariantCompare />
</button>
{/* 全局侧边栏面板 */}
<div style={{
...STYLES.globalPanel,
...(isPanelOpen ? STYLES.globalPanelVisible : {})
}}>
{/* Header */}
<div style={STYLES.globalPanelHeader}>
{docView ? (
<>
<button
onClick={() => setDocView(null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: '#666', display: 'flex', alignItems: 'center', gap: '4px' }}
>
<Icons.Back />
<span style={{ fontSize: '14px' }}></span>
</button>
<button
onClick={() => setIsPanelOpen(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, color: '#999' }}
>
<Icons.Close />
</button>
</>
) : (
<>
<span></span>
<button
onClick={() => setIsPanelOpen(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, color: '#999' }}
>
<Icons.Close />
</button>
</>
)}
</div>
{/* Content */}
<div style={STYLES.globalPanelContent}>
{docView ? (
/* 文档视图 - 简洁无风格 */
<div style={{ padding: '0 4px' }}>
<div style={{ fontSize: '13px', color: '#999', marginBottom: '16px' }}>{docView.title}</div>
<MarkdownViewer content={docView.content} />
</div>
) : (
/* 方案列表视图 */
sortedInstances.map(inst => (
<div key={inst.id} style={STYLES.nodeGroup}>
{/* Node Header */}
<div style={STYLES.nodeHeader}>
<span>{inst.name}</span>
<button
onClick={() => {
inst.focus();
setIsPanelOpen(false);
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: THEME_COLOR, display: 'flex', alignItems: 'center', fontSize: '12px'
}}
>
<Icons.Target />
<span style={{ marginLeft: 4 }}></span>
</button>
</div>
{/* Variants List */}
<div>
{inst.variants.map((v, idx) => {
const isActive = inst.currentIndex === idx;
return (
<div
key={v.key || idx}
style={{
...STYLES.variantCard,
...(isActive ? { background: THEME_COLOR_BG } : {}),
borderBottom: '1px solid #f9f9f9',
borderRadius: 0,
}}
>
<div
onClick={() => inst.select(idx)}
style={{ cursor: 'pointer' }}
>
<div style={STYLES.variantTitle}>
<span>{v.title}</span>
{isActive && <span style={{color: THEME_COLOR, fontSize: '12px'}}></span>}
</div>
<div style={STYLES.variantDesc}>{v.description}</div>
</div>
{/* 文档按钮 */}
{v.markdown && (
<button
onClick={(e) => {
e.stopPropagation();
setDocView({ title: v.title, content: v.markdown! });
}}
style={{
background: 'none', border: 'none', cursor: 'pointer',
color: '#999', display: 'flex', alignItems: 'center',
fontSize: '12px', marginTop: '6px', padding: 0
}}
onMouseEnter={e => e.currentTarget.style.color = THEME_COLOR}
onMouseLeave={e => e.currentTarget.style.color = '#999'}
>
<Icons.Doc />
<span style={{ marginLeft: 4 }}></span>
</button>
)}
</div>
);
})}
</div>
</div>
))
)}
</div>
{/* Footer: Exit Button - 仅在列表视图显示 */}
{!docView && (
<div style={STYLES.globalPanelFooter}>
<button
style={STYLES.exitBtn}
onClick={() => {
initGlobalManager().setVisibility(false);
setIsPanelOpen(false);
}}
title="隐藏比选入口 (Ctrl + . 重新开启)"
onMouseEnter={e => e.currentTarget.style.color = '#333'}
onMouseLeave={e => e.currentTarget.style.color = '#999'}
>
<Icons.Exit />
<span>退</span>
</button>
</div>
)}
</div>
{/* 遮罩层 */}
{isPanelOpen && (
<div
onClick={() => setIsPanelOpen(false)}
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.1)', zIndex: 99999
}}
/>
)}
</>
);
};
// --- 主组件 ---
export const VariantSwitcher: React.FC<VariantSwitcherProps> = ({
id: propId,
name: propName,
variants = [],
defaultIndex = 0,
onConfirm,
onReset,
style,
className,
}) => {
const [instanceId] = useState(() =>
propId || `axhub_vs_${Math.random().toString(36).substr(2, 9)}`
);
// 如果没有提供 name使用 id 作为显示名称
const displayName = propName || instanceId;
const containerRef = useRef<HTMLDivElement>(null);
const [currentIndex, setCurrentIndex] = useState(defaultIndex);
const [isDecided, setIsDecided] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const { isVisible: globalVisible } = useVariantManager();
// 标记当前组件负责渲染全局入口(单例,只渲染一次)
const [isGlobalControlOwner, setIsGlobalControlOwner] = useState(false);
useEffect(() => {
// 如果还没有组件负责渲染全局入口,则当前组件负责
if (!globalControlMountRef.current) {
globalControlMountRef.current = true;
setIsGlobalControlOwner(true);
}
// 组件卸载时,如果当前组件是全局入口的拥有者,则释放
return () => {
if (isGlobalControlOwner) {
globalControlMountRef.current = false;
}
};
}, [isGlobalControlOwner]);
// --- API Methods ---
const select = useCallback((index: number) => {
if (index >= 0 && index < variants.length) {
setCurrentIndex(index);
}
}, [variants.length]);
const confirm = useCallback(() => {
setIsDecided(true);
setIsPopoverOpen(false);
if (variants[currentIndex]) {
onConfirm?.(currentIndex, variants[currentIndex]);
}
}, [currentIndex, variants, onConfirm]);
const reset = useCallback(() => {
setIsDecided(false);
onReset?.();
}, [onReset]);
const focus = useCallback(() => {
if (containerRef.current) {
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
setIsHovered(true);
setTimeout(() => setIsHovered(false), 2000);
}
}, []);
// --- 注册到全局 ---
useEffect(() => {
if (variants.length > 0) {
const manager = initGlobalManager();
const api: VariantAPI = {
id: instanceId,
name: displayName,
currentIndex,
totalVariants: variants.length,
isDecided,
variants,
select,
confirm,
reset,
focus,
};
manager.register(instanceId, api);
return () => manager.unregister(instanceId);
}
}, [instanceId, displayName, currentIndex, variants, isDecided, select, confirm, reset, focus]);
// --- 点击外部关闭弹窗 ---
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsPopoverOpen(false);
}
};
if (isPopoverOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isPopoverOpen]);
if (variants.length === 0) return null;
return (
<>
{/* 全局入口(单例,通过 Portal 渲染到 body只由第一个组件渲染 */}
{isGlobalControlOwner && typeof document !== 'undefined' && createPortal(
<GlobalVariantControl />,
document.body
)}
<div
ref={containerRef}
className={className}
style={{ ...STYLES.container, ...style }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
data-axhub-variant-id={instanceId}
>
{/* 渲染当前方案内容 */}
{variants[currentIndex]?.content}
{/* 触发器图标 (轻量化,悬停显示) */}
{globalVisible && (
<button
style={{
...STYLES.triggerBtn,
opacity: (isHovered || isPopoverOpen) ? 1 : 0,
pointerEvents: (isHovered || isPopoverOpen) ? 'auto' : 'none',
backgroundColor: isPopoverOpen ? THEME_COLOR : '#fff',
color: isPopoverOpen ? '#fff' : '#666',
borderColor: isPopoverOpen ? THEME_COLOR : 'rgba(0,0,0,0.06)'
}}
onClick={(e) => {
e.stopPropagation();
setIsPopoverOpen(!isPopoverOpen);
}}
title="切换方案"
>
<Icons.Switcher />
</button>
)}
{/* 下拉选择面板 */}
{globalVisible && (
<div style={{
...STYLES.popover,
...(isPopoverOpen ? STYLES.popoverVisible : {})
}}>
<div style={{ padding: '4px 8px', fontSize: '12px', color: '#999', fontWeight: 600 }}>
</div>
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
{variants.map((variant, index) => {
const isActive = index === currentIndex;
return (
<div
key={variant.key || index}
onClick={(e) => {
e.stopPropagation();
select(index);
}}
style={{
...STYLES.variantCard,
...(isActive ? STYLES.variantCardActive : {}),
}}
>
<div style={STYLES.variantTitle}>
{variant.title}
{isActive && <Icons.Check />}
</div>
<div style={STYLES.variantDesc}>
{variant.description}
</div>
</div>
);
})}
</div>
<div style={{ borderTop: '1px solid #f5f5f5', marginTop: '4px', paddingTop: '8px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
{isDecided ? (
<button
onClick={(e) => { e.stopPropagation(); reset(); }}
style={{
background: 'transparent', border: '1px solid #eee', borderRadius: '4px',
padding: '4px 10px', fontSize: '12px', cursor: 'pointer', color: '#666'
}}
>
</button>
) : (
<button
onClick={(e) => { e.stopPropagation(); confirm(); }}
style={{
background: THEME_COLOR, border: 'none', borderRadius: '4px',
padding: '4px 10px', fontSize: '12px', cursor: 'pointer', color: '#fff'
}}
>
</button>
)}
</div>
</div>
)}
</div>
</>
);
};
export default VariantSwitcher;