@mezzanine-ui/react 1.0.0-rc.0 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/Badge/Badge.d.ts +4 -0
  2. package/Badge/Badge.js +2 -2
  3. package/Badge/typings.d.ts +13 -1
  4. package/Calendar/CalendarDays.js +4 -3
  5. package/Calendar/CalendarWeeks.js +4 -3
  6. package/Cascader/Cascader.d.ts +3 -0
  7. package/Cascader/Cascader.js +241 -0
  8. package/Cascader/CascaderPanel.d.ts +29 -0
  9. package/Cascader/CascaderPanel.js +29 -0
  10. package/Cascader/index.d.ts +5 -0
  11. package/Cascader/index.js +2 -0
  12. package/Cascader/typings.d.ts +92 -0
  13. package/Description/Description.d.ts +6 -1
  14. package/Description/Description.js +11 -4
  15. package/Description/DescriptionContent.d.ts +9 -3
  16. package/Description/DescriptionContent.js +4 -1
  17. package/Description/DescriptionContext.d.ts +6 -0
  18. package/Description/DescriptionContext.js +9 -0
  19. package/Description/index.d.ts +2 -0
  20. package/Description/index.js +1 -0
  21. package/Form/FormField.d.ts +6 -0
  22. package/Form/FormField.js +2 -2
  23. package/Form/FormHintText.d.ts +12 -0
  24. package/Form/FormHintText.js +3 -2
  25. package/Layout/Layout.d.ts +21 -5
  26. package/Layout/Layout.js +23 -19
  27. package/Layout/LayoutContext.d.ts +6 -6
  28. package/Layout/LayoutContext.js +2 -2
  29. package/Layout/LayoutHost.d.ts +0 -4
  30. package/Layout/LayoutHost.js +16 -13
  31. package/Layout/LayoutLeftPanel.d.ts +19 -0
  32. package/Layout/LayoutLeftPanel.js +86 -0
  33. package/Layout/LayoutMain.d.ts +10 -1
  34. package/Layout/LayoutMain.js +12 -3
  35. package/Layout/LayoutRightPanel.d.ts +19 -0
  36. package/Layout/{LayoutSidePanel.js → LayoutRightPanel.js} +32 -36
  37. package/Layout/index.d.ts +2 -2
  38. package/Layout/index.js +2 -1
  39. package/Modal/MediaPreviewModal.d.ts +4 -0
  40. package/Modal/MediaPreviewModal.js +2 -2
  41. package/Modal/Modal.d.ts +4 -0
  42. package/Modal/Modal.js +2 -2
  43. package/Modal/useModalContainer.js +0 -1
  44. package/Navigation/Navigation.d.ts +4 -0
  45. package/Navigation/Navigation.js +39 -3
  46. package/Navigation/NavigationFooter.js +19 -2
  47. package/Navigation/NavigationOption.d.ts +4 -0
  48. package/Navigation/NavigationOption.js +40 -19
  49. package/Navigation/NavigationOverflowMenuOption.js +11 -7
  50. package/Navigation/NavigationUserMenu.d.ts +1 -0
  51. package/Navigation/NavigationUserMenu.js +24 -5
  52. package/Navigation/context.d.ts +2 -0
  53. package/Navigation/context.js +4 -1
  54. package/Picker/RangePickerTrigger.js +1 -1
  55. package/Section/Section.js +6 -6
  56. package/Transition/Collapse.d.ts +2 -1
  57. package/Transition/Collapse.js +2 -1
  58. package/Upload/Upload.js +63 -9
  59. package/Upload/UploadPictureCard.d.ts +25 -15
  60. package/Upload/UploadPictureCard.js +14 -6
  61. package/index.d.ts +4 -2
  62. package/index.js +4 -1
  63. package/package.json +4 -4
  64. package/Layout/LayoutSidePanel.d.ts +0 -14
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ const DescriptionContext = createContext({
5
+ size: 'main',
6
+ });
7
+ const useDescriptionContext = () => useContext(DescriptionContext);
8
+
9
+ export { DescriptionContext, useDescriptionContext };
@@ -6,3 +6,5 @@ export { default as DescriptionContent } from './DescriptionContent';
6
6
  export type { DescriptionContentProps } from './DescriptionContent';
7
7
  export { default as DescriptionGroup } from './DescriptionGroup';
8
8
  export type { DescriptionGroupProps } from './DescriptionGroup';
9
+ export { DescriptionContext } from './DescriptionContext';
10
+ export type { DescriptionContextValue } from './DescriptionContext';
@@ -2,3 +2,4 @@ export { default as Description } from './Description.js';
2
2
  export { default as DescriptionTitle } from './DescriptionTitle.js';
3
3
  export { default as DescriptionContent } from './DescriptionContent.js';
4
4
  export { default as DescriptionGroup } from './DescriptionGroup.js';
5
+ export { DescriptionContext } from './DescriptionContext.js';
@@ -37,6 +37,12 @@ export interface FormFieldProps extends NativeElementPropsWithoutKeyAndRef<'div'
37
37
  * The icon to display alongside the hint text.
38
38
  */
39
39
  hintTextIcon?: IconDefinition;
40
+ /**
41
+ * Whether to display the hint text icon.
42
+ * When false, neither the custom icon nor the default severity icon will be shown.
43
+ * @default true
44
+ */
45
+ showHintTextIcon?: boolean;
40
46
  /**
41
47
  * The label text for the form field.
42
48
  */
package/Form/FormField.js CHANGED
@@ -10,7 +10,7 @@ import cx from 'clsx';
10
10
  * The React component for `mezzanine` form field.
11
11
  */
12
12
  const FormField = forwardRef(function FormField(props, ref) {
13
- const { children, className, counter, counterColor, controlFieldSlotLayout = ControlFieldSlotLayout.MAIN, density, disabled = false, fullWidth = false, hintText, hintTextIcon, label, labelInformationIcon, labelInformationText, labelOptionalMarker, labelSpacing = FormFieldLabelSpacing.MAIN, layout = FormFieldLayout.HORIZONTAL, name, required = false, severity = 'info', ...rest } = props;
13
+ const { children, className, counter, counterColor, controlFieldSlotLayout = ControlFieldSlotLayout.MAIN, density, disabled = false, fullWidth = false, hintText, hintTextIcon, showHintTextIcon, label, labelInformationIcon, labelInformationText, labelOptionalMarker, labelSpacing = FormFieldLabelSpacing.MAIN, layout = FormFieldLayout.HORIZONTAL, name, required = false, severity = 'info', ...rest } = props;
14
14
  const formControl = {
15
15
  disabled,
16
16
  fullWidth,
@@ -30,7 +30,7 @@ const FormField = forwardRef(function FormField(props, ref) {
30
30
  [formFieldClasses.fullWidth]: fullWidth,
31
31
  }, className), children: jsxs(FormControlContext.Provider, { value: formControl, children: [label && (jsx(FormLabel, { className: cx(formFieldClasses.labelArea, labelSpacingClass), htmlFor: name, informationIcon: labelInformationIcon, informationText: labelInformationText, labelText: label, optionalMarker: labelOptionalMarker })), jsxs("div", { className: cx(formFieldClasses.dataEntry), children: [jsx("div", { className: cx(`${formFieldClasses.controlFieldSlot}--${controlFieldSlotLayout}`), children: children }), hintText || hintTextIcon || counter ? (jsxs("div", { className: cx(formFieldClasses.hintTextAndCounterArea, {
32
32
  [formFieldClasses.hintTextAndCounterArea + '--align-right']: !(hintText || hintTextIcon) && counter,
33
- }), children: [(hintText || hintTextIcon) && (jsx(FormHintText, { hintText: hintText, hintTextIcon: hintTextIcon, severity: severity })), counter && (jsx("span", { className: cx(formFieldClasses.counter, formFieldClasses.counterColor(counterColor || FormFieldCounterColor.INFO)), children: counter }))] })) : null] })] }) }));
33
+ }), children: [(hintText || hintTextIcon) && (jsx(FormHintText, { hintText: hintText, hintTextIcon: hintTextIcon, severity: severity, showHintTextIcon: showHintTextIcon })), counter && (jsx("span", { className: cx(formFieldClasses.counter, formFieldClasses.counterColor(counterColor || FormFieldCounterColor.INFO)), children: counter }))] })) : null] })] }) }));
34
34
  });
35
35
 
36
36
  export { FormField as default };
@@ -17,6 +17,12 @@ export type FormHintTextProps = NativeElementPropsWithoutKeyAndRef<'span'> & {
17
17
  * if not provided, no icon will be displayed.
18
18
  */
19
19
  severity?: keyof typeof formHintIcons | undefined;
20
+ /**
21
+ * Whether to display the hint text icon.
22
+ * When false, neither the custom icon nor the default severity icon will be shown.
23
+ * @default true
24
+ */
25
+ showHintTextIcon?: boolean;
20
26
  };
21
27
  /**
22
28
  * The React component for `mezzanine` form message.
@@ -37,5 +43,11 @@ declare const FormHintText: import("react").ForwardRefExoticComponent<NativeElem
37
43
  * if not provided, no icon will be displayed.
38
44
  */
39
45
  severity?: keyof typeof formHintIcons | undefined;
46
+ /**
47
+ * Whether to display the hint text icon.
48
+ * When false, neither the custom icon nor the default severity icon will be shown.
49
+ * @default true
50
+ */
51
+ showHintTextIcon?: boolean;
40
52
  } & import("react").RefAttributes<HTMLSpanElement>>;
41
53
  export default FormHintText;
@@ -9,9 +9,10 @@ import cx from 'clsx';
9
9
  * The React component for `mezzanine` form message.
10
10
  */
11
11
  const FormHintText = forwardRef(function FormHintText(props, ref) {
12
- const { className, hintText, hintTextIcon, severity = 'info', ...rest } = props;
12
+ const { className, hintText, hintTextIcon, severity = 'info', showHintTextIcon = true, ...rest } = props;
13
13
  const defaultIcon = severity ? formHintIcons[severity] : null;
14
- return (jsxs("span", { ...rest, ref: ref, className: cx(formFieldClasses.hintText, severity ? formFieldClasses.hintTextSeverity(severity) : undefined, className), children: [hintTextIcon ? (jsx(Icon, { className: formFieldClasses.hintTextIcon, icon: hintTextIcon, color: severity })) : (defaultIcon && (jsx(Icon, { className: formFieldClasses.hintTextIcon, icon: defaultIcon, color: severity }))), hintText] }));
14
+ return (jsxs("span", { ...rest, ref: ref, className: cx(formFieldClasses.hintText, severity ? formFieldClasses.hintTextSeverity(severity) : undefined, className), children: [showHintTextIcon &&
15
+ (hintTextIcon ? (jsx(Icon, { className: formFieldClasses.hintTextIcon, icon: hintTextIcon, color: severity })) : (defaultIcon && (jsx(Icon, { className: formFieldClasses.hintTextIcon, icon: defaultIcon, color: severity })))), hintText] }));
15
16
  });
16
17
 
17
18
  export { FormHintText as default };
@@ -1,16 +1,32 @@
1
1
  import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
2
+ import { LayoutLeftPanel, LayoutLeftPanelProps } from './LayoutLeftPanel';
2
3
  import { LayoutMain, LayoutMainProps } from './LayoutMain';
3
- import { LayoutSidePanel, LayoutSidePanelProps } from './LayoutSidePanel';
4
+ import { LayoutRightPanel, LayoutRightPanelProps } from './LayoutRightPanel';
5
+ export { LayoutLeftPanel };
6
+ export type { LayoutLeftPanelProps };
4
7
  export { LayoutMain };
5
8
  export type { LayoutMainProps };
6
- export { LayoutSidePanel };
7
- export type { LayoutSidePanelProps };
9
+ export { LayoutRightPanel };
10
+ export type { LayoutRightPanelProps };
8
11
  export interface LayoutProps extends NativeElementPropsWithoutKeyAndRef<'div'> {
9
- /** The slot children: use `Layout.Main` and `Layout.SidePanel` as direct children. */
12
+ /**
13
+ * Slot children. Place `<Navigation>`, `<Layout.LeftPanel>`, `<Layout.Main>`,
14
+ * and `<Layout.RightPanel>` as direct children in any order.
15
+ * They will be re-ordered into the correct DOM sequence automatically.
16
+ */
10
17
  children?: React.ReactNode;
18
+ /**
19
+ * Additional class name applied to the content wrapper element.
20
+ */
21
+ contentWrapperClassName?: string;
22
+ /**
23
+ * Additional class name applied to the navigation wrapper element. Only has effect if a `<Navigation>` component is provided as a child.
24
+ */
25
+ navigationClassName?: string;
11
26
  }
12
27
  declare const LayoutWithSubComponents: import("react").ForwardRefExoticComponent<LayoutProps & import("react").RefAttributes<HTMLDivElement>> & {
28
+ LeftPanel: typeof LayoutLeftPanel;
13
29
  Main: typeof LayoutMain;
14
- SidePanel: typeof LayoutSidePanel;
30
+ RightPanel: typeof LayoutRightPanel;
15
31
  };
16
32
  export default LayoutWithSubComponents;
package/Layout/Layout.js CHANGED
@@ -2,36 +2,40 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { forwardRef, Children, isValidElement } from 'react';
3
3
  import { layoutClasses } from '@mezzanine-ui/core/layout';
4
4
  import { LayoutHost } from './LayoutHost.js';
5
+ import { LayoutLeftPanel } from './LayoutLeftPanel.js';
5
6
  import { LayoutMain } from './LayoutMain.js';
6
- import { LayoutSidePanel } from './LayoutSidePanel.js';
7
+ import { LayoutRightPanel } from './LayoutRightPanel.js';
8
+ import Navigation from '../Navigation/Navigation.js';
9
+ import cx from 'clsx';
7
10
 
8
11
  const Layout = forwardRef(function Layout(props, ref) {
9
- const { children, ...rest } = props;
10
- let mainContent = null;
11
- let sidePanelNode = null;
12
- let initialOpen = false;
13
- let initialSidePanelWidth = 320;
12
+ const { children, contentWrapperClassName, navigationClassName, ...rest } = props;
13
+ let navigationNode = null;
14
+ let leftPanelNode = null;
15
+ let mainNode = null;
16
+ let rightPanelNode = null;
14
17
  Children.forEach(children, (child) => {
15
- var _a, _b;
16
18
  if (!isValidElement(child))
17
19
  return;
18
- if (child.type === LayoutMain) {
19
- mainContent = child.props
20
- .children;
20
+ if (child.type === Navigation) {
21
+ navigationNode = child;
21
22
  }
22
- else if (child.type === LayoutSidePanel) {
23
- const sidePanelChild = child;
24
- sidePanelNode = sidePanelChild;
25
- initialOpen = (_a = sidePanelChild.props.open) !== null && _a !== void 0 ? _a : false;
26
- initialSidePanelWidth =
27
- (_b = sidePanelChild.props.defaultSidePanelWidth) !== null && _b !== void 0 ? _b : 320;
23
+ else if (child.type === LayoutLeftPanel) {
24
+ leftPanelNode = child;
25
+ }
26
+ else if (child.type === LayoutMain) {
27
+ mainNode = child;
28
+ }
29
+ else if (child.type === LayoutRightPanel) {
30
+ rightPanelNode = child;
28
31
  }
29
32
  });
30
- return (jsxs(LayoutHost, { ...rest, initialOpen: initialOpen, initialSidePanelWidth: initialSidePanelWidth, ref: ref, children: [jsx("main", { className: layoutClasses.main, children: mainContent }), sidePanelNode] }));
33
+ return (jsxs(LayoutHost, { ...rest, ref: ref, children: [navigationNode && (jsx("div", { className: cx(layoutClasses.navigation, navigationClassName), children: navigationNode })), jsxs("div", { className: cx(layoutClasses.contentWrapper, contentWrapperClassName), children: [leftPanelNode, mainNode, rightPanelNode] })] }));
31
34
  });
32
35
  const LayoutWithSubComponents = Object.assign(Layout, {
36
+ LeftPanel: LayoutLeftPanel,
33
37
  Main: LayoutMain,
34
- SidePanel: LayoutSidePanel,
38
+ RightPanel: LayoutRightPanel,
35
39
  });
36
40
 
37
- export { LayoutMain, LayoutSidePanel, LayoutWithSubComponents as default };
41
+ export { LayoutLeftPanel, LayoutMain, LayoutRightPanel, LayoutWithSubComponents as default };
@@ -1,7 +1,7 @@
1
- export interface LayoutPanelContextValue {
2
- onPanelStateChange: (state: {
3
- isOpen: boolean;
4
- width: number;
5
- }) => void;
1
+ import { RefObject } from 'react';
2
+ export interface LayoutContextValue {
3
+ hostRef: RefObject<HTMLDivElement | null>;
4
+ mainRef: RefObject<HTMLDivElement | null>;
5
+ registerMain: (el: HTMLDivElement | null) => void;
6
6
  }
7
- export declare const LayoutPanelContext: import("react").Context<LayoutPanelContextValue | undefined>;
7
+ export declare const LayoutContext: import("react").Context<LayoutContextValue | null>;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { createContext } from 'react';
3
3
 
4
- const LayoutPanelContext = createContext(undefined);
4
+ const LayoutContext = createContext(null);
5
5
 
6
- export { LayoutPanelContext };
6
+ export { LayoutContext };
@@ -2,9 +2,5 @@ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
2
2
  export interface LayoutHostProps extends NativeElementPropsWithoutKeyAndRef<'div'> {
3
3
  /** The content rendered inside the layout host. */
4
4
  children?: React.ReactNode;
5
- /** SSR hint: initial open state read from LayoutSidePanel.props.open */
6
- initialOpen?: boolean;
7
- /** SSR hint: initial width read from LayoutSidePanel.props.defaultSidePanelWidth */
8
- initialSidePanelWidth?: number;
9
5
  }
10
6
  export declare const LayoutHost: import("react").ForwardRefExoticComponent<LayoutHostProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,23 +1,26 @@
1
1
  'use client';
2
2
  import { jsx } from 'react/jsx-runtime';
3
- import { forwardRef, useState, useCallback, useMemo } from 'react';
3
+ import { forwardRef, useRef, useCallback, useMemo } from 'react';
4
4
  import { layoutClasses } from '@mezzanine-ui/core/layout';
5
- import { LayoutPanelContext } from './LayoutContext.js';
5
+ import { LayoutContext } from './LayoutContext.js';
6
6
  import cx from 'clsx';
7
7
 
8
- const MIN_PANEL_WIDTH = 240;
9
8
  const LayoutHost = forwardRef(function LayoutHost(props, ref) {
10
- const { children, className, initialOpen = false, initialSidePanelWidth = 320, style, ...rest } = props;
11
- const [isOpen, setIsOpen] = useState(initialOpen);
12
- const [sidePanelWidth, setSidePanelWidth] = useState(() => Math.max(MIN_PANEL_WIDTH, initialSidePanelWidth));
13
- const onPanelStateChange = useCallback(({ isOpen: nextIsOpen, width }) => {
14
- setIsOpen(nextIsOpen);
15
- setSidePanelWidth(width);
9
+ const { children, className, ...rest } = props;
10
+ const hostRef = useRef(null);
11
+ const mainRef = useRef(null);
12
+ const registerMain = useCallback((el) => {
13
+ mainRef.current = el;
16
14
  }, []);
17
- const contextValue = useMemo(() => ({ onPanelStateChange }), [onPanelStateChange]);
18
- return (jsx(LayoutPanelContext.Provider, { value: contextValue, children: jsx("div", { ...rest, className: cx(layoutClasses.host, { [layoutClasses.hostOpen]: isOpen }, className), ref: ref, style: {
19
- ...style,
20
- '--mzn-layout-side-panel-width': `${sidePanelWidth}px`,
15
+ const contextValue = useMemo(() => ({ hostRef, mainRef, registerMain }), [registerMain]);
16
+ return (jsx(LayoutContext.Provider, { value: contextValue, children: jsx("div", { ...rest, className: cx(layoutClasses.host, className), ref: (node) => {
17
+ hostRef.current = node;
18
+ if (typeof ref === 'function') {
19
+ ref(node);
20
+ }
21
+ else if (ref) {
22
+ ref.current = node;
23
+ }
21
24
  }, children: children }) }));
22
25
  });
23
26
 
@@ -0,0 +1,19 @@
1
+ import { ScrollbarProps } from '../Scrollbar';
2
+ export interface LayoutLeftPanelProps {
3
+ /** The content rendered inside the left panel. */
4
+ children?: React.ReactNode;
5
+ /** Additional class name applied to the panel element. */
6
+ className?: string;
7
+ /** Initial width (in px) of the panel. Clamped to a minimum of 240px. */
8
+ defaultWidth?: number;
9
+ /** Callback fired when the panel width changes during resize. */
10
+ onWidthChange?: (width: number) => void;
11
+ /** Controls whether the panel and its divider are visible. */
12
+ open?: boolean;
13
+ /** Props passed to the internal `Scrollbar` component. */
14
+ scrollbarProps?: Omit<ScrollbarProps, 'children'>;
15
+ }
16
+ export declare function LayoutLeftPanel({ children, className, defaultWidth, onWidthChange, open, scrollbarProps, }: LayoutLeftPanelProps): import("react/jsx-runtime").JSX.Element | null;
17
+ export declare namespace LayoutLeftPanel {
18
+ var displayName: string;
19
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { useContext, useState, useRef, useEffect, useCallback } from 'react';
4
+ import { layoutClasses } from '@mezzanine-ui/core/layout';
5
+ import { useDocumentEvents } from '../hooks/useDocumentEvents.js';
6
+ import { LayoutContext } from './LayoutContext.js';
7
+ import Scrollbar from '../Scrollbar/Scrollbar.js';
8
+ import cx from 'clsx';
9
+
10
+ const MIN_PANEL_WIDTH = 240;
11
+ const CONTENT_WRAPPER_MIN_WIDTH = 480;
12
+ const ARROW_KEY_STEP = 10;
13
+ function LayoutLeftPanel({ children, className, defaultWidth = 320, onWidthChange, open = false, scrollbarProps = {}, }) {
14
+ const context = useContext(LayoutContext);
15
+ const [isDragging, setIsDragging] = useState(false);
16
+ const [width, setWidth] = useState(() => Math.max(MIN_PANEL_WIDTH, defaultWidth));
17
+ const dragStartRef = useRef(null);
18
+ const rafIdRef = useRef(null);
19
+ useEffect(() => {
20
+ return () => {
21
+ if (rafIdRef.current !== null) {
22
+ window.cancelAnimationFrame(rafIdRef.current);
23
+ rafIdRef.current = null;
24
+ }
25
+ };
26
+ }, []);
27
+ const handleDividerMouseDown = useCallback((e) => {
28
+ var _a;
29
+ e.preventDefault();
30
+ const mainWidth = ((_a = context === null || context === void 0 ? void 0 : context.mainRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) || window.innerWidth;
31
+ const maxWidth = width + Math.max(0, mainWidth - CONTENT_WRAPPER_MIN_WIDTH);
32
+ setIsDragging(true);
33
+ dragStartRef.current = { maxWidth, width, x: e.clientX };
34
+ }, [context, width]);
35
+ const handleDividerKeyDown = useCallback((e) => {
36
+ var _a;
37
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')
38
+ return;
39
+ e.preventDefault();
40
+ const mainWidth = ((_a = context === null || context === void 0 ? void 0 : context.mainRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) || window.innerWidth;
41
+ const maxWidth = width + Math.max(0, mainWidth - CONTENT_WRAPPER_MIN_WIDTH);
42
+ const step = e.key === 'ArrowRight' ? ARROW_KEY_STEP : -ARROW_KEY_STEP;
43
+ const newWidth = Math.min(maxWidth, Math.max(MIN_PANEL_WIDTH, width + step));
44
+ setWidth(newWidth);
45
+ onWidthChange === null || onWidthChange === void 0 ? void 0 : onWidthChange(newWidth);
46
+ }, [context, onWidthChange, width]);
47
+ useDocumentEvents(() => {
48
+ if (!isDragging)
49
+ return undefined;
50
+ return {
51
+ mousemove: (e) => {
52
+ if (!dragStartRef.current)
53
+ return;
54
+ if (rafIdRef.current !== null) {
55
+ window.cancelAnimationFrame(rafIdRef.current);
56
+ }
57
+ rafIdRef.current = window.requestAnimationFrame(() => {
58
+ rafIdRef.current = null;
59
+ if (!dragStartRef.current)
60
+ return;
61
+ const delta = e.clientX - dragStartRef.current.x;
62
+ const newWidth = dragStartRef.current.width + delta;
63
+ const clamped = Math.min(dragStartRef.current.maxWidth, Math.max(MIN_PANEL_WIDTH, newWidth));
64
+ setWidth(clamped);
65
+ onWidthChange === null || onWidthChange === void 0 ? void 0 : onWidthChange(clamped);
66
+ });
67
+ },
68
+ mouseup: () => {
69
+ if (rafIdRef.current !== null) {
70
+ window.cancelAnimationFrame(rafIdRef.current);
71
+ rafIdRef.current = null;
72
+ }
73
+ setIsDragging(false);
74
+ dragStartRef.current = null;
75
+ },
76
+ };
77
+ }, [isDragging, onWidthChange]);
78
+ if (!open)
79
+ return null;
80
+ return (jsxs("aside", { "aria-label": "Left panel", className: cx(layoutClasses.sidePanel, layoutClasses.sidePanelLeft, className), style: { inlineSize: width }, children: [jsx("div", { className: layoutClasses.sidePanelContent, children: jsx(Scrollbar, { ...scrollbarProps, children: children }) }), jsx("div", { "aria-label": "Resize left panel", "aria-orientation": "vertical", "aria-valuemin": MIN_PANEL_WIDTH, "aria-valuenow": width, className: cx(layoutClasses.divider, {
81
+ [layoutClasses.dividerDragging]: isDragging,
82
+ }), onKeyDown: handleDividerKeyDown, onMouseDown: handleDividerMouseDown, role: "separator", tabIndex: 0 })] }));
83
+ }
84
+ LayoutLeftPanel.displayName = 'Layout.LeftPanel';
85
+
86
+ export { LayoutLeftPanel };
@@ -1,8 +1,17 @@
1
+ import { ScrollbarProps } from '../Scrollbar';
1
2
  export interface LayoutMainProps {
2
3
  /** The content rendered inside the main area. */
3
4
  children?: React.ReactNode;
5
+ /**
6
+ * Additional class name applied to the main element.
7
+ */
8
+ className?: string;
9
+ /**
10
+ * Props passed to the internal `Scrollbar` component. If not provided, the main area will still be scrollable but without the custom scrollbar styling and behavior.
11
+ */
12
+ scrollbarProps?: Omit<ScrollbarProps, 'children'>;
4
13
  }
5
- export declare function LayoutMain({ children }: LayoutMainProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function LayoutMain(props: LayoutMainProps): import("react/jsx-runtime").JSX.Element;
6
15
  export declare namespace LayoutMain {
7
16
  var displayName: string;
8
17
  }
@@ -1,7 +1,16 @@
1
- import { jsx, Fragment } from 'react/jsx-runtime';
1
+ 'use client';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { useContext } from 'react';
4
+ import { layoutClasses } from '@mezzanine-ui/core/layout';
5
+ import { LayoutContext } from './LayoutContext.js';
6
+ import Scrollbar from '../Scrollbar/Scrollbar.js';
7
+ import cx from 'clsx';
2
8
 
3
- function LayoutMain({ children }) {
4
- return jsx(Fragment, { children: children });
9
+ function LayoutMain(props) {
10
+ var _a;
11
+ const { children, className, scrollbarProps = {} } = props;
12
+ const context = useContext(LayoutContext);
13
+ return (jsx("div", { ref: (_a = context === null || context === void 0 ? void 0 : context.registerMain) !== null && _a !== void 0 ? _a : null, className: cx(layoutClasses.main, className), children: jsx(Scrollbar, { ...scrollbarProps, children: jsx("div", { className: layoutClasses.mainContent, children: children }) }) }));
5
14
  }
6
15
  LayoutMain.displayName = 'Layout.Main';
7
16
 
@@ -0,0 +1,19 @@
1
+ import { ScrollbarProps } from '../Scrollbar';
2
+ export interface LayoutRightPanelProps {
3
+ /** The content rendered inside the right panel. */
4
+ children?: React.ReactNode;
5
+ /** Additional class name applied to the panel element. */
6
+ className?: string;
7
+ /** Initial width (in px) of the panel. Clamped to a minimum of 240px. */
8
+ defaultWidth?: number;
9
+ /** Callback fired when the panel width changes during resize. */
10
+ onWidthChange?: (width: number) => void;
11
+ /** Controls whether the panel and its divider are visible. */
12
+ open?: boolean;
13
+ /** Props passed to the internal `Scrollbar` component. */
14
+ scrollbarProps?: Omit<ScrollbarProps, 'children'>;
15
+ }
16
+ export declare function LayoutRightPanel({ children, className, defaultWidth, onWidthChange, open, scrollbarProps, }: LayoutRightPanelProps): import("react/jsx-runtime").JSX.Element | null;
17
+ export declare namespace LayoutRightPanel {
18
+ var displayName: string;
19
+ }
@@ -1,22 +1,19 @@
1
1
  'use client';
2
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { useContext, useState, useRef, useEffect, useCallback } from 'react';
4
4
  import { layoutClasses } from '@mezzanine-ui/core/layout';
5
5
  import { useDocumentEvents } from '../hooks/useDocumentEvents.js';
6
- import { LayoutPanelContext } from './LayoutContext.js';
6
+ import { LayoutContext } from './LayoutContext.js';
7
+ import Scrollbar from '../Scrollbar/Scrollbar.js';
7
8
  import cx from 'clsx';
8
9
 
9
10
  const MIN_PANEL_WIDTH = 240;
11
+ const CONTENT_WRAPPER_MIN_WIDTH = 480;
10
12
  const ARROW_KEY_STEP = 10;
11
- function LayoutSidePanel({ children, defaultSidePanelWidth = 320, onSidePanelWidthChange, open = false, }) {
12
- const context = useContext(LayoutPanelContext);
13
+ function LayoutRightPanel({ children, className, defaultWidth = 320, onWidthChange, open = false, scrollbarProps = {}, }) {
14
+ const context = useContext(LayoutContext);
13
15
  const [isDragging, setIsDragging] = useState(false);
14
- const [sidePanelWidth, setSidePanelWidth] = useState(() => {
15
- const maxWidth = typeof window !== 'undefined'
16
- ? window.innerWidth - MIN_PANEL_WIDTH - 1
17
- : Infinity;
18
- return Math.min(maxWidth, Math.max(MIN_PANEL_WIDTH, defaultSidePanelWidth));
19
- });
16
+ const [width, setWidth] = useState(() => Math.max(MIN_PANEL_WIDTH, defaultWidth));
20
17
  const dragStartRef = useRef(null);
21
18
  const rafIdRef = useRef(null);
22
19
  useEffect(() => {
@@ -27,24 +24,26 @@ function LayoutSidePanel({ children, defaultSidePanelWidth = 320, onSidePanelWid
27
24
  }
28
25
  };
29
26
  }, []);
30
- useEffect(() => {
31
- context === null || context === void 0 ? void 0 : context.onPanelStateChange({ isOpen: open, width: sidePanelWidth });
32
- }, [context, open, sidePanelWidth]);
27
+ const handleDividerMouseDown = useCallback((e) => {
28
+ var _a;
29
+ e.preventDefault();
30
+ const mainWidth = ((_a = context === null || context === void 0 ? void 0 : context.mainRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) || window.innerWidth;
31
+ const maxWidth = width + Math.max(0, mainWidth - CONTENT_WRAPPER_MIN_WIDTH);
32
+ setIsDragging(true);
33
+ dragStartRef.current = { maxWidth, width, x: e.clientX };
34
+ }, [context, width]);
33
35
  const handleDividerKeyDown = useCallback((e) => {
36
+ var _a;
34
37
  if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')
35
38
  return;
36
39
  e.preventDefault();
40
+ const mainWidth = ((_a = context === null || context === void 0 ? void 0 : context.mainRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) || window.innerWidth;
41
+ const maxWidth = width + Math.max(0, mainWidth - CONTENT_WRAPPER_MIN_WIDTH);
37
42
  const step = e.key === 'ArrowLeft' ? ARROW_KEY_STEP : -ARROW_KEY_STEP;
38
- const maxWidth = window.innerWidth - MIN_PANEL_WIDTH - 1;
39
- const newWidth = Math.min(maxWidth, Math.max(MIN_PANEL_WIDTH, sidePanelWidth + step));
40
- setSidePanelWidth(newWidth);
41
- onSidePanelWidthChange === null || onSidePanelWidthChange === void 0 ? void 0 : onSidePanelWidthChange(newWidth);
42
- }, [onSidePanelWidthChange, sidePanelWidth]);
43
- const handleDividerMouseDown = useCallback((e) => {
44
- e.preventDefault();
45
- setIsDragging(true);
46
- dragStartRef.current = { width: sidePanelWidth, x: e.clientX };
47
- }, [sidePanelWidth]);
43
+ const newWidth = Math.min(maxWidth, Math.max(MIN_PANEL_WIDTH, width + step));
44
+ setWidth(newWidth);
45
+ onWidthChange === null || onWidthChange === void 0 ? void 0 : onWidthChange(newWidth);
46
+ }, [context, onWidthChange, width]);
48
47
  useDocumentEvents(() => {
49
48
  if (!isDragging)
50
49
  return undefined;
@@ -59,12 +58,11 @@ function LayoutSidePanel({ children, defaultSidePanelWidth = 320, onSidePanelWid
59
58
  rafIdRef.current = null;
60
59
  if (!dragStartRef.current)
61
60
  return;
62
- const delta = dragStartRef.current.x - e.clientX;
63
- const newWidth = dragStartRef.current.width + delta;
64
- const maxWidth = window.innerWidth - MIN_PANEL_WIDTH - 1;
65
- const clamped = Math.min(maxWidth, Math.max(MIN_PANEL_WIDTH, newWidth));
66
- setSidePanelWidth(clamped);
67
- onSidePanelWidthChange === null || onSidePanelWidthChange === void 0 ? void 0 : onSidePanelWidthChange(clamped);
61
+ const delta = e.clientX - dragStartRef.current.x;
62
+ const newWidth = dragStartRef.current.width - delta;
63
+ const clamped = Math.min(dragStartRef.current.maxWidth, Math.max(MIN_PANEL_WIDTH, newWidth));
64
+ setWidth(clamped);
65
+ onWidthChange === null || onWidthChange === void 0 ? void 0 : onWidthChange(clamped);
68
66
  });
69
67
  },
70
68
  mouseup: () => {
@@ -76,15 +74,13 @@ function LayoutSidePanel({ children, defaultSidePanelWidth = 320, onSidePanelWid
76
74
  dragStartRef.current = null;
77
75
  },
78
76
  };
79
- }, [isDragging, onSidePanelWidthChange]);
77
+ }, [isDragging, onWidthChange]);
80
78
  if (!open)
81
79
  return null;
82
- return (jsxs(Fragment, { children: [jsx("div", { "aria-label": "Resize side panel", "aria-orientation": "vertical", "aria-valuemax": typeof window !== 'undefined'
83
- ? window.innerWidth - MIN_PANEL_WIDTH - 1
84
- : undefined, "aria-valuemin": MIN_PANEL_WIDTH, "aria-valuenow": sidePanelWidth, className: cx(layoutClasses.divider, {
80
+ return (jsxs("aside", { "aria-label": "Right panel", className: cx(layoutClasses.sidePanel, layoutClasses.sidePanelRight, className), style: { inlineSize: width }, children: [jsx("div", { "aria-label": "Resize right panel", "aria-orientation": "vertical", "aria-valuemin": MIN_PANEL_WIDTH, "aria-valuenow": width, className: cx(layoutClasses.divider, {
85
81
  [layoutClasses.dividerDragging]: isDragging,
86
- }), onKeyDown: handleDividerKeyDown, onMouseDown: handleDividerMouseDown, role: "separator", tabIndex: 0 }), jsx("aside", { "aria-label": "Side panel", className: layoutClasses.sidePanel, children: children })] }));
82
+ }), onKeyDown: handleDividerKeyDown, onMouseDown: handleDividerMouseDown, role: "separator", tabIndex: 0 }), jsx("div", { className: layoutClasses.sidePanelContent, children: jsx(Scrollbar, { ...scrollbarProps, children: children }) })] }));
87
83
  }
88
- LayoutSidePanel.displayName = 'Layout.SidePanel';
84
+ LayoutRightPanel.displayName = 'Layout.RightPanel';
89
85
 
90
- export { LayoutSidePanel };
86
+ export { LayoutRightPanel };
package/Layout/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { default, LayoutMain, LayoutSidePanel } from './Layout';
2
- export type { LayoutMainProps, LayoutProps, LayoutSidePanelProps, } from './Layout';
1
+ export { default, LayoutLeftPanel, LayoutMain, LayoutRightPanel, } from './Layout';
2
+ export type { LayoutLeftPanelProps, LayoutMainProps, LayoutProps, LayoutRightPanelProps, } from './Layout';
3
3
  export type { LayoutHostProps } from './LayoutHost';
package/Layout/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default } from './Layout.js';
2
+ export { LayoutLeftPanel } from './LayoutLeftPanel.js';
2
3
  export { LayoutMain } from './LayoutMain.js';
3
- export { LayoutSidePanel } from './LayoutSidePanel.js';
4
+ export { LayoutRightPanel } from './LayoutRightPanel.js';
@@ -1,6 +1,10 @@
1
1
  import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
2
2
  import { ModalContainerProps } from './useModalContainer';
3
3
  export interface MediaPreviewModalProps extends Omit<ModalContainerProps, 'children'>, NativeElementPropsWithoutKeyAndRef<'div'> {
4
+ /**
5
+ * The custom class name applied to the modal container.
6
+ */
7
+ backdropClassName?: string;
4
8
  /**
5
9
  * The current index of the media being displayed (controlled mode).
6
10
  * If provided along with onNext/onPrev, the component operates in controlled mode.
@@ -15,7 +15,7 @@ import cx from 'clsx';
15
15
  * Displays media items with navigation controls and a close button.
16
16
  */
17
17
  const MediaPreviewModal = forwardRef(function MediaPreviewModal(props, ref) {
18
- const { className, container, currentIndex: controlledIndex, defaultIndex = 0, disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disableNext = false, disablePortal = false, disablePrev = false, enableCircularNavigation = false, mediaItems, onBackdropClick, onClose, onIndexChange, onNext, onPrev, open, showPaginationIndicator = true, ...rest } = props;
18
+ const { backdropClassName, className, container, currentIndex: controlledIndex, defaultIndex = 0, disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disableNext = false, disablePortal = false, disablePrev = false, enableCircularNavigation = false, mediaItems, onBackdropClick, onClose, onIndexChange, onNext, onPrev, open, showPaginationIndicator = true, ...rest } = props;
19
19
  const { Container: ModalContainer } = useModalContainer();
20
20
  // Determine if component is in controlled mode
21
21
  const isControlled = controlledIndex !== undefined;
@@ -169,7 +169,7 @@ const MediaPreviewModal = forwardRef(function MediaPreviewModal(props, ref) {
169
169
  exit: MOTION_EASING.standard,
170
170
  }, in: isCurrent, children: mediaElement.element }, index));
171
171
  };
172
- return (jsxs(ModalContainer, { className: modalClasses.overlay, container: container, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: [jsx("div", { ...rest, className: cx(modalClasses.host, modalClasses.mediaPreview, className), role: "dialog", children: jsx("div", { className: modalClasses.mediaPreviewContent, children: jsx("div", { className: modalClasses.mediaPreviewMediaContainer, children: displayedIndices.map((index) => renderMedia(index)) }) }) }), jsx(ClearActions, { className: modalClasses.mediaPreviewCloseButton, onClick: onClose, type: "embedded", variant: "contrast" }), mediaItems.length > 1 && (jsx("button", { "aria-disabled": isPrevDisabled, "aria-label": "Previous media", className: cx(modalClasses.mediaPreviewNavButton, modalClasses.mediaPreviewNavButtonPrev), disabled: isPrevDisabled, onClick: handlePrev, title: "Previous", type: "button", children: jsx(Icon, { icon: ChevronLeftIcon, size: 16, color: "fixed-light" }) })), mediaItems.length > 1 && (jsx("button", { "aria-disabled": isNextDisabled, "aria-label": "Next media", className: cx(modalClasses.mediaPreviewNavButton, modalClasses.mediaPreviewNavButtonNext), disabled: isNextDisabled, onClick: handleNext, title: "Next", type: "button", children: jsx(Icon, { icon: ChevronRightIcon, size: 16, color: "fixed-light" }) })), showPaginationIndicator && mediaItems.length > 1 && (jsxs("div", { "aria-label": `Page ${currentIndex + 1} of ${mediaItems.length}`, className: modalClasses.mediaPreviewPaginationIndicator, children: [currentIndex + 1, "/", mediaItems.length] }))] }));
172
+ return (jsxs(ModalContainer, { className: backdropClassName, container: container, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: [jsx("div", { ...rest, className: cx(modalClasses.host, modalClasses.mediaPreview, className), role: "dialog", children: jsx("div", { className: modalClasses.mediaPreviewContent, children: jsx("div", { className: modalClasses.mediaPreviewMediaContainer, children: displayedIndices.map((index) => renderMedia(index)) }) }) }), jsx(ClearActions, { className: modalClasses.mediaPreviewCloseButton, onClick: onClose, type: "embedded", variant: "contrast" }), mediaItems.length > 1 && (jsx("button", { "aria-disabled": isPrevDisabled, "aria-label": "Previous media", className: cx(modalClasses.mediaPreviewNavButton, modalClasses.mediaPreviewNavButtonPrev), disabled: isPrevDisabled, onClick: handlePrev, title: "Previous", type: "button", children: jsx(Icon, { icon: ChevronLeftIcon, size: 16, color: "fixed-light" }) })), mediaItems.length > 1 && (jsx("button", { "aria-disabled": isNextDisabled, "aria-label": "Next media", className: cx(modalClasses.mediaPreviewNavButton, modalClasses.mediaPreviewNavButtonNext), disabled: isNextDisabled, onClick: handleNext, title: "Next", type: "button", children: jsx(Icon, { icon: ChevronRightIcon, size: 16, color: "fixed-light" }) })), showPaginationIndicator && mediaItems.length > 1 && (jsxs("div", { "aria-label": `Page ${currentIndex + 1} of ${mediaItems.length}`, className: modalClasses.mediaPreviewPaginationIndicator, children: [currentIndex + 1, "/", mediaItems.length] }))] }));
173
173
  });
174
174
 
175
175
  export { MediaPreviewModal as default };
package/Modal/Modal.d.ts CHANGED
@@ -4,6 +4,10 @@ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
4
4
  import { ModalHeaderProps } from './ModalHeader';
5
5
  import { ModalFooterProps } from './ModalFooter';
6
6
  interface CommonModalProps extends Omit<ModalContainerProps, 'children'>, Pick<NativeElementPropsWithoutKeyAndRef<'div'>, 'children'>, Partial<Omit<ModalHeaderProps, 'children' | 'className' | 'title'>>, Partial<Omit<ModalFooterProps, 'children' | 'className' | 'confirmText'>> {
7
+ /**
8
+ * The custom class name applied to the modal container.
9
+ */
10
+ backdropClassName?: string;
7
11
  /**
8
12
  * Whether to force full screen on any breakpoint.
9
13
  * @default false