initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:36:37 +08:00
commit 3f5bf0e118
88 changed files with 10997 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
'use client';
import { ButtonHTMLAttributes, useState, useRef, useEffect } from 'react';
export type IconName =
| 'eye'
| 'credit-card'
| 'x-circle'
| 'check'
| 'x'
| 'edit'
| 'copy'
| 'pause'
| 'play'
| 'user-x'
| 'user-check'
| 'external-link'
| 'more'
| 'trash';
interface ActionIconProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
icon: IconName;
variant?: 'primary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
label: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
const ICONS: Record<IconName, (size: number) => React.ReactNode> = {
eye: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8z" />
<circle cx="8" cy="8" r="2" />
</svg>
),
'credit-card': (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="1.5" y="3" width="13" height="10" rx="1.5" />
<path d="M1.5 6.5h13M4.5 10h2" />
</svg>
),
'x-circle': (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="8" cy="8" r="6" />
<path d="M10 6l-4 4M6 6l4 4" />
</svg>
),
check: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 8.5l3.5 3.5 6.5-7" />
</svg>
),
x: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<path d="M4 4l8 8M12 4l-8 8" />
</svg>
),
edit: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M11.5 2.5l2 2-8.5 8.5H3v-2l8.5-8.5z" />
</svg>
),
copy: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="5" width="8" height="8" rx="1" />
<path d="M3 11V3.5A1.5 1.5 0 014.5 2H11" />
</svg>
),
pause: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="3" width="2.5" height="10" rx="0.5" />
<rect x="9.5" y="3" width="2.5" height="10" rx="0.5" />
</svg>
),
play: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M4.5 2.5l9 5.5-9 5.5V2.5z" />
</svg>
),
'user-x': (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="6.5" cy="5" r="2.5" />
<path d="M2 14c0-2.5 2-4.5 4.5-4.5S11 11.5 11 14" />
<path d="M12 5l3 3M15 5l-3 3" />
</svg>
),
'user-check': (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="6.5" cy="5" r="2.5" />
<path d="M2 14c0-2.5 2-4.5 4.5-4.5S11 11.5 11 14" />
<path d="M12 6l2 2 3.5-3.5" />
</svg>
),
'external-link': (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M8 2h6v6M14 2L7 9" />
<path d="M6 3H3v10h10v-3" />
</svg>
),
more: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="currentColor">
<circle cx="8" cy="3.5" r="1.5" />
<circle cx="8" cy="8" r="1.5" />
<circle cx="8" cy="12.5" r="1.5" />
</svg>
),
trash: (s) => (
<svg width={s} height={s} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 4.5h10M6.5 4.5V3a1 1 0 011-1h1a1 1 0 011 1v1.5M5 4.5l.5 8.5h5l.5-8.5" />
</svg>
),
};
const VARIANT_STYLES: Record<string, string> = {
primary:
'text-primary-600 hover:bg-primary-50 hover:text-primary-700 focus-visible:ring-primary-500/30',
ghost:
'text-surface-400 hover:bg-surface-100 hover:text-surface-700 focus-visible:ring-surface-500/30',
danger:
'text-surface-400 hover:bg-red-50 hover:text-red-600 focus-visible:ring-red-500/30',
};
const SIZE_MAP = { sm: 32, md: 36, lg: 40 } as const;
const ICON_SIZE_MAP = { sm: 15, md: 16, lg: 18 } as const;
export function ActionIcon({
icon,
variant = 'ghost',
size = 'sm',
label,
onClick,
className = '',
disabled,
...rest
}: ActionIconProps) {
const [showTooltip, setShowTooltip] = useState(false);
const btnRef = useRef<HTMLButtonElement>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
const btnSize = SIZE_MAP[size];
const iconSize = ICON_SIZE_MAP[size];
return (
<button
ref={btnRef}
type="button"
aria-label={label}
onClick={onClick}
disabled={disabled}
className={`relative inline-flex items-center justify-center rounded-lg transition-all duration-150 cursor-pointer focus:outline-none focus-visible:ring-2 disabled:opacity-40 disabled:cursor-not-allowed ${VARIANT_STYLES[variant]} ${className}`}
style={{ width: btnSize, height: btnSize }}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => {
timeoutRef.current = setTimeout(() => setShowTooltip(false), 100);
}}
onFocus={() => setShowTooltip(true)}
onBlur={() => setShowTooltip(false)}
{...rest}
>
{ICONS[icon](iconSize)}
{showTooltip && (
<span
role="tooltip"
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1.5 px-2 py-1 text-[11px] font-medium text-white bg-surface-800 rounded-md whitespace-nowrap pointer-events-none z-50 shadow-sm"
>
{label}
<span className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-surface-800" />
</span>
)}
</button>
);
}