editor: friendlier panels - collapsible rail, remembered state, no more overlap
Rail sections (Outline/Blocks/Nodes) collapse via their headers like Experiments already did, so long pages stay navigable. Rail open state and section toggles persist in localStorage across reloads (the QA preview flow reloads constantly). The assistant panel shifts left while the dev panel is open instead of stacking exactly underneath it, and the rail/dev/assistant widths cap at the viewport on small screens. Tests: rail-sections harness (collapse, reload persistence, shifted assistant); harness boots with a fresh profile unless keepStorage is passed; in-memory localStorage shim for the origin-less jsdom. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
1 parent
5786712078
commit
bb875a0956
10 files changed
+185
-20
No files matched your search
+39
-12
@@ -5,7 +5,7 @@ import { allowedChildren, connectionsOf, labelKey, sortGroups, typeName } from '
|
||||
import type { DropTarget } from '../page/dnd'
|
||||
import { Icon, TypeIcon } from './Icon'
|
||||
import { Summary } from './Summary'
|
||||
import { useCtrl, useEditorState } from './hooks'
|
||||
import { useCtrl, useEditorState, usePersistentState } from './hooks'
|
||||
import type { Block, Controller } from '../controller'
|
||||
|
||||
/**
|
||||
@@ -39,7 +39,7 @@ export function Rail() {
|
||||
<span className="cms-rail__title">
|
||||
<Icon name="outline" size={16} /> Page
|
||||
</span>
|
||||
<button className="cms-ico" onClick={() => ctrl.store.set({ rail: false })} aria-label="Close panel">
|
||||
<button className="cms-ico" onClick={() => ctrl.setRail(false)} aria-label="Close panel">
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</header>
|
||||
@@ -108,16 +108,25 @@ function OutlineTree() {
|
||||
}, [drag])
|
||||
|
||||
const roots = useMemo(() => blocks.filter((b) => !b.parentId), [blocks])
|
||||
const [open, setOpen] = usePersistentState('cms-onpage.sec.outline', true)
|
||||
if (!blocks.length) return null
|
||||
return (
|
||||
<section aria-label="Outline">
|
||||
<h4 className="cms-rail__sec">Outline</h4>
|
||||
<ul className="cms-outline cms-outline--tree">
|
||||
{roots.map((b) => (
|
||||
<OutlineNode key={b.id} block={b} level={0} collapsed={collapsed} setCollapsed={setCollapsed} over={over} setOver={setOver} dragging={dragging} selected={selected} />
|
||||
))}
|
||||
</ul>
|
||||
{dragging && <OutlineGap parentId="" index={roots.length} over={over} setOver={setOver} />}
|
||||
<h4 className="cms-rail__sec">
|
||||
<button className="cms-sectoggle" aria-expanded={open} aria-label="Outline" onClick={() => setOpen((o) => !o)}>
|
||||
Outline ({blocks.length})
|
||||
</button>
|
||||
</h4>
|
||||
{open && (
|
||||
<>
|
||||
<ul className="cms-outline cms-outline--tree">
|
||||
{roots.map((b) => (
|
||||
<OutlineNode key={b.id} block={b} level={0} collapsed={collapsed} setCollapsed={setCollapsed} over={over} setOver={setOver} dragging={dragging} selected={selected} />
|
||||
))}
|
||||
</ul>
|
||||
{dragging && <OutlineGap parentId="" index={roots.length} over={over} setOver={setOver} />}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -389,9 +398,16 @@ function Palette() {
|
||||
doc.addEventListener('pointerup', stop, true)
|
||||
}
|
||||
|
||||
const [open, setOpen] = usePersistentState('cms-onpage.sec.blocks', true)
|
||||
return (
|
||||
<section aria-label="Blocks">
|
||||
<h4 className="cms-rail__sec">Blocks</h4>
|
||||
<h4 className="cms-rail__sec">
|
||||
<button className="cms-sectoggle" aria-expanded={open} aria-label="Blocks" onClick={() => setOpen((o) => !o)}>
|
||||
Blocks
|
||||
</button>
|
||||
</h4>
|
||||
{open && (
|
||||
<>
|
||||
<div className="cms-pad">
|
||||
<input className="cms-input" placeholder="Search blocks…" aria-label="Search blocks" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<p className="cms-muted cms-hint">Drag onto the page or the outline, or click to add to the selection.</p>
|
||||
@@ -426,6 +442,8 @@ function Palette() {
|
||||
</details>
|
||||
))}
|
||||
{types && !shown.length && <p className="cms-muted cms-pad">Nothing matches.</p>}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -515,9 +533,16 @@ function NodeSearch() {
|
||||
doc.addEventListener('pointerup', stop, true)
|
||||
}
|
||||
|
||||
const [open, setOpen] = usePersistentState('cms-onpage.sec.nodes', true)
|
||||
return (
|
||||
<section aria-label="Nodes">
|
||||
<h4 className="cms-rail__sec">Nodes</h4>
|
||||
<h4 className="cms-rail__sec">
|
||||
<button className="cms-sectoggle" aria-expanded={open} aria-label="Nodes" onClick={() => setOpen((o) => !o)}>
|
||||
Nodes
|
||||
</button>
|
||||
</h4>
|
||||
{open && (
|
||||
<>
|
||||
<div className="cms-pad cms-nodefind">
|
||||
<input className="cms-input" placeholder="Search existing nodes…" aria-label="Search nodes" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<select className="cms-input" aria-label="Filter by type" value={type} onChange={(e) => setType(e.target.value)}>
|
||||
@@ -560,6 +585,8 @@ function NodeSearch() {
|
||||
</ul>
|
||||
)}
|
||||
{!!results?.length && total > results.length && <p className="cms-muted cms-pad">Showing {results.length} of {total} — refine the search.</p>}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -585,7 +612,7 @@ function ExperimentsPanel() {
|
||||
const [stats, setStats] = useState<Record<string, ExperimentAnalytics>>({})
|
||||
const [confirmOff, setConfirmOff] = useState<string | null>(null)
|
||||
const [tick, setTick] = useState(0) // re-read override cookies after set/clear
|
||||
const [open, setOpen] = useState(false)
|
||||
const [open, setOpen] = usePersistentState('cms-onpage.sec.experiments', false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
Reference in new issue
Block a user