보스 수익 계산기 페이지 테마 토큰화
- Select/Tooltip 공통 컴포넌트 테마 대응 - BossCrystal 루트/CharacterPanel/BossSelector 전체 이관 - 비활성 row/난이도 버튼 테마별 처리 (--disabled-opacity, --inactive-filter) - 라이트 테마 warning 색상 갈색 → 주황 계열로 조정 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f89d13431a
commit
e0dd7d1bc4
6 changed files with 320 additions and 73 deletions
|
|
@ -61,35 +61,49 @@ export default function Select({ value, onChange, options, disabled, className =
|
|||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: flipUp ? 6 : -6, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={`fixed z-[100] rounded-lg border border-white/10 bg-gray-900 text-white shadow-xl overflow-hidden ${
|
||||
className={`fixed z-[100] rounded-lg border overflow-hidden ${
|
||||
flipUp ? 'origin-bottom' : 'origin-top'
|
||||
}`}
|
||||
style={
|
||||
flipUp
|
||||
style={{
|
||||
background: 'var(--popup-bg)',
|
||||
borderColor: 'var(--popup-border)',
|
||||
boxShadow: 'var(--popup-shadow)',
|
||||
color: 'var(--text-strong)',
|
||||
...(flipUp
|
||||
? { bottom: pos.bottomOffset + 4, left: pos.left, minWidth: pos.width }
|
||||
: { top: pos.top + 4, left: pos.left, minWidth: pos.width }
|
||||
}
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className="max-h-60 overflow-y-auto py-1">
|
||||
{options.map((opt) => (
|
||||
{options.map((opt) => {
|
||||
const isActive = opt.value === value
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => { onChange(opt.value); setOpen(false) }}
|
||||
className={`w-full text-left px-3 py-2.5 text-sm transition flex items-center gap-2 ${
|
||||
opt.value === value
|
||||
? 'bg-emerald-500/10 text-emerald-300'
|
||||
: 'hover:bg-white/5'
|
||||
}`}
|
||||
className="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2"
|
||||
style={isActive ? {
|
||||
background: 'var(--option-selected-bg)',
|
||||
color: 'var(--option-selected-text)',
|
||||
} : undefined}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) e.currentTarget.style.background = 'var(--row-hover-bg)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) e.currentTarget.style.background = ''
|
||||
}}
|
||||
>
|
||||
{opt.value === value && (
|
||||
{isActive && (
|
||||
<svg className="w-3 h-3 shrink-0" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M2.5 6L5 8.5L9.5 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
<span className={opt.value !== value ? 'pl-5' : ''}>{opt.label}</span>
|
||||
<span className={!isActive ? 'pl-5' : ''}>{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
@ -103,14 +117,24 @@ export default function Select({ value, onChange, options, disabled, className =
|
|||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => !disabled && setOpen((v) => !v)}
|
||||
className={`w-full flex items-center justify-between gap-2 rounded-lg border bg-gray-950 px-3 py-2 text-sm transition outline-none ${
|
||||
open ? 'border-emerald-500/50' : 'border-white/10 hover:border-white/20'
|
||||
} ${disabled ? 'opacity-50 !cursor-default' : ''}`}
|
||||
className={`w-full flex items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm outline-none ${
|
||||
disabled ? 'opacity-50 !cursor-default' : ''
|
||||
}`}
|
||||
style={{
|
||||
background: 'var(--input-bg)',
|
||||
borderColor: open ? 'var(--input-border-focus)' : 'var(--input-border)',
|
||||
color: 'var(--text-strong)',
|
||||
}}
|
||||
>
|
||||
<span className={selected ? '' : 'text-gray-500'}>
|
||||
<span style={{ color: selected ? 'var(--text-strong)' : 'var(--input-placeholder)' }}>
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<svg className={`w-3.5 h-3.5 text-gray-500 transition ${open ? 'rotate-180' : ''}`} viewBox="0 0 12 12" fill="none">
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 transition ${open ? 'rotate-180' : ''}`}
|
||||
style={{ color: 'var(--input-icon)' }}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -98,8 +98,11 @@ export default function Tooltip({ text, children, placement = 'top', delay = 200
|
|||
zIndex: 9999,
|
||||
opacity: coords ? 1 : 0,
|
||||
transition: 'opacity 120ms ease-out',
|
||||
background: 'var(--tooltip-bg)',
|
||||
color: 'var(--tooltip-text)',
|
||||
borderColor: 'var(--tooltip-border)',
|
||||
}}
|
||||
className="pointer-events-none px-2 py-1 rounded-md bg-gray-900 border border-white/10 text-xs text-gray-200 shadow-lg whitespace-nowrap"
|
||||
className="pointer-events-none px-2 py-1 rounded-md border text-xs shadow-lg whitespace-nowrap"
|
||||
>
|
||||
{text}
|
||||
</div>,
|
||||
|
|
|
|||
|
|
@ -72,12 +72,26 @@ export default function BossCrystal() {
|
|||
return (
|
||||
<div className="h-full">
|
||||
{isLoading ? (
|
||||
<div className="rounded-2xl border border-white/5 bg-gray-900/40 p-16 text-center">
|
||||
<div className="w-6 h-6 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin mx-auto" />
|
||||
<div
|
||||
className="rounded-2xl border p-16 text-center"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
borderColor: 'var(--panel-border)',
|
||||
boxShadow: 'var(--panel-shadow)',
|
||||
}}
|
||||
>
|
||||
<div className="w-6 h-6 border-2 border-t-transparent rounded-full animate-spin mx-auto" style={{ borderColor: 'var(--accent)', borderTopColor: 'transparent' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 lg:grid-cols-[420px_1fr] h-full min-h-0">
|
||||
<div className="rounded-2xl border border-white/5 bg-gray-900/40 p-4 min-h-0 max-h-full self-start overflow-hidden flex flex-col">
|
||||
<div
|
||||
className="rounded-2xl border p-4 min-h-0 max-h-full self-start overflow-hidden flex flex-col"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
borderColor: 'var(--panel-border)',
|
||||
boxShadow: 'var(--panel-shadow)',
|
||||
}}
|
||||
>
|
||||
<CharacterPanel
|
||||
characters={characters}
|
||||
selectedName={selectedChar}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ const LABEL_EN = { easy: 'EASY', normal: 'NORMAL', hard: 'HARD', chaos: 'CHAOS',
|
|||
export default function BossSelector({ characterName, bosses, selections, onChange, maxReached, selectedCount, maxPerCharacter }) {
|
||||
if (!characterName) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-16 text-center text-sm text-gray-500">
|
||||
<div
|
||||
className="rounded-2xl border border-dashed p-16 text-center text-sm"
|
||||
style={{
|
||||
borderColor: 'var(--dashed-border)',
|
||||
background: 'var(--skeleton-bg)',
|
||||
color: 'var(--text-dim)',
|
||||
}}
|
||||
>
|
||||
좌측에서 캐릭터를 선택해주세요
|
||||
</div>
|
||||
)
|
||||
|
|
@ -14,16 +21,37 @@ export default function BossSelector({ characterName, bosses, selections, onChan
|
|||
|
||||
if (bosses.length === 0) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] p-16 text-center text-sm text-gray-500">
|
||||
<div
|
||||
className="rounded-2xl border border-dashed p-16 text-center text-sm"
|
||||
style={{
|
||||
borderColor: 'var(--dashed-border)',
|
||||
background: 'var(--skeleton-bg)',
|
||||
color: 'var(--text-dim)',
|
||||
}}
|
||||
>
|
||||
등록된 보스가 없습니다
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-white/5 bg-gray-900/40 overflow-hidden flex flex-col h-full">
|
||||
<div
|
||||
className="rounded-xl border overflow-hidden flex flex-col h-full"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
borderColor: 'var(--panel-border)',
|
||||
boxShadow: 'var(--panel-shadow)',
|
||||
}}
|
||||
>
|
||||
{/* 헤더 (고정) */}
|
||||
<div className="flex items-center gap-3 px-3 py-3 bg-gray-950/60 border-b border-white/5 text-base font-semibold text-gray-300 shrink-0">
|
||||
<div
|
||||
className="flex items-center gap-3 px-3 py-3 border-b text-base font-medium shrink-0"
|
||||
style={{
|
||||
background: 'var(--surface-2)',
|
||||
borderColor: 'var(--panel-border)',
|
||||
color: 'var(--text-emphasis)',
|
||||
}}
|
||||
>
|
||||
<div className="w-52 shrink-0">보스</div>
|
||||
<div className="flex-1">난이도</div>
|
||||
<div className="w-20 shrink-0 text-center">파티원 수</div>
|
||||
|
|
@ -31,7 +59,7 @@ export default function BossSelector({ characterName, bosses, selections, onChan
|
|||
</div>
|
||||
{/* 목록 (스크롤) */}
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<div className="divide-y divide-white/5">
|
||||
<div className="divide-y" style={{ '--tw-divide-opacity': 1 }}>
|
||||
{bosses.map((boss) => {
|
||||
const availableDiffs = DIFFICULTIES.filter((d) =>
|
||||
boss.difficulties.some((bd) => bd.difficulty === d.key)
|
||||
|
|
@ -52,13 +80,20 @@ export default function BossSelector({ characterName, bosses, selections, onChan
|
|||
return (
|
||||
<div
|
||||
key={boss.id}
|
||||
className={`flex items-center gap-3 px-3 py-3 transition ${
|
||||
disabled ? 'opacity-30 pointer-events-none' : ''
|
||||
className={`flex items-center gap-3 px-3 py-3 border-t first:border-t-0 ${
|
||||
disabled ? 'pointer-events-none' : ''
|
||||
}`}
|
||||
style={{
|
||||
borderColor: 'var(--panel-border)',
|
||||
opacity: disabled ? 'var(--disabled-opacity)' : 1,
|
||||
}}
|
||||
>
|
||||
{/* 보스 이미지 + 이름 */}
|
||||
<div className="flex items-center gap-2.5 w-52 shrink-0">
|
||||
<div className="shrink-0 w-11 h-11 rounded-lg bg-gray-900 overflow-hidden">
|
||||
<div
|
||||
className="shrink-0 w-11 h-11 rounded-lg overflow-hidden"
|
||||
style={{ background: 'var(--surface-nested)' }}
|
||||
>
|
||||
<img src={boss.image_url || '/default.png'} alt={boss.name} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<span className="text-base font-medium leading-tight whitespace-nowrap overflow-hidden text-ellipsis">{boss.name}</span>
|
||||
|
|
@ -72,7 +107,7 @@ export default function BossSelector({ characterName, bosses, selections, onChan
|
|||
background: d.colors.bg,
|
||||
borderColor: d.colors.border,
|
||||
color: d.colors.text,
|
||||
filter: active ? 'none' : 'brightness(0.4)',
|
||||
filter: active ? 'none' : 'var(--inactive-filter)',
|
||||
}
|
||||
return (
|
||||
<button
|
||||
|
|
@ -103,12 +138,20 @@ export default function BossSelector({ characterName, bosses, selections, onChan
|
|||
align="right"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-xs text-gray-700 text-center">-</div>
|
||||
<div
|
||||
className="text-xs text-center"
|
||||
style={{ color: 'var(--text-dim)' }}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 수익 */}
|
||||
<div className={`w-32 shrink-0 text-right text-sm font-medium tabular-nums ${sel ? 'text-emerald-300' : 'text-gray-700'}`}>
|
||||
<div
|
||||
className="w-32 shrink-0 text-right text-sm font-medium tabular-nums"
|
||||
style={{ color: sel ? 'var(--accent-bright)' : 'var(--text-dim)' }}
|
||||
>
|
||||
{sel ? formatMeso(revenue) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,14 +48,16 @@ function CharacterContent({ char, selections, bosses }) {
|
|||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-700 text-4xl">?</span>
|
||||
<span className="text-4xl" style={{ color: 'var(--text-dim)' }}>?</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="flex items-baseline gap-2 min-w-0">
|
||||
<span className="text-base font-semibold truncate">{char.character_name}</span>
|
||||
<span className="text-xs text-gray-500 truncate">Lv.{char.character_level} · {char.job_name}</span>
|
||||
<span className="text-xs truncate" style={{ color: 'var(--text-dim)' }}>
|
||||
Lv.{char.character_level} · {char.job_name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{visibleBosses.length > 0 ? (
|
||||
|
|
@ -68,7 +70,13 @@ function CharacterContent({ char, selections, bosses }) {
|
|||
text={`${diff?.label || ''} ${item.boss.name} · ${formatMeso(item.revenue)}`}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<div className="aspect-square rounded bg-gray-900 overflow-hidden border border-white/5">
|
||||
<div
|
||||
className="aspect-square rounded overflow-hidden border"
|
||||
style={{
|
||||
background: 'var(--surface-nested)',
|
||||
borderColor: 'var(--panel-border)',
|
||||
}}
|
||||
>
|
||||
<img src={item.boss.image_url || '/default.png'} alt="" draggable={false} className="w-full h-full object-cover select-none" />
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
|
|
@ -85,17 +93,38 @@ function CharacterContent({ char, selections, bosses }) {
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-600 italic h-[58px] flex items-center">보스 미선택</div>
|
||||
<div
|
||||
className="text-xs italic h-[58px] flex items-center"
|
||||
style={{ color: 'var(--text-dim)' }}
|
||||
>
|
||||
보스 미선택
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-white/5 pt-2">
|
||||
<div
|
||||
className="flex items-center justify-between border-t pt-2"
|
||||
style={{ borderColor: 'var(--panel-border)' }}
|
||||
>
|
||||
<div className="flex items-baseline gap-1 tabular-nums">
|
||||
<span className={`text-base font-bold ${count > 0 ? 'text-amber-300' : 'text-gray-600'}`}>{count}</span>
|
||||
<span className="text-base font-bold text-amber-300/40">/ {MAX_PER_CHARACTER}</span>
|
||||
<span
|
||||
className="text-base font-bold"
|
||||
style={{ color: count > 0 ? 'var(--warning-text-bright)' : 'var(--text-dim)' }}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
<span
|
||||
className="text-base font-bold"
|
||||
style={{ color: count > 0 ? 'var(--warning-text-dim)' : 'var(--text-dim)' }}
|
||||
>
|
||||
/ {MAX_PER_CHARACTER}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`text-sm font-semibold tabular-nums whitespace-nowrap ${count > 0 ? 'text-emerald-300' : 'text-gray-700'}`}>
|
||||
<div
|
||||
className="text-sm font-semibold tabular-nums whitespace-nowrap"
|
||||
style={{ color: count > 0 ? 'var(--accent-bright)' : 'var(--text-dim)' }}
|
||||
>
|
||||
{count > 0 ? formatMeso(totalRevenue) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -122,17 +151,17 @@ function CharacterItem({ char, isSelected, selections, bosses, onSelect, onRemov
|
|||
if (e.target.closest('button')) return
|
||||
onSelect(char.character_name)
|
||||
}}
|
||||
className={`group relative rounded-xl border cursor-pointer select-none ${
|
||||
isSelected
|
||||
? 'border-emerald-500/40 bg-emerald-500/[0.08]'
|
||||
: 'border-white/5 hover:border-white/15 bg-gray-950/40 hover:bg-gray-950/60'
|
||||
}`}
|
||||
className="group relative rounded-xl border cursor-pointer select-none"
|
||||
style={{
|
||||
borderColor: isSelected ? 'var(--selected-border)' : 'var(--panel-border)',
|
||||
background: isSelected ? 'var(--selected-bg)' : 'var(--surface-3)',
|
||||
}}
|
||||
>
|
||||
{/* 드래그 핸들 */}
|
||||
<div
|
||||
onPointerDown={(e) => { e.preventDefault(); dragControls.start(e) }}
|
||||
className="absolute left-0 top-0 bottom-0 w-8 flex items-center justify-center text-gray-600 hover:text-gray-400 cursor-grab active:cursor-grabbing"
|
||||
style={{ touchAction: 'none' }}
|
||||
className="absolute left-0 top-0 bottom-0 w-8 flex items-center justify-center cursor-grab active:cursor-grabbing"
|
||||
style={{ touchAction: 'none', color: 'var(--text-dim)' }}
|
||||
>
|
||||
<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
|
||||
<circle cx="3" cy="3" r="1.2" />
|
||||
|
|
@ -147,7 +176,8 @@ function CharacterItem({ char, isSelected, selections, bosses, onSelect, onRemov
|
|||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); onRemove(char) }}
|
||||
className="absolute top-2 right-2 z-10 w-6 h-6 rounded text-gray-600 hover:text-red-400 hover:bg-red-500/10 transition opacity-0 group-hover:opacity-100 flex items-center justify-center text-base"
|
||||
className="absolute top-2 right-2 z-10 w-6 h-6 rounded opacity-0 group-hover:opacity-100 flex items-center justify-center text-base hover:bg-[var(--danger-bg-hover)] hover:text-[var(--danger-text)]"
|
||||
style={{ color: 'var(--text-dim)' }}
|
||||
aria-label="삭제"
|
||||
>
|
||||
×
|
||||
|
|
@ -222,13 +252,20 @@ export default function CharacterPanel({
|
|||
return (
|
||||
<div className="flex flex-col gap-4 min-h-0 flex-1">
|
||||
{/* 총 수익 카드 (고정) */}
|
||||
<div className="rounded-2xl border border-emerald-500/30 bg-gradient-to-br from-emerald-500/15 to-emerald-500/5 p-4 space-y-3 shrink-0">
|
||||
<div
|
||||
className="rounded-2xl border p-4 space-y-3 shrink-0"
|
||||
style={{
|
||||
borderColor: 'var(--selected-border)',
|
||||
background: 'var(--selected-bg)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="text-xs text-emerald-200/80">총 주간 수익</div>
|
||||
<div className="text-xs" style={{ color: 'var(--accent-bright)' }}>총 주간 수익</div>
|
||||
<div ref={totalContainerRef} className="mt-1 overflow-hidden">
|
||||
<div
|
||||
ref={totalTextRef}
|
||||
className="font-bold text-emerald-300 leading-tight whitespace-nowrap inline-block"
|
||||
className="font-bold leading-tight whitespace-nowrap inline-block"
|
||||
style={{ color: 'var(--accent-bright)' }}
|
||||
>
|
||||
{totalText}
|
||||
</div>
|
||||
|
|
@ -237,25 +274,42 @@ export default function CharacterPanel({
|
|||
|
||||
<div className="grid grid-cols-[1fr_auto] gap-x-3 items-center">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-400">총 결정 개수</div>
|
||||
<div className="h-2 rounded-full bg-gray-900 overflow-hidden">
|
||||
<div className="text-sm" style={{ color: 'var(--text-muted)' }}>총 결정 개수</div>
|
||||
<div
|
||||
className={`h-full transition-all ${totalCount > MAX_PER_ACCOUNT ? 'bg-amber-500' : 'bg-emerald-500'}`}
|
||||
style={{ width: `${usagePct}%` }}
|
||||
className="h-2 rounded-full overflow-hidden"
|
||||
style={{ background: 'var(--progress-track)' }}
|
||||
>
|
||||
<div
|
||||
className="h-full transition-all"
|
||||
style={{
|
||||
width: `${usagePct}%`,
|
||||
background: totalCount > MAX_PER_ACCOUNT ? 'var(--progress-amber)' : 'var(--progress-emerald)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1 tabular-nums">
|
||||
<span className={`text-2xl font-bold leading-none ${totalCount > MAX_PER_ACCOUNT ? 'text-red-400' : 'text-amber-300'}`}>
|
||||
<span
|
||||
className="text-2xl font-bold leading-none"
|
||||
style={{ color: totalCount > MAX_PER_ACCOUNT ? 'var(--danger-text)' : 'var(--warning-text-bright)' }}
|
||||
>
|
||||
{accountUsage}
|
||||
</span>
|
||||
<span className={`text-2xl font-bold leading-none ${totalCount > MAX_PER_ACCOUNT ? 'text-red-400/40' : 'text-amber-300/40'}`}>
|
||||
<span
|
||||
className="text-2xl font-bold leading-none"
|
||||
style={{
|
||||
color: totalCount > MAX_PER_ACCOUNT ? 'var(--danger-text)' : 'var(--warning-text-dim)',
|
||||
opacity: totalCount > MAX_PER_ACCOUNT ? 0.4 : 1,
|
||||
}}
|
||||
>
|
||||
/ {MAX_PER_ACCOUNT}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{totalCount > MAX_PER_ACCOUNT && (
|
||||
<p className="text-[10px] text-amber-400">⚠ 한도 {totalCount - MAX_PER_ACCOUNT}개 초과</p>
|
||||
<p className="text-[10px]" style={{ color: 'var(--warning-text)' }}>
|
||||
⚠ 한도 {totalCount - MAX_PER_ACCOUNT}개 초과
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -263,7 +317,10 @@ export default function CharacterPanel({
|
|||
<div className="shrink-0">
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none">
|
||||
<span
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"
|
||||
style={{ color: 'var(--input-icon)' }}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M10 10L14 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
|
|
@ -274,18 +331,30 @@ export default function CharacterPanel({
|
|||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); if (error) setError('') }}
|
||||
placeholder="캐릭터 닉네임 검색"
|
||||
className="w-full rounded-lg border-2 border-white/10 bg-gray-950 pl-10 pr-3 py-2.5 text-sm outline-none focus:border-emerald-500/60 hover:border-white/20 transition"
|
||||
className="w-full rounded-lg border-2 pl-10 pr-3 py-2.5 text-sm outline-none transition focus:border-[var(--input-border-focus)] hover:border-[var(--input-border-hover)]"
|
||||
style={{
|
||||
background: 'var(--input-bg)',
|
||||
borderColor: 'var(--input-border)',
|
||||
color: 'var(--text-strong)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={searchMutation.isPending}
|
||||
className="rounded-lg bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 px-5 py-2.5 text-sm font-medium transition shrink-0 shadow-lg shadow-emerald-500/20"
|
||||
className="rounded-lg disabled:opacity-50 px-5 py-2.5 text-sm font-medium shrink-0 hover:bg-[var(--btn-primary-bg-hover)]"
|
||||
style={{
|
||||
background: 'var(--btn-primary-bg)',
|
||||
color: 'var(--btn-primary-text)',
|
||||
boxShadow: 'var(--btn-primary-shadow)',
|
||||
}}
|
||||
>
|
||||
{searchMutation.isPending ? '...' : '추가'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <p className="text-xs text-red-400 mt-1.5">{error}</p>}
|
||||
{error && (
|
||||
<p className="text-xs mt-1.5" style={{ color: 'var(--danger-text)' }}>{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 캐릭터 목록 (스크롤) */}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,53 @@
|
|||
--maintenance-text: #fbbf24;
|
||||
|
||||
--dashed-border: rgba(255, 255, 255, 0.1);
|
||||
|
||||
--surface-2: rgba(2, 6, 23, 0.6);
|
||||
--surface-3: rgba(17, 24, 39, 0.4);
|
||||
--surface-nested: #0f172a;
|
||||
|
||||
--input-bg: #020617;
|
||||
--input-border: rgba(255, 255, 255, 0.1);
|
||||
--input-border-hover: rgba(255, 255, 255, 0.2);
|
||||
--input-border-focus: rgba(16, 185, 129, 0.5);
|
||||
--input-placeholder: #6b7280;
|
||||
--input-icon: #6b7280;
|
||||
|
||||
--selected-bg: rgba(16, 185, 129, 0.08);
|
||||
--selected-border: rgba(16, 185, 129, 0.4);
|
||||
--option-selected-bg: rgba(16, 185, 129, 0.12);
|
||||
--option-selected-text: #6ee7b7;
|
||||
|
||||
--btn-primary-bg: #059669;
|
||||
--btn-primary-bg-hover: #10b981;
|
||||
--btn-primary-text: #ffffff;
|
||||
--btn-primary-shadow: 0 4px 14px rgba(16, 185, 129, 0.2);
|
||||
|
||||
--danger-text: #f87171;
|
||||
--danger-text-strong: #dc2626;
|
||||
--danger-bg-hover: rgba(239, 68, 68, 0.1);
|
||||
--warning-text: #fbbf24;
|
||||
--warning-text-bright: #fcd34d;
|
||||
--warning-text-dim: rgba(252, 211, 77, 0.4);
|
||||
|
||||
--progress-track: #0f172a;
|
||||
--progress-emerald: #10b981;
|
||||
--progress-amber: #f59e0b;
|
||||
|
||||
--accent-bright: #6ee7b7;
|
||||
--accent-muted: rgba(16, 185, 129, 0.1);
|
||||
--accent-text-on-emerald: #ecfdf5;
|
||||
|
||||
--tooltip-bg: #111827;
|
||||
--tooltip-border: rgba(255, 255, 255, 0.1);
|
||||
--tooltip-text: #e5e7eb;
|
||||
|
||||
--popup-bg: #111827;
|
||||
--popup-border: rgba(255, 255, 255, 0.1);
|
||||
--popup-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
|
||||
--disabled-opacity: 0.3;
|
||||
--inactive-filter: brightness(0.4);
|
||||
}
|
||||
|
||||
/* 테마 토큰 - light */
|
||||
|
|
@ -148,6 +195,53 @@
|
|||
--maintenance-text: #b45309;
|
||||
|
||||
--dashed-border: rgba(0, 0, 0, 0.12);
|
||||
|
||||
--surface-2: #f8fafc;
|
||||
--surface-3: #f9fafb;
|
||||
--surface-nested: #f3f4f6;
|
||||
|
||||
--input-bg: #ffffff;
|
||||
--input-border: rgba(0, 0, 0, 0.12);
|
||||
--input-border-hover: rgba(0, 0, 0, 0.22);
|
||||
--input-border-focus: rgba(5, 150, 105, 0.55);
|
||||
--input-placeholder: #9ca3af;
|
||||
--input-icon: #94a3b8;
|
||||
|
||||
--selected-bg: rgba(16, 185, 129, 0.08);
|
||||
--selected-border: rgba(5, 150, 105, 0.5);
|
||||
--option-selected-bg: rgba(16, 185, 129, 0.12);
|
||||
--option-selected-text: #047857;
|
||||
|
||||
--btn-primary-bg: #059669;
|
||||
--btn-primary-bg-hover: #047857;
|
||||
--btn-primary-text: #ffffff;
|
||||
--btn-primary-shadow: 0 4px 14px rgba(16, 185, 129, 0.25);
|
||||
|
||||
--danger-text: #dc2626;
|
||||
--danger-text-strong: #b91c1c;
|
||||
--danger-bg-hover: rgba(220, 38, 38, 0.08);
|
||||
--warning-text: #c2410c;
|
||||
--warning-text-bright: #ea580c;
|
||||
--warning-text-dim: rgba(234, 88, 12, 0.4);
|
||||
|
||||
--progress-track: #e5e7eb;
|
||||
--progress-emerald: #10b981;
|
||||
--progress-amber: #f59e0b;
|
||||
|
||||
--accent-bright: #047857;
|
||||
--accent-muted: rgba(16, 185, 129, 0.1);
|
||||
--accent-text-on-emerald: #ecfdf5;
|
||||
|
||||
--tooltip-bg: #111827;
|
||||
--tooltip-border: rgba(255, 255, 255, 0.08);
|
||||
--tooltip-text: #f3f4f6;
|
||||
|
||||
--popup-bg: #ffffff;
|
||||
--popup-border: rgba(0, 0, 0, 0.1);
|
||||
--popup-shadow: 0 10px 30px rgba(15, 23, 42, 0.15);
|
||||
|
||||
--disabled-opacity: 0.5;
|
||||
--inactive-filter: opacity(0.25);
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue