@moderneinc/design-system-components 7.7.0-next.19b91c → 7.7.0-next.7feadd

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,56 @@ 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
+ // Square on the icon's centre; the slot can be taller than the icon, and a non-square box wobbles
799
+ inset: 'auto auto auto anchor(left)',
800
+ alignSelf: 'anchor-center',
801
+ width: 'anchor-size(width)',
802
+ height: 'anchor-size(width)',
803
+ },
804
+ // Without anchor positioning, the spinner takes the standard icon size at the icon's edge
805
+ '@supports not (anchor-name: --a)': {
806
+ [`&.${buttonClasses.loadingPositionStart} .${buttonClasses.loadingIndicator}`]: {
807
+ inset: 'auto',
808
+ left: paddingX,
809
+ width: spinnerSize,
810
+ height: spinnerSize,
811
+ },
812
+ [`&.${buttonClasses.loadingPositionEnd} .${buttonClasses.loadingIndicator}`]: {
813
+ inset: 'auto',
814
+ right: paddingX,
815
+ width: spinnerSize,
816
+ height: spinnerSize,
817
+ },
818
+ },
819
+ // Hide the label at once when loading starts, so it doesn't fade out under the spinner
820
+ [`&.${buttonClasses.loading}`]: {
821
+ transitionProperty: 'background-color, border-color',
822
+ },
785
823
  // Disabled state (always show cursor)
786
- [`&.${buttonBaseClasses.disabled}`]: {
824
+ [`&.${buttonClasses.disabled}`]: {
787
825
  cursor: 'not-allowed',
788
826
  pointerEvents: 'auto',
789
827
  },
790
828
  // Focus visible for keyboard navigation
791
- [`&.${buttonBaseClasses.focusVisible}`]: {
829
+ [`&.${buttonClasses.focusVisible}`]: {
792
830
  outline: `2px solid ${semanticColors.color.border.focus}`,
793
831
  outlineOffset: 2,
794
832
  },
@@ -805,14 +843,10 @@ const StyledButtonBase$1 = styled(ButtonBase, {
805
843
  '&:active': {
806
844
  backgroundColor: semanticColors.button.primary.bgPressed,
807
845
  },
808
- ...(loading
809
- ? {}
810
- : {
811
- [`&.${buttonBaseClasses.disabled}`]: {
812
- backgroundColor: semanticColors.button.primary.bgDisabled,
813
- color: semanticColors.button.fgDisabled,
814
- },
815
- }),
846
+ [disabled]: {
847
+ backgroundColor: semanticColors.button.primary.bgDisabled,
848
+ color: semanticColors.button.fgDisabled,
849
+ },
816
850
  },
817
851
  secondary: {
818
852
  backgroundColor: semanticColors.button.secondary.bg,
@@ -824,15 +858,11 @@ const StyledButtonBase$1 = styled(ButtonBase, {
824
858
  '&:active': {
825
859
  backgroundColor: semanticColors.button.secondary.bgPressed,
826
860
  },
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
- }),
861
+ [disabled]: {
862
+ backgroundColor: semanticColors.button.secondary.bgDisabled,
863
+ borderColor: semanticColors.color.border.primary,
864
+ color: semanticColors.button.fgDisabled,
865
+ },
836
866
  },
837
867
  destructive: {
838
868
  backgroundColor: semanticColors.button.destructive.bg,
@@ -845,20 +875,15 @@ const StyledButtonBase$1 = styled(ButtonBase, {
845
875
  backgroundColor: semanticColors.color.status.error.fg,
846
876
  filter: 'brightness(0.9)',
847
877
  },
848
- ...(loading
849
- ? {}
850
- : {
851
- [`&.${buttonBaseClasses.disabled}`]: {
852
- backgroundColor: semanticColors.button.primary.bgDisabled,
853
- color: semanticColors.button.fgDisabled,
854
- },
855
- }),
878
+ [disabled]: {
879
+ backgroundColor: semanticColors.button.primary.bgDisabled,
880
+ color: semanticColors.button.fgDisabled,
881
+ },
856
882
  },
857
883
  tertiary: {
858
884
  backgroundColor: 'transparent',
859
885
  color: semanticColors.color.text.input,
860
886
  border: 'none',
861
- padding: 0,
862
887
  lineHeight: 1,
863
888
  '&:hover': {
864
889
  backgroundColor: 'transparent',
@@ -867,24 +892,26 @@ const StyledButtonBase$1 = styled(ButtonBase, {
867
892
  '&:active': {
868
893
  color: semanticColors.button.tertiary.fgPressed,
869
894
  },
870
- ...(loading
871
- ? {}
872
- : {
873
- [`&.${buttonBaseClasses.disabled}`]: {
874
- backgroundColor: 'transparent',
875
- color: semanticColors.button.tertiary.fgDisabled,
876
- },
877
- }),
895
+ [disabled]: {
896
+ backgroundColor: 'transparent',
897
+ color: semanticColors.button.tertiary.fgDisabled,
898
+ },
878
899
  },
879
900
  };
901
+ // MUI greys a loading button as disabled; keep the label in the variant's colour
902
+ const { color } = variantStyles[variant];
880
903
  return {
881
904
  ...baseStyles,
882
905
  ...variantStyles[variant],
906
+ [`&.${buttonClasses.loadingPositionStart}, &.${buttonClasses.loadingPositionEnd}`]: { color },
883
907
  };
884
908
  });
909
+ const IconSlot = styled('span')({
910
+ display: 'contents',
911
+ });
885
912
  const LoadingSpinner = styled(CircularProgress, {
886
- shouldForwardProp: prop => prop !== '$variant',
887
- })(({ $variant }) => {
913
+ shouldForwardProp: prop => prop !== 'buttonVariant',
914
+ })(({ buttonVariant }) => {
888
915
  // Use appropriate spinner color for each variant to ensure visibility
889
916
  const spinnerColors = {
890
917
  primary: semanticColors.color.surface.raised,
@@ -893,14 +920,22 @@ const LoadingSpinner = styled(CircularProgress, {
893
920
  tertiary: semanticColors.button.tertiary.fg,
894
921
  };
895
922
  return {
896
- color: spinnerColors[$variant],
923
+ color: spinnerColors[buttonVariant],
897
924
  };
898
925
  });
899
926
  /**
900
- * ModButton - Text button component based on MUI ButtonBase
927
+ * ModButton - Text button component based on MUI Button
901
928
  */
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] })) }));
929
+ function ModButton({ variant = 'primary', size = 'medium', loading = false, children, startIcon, endIcon, ...rest }) {
930
+ const loadingPosition = startIcon ? 'start' : endIcon ? 'end' : 'center';
931
+ // Mirrors MUI: the spinner is named by the button, which gets an id while loading
932
+ const generatedId = useId();
933
+ const id = rest.id ?? (loading ? generatedId : undefined);
934
+ return (jsx(StyledButton, { buttonVariant: variant, buttonSize: size,
935
+ // Pinned so theme defaultProps or a ButtonGroup can't restyle it; ButtonBase had no focus ripple
936
+ variant: "text", size: "medium", color: "primary", disableFocusRipple: true, loading: loading, loadingPosition: loadingPosition, loadingIndicator: jsx(LoadingSpinner, { buttonVariant: variant,
937
+ // With an icon, the anchored indicator is the icon's size and the spinner fills it
938
+ 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
939
  }
905
940
  ModButton.displayName = 'ModButton';
906
941