feat: 모바일 반응형 UI (drawer, 아코디언, 범례)

- Topbar: 햄버거 메뉴 버튼, 반응형 stats 숨김
- Sidebar: 모바일 drawer (fixed + translate-x), backdrop
- Sidebar: Section 아코디언으로 전환 (details/summary)
- Legend: 접기/펼치기 토글 추가
- panels.css: .sb/.sb-t 클래스 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-17 06:54:27 +09:00
부모 40229a75c0
커밋 61fc3bbce4
5개의 변경된 파일437개의 추가작업 그리고 428개의 파일을 삭제

파일 보기

@ -1,24 +1,3 @@
.sb {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.sb-t {
font-size: 9px;
font-weight: 700;
color: var(--muted);
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 6px;
}
.sb-t-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.relation-sort {
display: flex;
align-items: center;

파일 보기

@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import { useAuth } from "../../shared/auth";
import { useTheme } from "../../shared/hooks";
import { useAisTargetPolling } from "../../features/aisPolling/useAisTargetPolling";
@ -60,6 +60,7 @@ function useLegacyIndex(data: LegacyVesselDataset | null) {
export function DashboardPage() {
const { user, logout } = useAuth();
const { theme, toggleTheme } = useTheme();
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const uid = user?.id ?? null;
// ── Data fetching ──
@ -263,9 +264,13 @@ export function DashboardPage() {
onLogout={logout}
theme={theme}
onToggleTheme={toggleTheme}
isSidebarOpen={isSidebarOpen}
onMenuToggle={() => setIsSidebarOpen((v) => !v)}
/>
<DashboardSidebar
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
state={state}
legacyVesselsAll={legacyVesselsAll}
legacyVesselsFiltered={legacyVesselsFiltered}

파일 보기

@ -1,4 +1,5 @@
import { ToggleButton } from '@wing/ui';
import { useEffect } from 'react';
import { ToggleButton, Section } from '@wing/ui';
import type { AisTarget } from '../../entities/aisTarget/model/types';
import type { LegacyVesselIndex } from '../../entities/legacyVessel/lib';
import type { LegacyVesselDataset, LegacyVesselInfo } from '../../entities/legacyVessel/model/types';
@ -28,8 +29,9 @@ function fmtBbox(b: Bbox | null) {
}
interface DashboardSidebarProps {
isOpen: boolean;
onClose: () => void;
state: ReturnType<typeof useDashboardState>;
// Derived data
legacyVesselsAll: DerivedLegacyVessel[];
legacyVesselsFiltered: DerivedLegacyVessel[];
legacyCounts: Record<VesselTypeCode, number>;
@ -43,7 +45,6 @@ interface DashboardSidebarProps {
alarmFilterSummary: string;
speedPanelType: VesselTypeCode;
onFleetContextMenu: (ownerKey: string, mmsis: number[]) => void;
// Data fetching (admin panels)
snapshot: AisPollingSnapshot;
legacyError: string | null;
legacyData: LegacyVesselDataset | null;
@ -54,6 +55,8 @@ interface DashboardSidebarProps {
}
export function DashboardSidebar({
isOpen,
onClose,
state,
legacyVesselsAll,
legacyVesselsFiltered,
@ -94,10 +97,30 @@ export function DashboardSidebar({
setUniqueSorted, setSortedIfChanged, toggleHighlightedMmsi,
} = state;
useEffect(() => {
if (!isOpen) return;
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [isOpen, onClose]);
return (
<div className="flex flex-col overflow-y-auto border-r border-wing-border bg-wing-surface max-md:hidden">
<div className="sb">
<div className="sb-t"> </div>
<>
{isOpen && (
<div className="fixed inset-0 z-[1100] bg-black/50 md:hidden" onClick={onClose} aria-hidden />
)}
<div
className={`
fixed inset-y-0 left-0 z-[1200] w-[310px] max-w-[100vw] transform overflow-y-auto
border-r border-wing-border bg-wing-surface transition-transform duration-200
${isOpen ? 'translate-x-0' : '-translate-x-full'}
md:static md:z-auto md:translate-x-0 md:transition-none
`}
>
<div className="h-[44px] md:hidden" />
<Section title="업종 필터">
<div className="flex flex-wrap gap-0.75 mb-1.5">
<ToggleButton
on={showTargets}
@ -128,43 +151,42 @@ export function DashboardSidebar({
const allOn = VESSEL_TYPE_ORDER.every((c) => typeEnabled[c]);
const nextVal = !allOn;
if (!nextVal && selectedLegacyVessel) setSelectedMmsi(null);
setTypeEnabled({
PT: nextVal, 'PT-S': nextVal, GN: nextVal, OT: nextVal, PS: nextVal, FC: nextVal,
});
setTypeEnabled({ PT: nextVal, 'PT-S': nextVal, GN: nextVal, OT: nextVal, PS: nextVal, FC: nextVal });
}}
/>
</div>
</Section>
<div className="sb">
<div className="sb-t" style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ flex: 1 }} />
<Section
title="지도 표시 설정"
actions={
<ToggleButton
on={projection === 'globe'}
onClick={isProjectionToggleDisabled ? undefined : () => setProjection((p) => (p === 'globe' ? 'mercator' : 'globe'))}
title={isProjectionToggleDisabled ? '3D 모드 준비 중...' : '3D 지구본 투영: 드래그로 회전, 휠로 확대/축소'}
title={isProjectionToggleDisabled ? '3D 모드 준비 중...' : '3D 지구본 투영'}
className="px-2 py-0.5 text-[9px]"
style={{ opacity: isProjectionToggleDisabled ? 0.4 : 1, cursor: isProjectionToggleDisabled ? 'not-allowed' : 'pointer' }}
>
3D
</ToggleButton>
</div>
}
>
<MapToggles value={overlays} onToggle={(k) => setOverlays((prev) => ({ ...prev, [k]: !prev[k] }))} />
</div>
</Section>
<div className="sb">
<div className="sb-t"> </div>
<Section title="속도 프로파일" defaultOpen={false}>
<SpeedProfilePanel selectedType={speedPanelType} />
</div>
</Section>
<div className="sb" style={{ maxHeight: 260, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div className="sb-t sb-t-row">
<div>
<Section
title={
<>
{' '}
<span style={{ color: 'var(--accent)', fontSize: 8 }}>
<span className="text-[8px] font-normal normal-case tracking-normal text-wing-accent">
{selectedLegacyVessel ? `${selectedLegacyVessel.permitNo} 연관` : '(선박 클릭 시 표시)'}
</span>
</div>
</>
}
actions={
<div className="relation-sort">
<label className="relation-sort__option">
<input type="radio" name="fleet-relation-sort" checked={fleetRelationSortMode === 'count'} onChange={() => setFleetRelationSortMode('count')} />
@ -175,8 +197,9 @@ export function DashboardSidebar({
</label>
</div>
</div>
<div style={{ overflowY: 'auto', minHeight: 0 }}>
}
className="max-h-[260px] flex flex-col overflow-hidden [&>div:last-child]:overflow-y-auto [&>div:last-child]:min-h-0"
>
<RelationsPanel
selectedVessel={selectedLegacyVessel}
vessels={legacyVesselsAll}
@ -200,16 +223,17 @@ export function DashboardSidebar({
hoveredFleetMmsiSet={hoveredFleetMmsiSet}
onContextMenuFleet={onFleetContextMenu}
/>
</div>
</div>
</Section>
<div className="sb" style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<div className="sb-t">
<Section
title={
<>
{' '}
<span style={{ color: 'var(--accent)', fontSize: 8 }}>
({legacyVesselsFiltered.length})
</span>
</div>
<span className="text-[8px] font-normal normal-case tracking-normal text-wing-accent">({legacyVesselsFiltered.length})</span>
</>
}
className="flex-1 min-h-0 flex flex-col [&>div:last-child]:overflow-y-auto [&>div:last-child]:min-h-0 [&>div:last-child]:flex-1"
>
<VesselList
vessels={legacyVesselsFiltered}
selectedMmsi={selectedMmsi}
@ -219,35 +243,28 @@ export function DashboardSidebar({
onHoverMmsi={(mmsi) => setHoveredMmsiSet([mmsi])}
onClearHover={() => setHoveredMmsiSet([])}
/>
</div>
</Section>
<div className="sb" style={{ maxHeight: 130, display: 'flex', flexDirection: 'column', overflow: 'visible' }}>
<div className="sb-t sb-t-row" style={{ marginBottom: 6 }}>
<div>
<Section
title={
<>
{' '}
<span style={{ color: 'var(--accent)', fontSize: 8 }}>
({filteredAlarms.length}/{alarms.length})
</span>
</div>
{LEGACY_ALARM_KINDS.length <= 3 ? (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<span className="text-[8px] font-normal normal-case tracking-normal text-wing-accent">({filteredAlarms.length}/{alarms.length})</span>
</>
}
actions={
LEGACY_ALARM_KINDS.length <= 3 ? (
<div className="flex gap-1.5 items-center">
{LEGACY_ALARM_KINDS.map((k) => (
<label key={k} style={{ display: 'inline-flex', gap: 4, alignItems: 'center', cursor: 'pointer', userSelect: 'none' }}>
<input
type="checkbox"
checked={!!alarmKindEnabled[k]}
onChange={() => setAlarmKindEnabled((prev) => ({ ...prev, [k]: !prev[k] }))}
/>
<span style={{ fontSize: 8, color: 'var(--muted)', whiteSpace: 'nowrap' }}>{LEGACY_ALARM_KIND_LABEL[k]}</span>
<label key={k} className="inline-flex gap-1 items-center cursor-pointer select-none">
<input type="checkbox" checked={!!alarmKindEnabled[k]} onChange={() => setAlarmKindEnabled((prev) => ({ ...prev, [k]: !prev[k] }))} />
<span className="text-[8px] text-wing-muted whitespace-nowrap">{LEGACY_ALARM_KIND_LABEL[k]}</span>
</label>
))}
</div>
) : (
<details className="alarm-filter">
<summary className="alarm-filter__summary" title="경고 종류 필터">
{alarmFilterSummary}
</summary>
<summary className="alarm-filter__summary" title="경고 종류 필터">{alarmFilterSummary}</summary>
<div className="alarm-filter__menu" role="menu" aria-label="alarm kind filter">
<label className="alarm-filter__row">
<input
@ -256,7 +273,7 @@ export function DashboardSidebar({
onChange={() =>
setAlarmKindEnabled((prev) => {
const allOn = LEGACY_ALARM_KINDS.every((k) => prev[k]);
const nextVal = allOn ? false : true;
const nextVal = !allOn;
return Object.fromEntries(LEGACY_ALARM_KINDS.map((k) => [k, nextVal])) as Record<LegacyAlarmKind, boolean>;
})
}
@ -267,29 +284,23 @@ export function DashboardSidebar({
<div className="alarm-filter__sep" />
{LEGACY_ALARM_KINDS.map((k) => (
<label key={k} className="alarm-filter__row">
<input
type="checkbox"
checked={!!alarmKindEnabled[k]}
onChange={() => setAlarmKindEnabled((prev) => ({ ...prev, [k]: !prev[k] }))}
/>
<input type="checkbox" checked={!!alarmKindEnabled[k]} onChange={() => setAlarmKindEnabled((prev) => ({ ...prev, [k]: !prev[k] }))} />
{LEGACY_ALARM_KIND_LABEL[k]}
<span className="alarm-filter__cnt">{alarmKindCounts[k] ?? 0}</span>
</label>
))}
</div>
</details>
)}
</div>
<div style={{ overflowY: 'auto', minHeight: 0, flex: 1 }}>
)
}
className="max-h-[130px] flex flex-col overflow-visible [&>div:last-child]:overflow-y-auto [&>div:last-child]:min-h-0 [&>div:last-child]:flex-1"
>
<AlarmsPanel alarms={filteredAlarms} onSelectMmsi={setSelectedMmsi} />
</div>
</div>
</Section>
{adminMode ? (
<>
<div className="sb">
<div className="sb-t">ADMIN · AIS Target Polling</div>
<Section title="ADMIN · AIS Target Polling" defaultOpen={false}>
<div style={{ fontSize: 11, lineHeight: 1.45 }}>
<div style={{ color: 'var(--muted)', fontSize: 10 }}></div>
<div style={{ wordBreak: 'break-all' }}>{AIS_API_BASE}/api/ais-target/search</div>
@ -310,10 +321,9 @@ export function DashboardSidebar({
<div style={{ marginTop: 6, color: 'var(--muted)', fontSize: 10 }}></div>
<div style={{ color: 'var(--text)', fontSize: 10 }}>{snapshot.lastMessage ?? '-'}</div>
</div>
</div>
</Section>
<div className="sb">
<div className="sb-t">ADMIN · Legacy (CN Permit)</div>
<Section title="ADMIN · Legacy (CN Permit)" defaultOpen={false}>
{legacyError ? (
<div style={{ fontSize: 11, color: '#EF4444' }}>legacy load error: {legacyError}</div>
) : (
@ -329,10 +339,9 @@ export function DashboardSidebar({
<div style={{ fontSize: 10, color: 'var(--text)' }}>{legacyData?.generatedAt ? fmtIsoFull(legacyData.generatedAt) : 'loading...'}</div>
</div>
)}
</div>
</Section>
<div className="sb">
<div className="sb-t">ADMIN · Viewport / BBox</div>
<Section title="ADMIN · Viewport / BBox" defaultOpen={false}>
<div style={{ fontSize: 11, lineHeight: 1.45 }}>
<div style={{ color: 'var(--muted)', fontSize: 10 }}> View BBox</div>
<div style={{ wordBreak: 'break-all', fontSize: 10 }}>{fmtBbox(viewBbox)}</div>
@ -349,7 +358,6 @@ export function DashboardSidebar({
>
Viewport filter {useViewportFilter ? 'ON' : 'OFF'}
</button>
<button
onClick={() => {
if (!viewBbox) return;
@ -367,24 +375,17 @@ export function DashboardSidebar({
color: viewBbox ? 'var(--text)' : 'var(--muted)',
cursor: viewBbox ? 'pointer' : 'not-allowed',
}}
title="서버에서 bbox로 필터링해서 내려받기(페이로드 감소). 켜는 순간 현재 view bbox로 고정됨."
title="서버에서 bbox로 필터링해서 내려받기"
disabled={!viewBbox}
>
API bbox {useApiBbox ? 'ON' : 'OFF'}
</button>
<button
onClick={() => {
if (!viewBbox) return;
setApiBbox(fmtBbox(viewBbox));
setUseApiBbox(true);
}}
onClick={() => { if (!viewBbox) return; setApiBbox(fmtBbox(viewBbox)); setUseApiBbox(true); }}
style={{
fontSize: 10, padding: '4px 8px', borderRadius: 6,
border: '1px solid var(--border)',
background: 'var(--card)',
color: viewBbox ? 'var(--text)' : 'var(--muted)',
cursor: viewBbox ? 'pointer' : 'not-allowed',
border: '1px solid var(--border)', background: 'var(--card)',
color: viewBbox ? 'var(--text)' : 'var(--muted)', cursor: viewBbox ? 'pointer' : 'not-allowed',
}}
disabled={!viewBbox}
title="현재 view bbox로 API bbox를 갱신"
@ -397,36 +398,31 @@ export function DashboardSidebar({
<b style={{ color: 'var(--text)' }}>{snapshot.total}</b>
</div>
</div>
</div>
</Section>
<div className="sb">
<div className="sb-t">ADMIN · Map (Extras)</div>
<Section title="ADMIN · Map (Extras)" defaultOpen={false}>
<Map3DSettingsToggles value={settings} onToggle={(k) => setSettings((prev) => ({ ...prev, [k]: !prev[k] }))} />
<div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 6 }}> WebGL 컨텍스트: MapboxOverlay(interleaved)</div>
</div>
</Section>
<div className="sb" style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<div className="sb-t">ADMIN · AIS Targets (All)</div>
<AisTargetList
targets={targetsInScope}
selectedMmsi={selectedMmsi}
onSelectMmsi={setSelectedMmsi}
legacyIndex={legacyIndex}
/>
</div>
<Section
title="ADMIN · AIS Targets (All)"
defaultOpen={false}
className="flex-1 min-h-0 flex flex-col [&>div:last-child]:overflow-y-auto [&>div:last-child]:min-h-0 [&>div:last-child]:flex-1"
>
<AisTargetList targets={targetsInScope} selectedMmsi={selectedMmsi} onSelectMmsi={setSelectedMmsi} legacyIndex={legacyIndex} />
</Section>
<div className="sb" style={{ maxHeight: 130, overflowY: 'auto' }}>
<div className="sb-t">ADMIN · </div>
<Section title="ADMIN · 수역 데이터" defaultOpen={false} className="max-h-[130px] overflow-y-auto">
{zonesError ? (
<div style={{ fontSize: 11, color: '#EF4444' }}>zones load error: {zonesError}</div>
) : (
<div style={{ fontSize: 11, color: 'var(--muted)' }}>
{zones ? `loaded (${zones.features.length} features)` : 'loading...'}
</div>
<div style={{ fontSize: 11, color: 'var(--muted)' }}>{zones ? `loaded (${zones.features.length} features)` : 'loading...'}</div>
)}
</div>
</Section>
</>
) : null}
</div>
</>
);
}

파일 보기

@ -1,10 +1,23 @@
import { useState } from 'react';
import { ZONE_IDS, ZONE_META } from "../../entities/zone/model/meta";
import { LEGACY_CODE_COLORS_HEX, OTHER_AIS_SPEED_HEX, OVERLAY_RGB, rgbToHex, rgba } from "../../shared/lib/map/palette";
export function MapLegend() {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="map-legend">
<div className="lt"></div>
<button
className="flex w-full cursor-pointer items-center justify-between border-none bg-transparent p-0 text-left"
onClick={() => setIsOpen((v) => !v)}
>
<span className="lt" style={{ marginBottom: 0 }}></span>
<span className="text-[9px] text-wing-muted">{isOpen ? '▾' : '▸'}</span>
</button>
{isOpen && (
<>
<div className="lt" style={{ marginTop: 6 }}></div>
{ZONE_IDS.map((z) => (
<div key={z} className="li">
<div className="ls" style={{ background: `${ZONE_META[z].color}33`, border: `1px solid ${ZONE_META[z].color}` }} />
@ -12,9 +25,7 @@ export function MapLegend() {
</div>
))}
<div className="lt" style={{ marginTop: 8 }}>
AIS ()
</div>
<div className="lt" style={{ marginTop: 8 }}> AIS ()</div>
<div className="li">
<div className="ls" style={{ background: OTHER_AIS_SPEED_HEX.fast, borderRadius: 999 }} />
SOG 10 kt
@ -32,12 +43,10 @@ export function MapLegend() {
SOG unknown
</div>
<div className="lt" style={{ marginTop: 8 }}>
CN Permit()
</div>
<div className="lt" style={{ marginTop: 8 }}>CN Permit()</div>
<div className="li">
<div className="ls" style={{ background: LEGACY_CODE_COLORS_HEX.PT, borderRadius: 999 }} />
PT (ring + )
PT
</div>
<div className="li">
<div className="ls" style={{ background: LEGACY_CODE_COLORS_HEX["PT-S"], borderRadius: 999 }} />
@ -64,16 +73,12 @@ export function MapLegend() {
C21
</div>
<div className="lt" style={{ marginTop: 8 }}>
(3D)
</div>
<div className="lt" style={{ marginTop: 8 }}>(3D)</div>
<div className="li" style={{ color: "var(--muted)" }}>
Hexagon: 화면 AIS
</div>
<div className="lt" style={{ marginTop: 8 }}>
</div>
<div className="lt" style={{ marginTop: 8 }}></div>
<div className="li">
<div style={{ width: 20, height: 2, background: rgba(OVERLAY_RGB.pairNormal, 0.35), borderRadius: 1 }} />
PTPT-S ()
@ -109,6 +114,8 @@ export function MapLegend() {
/>
(15)
</div>
</>
)}
</div>
);
}

파일 보기

@ -13,27 +13,49 @@ type Props = {
onLogout?: () => void;
theme?: "dark" | "light";
onToggleTheme?: () => void;
isSidebarOpen?: boolean;
onMenuToggle?: () => void;
};
export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout, theme, onToggleTheme }: Props) {
export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStatus, lastFetchMinutes, clock, adminMode, onLogoClick, userName, onLogout, theme, onToggleTheme, isSidebarOpen, onMenuToggle }: Props) {
const statusColor =
pollingStatus === "ready" ? "#22C55E" : pollingStatus === "loading" ? "#F59E0B" : pollingStatus === "error" ? "#EF4444" : "var(--muted)";
return (
<div className="col-span-full flex items-center gap-2.5 border-b border-wing-border bg-wing-surface px-3.5 z-[1000]">
{onMenuToggle && (
<button
className="flex cursor-pointer items-center justify-center rounded border border-wing-border bg-transparent p-1 text-wing-muted transition-colors hover:border-wing-accent hover:text-wing-text md:hidden"
onClick={onMenuToggle}
aria-label={isSidebarOpen ? "메뉴 닫기" : "메뉴 열기"}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
{isSidebarOpen ? (
<>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</>
) : (
<>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</>
)}
</svg>
</button>
)}
<div
className="flex items-center gap-1.5 whitespace-nowrap text-sm font-extrabold"
onClick={onLogoClick}
style={{ cursor: onLogoClick ? "pointer" : undefined }}
title={adminMode ? "ADMIN" : undefined}
>
🛰 <span className="text-wing-accent">WING</span> ·{" "}
<span className="text-wing-accent">WING</span>
<span className="hidden sm:inline">·</span>
{adminMode ? <span className="text-[10px] text-wing-warning">(ADMIN)</span> : null}
</div>
<div className="ml-auto flex flex-wrap justify-end gap-3.5">
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
DATA <b style={{ color: "#22C55E" }}>API</b>
</div>
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
<div className="hidden items-center gap-1 text-[10px] text-wing-muted sm:flex">
POLL{" "}
<b style={{ color: statusColor }}>
{pollingStatus.toUpperCase()}
@ -41,15 +63,15 @@ export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStat
</b>
</div>
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
<b className="text-xs text-wing-text">{total}</b>
<b className="text-xs text-wing-text">{total}</b>
</div>
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
<div className="hidden items-center gap-1 text-[10px] text-wing-muted lg:flex">
<b style={{ color: "#22C55E" }}>{fishing}</b>
</div>
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
<div className="hidden items-center gap-1 text-[10px] text-wing-muted lg:flex">
<b style={{ color: "#3B82F6" }}>{transit}</b>
</div>
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
<div className="hidden items-center gap-1 text-[10px] text-wing-muted md:flex">
<b style={{ color: "#F59E0B" }}>{pairLinks}</b>
</div>
<div className="flex items-center gap-1 text-[10px] text-wing-muted">
@ -67,7 +89,7 @@ export function Topbar({ total, fishing, transit, pairLinks, alarms, pollingStat
</button>
)}
{userName && (
<div className="ml-2.5 flex shrink-0 items-center gap-2">
<div className="ml-2.5 hidden shrink-0 items-center gap-2 sm:flex">
<span className="whitespace-nowrap text-[10px] font-medium text-wing-text">{userName}</span>
{onLogout && (
<button