Sidebar improvements (roadmap #10)

- sidebarCollapsed now persisted to localStorage — remembered across sessions
- Explicit ‹/› toggle button always visible in the header; no longer
  requires knowing to click the WRYSTR wordmark
  - Expanded: WRYSTR on left, ‹ collapse button on right
  - Collapsed: centered › expand button fills the header
- Write article (✦) now visible in collapsed mode as icon-only with
  tooltip, consistent with the rest of the nav
- Nav items have title tooltip in collapsed mode for discoverability
- Status footer: collapsed shows just the connection dot with tooltip;
  expanded shows dot + online/offline text + note count as before

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-10 19:41:16 +01:00
parent faaf8ac4a4
commit 288abdc180
2 changed files with 83 additions and 53 deletions

View File

@@ -18,32 +18,47 @@ export function Sidebar() {
const { connected, notes } = useFeedStore(); const { connected, notes } = useFeedStore();
const { loggedIn } = useUserStore(); const { loggedIn } = useUserStore();
const c = sidebarCollapsed;
return ( return (
<>
<aside <aside
className={`h-full border-r border-border bg-bg flex flex-col transition-all duration-150 ${ className={`h-full border-r border-border bg-bg flex flex-col transition-all duration-150 shrink-0 ${
sidebarCollapsed ? "w-12" : "w-48" c ? "w-12" : "w-48"
}`} }`}
> >
{/* Logo */} {/* Header / logo */}
<div className="border-b border-border px-3 py-2.5 flex items-center justify-between shrink-0"> <div className="border-b border-border px-2 py-2.5 flex items-center justify-between shrink-0">
{c ? (
/* Collapsed: just the expand chevron, centred */
<button <button
onClick={toggleSidebar} onClick={toggleSidebar}
className="text-text hover:text-accent transition-colors" title="Expand sidebar"
className="w-full flex items-center justify-center text-text-dim hover:text-accent transition-colors"
> >
{sidebarCollapsed ? ( <span className="text-[13px]"></span>
<span className="text-sm font-bold">W</span>
) : (
<span className="text-sm font-bold tracking-widest">WRYSTR</span>
)}
</button> </button>
) : (
/* Expanded: brand on left, collapse chevron on right */
<>
<span className="text-sm font-bold tracking-widest text-text select-none">WRYSTR</span>
<button
onClick={toggleSidebar}
title="Collapse sidebar"
className="text-text-dim hover:text-accent transition-colors px-1"
>
<span className="text-[13px]"></span>
</button>
</>
)}
</div> </div>
{/* Nav */} {/* Nav */}
<nav className="flex-1 overflow-y-auto py-2"> <nav className="flex-1 overflow-y-auto py-2">
{loggedIn && !!getNDK().signer && !sidebarCollapsed && ( {/* Write article — show icon even when collapsed */}
{loggedIn && !!getNDK().signer && (
<button <button
onClick={() => setView("article-editor")} onClick={() => setView("article-editor")}
title="Write article"
className={`w-full text-left px-3 py-1.5 flex items-center gap-2 text-[12px] transition-colors mb-1 ${ className={`w-full text-left px-3 py-1.5 flex items-center gap-2 text-[12px] transition-colors mb-1 ${
currentView === "article-editor" currentView === "article-editor"
? "text-accent bg-accent/8" ? "text-accent bg-accent/8"
@@ -51,13 +66,15 @@ export function Sidebar() {
}`} }`}
> >
<span className="w-4 text-center text-[14px]"></span> <span className="w-4 text-center text-[14px]"></span>
<span>write article</span> {!c && <span>write article</span>}
</button> </button>
)} )}
{NAV_ITEMS.map((item) => ( {NAV_ITEMS.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
title={c ? item.label : undefined}
className={`w-full text-left px-3 py-1.5 flex items-center gap-2 text-[12px] transition-colors ${ className={`w-full text-left px-3 py-1.5 flex items-center gap-2 text-[12px] transition-colors ${
currentView === item.id currentView === item.id
? "text-accent bg-accent/8" ? "text-accent bg-accent/8"
@@ -65,17 +82,25 @@ export function Sidebar() {
}`} }`}
> >
<span className="w-4 text-center text-[14px]">{item.icon}</span> <span className="w-4 text-center text-[14px]">{item.icon}</span>
{!sidebarCollapsed && <span>{item.label}</span>} {!c && <span>{item.label}</span>}
</button> </button>
))} ))}
</nav> </nav>
{/* Account switcher */} {/* Account switcher (full) — expanded only */}
{!sidebarCollapsed && <AccountSwitcher />} {!c && <AccountSwitcher />}
{/* Status footer */} {/* Footer — connection status */}
{!sidebarCollapsed && ( <div className={`border-t border-border shrink-0 ${c ? "py-2 flex justify-center" : "px-3 py-2"}`}>
<div className="border-t border-border px-3 py-2 text-[10px] text-text-dim shrink-0"> {c ? (
/* Collapsed: single dot */
<span
title={connected ? "Online" : "Offline"}
className={`w-2 h-2 rounded-full inline-block ${connected ? "bg-success" : "bg-danger"}`}
/>
) : (
/* Expanded: dot + label + note count */
<div className="text-[10px] text-text-dim">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-success" : "bg-danger"}`} /> <span className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-success" : "bg-danger"}`} />
<span>{connected ? "online" : "offline"}</span> <span>{connected ? "online" : "offline"}</span>
@@ -83,8 +108,7 @@ export function Sidebar() {
<div className="mt-0.5">{notes.length} notes</div> <div className="mt-0.5">{notes.length} notes</div>
</div> </div>
)} )}
</div>
</aside> </aside>
</>
); );
} }

View File

@@ -19,9 +19,11 @@ interface UIState {
toggleSidebar: () => void; toggleSidebar: () => void;
} }
const SIDEBAR_KEY = "wrystr_sidebar_collapsed";
export const useUIStore = create<UIState>((set, _get) => ({ export const useUIStore = create<UIState>((set, _get) => ({
currentView: "feed", currentView: "feed",
sidebarCollapsed: false, sidebarCollapsed: localStorage.getItem(SIDEBAR_KEY) === "true",
selectedPubkey: null, selectedPubkey: null,
selectedNote: null, selectedNote: null,
previousView: "feed", previousView: "feed",
@@ -34,5 +36,9 @@ export const useUIStore = create<UIState>((set, _get) => ({
currentView: s.previousView !== s.currentView ? s.previousView : "feed", currentView: s.previousView !== s.currentView ? s.previousView : "feed",
selectedNote: null, selectedNote: null,
})), })),
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), toggleSidebar: () => set((s) => {
const next = !s.sidebarCollapsed;
localStorage.setItem(SIDEBAR_KEY, String(next));
return { sidebarCollapsed: next };
}),
})); }));