Files
stock_chart_site/web/lib/collapsible.ts
claude-owner f9027f5365 feat(sidebar): collapsible AlertsPanel / InvestmentNote / RelatedStocks
Three lower-sidebar panels are now wrapped in a shared Collapsible so
mobile readers can fold away sections they're not actively using.

New shared pieces:
- lib/collapsible.ts — readOpen/writeOpen helpers backed by
  localStorage under 'panel-open:<name>'. Per-panel-name (not
  per-code) because the preference is a panel-level habit; users
  don't want different fold state for every symbol.
- components/Collapsible.tsx — button-as-header + chevron + body.
  aria-expanded for AT. Body uses 'hidden' instead of conditional
  unmount so textarea text, partial alert form input, and
  RelatedStocks fetch results all survive collapse cycles.
  SSR-safe: useState(defaultOpen) then useEffect-sync to avoid React
  hydration mismatch when localStorage diverges from default.

Panel refactors:
- RelatedStocks: outer rounded div + header replaced with Collapsible;
  loading/error/empty/data states all rendered inside body (header
  stays visible so user can still toggle/recognize the section even
  while loading).
- InvestmentNote: same swap. saved.updatedAt drives the subtitle.
- AlertsPanel: same swap. current price drives the subtitle.

Defaults are open=true everywhere; the surface area is the toggle
itself, not a forced collapse. User's first interaction sets the
sticky preference.
2026-06-01 10:26:33 +09:00

30 lines
1.1 KiB
TypeScript

// 사이드바 collapsible 패널의 펼침/접힘 상태 영속 저장.
// 키 형식: `panel-open:<name>` — 종목별이 아닌 panel-name 단위. 사용자는 "관련 종목은 늘
// 접어 둔다" 같은 panel 단위 습관이 있지 종목별로 다르게 설정하지 않음. 모든 종목 페이지에서
// 같은 상태로 보이는 것이 직관적.
//
// 값 형식: '1' = open, '0' = closed. 다른 값은 default 로 폴백.
// 쿼타/잠금 환경에선 silently fail — 그 세션 동안 default 상태 유지.
const STORAGE_PREFIX = "panel-open:";
export function readOpen(key: string, defaultOpen: boolean): boolean {
if (typeof window === "undefined") return defaultOpen;
try {
const v = window.localStorage.getItem(STORAGE_PREFIX + key);
if (v === null) return defaultOpen;
return v === "1";
} catch {
return defaultOpen;
}
}
export function writeOpen(key: string, open: boolean): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(STORAGE_PREFIX + key, open ? "1" : "0");
} catch {
// quota / private mode — 다음 페이지 진입엔 default 로 되돌아감.
}
}