@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
package/.babelrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "presets": ["next/babel"]
3
+ }
@@ -0,0 +1,20 @@
1
+ import { HTMLMotionProps, m } from 'framer-motion'
2
+ import { ReactHTML } from 'react'
3
+
4
+ export type AnimatedRowProps = Omit<
5
+ ReactHTML['div'] & HTMLMotionProps<'div'>,
6
+ 'layout' | 'initial' | 'animate' | 'exit' | 'transition'
7
+ >
8
+
9
+ export default function AnimatedRow(props: AnimatedRowProps) {
10
+ return (
11
+ <m.div
12
+ {...props}
13
+ layout
14
+ initial={{ opacity: 0, height: 0 }}
15
+ animate={{ opacity: 1, height: 'auto' }}
16
+ exit={{ opacity: 0, height: 0 }}
17
+ transition={{ type: 'tween' }}
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,59 @@
1
+ import { ApolloError } from '@apollo/client'
2
+ import { makeStyles, Theme } from '@material-ui/core'
3
+ import { Alert, AlertProps } from '@material-ui/lab'
4
+ import { AnimatePresence } from 'framer-motion'
5
+ import React from 'react'
6
+ import AnimatedRow from '../AnimatedRow'
7
+
8
+ export const useStyles = makeStyles(
9
+ (theme: Theme) => ({
10
+ alerts: {},
11
+ alert: {
12
+ paddingTop: `calc(${theme.spacings.xxs} / 2)`,
13
+ paddingBottom: `calc(${theme.spacings.xxs} / 2)`,
14
+ },
15
+ }),
16
+ { name: 'ApolloErrorAlert' },
17
+ )
18
+
19
+ export type ApolloErrorAlertProps = {
20
+ error?: ApolloError
21
+ graphqlErrorAlertProps?: Omit<AlertProps, 'severity'>
22
+ networkErrorAlertProps?: Omit<AlertProps, 'severity'>
23
+ }
24
+ export default function ApolloErrorAlert(props: ApolloErrorAlertProps) {
25
+ const classes = useStyles()
26
+ const { error, graphqlErrorAlertProps, networkErrorAlertProps } = props
27
+
28
+ return (
29
+ <AnimatePresence initial={false}>
30
+ {error && (
31
+ <AnimatedRow key='alerts'>
32
+ <div className={classes.alerts}>
33
+ <AnimatePresence initial={false}>
34
+ {error.graphQLErrors.map((e, index) => (
35
+ // eslint-disable-next-line react/no-array-index-key
36
+ <AnimatedRow key={index}>
37
+ <div className={classes.alert}>
38
+ <Alert severity='error' {...graphqlErrorAlertProps}>
39
+ {e.message}
40
+ </Alert>
41
+ </div>
42
+ </AnimatedRow>
43
+ ))}
44
+ {error.networkError && (
45
+ <AnimatedRow key='networkError'>
46
+ <div className={classes.alert} key='networkError'>
47
+ <Alert severity='error' {...networkErrorAlertProps}>
48
+ Network Error: {error.networkError.message}
49
+ </Alert>
50
+ </div>
51
+ </AnimatedRow>
52
+ )}
53
+ </AnimatePresence>
54
+ </div>
55
+ </AnimatedRow>
56
+ )}
57
+ </AnimatePresence>
58
+ )
59
+ }
@@ -0,0 +1,25 @@
1
+ import { ApolloError } from '@apollo/client'
2
+ import { AlertProps } from '@material-ui/lab'
3
+ import React from 'react'
4
+ import FullPageMessage, { FullPageMessageProps } from '../FullPageMessage'
5
+ import ApolloErrorAlert from './ApolloErrorAlert'
6
+
7
+ export type ApolloErrorFullPageProps = {
8
+ error?: ApolloError
9
+ graphqlErrorAlertProps?: Omit<AlertProps, 'severity'>
10
+ networkErrorAlertProps?: Omit<AlertProps, 'severity'>
11
+ } & Omit<FullPageMessageProps, 'title' | 'description'>
12
+
13
+ export default function ApolloErrorFullPage(props: ApolloErrorFullPageProps) {
14
+ const { error, graphqlErrorAlertProps, networkErrorAlertProps, ...fullPageMessageProps } = props
15
+
16
+ const singleError = error?.graphQLErrors.length === 1
17
+
18
+ return (
19
+ <FullPageMessage
20
+ title={singleError ? error?.graphQLErrors[0].message : 'Several errors occured'}
21
+ description={singleError ? undefined : <ApolloErrorAlert error={error} />}
22
+ {...fullPageMessageProps}
23
+ />
24
+ )
25
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+
3
+ export type AppShellHeaderContext = {
4
+ titleRef: React.RefObject<HTMLDivElement>
5
+ contentHeaderRef: React.RefObject<HTMLDivElement>
6
+ }
7
+
8
+ const appShellHeaderContext = React.createContext(undefined as unknown as AppShellHeaderContext)
9
+ appShellHeaderContext.displayName = 'appShellHeaderContext'
10
+
11
+ export default appShellHeaderContext
@@ -0,0 +1,433 @@
1
+ import { Fab, makeStyles, Theme } from '@material-ui/core'
2
+ import { useHistoryLink, usePageContext, usePageRouter } from '@graphcommerce/framer-next-pages'
3
+ import clsx from 'clsx'
4
+ import { m, MotionValue, useMotionValue, useTransform } from 'framer-motion'
5
+ import PageLink from 'next/link'
6
+ import React, { useCallback, useEffect } from 'react'
7
+ import Button from '../../Button'
8
+ import { UseStyles } from '../../Styles'
9
+ import SvgImage from '../../SvgImage'
10
+ import { iconChevronLeft, iconClose } from '../../icons'
11
+ import useAppShellHeaderContext from './useAppShellHeaderContext'
12
+
13
+ export type AppShellHeaderProps = {
14
+ children?: React.ReactNode
15
+ primary?: React.ReactNode
16
+ secondary?: React.ReactNode
17
+ divider?: React.ReactNode
18
+ /* When a logo is given, children should be given too */
19
+ logo?: React.ReactNode
20
+ scrollY: MotionValue<number>
21
+ hideClose?: boolean
22
+ scrolled?: boolean
23
+ dragIndicator?: React.ReactNode
24
+ additional?: React.ReactNode
25
+ backFallbackHref?: string | null
26
+ backFallbackTitle?: string | null
27
+ fill?: 'both' | 'mobile-only'
28
+ sheet?: boolean
29
+ } & UseStyles<typeof useStyles>
30
+
31
+ // minHeight: x
32
+ // to reserve space for back & primary buttons,
33
+ // even when there is no app shell header on scroll (e.g. on full page shell)
34
+ const minHeight = 40
35
+ const useStyles = makeStyles(
36
+ (theme: Theme) => ({
37
+ divider: {
38
+ borderBottom: `1px solid ${theme.palette.divider}`,
39
+ },
40
+ dividerSpacer: {
41
+ minHeight: 2,
42
+ },
43
+ dividerSheetShell: {
44
+ marginTop: `calc((${theme.page.headerInnerHeight.md} * 0.15))`,
45
+ },
46
+ sheetHeaderContainer: {
47
+ position: 'sticky',
48
+ top: 0,
49
+ zIndex: 98,
50
+ // reserve space in the container even without any buttons added
51
+ minHeight: 58,
52
+ },
53
+ sheetHeaderContainerSheetShell: {
54
+ marginBottom: `calc((${theme.page.headerInnerHeight.md} * 0.15) * -1)`,
55
+ },
56
+ sheetHeader: {
57
+ background: theme.palette.background.default,
58
+ paddingTop: 8,
59
+ paddingBottom: 8,
60
+ minHeight,
61
+ [theme.breakpoints.up('md')]: {
62
+ minHeight: `calc(${minHeight}px + ${theme.spacings.xxs} * 2)`,
63
+ paddingTop: theme.spacings.xxs,
64
+ paddingBottom: theme.spacings.xxs,
65
+ },
66
+ },
67
+ sheetHeaderSheetShell: {
68
+ // The bottom sheets offset top is x% short compared to the page headers height,
69
+ // so we add x% of the header height to padding top of bottomsheet actions bar
70
+ // to keep consistency between app shell buttons.
71
+ paddingTop: `calc(${theme.spacings.xxs} + (${theme.page.headerInnerHeight.md} * 0.15))`,
72
+ paddingBottom: `calc(${theme.spacings.xxs} + (${theme.page.headerInnerHeight.md} * 0.15))`,
73
+ [theme.breakpoints.down('sm')]: {
74
+ paddingTop: 8,
75
+ paddingBottom: 8,
76
+ },
77
+ },
78
+ sheetHeaderScrolled: {
79
+ [theme.breakpoints.up('md')]: {
80
+ marginTop: -60,
81
+ },
82
+ },
83
+ sheetHeaderActions: {
84
+ display: 'grid',
85
+ gridTemplateColumns: `1fr auto 1fr`,
86
+ gridAutoFlow: 'column',
87
+ alignItems: 'center',
88
+ justifyContent: 'space-between',
89
+ padding: `0 calc(${theme.page.horizontal} + 2px) 0`,
90
+ width: '100%',
91
+ minHeight,
92
+ [theme.breakpoints.up('md')]: {
93
+ '& * > a, & * > button': {
94
+ height: minHeight,
95
+ },
96
+ },
97
+ [theme.breakpoints.down('sm')]: {
98
+ '& div > .MuiFab-sizeSmall': {
99
+ marginLeft: -8,
100
+ marginRight: -8,
101
+ },
102
+ '& div > .MuiButtonBase-root': {
103
+ minWidth: 'unset',
104
+ marginRight: -8,
105
+ marginLeft: -8,
106
+ },
107
+ },
108
+ },
109
+ sheetHeaderActionRight: {
110
+ justifySelf: 'flex-end',
111
+ '& > .Mui-disabled': {
112
+ opacity: 0.25,
113
+ color: `${theme.palette.primary.contrastText} !important`,
114
+ [theme.breakpoints.up('md')]: {
115
+ color: `${theme.palette.secondary.contrastText} !important`,
116
+ },
117
+ },
118
+ },
119
+ sheetHeaderNoTitle: {
120
+ pointerEvents: 'none',
121
+ background: 'transparent',
122
+ },
123
+ sheetHeaderNoTitleFillMobileOnly: {
124
+ [theme.breakpoints.up('md')]: {
125
+ pointerEvents: 'none',
126
+ background: 'transparent',
127
+ top: 98,
128
+ },
129
+ },
130
+ sheetHeaderFillMobileOnly: {
131
+ [theme.breakpoints.up('md')]: {
132
+ pointerEvents: 'none',
133
+ background: 'transparent',
134
+ marginBottom: `calc((${theme.page.headerInnerHeight.md} * -1) + ${theme.spacings.xxs} * 2)`,
135
+ },
136
+ },
137
+ innerContainer: {
138
+ display: 'grid',
139
+ textAlign: 'center',
140
+ pointerEvents: 'all',
141
+ },
142
+ fab: {
143
+ maxWidth: 38,
144
+ maxHeight: 38,
145
+ [theme.breakpoints.down('sm')]: {
146
+ boxShadow: 'none',
147
+ },
148
+ },
149
+ childs: {
150
+ marginLeft: 12,
151
+ marginRight: 12,
152
+ whiteSpace: 'nowrap',
153
+ overflow: 'hidden',
154
+ textOverflow: 'ellipsis',
155
+ },
156
+ fillMobileOnly: {
157
+ [theme.breakpoints.up('md')]: {
158
+ display: 'none',
159
+ },
160
+ },
161
+ dividerFillMobileOnly: {
162
+ [theme.breakpoints.up('md')]: {
163
+ visibility: 'hidden',
164
+ },
165
+ },
166
+ logoContainer: {
167
+ position: 'absolute',
168
+ top: 0,
169
+ left: 0,
170
+ right: 0,
171
+ paddingTop: 8,
172
+ paddingBottom: 8,
173
+ },
174
+ logoInnerContainer: {
175
+ minHeight,
176
+ display: 'flex',
177
+ alignItems: 'center',
178
+ [theme.breakpoints.up('md')]: {
179
+ display: 'none',
180
+ },
181
+ },
182
+ backButton: {
183
+ background: theme.palette.background.default,
184
+ color: theme.palette.text.primary,
185
+ '&:hover': {
186
+ background: theme.palette.background.highlight,
187
+ },
188
+ },
189
+ sheetShellActionsFullPage: {
190
+ '& * > a, & * > button': {
191
+ pointerEvents: 'all',
192
+ },
193
+ '& * > button': {
194
+ boxShadow: theme.shadows[3],
195
+ },
196
+ [theme.breakpoints.up('md')]: {
197
+ position: 'fixed',
198
+ top: `calc(${theme.page.headerInnerHeight.md} + ${theme.spacings.xxs})`,
199
+ },
200
+ },
201
+ sheetShellActionsNoButtonShadow: {
202
+ [theme.breakpoints.down('sm')]: {
203
+ '& * > button': {
204
+ boxShadow: 'none',
205
+ },
206
+ },
207
+ },
208
+ }),
209
+ { name: 'AppShellHeader' },
210
+ )
211
+
212
+ export default function AppShellHeader(props: AppShellHeaderProps) {
213
+ const {
214
+ children,
215
+ logo,
216
+ divider,
217
+ primary = null,
218
+ secondary = null,
219
+ hideClose,
220
+ scrollY,
221
+ additional,
222
+ dragIndicator,
223
+ scrolled,
224
+ backFallbackHref,
225
+ backFallbackTitle,
226
+ fill = 'both',
227
+ sheet,
228
+ } = props
229
+ const router = usePageRouter()
230
+ const { closeSteps, backSteps } = usePageContext()
231
+ const classes = useStyles(props)
232
+
233
+ const { titleRef, contentHeaderRef } = useAppShellHeaderContext()
234
+
235
+ const noChildren = typeof children === 'undefined' || !children
236
+
237
+ const fillMobileOnly = fill === 'mobile-only'
238
+
239
+ const sheetHeaderHeight = useMotionValue<number>(0)
240
+ const titleOffset = useMotionValue<number>(100)
241
+ const titleHeight = useMotionValue<number>(100)
242
+
243
+ const { href: historyHref, onClick: historyOnClick } = useHistoryLink({
244
+ href: backFallbackHref ?? '',
245
+ })
246
+
247
+ const setOffset = useCallback(
248
+ (offsetTop: number, offsetParent: Element | null, clientHeight: number) => {
249
+ titleHeight.set(clientHeight)
250
+ titleOffset.set(offsetTop)
251
+ },
252
+ [titleHeight, titleOffset],
253
+ )
254
+
255
+ if (titleRef.current) {
256
+ setOffset(
257
+ titleRef.current.offsetTop,
258
+ titleRef.current.offsetParent,
259
+ titleRef.current.clientHeight,
260
+ )
261
+ }
262
+
263
+ // Measure the title sizes so we can animate the opacity
264
+ useEffect(() => {
265
+ if (!titleRef.current) return () => {}
266
+
267
+ const ro = new ResizeObserver(([entry]) => {
268
+ const { offsetTop, offsetParent, clientHeight } = entry.target as HTMLDivElement
269
+
270
+ setOffset(offsetTop, offsetParent, clientHeight)
271
+ })
272
+
273
+ ro.observe(titleRef.current)
274
+ return () => ro.disconnect()
275
+ }, [setOffset, titleHeight, titleOffset, titleRef])
276
+
277
+ // Measure the sheetHeight sizes so we can animate the opacity
278
+ useEffect(() => {
279
+ if (!contentHeaderRef.current) return () => {}
280
+
281
+ const ro = new ResizeObserver(([entry]) =>
282
+ sheetHeaderHeight.set((entry.target as HTMLDivElement).clientHeight),
283
+ )
284
+
285
+ ro.observe(contentHeaderRef.current)
286
+
287
+ return () => ro.disconnect()
288
+ }, [contentHeaderRef, sheetHeaderHeight])
289
+
290
+ const opacityTitle = useTransform(
291
+ [scrollY, sheetHeaderHeight, titleOffset, titleHeight] as MotionValue[],
292
+ ([scrollYV, sheetHeaderHeightV, titleOffsetV, titleHeigthV]: number[]) =>
293
+ Math.min(
294
+ Math.max(
295
+ 0,
296
+ scrolled
297
+ ? 1
298
+ : (scrollYV - Math.max(titleOffsetV, 80) + sheetHeaderHeightV) / titleHeigthV,
299
+ ),
300
+ 1,
301
+ ),
302
+ )
303
+
304
+ const pointerEvents = useTransform(opacityTitle, (o) => (o < 0.2 ? 'none' : 'all'))
305
+ const opacityLogo = useTransform(opacityTitle, [0, 1], [1, fillMobileOnly && primary ? 1 : 0])
306
+ const pointerEventsLogo = useTransform(opacityLogo, (o) => (o < 0.2 ? 'none' : 'all'))
307
+
308
+ const close =
309
+ !hideClose &&
310
+ (closeSteps > 0 ? (
311
+ <Fab
312
+ size='small'
313
+ type='button'
314
+ classes={{ root: classes.fab }}
315
+ onClick={() => router.go(closeSteps * -1)}
316
+ >
317
+ <SvgImage src={iconClose} mobileSize={20} size={20} alt='Close overlay' loading='eager' />
318
+ </Fab>
319
+ ) : (
320
+ <PageLink href='/' passHref>
321
+ <Fab size='small' classes={{ root: classes.fab }}>
322
+ <SvgImage src={iconClose} alt='Close overlay' size={20} mobileSize={20} loading='eager' />
323
+ </Fab>
324
+ </PageLink>
325
+ ))
326
+
327
+ const backIcon = (
328
+ <SvgImage src={iconChevronLeft} alt='chevron back' loading='eager' size={26} mobileSize={30} />
329
+ )
330
+ let back = backSteps > 0 && (
331
+ <Button
332
+ onClick={() => router.back()}
333
+ variant='pill-link'
334
+ className={classes.backButton}
335
+ startIcon={backIcon}
336
+ >
337
+ {historyOnClick ? backFallbackTitle : 'Back'}
338
+ </Button>
339
+ )
340
+ if (!back && backFallbackHref) {
341
+ back = (
342
+ <PageLink href={backFallbackHref} passHref>
343
+ <Button variant='pill-link' className={classes.backButton} startIcon={backIcon}>
344
+ {backFallbackTitle ?? 'Back'}
345
+ </Button>
346
+ </PageLink>
347
+ )
348
+ }
349
+
350
+ let leftAction: React.ReactNode = secondary ?? back
351
+ const rightAction: React.ReactNode = primary ?? close
352
+ if (rightAction !== close && !leftAction) leftAction = close
353
+ if (!leftAction) leftAction = <div />
354
+
355
+ const showDivider = children || (fillMobileOnly && primary)
356
+
357
+ return (
358
+ <div
359
+ className={clsx(
360
+ classes.sheetHeaderContainer,
361
+ noChildren && !primary && classes.sheetHeaderNoTitle,
362
+ fillMobileOnly && classes.sheetHeaderFillMobileOnly,
363
+ sheet && classes.sheetHeaderContainerSheetShell,
364
+ )}
365
+ >
366
+ <div
367
+ className={clsx(
368
+ classes?.sheetHeader,
369
+ sheet && classes.sheetHeaderSheetShell,
370
+ scrolled && classes?.sheetHeaderScrolled,
371
+ noChildren && !primary && classes.sheetHeaderNoTitle,
372
+ fillMobileOnly && noChildren && classes.sheetHeaderNoTitleFillMobileOnly,
373
+ fillMobileOnly && classes.sheetHeaderFillMobileOnly,
374
+ )}
375
+ ref={contentHeaderRef}
376
+ >
377
+ <div className={classes.logoContainer}>
378
+ {logo && (
379
+ <m.div
380
+ style={{
381
+ opacity: opacityLogo,
382
+ pointerEvents: pointerEventsLogo,
383
+ }}
384
+ className={classes.logoInnerContainer}
385
+ >
386
+ {logo}
387
+ </m.div>
388
+ )}
389
+ </div>
390
+
391
+ {dragIndicator}
392
+
393
+ <div
394
+ className={clsx(
395
+ classes.sheetHeaderActions,
396
+ (noChildren || fillMobileOnly) && classes.sheetShellActionsFullPage,
397
+ fillMobileOnly && showDivider && classes.sheetShellActionsNoButtonShadow,
398
+ )}
399
+ >
400
+ {leftAction && <div>{leftAction}</div>}
401
+ <div className={classes.innerContainer}>
402
+ {children && (
403
+ <m.div
404
+ style={{ opacity: opacityTitle, pointerEvents }}
405
+ className={clsx(classes.childs, fillMobileOnly && classes.fillMobileOnly)}
406
+ >
407
+ {children}
408
+ </m.div>
409
+ )}
410
+ </div>
411
+ <div className={classes?.sheetHeaderActionRight}>{rightAction}</div>
412
+ </div>
413
+ {additional && (
414
+ <div className={clsx(fillMobileOnly && classes.fillMobileOnly)}>
415
+ <>{additional}</>
416
+ </div>
417
+ )}
418
+ </div>
419
+ {showDivider &&
420
+ (divider ?? (
421
+ <m.div
422
+ className={clsx(
423
+ classes.dividerSpacer,
424
+ classes.divider,
425
+ fillMobileOnly && classes.dividerFillMobileOnly,
426
+ )}
427
+ style={{ opacity: opacityTitle }}
428
+ />
429
+ ))}
430
+ {!showDivider && <div className={classes.dividerSpacer} />}
431
+ </div>
432
+ )
433
+ }
@@ -0,0 +1,6 @@
1
+ import { useContext } from 'react'
2
+ import appShellHeaderContext from './appShellHeaderContext'
3
+
4
+ export default function useAppShellHeaderContext() {
5
+ return useContext(appShellHeaderContext)
6
+ }
@@ -0,0 +1,18 @@
1
+ import { useRef } from 'react'
2
+ import appShellHeaderContext, {
3
+ AppShellHeaderContext,
4
+ } from '../AppShellHeader/appShellHeaderContext'
5
+
6
+ type AppShellProviderProps = {
7
+ children: React.ReactNode
8
+ }
9
+
10
+ export default function AppShellProvider(props: AppShellProviderProps) {
11
+ const { children } = props
12
+ const context: AppShellHeaderContext = {
13
+ titleRef: useRef<HTMLDivElement>(null),
14
+ contentHeaderRef: useRef<HTMLDivElement>(null),
15
+ }
16
+
17
+ return <appShellHeaderContext.Provider value={context}>{children}</appShellHeaderContext.Provider>
18
+ }
@@ -0,0 +1,66 @@
1
+ import { Container, makeStyles, Theme } from '@material-ui/core'
2
+ import clsx from 'clsx'
3
+ import { useMotionValue } from 'framer-motion'
4
+ import React, { useEffect } from 'react'
5
+ import { useMotionValueValue } from '../../../framer-utils'
6
+ import { UseStyles } from '../../Styles'
7
+ import useAppShellHeaderContext from '../AppShellHeader/useAppShellHeaderContext'
8
+
9
+ const useStyles = makeStyles(
10
+ (theme: Theme) => ({
11
+ root: {
12
+ position: 'sticky',
13
+ zIndex: 96,
14
+ },
15
+ fillMobileOnly: {
16
+ [theme.breakpoints.up('md')]: {
17
+ top: `${theme.page.vertical} !important`,
18
+ },
19
+ },
20
+ }),
21
+ {
22
+ name: 'AppShellSticky',
23
+ },
24
+ )
25
+
26
+ type AppShellStickyBaseProps = {
27
+ children: React.ReactNode
28
+ headerFill?: 'mobile-only' | 'both'
29
+ }
30
+
31
+ type AppShellStickyProps = AppShellStickyBaseProps & UseStyles<typeof useStyles>
32
+
33
+ /*
34
+ - makes the children sticky to the parent container
35
+ - determines top offset based on header height dynamically
36
+ */
37
+ export default function AppShellSticky(props: AppShellStickyProps) {
38
+ const { children, headerFill = 'both' } = props
39
+ const classes = useStyles(props)
40
+
41
+ const { contentHeaderRef } = useAppShellHeaderContext()
42
+ const offsetTop = useMotionValue<number>(0)
43
+
44
+ useEffect(() => {
45
+ if (!contentHeaderRef?.current) return () => {}
46
+
47
+ const ro = new ResizeObserver(([entry]) =>
48
+ offsetTop.set(contentHeaderRef?.current?.clientHeight ?? 0),
49
+ )
50
+
51
+ ro.observe(contentHeaderRef.current)
52
+ return () => ro.disconnect()
53
+ }, [contentHeaderRef, offsetTop])
54
+
55
+ const top = useMotionValueValue(offsetTop, (v) => v)
56
+
57
+ return (
58
+ <Container
59
+ maxWidth={false}
60
+ className={clsx(classes.root, headerFill === 'mobile-only' && classes.fillMobileOnly)}
61
+ style={{ top }}
62
+ >
63
+ <>{children}</>
64
+ </Container>
65
+ )
66
+ }
@@ -0,0 +1,45 @@
1
+ import { makeStyles, Theme, TypographyProps } from '@material-ui/core'
2
+ import clsx from 'clsx'
3
+ import React from 'react'
4
+ import { UseStyles } from '../../Styles'
5
+ import Title, { TitleProps } from '../../Title'
6
+ import useAppShellHeaderContext from '../AppShellHeader/useAppShellHeaderContext'
7
+
8
+ type AppShellTitleProps = {
9
+ children: React.ReactNode
10
+ bare?: boolean
11
+ variant?: TypographyProps['variant']
12
+ } & Pick<TitleProps, 'icon'> &
13
+ UseStyles<typeof useStyles>
14
+
15
+ const useStyles = makeStyles(
16
+ (theme: Theme) => ({
17
+ title: {},
18
+ margin: {
19
+ marginTop: theme.spacings.md,
20
+ marginBottom: theme.spacings.lg,
21
+ },
22
+ }),
23
+ {
24
+ name: 'AppShellTitle',
25
+ },
26
+ )
27
+
28
+ export default function AppShellTitle(props: AppShellTitleProps) {
29
+ const { children, icon, bare, variant } = props
30
+ const { titleRef } = useAppShellHeaderContext()
31
+ const classes = useStyles(props)
32
+
33
+ return (
34
+ <Title
35
+ ref={titleRef}
36
+ component='h2'
37
+ size='medium'
38
+ variant={variant}
39
+ icon={icon ?? undefined}
40
+ classes={{ container: clsx(classes.title, !bare && classes.margin) }}
41
+ >
42
+ {children}
43
+ </Title>
44
+ )
45
+ }