Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
7fdf731
feat(sidebar): add the imperative resizable-panel engine
frankieyan Jun 28, 2026
09426e3
feat(sidebar): add docked Sidebar with controlled width and collapse
frankieyan Jun 28, 2026
1b31115
feat(sidebar): add overlay modes, modal backdrop, focus trap, and Esc…
frankieyan Jun 28, 2026
0758323
feat(sidebar): add the SidebarResizeHandle slot
frankieyan Jun 28, 2026
c7c5cee
feat(sidebar): unmount content on hide after the exit transition
frankieyan Jun 28, 2026
2cd3b7c
docs(sidebar): add Storybook stories
frankieyan Jun 28, 2026
f4525d9
docs(sidebar): add the MDX docs page
frankieyan Jun 28, 2026
e95dbfe
docs(sidebar): inline the composition so story source shows the real …
frankieyan Jun 29, 2026
4f3ef98
chore: set displayName on forwardRef components for readable Storyboo…
frankieyan Jun 29, 2026
bb135e3
docs(sidebar): add a responsive docked/overlay story
frankieyan Jun 29, 2026
8aedace
docs(sidebar): add a stacked sidebars story
frankieyan Jun 29, 2026
d8e5a43
docs(sidebar): add a left and right sidebars story
frankieyan Jun 29, 2026
63b5511
docs(sidebar): link the Figma design in the stories
frankieyan Jun 29, 2026
2ac5ee8
feat(sidebar): make SidebarContent a neutral panel with a landmark child
frankieyan Jun 29, 2026
447bf6f
feat(sidebar): hide the background from assistive tech for modal over…
frankieyan Jun 29, 2026
e719ed5
fix(sidebar): scope the Escape dismiss to the panel
frankieyan Jun 29, 2026
48791b3
refactor(sidebar): remove the unused isResizing state from the resize…
frankieyan Jun 30, 2026
ad28d7a
refactor(sidebar): drive the panel width from a single CSS variable
frankieyan Jun 30, 2026
daa84f4
fix(sidebar): derive the unmountOnHide fallback from the transition d…
frankieyan Jun 30, 2026
7670768
feat(sidebar): warn when a resize handle has no usable bounds
frankieyan Jun 30, 2026
8921459
fix(sidebar): make Home/End edge-aware so align="end" swaps them
frankieyan Jun 30, 2026
ce2a37a
test(sidebar): restructure suite into scenario-level tests
frankieyan Jun 30, 2026
64755fa
feat(sidebar): add SidebarPersistentContent and inert closed content
frankieyan Jul 1, 2026
800ef45
fix(sidebar): honor prefers-reduced-motion on the panel transition
frankieyan Jul 2, 2026
1eee402
docs(sidebar): smooth the collapse toggle transition
frankieyan Jul 4, 2026
d0600ba
fix(sidebar): zero the transition-duration variable under reduced motion
frankieyan Jul 4, 2026
566667c
refactor(sidebar): rename the overlay viewport-margin variable to min…
frankieyan Jul 4, 2026
51beb79
feat(sidebar): forward data-* attributes on SidebarResizeHandle
frankieyan Jul 4, 2026
e8f78a5
fix(sidebar): stop width-less panels collapsing and gate the warning
frankieyan Jul 4, 2026
aba9889
fix(sidebar): warn when unmountOnHide would unmount SidebarPersistent…
frankieyan Jul 4, 2026
31b2502
feat(sidebar): inert the background from the modal overlay itself
frankieyan Jul 4, 2026
03d3623
fix(sidebar): ignore non-primary pointer buttons on the resize handle
frankieyan Jul 4, 2026
731aad7
refactor(sidebar): tidy the public API surface
frankieyan Jul 4, 2026
290e18a
docs(sidebar): demonstrate consumer-owned Escape consumption
frankieyan Jul 4, 2026
978a8d0
docs(sidebar): fix and fill gaps in the MDX
frankieyan Jul 4, 2026
f2d67ce
docs(sidebar): document the resize handle data-* pass-through
frankieyan Jul 4, 2026
495d953
fix: keep the modal sidebar backdrop clickable when mounted open
frankieyan Jul 5, 2026
eb38e5c
docs: document the one-modal-overlay-at-a-time constraint
frankieyan Jul 5, 2026
5f09bde
fix: honor panel onKeyDown veto and skip no-op resize commits
frankieyan Jul 5, 2026
03d0b01
refactor: make Sidebar layout direction-aware with logical properties
frankieyan Jul 5, 2026
86597a0
fix: account for the inline inset in the overlay Sidebar max-width
frankieyan Jul 5, 2026
0008dfb
docs: document Sidebar slot props, shell clipping, and the unmountOnH…
frankieyan Jul 5, 2026
cd13c73
test: cover Sidebar slot-prop passthrough and misconfiguration warnings
frankieyan Jul 5, 2026
6ba996f
test: drop redundant 'children mounted while closed' Sidebar test
frankieyan Jul 5, 2026
b81d007
docs: clarify overlay dialog-role timing and panel-scoped Escape
frankieyan Jul 5, 2026
5079dbb
feat(sidebar): add per-edge overlay block-inset custom properties
frankieyan Jul 5, 2026
a5b425e
fix: resolve overlay block-inset custom properties on the panel, not …
frankieyan Jul 5, 2026
9579487
docs: note the closed-overlay inline-inset sliver is intended
frankieyan Jul 6, 2026
6ec0c98
docs: clarify Sidebar RTL, resize, collapse, and overlay-cap edge cases
frankieyan Jul 6, 2026
60ad562
docs: add a Sidebar data-attributes reference to Storybook
frankieyan Jul 6, 2026
101dc1c
refactor(sidebar): prune dead resize-engine code and trim comments
frankieyan Jul 6, 2026
b590682
refactor(sidebar): drop data-disabled; style the closed handle via ar…
frankieyan Jul 7, 2026
aa6ced1
docs: drop the unneeded overflow clip from the Docked story
frankieyan Jul 7, 2026
87fab98
fix(sidebar): keep a controlled width within its min/max bounds
frankieyan Jul 7, 2026
2d2fd80
refactor(sidebar): trim JSDoc and inline comments that restate the code
frankieyan Jul 8, 2026
25543ce
docs(sidebar): cut story JSDoc/demo lectures and de-duplicate the MDX
frankieyan Jul 8, 2026
a199e96
docs(sidebar): lead with CollapsibleNav, drop Docked, defer the Playg…
frankieyan Jul 8, 2026
ad3d4b4
docs(sidebar): restyle the demos with Box tokens and rework the docs …
frankieyan Jul 8, 2026
554c3c3
docs(sidebar): combine the multi-sidebar stories into one responsive …
frankieyan Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from './badge'
export * from './expansion-panel'
export * from './menu'
export * from './modal'
export * from './sidebar'
export * from './tabs'
export * from './tooltip'

Expand Down
1 change: 1 addition & 0 deletions src/sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './sidebar'
353 changes: 353 additions & 0 deletions src/sidebar/sidebar.mdx

Large diffs are not rendered by default.

200 changes: 200 additions & 0 deletions src/sidebar/sidebar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
:root {
/* Overlay layering and viewport behaviour. */
--reactist-sidebar-overlay-z-index: 40;
--reactist-sidebar-collapsed-z-index: 1;
--reactist-sidebar-overlay-min-viewport-gap: 0px;
--reactist-sidebar-overlay-inset-block: 0px;
/*
* `-block` is the shorthand; the per-edge `-block-start`/`-block-end`
* longhands are read at the point of use below and fall back to it, so a
* consumer can inset a single edge (e.g. below a top title bar) or both via
* the shorthand. The longhands are deliberately not declared here: resolving
* their fallback at :root would bake in the shorthand's :root value and a
* consumer override set lower (on the panel or an ancestor) would not take.
* Inline stays a single `-inline`: the overlay anchors to one inline edge and
* is width-sized, so only that edge is a positional inset.
*/
--reactist-sidebar-overlay-inset-inline: 0px;

/* Modal backdrop. */
--reactist-sidebar-backdrop-color: #000000;
--reactist-sidebar-backdrop-opacity: 0.5;
--reactist-sidebar-backdrop-z-index: 39;

/* Resize handle. `fill` is the line colour; the state prefix selects idle / hover / focus. */
--reactist-sidebar-resize-handle-width: 4px;
--reactist-sidebar-resize-handle-idle-fill: transparent;
--reactist-sidebar-resize-handle-hover-fill: var(--reactist-divider-primary);
--reactist-sidebar-resize-handle-focus-fill: var(--reactist-actionable-primary-idle-fill);

--reactist-sidebar-transition-duration: 300ms;
--reactist-sidebar-transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
}

.panel {
position: relative;
box-sizing: border-box;
width: var(--reactist-sidebar-width);
/*
* `contain: layout` bounds the collapse reflow without `paint`, so consumer
* popovers, focus rings, the resize handle, and an overlay card's shadow are
* never clipped to the panel box.
*/
contain: layout;
}

/*
* Docked: the panel is an in-flow flex child that holds its width (`flex-shrink`
* comes from the Box prop). Collapsing slides it out with a negative inline
* margin so the main absorber reflows into the freed space.
*/
.panel[data-overlay='false'] {
height: 100%;
transition: margin var(--reactist-sidebar-transition-duration)
var(--reactist-sidebar-transition-easing);
}

.panel[data-overlay='false'][data-state='closed'][data-align='start'] {
margin-inline-start: calc(-1 * var(--reactist-sidebar-width, 0px));
}

.panel[data-overlay='false'][data-state='closed'][data-align='end'] {
margin-inline-end: calc(-1 * var(--reactist-sidebar-width, 0px));
}

/*
* Collapsed + docked, the panel's own `contain: layout` traps a persistent
* control's z-index in the panel's stacking context, so it paints under a main
* that is itself a stacking context. Lift the panel to keep the control visible.
*/
.panel[data-overlay='false'][data-state='closed'] {
z-index: var(--reactist-sidebar-collapsed-z-index);
}

/*
* Overlay: the panel floats over the content layer, anchored to its align edge
* and capped to the viewport. It slides off-edge with a compositor-only
* transform. Box geometry (insets, rounding, skin) is the consumer's; the inset
* custom properties let them inset a floating card without a specificity fight.
*/
.panel[data-overlay='true'] {
position: fixed;
inset-block-start: var(
--reactist-sidebar-overlay-inset-block-start,
var(--reactist-sidebar-overlay-inset-block)
);
inset-block-end: var(
--reactist-sidebar-overlay-inset-block-end,
var(--reactist-sidebar-overlay-inset-block)
);
z-index: var(--reactist-sidebar-overlay-z-index);
max-width: calc(
100vw - var(--reactist-sidebar-overlay-inset-inline) -
var(--reactist-sidebar-overlay-min-viewport-gap)
);
transition: transform var(--reactist-sidebar-transition-duration)
var(--reactist-sidebar-transition-easing);
}

.panel[data-overlay='true'][data-align='start'] {
inset-inline-start: var(--reactist-sidebar-overlay-inset-inline);
}

.panel[data-overlay='true'][data-align='end'] {
inset-inline-end: var(--reactist-sidebar-overlay-inset-inline);
}

.panel[data-overlay='true'][data-state='closed'][data-align='start'] {
transform: translateX(-100%);
}

.panel[data-overlay='true'][data-state='closed'][data-align='end'] {
transform: translateX(100%);
}

/*
* `translateX` is physical, so flip the off-edge slide under RTL to stay aligned
* with the logical `inset-inline` anchor above.
*/
[dir='rtl'] .panel[data-overlay='true'][data-state='closed'][data-align='start'] {
transform: translateX(100%);
}

[dir='rtl'] .panel[data-overlay='true'][data-state='closed'][data-align='end'] {
transform: translateX(-100%);
}

/*
* Layout-neutral wrapper for the focus trap: it must not establish a positioning
* or stacking context, so the absolutely-positioned resize handle still resolves
* against the panel.
*/
.focusLock {
display: contents;
}

.persistentContent,
.inertContent {
display: contents;
}

.backdrop {
position: fixed;
inset: 0;
z-index: var(--reactist-sidebar-backdrop-z-index);
background-color: var(--reactist-sidebar-backdrop-color);
opacity: 0;
pointer-events: none;
transition: opacity var(--reactist-sidebar-transition-duration)
var(--reactist-sidebar-transition-easing);
}

.backdrop[data-state='open'] {
opacity: var(--reactist-sidebar-backdrop-opacity);
pointer-events: auto;
}

.resizeHandle {
position: absolute;
inset-block: 0;
z-index: 1;
width: var(--reactist-sidebar-resize-handle-width);
background-color: var(--reactist-sidebar-resize-handle-idle-fill);
cursor: col-resize;
touch-action: none;
transition: background-color 120ms ease;
}

.resizeHandle[data-align='start'] {
inset-inline-end: 0;
}

.resizeHandle[data-align='end'] {
inset-inline-start: 0;
}

.resizeHandle:hover {
background-color: var(--reactist-sidebar-resize-handle-hover-fill);
}

.resizeHandle:focus-visible {
background-color: var(--reactist-sidebar-resize-handle-focus-fill);
outline: none;
}

/* The closed handle is `aria-hidden`; drop pointer events so it can't be grabbed. */
.resizeHandle[aria-hidden='true'] {
pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
:root {
--reactist-sidebar-transition-duration: 0s;
}

.panel,
.backdrop,
.resizeHandle {
transition: none !important;
}
}
Loading
Loading