UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

936 lines (896 loc) • 32 kB
import React, { useRef, memo } from 'react'; import { clsx } from 'clsx'; import { useId } from '../hooks/useId.js'; import { isResponsiveValue } from '../hooks/useResponsiveValue.js'; import { useSlots } from '../hooks/useSlots.js'; import { useOverflow } from '../hooks/useOverflow.js'; import { warning } from '../utils/warning.js'; import { getResponsiveAttributes } from '../internal/utils/getResponsiveAttributes.js'; import classes from './PageLayout.module.css.js'; import { usePaneWidth, isPaneWidth, isCustomWidthOptions, updateAriaValues, ARROW_KEY_STEP } from './usePaneWidth.js'; import { setDraggingStyles, removeDraggingStyles } from './paneUtils.js'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useMergedRefs } from '../hooks/useMergedRefs.js'; const isArrowKey = key => key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowDown'; const isShrinkKey = key => key === 'ArrowLeft' || key === 'ArrowDown'; const PageLayoutContext = /*#__PURE__*/React.createContext({ padding: 'normal', rowGap: 'normal', columnGap: 'normal', paneRef: { current: null }, contentWrapperRef: { current: null }, sidebarRef: { current: null }, sidebarContentWrapperRef: { current: null } }); // ---------------------------------------------------------------------------- // PageLayout // TODO: refs const Root = ({ containerWidth = 'xlarge', padding = 'normal', rowGap = 'normal', columnGap = 'normal', children, className, style, _slotsConfig: slotsConfig }) => { const paneRef = useRef(null); const contentWrapperRef = useRef(null); const sidebarRef = useRef(null); const sidebarContentWrapperRef = useRef(null); const [slots, rest] = useSlots(children, slotsConfig !== null && slotsConfig !== void 0 ? slotsConfig : { header: Header, footer: Footer, sidebar: Sidebar }); const memoizedContextValue = React.useMemo(() => { return { padding, rowGap, columnGap, paneRef, contentWrapperRef, sidebarRef, sidebarContentWrapperRef }; }, [padding, rowGap, columnGap, paneRef, contentWrapperRef, sidebarRef, sidebarContentWrapperRef]); return /*#__PURE__*/jsx(PageLayoutContext.Provider, { value: memoizedContextValue, children: /*#__PURE__*/jsxs(RootWrapper, { style: style, padding: padding, className: className, hasSidebar: !!slots.sidebar, children: [slots.sidebar, /*#__PURE__*/jsxs("div", { ref: sidebarContentWrapperRef, className: classes.PageLayoutWrapper, "data-width": containerWidth, children: [slots.header, /*#__PURE__*/jsx("div", { className: clsx(classes.PageLayoutContent), children: rest }), slots.footer] })] }) }); }; Root.displayName = "Root"; const RootWrapper = /*#__PURE__*/memo(({ style, padding, children, className, hasSidebar }) => { return /*#__PURE__*/jsx("div", { style: { '--spacing': `var(--spacing-${padding})`, ...style }, className: clsx(classes.PageLayoutRoot, className), "data-has-sidebar": hasSidebar || undefined, children: children }); }); Root.displayName = 'PageLayout'; // ---------------------------------------------------------------------------- // Divider (internal) const HorizontalDivider = /*#__PURE__*/memo(({ variant = 'none', className, position, style }) => { const { padding } = React.useContext(PageLayoutContext); return /*#__PURE__*/jsx("div", { className: clsx(classes.HorizontalDivider, className), ...getResponsiveAttributes('variant', variant), ...getResponsiveAttributes('position', position), style: { '--spacing-divider': `var(--spacing-${padding})`, ...style } }); }); HorizontalDivider.displayName = 'HorizontalDivider'; const VerticalDivider = /*#__PURE__*/memo(({ variant = 'none', position, className, style, children }) => { return /*#__PURE__*/jsx("div", { className: clsx(classes.VerticalDivider, className), ...getResponsiveAttributes('variant', variant), ...getResponsiveAttributes('position', position), style: style, children: children }); }); VerticalDivider.displayName = 'VerticalDivider'; const SidebarDivider = /*#__PURE__*/memo(function SidebarDivider({ position, divider, resizable, minPaneWidth, maxPaneWidth, currentWidth, currentWidthRef, handleRef, sidebarRef, dragStartClientXRef, dragStartWidthRef, dragMaxWidthRef, getMaxPaneWidth, getDefaultWidth, saveWidth }) { const { columnGap } = React.useContext(PageLayoutContext); return /*#__PURE__*/jsx(VerticalDivider, { variant: resizable ? 'line' : divider, position: position, className: classes.SidebarVerticalDivider, style: { '--spacing': `var(--spacing-${columnGap})` }, children: resizable ? /*#__PURE__*/jsx(DragHandle, { handleRef: handleRef, "aria-valuemin": minPaneWidth, "aria-valuemax": maxPaneWidth, "aria-valuenow": currentWidth, onDragStart: clientX => { var _sidebarRef$current$g, _sidebarRef$current; dragStartClientXRef.current = clientX; dragStartWidthRef.current = (_sidebarRef$current$g = (_sidebarRef$current = sidebarRef.current) === null || _sidebarRef$current === void 0 ? void 0 : _sidebarRef$current.getBoundingClientRect().width) !== null && _sidebarRef$current$g !== void 0 ? _sidebarRef$current$g : currentWidthRef.current; dragMaxWidthRef.current = getMaxPaneWidth(); }, onDrag: (value, isKeyboard) => { const maxWidth = isKeyboard ? getMaxPaneWidth() : dragMaxWidthRef.current; if (isKeyboard) { // For position='end': invert the delta so arrow keys feel natural // ArrowRight should shrink (move divider right), ArrowLeft should expand const delta = position === 'end' ? -value : value; const newWidth = Math.max(minPaneWidth, Math.min(maxWidth, currentWidthRef.current + delta)); if (newWidth !== currentWidthRef.current) { var _sidebarRef$current2; currentWidthRef.current = newWidth; (_sidebarRef$current2 = sidebarRef.current) === null || _sidebarRef$current2 === void 0 ? void 0 : _sidebarRef$current2.style.setProperty('--pane-width', `${newWidth}px`); updateAriaValues(handleRef.current, { current: newWidth, max: maxWidth }); } } else { if (sidebarRef.current) { const deltaX = value - dragStartClientXRef.current; // For position='end': cursor moving left (negative delta) increases width // For position='start': cursor moving right (positive delta) increases width const directedDelta = position === 'end' ? -deltaX : deltaX; const newWidth = dragStartWidthRef.current + directedDelta; const clampedWidth = Math.max(minPaneWidth, Math.min(maxWidth, newWidth)); if (Math.round(clampedWidth) !== Math.round(currentWidthRef.current)) { sidebarRef.current.style.setProperty('--pane-width', `${clampedWidth}px`); currentWidthRef.current = clampedWidth; updateAriaValues(handleRef.current, { current: Math.round(clampedWidth), max: maxWidth }); } } } }, onDragEnd: () => { saveWidth(currentWidthRef.current); }, onDoubleClick: () => { const resetWidth = getDefaultWidth(); if (sidebarRef.current) { sidebarRef.current.style.setProperty('--pane-width', `${resetWidth}px`); currentWidthRef.current = resetWidth; updateAriaValues(handleRef.current, { current: resetWidth }); } saveWidth(resetWidth); } }) : null }); }); /** * DragHandle - handles all pointer and keyboard interactions for resizing * ARIA values are set in JSX for SSR accessibility, * then updated via DOM manipulation during drag for performance */ const DragHandle = /*#__PURE__*/memo(function DragHandle({ handleRef, onDragStart, onDrag, onDragEnd, onDoubleClick, 'aria-valuemin': ariaValueMin, 'aria-valuemax': ariaValueMax, 'aria-valuenow': ariaValueNow }) { const stableOnDragStart = React.useRef(onDragStart); const stableOnDrag = React.useRef(onDrag); const stableOnDragEnd = React.useRef(onDragEnd); React.useEffect(() => { stableOnDragStart.current = onDragStart; stableOnDrag.current = onDrag; stableOnDragEnd.current = onDragEnd; }); const { paneRef, contentWrapperRef } = React.useContext(PageLayoutContext); // Dragging state as a ref - cheaper than reading from DOM style const isDraggingRef = React.useRef(false); // Set inline styles for drag optimizations - zero overhead at rest const startDragging = React.useCallback(() => { if (isDraggingRef.current) return; setDraggingStyles({ handle: handleRef.current, pane: paneRef.current, contentWrapper: contentWrapperRef.current }); isDraggingRef.current = true; }, [handleRef, contentWrapperRef, paneRef]); const endDragging = React.useCallback(() => { if (!isDraggingRef.current) return; removeDraggingStyles({ handle: handleRef.current, pane: paneRef.current, contentWrapper: contentWrapperRef.current }); isDraggingRef.current = false; }, [handleRef, contentWrapperRef, paneRef]); /** * Pointer down starts a drag operation * Capture the pointer to continue receiving events outside the handle area */ const handlePointerDown = React.useCallback(event => { if (event.button !== 0) return; event.preventDefault(); const target = event.currentTarget; // Try to capture pointer - may fail in test environments or if pointer is already released try { target.setPointerCapture(event.pointerId); } catch { // Ignore - pointer capture is a nice-to-have for dragging outside the element } stableOnDragStart.current(event.clientX); startDragging(); }, [startDragging]); // Simple rAF throttle - one update per frame, latest position wins const rafIdRef = React.useRef(null); const pendingClientXRef = React.useRef(null); /** * Pointer move during drag * Calls onDrag with absolute cursor X position * Uses rAF to coalesce updates to one per frame */ const handlePointerMove = React.useCallback(event => { if (!isDraggingRef.current) return; event.preventDefault(); // Store latest position - only the final position before rAF fires matters pendingClientXRef.current = event.clientX; // Schedule update if not already scheduled if (rafIdRef.current === null) { rafIdRef.current = requestAnimationFrame(() => { rafIdRef.current = null; if (pendingClientXRef.current !== null) { stableOnDrag.current(pendingClientXRef.current, false); pendingClientXRef.current = null; } }); } }, []); /** * Pointer up - cleanup is handled by onLostPointerCapture event * which fires when pointer capture is released (including on pointerup) */ const handlePointerUp = React.useCallback(event => { if (!isDraggingRef.current) return; event.preventDefault(); }, []); /** * Lost pointer capture ends a drag operation * Cleans up dragging state and cancels any pending rAF * Calls onDragEnd callback */ const handleLostPointerCapture = React.useCallback(() => { if (!isDraggingRef.current) return; // Cancel any pending rAF to prevent stale updates if (rafIdRef.current !== null) { cancelAnimationFrame(rafIdRef.current); rafIdRef.current = null; pendingClientXRef.current = null; } endDragging(); stableOnDragEnd.current(); }, [endDragging]); /** * Keyboard handling for accessibility * Arrow keys adjust the pane size in 3px increments * Prevents default scrolling behavior * Sets and clears dragging state via data attribute * Calls onDrag */ const handleKeyDown = React.useCallback(event => { if (!isArrowKey(event.key)) return; event.preventDefault(); // https://github.com/github/accessibility/issues/5101#issuecomment-1822870655 const delta = isShrinkKey(event.key) ? -ARROW_KEY_STEP : ARROW_KEY_STEP; // Only set dragging on first keydown (not repeats) if (!isDraggingRef.current) { startDragging(); } stableOnDrag.current(delta, true); }, [startDragging]); const handleKeyUp = React.useCallback(event => { if (!isArrowKey(event.key)) return; event.preventDefault(); endDragging(); stableOnDragEnd.current(); }, [endDragging]); // Cleanup rAF on unmount to prevent stale callbacks React.useEffect(() => { return () => { if (rafIdRef.current !== null) { cancelAnimationFrame(rafIdRef.current); rafIdRef.current = null; } }; }, []); return /*#__PURE__*/jsx("div", { ref: handleRef, className: classes.DraggableHandle, role: "slider", "aria-label": "Draggable pane splitter", "aria-valuemin": ariaValueMin, "aria-valuemax": ariaValueMax, "aria-valuenow": ariaValueNow, "aria-valuetext": ariaValueNow !== undefined ? `Pane width ${ariaValueNow} pixels` : undefined, tabIndex: 0, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onLostPointerCapture: handleLostPointerCapture, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onDoubleClick: onDoubleClick }); }); // ---------------------------------------------------------------------------- // PageLayout.Header const Header = ({ 'aria-label': label, 'aria-labelledby': labelledBy, padding = 'none', divider = 'none', dividerWhenNarrow = 'inherit', hidden = false, children, style, className }) => { // Combine divider and dividerWhenNarrow for backwards compatibility const dividerProp = !isResponsiveValue(divider) && dividerWhenNarrow !== 'inherit' ? { regular: divider, narrow: dividerWhenNarrow } : divider; const { rowGap } = React.useContext(PageLayoutContext); return /*#__PURE__*/jsxs("header", { "aria-label": label, "aria-labelledby": labelledBy, ...getResponsiveAttributes('hidden', hidden), className: clsx(classes.Header, className), style: { '--spacing': `var(--spacing-${rowGap})`, ...style }, children: [/*#__PURE__*/jsx("div", { className: classes.HeaderContent, style: { '--spacing': `var(--spacing-${padding})` }, children: children }), /*#__PURE__*/jsx(HorizontalDivider, { variant: dividerProp, className: classes.HeaderHorizontalDivider, style: { '--spacing': `var(--spacing-${rowGap})` } })] }); }; Header.displayName = "Header"; Header.displayName = 'PageLayout.Header'; // ---------------------------------------------------------------------------- // PageLayout.Content // TODO: Account for pane width when centering content const Content = ({ as = 'main', 'aria-label': label, 'aria-labelledby': labelledBy, width = 'full', padding = 'none', hidden = false, children, className, style }) => { const Component = as; const { contentWrapperRef } = React.useContext(PageLayoutContext); return /*#__PURE__*/jsx(Component, { ref: contentWrapperRef, "aria-label": label, "aria-labelledby": labelledBy, style: style, className: clsx(classes.ContentWrapper, className), ...getResponsiveAttributes('is-hidden', hidden), children: /*#__PURE__*/jsx("div", { className: classes.Content, "data-width": width, style: { '--spacing': `var(--spacing-${padding})` }, children: children }) }); }; Content.displayName = "Content"; Content.displayName = 'PageLayout.Content'; // ---------------------------------------------------------------------------- // PageLayout.Pane const overflowProps = { tabIndex: 0, role: 'region' }; const Pane = /*#__PURE__*/React.forwardRef(({ 'aria-label': label, 'aria-labelledby': labelledBy, position: responsivePosition = 'end', positionWhenNarrow = 'inherit', width = 'medium', minWidth = 256, currentWidth: controlledWidth, onResizeEnd, padding = 'none', resizable = false, widthStorageKey = 'paneWidth', divider: responsiveDivider = 'none', dividerWhenNarrow = 'inherit', sticky = false, offsetHeader = 0, hidden: responsiveHidden = false, children, id, className, style }, forwardRef) => { // Combine position and positionWhenNarrow for backwards compatibility const positionProp = !isResponsiveValue(responsivePosition) && positionWhenNarrow !== 'inherit' ? { regular: responsivePosition, narrow: positionWhenNarrow } : responsivePosition; // Combine divider and dividerWhenNarrow for backwards compatibility const dividerProp = !isResponsiveValue(responsiveDivider) && dividerWhenNarrow !== 'inherit' ? { regular: responsiveDivider, narrow: dividerWhenNarrow } : responsiveDivider; // For components that need responsive values in JavaScript logic, we'll use a fallback value // The actual responsive behavior will be handled by CSS through data attributes const position = isResponsiveValue(positionProp) ? 'end' : positionProp; const dividerVariant = isResponsiveValue(dividerProp) ? 'none' : dividerProp; const { rowGap, columnGap, paneRef, contentWrapperRef } = React.useContext(PageLayoutContext); // Ref to the drag handle for updating ARIA attributes const handleRef = React.useRef(null); // Cache drag start values to calculate relative delta during drag // This approach is immune to layout shifts (scrollbars appearing/disappearing) const dragStartClientXRef = React.useRef(0); const dragStartWidthRef = React.useRef(0); // Cache max width at drag start - won't change during a drag operation const dragMaxWidthRef = React.useRef(0); const { currentWidth, currentWidthRef, minPaneWidth, maxPaneWidth, getMaxPaneWidth, saveWidth, getDefaultWidth } = usePaneWidth({ width, minWidth, resizable, widthStorageKey, paneRef, handleRef, contentWrapperRef, onResizeEnd, currentWidth: controlledWidth }); const mergedPaneRef = useMergedRefs(forwardRef, paneRef); const hasOverflow = useOverflow(paneRef); const paneId = useId(id); const labelProp = {}; if (hasOverflow) { process.env.NODE_ENV !== "production" ? warning(label === undefined && labelledBy === undefined, 'The <PageLayout.Pane> has overflow and `aria-label` or `aria-labelledby` has not been set. ' + 'Please provide `aria-label` or `aria-labelledby` to <PageLayout.Pane> in order to label this ' + 'region.') : void 0; if (labelledBy) { labelProp['aria-labelledby'] = labelledBy; } else if (label) { labelProp['aria-label'] = label; } } return /*#__PURE__*/jsxs("div", { className: clsx(classes.PaneWrapper, className), style: { '--offset-header': typeof offsetHeader === 'number' ? `${offsetHeader}px` : offsetHeader, '--spacing-row': `var(--spacing-${rowGap})`, '--spacing-column': `var(--spacing-${columnGap})`, ...style }, ...getResponsiveAttributes('is-hidden', responsiveHidden), ...getResponsiveAttributes('position', positionProp), "data-sticky": sticky || undefined, children: [/*#__PURE__*/jsx(HorizontalDivider, { variant: isResponsiveValue(dividerProp) ? dividerProp : { narrow: dividerVariant, regular: 'none' }, className: classes.PaneHorizontalDivider, style: { '--spacing': `var(--spacing-${rowGap})` }, position: positionProp }), /*#__PURE__*/jsx("div", { ref: mergedPaneRef // Suppress hydration mismatch for --pane-width when localStorage // provides a width that differs from the server-rendered default. // Not needed when onResizeEnd is provided (localStorage isn't read). , suppressHydrationWarning: resizable === true && !onResizeEnd, ...(hasOverflow ? overflowProps : {}), ...labelProp, ...(id && { id: paneId }), className: classes.Pane, "data-resizable": resizable || undefined, style: { '--spacing': `var(--spacing-${padding})`, '--pane-min-width': isCustomWidthOptions(width) ? width.min : `${minWidth}px`, '--pane-max-width': isCustomWidthOptions(width) ? width.max : `calc(100vw - var(--pane-max-width-diff))`, '--pane-width-custom': isCustomWidthOptions(width) ? width.default : undefined, '--pane-width-size': `var(--pane-width-${isPaneWidth(width) ? width : 'custom'})`, '--pane-width': `${currentWidth}px` }, children: children }), /*#__PURE__*/jsx(VerticalDivider, { variant: isResponsiveValue(dividerProp) ? { narrow: 'none', regular: resizable ? 'line' : dividerProp.regular || 'none', wide: resizable ? 'line' : dividerProp.wide || dividerProp.regular || 'none' } : { narrow: 'none', // If pane is resizable, always show a vertical divider on regular viewports regular: resizable ? 'line' : dividerVariant } // If pane is resizable, the divider should be draggable , draggable: resizable, position: positionProp, className: classes.PaneVerticalDivider, style: { '--spacing': `var(--spacing-${columnGap})` }, children: resizable ? /*#__PURE__*/jsx(DragHandle, { handleRef: handleRef, "aria-valuemin": minPaneWidth, "aria-valuemax": maxPaneWidth, "aria-valuenow": currentWidth, onDragStart: clientX => { var _paneRef$current$getB, _paneRef$current; // Cache cursor position and pane width at drag start // Using relative delta (current - start) is immune to layout shifts // (e.g., scrollbars appearing/disappearing during drag) dragStartClientXRef.current = clientX; dragStartWidthRef.current = (_paneRef$current$getB = (_paneRef$current = paneRef.current) === null || _paneRef$current === void 0 ? void 0 : _paneRef$current.getBoundingClientRect().width) !== null && _paneRef$current$getB !== void 0 ? _paneRef$current$getB : currentWidthRef.current; // Cache max width - won't change during drag dragMaxWidthRef.current = getMaxPaneWidth(); }, onDrag: (value, isKeyboard) => { // Use cached max width for pointer drag, fresh value for keyboard (less frequent) const maxWidth = isKeyboard ? getMaxPaneWidth() : dragMaxWidthRef.current; if (isKeyboard) { // Keyboard: value is a delta (e.g., +3 or -3) const delta = value; const newWidth = Math.max(minPaneWidth, Math.min(maxWidth, currentWidthRef.current + delta)); if (newWidth !== currentWidthRef.current) { var _paneRef$current2; currentWidthRef.current = newWidth; (_paneRef$current2 = paneRef.current) === null || _paneRef$current2 === void 0 ? void 0 : _paneRef$current2.style.setProperty('--pane-width', `${newWidth}px`); updateAriaValues(handleRef.current, { current: newWidth, max: maxWidth }); } } else { // Pointer: value is clientX - calculate width using relative delta from drag start // This approach is immune to layout shifts during drag if (paneRef.current) { const deltaX = value - dragStartClientXRef.current; // For position='end': cursor moving left (negative delta) increases width // For position='start': cursor moving right (positive delta) increases width const directedDelta = position === 'end' ? -deltaX : deltaX; const newWidth = dragStartWidthRef.current + directedDelta; const clampedWidth = Math.max(minPaneWidth, Math.min(maxWidth, newWidth)); // Only update if width actually changed if (Math.round(clampedWidth) !== Math.round(currentWidthRef.current)) { paneRef.current.style.setProperty('--pane-width', `${clampedWidth}px`); currentWidthRef.current = clampedWidth; updateAriaValues(handleRef.current, { current: Math.round(clampedWidth), max: maxWidth }); } } } }, onDragEnd: () => { // Sync React state so parent re-renders use the correct width saveWidth(currentWidthRef.current); }, onDoubleClick: () => { const resetWidth = getDefaultWidth(); if (paneRef.current) { paneRef.current.style.setProperty('--pane-width', `${resetWidth}px`); currentWidthRef.current = resetWidth; updateAriaValues(handleRef.current, { current: resetWidth }); } saveWidth(resetWidth); } }) : null })] }); }); Pane.displayName = 'PageLayout.Pane'; // ---------------------------------------------------------------------------- // PageLayout.Footer // ---------------------------------------------------------------------------- // PageLayout.Sidebar const Sidebar = /*#__PURE__*/React.forwardRef(({ 'aria-label': label, 'aria-labelledby': labelledBy, position = 'start', width = 'medium', minWidth = 256, padding = 'none', resizable = false, widthStorageKey, divider = 'none', sticky = false, responsiveVariant = 'default', hidden: responsiveHidden = false, children, id, className, style }, forwardRef) => { const { columnGap, sidebarRef, sidebarContentWrapperRef } = React.useContext(PageLayoutContext); // Ref to the drag handle for updating ARIA attributes const handleRef = React.useRef(null); // Cache drag start values to calculate relative delta during drag const dragStartClientXRef = React.useRef(0); const dragStartWidthRef = React.useRef(0); const dragMaxWidthRef = React.useRef(0); const { currentWidth, currentWidthRef, minPaneWidth, maxPaneWidth, getMaxPaneWidth, saveWidth, getDefaultWidth } = usePaneWidth({ width, minWidth, resizable, widthStorageKey, paneRef: sidebarRef, handleRef, contentWrapperRef: sidebarContentWrapperRef, constrainToViewport: true }); const mergedSidebarRef = useMergedRefs(forwardRef, sidebarRef); const hasOverflow = useOverflow(sidebarRef); const sidebarId = useId(id); const labelProp = {}; if (hasOverflow) { process.env.NODE_ENV !== "production" ? warning(label === undefined && labelledBy === undefined, 'The <PageLayout.Sidebar> has overflow and `aria-label` or `aria-labelledby` has not been set. ' + 'Please provide `aria-label` or `aria-labelledby` to <PageLayout.Sidebar> in order to label this ' + 'region.') : void 0; if (labelledBy) { labelProp['aria-labelledby'] = labelledBy; } else if (label) { labelProp['aria-label'] = label; } } return /*#__PURE__*/jsxs("div", { className: clsx(classes.SidebarWrapper, className), style: { '--spacing-column': `var(--spacing-${columnGap})`, ...style }, ...getResponsiveAttributes('is-hidden', responsiveHidden), "data-position": position, "data-sticky": sticky || undefined, "data-responsive-variant": responsiveVariant !== 'default' ? responsiveVariant : undefined, children: [position === 'end' && /*#__PURE__*/jsx(SidebarDivider, { position: position, divider: divider, resizable: resizable, minPaneWidth: minPaneWidth, maxPaneWidth: maxPaneWidth, currentWidth: currentWidth, currentWidthRef: currentWidthRef, handleRef: handleRef, sidebarRef: sidebarRef, dragStartClientXRef: dragStartClientXRef, dragStartWidthRef: dragStartWidthRef, dragMaxWidthRef: dragMaxWidthRef, getMaxPaneWidth: getMaxPaneWidth, getDefaultWidth: getDefaultWidth, saveWidth: saveWidth }), /*#__PURE__*/jsx("div", { ref: mergedSidebarRef // Suppress hydration mismatch for --pane-width when localStorage // provides a width that differs from the server-rendered default. , suppressHydrationWarning: resizable === true && !!widthStorageKey, ...(hasOverflow ? overflowProps : {}), ...labelProp, ...(id && { id: sidebarId }), className: classes.Sidebar, "data-resizable": resizable || undefined, style: { '--spacing': `var(--spacing-${padding})`, '--pane-min-width': isCustomWidthOptions(width) ? width.min : `${minWidth}px`, '--pane-max-width': isCustomWidthOptions(width) ? width.max : `calc(100vw - var(--sidebar-max-width-diff))`, '--pane-width-custom': isCustomWidthOptions(width) ? width.default : undefined, '--pane-width-size': `var(--pane-width-${isPaneWidth(width) ? width : 'custom'})`, '--pane-width': `${currentWidth}px` }, children: children }), position === 'start' && /*#__PURE__*/jsx(SidebarDivider, { position: position, divider: divider, resizable: resizable, minPaneWidth: minPaneWidth, maxPaneWidth: maxPaneWidth, currentWidth: currentWidth, currentWidthRef: currentWidthRef, handleRef: handleRef, sidebarRef: sidebarRef, dragStartClientXRef: dragStartClientXRef, dragStartWidthRef: dragStartWidthRef, dragMaxWidthRef: dragMaxWidthRef, getMaxPaneWidth: getMaxPaneWidth, getDefaultWidth: getDefaultWidth, saveWidth: saveWidth })] }); }); Sidebar.displayName = 'PageLayout.Sidebar'; // ---------------------------------------------------------------------------- // PageLayout.Footer const Footer = ({ 'aria-label': label, 'aria-labelledby': labelledBy, padding = 'none', divider = 'none', dividerWhenNarrow = 'inherit', hidden = false, children, className, style }) => { // Combine divider and dividerWhenNarrow for backwards compatibility const dividerProp = !isResponsiveValue(divider) && dividerWhenNarrow !== 'inherit' ? { regular: divider, narrow: dividerWhenNarrow } : divider; const { rowGap } = React.useContext(PageLayoutContext); return /*#__PURE__*/jsxs("footer", { "aria-label": label, "aria-labelledby": labelledBy, ...getResponsiveAttributes('hidden', hidden), className: clsx(classes.FooterWrapper, className), style: { '--spacing': `var(--spacing-${rowGap})`, ...style }, children: [/*#__PURE__*/jsx(HorizontalDivider, { className: classes.FooterHorizontalDivider, style: { '--spacing': `var(--spacing-${rowGap})` }, variant: dividerProp }), /*#__PURE__*/jsx("div", { className: classes.FooterContent, style: { '--spacing': `var(--spacing-${padding})` }, children: children })] }); }; Footer.displayName = "Footer"; Footer.displayName = 'PageLayout.Footer'; // ---------------------------------------------------------------------------- // Export const PageLayout = Object.assign(Root, { __SLOT__: Symbol('PageLayout'), Header, Content, Pane: Pane, Sidebar: Sidebar, Footer }); Header.__SLOT__ = Symbol('PageLayout.Header'); Content.__SLOT__ = Symbol('PageLayout.Content'); Pane.__SLOT__ = Symbol('PageLayout.Pane'); Sidebar.__SLOT__ = Symbol('PageLayout.Sidebar'); Footer.__SLOT__ = Symbol('PageLayout.Footer'); export { PageLayout };