@graphcommerce/next-ui 3.25.3 → 4.1.0

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 (140) hide show
  1. package/AnimatedRow/index.tsx +6 -3
  2. package/ApolloError/ApolloErrorAlert.tsx +20 -21
  3. package/ApolloError/ApolloErrorFullPage.tsx +3 -4
  4. package/ApolloError/ApolloErrorSnackbar.tsx +3 -3
  5. package/Blog/BlogAuthor/index.tsx +42 -48
  6. package/Blog/BlogContent/index.tsx +6 -23
  7. package/Blog/BlogHeader/index.tsx +26 -23
  8. package/Blog/BlogList/index.tsx +7 -25
  9. package/Blog/BlogListItem/index.tsx +63 -52
  10. package/Blog/BlogTags/BlogTag.tsx +27 -0
  11. package/Blog/BlogTags/index.tsx +7 -32
  12. package/Blog/BlogTitle/index.tsx +7 -21
  13. package/Button/Button.tsx +5 -0
  14. package/Button/LinkOrButton.tsx +80 -0
  15. package/Button/index.tsx +2 -177
  16. package/CHANGELOG.md +524 -645
  17. package/ChipMenu/index.tsx +71 -77
  18. package/ContainerWithHeader/index.tsx +33 -30
  19. package/FlagAvatar/index.tsx +2 -12
  20. package/Footer/Footer.tsx +105 -88
  21. package/Footer/index.ts +0 -1
  22. package/Form/Form.tsx +35 -0
  23. package/Form/FormActions.tsx +9 -14
  24. package/Form/FormDivider.tsx +8 -13
  25. package/Form/FormHeader.tsx +5 -26
  26. package/Form/FormRow.tsx +8 -13
  27. package/Form/InputCheckmark.tsx +9 -22
  28. package/FramerScroller/SidebarGallery.tsx +340 -0
  29. package/FramerScroller/SidebarSlider.tsx +107 -0
  30. package/FramerScroller/index.ts +2 -0
  31. package/FullPageMessage/index.tsx +52 -48
  32. package/Highlight/index.tsx +1 -1
  33. package/IconHeader/index.tsx +63 -58
  34. package/JsonLd/index.tsx +3 -2
  35. package/Layout/components/LayoutHeader.tsx +75 -82
  36. package/Layout/components/LayoutHeaderBack.tsx +26 -18
  37. package/Layout/components/LayoutHeaderClose.tsx +9 -22
  38. package/Layout/components/LayoutHeaderContent.tsx +190 -154
  39. package/Layout/components/LayoutHeadertypes.ts +1 -1
  40. package/Layout/components/LayoutTitle.tsx +60 -55
  41. package/LayoutDefault/components/LayoutDefault.tsx +96 -85
  42. package/LayoutOverlay/components/LayoutOverlay.tsx +2 -8
  43. package/LayoutOverlay/components/LayoutOverlayBase.tsx +233 -239
  44. package/LayoutOverlay/test/LayoutOverlayDemo.tsx +1 -1
  45. package/LayoutParts/DesktopHeaderBadge.tsx +28 -0
  46. package/LayoutParts/DesktopNavActions.tsx +15 -0
  47. package/LayoutParts/DesktopNavBar.tsx +113 -0
  48. package/LayoutParts/DesktopNavBarItem.tsx +44 -0
  49. package/{AppShell → LayoutParts}/GlobalHead.tsx +1 -2
  50. package/LayoutParts/Logo.tsx +77 -0
  51. package/LayoutParts/MenuFab.tsx +132 -0
  52. package/LayoutParts/MenuFabItem.tsx +25 -0
  53. package/LayoutParts/MenuFabSecondaryItem.tsx +37 -0
  54. package/{AppShell/PlaceholderFab/index.tsx → LayoutParts/PlaceholderFab.tsx} +1 -1
  55. package/LayoutParts/StickyBelowHeader.tsx +25 -0
  56. package/LayoutParts/index.ts +12 -0
  57. package/{AppShell → LayoutParts}/useFabAnimation.ts +0 -0
  58. package/Page/CssAndFramerMotionProvider.tsx +21 -0
  59. package/Page/index.ts +2 -0
  60. package/Page/types.ts +2 -8
  61. package/PageLoadIndicator/index.tsx +25 -30
  62. package/PageMeta/index.tsx +1 -1
  63. package/Pagination/index.tsx +37 -54
  64. package/RenderType/index.tsx +1 -1
  65. package/Row/ButtonLinkList/ButtonLinkList.tsx +35 -37
  66. package/Row/ButtonLinkList/ButtonLinkListItem.tsx +16 -33
  67. package/Row/ColumnOne/index.tsx +5 -10
  68. package/Row/ColumnOneBoxed/index.tsx +18 -19
  69. package/Row/ColumnOneCentered/index.tsx +3 -4
  70. package/Row/ColumnThree/index.tsx +62 -57
  71. package/Row/ColumnTwo/index.tsx +37 -32
  72. package/Row/ColumnTwoSpread/index.tsx +28 -37
  73. package/Row/ColumnTwoWithTop/index.tsx +37 -43
  74. package/Row/ContentLinks/index.tsx +24 -25
  75. package/Row/HeroBanner/index.tsx +98 -82
  76. package/Row/IconBlocks/IconBlock/index.tsx +45 -37
  77. package/Row/IconBlocks/index.tsx +29 -44
  78. package/Row/ImageText/index.tsx +71 -67
  79. package/Row/ImageTextBoxed/index.tsx +66 -65
  80. package/Row/ParagraphWithSidebarSlide/index.tsx +80 -76
  81. package/Row/Quote/index.tsx +3 -3
  82. package/Row/SpecialBanner/index.tsx +97 -97
  83. package/Row/index.tsx +4 -9
  84. package/SectionContainer/index.tsx +32 -31
  85. package/SectionHeader/index.tsx +41 -43
  86. package/Separator/index.tsx +19 -18
  87. package/Snackbar/MessageSnackbar.tsx +1 -2
  88. package/Snackbar/MessageSnackbarImpl.tsx +68 -115
  89. package/StarRatingField/index.tsx +24 -25
  90. package/Stepper/Stepper.tsx +34 -32
  91. package/Styles/EmotionProvider.tsx +14 -0
  92. package/Styles/breakpointVal.tsx +16 -10
  93. package/Styles/extendableComponent.ts +70 -0
  94. package/Styles/index.tsx +9 -2
  95. package/Styles/withEmotionCache.tsx +36 -0
  96. package/Styles/withTheme.tsx +15 -24
  97. package/SvgIcon/SvgIcon.tsx +60 -0
  98. package/TextInputNumber/index.tsx +49 -50
  99. package/Theme/DarkLightModeThemeProvider.tsx +119 -0
  100. package/Theme/MuiButton.ts +128 -0
  101. package/Theme/MuiSlider.ts +28 -0
  102. package/Theme/MuiSnackbar.ts +31 -0
  103. package/Theme/{types.ts → createTheme.ts} +8 -2
  104. package/Theme/index.ts +5 -0
  105. package/Theme/themeDefaults.ts +51 -0
  106. package/TimeAgo/index.tsx +1 -1
  107. package/ToggleButton/index.tsx +43 -49
  108. package/ToggleButtonGroup/index.tsx +39 -39
  109. package/UspList/UspListItem.tsx +56 -46
  110. package/UspList/index.tsx +29 -26
  111. package/docs/building-components.mdx +3 -0
  112. package/docs/components/ComponentBasic.tsx +26 -0
  113. package/docs/components/ComponentChild.tsx +48 -0
  114. package/docs/components/ComponentChildVariant.tsx +54 -0
  115. package/docs/components/ComponentChildVariantExtendable.tsx +62 -0
  116. package/docs/components/ComponentStylable.tsx +32 -0
  117. package/docs/pages/building-components.tsx +62 -0
  118. package/icons/index.tsx +2 -0
  119. package/index.ts +27 -81
  120. package/package.json +27 -27
  121. package/types.d.ts +1 -1
  122. package/AppShell/AppShellSticky/index.tsx +0 -38
  123. package/AppShell/DesktopNavActions.tsx +0 -32
  124. package/AppShell/DesktopNavBar.tsx +0 -158
  125. package/AppShell/Logo.tsx +0 -46
  126. package/AppShell/Menu.tsx +0 -7
  127. package/AppShell/MenuFab.tsx +0 -162
  128. package/AppShell/MenuFabSecondaryItem.tsx +0 -32
  129. package/AppShell/index.ts +0 -15
  130. package/AspectRatioContainer/index.tsx +0 -27
  131. package/Footer/SocialIcon.tsx +0 -23
  132. package/Form/index.tsx +0 -67
  133. package/FramerScroller/components/SidebarGallery.tsx +0 -317
  134. package/FramerScroller/components/SidebarSlider.tsx +0 -97
  135. package/Page/App.tsx +0 -17
  136. package/Page/Document.tsx +0 -24
  137. package/StyledBadge/index.tsx +0 -20
  138. package/Styles/classesPicker.ts +0 -41
  139. package/SvgImage/SvgImageSimple.tsx +0 -100
  140. package/SvgImage/index.tsx +0 -74
@@ -8,15 +8,21 @@ export function breakpointVal(
8
8
  const breakpoints = Object.values(breakpointsObject)
9
9
  const spread = breakpoints[breakpoints.length - 1] - minSize
10
10
 
11
- return Object.fromEntries(
12
- breakpoints.map((breakpoint, index) => {
13
- // Get the size between this breakpoint and the previous breakpoint
14
- const between = (breakpoint + (breakpoints[index + 1] ?? breakpoint)) / 2
15
- // Calculate the size of the value
16
- const size = Math.max(min, ((between - minSize) / spread) * (max - min) + min)
17
- const value = `${Math.round(size * 100) / 100}px`
11
+ const entries = {}
18
12
 
19
- return [`@media (min-width: ${breakpoint}px )`, { [property]: value }]
20
- }),
21
- )
13
+ breakpoints.forEach((breakpoint, index) => {
14
+ // Get the size between this breakpoint and the previous breakpoint
15
+ const between = (breakpoint + (breakpoints[index + 1] ?? breakpoint)) / 2
16
+ // Calculate the size of the value
17
+ const size = Math.max(min, ((between - minSize) / spread) * (max - min) + min)
18
+ const value = `${Math.round(size * 100) / 100}px`
19
+
20
+ if (breakpoint) {
21
+ entries[`@media (min-width: ${breakpoint}px )`] = { [property]: value }
22
+ } else {
23
+ entries[property] = value
24
+ }
25
+ })
26
+
27
+ return entries
22
28
  }
@@ -0,0 +1,70 @@
1
+ import { capitalize, Interpolation, Theme } from '@mui/material'
2
+ import React from 'react'
3
+
4
+ export type ExtendableComponent<StyleProps extends Record<string, unknown>> = {
5
+ defaultProps?: Partial<StyleProps>
6
+ variants?: { props: Partial<StyleProps>; style: Interpolation<{ theme: Theme }> }[]
7
+ }
8
+
9
+ export function slotClasses<Name extends string, ClassNames extends ReadonlyArray<string>>(
10
+ name: Name,
11
+ slotNames: ClassNames,
12
+ ) {
13
+ return Object.fromEntries(slotNames.map((slot) => [slot, `${name}-${slot}`])) as {
14
+ [P in ClassNames[number]]: `${Name}-${P}`
15
+ }
16
+ }
17
+
18
+ /** Maps incoming classes to a selectors that can be used to extend the component */
19
+ export const partselectorsMap = <O extends Record<string, string>>(
20
+ obj: O,
21
+ ): {
22
+ [P in keyof O]: `& .${O[P]}`
23
+ } => {
24
+ const mapped = Object.entries(obj).map(([target, className]) => [target, `& .${className}`])
25
+ return Object.fromEntries(mapped)
26
+ }
27
+
28
+ /**
29
+ * Utility function to:
30
+ *
31
+ * - Define parts
32
+ * - Generate state css classes.
33
+ */
34
+ export function extendableComponent<
35
+ ComponentStyleProps extends Record<string, boolean | string | number | undefined>,
36
+ Name extends string = string,
37
+ ClassNames extends ReadonlyArray<string> = ReadonlyArray<string>,
38
+ >(componentName: Name, slotNames: ClassNames) {
39
+ const classes = slotClasses(componentName, slotNames)
40
+ const partselectors = partselectorsMap(classes)
41
+
42
+ const withState = (state: ComponentStyleProps) => {
43
+ const stateClas = Object.fromEntries(
44
+ Object.entries<string>(classes).map(([slot, className]) => {
45
+ const mapped = Object.entries(state).map(([key, value]) => {
46
+ if (typeof value === 'boolean' && value === true) return key
47
+ if (typeof value === 'string' && value.length > 0) return `${key}${capitalize(value)}`
48
+ if (typeof value === 'number' && value > 0) return `${key}${value}`
49
+ return ''
50
+ })
51
+
52
+ if (className) mapped.unshift(className)
53
+ return [slot, mapped.filter(Boolean).join(' ')]
54
+ }),
55
+ ) as {
56
+ [P in ClassNames[number]]: `${Name}-${P} ${string}`
57
+ }
58
+
59
+ return stateClas
60
+ }
61
+
62
+ return {
63
+ classes,
64
+ selectors: {
65
+ // ...stateSelectors,
66
+ ...partselectors,
67
+ },
68
+ withState,
69
+ }
70
+ }
package/Styles/index.tsx CHANGED
@@ -1,8 +1,15 @@
1
+ /**
2
+ * Consider moving to the `sx` prop with `selectors`
3
+ *
4
+ * @deprecated
5
+ */
1
6
  export type UseStyles<T extends (...args: never[]) => unknown> = {
2
- classes?: Partial<ReturnType<T>>
7
+ classes?: Partial<ReturnType<T>['classes']>
3
8
  }
4
9
 
5
10
  export * from './breakpointVal'
11
+ export * from './EmotionProvider'
12
+ export * from './extendableComponent'
6
13
  export * from './responsiveVal'
7
- export * from './classesPicker'
14
+ export * from './withEmotionCache'
8
15
  export * from './withTheme'
@@ -0,0 +1,36 @@
1
+ import { EmotionJSX } from '@emotion/react/types/jsx-namespace'
2
+ import createEmotionServer from '@emotion/server/create-instance'
3
+ // eslint-disable-next-line @next/next/no-document-import-in-page
4
+ import type NextDocument from 'next/document'
5
+ // eslint-disable-next-line @next/next/no-document-import-in-page
6
+ import type { DocumentContext, DocumentInitialProps } from 'next/document'
7
+ import { createMuiCache } from './EmotionProvider'
8
+
9
+ export type EmotionCacheProps = { emotionStyleTags: EmotionJSX.Element[] }
10
+
11
+ export function withEmotionCache(Document: typeof NextDocument): typeof NextDocument {
12
+ return class DocumentWithEmotionCache extends Document {
13
+ static async getInitialProps(ctx: DocumentContext) {
14
+ const emotionServer = createEmotionServer(createMuiCache())
15
+ const initialProps = await Document.getInitialProps(ctx)
16
+
17
+ const emotionStyleTags = emotionServer
18
+ .extractCriticalToChunks(initialProps.html)
19
+ .styles.filter(({ css }) => css !== '')
20
+ .map((style) => (
21
+ <style
22
+ data-emotion={`${style.key} ${style.ids.join(' ')}`}
23
+ key={style.key}
24
+ // eslint-disable-next-line react/no-danger
25
+ dangerouslySetInnerHTML={{ __html: style.css }}
26
+ />
27
+ ))
28
+
29
+ const props: DocumentInitialProps & EmotionCacheProps = {
30
+ ...initialProps,
31
+ emotionStyleTags,
32
+ }
33
+ return props
34
+ }
35
+ }
36
+ }
@@ -1,17 +1,4 @@
1
- import { makeStyles, Theme, ThemeProvider } from '@material-ui/core'
2
-
3
- const useStyles = makeStyles(
4
- {
5
- // These theme specific styles are copied from
6
- // https://github.com/mui-org/material-ui/blob/master/packages/mui-material/src/CssBaseline/CssBaseline.js#L18-L24
7
- root: (theme: Theme) => ({
8
- color: theme.palette.text.primary,
9
- ...theme.typography.body1,
10
- backgroundColor: theme.palette.background.default,
11
- }),
12
- },
13
- { name: 'Theme' },
14
- )
1
+ import { css, Theme, ThemeProvider } from '@mui/material'
15
2
 
16
3
  /**
17
4
  * It will provide a theme for the underlying tree and will set the color/font and backgroundColor
@@ -38,14 +25,18 @@ const useStyles = makeStyles(
38
25
  * export default withTheme(MyPage, darkTheme)
39
26
  * ```
40
27
  */
41
- export function withTheme<P extends { className?: string }>(Component: React.FC<P>, theme: Theme) {
42
- return (props: P) => {
43
- const classes = useStyles(theme)
44
-
45
- return (
46
- <ThemeProvider theme={theme}>
47
- <Component {...props} className={classes.root} />
48
- </ThemeProvider>
49
- )
50
- }
28
+ export function withTheme(Component: React.FC<{ className?: string }>, theme: Theme) {
29
+ return (props: Record<string, unknown>) => (
30
+ <ThemeProvider theme={theme}>
31
+ <Component
32
+ {...props}
33
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
34
+ css={css({
35
+ color: theme.palette.text.primary,
36
+ backgroundColor: theme.palette.background.default,
37
+ ...(theme.typography.body1 as any),
38
+ })}
39
+ />
40
+ </ThemeProvider>
41
+ )
51
42
  }
@@ -0,0 +1,60 @@
1
+ import { ImageProps, srcToString } from '@graphcommerce/image'
2
+ import { Box, SxProps, Theme } from '@mui/material'
3
+ import { ComponentProps, forwardRef } from 'react'
4
+ import { ExtendableComponent } from '../Styles/extendableComponent'
5
+ import { responsiveVal as rv } from '../Styles/responsiveVal'
6
+
7
+ const name = 'SvgIcon'
8
+ type StyleProps = {
9
+ size?: 'default' | 'inherit' | 'xxl' | 'xl' | 'large' | 'medium' | 'small' | 'xs'
10
+ fillIcon?: boolean
11
+ }
12
+
13
+ // Expose the component to be exendable in your theme.components
14
+ declare module '@mui/material/styles/components' {
15
+ interface Components {
16
+ SvgIcon?: ExtendableComponent<StyleProps>
17
+ }
18
+ }
19
+
20
+ export type SvgIconProps = StyleProps &
21
+ Pick<ImageProps, 'src'> &
22
+ Pick<ComponentProps<'svg'>, 'className'> & { sx?: SxProps<Theme> }
23
+
24
+ /** SvgIcon component is supposed to be used in combination with `icons` */
25
+ export const SvgIcon = forwardRef<SVGSVGElement, SvgIconProps>((props, ref) => {
26
+ const { className, src, size, fillIcon, sx = [], ...svgProps } = props
27
+
28
+ return (
29
+ <Box
30
+ component='svg'
31
+ ref={ref}
32
+ aria-hidden='true'
33
+ className={`${name} ${className ?? ''}`}
34
+ {...svgProps}
35
+ sx={[
36
+ {
37
+ userSelect: 'none',
38
+ width: '1.3em',
39
+ height: '1.3em',
40
+ strokeWidth: 1.8,
41
+ strokeLinecap: 'square',
42
+ strokeLinejoin: 'miter',
43
+ fill: 'none',
44
+ stroke: 'currentColor',
45
+ },
46
+ size === 'xs' && { width: rv(11, 13), height: rv(11, 13), strokeWidth: 2.1 },
47
+ size === 'small' && { width: rv(12, 16), height: rv(12, 16), strokeWidth: 2.1 },
48
+ size === 'medium' && { width: rv(22, 24), height: rv(22, 24), strokeWidth: 1.8 },
49
+ size === 'large' && { width: rv(24, 28), height: rv(24, 28), strokeWidth: 1.4 },
50
+ size === 'xl' && { width: rv(38, 62), height: rv(38, 62), strokeWidth: 1.1 },
51
+ size === 'xxl' && { width: rv(96, 148), height: rv(96, 148), strokeWidth: 0.8 },
52
+ fillIcon === true && { fill: 'currentColor', stroke: `none` },
53
+ ...(Array.isArray(sx) ? sx : [sx]),
54
+ ]}
55
+ >
56
+ <use href={`${srcToString(src)}#icon`} />
57
+ </Box>
58
+ )
59
+ })
60
+ SvgIcon.displayName = 'SvgIcon'
@@ -1,42 +1,19 @@
1
1
  import {
2
2
  IconButton,
3
3
  IconButtonProps,
4
- makeStyles,
5
4
  OutlinedTextFieldProps,
5
+ SxProps,
6
6
  TextField,
7
7
  TextFieldProps,
8
8
  useForkRef,
9
- } from '@material-ui/core'
10
- import clsx from 'clsx'
11
- import React, { ChangeEvent, Ref, useCallback, useEffect, useRef, useState } from 'react'
12
- import { UseStyles } from '../Styles'
9
+ Theme,
10
+ } from '@mui/material'
11
+ import { ChangeEvent, Ref, useCallback, useEffect, useRef, useState } from 'react'
12
+ import { extendableComponent } from '../Styles'
13
13
  import { responsiveVal } from '../Styles/responsiveVal'
14
- import SvgImageSimple from '../SvgImage/SvgImageSimple'
14
+ import { SvgIcon } from '../SvgIcon/SvgIcon'
15
15
  import { iconMin, iconPlus } from '../icons'
16
16
 
17
- const useStyles = makeStyles(
18
- {
19
- quantity: {
20
- width: responsiveVal(80, 120),
21
- backgroundColor: 'inherit',
22
- },
23
- quantityInput: {
24
- textAlign: 'center',
25
- '&::-webkit-inner-spin-button,&::-webkit-outer-spin-button': {
26
- appearance: 'none',
27
- },
28
- },
29
- button: {},
30
- adornedEnd: {
31
- paddingRight: responsiveVal(7, 14),
32
- },
33
- adornedStart: {
34
- paddingLeft: responsiveVal(7, 14),
35
- },
36
- },
37
- { name: 'TextInputNumber' },
38
- )
39
-
40
17
  export type IconButtonPropsOmit = Omit<
41
18
  IconButtonProps,
42
19
  'aria-label' | 'size' | 'onMouseDown' | 'onMouseUp' | 'disabled'
@@ -45,15 +22,30 @@ export type IconButtonPropsOmit = Omit<
45
22
  export type TextInputNumberProps = Omit<TextFieldProps, 'type'> & {
46
23
  DownProps?: IconButtonPropsOmit
47
24
  UpProps?: IconButtonPropsOmit
48
- } & UseStyles<typeof useStyles>
25
+ sx?: SxProps<Theme>
26
+ }
49
27
 
50
28
  function isOutlined(props: TextFieldProps): props is OutlinedTextFieldProps {
51
29
  return props.variant === 'outlined'
52
30
  }
53
31
 
54
- export default function TextInputNumber(props: TextInputNumberProps) {
55
- const { DownProps = {}, UpProps = {}, inputProps = {}, inputRef, ...textFieldProps } = props
56
- const classes = useStyles(props)
32
+ type OwnerState = { size?: 'small' | 'medium' }
33
+ const name = 'TextInputNumber' as const
34
+ const parts = ['quantity', 'quantityInput', 'button'] as const
35
+ const { withState } = extendableComponent<OwnerState, typeof name, typeof parts>(name, parts)
36
+
37
+ export function TextInputNumber(props: TextInputNumberProps) {
38
+ const {
39
+ DownProps = {},
40
+ UpProps = {},
41
+ inputProps = {},
42
+ inputRef,
43
+ sx = [],
44
+ ...textFieldProps
45
+ } = props
46
+
47
+ const classes = withState({})
48
+
57
49
  const ref = useRef<HTMLInputElement>(null)
58
50
  const forkRef = useForkRef<HTMLInputElement>(ref, inputRef as Ref<HTMLInputElement>)
59
51
 
@@ -98,21 +90,20 @@ export default function TextInputNumber(props: TextInputNumberProps) {
98
90
  setTimeout(() => ref.current && updateDisabled(ref.current))
99
91
  }, [ref, inputProps.min, inputProps.max])
100
92
 
101
- if (!textFieldProps.InputProps) textFieldProps.InputProps = {}
102
- if (isOutlined(textFieldProps)) {
103
- textFieldProps.InputProps.classes = {
104
- ...textFieldProps.InputProps?.classes,
105
- adornedEnd: classes.adornedEnd,
106
- adornedStart: classes.adornedStart,
107
- }
108
- }
109
-
110
93
  return (
111
94
  <TextField
112
95
  {...textFieldProps}
113
96
  type='number'
114
97
  inputRef={forkRef}
115
- className={clsx(textFieldProps.className, classes.quantity)}
98
+ className={`${textFieldProps.className ?? ''} ${classes.quantity}`}
99
+ sx={[
100
+ {
101
+ width: responsiveVal(80, 120),
102
+ backgroundColor: 'inherit',
103
+ },
104
+
105
+ ...(Array.isArray(sx) ? sx : [sx]),
106
+ ]}
116
107
  autoComplete='off'
117
108
  label={' '}
118
109
  id='quantity-input'
@@ -127,12 +118,12 @@ export default function TextInputNumber(props: TextInputNumberProps) {
127
118
  onPointerDown={() => setDirection('down')}
128
119
  onPointerUp={stop}
129
120
  // disabled={textFieldProps.disabled || disabled === 'min'}
130
- tabIndex='-1'
121
+ tabIndex={-1}
131
122
  color='inherit'
132
- className={clsx(classes.button, DownProps.className)}
133
123
  {...DownProps}
124
+ className={`${classes.button} ${DownProps.className ?? ''}`}
134
125
  >
135
- {DownProps.children ?? <SvgImageSimple src={iconMin} size='small' />}
126
+ {DownProps.children ?? <SvgIcon src={iconMin} size='small' />}
136
127
  </IconButton>
137
128
  ),
138
129
  endAdornment: (
@@ -143,12 +134,12 @@ export default function TextInputNumber(props: TextInputNumberProps) {
143
134
  onPointerDown={() => setDirection('up')}
144
135
  onPointerUp={() => setDirection(null)}
145
136
  // disabled={textFieldProps.disabled || disabled === 'max'}
146
- tabIndex='-1'
137
+ tabIndex={-1}
147
138
  color='inherit'
148
- className={clsx(classes.button, UpProps.className)}
149
139
  {...UpProps}
140
+ className={`${classes.button} ${UpProps.className ?? ''}`}
150
141
  >
151
- {UpProps.children ?? <SvgImageSimple src={iconPlus} size='small' />}
142
+ {UpProps.children ?? <SvgIcon src={iconPlus} size='small' />}
152
143
  </IconButton>
153
144
  ),
154
145
  }}
@@ -158,7 +149,15 @@ export default function TextInputNumber(props: TextInputNumberProps) {
158
149
  }}
159
150
  inputProps={{
160
151
  ...inputProps,
161
- className: clsx(inputProps?.className, classes.quantityInput),
152
+ sx: [
153
+ {
154
+ textAlign: 'center',
155
+ '&::-webkit-inner-spin-button,&::-webkit-outer-spin-button': {
156
+ appearance: 'none',
157
+ },
158
+ },
159
+ ],
160
+ className: `${inputProps?.className ?? ''} ${classes.quantityInput}`,
162
161
  }}
163
162
  />
164
163
  )
@@ -0,0 +1,119 @@
1
+ import { Trans } from '@lingui/macro'
2
+ import {
3
+ Theme,
4
+ ThemeProvider,
5
+ useMediaQuery,
6
+ Fab,
7
+ FabProps,
8
+ ListItemButton,
9
+ ListItemIcon,
10
+ ListItemText,
11
+ ListItemButtonProps,
12
+ } from '@mui/material'
13
+ import { useRouter } from 'next/router'
14
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react'
15
+ import { SvgIcon } from '../SvgIcon/SvgIcon'
16
+ import { iconMoon, iconSun } from '../icons'
17
+
18
+ type Mode = 'dark' | 'light'
19
+ type UserMode = 'auto' | Mode
20
+
21
+ type ColorModeContext = {
22
+ userMode: UserMode
23
+ browserMode: Mode
24
+ currentMode: Mode
25
+ toggle: () => void
26
+ }
27
+
28
+ export const colorModeContext = createContext(undefined as unknown as ColorModeContext)
29
+ colorModeContext.displayName = 'ColorModeContext'
30
+
31
+ type ThemeProviderProps = {
32
+ // eslint-disable-next-line react/no-unused-prop-types
33
+ light: Theme
34
+ // eslint-disable-next-line react/no-unused-prop-types
35
+ dark: Theme
36
+
37
+ children: React.ReactNode
38
+ }
39
+
40
+ /**
41
+ * Wrapper around `import { ThemeProvider } from '@mui/material'`
42
+ *
43
+ * The multi DarkLightModeThemeProvider allows switching between light and dark mode based on URL
44
+ * and on user input.
45
+ *
46
+ * If you *just* wan't a single theme, use the import { ThemeProvider } from '@mui/material' instead.
47
+ */
48
+ export function DarkLightModeThemeProvider(props: ThemeProviderProps) {
49
+ const { children, light, dark } = props
50
+
51
+ // todo: Save this in local storage
52
+ const [userMode, setUserMode] = useState<UserMode>('auto')
53
+ const browserMode: Mode = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light'
54
+
55
+ // If the user has set a mode, use that. Otherwise, use the browser mode.
56
+ const currentMode = userMode === 'auto' ? browserMode : userMode
57
+ const theme = currentMode === 'light' ? light : dark
58
+
59
+ // If a URL parameter is present, switch from auto to light or dark mode
60
+ const { asPath } = useRouter()
61
+ useEffect(() => {
62
+ if (asPath.includes('darkmode')) setUserMode('dark')
63
+ }, [asPath])
64
+
65
+ // Create the context
66
+ const colorContext: ColorModeContext = useMemo(
67
+ () => ({
68
+ browserMode,
69
+ userMode,
70
+ currentMode,
71
+ toggle: () => setUserMode(currentMode === 'light' ? 'dark' : 'light'),
72
+ }),
73
+ [browserMode, currentMode, userMode],
74
+ )
75
+
76
+ return (
77
+ <colorModeContext.Provider value={colorContext}>
78
+ <ThemeProvider theme={theme}>{children}</ThemeProvider>
79
+ </colorModeContext.Provider>
80
+ )
81
+ }
82
+
83
+ export function useColorMode() {
84
+ return useContext(colorModeContext)
85
+ }
86
+
87
+ export function DarkLightModeToggleFab(props: Omit<FabProps, 'onClick'>) {
88
+ const { currentMode, toggle } = useColorMode()
89
+ return (
90
+ <Fab size='large' color='inherit' onClick={toggle} {...props}>
91
+ <SvgIcon src={currentMode === 'light' ? iconMoon : iconSun} size='large' />
92
+ </Fab>
93
+ )
94
+ }
95
+
96
+ /**
97
+ * A button that switches between light and dark mode
98
+ *
99
+ * To disable this functionality
100
+ */
101
+ export function DarkLightModeMenuSecondaryItem(props: ListItemButtonProps) {
102
+ const { sx = [] } = props
103
+ const { currentMode, toggle } = useColorMode()
104
+
105
+ return (
106
+ <ListItemButton {...props} sx={[{}, ...(Array.isArray(sx) ? sx : [sx])]} dense onClick={toggle}>
107
+ <ListItemIcon sx={{ minWidth: 'unset', paddingRight: '8px' }}>
108
+ <SvgIcon src={currentMode === 'light' ? iconMoon : iconSun} size='medium' />
109
+ </ListItemIcon>
110
+ <ListItemText>
111
+ {currentMode === 'light' ? (
112
+ <Trans>Switch to Dark Mode</Trans>
113
+ ) : (
114
+ <Trans>Switch to Light Mode</Trans>
115
+ )}
116
+ </ListItemText>
117
+ </ListItemButton>
118
+ )
119
+ }
@@ -0,0 +1,128 @@
1
+ import { ComponentsVariants } from '@mui/material'
2
+ import { responsiveVal } from '../Styles/responsiveVal'
3
+
4
+ declare module '@mui/material/Button' {
5
+ interface ButtonPropsVariantOverrides {
6
+ pill: true
7
+ }
8
+ }
9
+
10
+ type ButtonVariants = NonNullable<ComponentsVariants['MuiButton']>
11
+
12
+ export const MuiButtonResponsive: ButtonVariants = [
13
+ {
14
+ props: {},
15
+ style: ({ theme }) => ({
16
+ textTransform: 'none',
17
+ fontWeight: 500,
18
+ }),
19
+ },
20
+ {
21
+ props: { size: 'small' },
22
+ style: ({ theme }) => ({
23
+ ...theme.typography.body2,
24
+ padding: `${responsiveVal(3, 5)} ${responsiveVal(9, 15)}`,
25
+ '& .MuiLoadingButton-loadingIndicatorEnd': { right: responsiveVal(9, 15) },
26
+ '& .MuiLoadingButton-loadingIndicatorStart': { left: responsiveVal(9, 15) },
27
+ }),
28
+ },
29
+ {
30
+ props: { size: 'medium' },
31
+ style: ({ theme }) => ({
32
+ ...theme.typography.body1,
33
+ padding: `${responsiveVal(8, 11)} ${responsiveVal(16, 24)}`,
34
+ '& .MuiLoadingButton-loadingIndicatorEnd': { right: responsiveVal(16, 24) },
35
+ '& .MuiLoadingButton-loadingIndicatorStart': { left: responsiveVal(16, 24) },
36
+ }),
37
+ },
38
+ {
39
+ props: { size: 'large' },
40
+ style: ({ theme }) => ({
41
+ ...theme.typography.subtitle1,
42
+ fontWeight: theme.typography.fontWeightBold,
43
+ padding: `${responsiveVal(10, 15)} ${responsiveVal(30, 60)}`,
44
+ '& .MuiLoadingButton-loadingIndicatorEnd': { right: responsiveVal(30, 60) },
45
+ '& .MuiLoadingButton-loadingIndicatorStart': { left: responsiveVal(30, 60) },
46
+ }),
47
+ },
48
+ {
49
+ props: { variant: 'text', size: 'small' },
50
+ style: {
51
+ padding: '3px 9px',
52
+ '& .MuiLoadingButton-loadingIndicatorEnd': { right: 3 },
53
+ '& .MuiLoadingButton-loadingIndicatorStart': { left: 9 },
54
+ },
55
+ },
56
+ {
57
+ props: { variant: 'text', size: 'medium' },
58
+ style: {
59
+ padding: `${responsiveVal(3, 5)} ${responsiveVal(9, 15)}`,
60
+ '& .MuiLoadingButton-loadingIndicatorEnd': { right: responsiveVal(9, 15) },
61
+ '& .MuiLoadingButton-loadingIndicatorStart': { left: responsiveVal(9, 15) },
62
+ },
63
+ },
64
+ {
65
+ props: { variant: 'text', size: 'large' },
66
+ style: {
67
+ padding: `${responsiveVal(8, 10)} ${responsiveVal(12, 22)}`,
68
+ '& .MuiLoadingButton-loadingIndicatorEnd': { right: responsiveVal(16, 24) },
69
+ '& .MuiLoadingButton-loadingIndicatorStart': { left: responsiveVal(16, 24) },
70
+ },
71
+ },
72
+ ]
73
+
74
+ export const MuiButtonPill: ButtonVariants = [
75
+ {
76
+ props: { variant: 'pill' },
77
+ style: { borderRadius: '99em' },
78
+ },
79
+ {
80
+ props: { variant: 'pill', size: 'small' },
81
+ style: ({ theme }) => ({
82
+ '&:not(.Mui-disabled)': { boxShadow: theme.shadows[2] },
83
+ }),
84
+ },
85
+ {
86
+ props: { variant: 'pill', size: 'medium' },
87
+ style: ({ theme }) => ({
88
+ '&:not(.Mui-disabled)': { boxShadow: theme.shadows[4] },
89
+ }),
90
+ },
91
+ {
92
+ props: { variant: 'pill', size: 'large' },
93
+ style: ({ theme }) => ({
94
+ '&:not(.Mui-disabled)': { boxShadow: theme.shadows[6] },
95
+ }),
96
+ },
97
+ {
98
+ props: { variant: 'pill', disableElevation: true },
99
+ style: { boxShadow: 'none' },
100
+ },
101
+ {
102
+ props: { variant: 'pill', color: 'primary' },
103
+ style: ({ theme }) => ({
104
+ backgroundColor: theme.palette.primary.main,
105
+ color: theme.palette.primary.contrastText,
106
+ '&:hover:not(.Mui-disabled)': { backgroundColor: theme.palette.primary.dark },
107
+ }),
108
+ },
109
+ {
110
+ props: { variant: 'pill', color: 'secondary' },
111
+ style: ({ theme }) => ({
112
+ backgroundColor: theme.palette.secondary.main,
113
+ color: theme.palette.secondary.contrastText,
114
+ '&:hover:not(.Mui-disabled)': { backgroundColor: theme.palette.secondary.dark },
115
+ }),
116
+ },
117
+ {
118
+ props: { variant: 'pill', color: 'inherit' },
119
+ style: ({ theme }) => ({ backgroundColor: theme.palette.background.paper }),
120
+ },
121
+ {
122
+ props: { variant: 'pill', disabled: true },
123
+ style: ({ theme }) => ({
124
+ backgroundColor: theme.palette.action.disabledBackground,
125
+ color: theme.palette.action.disabled,
126
+ }),
127
+ },
128
+ ]