wing-ops/frontend/src/tabs/incidents/components/IncidentsLeftPanel.tsx

833 lines
30 KiB
TypeScript
Executable File
Raw Blame 히스토리

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.

import { useState, useMemo, useRef, useEffect, forwardRef } from 'react';
import { MediaModal } from './MediaModal';
export interface Incident {
id: string;
name: string;
status: 'active' | 'investigating' | 'closed';
date: string;
time: string;
region: string;
office: string;
location: { lat: number; lon: number };
causeType?: string;
oilType?: string;
prediction?: string;
vesselName?: string;
mediaCount?: number;
hasImgAnalysis?: boolean;
}
interface IncidentsLeftPanelProps {
incidents: Incident[];
selectedIncidentId: string | null;
onIncidentSelect: (id: string | null) => void;
onFilteredChange?: (filtered: Incident[]) => void;
}
const PERIOD_PRESETS = ['오늘', '1주일', '1개월', '3개월', '6개월', '1년'] as const;
const REGIONS = ['전체', '남해청', '서해청', '동해청', '제주청', '중부청'] as const;
import { fetchIncidentWeather } from '../services/incidentsApi';
import type { WeatherInfo } from '../services/incidentsApi';
function formatDate(d: Date) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${dd}`;
}
function getPresetStartDate(preset: string): string {
const now = new Date();
switch (preset) {
case '오늘':
return formatDate(now);
case '1주일': {
const d = new Date(now);
d.setDate(d.getDate() - 7);
return formatDate(d);
}
case '1개월': {
const d = new Date(now);
d.setMonth(d.getMonth() - 1);
return formatDate(d);
}
case '3개월': {
const d = new Date(now);
d.setMonth(d.getMonth() - 3);
return formatDate(d);
}
case '6개월': {
const d = new Date(now);
d.setMonth(d.getMonth() - 6);
return formatDate(d);
}
case '1년': {
const d = new Date(now);
d.setFullYear(d.getFullYear() - 1);
return formatDate(d);
}
default:
return formatDate(now);
}
}
export function IncidentsLeftPanel({
incidents,
selectedIncidentId,
onIncidentSelect,
onFilteredChange,
}: IncidentsLeftPanelProps) {
const today = formatDate(new Date());
const todayLabel = today.replace(/-/g, '-');
const [searchTerm, setSearchTerm] = useState('');
const [selectedPeriod, setSelectedPeriod] = useState<string>('1개월');
const [dateFrom, setDateFrom] = useState(getPresetStartDate('1개월'));
const [dateTo, setDateTo] = useState(today);
const [selectedRegion, setSelectedRegion] = useState<string>('전체');
const [selectedStatus, setSelectedStatus] = useState<string>('전체');
// Media modal
const [mediaModalIncident, setMediaModalIncident] = useState<Incident | null>(null);
// Weather popup
const [weatherPopupId, setWeatherPopupId] = useState<string | null>(null);
const [weatherPos, setWeatherPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
// undefined = 로딩 중, null = 데이터 없음, WeatherInfo = 데이터 있음
const [weatherInfo, setWeatherInfo] = useState<WeatherInfo | null | undefined>(undefined);
const weatherRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!weatherPopupId) return;
let cancelled = false;
fetchIncidentWeather(parseInt(weatherPopupId)).then((data) => {
if (!cancelled) setWeatherInfo(data);
});
return () => {
cancelled = true;
setWeatherInfo(undefined);
};
}, [weatherPopupId]);
useEffect(() => {
if (!weatherPopupId) return;
const handler = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (
weatherRef.current &&
!weatherRef.current.contains(target) &&
!target.closest('.inc-wx-btn')
) {
setWeatherPopupId(null);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [weatherPopupId]);
const handleWeatherClick = (e: React.MouseEvent, incId: string) => {
e.stopPropagation();
if (weatherPopupId === incId) {
setWeatherPopupId(null);
return;
}
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
let top = rect.bottom + 6;
let left = rect.left;
if (top + 480 > window.innerHeight) top = rect.top - 480;
if (left + 280 > window.innerWidth) left = window.innerWidth - 290;
if (left < 10) left = 10;
setWeatherPos({ top, left });
setWeatherPopupId(incId);
};
const filteredIncidents = useMemo(() => {
return incidents.filter((incident) => {
const matchesSearch =
incident.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(incident.vesselName &&
incident.vesselName.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesRegion = selectedRegion === '전체' || incident.region === selectedRegion;
const matchesStatus =
selectedStatus === '전체' ||
(selectedStatus === 'active' && incident.status === 'active') ||
(selectedStatus === 'investigating' && incident.status === 'investigating') ||
(selectedStatus === 'closed' && incident.status === 'closed');
const matchesDate = incident.date >= dateFrom && incident.date <= dateTo;
return matchesSearch && matchesRegion && matchesStatus && matchesDate;
});
}, [incidents, searchTerm, selectedRegion, selectedStatus, dateFrom, dateTo]);
useEffect(() => {
onFilteredChange?.(filteredIncidents);
}, [filteredIncidents, onFilteredChange]);
const regionCounts = useMemo(() => {
const dateFiltered = incidents.filter((i) => {
const matchesSearch =
i.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(i.vesselName && i.vesselName.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesDate = i.date >= dateFrom && i.date <= dateTo;
return matchesSearch && matchesDate;
});
const counts: Record<string, number> = { 전체: dateFiltered.length };
REGIONS.forEach((r) => {
if (r !== '전체') counts[r] = dateFiltered.filter((i) => i.region === r).length;
});
return counts;
}, [incidents, searchTerm, dateFrom, dateTo]);
const statusCounts = useMemo(() => {
const base = incidents.filter((i) => {
const matchesSearch =
i.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(i.vesselName && i.vesselName.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesRegion = selectedRegion === '전체' || i.region === selectedRegion;
const matchesDate = i.date >= dateFrom && i.date <= dateTo;
return matchesSearch && matchesRegion && matchesDate;
});
return {
active: base.filter((i) => i.status === 'active').length,
investigating: base.filter((i) => i.status === 'investigating').length,
closed: base.filter((i) => i.status === 'closed').length,
};
}, [incidents, searchTerm, selectedRegion, dateFrom, dateTo]);
// Pagination
const pageSize = 6;
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.max(1, Math.ceil(filteredIncidents.length / pageSize));
const safePage = Math.min(currentPage, totalPages);
const pagedIncidents = filteredIncidents.slice((safePage - 1) * pageSize, safePage * pageSize);
const resetPage = () => setCurrentPage(1);
const handlePeriodClick = (preset: string) => {
setSelectedPeriod(preset);
setDateFrom(getPresetStartDate(preset));
setDateTo(today);
resetPage();
};
return (
<div className="flex flex-col h-full bg-bg-surface border-r border-stroke overflow-hidden shrink-0 w-[360px]">
{/* Search */}
<div className="px-4 py-3 border-b border-stroke shrink-0">
<div className="relative">
<span className="absolute left-[10px] top-1/2 -translate-y-1/2 text-caption">🔍</span>
<input
type="text"
placeholder="사고명, 선박명 검색..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
resetPage();
}}
className="w-full py-2 pr-3 pl-8 bg-bg-base border border-stroke text-caption outline-none"
style={{ borderRadius: 'var(--radius-sm)' }}
/>
</div>
</div>
{/* Date Range */}
<div className="flex items-center gap-1.5 px-4 py-2 border-b border-stroke shrink-0">
<input
type="date"
value={dateFrom}
onChange={(e) => {
setDateFrom(e.target.value);
setSelectedPeriod('');
resetPage();
}}
className="bg-bg-base border border-stroke font-mono text-label-2 outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--radius-sm)' }}
/>
<span className="text-fg-disabled text-label-2">~</span>
<input
type="date"
value={dateTo}
onChange={(e) => {
setDateTo(e.target.value);
setSelectedPeriod('');
resetPage();
}}
className="bg-bg-base border border-stroke font-mono text-label-2 outline-none flex-1"
style={{ padding: '5px 8px', borderRadius: 'var(--radius-sm)' }}
/>
<button
onClick={resetPage}
className="rounded-sm text-label-2 font-semibold cursor-pointer whitespace-nowrap text-color-accent"
style={{
padding: '5px 12px',
border: '1px solid rgba(6,182,212,.3)',
background: 'rgba(6,182,212,.08)',
}}
>
</button>
</div>
{/* Period Presets */}
<div className="flex gap-1 px-4 py-1.5 border-b border-stroke shrink-0">
{PERIOD_PRESETS.map((p) => (
<button
key={p}
onClick={() => handlePeriodClick(p)}
className="text-caption font-semibold cursor-pointer"
style={{
padding: '3px 8px',
borderRadius: '14px',
border:
selectedPeriod === p
? '1px solid rgba(6,182,212,0.3)'
: '1px solid var(--stroke-default)',
background: selectedPeriod === p ? 'rgba(6,182,212,0.1)' : 'transparent',
color: selectedPeriod === p ? 'var(--color-accent)' : 'var(--fg-disabled)',
}}
>
{p}
</button>
))}
</div>
{/* Today Summary */}
<div
className="px-4 py-2.5 border-b border-stroke shrink-0"
style={{ background: 'rgba(6,182,212,0.03)' }}
>
<div
className="text-caption font-bold text-fg-disabled mb-2"
style={{ letterSpacing: '0.8px' }}
>
📅 ({todayLabel})
</div>
<div className="flex gap-1.5 flex-wrap">
{REGIONS.map((r) => {
const count = regionCounts[r] ?? 0;
const isActive = selectedRegion === r;
return (
<button
key={r}
onClick={() => {
setSelectedRegion(r);
resetPage();
}}
className="text-label-2 cursor-pointer"
style={{
padding: '4px 10px',
borderRadius: 'var(--radius-sm)',
background: isActive ? 'rgba(6,182,212,0.1)' : 'var(--bg-card)',
border: isActive
? '1px solid rgba(6,182,212,0.25)'
: '1px solid var(--stroke-default)',
color: isActive ? 'var(--color-accent)' : 'var(--fg-sub)',
fontWeight: isActive ? 700 : 400,
}}
>
{r === '전체' ? '전체 ' : `${r} `}
<span
className="font-bold font-mono"
style={{ color: isActive ? 'var(--color-accent)' : undefined }}
>
{r === '전체' ? count : `(${count})`}
</span>
</button>
);
})}
</div>
</div>
{/* Status Filter */}
<div className="flex gap-[5px] px-4 py-2 border-b border-stroke shrink-0">
{[
{ id: '전체', label: '전체', dot: '' },
{ id: 'active', label: `대응중 (${statusCounts.active})`, dot: 'var(--color-danger)' },
{
id: 'investigating',
label: `조사중 (${statusCounts.investigating})`,
dot: 'var(--color-warning)',
},
{ id: 'closed', label: `종료 (${statusCounts.closed})`, dot: 'var(--fg-disabled)' },
].map((s) => (
<button
key={s.id}
onClick={() => {
setSelectedStatus(s.id);
resetPage();
}}
className="flex items-center gap-1 text-caption cursor-pointer"
style={{
padding: '4px 10px',
borderRadius: '12px',
border:
selectedStatus === s.id
? '1px solid rgba(6,182,212,0.3)'
: '1px solid var(--stroke-default)',
background: 'transparent',
color: selectedStatus === s.id ? 'var(--fg-sub)' : 'var(--fg-disabled)',
}}
>
{s.dot && (
<span
style={{ width: '6px', height: '6px', borderRadius: '50%', background: s.dot }}
/>
)}
{s.label}
</button>
))}
</div>
{/* Count */}
<div className="px-4 py-1.5 text-label-2 text-fg-disabled shrink-0 border-b border-stroke">
{filteredIncidents.length}
</div>
{/* Incident List */}
<div
className="flex-1 overflow-y-auto"
style={{
scrollbarWidth: 'thin' as const,
scrollbarColor: 'var(--stroke-light) transparent',
}}
>
{pagedIncidents.length === 0 ? (
<div className="px-4 py-10 text-center text-fg-disabled text-label-2">
.
</div>
) : (
pagedIncidents.map((inc) => {
const isSel = selectedIncidentId === inc.id;
const dotStyle: Record<string, string> = {
active: 'var(--color-danger)',
investigating: 'var(--color-warning)',
closed: 'var(--fg-disabled)',
};
const dotShadow: Record<string, string> = {
active: '0 0 6px var(--color-danger)',
investigating: '0 0 6px var(--color-warning)',
closed: 'none',
};
const stBg: Record<string, string> = {
active: 'rgba(239,68,68,0.15)',
investigating: 'rgba(249,115,22,0.15)',
closed: 'rgba(100,116,139,0.15)',
};
const stColor: Record<string, string> = {
active: 'var(--color-danger)',
investigating: 'var(--color-warning)',
closed: 'var(--fg-disabled)',
};
const stLabel: Record<string, string> = {
active: '대응중',
investigating: '조사중',
closed: '종료',
};
return (
<div
key={inc.id}
onClick={() => onIncidentSelect(isSel ? null : inc.id)}
className="px-4 py-3 border-b border-stroke cursor-pointer"
style={{
background: isSel ? 'rgba(6,182,212,0.04)' : undefined,
borderLeft: isSel ? '3px solid var(--color-accent)' : '3px solid transparent',
transition: 'background 0.15s',
}}
onMouseEnter={(e) => {
if (!isSel) e.currentTarget.style.background = 'rgba(255,255,255,0.02)';
}}
onMouseLeave={(e) => {
if (!isSel) e.currentTarget.style.background = '';
}}
>
{/* Row 1: name + status */}
<div className="flex items-center justify-between mb-[5px]">
<div className="flex items-center gap-1.5 text-caption">
<span
className="shrink-0"
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: dotStyle[inc.status],
boxShadow: dotShadow[inc.status],
}}
/>
{inc.name}
</div>
<span
className="shrink-0 text-caption"
style={{
padding: '2px 10px',
borderRadius: '10px',
background: stBg[inc.status],
color: stColor[inc.status],
}}
>
{stLabel[inc.status]}
</span>
</div>
{/* Row 2: meta */}
<div className="flex items-center gap-2 text-caption text-fg-disabled mb-[5px]">
<span>
{inc.date} {inc.time}
</span>
<span> {inc.office}</span>
</div>
{/* Row 3: tags + buttons */}
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{inc.causeType && (
<span
className="text-caption font-medium text-fg-sub"
style={{
padding: '2px 8px',
borderRadius: '3px',
background: 'rgba(100,116,139,0.08)',
border: '1px solid rgba(100,116,139,0.2)',
}}
>
{inc.causeType}
</span>
)}
{inc.oilType && (
<span
className="text-caption font-medium text-fg-sub"
style={{
padding: '2px 8px',
borderRadius: '3px',
background: 'rgba(100,116,139,0.08)',
border: '1px solid rgba(100,116,139,0.2)',
}}
>
{inc.oilType}
</span>
)}
{inc.prediction && (
<span
className="text-caption font-medium text-fg-sub"
style={{
padding: '2px 8px',
borderRadius: '3px',
background: 'rgba(100,116,139,0.08)',
border: '1px solid rgba(100,116,139,0.2)',
}}
>
{inc.prediction}
</span>
)}
</div>
<div className="flex gap-1">
<button
className="inc-wx-btn cursor-pointer text-label-2"
onClick={(e) => handleWeatherClick(e, inc.id)}
title="사고 위치 기상정보"
style={{
padding: '3px 7px',
borderRadius: '4px',
lineHeight: 1,
border: '1px solid rgba(59,130,246,0.25)',
background:
weatherPopupId === inc.id
? 'rgba(59,130,246,0.18)'
: 'rgba(59,130,246,0.08)',
color: '#60a5fa',
transition: '0.15s',
}}
>
</button>
{(inc.mediaCount ?? 0) > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
setMediaModalIncident(inc);
}}
title="현장정보 조회"
className="cursor-pointer text-label-2"
style={{
padding: '3px 7px',
borderRadius: '4px',
lineHeight: 1,
border: '1px solid rgba(59,130,246,0.25)',
background: 'rgba(59,130,246,0.08)',
color: '#60a5fa',
transition: '0.15s',
}}
>
<span className="text-caption">{inc.mediaCount}</span>
</button>
)}
{inc.hasImgAnalysis && (
<button
onClick={(e) => {
e.stopPropagation();
setMediaModalIncident(inc);
}}
title="현장 이미지 보기"
className="cursor-pointer text-label-2"
style={{
padding: '3px 7px',
borderRadius: '4px',
lineHeight: 1,
border: '1px solid rgba(59,130,246,0.25)',
background: 'rgba(59,130,246,0.08)',
color: '#60a5fa',
transition: '0.15s',
}}
>
📷
</button>
)}
</div>
</div>
</div>
);
})
)}
</div>
{/* Media Modal */}
{mediaModalIncident && (
<MediaModal incident={mediaModalIncident} onClose={() => setMediaModalIncident(null)} />
)}
{/* Weather Popup (fixed position) */}
{weatherPopupId && weatherInfo !== undefined && (
<WeatherPopup
ref={weatherRef}
data={weatherInfo}
position={weatherPos}
onClose={() => setWeatherPopupId(null)}
/>
)}
{/* Pagination */}
<div className="flex items-center justify-between bg-bg-surface shrink-0 border-t border-stroke px-3 py-2">
<div className="text-caption text-fg-disabled">
<b>{filteredIncidents.length}</b> {(safePage - 1) * pageSize + 1}-
{Math.min(safePage * pageSize, filteredIncidents.length)}
</div>
<div className="flex items-center gap-[3px]">
<PgBtn label="⏮" disabled={safePage <= 1} onClick={() => setCurrentPage(1)} />
<PgBtn
label="◀"
disabled={safePage <= 1}
onClick={() => setCurrentPage(Math.max(1, safePage - 1))}
/>
{Array.from({ length: totalPages }, (_, i) => i + 1)
.filter((p) => Math.abs(p - safePage) <= 2)
.map((p) => (
<PgBtn
key={p}
label={String(p)}
active={p === safePage}
onClick={() => setCurrentPage(p)}
/>
))}
<PgBtn
label="▶"
disabled={safePage >= totalPages}
onClick={() => setCurrentPage(Math.min(totalPages, safePage + 1))}
/>
<PgBtn
label="⏭"
disabled={safePage >= totalPages}
onClick={() => setCurrentPage(totalPages)}
/>
</div>
<select
onChange={(e) => {
/* page size change placeholder */ void e;
}}
className="bg-bg-base border border-stroke text-fg-sub text-caption outline-none rounded px-1.5 py-[3px]"
>
<option>6</option>
<option>10</option>
<option>20</option>
</select>
</div>
</div>
);
}
function PgBtn({
label,
active,
disabled,
onClick,
}: {
label: string;
active?: boolean;
disabled?: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
disabled={disabled}
className="flex items-center justify-center font-mono text-caption"
style={{
minWidth: '24px',
height: '24px',
padding: '0 5px',
borderRadius: '4px',
fontWeight: active ? 700 : 600,
background: active ? 'rgba(6,182,212,0.15)' : 'var(--bg-card)',
border: active ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--stroke-default)',
color: active ? 'var(--color-accent)' : 'var(--fg-disabled)',
opacity: disabled ? 0.4 : 1,
pointerEvents: disabled ? 'none' : undefined,
cursor: disabled ? 'default' : 'pointer',
}}
>
{label}
</button>
);
}
/* ════════════════════════════════════════════════════
WeatherPopup 사고 위치 기상정보 팝업
════════════════════════════════════════════════════ */
const WeatherPopup = forwardRef<
HTMLDivElement,
{
data: WeatherInfo | null;
position: { top: number; left: number };
onClose: () => void;
}
>(({ data, position, onClose }, ref) => {
const forecast = data?.forecast ?? [];
return (
<div
ref={ref}
className="fixed overflow-hidden rounded-xl border border-stroke bg-bg-surface"
style={{
zIndex: 9990,
width: 280,
top: position.top,
left: position.left,
borderColor: 'rgba(59,130,246,0.3)',
boxShadow: '0 12px 40px rgba(0,0,0,0.5)',
backdropFilter: 'blur(12px)',
}}
>
{/* Header */}
<div
className="flex items-center justify-between border-b border-stroke px-3.5 py-2.5"
style={{
background: 'linear-gradient(135deg, rgba(59,130,246,0.08), rgba(6,182,212,0.04))',
}}
>
<div className="flex items-center gap-1.5">
<span className="text-body-2">🌤</span>
<div>
<div className="text-label-2 font-bold">{data?.locNm || '기상정보 없음'}</div>
<div className="text-fg-disabled font-mono text-caption">{data?.obsDtm || '-'}</div>
</div>
</div>
<span onClick={onClose} className="cursor-pointer text-fg-disabled text-body-2 p-0.5">
</span>
</div>
{/* Body */}
<div className="px-3.5 py-3">
{/* Main weather */}
<div className="flex items-center gap-3 mb-2.5">
<div className="text-[28px]">{data?.icon || '❓'}</div>
<div>
<div className="font-bold font-mono text-[20px]">{data?.temp || '-'}</div>
<div className="text-fg-disabled text-caption">{data?.weatherDc || '-'}</div>
</div>
</div>
{/* Detail grid */}
<div className="grid grid-cols-2 gap-1.5 text-caption">
<WxCell icon="💨" label="풍향/풍속" value={data?.wind} />
<WxCell icon="🌊" label="파고" value={data?.wave} />
<WxCell icon="💧" label="습도" value={data?.humid} />
<WxCell icon="👁" label="시정" value={data?.vis} />
<WxCell icon="🌡" label="수온" value={data?.sst} />
<WxCell icon="🔄" label="조류" value={data?.tide} />
</div>
{/* Tide info */}
<div className="flex gap-1.5 mt-2">
<div
className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
style={{
background: 'rgba(59,130,246,0.06)',
border: '1px solid rgba(59,130,246,0.1)',
}}
>
<span className="text-caption"></span>
<div>
<div className="text-fg-disabled text-caption"> ()</div>
<div className="font-bold font-mono text-caption text-color-info">
{data?.highTide || '-'}
</div>
</div>
</div>
<div
className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
style={{ background: 'rgba(6,182,212,0.06)', border: '1px solid rgba(6,182,212,0.1)' }}
>
<span className="text-caption"></span>
<div>
<div className="text-fg-disabled text-caption"> ()</div>
<div className="text-color-accent font-bold font-mono text-caption">
{data?.lowTide || '-'}
</div>
</div>
</div>
</div>
{/* 24h Forecast */}
<div className="bg-bg-base mt-2.5 px-2.5 py-2 rounded-md">
<div className="font-bold text-fg-disabled text-caption mb-1.5">24h </div>
{forecast.length > 0 ? (
<div className="flex justify-between font-mono text-fg-sub text-caption">
{forecast.map((f, i) => (
<div key={i} className="text-center">
<div>{f.hour}</div>
<div className="text-caption my-0.5">{f.icon}</div>
<div className="font-semibold">{f.temp}</div>
</div>
))}
</div>
) : (
<div className="text-fg-disabled text-center text-caption py-1"> </div>
)}
</div>
{/* Impact */}
<div
className="mt-2 rounded"
style={{
padding: '6px 10px',
background: 'rgba(249,115,22,0.05)',
border: '1px solid rgba(249,115,22,0.12)',
}}
>
<div className="font-bold text-color-warning text-caption mb-[3px]"> </div>
<div className="text-fg-sub text-caption leading-[1.5]">{data?.impactDc || '-'}</div>
</div>
</div>
</div>
);
});
WeatherPopup.displayName = 'WeatherPopup';
function WxCell({ icon, label, value }: { icon: string; label: string; value?: string | null }) {
return (
<div className="flex items-center bg-bg-base rounded gap-[6px] py-1.5 px-2">
<span className="text-label-1">{icon}</span>
<div>
<div className="text-fg-disabled text-caption">{label}</div>
<div className="font-semibold font-mono">{value || '-'}</div>
</div>
</div>
);
}