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.
- New ShareButton in the page header tries navigator.share() first
(native sheet on mobile), then falls back to navigator.clipboard, then
to a hidden textarea + execCommand("copy") for non-secure-context LAN.
Shows a brief "링크 복사됨" / "복사 안됨" toast; native share success
shows none (the OS sheet is the feedback).
- Mounted between ⇄ 비교 and the Star button.
Fixes reviewer concern on 442cc38: writeNote() used to return null for
both "user emptied the textarea (intentional delete)" and "localStorage
quota failure," so clearing a note showed "저장 안됨." It now returns a
discriminated { status: "saved" | "deleted" | "failed" } and the panel
shows "저장됨" / "삭제됨" / "저장 안됨" accordingly. Failure keeps the
user's text so they can retry or shorten it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New lib/notes.ts: localStorage key `notes:<code>` storing { text, updatedAt },
2000-char cap, blank/whitespace removes the key.
- New InvestmentNote sidebar panel: textarea + character counter, save button
enabled only when dirty, "방금 전 / N분 전 / 오늘 HH:MM / YYYY-MM-DD"
relative timestamp on the last save, short save-confirmation toast.
- Mounted in [code]/page.tsx sidebar after AlertsPanel.
Also tightens writeTargets() to require strictly positive finite values,
matching the UI's parseNum() guard so the lib is safe under direct external
calls (reviewer's non-blocking note on a4a4a7f).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>