Compare commits
4 Commits
ba6028c2ea
...
v0.1.25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f1df869db | ||
|
|
0cf696cded | ||
|
|
50a1296a9d | ||
|
|
9b8739b5d8 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -194,7 +194,15 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
if (!open) return null
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.overlay} onClick={onClose} data-testid="command-palette-overlay">
|
||||
<div
|
||||
className={styles.overlay}
|
||||
onClick={onClose}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onClose() }}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close command palette"
|
||||
data-testid="command-palette-overlay"
|
||||
>
|
||||
<div
|
||||
className={styles.panel}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -298,6 +306,12 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
||||
onSelect(result)
|
||||
onClose()
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onSelect(result)
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => { userNavigated.current = true; setFocusedIdx(flatIdx) }}
|
||||
>
|
||||
<div className={styles.itemMain}>
|
||||
|
||||
@@ -230,6 +230,10 @@ function TreeNodeRow({
|
||||
|
||||
return (
|
||||
<li role="none">
|
||||
{/* S1082: No onKeyDown here by design — the parent <ul role="tree"> carries
|
||||
onKeyDown={handleKeyDown} which handles Enter (select) and all arrow keys
|
||||
per the WAI-ARIA tree widget pattern. Adding a duplicate handler here would
|
||||
fire the action twice. */}
|
||||
<div
|
||||
role="treeitem"
|
||||
aria-expanded={hasChildren ? isExpanded : undefined}
|
||||
|
||||
@@ -215,6 +215,8 @@
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 8px 0 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.treeSectionChevronBtn {
|
||||
|
||||
@@ -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'
|
||||
@@ -115,15 +113,15 @@ function SidebarSection({
|
||||
|
||||
return (
|
||||
<div className={`${styles.treeSection} ${active ? styles.treeSectionActive : ''} ${className ?? ''}`}>
|
||||
<div className={styles.treeSectionToggle}>
|
||||
<button
|
||||
className={styles.treeSectionChevronBtn}
|
||||
onClick={onToggle}
|
||||
aria-expanded={open}
|
||||
aria-label={open ? `Collapse ${label}` : `Expand ${label}`}
|
||||
>
|
||||
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</button>
|
||||
<div
|
||||
className={styles.treeSectionToggle}
|
||||
onClick={onToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={open}
|
||||
aria-label={open ? `Collapse ${label}` : `Expand ${label}`}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle() } }}
|
||||
>
|
||||
{icon && <span className={styles.sectionIcon}>{icon}</span>}
|
||||
<span className={styles.treeSectionLabel}>{label}</span>
|
||||
</div>
|
||||
@@ -196,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>
|
||||
)
|
||||
|
||||
@@ -366,6 +366,10 @@ function SidebarTreeRow({
|
||||
|
||||
return (
|
||||
<li role="none">
|
||||
{/* S1082: No onKeyDown here by design — the parent <ul role="tree"> carries
|
||||
onKeyDown={handleKeyDown} which handles Enter (navigate) and all arrow keys
|
||||
per the WAI-ARIA tree widget pattern. Adding a duplicate handler here would
|
||||
fire the action twice. */}
|
||||
<div
|
||||
role="treeitem"
|
||||
aria-expanded={hasChildren ? isExpanded : undefined}
|
||||
|
||||
@@ -59,7 +59,15 @@ export function InlineEdit({ value, onSave, placeholder, disabled, className }:
|
||||
<span className={`${styles.display} ${disabled ? styles.disabled : ''} ${className ?? ''}`}>
|
||||
<span
|
||||
className={isEmpty ? styles.placeholder : styles.value}
|
||||
role="button"
|
||||
tabIndex={disabled ? undefined : 0}
|
||||
onClick={startEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
startEdit()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEmpty ? placeholder : value}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user