|
| 1 | +--- |
| 2 | +name: Collapsible Sidebar Implementation |
| 3 | +overview: Implement a collapsible sidebar with icon-only collapsed state, search icon that opens the search popup directly, Ctrl+K opening popup when collapsed, tooltips for icons, and hide theme switcher in collapsed mode. |
| 4 | +todos: [] |
| 5 | +isProject: false |
| 6 | +--- |
| 7 | + |
| 8 | +# Collapsible Sidebar Implementation |
| 9 | + |
| 10 | +## Current Architecture |
| 11 | + |
| 12 | +- **Sidebar**: [components/common/sidebar.templ](components/common/sidebar.templ) - static 14rem width, contains SearchBar, nav links (icon + text), SidebarThemeSwitcher, SidebarProfile |
| 13 | +- **Search flow**: Typing in `#search` input updates `window.searchState` and hash `#search?q=...`. Base layout's `toggleSearchView()` shows search overlay when hash is `#search?q=` (even empty) |
| 14 | +- **Ctrl+K**: Sidebar handler (lines 569-582) focuses `#sidebar #search` on non-homepage; index.templ focuses homepage search |
| 15 | +- **SidebarProfile**: React component mounted in `#sidebar-profile-container`, shows user/sign-in with avatar + text |
| 16 | + |
| 17 | +## Implementation Plan |
| 18 | + |
| 19 | +### 1. State and Toggle Button |
| 20 | + |
| 21 | +- Add `sidebar-collapsed` class to `#sidebar` when collapsed; persist state in `localStorage` (e.g. `fdt-sidebar-collapsed`) |
| 22 | +- Add collapse/expand toggle button (chevron or panel icon) - positioned after logo in expanded mode, visible in both modes |
| 23 | +- Collapsed width: ~4rem (64px) for icon-only layout; expanded: 14rem (unchanged) |
| 24 | +- Animate width transition via CSS |
| 25 | + |
| 26 | +### 2. Collapsed Layout Structure |
| 27 | + |
| 28 | +**Logo section**: Show only logo image (32px), hide title/subtitle. Wrap in same homepage link. Toggle button next to it. |
| 29 | + |
| 30 | +**Search section**: |
| 31 | + |
| 32 | +- Expanded: Keep current SearchBar (input visible) |
| 33 | +- Collapsed: Replace with search icon button. On click: set `window.location.hash = '#search?q='`, `window.searchState.setQuery('')` to open search overlay directly (no focus on hidden input) |
| 34 | + |
| 35 | +**Nav section**: In collapsed mode, hide all `<span>` text nodes; show only icons. Each nav item becomes icon-only with `title` attribute for native tooltip (or use a small tooltip lib if needed - native `title` is simplest). Preserve existing `href` and click behavior. |
| 36 | + |
| 37 | +**Theme section**: Hide `#sidebar-theme-switcher-section` when collapsed (CSS or JS based on `sidebar-collapsed`) |
| 38 | + |
| 39 | +**Profile section**: SidebarProfile needs collapsed variant: |
| 40 | + |
| 41 | +- Option A: Pass `collapsed` prop via data attribute from templ, SidebarProfile reads it and renders icon-only (avatar or sign-in icon) |
| 42 | +- Option B: In collapsed mode, hide profile container and show minimal icon - requires coordination between templ (hiding container) and possibly a separate icon |
| 43 | +- Simplest: In collapsed mode, hide `#sidebar-profile-container` entirely, or render a compact icon that links to /pro or triggers sign-in. Recommend: Show avatar/sign-in icon only with tooltip "Sign In" or user name. |
| 44 | + |
| 45 | +### 3. Ctrl+K Behavior |
| 46 | + |
| 47 | +Modify the Ctrl+K handler in [sidebar.templ](components/common/sidebar.templ) (lines 569-582): |
| 48 | + |
| 49 | +```javascript |
| 50 | +// When collapsed: open search popup directly (set hash + searchState) |
| 51 | +// When expanded: focus sidebar #search (current behavior) |
| 52 | +``` |
| 53 | + |
| 54 | +Check for `#sidebar.sidebar-collapsed` - if true, call `openSearchPopup()` instead of focusing input. The search_bar.templ also has a global Ctrl+K - we need a single source of truth. Recommendation: Centralize Ctrl+K in base_layout or have sidebar handler take precedence when sidebar is visible and collapsed. |
| 55 | + |
| 56 | +**Order of handlers**: index.templ (homepage) has its own handler. For non-homepage: sidebar.templ handler runs. Update sidebar handler to: |
| 57 | + |
| 58 | +- If homepage: do nothing (index handles it) |
| 59 | +- If non-homepage + sidebar collapsed: `e.preventDefault()`, set hash `#search?q=`, `searchState.setQuery('')` |
| 60 | +- If non-homepage + sidebar expanded: focus `#sidebar #search` |
| 61 | + |
| 62 | +The search_bar.templ attaches to `document` - so both fire. We need the sidebar handler to run first and `preventDefault` + return when collapsed, so search_bar doesn't try to focus (and fail since input may be hidden). Use `stopImmediatePropagation` or check collapsed state in a unified handler. |
| 63 | + |
| 64 | +### 4. Tooltips |
| 65 | + |
| 66 | +Add `title` attribute to each nav link with the text content: |
| 67 | + |
| 68 | +- "VS Code extension", "My Bookmarks", "Pro", "Tools", "Installerpedia", "Emojis", "SVG Icons", "PNG Icons", "MCP", "TLDR", "Man Pages", "Cheatsheets" |
| 69 | + |
| 70 | +For collapsed search icon button: `title="Search (Ctrl+K)"` |
| 71 | + |
| 72 | +### 5. Files to Modify |
| 73 | + |
| 74 | + |
| 75 | +| File | Changes | |
| 76 | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |
| 77 | +| [sidebar.templ](components/common/sidebar.templ) | Add collapse state, toggle button, collapsed layout (hide text/spans, replace search with icon), CSS for collapsed width, update hideSidebarSearch for collapsed, Ctrl+K logic | |
| 78 | +| [SidebarProfile.tsx](frontend/components/common/SidebarProfile.tsx) | Accept collapsed prop (via container data attribute or React context) and render icon-only variant with tooltip | |
| 79 | +| [search_bar.templ](components/common/search_bar.templ) | May need to support "collapsed sidebar" mode - or sidebar uses a different structure when collapsed and doesn't include SearchBar, instead has custom icon button | |
| 80 | + |
| 81 | + |
| 82 | +### 6. SearchBar in Collapsed Mode |
| 83 | + |
| 84 | +The SearchBar is one component. For collapsed sidebar we have two options: |
| 85 | + |
| 86 | +**A)** Keep SearchBar in DOM but hide `#desktop-search-container` and show a new `#sidebar-search-icon-btn` when collapsed. The templ would conditionally render both; CSS/JS shows one based on collapsed state. |
| 87 | + |
| 88 | +**B)** Create a `SidebarSearch` component or block in sidebar.templ that: |
| 89 | + |
| 90 | +- Expanded: renders `@SearchBar()` |
| 91 | +- Collapsed: renders icon button that opens popup |
| 92 | + |
| 93 | +Since SearchBar is shared and has complex logic, option A is cleaner: add a sibling icon button in `#sidebar-search-section`, hide it when expanded and hide SearchBar when collapsed. Wire the icon click to open popup. |
| 94 | + |
| 95 | +### 7. CSS Structure |
| 96 | + |
| 97 | +```css |
| 98 | +#sidebar { transition: width 0.2s; } |
| 99 | +#sidebar.sidebar-collapsed { width: 4rem !important; } |
| 100 | +#sidebar.sidebar-collapsed #sidebar-logo-text-container, |
| 101 | +#sidebar.sidebar-collapsed .nav-link-text span, |
| 102 | +#sidebar.sidebar-collapsed #sidebar-theme-switcher-section, |
| 103 | +#sidebar.sidebar-collapsed #desktop-search-container { display: none !important; } |
| 104 | +#sidebar.sidebar-collapsed #sidebar-search-icon-btn { display: flex !important; } |
| 105 | +``` |
| 106 | + |
| 107 | +### 8. SidebarProfile Collapsed |
| 108 | + |
| 109 | +SidebarProfile is rendered by `window.renderTool('sidebarProfile', 'sidebar-profile-container')`. The parent div can have `data-collapsed="true"` when collapsed. SidebarProfile reads `document.getElementById('sidebar-profile-container')?.closest('#sidebar')?.classList.contains('sidebar-collapsed')` on mount and when receiving a custom event `sidebar-collapsed-changed`. Or simpler: pass nothing and have SidebarProfile check `#sidebar.sidebar-collapsed` in a `useEffect` + `ResizeObserver`/`MutationObserver` - overkill. |
| 110 | + |
| 111 | +Simpler: Dispatch `sidebar-collapsed-changed` when toggle is clicked; SidebarProfile listens and re-renders. Or use a data attribute on the container that the parent updates - React would need to re-mount or receive new props. Easiest: Put a class on `#sidebar-profile-container` when collapsed, and use CSS to hide text and show icon-only. But SidebarProfile is React - its DOM is managed by React. So we need React to know about collapsed state. |
| 112 | + |
| 113 | +**Approach**: Add `window.sidebarCollapsed = true/false` or dispatch `sidebar-collapsed-changed` with `detail: { collapsed: true }`. SidebarProfile subscribes and sets local state to re-render icon-only layout. |
| 114 | + |
| 115 | +### 9. Edge Cases |
| 116 | + |
| 117 | +- **Mobile**: Current responsive layout hides sidebar by default, shows as overlay. Collapse/expand is desktop-only - on mobile keep current overlay behavior. Add `@media (min-width: 1024px)` guard so collapse UI only appears on desktop. |
| 118 | +- **Homepage**: Sidebar search is already hidden on homepage (`hideSidebarSearch`). In collapsed mode, the search icon should still show and open popup when clicked. |
| 119 | +- **Hash changes**: When user opens search via collapsed icon, hash becomes `#search?q=`, which triggers `toggleSearchView` - good. |
| 120 | + |
| 121 | +## Diagram |
| 122 | + |
| 123 | +```mermaid |
| 124 | +flowchart TD |
| 125 | + subgraph Expanded [Expanded Sidebar] |
| 126 | + E1[Logo + Text] |
| 127 | + E2[Search Input] |
| 128 | + E3[Nav: Icon + Text] |
| 129 | + E4[Theme Switcher] |
| 130 | + E5[Profile Full] |
| 131 | + end |
| 132 | + |
| 133 | + subgraph Collapsed [Collapsed Sidebar] |
| 134 | + C1[Logo only] |
| 135 | + C2[Search Icon] |
| 136 | + C3[Nav: Icon only + tooltip] |
| 137 | + C4[Hidden] |
| 138 | + C5[Profile Icon] |
| 139 | + end |
| 140 | + |
| 141 | + Toggle[Toggle Button] --> Expanded |
| 142 | + Toggle --> Collapsed |
| 143 | +``` |
| 144 | + |
| 145 | + |
| 146 | + |
0 commit comments