Files
awoooi/apps/web/src/components/shared/lobster-loading.tsx
OG T 65e1edb0ad
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 1m39s
feat(web): OpenClaw 風格龍蝦 SVG + 三色狀態燈號 + 測試修正
前端:
- OpenClawLobster 全新 SVG (參考 dashboardicons.com/icons/openclaw)
  圓潤身體 + 大眼睛 + 鉗子 + 觸角 + 微笑 + 小腳
- 三色版本: red(異常/預設) / green(健康) / yellow(警告)
- LobsterLoading 改用新 SVG

測試修正:
- test_nemotron_failure_still_returns_proposal: func_body 截取 5000→10000
  原因: 函數超過 5000 字元,導致 rfind 找不到最後的 return

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 08:55:21 +08:00

124 lines
5.0 KiB
TypeScript

'use client'
/**
* LobsterLoading — 小龍蝦載入動畫
* =================================
* Sprint 5: 所有「載入中」用小龍蝦動畫取代純文字
* 設計: Q版龍蝦上下浮動 + 文字提示
*
* 建立時間: 2026-04-08 (台北時區)
*/
import { useTranslations } from 'next-intl'
// =============================================================================
// OpenClaw 風格龍蝦 SVG (參考 dashboardicons.com/icons/openclaw)
// 圓潤可愛風格 + 三色版本: red(預設/異常) / green(健康) / yellow(警告)
// =============================================================================
type LobsterColor = 'red' | 'green' | 'yellow'
const COLOR_MAP: Record<LobsterColor, { body: string; dark: string; eye: string }> = {
red: { body: '#ef0011', dark: '#8a000a', eye: '#0b0303' },
green: { body: '#22C55E', dark: '#15803d', eye: '#0b0303' },
yellow: { body: '#F59E0B', dark: '#b45309', eye: '#0b0303' },
}
/** OpenClaw 風格可愛龍蝦 SVG */
export function OpenClawLobster({ size = 36, color = 'red' }: { size?: number; color?: LobsterColor }) {
const c = COLOR_MAP[color]
return (
<svg width={size} height={size} viewBox="0 0 100 100" fill="none">
{/* 身體 (圓潤橢圓) */}
<ellipse cx="50" cy="58" rx="28" ry="32" fill={c.body} />
{/* 頭 (大圓) */}
<circle cx="50" cy="35" r="22" fill={c.body} />
{/* 肚子高光 */}
<ellipse cx="50" cy="60" rx="18" ry="20" fill={c.body} opacity="0.6" />
<ellipse cx="48" cy="56" rx="12" ry="14" fill="white" opacity="0.15" />
{/* 眼睛 (大圓白底 + 黑瞳) */}
<circle cx="40" cy="30" r="7" fill="white" />
<circle cx="60" cy="30" r="7" fill="white" />
<circle cx="41" cy="31" r="4" fill={c.eye} />
<circle cx="61" cy="31" r="4" fill={c.eye} />
{/* 眼睛高光 */}
<circle cx="43" cy="29" r="1.5" fill="white" />
<circle cx="63" cy="29" r="1.5" fill="white" />
{/* 左鉗 */}
<ellipse cx="16" cy="52" rx="10" ry="8" fill={c.body} transform="rotate(-15 16 52)" />
<ellipse cx="13" cy="48" rx="6" ry="5" fill={c.body} transform="rotate(-20 13 48)" />
<path d="M22 56 Q24 50 22 44" stroke={c.dark} strokeWidth="1.5" fill="none" strokeLinecap="round" />
{/* 右鉗 */}
<ellipse cx="84" cy="52" rx="10" ry="8" fill={c.body} transform="rotate(15 84 52)" />
<ellipse cx="87" cy="48" rx="6" ry="5" fill={c.body} transform="rotate(20 87 48)" />
<path d="M78 56 Q76 50 78 44" stroke={c.dark} strokeWidth="1.5" fill="none" strokeLinecap="round" />
{/* 觸角 */}
<path d="M38 16 Q32 4 24 8" stroke={c.dark} strokeWidth="2" fill="none" strokeLinecap="round" />
<circle cx="24" cy="8" r="3" fill={c.body} />
<path d="M62 16 Q68 4 76 8" stroke={c.dark} strokeWidth="2" fill="none" strokeLinecap="round" />
<circle cx="76" cy="8" r="3" fill={c.body} />
{/* 嘴巴 (微笑) */}
<path d="M43 40 Q50 46 57 40" stroke={c.dark} strokeWidth="1.5" fill="none" strokeLinecap="round" />
{/* 腳 */}
<line x1="35" y1="82" x2="30" y2="94" stroke={c.dark} strokeWidth="2" strokeLinecap="round" />
<line x1="45" y1="84" x2="42" y2="96" stroke={c.dark} strokeWidth="2" strokeLinecap="round" />
<line x1="55" y1="84" x2="58" y2="96" stroke={c.dark} strokeWidth="2" strokeLinecap="round" />
<line x1="65" y1="82" x2="70" y2="94" stroke={c.dark} strokeWidth="2" strokeLinecap="round" />
</svg>
)
}
// =============================================================================
// Loading 元件
// =============================================================================
interface LobsterLoadingProps {
text?: string
size?: 'sm' | 'md' | 'lg'
color?: LobsterColor
}
const SIZES = {
sm: { svg: 28, bob: 2, fontSize: 11 },
md: { svg: 40, bob: 3, fontSize: 12 },
lg: { svg: 56, bob: 4, fontSize: 13 },
}
export function LobsterLoading({ text, size = 'md', color = 'red' }: LobsterLoadingProps) {
const tc = useTranslations('common')
const s = SIZES[size]
const displayText = text ?? tc('loading')
return (
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
padding: size === 'lg' ? 48 : size === 'md' ? 32 : 16,
gap: 8,
}}>
<style>{`
@keyframes lobster-loading-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-${s.bob}px); }
}
@keyframes lobster-loading-wave {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
`}</style>
<div style={{ animation: 'lobster-loading-bob 1.2s ease-in-out infinite' }}>
<OpenClawLobster size={s.svg} color={color} />
</div>
<div style={{
fontSize: s.fontSize,
color: '#87867f',
fontFamily: "'DM Mono', monospace",
animation: 'lobster-loading-wave 2s ease-in-out infinite',
}}>
{displayText}
</div>
</div>
)
}
export default LobsterLoading