feat(chart): split toolbar into indicators row + actions row

When showSma is true, the indicator chips (MA5/20/60 + RSI + MACD) and the
action buttons (수평선 / CSV / 전체화면, plus the fullscreen symbol label) were
sharing one flex-wrap row. On narrow widths the actions wrapped into the
middle of the indicators because `ml-auto` only enforces right-pull while
everything fits on one line — once wrap kicks in the right-side group loses
its anchor.

Split into two rows so wrap in either group stays local:
- Indicators row: rendered only when showSma. Hidden in 1D (intraday) where
  none of these are meaningful, matching prior behavior.
- Actions row: always rendered. Leading fullscreen label + `ml-auto` to push
  the three action buttons right; on narrow widths they wrap among themselves
  without colliding with indicator chips.
This commit is contained in:
claude-owner
2026-05-29 00:54:04 +09:00
parent a6cd6f3a35
commit 900c56550c

View File

@@ -929,11 +929,80 @@ export function StockChart({ chart, prediction, targets }: Props) {
return (
<div ref={wrapperRef} className={wrapperCls}>
{/*
툴바를 2 그룹으로 분리:
A) 보조지표 (MA5/20/60 + RSI + MACD) — 일봉 계열에서만 의미. 안 보이면 row 자체 생략.
B) 액션 (전체화면 종목 라벨 + 수평선 + CSV + 전체화면 토글) — 항상 표시.
같은 줄에 다 묶었을 때 모바일/좁은 폭에서 indicators 와 actions 가 섞여서 wrap 되며
"ml-auto" 가 줄바꿈 직전에서만 분리 효과를 주고 그 외엔 actions 가 indicators 사이로 끼어듦.
분리하면 indicators wrap 은 indicators 안에서만 일어남 — actions 그룹은 자체 정렬 유지.
*/}
{showSma && (
<div className="mb-1 flex flex-wrap items-center gap-1.5 px-1">
{SMA_PRESETS.map((p) => {
const on = active.has(p.window);
return (
<button
key={p.window}
type="button"
onClick={() => toggle(p.window)}
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
on
? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
: "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
}`}
aria-pressed={on}
>
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: on ? p.color : "transparent", border: `1px solid ${p.color}` }}
/>
{p.label}
</button>
);
})}
<span className="mx-1 text-zinc-700">|</span>
<button
type="button"
onClick={() => setRsiOn((v) => !v)}
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
rsiOn
? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
: "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
}`}
aria-pressed={rsiOn}
title="상대강도지수 (Relative Strength Index, 14일)"
>
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: rsiOn ? "#e879f9" : "transparent", border: "1px solid #e879f9" }}
/>
RSI
</button>
<button
type="button"
onClick={() => setMacdOn((v) => !v)}
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
macdOn
? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
: "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
}`}
aria-pressed={macdOn}
title="MACD (12/26/9) — 추세 전환 신호"
>
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: macdOn ? "#60a5fa" : "transparent", border: "1px solid #60a5fa" }}
/>
MACD
</button>
</div>
)}
<div className="mb-1 flex flex-wrap items-center gap-1.5 px-1">
{/*
전체화면 모드 한정 종목 라벨 — 일반 모드는 위에 PriceHero 가 종목명/코드를 크게 보여주지만
fullscreen 은 fixed inset 으로 PriceHero 가 가려져서 무슨 종목 차트인지 식별이 어려워짐.
툴바 leading 자리에 작은 라벨을 끼워 시각적 노이즈 최소로 보완.
actions row leading 자리에 작은 라벨을 끼워 시각적 노이즈 최소로 보완.
(캔버스 위의 CrosshairLabel 과 위치 겹치지 않도록 캔버스가 아닌 툴바에 둠.)
*/}
{fullscreen && (
@@ -942,67 +1011,6 @@ export function StockChart({ chart, prediction, targets }: Props) {
<span className="font-mono text-[10px] text-zinc-500">{chart.code}</span>
</span>
)}
{showSma && (
<>
{SMA_PRESETS.map((p) => {
const on = active.has(p.window);
return (
<button
key={p.window}
type="button"
onClick={() => toggle(p.window)}
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
on
? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
: "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
}`}
aria-pressed={on}
>
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: on ? p.color : "transparent", border: `1px solid ${p.color}` }}
/>
{p.label}
</button>
);
})}
<span className="mx-1 text-zinc-700">|</span>
<button
type="button"
onClick={() => setRsiOn((v) => !v)}
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
rsiOn
? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
: "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
}`}
aria-pressed={rsiOn}
title="상대강도지수 (Relative Strength Index, 14일)"
>
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: rsiOn ? "#e879f9" : "transparent", border: "1px solid #e879f9" }}
/>
RSI
</button>
<button
type="button"
onClick={() => setMacdOn((v) => !v)}
className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
macdOn
? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
: "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
}`}
aria-pressed={macdOn}
title="MACD (12/26/9) — 추세 전환 신호"
>
<span
className="inline-block h-2 w-2 rounded-full"
style={{ backgroundColor: macdOn ? "#60a5fa" : "transparent", border: "1px solid #60a5fa" }}
/>
MACD
</button>
</>
)}
<button
type="button"
onClick={() => {