Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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.

209 changes: 209 additions & 0 deletions src/sidebar/sidebar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
: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 its containing block, bounded by 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(
min(100%, 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;
}

/*
* While the panel is closed, hide its inert content (i.e. not the persistent slot)
* so content wider than the panel doesn't flow into the viewport/container
*/
.panel[data-state='closed'] .inertContent {
visibility: hidden;
transition: visibility 0s var(--reactist-sidebar-transition-duration);
}

.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