Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f1df869db | ||
|
|
0cf696cded |
@@ -114,8 +114,11 @@ Sidebar compound API:
|
|||||||
</Sidebar.Footer>
|
</Sidebar.Footer>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
The app controls all content — sections, order, tree data, collapse state.
|
Notes:
|
||||||
Sidebar provides the frame, search input, and icon-rail collapse mode.
|
- 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
|
### Data page pattern
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ The outer shell. Renders the sidebar frame with an optional search input and col
|
|||||||
- Width transition: `transition: width 200ms ease`
|
- Width transition: `transition: width 200ms ease`
|
||||||
- Collapse toggle button (`<<` / `>>` chevron) in top-right corner
|
- Collapse toggle button (`<<` / `>>` chevron) in top-right corner
|
||||||
- Search input hidden when collapsed
|
- Search input hidden when collapsed
|
||||||
|
- Search input auto-positioned between `Sidebar.Header` and first `Sidebar.Section` (not above Header)
|
||||||
|
|
||||||
### `<Sidebar.Header>`
|
### `<Sidebar.Header>`
|
||||||
|
|
||||||
@@ -119,13 +120,13 @@ An accordion section with a collapsible header and content area.
|
|||||||
|
|
||||||
**Expanded rendering:**
|
**Expanded rendering:**
|
||||||
```
|
```
|
||||||
v [icon] APPLICATIONS
|
[icon] APPLICATIONS
|
||||||
(children rendered here)
|
(children rendered here)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Collapsed rendering:**
|
**Collapsed rendering:**
|
||||||
```
|
```
|
||||||
> [icon] APPLICATIONS
|
[icon] APPLICATIONS
|
||||||
```
|
```
|
||||||
|
|
||||||
**In sidebar icon-rail mode:**
|
**In sidebar icon-rail mode:**
|
||||||
@@ -133,7 +134,7 @@ v [icon] APPLICATIONS
|
|||||||
[icon] <- centered, tooltip shows label on hover
|
[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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user