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>
This commit is contained in:
hsiegeln
2026-04-02 22:40:18 +02:00
parent 50a1296a9d
commit 0cf696cded

View File

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