@graphcommerce/next-ui 3.0.1

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 (153) hide show
  1. package/.babelrc +3 -0
  2. package/AnimatedRow/index.tsx +20 -0
  3. package/ApolloError/ApolloErrorAlert.tsx +59 -0
  4. package/ApolloError/ApolloErrorFullPage.tsx +25 -0
  5. package/AppShell/AppShellHeader/appShellHeaderContext.tsx +11 -0
  6. package/AppShell/AppShellHeader/index.tsx +433 -0
  7. package/AppShell/AppShellHeader/useAppShellHeaderContext.tsx +6 -0
  8. package/AppShell/AppShellProvider/index.tsx +18 -0
  9. package/AppShell/AppShellSticky/index.tsx +66 -0
  10. package/AppShell/AppShellTitle/index.tsx +45 -0
  11. package/AppShell/DesktopNavActions.tsx +28 -0
  12. package/AppShell/DesktopNavBar.tsx +110 -0
  13. package/AppShell/FixedFab.tsx +41 -0
  14. package/AppShell/ForwardButton.tsx +53 -0
  15. package/AppShell/FullPageShellBase.tsx +59 -0
  16. package/AppShell/Menu.tsx +7 -0
  17. package/AppShell/MenuFab.tsx +132 -0
  18. package/AppShell/MenuFabSecondaryItem.tsx +32 -0
  19. package/AppShell/MinimalPageShellBase.tsx +22 -0
  20. package/AppShell/PageShellHeader/index.tsx +14 -0
  21. package/AppShell/SheetShellBase/index.tsx +107 -0
  22. package/AppShell/SheetShellBase/useSheetStyles.ts +11 -0
  23. package/AppShell/SheetShellDragIndicator/index.tsx +48 -0
  24. package/AppShell/SheetShellHeader/index.tsx +28 -0
  25. package/AppShell/ShellBase.tsx +45 -0
  26. package/AppShell/useFabAnimation.tsx +19 -0
  27. package/AppShell/useFixedFabAnimation.tsx +18 -0
  28. package/AspectRatioContainer/index.tsx +27 -0
  29. package/Blog/BlogAuthor/index.tsx +60 -0
  30. package/Blog/BlogContent/index.tsx +24 -0
  31. package/Blog/BlogHeader/index.tsx +64 -0
  32. package/Blog/BlogList/index.tsx +27 -0
  33. package/Blog/BlogListItem/index.tsx +83 -0
  34. package/Blog/BlogTags/index.tsx +34 -0
  35. package/Blog/BlogTitle/index.tsx +29 -0
  36. package/Button/index.tsx +164 -0
  37. package/ButtonLink/index.tsx +45 -0
  38. package/CHANGELOG.md +1095 -0
  39. package/ChipMenu/index.tsx +130 -0
  40. package/ContainerWithHeader/index.tsx +49 -0
  41. package/Debug/DebugSpacer.tsx +51 -0
  42. package/FlagAvatar/index.tsx +28 -0
  43. package/Form/FormActions.tsx +15 -0
  44. package/Form/FormDivider.tsx +14 -0
  45. package/Form/FormHeader.tsx +27 -0
  46. package/Form/FormRow.tsx +14 -0
  47. package/Form/InputCheckmark.tsx +19 -0
  48. package/Form/index.tsx +62 -0
  49. package/FramerNextPagesSlider/Slide.tsx +71 -0
  50. package/FramerNextPagesSlider/Slider.tsx +39 -0
  51. package/FramerNextPagesSlider/index.ts +1 -0
  52. package/FramerNextPagesSlider/types.ts +3 -0
  53. package/FramerScroller/components/SidebarGallery.tsx +246 -0
  54. package/FramerScroller/components/SidebarSlider.tsx +103 -0
  55. package/FullPageMessage/index.tsx +68 -0
  56. package/Highlight/index.tsx +14 -0
  57. package/IconHeader/index.tsx +109 -0
  58. package/JsonLd/index.tsx +18 -0
  59. package/Page/App.tsx +15 -0
  60. package/Page/Document.tsx +23 -0
  61. package/Page/framerFeatures.ts +4 -0
  62. package/Page/types.ts +19 -0
  63. package/PageLoadIndicator/index.tsx +46 -0
  64. package/PageMeta/index.tsx +40 -0
  65. package/Pagination/index.tsx +123 -0
  66. package/RenderType/index.tsx +40 -0
  67. package/Row/ButtonLinkList/index.tsx +53 -0
  68. package/Row/ColumnOne/index.tsx +11 -0
  69. package/Row/ColumnOneBoxed/index.tsx +27 -0
  70. package/Row/ColumnOneCentered/index.tsx +22 -0
  71. package/Row/ColumnThree/index.tsx +66 -0
  72. package/Row/ColumnTwo/index.tsx +44 -0
  73. package/Row/ColumnTwoSpread/index.tsx +41 -0
  74. package/Row/ColumnTwoWithTop/index.tsx +53 -0
  75. package/Row/ContentLinks/index.tsx +48 -0
  76. package/Row/HeroBanner/index.tsx +102 -0
  77. package/Row/IconBlocks/IconBlock/index.tsx +56 -0
  78. package/Row/IconBlocks/index.tsx +57 -0
  79. package/Row/ParagraphWithSidebarSlide/index.tsx +114 -0
  80. package/Row/Quote/index.tsx +13 -0
  81. package/Row/RowImageText/index.tsx +67 -0
  82. package/Row/RowImageTextBoxed/index.tsx +75 -0
  83. package/Row/SpecialBanner/index.tsx +107 -0
  84. package/Row/index.tsx +13 -0
  85. package/SectionContainer/index.tsx +39 -0
  86. package/SectionHeader/index.tsx +69 -0
  87. package/Separator/index.tsx +33 -0
  88. package/Snackbar/ErrorSnackbar.tsx +9 -0
  89. package/Snackbar/MessageSnackbar.tsx +5 -0
  90. package/Snackbar/MessageSnackbarImpl.tsx +202 -0
  91. package/StarRatingField/index.tsx +58 -0
  92. package/Stepper/Stepper.tsx +45 -0
  93. package/StyledBadge/index.tsx +21 -0
  94. package/Styles/index.tsx +3 -0
  95. package/Styles/responsiveVal.tsx +20 -0
  96. package/SvgImage/SvgImageSimple.tsx +66 -0
  97. package/SvgImage/index.tsx +76 -0
  98. package/TextInputNumber/index.tsx +169 -0
  99. package/Theme/types.ts +63 -0
  100. package/TimeAgo/index.tsx +29 -0
  101. package/Title/index.tsx +71 -0
  102. package/ToggleButton/index.tsx +100 -0
  103. package/ToggleButtonGroup/index.tsx +112 -0
  104. package/UspList/UspListItem.tsx +46 -0
  105. package/UspList/index.tsx +32 -0
  106. package/icons/icon_addresses.svg +17 -0
  107. package/icons/icon_arrow_back.svg +1 -0
  108. package/icons/icon_arrow_forward.svg +1 -0
  109. package/icons/icon_box.svg +6 -0
  110. package/icons/icon_chat.svg +1 -0
  111. package/icons/icon_checkmark.svg +1 -0
  112. package/icons/icon_checkmark_green.svg +1 -0
  113. package/icons/icon_chevron_back.svg +8 -0
  114. package/icons/icon_chevron_down.svg +8 -0
  115. package/icons/icon_chevron_left.svg +8 -0
  116. package/icons/icon_chevron_right.svg +8 -0
  117. package/icons/icon_chevron_up.svg +8 -0
  118. package/icons/icon_close.svg +6 -0
  119. package/icons/icon_close_circle.svg +1 -0
  120. package/icons/icon_collapse_vertical.svg +11 -0
  121. package/icons/icon_customer_service.svg +6 -0
  122. package/icons/icon_email.svg +1 -0
  123. package/icons/icon_email_outline.svg +6 -0
  124. package/icons/icon_expand_vertical.svg +11 -0
  125. package/icons/icon_heart.svg +15 -0
  126. package/icons/icon_home.svg +6 -0
  127. package/icons/icon_id.svg +6 -0
  128. package/icons/icon_invoice_red.svg +7 -0
  129. package/icons/icon_location_red.svg +7 -0
  130. package/icons/icon_lock.svg +6 -0
  131. package/icons/icon_menu.svg +1 -0
  132. package/icons/icon_min.svg +1 -0
  133. package/icons/icon_newspaper.svg +6 -0
  134. package/icons/icon_party.svg +7 -0
  135. package/icons/icon_person.svg +6 -0
  136. package/icons/icon_person_alt.svg +6 -0
  137. package/icons/icon_person_alt_big.svg +15 -0
  138. package/icons/icon_phone.svg +1 -0
  139. package/icons/icon_plus.svg +1 -0
  140. package/icons/icon_sad_face.svg +11 -0
  141. package/icons/icon_sad_shoppingbag.svg +16 -0
  142. package/icons/icon_search.svg +1 -0
  143. package/icons/icon_shopping_bag.svg +7 -0
  144. package/icons/icon_shutdown.svg +6 -0
  145. package/icons/icon_star.svg +6 -0
  146. package/icons/icon_star_filled_muted.svg +6 -0
  147. package/icons/icon_star_yellow.svg +6 -0
  148. package/icons/index.tsx +42 -0
  149. package/index.ts +163 -0
  150. package/package.json +61 -0
  151. package/tsconfig.json +5 -0
  152. package/types.d.ts +13 -0
  153. package/useIntersectionObserver/index.tsx +31 -0
@@ -0,0 +1,202 @@
1
+ import {
2
+ Button,
3
+ Fab,
4
+ makeStyles,
5
+ PropTypes,
6
+ Snackbar,
7
+ SnackbarContent,
8
+ SnackbarProps,
9
+ Theme,
10
+ } from '@material-ui/core'
11
+ import clsx from 'clsx'
12
+ import React, { useEffect, useState } from 'react'
13
+ import SvgImage from '../SvgImage'
14
+ import { iconClose } from '../icons'
15
+
16
+ type Size = 'normal' | 'wide'
17
+ type Variant = 'contained' | 'pill'
18
+
19
+ const useStyles = makeStyles(
20
+ (theme: Theme) => ({
21
+ snackbarRoot: {},
22
+ anchorOriginBottomCenter: {
23
+ left: 0,
24
+ right: 0,
25
+ transform: 'unset',
26
+ bottom: 0,
27
+ pointerEvents: 'none',
28
+ [theme.breakpoints.up('md')]: {
29
+ padding: `${theme.page.vertical} ${theme.page.horizontal}`,
30
+ },
31
+ },
32
+ root: {
33
+ pointerEvents: 'all',
34
+ padding: `16px ${theme.page.horizontal}px max(16px, env(safe-area-inset-bottom))`,
35
+ },
36
+ rootPill: {
37
+ backgroundColor: theme.palette.background.paper,
38
+ color: theme.palette.text.primary,
39
+
40
+ [theme.breakpoints.up('md')]: {
41
+ borderRadius: 50,
42
+ },
43
+ },
44
+ rootPillLarge: {},
45
+ rootPillColorPrimary: {
46
+ backgroundColor: theme.palette.primary.main,
47
+ color: theme.palette.primary.contrastText,
48
+ },
49
+ message: {
50
+ width: '100%',
51
+ padding: theme.spacings.xxs,
52
+ display: 'grid',
53
+ alignItems: 'center',
54
+ gap: theme.spacings.xs,
55
+ gridTemplate: `
56
+ "children close"
57
+ "action action"
58
+ `,
59
+ [theme.breakpoints.up('md')]: {
60
+ gridTemplate: `"children action close"`,
61
+ },
62
+ },
63
+ children: {
64
+ gridArea: 'children',
65
+ ...theme.typography.h4,
66
+ fontWeight: 400,
67
+
68
+ '& .MuiSvgIcon-root': {
69
+ position: 'relative',
70
+ fontSize: '1.1em',
71
+ top: '0.15em',
72
+ },
73
+ },
74
+ actionButton: {
75
+ gridArea: 'action',
76
+ '&:hover, &:focus': {
77
+ backgroundColor: 'transparent',
78
+ },
79
+ '& .MuiPillButton-pill': {
80
+ width: '100%',
81
+ padding: theme.spacings.xxs,
82
+ borderRadius: 40,
83
+ },
84
+ [theme.breakpoints.up('md')]: {
85
+ margin: 0,
86
+ '& .MuiPillButton-pill': {
87
+ width: '100%',
88
+ padding: '8px 16px',
89
+ },
90
+ },
91
+ },
92
+ closeButton: {
93
+ gridArea: 'close',
94
+ '& .MuiSvgIcon-root': {
95
+ height: 24,
96
+ },
97
+ '& .MuiFab-sizeMedium': {
98
+ height: 36,
99
+ width: 36,
100
+ },
101
+ [theme.breakpoints.up('md')]: {
102
+ '& .MuiSvgIcon-root': {
103
+ height: 28,
104
+ },
105
+ '& .MuiFab-sizeMedium': {
106
+ height: 44,
107
+ width: 44,
108
+ },
109
+ },
110
+ },
111
+ sticky: {
112
+ position: 'sticky',
113
+ },
114
+ }),
115
+ { name: 'MessageSnackbar' },
116
+ )
117
+
118
+ export type MessageSnackbarImplProps = Omit<
119
+ SnackbarProps,
120
+ 'autoHideDuration' | 'onClose' | 'anchorOrigin'
121
+ > & {
122
+ autoHide?: boolean
123
+ sticky?: boolean
124
+ variant?: Variant
125
+ size?: Size
126
+ color?: PropTypes.Color
127
+ action?: React.ReactNode
128
+ children?: React.ReactNode
129
+ }
130
+
131
+ export default function MessageSnackbarImpl(props: MessageSnackbarImplProps) {
132
+ const [showSnackbar, setShowSnackbar] = useState<boolean>(false)
133
+
134
+ const {
135
+ variant = 'contained',
136
+ size = 'normal',
137
+ color = 'default',
138
+ autoHide,
139
+ action,
140
+ open,
141
+ message,
142
+ sticky,
143
+ children,
144
+ ...snackbarProps
145
+ } = props
146
+
147
+ const classes = useStyles(props)
148
+
149
+ useEffect(() => {
150
+ setShowSnackbar(!!open)
151
+ }, [open])
152
+
153
+ const clsxBonus = (base: string) => {
154
+ const Size = size[0].toUpperCase() + size.slice(1)
155
+ const Color = color[0].toUpperCase() + color.slice(1)
156
+ const Variant = variant[0].toUpperCase() + variant.slice(1)
157
+
158
+ return clsx(
159
+ classes[base],
160
+ classes[`${base}${Variant}`],
161
+ classes[`${base}${Variant}Size${Size}`],
162
+ classes[`${base}${Variant}Color${Color}`],
163
+ )
164
+ }
165
+
166
+ return (
167
+ <Snackbar
168
+ {...snackbarProps}
169
+ message={message}
170
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
171
+ open={showSnackbar}
172
+ autoHideDuration={autoHide ? 6000 : null}
173
+ classes={{
174
+ root: classes.snackbarRoot,
175
+ anchorOriginBottomCenter: clsx(classes.anchorOriginBottomCenter, sticky && classes.sticky),
176
+ }}
177
+ >
178
+ <SnackbarContent
179
+ classes={{
180
+ root: clsxBonus('root'),
181
+ message: clsxBonus('message'),
182
+ action: clsxBonus('action'),
183
+ }}
184
+ message={
185
+ <>
186
+ <div className={classes.children}>{children}</div>
187
+ {action && (
188
+ <div className={classes.actionButton} onClick={() => setShowSnackbar(false)}>
189
+ {action}
190
+ </div>
191
+ )}
192
+ <div className={classes.closeButton}>
193
+ <Fab aria-label='Close snackbar' size='medium' onClick={() => setShowSnackbar(false)}>
194
+ <SvgImage src={iconClose} size='small' alt='close' />
195
+ </Fab>
196
+ </div>
197
+ </>
198
+ }
199
+ />
200
+ </Snackbar>
201
+ )
202
+ }
@@ -0,0 +1,58 @@
1
+ import { makeStyles, Theme } from '@material-ui/core'
2
+ import { Rating, RatingProps } from '@material-ui/lab'
3
+ import React from 'react'
4
+ import SvgImage from '../SvgImage'
5
+ import { iconStarFilledMuted, iconStarYellow } from '../icons'
6
+
7
+ export type StarRatingFieldProps = {
8
+ id?: string
9
+ onChange?: (id: string, value: number) => void
10
+ iconSize?: number
11
+ } & Omit<RatingProps, 'id' | 'onChange'>
12
+
13
+ const useStyles = makeStyles(
14
+ (theme: Theme) => ({
15
+ icon: {
16
+ marginLeft: 4,
17
+ marginRight: 4,
18
+ },
19
+ }),
20
+ {
21
+ name: 'StarRatingField',
22
+ },
23
+ )
24
+
25
+ export default function StarRatingField(props: StarRatingFieldProps) {
26
+ const { id, onChange = () => {}, iconSize = 20, ...ratingProps } = props
27
+ const classes = useStyles(props)
28
+
29
+ return (
30
+ <Rating
31
+ name={`star-rating-${id}`}
32
+ max={5}
33
+ size='small'
34
+ emptyIcon={
35
+ <SvgImage
36
+ src={iconStarFilledMuted}
37
+ size={iconSize}
38
+ alt='star'
39
+ loading='eager'
40
+ classes={{ root: classes.icon }}
41
+ />
42
+ }
43
+ icon={
44
+ <SvgImage
45
+ src={iconStarYellow}
46
+ size={iconSize}
47
+ alt='star'
48
+ loading='eager'
49
+ classes={{ root: classes.icon }}
50
+ />
51
+ }
52
+ onChange={(event, value) => {
53
+ onChange(id ?? '', value ?? 0)
54
+ }}
55
+ {...ratingProps}
56
+ />
57
+ )
58
+ }
@@ -0,0 +1,45 @@
1
+ import { makeStyles, Theme } from '@material-ui/core'
2
+ import clsx from 'clsx'
3
+ import React from 'react'
4
+ import { UseStyles } from '../Styles'
5
+ import responsiveVal from '../Styles/responsiveVal'
6
+
7
+ const useStyles = makeStyles(
8
+ (theme: Theme) => ({
9
+ root: {
10
+ display: 'grid',
11
+ gridAutoFlow: 'column',
12
+ gap: responsiveVal(8, 12),
13
+ // padding: `0 ${theme.page.horizontal}`,
14
+ },
15
+ step: {
16
+ height: 2,
17
+ background: theme.palette.divider,
18
+ },
19
+ current: {
20
+ background: theme.palette.secondary.main,
21
+ },
22
+ }),
23
+ { name: 'Stepper' },
24
+ )
25
+
26
+ export type StepperProps = {
27
+ steps: number
28
+ currentStep: number
29
+ } & UseStyles<typeof useStyles>
30
+
31
+ export default function Stepper(props: StepperProps) {
32
+ const { steps, currentStep } = props
33
+ const classes = useStyles(props)
34
+
35
+ return (
36
+ <div className={classes.root}>
37
+ {[...Array(steps).keys()].map((step: number) => (
38
+ <div
39
+ className={clsx(classes.step, { [classes.current]: currentStep - 1 >= step })}
40
+ key={step}
41
+ />
42
+ ))}
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,21 @@
1
+ import { Badge } from '@material-ui/core'
2
+ import { withStyles } from '@material-ui/styles'
3
+
4
+ const StyledBadge = withStyles((theme) => ({
5
+ colorError: {},
6
+ badge: {
7
+ right: 6,
8
+ top: 6,
9
+ padding: 3,
10
+ color: '#FFF',
11
+ borderRadius: '100%',
12
+ fontSize: 10,
13
+ [theme.breakpoints.up('md')]: {
14
+ padding: 6,
15
+ right: 8,
16
+ top: 8,
17
+ },
18
+ },
19
+ }))(Badge)
20
+
21
+ export default StyledBadge
@@ -0,0 +1,3 @@
1
+ export type UseStyles<T extends (...args: never[]) => unknown> = {
2
+ classes?: Partial<ReturnType<T>>
3
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This will create a css value that sizes based on the viewport width.
3
+ *
4
+ * E.g.: `responsiveVal(16, 22)` -> Will render 16px at 320px window width, 22 ad 1280 window width
5
+ */
6
+ export default function responsiveVal(
7
+ min: number,
8
+ max: number,
9
+ maxBreakpoint = 1280,
10
+ ): `max(${number}px, min(${string}, ${number}px))` {
11
+ const round = (x: number, n: number): number => Math.round(x * 10 ** n) / 10 ** n
12
+
13
+ const minBreakpoint = 320
14
+ const growth = (max - min) / (maxBreakpoint - minBreakpoint)
15
+ const base = round(min - growth * minBreakpoint, 2)
16
+ const vsize = round(growth * 100, 2)
17
+
18
+ const calc = `(${base}px + ${vsize}vw)`
19
+ return `max(${min}px, min(${calc}, ${max}px))`
20
+ }
@@ -0,0 +1,66 @@
1
+ import { makeStyles, capitalize, Theme } from '@material-ui/core'
2
+ import { Image, ImageProps } from '@graphcommerce/image'
3
+ import clsx from 'clsx'
4
+ import { forwardRef } from 'react'
5
+ import responsiveVal from '../Styles/responsiveVal'
6
+
7
+ export type SvgImageShade = 'muted' | 'default' | 'inverted'
8
+
9
+ const useStyles = makeStyles(
10
+ (theme: Theme) => ({
11
+ image: {
12
+ display: 'block',
13
+ flexShrink: 0,
14
+ userSelect: 'none',
15
+ width: 24,
16
+ height: 'auto',
17
+ },
18
+ /* Styles applied to the element if `size='inherit'`. */
19
+ sizeInherit: {
20
+ fontSize: 'inherit',
21
+ },
22
+ /* Styles applied to the element if `size='small'`. */
23
+ sizeSmall: {
24
+ width: 20,
25
+ },
26
+ /* Styles applied to the element if `size='large'`. */
27
+ sizeLarge: {
28
+ width: responsiveVal(28, 32),
29
+ },
30
+ muted: { filter: `invert(75%)` },
31
+ inverted: { filter: `invert(100%)` },
32
+ }),
33
+ { name: 'SvgImageSimple' },
34
+ )
35
+
36
+ type SvgImageSimpleProps = Omit<ImageProps, 'fixed'> & {
37
+ /** The fontSize applied to the icon. Defaults to 24px, but can be configure to inherit font size. */
38
+ size?: 'default' | 'inherit' | 'large' | 'medium' | 'small'
39
+
40
+ muted?: boolean
41
+ inverted?: boolean
42
+ }
43
+
44
+ const SvgImageSimple = forwardRef<HTMLImageElement, SvgImageSimpleProps>((props, ref) => {
45
+ const { className, size = 'medium', muted, inverted, ...imageProps } = props
46
+ const classes = useStyles()
47
+
48
+ return (
49
+ <Image
50
+ {...imageProps}
51
+ ref={ref}
52
+ layout='fixed'
53
+ className={clsx(
54
+ className,
55
+ classes.image,
56
+ classes?.[`size${capitalize(size)}`],
57
+ muted && classes.muted,
58
+ inverted && classes.inverted,
59
+ )}
60
+ unoptimized
61
+ />
62
+ )
63
+ })
64
+ SvgImageSimple.displayName = 'SvgImageSimple'
65
+
66
+ export default SvgImageSimple
@@ -0,0 +1,76 @@
1
+ import { makeStyles, Theme } from '@material-ui/core'
2
+ import { Image, ImageProps } from '@graphcommerce/image'
3
+ import React from 'react'
4
+ import { UseStyles } from '../Styles'
5
+
6
+ // TODO: fix hot reloading issues when modifying implementations of this component
7
+ export type SvgImageSize = 'small' | 'medium' | 'large' | 'extralarge'
8
+
9
+ export type SvgImageProps = ImageProps & {
10
+ size?: SvgImageSize | number
11
+ mobileSize?: SvgImageSize | number
12
+ shade?: SvgImageShade
13
+ } & UseStyles<typeof useStyles>
14
+
15
+ export type SvgImageShade = 'muted' | 'default' | 'inverted'
16
+
17
+ export const SvgImageShades: Record<SvgImageShade, number> = {
18
+ muted: 75,
19
+ default: 0,
20
+ inverted: 100,
21
+ }
22
+
23
+ export type UseStylesProps = { shade: number; baseSize: number; mobileSize: number }
24
+
25
+ const useStyles = makeStyles(
26
+ (theme: Theme) => ({
27
+ root: ({ shade, baseSize, mobileSize }: UseStylesProps) => ({
28
+ filter: shade > 0 ? `invert(${shade}%)` : undefined,
29
+ objectFit: 'contain',
30
+ width: mobileSize ?? Math.round(baseSize * 0.75),
31
+ height: mobileSize ?? Math.round(baseSize * 0.75),
32
+ [theme.breakpoints.up('md')]: {
33
+ width: baseSize,
34
+ height: baseSize,
35
+ },
36
+ }),
37
+ }),
38
+ { name: 'SvgImage' },
39
+ )
40
+
41
+ export default function SvgImage(props: SvgImageProps) {
42
+ const { src, size = 'medium', mobileSize, shade = 'default', ...imageProps } = props
43
+
44
+ // if (isStaticImageData(src)) {
45
+ // console.log(src.height)
46
+ // console.log(src.width)
47
+ // }
48
+
49
+ const baseSizes = {
50
+ small: 24,
51
+ medium: 32,
52
+ large: 48,
53
+ extralarge: 64,
54
+ }
55
+
56
+ const classes = useStyles({
57
+ ...props,
58
+ mobileSize: baseSizes[mobileSize ?? ''] ?? mobileSize,
59
+ baseSize: baseSizes[size ?? ''] ?? size,
60
+ shade: SvgImageShades[shade],
61
+ })
62
+
63
+ return (
64
+ <>
65
+ <Image
66
+ layout='fixed'
67
+ unoptimized
68
+ src={src}
69
+ className={classes.root}
70
+ // height={24}
71
+ // width={24}
72
+ {...imageProps}
73
+ />
74
+ </>
75
+ )
76
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ IconButton,
3
+ IconButtonProps,
4
+ makeStyles,
5
+ OutlinedTextFieldProps,
6
+ TextField,
7
+ TextFieldProps,
8
+ Theme,
9
+ useForkRef,
10
+ } from '@material-ui/core'
11
+ import clsx from 'clsx'
12
+ import React, { ChangeEvent, Ref, useCallback, useEffect, useRef, useState } from 'react'
13
+ import { UseStyles } from '../Styles'
14
+ import responsiveVal from '../Styles/responsiveVal'
15
+ import SvgImage from '../SvgImage'
16
+ import { iconMin, iconPlus } from '../icons'
17
+
18
+ const useStyles = makeStyles(
19
+ (theme: Theme) => ({
20
+ quantity: {
21
+ width: responsiveVal(80, 120),
22
+ backgroundColor: theme.palette.background.default,
23
+ },
24
+ quantityInput: {
25
+ textAlign: 'center',
26
+ '&::-webkit-inner-spin-button,&::-webkit-outer-spin-button': {
27
+ appearance: 'none',
28
+ },
29
+ },
30
+ button: {
31
+ fontSize: 22,
32
+ },
33
+ adornedEnd: {
34
+ paddingRight: responsiveVal(7, 14),
35
+ },
36
+ adornedStart: {
37
+ paddingLeft: responsiveVal(7, 14),
38
+ },
39
+ }),
40
+ { name: 'TextInputNumber' },
41
+ )
42
+
43
+ export type IconButtonPropsOmit = Omit<
44
+ IconButtonProps,
45
+ 'aria-label' | 'size' | 'onMouseDown' | 'onMouseUp' | 'disabled'
46
+ >
47
+
48
+ export type TextInputNumberProps = Omit<TextFieldProps, 'type'> & {
49
+ DownProps?: IconButtonPropsOmit
50
+ UpProps?: IconButtonPropsOmit
51
+ } & UseStyles<typeof useStyles>
52
+
53
+ function isOutlined(props: TextFieldProps): props is OutlinedTextFieldProps {
54
+ return props.variant === 'outlined'
55
+ }
56
+
57
+ export default function TextInputNumber(props: TextInputNumberProps) {
58
+ const { DownProps = {}, UpProps = {}, inputProps = {}, inputRef, ...textFieldProps } = props
59
+ const classes = useStyles(props)
60
+ const ref = useRef<HTMLInputElement>(null)
61
+ const forkRef = useForkRef<HTMLInputElement>(ref, inputRef as Ref<HTMLInputElement>)
62
+
63
+ const [direction, setDirection] = useState<'up' | 'down' | 'runUp' | 'runDown' | null>(null)
64
+ const [disabled, setDisabled] = useState<'min' | 'max' | null>(null)
65
+
66
+ const stop = useCallback(() => setDirection(null), [])
67
+
68
+ const down = useCallback(() => {
69
+ if ((ref.current?.value ?? 0) <= inputProps.min) {
70
+ setDirection(null)
71
+ return
72
+ }
73
+
74
+ ref.current?.stepDown()
75
+ ref.current?.dispatchEvent(new Event('change', { bubbles: true }))
76
+ }, [inputProps.min])
77
+
78
+ const up = useCallback(() => {
79
+ if ((ref.current?.value ?? Infinity) >= inputProps.max) {
80
+ setDirection(null)
81
+ return
82
+ }
83
+
84
+ ref.current?.stepUp()
85
+ ref.current?.dispatchEvent(new Event('change', { bubbles: true }))
86
+ }, [inputProps.max])
87
+
88
+ useEffect(() => {
89
+ if (direction === 'up') up()
90
+ if (direction === 'down') down()
91
+ }, [direction, down, inputProps.max, inputProps.min, ref, stop, up])
92
+
93
+ const updateDisabled = (target: HTMLInputElement) => {
94
+ if (target.value === target.min) setDisabled('min')
95
+ else if (target.value === target.max) setDisabled('max')
96
+ else setDisabled(null)
97
+ }
98
+
99
+ useEffect(() => {
100
+ if (!ref.current) return
101
+ setTimeout(() => ref.current && updateDisabled(ref.current))
102
+ }, [ref, inputProps.min, inputProps.max])
103
+
104
+ if (!textFieldProps.InputProps) textFieldProps.InputProps = {}
105
+ if (isOutlined(textFieldProps)) {
106
+ textFieldProps.InputProps.classes = {
107
+ ...textFieldProps.InputProps?.classes,
108
+ adornedEnd: classes.adornedEnd,
109
+ adornedStart: classes.adornedStart,
110
+ }
111
+ }
112
+
113
+ return (
114
+ <TextField
115
+ {...textFieldProps}
116
+ type='number'
117
+ inputRef={forkRef}
118
+ className={clsx(textFieldProps.className, classes.quantity)}
119
+ autoComplete='off'
120
+ InputProps={{
121
+ ...textFieldProps.InputProps,
122
+ startAdornment: (
123
+ <IconButton
124
+ aria-label='step down'
125
+ size='small'
126
+ edge='start'
127
+ onPointerDown={() => setDirection('down')}
128
+ onPointerUp={stop}
129
+ // disabled={textFieldProps.disabled || disabled === 'min'}
130
+ tabIndex='-1'
131
+ color='inherit'
132
+ className={clsx(classes.button, DownProps.className)}
133
+ {...DownProps}
134
+ >
135
+ {DownProps.children ?? (
136
+ <SvgImage src={iconMin} size='small' alt='Step down' loading='eager' />
137
+ )}
138
+ </IconButton>
139
+ ),
140
+ endAdornment: (
141
+ <IconButton
142
+ aria-label='step up'
143
+ size='small'
144
+ edge='end'
145
+ onPointerDown={() => setDirection('up')}
146
+ onPointerUp={() => setDirection(null)}
147
+ // disabled={textFieldProps.disabled || disabled === 'max'}
148
+ tabIndex='-1'
149
+ color='inherit'
150
+ className={clsx(classes.button, UpProps.className)}
151
+ {...UpProps}
152
+ >
153
+ {UpProps.children ?? (
154
+ <SvgImage src={iconPlus} size='small' alt='Step up' loading='eager' />
155
+ )}
156
+ </IconButton>
157
+ ),
158
+ }}
159
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
160
+ if (textFieldProps.onChange) textFieldProps.onChange(e)
161
+ updateDisabled(e.target)
162
+ }}
163
+ inputProps={{
164
+ ...inputProps,
165
+ className: clsx(inputProps?.className, classes.quantityInput),
166
+ }}
167
+ />
168
+ )
169
+ }