@moderneinc/design-system-components 7.6.1-next.f065cb → 7.7.0-next.6ace41

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.
@@ -1,5 +1,6 @@
1
- import ButtonBase from '@mui/material/ButtonBase';
2
- import React, { type ElementType, type ReactNode } from 'react';
1
+ import type ButtonBase from '@mui/material/ButtonBase';
2
+ import type React from 'react';
3
+ import { type ElementType, type ReactNode } from 'react';
3
4
  declare module '@mui/material/ButtonBase' {
4
5
  interface ButtonBasePropsVariantOverrides {
5
6
  primary: true;
@@ -22,7 +23,8 @@ type ModButtonOwnProps = {
22
23
  */
23
24
  size?: ButtonSize;
24
25
  /**
25
- * Show loading spinner instead of children
26
+ * Show a loading spinner in place of `startIcon` (or `endIcon`), or over the label when
27
+ * there is no icon. The button keeps its width either way.
26
28
  * @default false
27
29
  */
28
30
  loading?: boolean;
@@ -47,9 +49,9 @@ export type ModButtonProps<C extends ElementType = typeof ButtonBase> = ModButto
47
49
  component?: C;
48
50
  };
49
51
  /**
50
- * ModButton - Text button component based on MUI ButtonBase
52
+ * ModButton - Text button component based on MUI Button
51
53
  */
52
- export declare function ModButton<C extends ElementType = typeof ButtonBase>({ variant, size, loading, children, disabled, startIcon, endIcon, ...rest }: ModButtonProps<C>): import("react/jsx-runtime").JSX.Element;
54
+ export declare function ModButton<C extends ElementType = typeof ButtonBase>({ variant, size, loading, children, startIcon, endIcon, ...rest }: ModButtonProps<C>): import("react/jsx-runtime").JSX.Element;
53
55
  export declare namespace ModButton {
54
56
  var displayName: string;
55
57
  }
package/dist/index.d.ts CHANGED
@@ -523,7 +523,8 @@ type ModButtonOwnProps = {
523
523
  */
524
524
  size?: ButtonSize;
525
525
  /**
526
- * Show loading spinner instead of children
526
+ * Show a loading spinner in place of `startIcon` (or `endIcon`), or over the label when
527
+ * there is no icon. The button keeps its width either way.
527
528
  * @default false
528
529
  */
529
530
  loading?: boolean;
@@ -548,9 +549,9 @@ type ModButtonProps<C extends ElementType = typeof ButtonBase> = ModButtonOwnPro
548
549
  component?: C;
549
550
  };
550
551
  /**
551
- * ModButton - Text button component based on MUI ButtonBase
552
+ * ModButton - Text button component based on MUI Button
552
553
  */
553
- declare function ModButton<C extends ElementType = typeof ButtonBase>({ variant, size, loading, children, disabled, startIcon, endIcon, ...rest }: ModButtonProps<C>): react_jsx_runtime.JSX.Element;
554
+ declare function ModButton<C extends ElementType = typeof ButtonBase>({ variant, size, loading, children, startIcon, endIcon, ...rest }: ModButtonProps<C>): react_jsx_runtime.JSX.Element;
554
555
  declare namespace ModButton {
555
556
  var displayName: string;
556
557
  }
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { semanticColors, spacing, typography, borderRadius, shadows, colors, semanticColorsDark, semanticTypography, iconSize, height } from '@moderneinc/design-system-tokens';
2
+ import { semanticColors, spacing, typography, borderRadius, shadows, iconSize, colors, semanticColorsDark, semanticTypography, height } from '@moderneinc/design-system-tokens';
3
3
  import Box from '@mui/material/Box';
4
4
  import { styled, useTheme, alpha } from '@mui/material/styles';
5
5
  import Chip, { chipClasses } from '@mui/material/Chip';
@@ -9,9 +9,10 @@ import Alert, { alertClasses } from '@mui/material/Alert';
9
9
  import ButtonBase, { buttonBaseClasses } from '@mui/material/ButtonBase';
10
10
  import { Info, WifiOff, TriangleAlert, CircleAlert, CircleCheckBig, X, ChevronRight, Square, SquareCheck, SquareMinus, Search, ChevronDown, Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Columns3, ListFilter, HelpCircle, CalendarDays, Minus, ChevronLeft, AlertCircle, ChevronUp } from 'lucide-react';
11
11
  import IconButton, { iconButtonClasses } from '@mui/material/IconButton';
12
- import React, { useState, useRef, useCallback, useMemo, forwardRef, useEffect, createContext, useContext, useLayoutEffect, Children, isValidElement, cloneElement } from 'react';
12
+ import { useState, useId, useRef, useCallback, useMemo, forwardRef, useEffect, createContext, useContext, useLayoutEffect, Children, isValidElement, cloneElement } from 'react';
13
13
  import Breadcrumbs, { breadcrumbsClasses } from '@mui/material/Breadcrumbs';
14
14
  import Link from '@mui/material/Link';
15
+ import Button, { buttonClasses } from '@mui/material/Button';
15
16
  import CircularProgress from '@mui/material/CircularProgress';
16
17
  import MuiToggleButton, { toggleButtonClasses } from '@mui/material/ToggleButton';
17
18
  import MuiToggleButtonGroup, { toggleButtonGroupClasses } from '@mui/material/ToggleButtonGroup';
@@ -19,7 +20,6 @@ import MuiCard from '@mui/material/Card';
19
20
  import Checkbox, { checkboxClasses } from '@mui/material/Checkbox';
20
21
  import { gridClasses, useGridApiContext, useGridSelector, gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector, GridFilterPanel, GridPanel, gridPanelClasses, ColumnsPanelTrigger, ToolbarButton, gridFilterModelSelector, FilterPanelTrigger, gridPaginationModelSelector, gridRowCountSelector, gridRowSelectionCountSelector, GridActionsCell, QuickFilter, QuickFilterControl, QuickFilterClear, Toolbar } from '@mui/x-data-grid';
21
22
  import { useGridApiRef, DataGridPro } from '@mui/x-data-grid-pro';
22
- import Button, { buttonClasses } from '@mui/material/Button';
23
23
  import Divider from '@mui/material/Divider';
24
24
  import InputAdornment, { inputAdornmentClasses } from '@mui/material/InputAdornment';
25
25
  import InputBase, { inputBaseClasses } from '@mui/material/InputBase';
@@ -740,39 +740,34 @@ function ModBreadcrumbs({ children, className }) {
740
740
  return (jsx(StyledBreadcrumbs, { separator: jsx(ChevronRight, { size: 10.67, color: semanticColors.color.icon.utility }), className: className, "aria-label": "breadcrumb", children: children }));
741
741
  }
742
742
 
743
- // Props that should not be forwarded to ButtonBase (Button-specific props that ButtonBase doesn't support)
744
- const buttonOnlyProps = [
745
- 'variant',
746
- 'size',
747
- 'loading',
748
- 'startIcon',
749
- 'endIcon',
750
- 'fullWidth',
751
- 'disableElevation',
752
- ];
753
- const StyledButtonBase$1 = styled(ButtonBase, {
754
- shouldForwardProp: prop => !buttonOnlyProps.includes(prop),
755
- })(({ theme, variant = 'primary', size = 'medium', loading = false }) => {
743
+ const StyledButton = styled(Button, {
744
+ shouldForwardProp: prop => prop !== 'buttonVariant' && prop !== 'buttonSize',
745
+ })(({ theme, buttonVariant: variant, buttonSize: size }) => {
756
746
  // Size configurations
757
747
  const sizeConfig = {
758
748
  small: {
759
749
  height: 32,
760
- padding: `0 ${spacing.spacing_1_1_2}px`,
750
+ paddingX: spacing.spacing_1_1_2,
761
751
  fontSize: theme.typography.pxToRem(14),
762
752
  },
763
753
  medium: {
764
754
  height: spacing.spacing_5,
765
- padding: `0 ${spacing.spacing_2}px`,
755
+ paddingX: spacing.spacing_2,
766
756
  fontSize: theme.typography.pxToRem(14),
767
757
  },
768
758
  };
769
759
  const sizeStyles = sizeConfig[size];
760
+ const paddingX = variant === 'tertiary' ? 0 : sizeStyles.paddingX;
761
+ const spinnerSize = size === 'small' ? iconSize.md : iconSize.xl;
762
+ const disabled = `&.${buttonClasses.disabled}:not(.${buttonClasses.loading})`;
770
763
  // Base styles shared by all variants
771
764
  const baseStyles = {
772
765
  height: sizeStyles.height,
773
- padding: sizeStyles.padding,
766
+ padding: `0 ${paddingX}px`,
767
+ fontFamily: 'inherit',
774
768
  fontSize: sizeStyles.fontSize,
775
769
  fontWeight: typography.fontWeight.semiBold,
770
+ letterSpacing: 'inherit',
776
771
  lineHeight: '1.25rem',
777
772
  borderRadius: borderRadius.button,
778
773
  textTransform: 'none',
@@ -782,13 +777,48 @@ const StyledButtonBase$1 = styled(ButtonBase, {
782
777
  position: 'relative',
783
778
  minWidth: 'fit-content',
784
779
  gap: spacing.spacing_1,
780
+ // `gap` spaces the icons; MUI's slot margins would add to it
781
+ [`& .${buttonClasses.startIcon}, & .${buttonClasses.endIcon}`]: {
782
+ margin: 0,
783
+ },
784
+ // MUI sets font-size on the icon slot's first child; IconSlot takes it so 1em icons keep the label's size
785
+ [`& .${buttonClasses.startIcon} > *:nth-of-type(1), & .${buttonClasses.endIcon} > *:nth-of-type(1)`]: {
786
+ fontSize: 'inherit',
787
+ },
788
+ // MUI's indicator is a sibling of the icon; anchoring sizes and places it on the icon
789
+ [`&.${buttonClasses.loadingPositionStart} .${buttonClasses.startIcon}, &.${buttonClasses.loadingPositionEnd} .${buttonClasses.endIcon}`]: {
790
+ anchorName: '--mod-button-icon',
791
+ // MUI fades the icon out while the spinner appears at full opacity
792
+ transition: 'none',
793
+ },
794
+ [`&.${buttonClasses.loadingPositionStart} .${buttonClasses.loadingIndicator}, &.${buttonClasses.loadingPositionEnd} .${buttonClasses.loadingIndicator}`]: {
795
+ // MUI switches it to relative for fullWidth
796
+ position: 'absolute',
797
+ positionAnchor: '--mod-button-icon',
798
+ inset: 'anchor(top) anchor(right) anchor(bottom) anchor(left)',
799
+ },
800
+ // Without anchor positioning, the spinner takes the standard icon size at the icon's edge
801
+ '@supports not (anchor-name: --a)': {
802
+ [`&.${buttonClasses.loadingPositionStart} .${buttonClasses.loadingIndicator}`]: {
803
+ inset: 'auto',
804
+ left: paddingX,
805
+ width: spinnerSize,
806
+ height: spinnerSize,
807
+ },
808
+ [`&.${buttonClasses.loadingPositionEnd} .${buttonClasses.loadingIndicator}`]: {
809
+ inset: 'auto',
810
+ right: paddingX,
811
+ width: spinnerSize,
812
+ height: spinnerSize,
813
+ },
814
+ },
785
815
  // Disabled state (always show cursor)
786
- [`&.${buttonBaseClasses.disabled}`]: {
816
+ [`&.${buttonClasses.disabled}`]: {
787
817
  cursor: 'not-allowed',
788
818
  pointerEvents: 'auto',
789
819
  },
790
820
  // Focus visible for keyboard navigation
791
- [`&.${buttonBaseClasses.focusVisible}`]: {
821
+ [`&.${buttonClasses.focusVisible}`]: {
792
822
  outline: `2px solid ${semanticColors.color.border.focus}`,
793
823
  outlineOffset: 2,
794
824
  },
@@ -805,14 +835,10 @@ const StyledButtonBase$1 = styled(ButtonBase, {
805
835
  '&:active': {
806
836
  backgroundColor: semanticColors.button.primary.bgPressed,
807
837
  },
808
- ...(loading
809
- ? {}
810
- : {
811
- [`&.${buttonBaseClasses.disabled}`]: {
812
- backgroundColor: semanticColors.button.primary.bgDisabled,
813
- color: semanticColors.button.fgDisabled,
814
- },
815
- }),
838
+ [disabled]: {
839
+ backgroundColor: semanticColors.button.primary.bgDisabled,
840
+ color: semanticColors.button.fgDisabled,
841
+ },
816
842
  },
817
843
  secondary: {
818
844
  backgroundColor: semanticColors.button.secondary.bg,
@@ -824,15 +850,11 @@ const StyledButtonBase$1 = styled(ButtonBase, {
824
850
  '&:active': {
825
851
  backgroundColor: semanticColors.button.secondary.bgPressed,
826
852
  },
827
- ...(loading
828
- ? {}
829
- : {
830
- [`&.${buttonBaseClasses.disabled}`]: {
831
- backgroundColor: semanticColors.button.secondary.bgDisabled,
832
- borderColor: semanticColors.color.border.primary,
833
- color: semanticColors.button.fgDisabled,
834
- },
835
- }),
853
+ [disabled]: {
854
+ backgroundColor: semanticColors.button.secondary.bgDisabled,
855
+ borderColor: semanticColors.color.border.primary,
856
+ color: semanticColors.button.fgDisabled,
857
+ },
836
858
  },
837
859
  destructive: {
838
860
  backgroundColor: semanticColors.button.destructive.bg,
@@ -845,20 +867,15 @@ const StyledButtonBase$1 = styled(ButtonBase, {
845
867
  backgroundColor: semanticColors.color.status.error.fg,
846
868
  filter: 'brightness(0.9)',
847
869
  },
848
- ...(loading
849
- ? {}
850
- : {
851
- [`&.${buttonBaseClasses.disabled}`]: {
852
- backgroundColor: semanticColors.button.primary.bgDisabled,
853
- color: semanticColors.button.fgDisabled,
854
- },
855
- }),
870
+ [disabled]: {
871
+ backgroundColor: semanticColors.button.primary.bgDisabled,
872
+ color: semanticColors.button.fgDisabled,
873
+ },
856
874
  },
857
875
  tertiary: {
858
876
  backgroundColor: 'transparent',
859
877
  color: semanticColors.color.text.input,
860
878
  border: 'none',
861
- padding: 0,
862
879
  lineHeight: 1,
863
880
  '&:hover': {
864
881
  backgroundColor: 'transparent',
@@ -867,24 +884,26 @@ const StyledButtonBase$1 = styled(ButtonBase, {
867
884
  '&:active': {
868
885
  color: semanticColors.button.tertiary.fgPressed,
869
886
  },
870
- ...(loading
871
- ? {}
872
- : {
873
- [`&.${buttonBaseClasses.disabled}`]: {
874
- backgroundColor: 'transparent',
875
- color: semanticColors.button.tertiary.fgDisabled,
876
- },
877
- }),
887
+ [disabled]: {
888
+ backgroundColor: 'transparent',
889
+ color: semanticColors.button.tertiary.fgDisabled,
890
+ },
878
891
  },
879
892
  };
893
+ // MUI greys a loading button as disabled; keep the label in the variant's colour
894
+ const { color } = variantStyles[variant];
880
895
  return {
881
896
  ...baseStyles,
882
897
  ...variantStyles[variant],
898
+ [`&.${buttonClasses.loadingPositionStart}, &.${buttonClasses.loadingPositionEnd}`]: { color },
883
899
  };
884
900
  });
901
+ const IconSlot = styled('span')({
902
+ display: 'contents',
903
+ });
885
904
  const LoadingSpinner = styled(CircularProgress, {
886
- shouldForwardProp: prop => prop !== '$variant',
887
- })(({ $variant }) => {
905
+ shouldForwardProp: prop => prop !== 'buttonVariant',
906
+ })(({ buttonVariant }) => {
888
907
  // Use appropriate spinner color for each variant to ensure visibility
889
908
  const spinnerColors = {
890
909
  primary: semanticColors.color.surface.raised,
@@ -893,14 +912,22 @@ const LoadingSpinner = styled(CircularProgress, {
893
912
  tertiary: semanticColors.button.tertiary.fg,
894
913
  };
895
914
  return {
896
- color: spinnerColors[$variant],
915
+ color: spinnerColors[buttonVariant],
897
916
  };
898
917
  });
899
918
  /**
900
- * ModButton - Text button component based on MUI ButtonBase
919
+ * ModButton - Text button component based on MUI Button
901
920
  */
902
- function ModButton({ variant = 'primary', size = 'medium', loading = false, children, disabled, startIcon, endIcon, ...rest }) {
903
- return (jsx(StyledButtonBase$1, { variant: variant, size: size, loading: loading, disabled: disabled || loading, ...rest, children: loading ? (jsx(LoadingSpinner, { "$variant": variant, size: size === 'small' ? 16 : 20 })) : (jsxs(React.Fragment, { children: [startIcon, children, endIcon] })) }));
921
+ function ModButton({ variant = 'primary', size = 'medium', loading = false, children, startIcon, endIcon, ...rest }) {
922
+ const loadingPosition = startIcon ? 'start' : endIcon ? 'end' : 'center';
923
+ // Mirrors MUI: the spinner is named by the button, which gets an id while loading
924
+ const generatedId = useId();
925
+ const id = rest.id ?? (loading ? generatedId : undefined);
926
+ return (jsx(StyledButton, { buttonVariant: variant, buttonSize: size,
927
+ // Pinned so theme defaultProps or a ButtonGroup can't restyle it; ButtonBase had no focus ripple
928
+ variant: "text", size: "medium", color: "primary", disableFocusRipple: true, loading: loading, loadingPosition: loadingPosition, loadingIndicator: jsx(LoadingSpinner, { buttonVariant: variant,
929
+ // With an icon, the anchored indicator is the icon's size and the spinner fills it
930
+ size: loadingPosition === 'center' ? (size === 'small' ? iconSize.md : iconSize.xl) : '100%', "aria-labelledby": id }), startIcon: startIcon && jsx(IconSlot, { children: startIcon }), endIcon: endIcon && jsx(IconSlot, { children: endIcon }), "aria-busy": loading || undefined, ...rest, id: id, children: children }));
904
931
  }
905
932
  ModButton.displayName = 'ModButton';
906
933
 
@@ -943,8 +970,9 @@ function useTabAriaSync() {
943
970
  observerRef.current.disconnect();
944
971
  observerRef.current = null;
945
972
  }
946
- if (!node)
973
+ if (!node) {
947
974
  return;
975
+ }
948
976
  const sync = () => {
949
977
  node.setAttribute('role', 'tab');
950
978
  node.setAttribute('aria-selected', String(node.classList.contains('Mui-selected')));
@@ -1764,8 +1792,9 @@ sx: _sx, theme: _theme, ...props }) => {
1764
1792
  const [searchText, setSearchText] = useState('');
1765
1793
  // Filter columns based on search
1766
1794
  const filteredColumns = useMemo(() => {
1767
- if (!searchText)
1795
+ if (!searchText) {
1768
1796
  return columns;
1797
+ }
1769
1798
  const search = searchText.toLowerCase();
1770
1799
  return columns.filter(col => {
1771
1800
  const headerName = col.headerName || col.field;
@@ -1798,8 +1827,9 @@ sx: _sx, theme: _theme, ...props }) => {
1798
1827
  disableUnderline: true,
1799
1828
  },
1800
1829
  } })), filteredColumns.map(column => {
1801
- if (column.hideable === false)
1830
+ if (column.hideable === false) {
1802
1831
  return null;
1832
+ }
1803
1833
  const isVisible = columnVisibilityModel[column.field] !== false;
1804
1834
  const headerName = column.headerName || column.field;
1805
1835
  return (jsx(ColumnItemContainer, { onClick: () => handleColumnToggle(column.field), children: jsxs(MenuItemContent$1, { children: [jsx(ModCheckbox, { checked: isVisible, size: "small", disableRipple: true }), jsx(MenuItemLabel, { children: headerName })] }) }, column.field));
@@ -3091,11 +3121,13 @@ const ActionButton = styled(IconButton)({
3091
3121
  */
3092
3122
  const ModDownloadToast = ({ title, fileName, progress, showClose = true, showPause = true, onClose, onPause, actions, ...props }) => {
3093
3123
  const renderActions = () => {
3094
- if (actions)
3124
+ if (actions) {
3095
3125
  return actions;
3126
+ }
3096
3127
  const hasActions = showPause || showClose;
3097
- if (!hasActions)
3128
+ if (!hasActions) {
3098
3129
  return null;
3130
+ }
3099
3131
  return (jsxs(Stack, { direction: "row", sx: { alignItems: 'center', gap: `${spacing.spacing_2}px` }, children: [showPause && (jsx(ActionButton, { size: "small", onClick: onPause, "aria-label": "Pause download", children: jsx(Minus, { size: spacing.spacing_2 }) })), showClose && (jsx(ActionButton, { size: "small", onClick: onClose, "aria-label": "Close", children: jsx(X, { size: spacing.spacing_2 }) }))] }));
3100
3132
  };
3101
3133
  return (jsxs(StyledRoot$1, { elevation: 0, role: "status", ...props, children: [jsxs(Stack, { direction: "row", sx: {
@@ -4449,8 +4481,9 @@ const CellRoot = styled('div', {
4449
4481
  * ModIconCell — Icon cell for DataGrid. Renders an icon centered in the cell.
4450
4482
  */
4451
4483
  const ModIconCell = ({ icon, ariaLabel, disabled = false }) => {
4452
- if (!icon)
4484
+ if (!icon) {
4453
4485
  return null;
4486
+ }
4454
4487
  return (jsx(CellRoot, { disabled: disabled, "aria-label": ariaLabel, children: jsx(ModIconWrapper, { size: 16, children: icon }) }));
4455
4488
  };
4456
4489
  ModIconCell.displayName = 'ModIconCell';
@@ -4940,8 +4973,9 @@ function ModPageContent({ pageTitle, subtitle, breadcrumbs, action, sticky = fal
4940
4973
  const containerRef = useRef(null);
4941
4974
  // Intersection observer for back to top button
4942
4975
  useEffect(() => {
4943
- if (!showBackToTop || !headerRef.current)
4976
+ if (!showBackToTop || !headerRef.current) {
4944
4977
  return;
4978
+ }
4945
4979
  const observer = new IntersectionObserver(([entry]) => {
4946
4980
  setShowBackToTopButton(!entry.isIntersecting);
4947
4981
  }, {