Compare commits

..

2 Commits

Author SHA1 Message Date
hsiegeln
2f1df869db docs: update spec and guide for search position and chevron removal
All checks were successful
Build & Publish / publish (push) Successful in 1m7s
- COMPONENT_GUIDE: note search renders between Header and Sections,
  no chevrons on section headers
- Spec: update rendering diagrams and description to match
  implemented behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:43:00 +02:00
hsiegeln
0cf696cded fix(sidebar): move search below Header, remove section chevrons
All checks were successful
Build & Publish / publish (push) Successful in 1m3s
- Search input now renders between Sidebar.Header and first Section
  instead of above Header (fixes cameleer3-server#120)
- Remove ChevronRight/ChevronDown from section headers — the entire
  row is already clickable, chevrons added visual noise

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:40:18 +02:00
3 changed files with 53 additions and 39 deletions

View File

@@ -114,8 +114,11 @@ Sidebar compound API:
</Sidebar.Footer>
</Sidebar>
The app controls all content — sections, order, tree data, collapse state.
Sidebar provides the frame, search input, and icon-rail collapse mode.
Notes:
- Search input auto-renders between Header and first Section (not above Header)
- Section headers have no chevron — the entire row is clickable to toggle
- The app controls all content — sections, order, tree data, collapse state
- Sidebar provides the frame, search input, and icon-rail collapse mode
```
### Data page pattern

View File

@@ -73,6 +73,7 @@ The outer shell. Renders the sidebar frame with an optional search input and col
- Width transition: `transition: width 200ms ease`
- Collapse toggle button (`<<` / `>>` chevron) in top-right corner
- Search input hidden when collapsed
- Search input auto-positioned between `Sidebar.Header` and first `Sidebar.Section` (not above Header)
### `<Sidebar.Header>`
@@ -119,13 +120,13 @@ An accordion section with a collapsible header and content area.
**Expanded rendering:**
```
v [icon] APPLICATIONS
[icon] APPLICATIONS
(children rendered here)
```
**Collapsed rendering:**
```
> [icon] APPLICATIONS
[icon] APPLICATIONS
```
**In sidebar icon-rail mode:**
@@ -133,7 +134,7 @@ v [icon] APPLICATIONS
[icon] <- centered, tooltip shows label on hover
```
Header has: chevron (left), icon, label. Chevron rotates on collapse/expand. Active section gets the amber left-border accent (existing pattern). Clicking the header calls `onToggle`.
Header has: icon and label (no chevron — the entire row is clickable). Active section gets the amber left-border accent (existing pattern). Clicking anywhere on the header row calls `onToggle`.
**Implementation detail:** `Sidebar.Section` and `Sidebar.Header` need to know the parent's `collapsed` state to switch between expanded and icon-rail rendering. The `<Sidebar>` component provides `collapsed` and `onCollapseToggle` via React context (`SidebarContext`). Sub-components read from context — no prop drilling needed.

View File

@@ -1,11 +1,9 @@
import { type ReactNode } from 'react'
import { type ReactNode, Children, isValidElement } from 'react'
import {
Search,
X,
ChevronsLeft,
ChevronsRight,
ChevronRight,
ChevronDown,
} from 'lucide-react'
import styles from './Sidebar.module.css'
import { SidebarContext, useSidebarContext } from './SidebarContext'
@@ -124,9 +122,6 @@ function SidebarSection({
aria-label={open ? `Collapse ${label}` : `Expand ${label}`}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle() } }}
>
<span className={styles.treeSectionChevronBtn} aria-hidden="true">
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</span>
{icon && <span className={styles.sectionIcon}>{icon}</span>}
<span className={styles.treeSectionLabel}>{label}</span>
</div>
@@ -199,35 +194,50 @@ function SidebarRoot({
</button>
)}
{/* Search (only when expanded and handler provided) */}
{onSearchChange && !collapsed && (
<div className={styles.searchWrap}>
<div className={styles.searchInner}>
<span className={styles.searchIcon} aria-hidden="true">
<Search size={12} />
</span>
<input
className={styles.searchInput}
type="text"
placeholder="Filter..."
value={searchValue ?? ''}
onChange={(e) => onSearchChange(e.target.value)}
/>
{searchValue && (
<button
type="button"
className={styles.searchClear}
onClick={() => onSearchChange('')}
aria-label="Clear search"
>
<X size={12} />
</button>
)}
</div>
</div>
)}
{/* Render Header first, then search, then remaining children */}
{(() => {
const childArray = Children.toArray(children)
const headerIdx = childArray.findIndex(
(child) => isValidElement(child) && child.type === SidebarHeader,
)
const header = headerIdx >= 0 ? childArray[headerIdx] : null
const rest = headerIdx >= 0
? [...childArray.slice(0, headerIdx), ...childArray.slice(headerIdx + 1)]
: childArray
{children}
return (
<>
{header}
{onSearchChange && !collapsed && (
<div className={styles.searchWrap}>
<div className={styles.searchInner}>
<span className={styles.searchIcon} aria-hidden="true">
<Search size={12} />
</span>
<input
className={styles.searchInput}
type="text"
placeholder="Filter..."
value={searchValue ?? ''}
onChange={(e) => onSearchChange(e.target.value)}
/>
{searchValue && (
<button
type="button"
className={styles.searchClear}
onClick={() => onSearchChange('')}
aria-label="Clear search"
>
<X size={12} />
</button>
)}
</div>
</div>
)}
{rest}
</>
)
})()}
</aside>
</SidebarContext.Provider>
)