@chem-po/react-web 0.0.5 → 0.0.7

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 (123) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.js +2 -2
  3. package/package.json +23 -20
  4. package/src/components/auth/SignIn.tsx +43 -0
  5. package/src/components/auth/index.ts +1 -0
  6. package/src/components/box/CollapseHorizontal.tsx +18 -0
  7. package/src/components/box/ContentBox.tsx +17 -0
  8. package/src/components/box/ExpandOnMount.tsx +48 -0
  9. package/src/components/box/Expandable.tsx +96 -0
  10. package/src/components/box/FullSizeContainer.tsx +50 -0
  11. package/src/components/box/MobileFrame/index.tsx +145 -0
  12. package/src/components/box/MobileFrame/styles.css +35 -0
  13. package/src/components/box/index.ts +6 -0
  14. package/src/components/button/DeleteButton.tsx +178 -0
  15. package/src/components/button/Toggle.tsx +88 -0
  16. package/src/components/button/ViewButton.tsx +30 -0
  17. package/src/components/button/index.ts +3 -0
  18. package/src/components/feed/FeedContentPane.tsx +111 -0
  19. package/src/components/feed/MediaFeed.tsx +200 -0
  20. package/src/components/feed/MediaFeedBackground.tsx +127 -0
  21. package/src/components/feed/MediaFeedRefresh.tsx +78 -0
  22. package/src/components/feed/MediaFeedSwipeUp.tsx +34 -0
  23. package/src/components/feed/constants.ts +11 -0
  24. package/src/components/feed/context.tsx +19 -0
  25. package/src/components/feed/hooks.ts +290 -0
  26. package/src/components/feed/index.ts +2 -0
  27. package/src/components/feed/types.ts +50 -0
  28. package/src/components/form/Condition.tsx +26 -0
  29. package/src/components/form/Field.tsx +39 -0
  30. package/src/components/form/Form.tsx +425 -0
  31. package/src/components/form/FormFooter.tsx +82 -0
  32. package/src/components/form/UploadProgress/index.tsx +38 -0
  33. package/src/components/form/UploadProgress/styles.css +23 -0
  34. package/src/components/form/index.ts +4 -0
  35. package/src/components/form/input/Editable.tsx +129 -0
  36. package/src/components/form/input/InputSlider.tsx +75 -0
  37. package/src/components/form/input/OptionalTag.tsx +33 -0
  38. package/src/components/form/input/StandaloneInput.tsx +41 -0
  39. package/src/components/form/input/boolean/index.tsx +53 -0
  40. package/src/components/form/input/color/index.tsx +126 -0
  41. package/src/components/form/input/date/index.tsx +122 -0
  42. package/src/components/form/input/datetime/index.tsx +93 -0
  43. package/src/components/form/input/file.tsx +379 -0
  44. package/src/components/form/input/hooks/index.ts +2 -0
  45. package/src/components/form/input/hooks/useInputImperativeHandle.ts +16 -0
  46. package/src/components/form/input/hooks/useInputStyle.ts +39 -0
  47. package/src/components/form/input/index.ts +2 -0
  48. package/src/components/form/input/input.css +44 -0
  49. package/src/components/form/input/input.tsx +130 -0
  50. package/src/components/form/input/multipleSelect/index.tsx +55 -0
  51. package/src/components/form/input/number/index.tsx +83 -0
  52. package/src/components/form/input/number/styles.css +8 -0
  53. package/src/components/form/input/select/index.tsx +80 -0
  54. package/src/components/form/input/socialMedia/index.tsx +158 -0
  55. package/src/components/form/input/text/index.tsx +72 -0
  56. package/src/components/form/input/text/textarea.tsx +44 -0
  57. package/src/components/form/input/time/index.tsx +33 -0
  58. package/src/components/form/input/type.ts +0 -0
  59. package/src/components/form/input/types.ts +4 -0
  60. package/src/components/form/view/file.tsx +45 -0
  61. package/src/components/form/view/index.tsx +61 -0
  62. package/src/components/form/view/multipleSelect.tsx +38 -0
  63. package/src/components/form/view/select.tsx +33 -0
  64. package/src/components/index.ts +14 -0
  65. package/src/components/list/Body/InfiniteScrollGridBody.tsx +177 -0
  66. package/src/components/list/Body/InfiniteScrollListBody.tsx +114 -0
  67. package/src/components/list/Body/ListBody.tsx +23 -0
  68. package/src/components/list/Body/PagedGridBody.tsx +104 -0
  69. package/src/components/list/Body/PagedListBody.tsx +92 -0
  70. package/src/components/list/Body/hooks.ts +84 -0
  71. package/src/components/list/DataList.tsx +32 -0
  72. package/src/components/list/ListContainer.tsx +20 -0
  73. package/src/components/list/ListContent.tsx +54 -0
  74. package/src/components/list/ListCreate.tsx +57 -0
  75. package/src/components/list/ListFilters.tsx +182 -0
  76. package/src/components/list/ListFooter.tsx +458 -0
  77. package/src/components/list/ListHeader.tsx +180 -0
  78. package/src/components/list/ListItem/ListCell.tsx +48 -0
  79. package/src/components/list/ListItem/ListRow.tsx +38 -0
  80. package/src/components/list/ListItemView.tsx +53 -0
  81. package/src/components/list/ListSort.tsx +84 -0
  82. package/src/components/list/NoItems.tsx +33 -0
  83. package/src/components/list/constants.ts +1 -0
  84. package/src/components/list/index.ts +4 -0
  85. package/src/components/list/types.ts +29 -0
  86. package/src/components/list/utils.ts +62 -0
  87. package/src/components/loading/CircularProgress.tsx +11 -0
  88. package/src/components/loading/Loading.tsx +160 -0
  89. package/src/components/loading/LoadingImage.tsx +123 -0
  90. package/src/components/loading/LoadingSwitch.tsx +78 -0
  91. package/src/components/loading/index.ts +4 -0
  92. package/src/components/media/PlayButton.tsx +94 -0
  93. package/src/components/media/index.ts +1 -0
  94. package/src/components/modal/DefaultModal.tsx +18 -0
  95. package/src/components/modal/DesktopModal.tsx +11 -0
  96. package/src/components/modal/ForceMobile.tsx +7 -0
  97. package/src/components/modal/MobileModal.tsx +89 -0
  98. package/src/components/modal/index.ts +3 -0
  99. package/src/components/modal/type.ts +7 -0
  100. package/src/components/nav/NavBar.tsx +101 -0
  101. package/src/components/nav/index.ts +1 -0
  102. package/src/components/overlay/ImageViewOverlay.tsx +88 -0
  103. package/src/components/overlay/MobileOverlay.tsx +22 -0
  104. package/src/components/overlay/index.ts +2 -0
  105. package/src/components/text/GradientText/index.tsx +16 -0
  106. package/src/components/text/GradientText/styles.css +5 -0
  107. package/src/components/text/NumberTicker.tsx +28 -0
  108. package/src/components/text/index.ts +1 -0
  109. package/src/components/theme/colorMode/DarkModeToggle.tsx +40 -0
  110. package/src/components/theme/colorMode/index.ts +1 -0
  111. package/src/components/theme/index.ts +1 -0
  112. package/src/components/view/ErrorView.tsx +13 -0
  113. package/src/components/view/RedirectView.tsx +42 -0
  114. package/src/components/view/index.ts +2 -0
  115. package/src/contexts/index.ts +1 -0
  116. package/src/contexts/theme.ts +316 -0
  117. package/src/custom.d.ts +4 -0
  118. package/src/hooks/index.ts +1 -0
  119. package/src/hooks/ui/index.ts +1 -0
  120. package/src/hooks/ui/useBorderColor.ts +4 -0
  121. package/src/store/index.ts +1 -0
  122. package/src/store/usePlayer.ts +75 -0
  123. package/src/store/useScreen.ts +22 -0
@@ -0,0 +1,18 @@
1
+ import { Center, CenterProps } from '@chakra-ui/react'
2
+
3
+ export const CollapseHorizontal = ({
4
+ width,
5
+ children,
6
+ active,
7
+ duration = 400,
8
+ ...props
9
+ }: CenterProps & { width: number; active: boolean; duration?: number }) => (
10
+ <Center
11
+ width={`${active ? width : 0}px`}
12
+ opacity={active ? 1 : 0}
13
+ transition={`all ${duration}ms ease-in-out`}
14
+ overflow="hidden"
15
+ {...props}>
16
+ {children}
17
+ </Center>
18
+ )
@@ -0,0 +1,17 @@
1
+ import { Flex, FlexProps } from '@chakra-ui/react'
2
+ import { ForwardedRef, forwardRef } from 'react'
3
+
4
+ const ContentBoxBase = (props: FlexProps, ref: ForwardedRef<HTMLDivElement>) => {
5
+ return (
6
+ <Flex
7
+ ref={ref}
8
+ bg="background.100"
9
+ p={3}
10
+ borderRadius={6}
11
+ boxShadow="1px 1px 4px #00000066"
12
+ {...props}
13
+ />
14
+ )
15
+ }
16
+
17
+ export const ContentBox = forwardRef<HTMLDivElement, FlexProps>(ContentBoxBase)
@@ -0,0 +1,48 @@
1
+ import { Box, BoxProps } from '@chakra-ui/react'
2
+ import { useMounted } from '@chem-po/react'
3
+ import useResizeObserver from '@react-hook/resize-observer'
4
+ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
+
6
+ export const ExpandOnMount: FC<
7
+ BoxProps & { in?: boolean; animateOpacity?: boolean; duration?: number; onExited?: () => void }
8
+ > = ({ animateOpacity, duration = 300, children, in: isIn = true, onExited, ...props }) => {
9
+ const mounted = useMounted()
10
+
11
+ const [bodyHeight, setBodyHeight] = useState<number>(0)
12
+ const bodyRef = useRef<HTMLDivElement | null>(null)
13
+
14
+ const [isAnimating, setIsAnimating] = useState(false)
15
+ const expanded = useMemo(() => mounted && isIn, [isIn, mounted])
16
+ const height = useMemo(() => (expanded ? bodyHeight : 0), [bodyHeight, expanded])
17
+ const handleBodyResize = useCallback((e: ResizeObserverEntry) => {
18
+ setBodyHeight(e.target.scrollHeight)
19
+ }, [])
20
+
21
+ useResizeObserver(bodyRef, handleBodyResize)
22
+
23
+ const animatingTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
24
+ useEffect(() => {
25
+ if (animatingTimeout.current) {
26
+ clearTimeout(animatingTimeout.current)
27
+ animatingTimeout.current = null
28
+ }
29
+ setIsAnimating(true)
30
+ animatingTimeout.current = setTimeout(() => {
31
+ setIsAnimating(false)
32
+ if (onExited && !expanded) onExited()
33
+ }, duration)
34
+ }, [expanded, duration, onExited])
35
+ const transition = useMemo(() => {
36
+ if (!isAnimating && expanded) return 'height 0ms'
37
+ if (!animateOpacity) return `height ${duration}ms`
38
+ return `height ${duration}ms, opacity ${duration}ms ease ${expanded ? duration : 0}ms`
39
+ }, [animateOpacity, duration, expanded, isAnimating])
40
+
41
+ return (
42
+ <Box overflow="hidden" w="100%" transition={transition} height={`${height}px`} {...props}>
43
+ <Box ref={bodyRef} w="100%" {...props}>
44
+ {children}
45
+ </Box>
46
+ </Box>
47
+ )
48
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ Box,
3
+ Collapse,
4
+ Flex,
5
+ HStack,
6
+ StackProps,
7
+ Text,
8
+ useDisclosure,
9
+ VStack,
10
+ } from '@chakra-ui/react'
11
+ import React, { FC, PropsWithChildren, useCallback } from 'react'
12
+ import { useBorderColor } from '../../hooks/ui/useBorderColor'
13
+ import { ViewButton } from '../button/ViewButton'
14
+
15
+ export const Expandable = ({
16
+ header,
17
+ children,
18
+ initExpanded = false,
19
+ nested,
20
+ alwaysExpanded,
21
+ headerProps,
22
+ iconColor,
23
+ footer,
24
+ isOpen: isOpenProp,
25
+ onClose: onCloseProp,
26
+ onOpen: onOpenProp,
27
+ ...stackProps
28
+ }: PropsWithChildren<
29
+ StackProps & {
30
+ header: string | FC<{ isOpen: boolean; onClose: () => void }>
31
+ footer?: FC<{ isOpen: boolean; onClose: () => void }>
32
+ headerProps?: StackProps
33
+ nested?: boolean
34
+ alwaysExpanded?: boolean
35
+ initExpanded?: boolean
36
+ isOpen?: boolean
37
+ onClose?: () => void
38
+ iconColor?: string
39
+ onOpen?: () => void
40
+ }
41
+ >) => {
42
+ const { isOpen, onClose, onOpen } = useDisclosure({
43
+ defaultIsOpen: alwaysExpanded ?? initExpanded,
44
+ isOpen: isOpenProp,
45
+ onClose: onCloseProp,
46
+ onOpen: onOpenProp,
47
+ })
48
+
49
+ const handleOpenClick = useCallback(
50
+ (e: React.MouseEvent) => {
51
+ e.stopPropagation()
52
+ if (alwaysExpanded) return
53
+ if (isOpen) {
54
+ onClose()
55
+ return
56
+ }
57
+ onOpen()
58
+ },
59
+ [alwaysExpanded, onOpen, onClose, isOpen],
60
+ )
61
+ const borderColor = useBorderColor()
62
+ return (
63
+ <VStack spacing={0} borderColor={borderColor} w="100%" {...stackProps}>
64
+ <HStack
65
+ borderBottom={isOpen ? `1px solid ${borderColor}` : undefined}
66
+ px={2}
67
+ spacing={0}
68
+ w="100%"
69
+ {...headerProps}>
70
+ <Flex
71
+ onClick={handleOpenClick}
72
+ cursor="pointer"
73
+ aria-label="expand/hide"
74
+ align="center"
75
+ flex={1}>
76
+ {typeof header === 'string' ? (
77
+ <Text fontWeight={600} color="gray.500" flex={1}>
78
+ {header}
79
+ </Text>
80
+ ) : (
81
+ header({ isOpen, onClose })
82
+ )}
83
+ </Flex>
84
+ {alwaysExpanded ? null : (
85
+ <ViewButton color={iconColor} onClick={handleOpenClick} isOpen={isOpen} />
86
+ )}
87
+ </HStack>
88
+ <Box w="100%" borderLeft={nested ? '4px solid #00000033' : undefined}>
89
+ <Collapse unmountOnExit style={{ width: '100%' }} in={isOpen}>
90
+ {children as any}
91
+ </Collapse>
92
+ </Box>
93
+ {footer ? footer({ isOpen, onClose }) : null}
94
+ </VStack>
95
+ )
96
+ }
@@ -0,0 +1,50 @@
1
+ import { Box, BoxProps } from '@chakra-ui/react'
2
+ import { FullSizeContext } from '@chem-po/react'
3
+ import useResizeObserver from '@react-hook/resize-observer'
4
+ import { FC, useCallback, useMemo, useRef, useState } from 'react'
5
+
6
+ export const FullSizeContainer = ({
7
+ children,
8
+ ...props
9
+ }: Omit<BoxProps, 'children'> & { children: FC<{ width: number; height: number }> }) => {
10
+ const [width, setWidth] = useState(0)
11
+ const [height, setHeight] = useState(0)
12
+
13
+ const ref = useRef<HTMLDivElement>(null)
14
+ const handleResize = useCallback((entry: ResizeObserverEntry) => {
15
+ // const rect = entry.target.getBoundingClientRect()
16
+ setWidth(entry.target.scrollWidth)
17
+ setHeight(entry.target.scrollHeight)
18
+ }, [])
19
+
20
+ useResizeObserver(ref, handleResize)
21
+
22
+ return (
23
+ <Box ref={ref} {...props} width="100%" height="100%">
24
+ {children({ width, height })}
25
+ </Box>
26
+ )
27
+ }
28
+
29
+ // same as FullSizeContainer, but provides the width and height in a context instead of a render prop
30
+ export const FullSizeProvider = ({ children, ...props }: BoxProps) => {
31
+ const [width, setWidth] = useState(0)
32
+ const [height, setHeight] = useState(0)
33
+
34
+ const ref = useRef<HTMLDivElement>(null)
35
+ const handleResize = useCallback((entry: ResizeObserverEntry) => {
36
+ // const rect = entry.target.getBoundingClientRect()
37
+ setWidth(entry.target.scrollWidth)
38
+ setHeight(entry.target.scrollHeight)
39
+ }, [])
40
+
41
+ useResizeObserver(ref, handleResize)
42
+
43
+ const contextValue = useMemo(() => ({ width, height }), [width, height])
44
+
45
+ return (
46
+ <Box ref={ref} {...props} width="100%" height="100%">
47
+ <FullSizeContext.Provider value={contextValue}>{children}</FullSizeContext.Provider>
48
+ </Box>
49
+ )
50
+ }
@@ -0,0 +1,145 @@
1
+ import { Box, Center, Flex, useColorModeValue } from '@chakra-ui/react'
2
+ import { cssGradients } from '@chem-po/core'
3
+ import { MobileFrameProvider, useScreen, useViews, View, ViewsProvider } from '@chem-po/react'
4
+ import { AnimatePresence, motion } from 'framer-motion'
5
+ import { PropsWithChildren, ReactNode, useMemo, useRef } from 'react'
6
+ import { ErrorBoundary } from 'react-error-boundary'
7
+ import { matchRoutes, useLocation, useOutlet } from 'react-router-dom'
8
+ import { CSSTransition, SwitchTransition } from 'react-transition-group'
9
+ import { ForceMobile } from '../../modal/ForceMobile'
10
+ import { NAV_BAR_HEIGHT, NavBar } from '../../nav/NavBar'
11
+ import { ErrorView } from '../../view/ErrorView'
12
+ import { RedirectView } from '../../view/RedirectView'
13
+ import './styles.css'
14
+
15
+ const IPHONE_12_WIDTH = 390
16
+ const IPHONE_12_HEIGHT = 844
17
+ const IPHONE_12_ASPECT = IPHONE_12_WIDTH / IPHONE_12_HEIGHT
18
+
19
+ const MobileFrameBody = ({
20
+ children,
21
+ navBarChildren,
22
+ }: PropsWithChildren<{ navBarChildren?: ReactNode }>) => {
23
+ const { isMobile, height: screenHeight, width: screenWidth } = useScreen()
24
+
25
+ // const contentHeight = useMemo(() => (isMobile ? screenHeight : IPHONE_12_HEIGHT), [isMobile, screenHeight])
26
+ // const contentWidth = useMemo(() => (isMobile ? screenWidth : IPHONE_12_WIDTH), [isMobile, screenWidth])
27
+ // should be constrained to the screen size
28
+ const { contentHeight, contentWidth } = useMemo(() => {
29
+ let h = isMobile ? screenHeight : IPHONE_12_HEIGHT
30
+ let w = isMobile ? screenWidth : IPHONE_12_WIDTH
31
+ if (isMobile) return { contentHeight: h, contentWidth: w }
32
+ const cappedHeight = Math.floor(screenHeight * 0.9)
33
+ const cappedWidth = Math.floor(screenWidth * 0.9)
34
+ if (h > cappedHeight) {
35
+ w = Math.floor(cappedHeight * IPHONE_12_ASPECT)
36
+ h = cappedHeight
37
+ } else if (w > cappedWidth) {
38
+ h = Math.floor(cappedWidth / IPHONE_12_ASPECT)
39
+ w = cappedWidth
40
+ }
41
+ return { contentHeight: h, contentWidth: w }
42
+ }, [isMobile, screenHeight, screenWidth])
43
+
44
+ const outerBg = useColorModeValue(cssGradients.accentGray, cssGradients.darkGray)
45
+ const location = useLocation()
46
+ const currentOutlet = useOutlet()
47
+ const views = useViews()
48
+ const view = useMemo(() => views.find(v => !!matchRoutes(v.routes, location)), [views, location])
49
+ const viewKey = useMemo(() => {
50
+ if (!view) return '404'
51
+ return typeof view.view.path === 'string' ? view.view.path : view.view.path[0]
52
+ }, [view])
53
+ const overlayRef = useRef<HTMLDivElement>(null)
54
+ const nodeRef = useRef<HTMLDivElement>(null)
55
+ const absoluteNavBar = useMemo(() => !!view?.view?.navBar?.absolute, [view])
56
+ const bodyHeight = useMemo(
57
+ () => contentHeight - (absoluteNavBar ? 0 : NAV_BAR_HEIGHT),
58
+ [contentHeight, absoluteNavBar],
59
+ )
60
+ const contextData = useMemo(
61
+ () => ({
62
+ overlayRef,
63
+ height: contentHeight,
64
+ width: contentWidth,
65
+ absoluteNavBar,
66
+ bodyHeight,
67
+ }),
68
+ [overlayRef, contentHeight, contentWidth, absoluteNavBar, bodyHeight],
69
+ )
70
+ return (
71
+ <ForceMobile>
72
+ <MobileFrameProvider value={contextData}>
73
+ <Center position="relative" bg={outerBg} w="100%" height="100%">
74
+ <Flex
75
+ bg="background.100"
76
+ // ref={frameRef}
77
+ position="relative"
78
+ style={{ height: `${contentHeight}px`, width: `${contentWidth}px` }}
79
+ boxShadow={isMobile ? 'none' : '1px 1px 4px rgba(0,0,0,0.4)'}
80
+ borderRadius={isMobile ? 0 : 10}
81
+ overflow="hidden"
82
+ direction="column"
83
+ maxH={contentHeight}>
84
+ <AnimatePresence>
85
+ <SwitchTransition>
86
+ <CSSTransition
87
+ key={viewKey}
88
+ nodeRef={nodeRef}
89
+ unmountOnExit
90
+ classNames="page"
91
+ timeout={300}>
92
+ {() => (
93
+ <motion.div
94
+ style={{
95
+ position: 'relative',
96
+ width: '100%',
97
+ overflow: 'hidden',
98
+ }}
99
+ animate={{
100
+ height: bodyHeight,
101
+ opacity: 1,
102
+ dur: 0.5,
103
+ scale: 1,
104
+ }}
105
+ initial={{ opacity: 0, scale: 0.9, height: bodyHeight }}
106
+ exit={{ opacity: 0 }}>
107
+ <ErrorBoundary FallbackComponent={() => <ErrorView />}>
108
+ {view ? currentOutlet : <RedirectView />}
109
+ </ErrorBoundary>
110
+ {children ? (
111
+ <Box
112
+ height="100%"
113
+ w="100%"
114
+ position="absolute"
115
+ top={0}
116
+ left={0}
117
+ pointerEvents="none">
118
+ {children}
119
+ </Box>
120
+ ) : null}
121
+ </motion.div>
122
+ )}
123
+ </CSSTransition>
124
+ </SwitchTransition>
125
+ </AnimatePresence>
126
+ <NavBar selectedView={view?.view}>{navBarChildren}</NavBar>
127
+ <Flex pos="absolute" pointerEvents="none" bottom={0} w="100%" h="100%">
128
+ <Flex ref={overlayRef} w="100%" h="100%" pos="relative" />
129
+ </Flex>
130
+ </Flex>
131
+ </Center>
132
+ </MobileFrameProvider>
133
+ </ForceMobile>
134
+ )
135
+ }
136
+
137
+ export const MobileFrame = ({
138
+ children,
139
+ navBarChildren,
140
+ views,
141
+ }: PropsWithChildren<{ views: View[]; navBarChildren?: ReactNode }>) => (
142
+ <ViewsProvider views={views}>
143
+ <MobileFrameBody navBarChildren={navBarChildren}>{children}</MobileFrameBody>
144
+ </ViewsProvider>
145
+ )
@@ -0,0 +1,35 @@
1
+ .page {
2
+ display: flex;
3
+ align-items: center;
4
+ flex-direction: column;
5
+ position: relative;
6
+ overflow: hidden;
7
+ width: 100%;
8
+ min-height: 0;
9
+ flex: 1;
10
+ }
11
+
12
+ .page-enter {
13
+ opacity: 0;
14
+ transform: scale(1.05);
15
+ overflow: hidden;
16
+ }
17
+
18
+ .page-enter-active {
19
+ opacity: 1;
20
+ transform: scale(1);
21
+ overflow: hidden !important;
22
+ transition: opacity 300ms, transform 300ms;
23
+ }
24
+
25
+ .page-exit {
26
+ opacity: 1;
27
+ transform: scale(1);
28
+ }
29
+
30
+ .page-exit-active {
31
+ opacity: 0;
32
+ overflow: hidden !important;
33
+ /* transform: scale(0.95); */
34
+ transition: opacity 300ms, transform 300ms;
35
+ }
@@ -0,0 +1,6 @@
1
+ export * from './CollapseHorizontal'
2
+ export * from './ContentBox'
3
+ export * from './Expandable'
4
+ export * from './ExpandOnMount'
5
+ export * from './FullSizeContainer'
6
+ export * from './MobileFrame'
@@ -0,0 +1,178 @@
1
+ import { DeleteIcon } from '@chakra-ui/icons'
2
+ import {
3
+ AlertDialog,
4
+ AlertDialogBody,
5
+ AlertDialogContent,
6
+ AlertDialogOverlay,
7
+ Button,
8
+ Flex,
9
+ HStack,
10
+ IconButton,
11
+ IconButtonProps,
12
+ Text,
13
+ useToast,
14
+ VStack,
15
+ } from '@chakra-ui/react'
16
+ import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
17
+
18
+ const defaultAlertBodyText = "Are you sure? You can't undo this action afterwards."
19
+ export const DeleteConfirmAlert = ({
20
+ confirmActive,
21
+ onCancel,
22
+ onConfirm,
23
+ body = defaultAlertBodyText,
24
+ actionName = 'Delete',
25
+ actionLoading,
26
+ itemName,
27
+ }: {
28
+ confirmActive: boolean
29
+ onCancel: () => void
30
+ onConfirm: () => void
31
+ body?: string | ReactNode
32
+ actionLoading?: boolean
33
+ actionName?: string
34
+ itemName: string
35
+ }) => {
36
+ const cancelRef = useRef<HTMLButtonElement>(null)
37
+ return (
38
+ <AlertDialog
39
+ isCentered
40
+ isOpen={confirmActive}
41
+ leastDestructiveRef={cancelRef}
42
+ onClose={() => onCancel()}>
43
+ <AlertDialogOverlay>
44
+ <AlertDialogContent gap={0}>
45
+ <AlertDialogBody>
46
+ <VStack align="flex-start" py={1}>
47
+ <Text fontSize="lg" fontWeight={500}>
48
+ {actionName} {itemName}?
49
+ </Text>
50
+ {typeof body === 'string' ? <Text fontSize="md">{body}</Text> : body}
51
+ <HStack justify="flex-end" w="100%">
52
+ <Button size="sm" ref={cancelRef} onClick={onCancel}>
53
+ Cancel
54
+ </Button>
55
+ <Button
56
+ size="sm"
57
+ isLoading={actionLoading}
58
+ color="white"
59
+ textShadow="1px 1px 3px #00000077"
60
+ bg="red.500"
61
+ _dark={{
62
+ bg: 'red.500',
63
+ }}
64
+ onClick={onConfirm}>
65
+ {actionName}
66
+ </Button>
67
+ </HStack>
68
+ </VStack>
69
+ </AlertDialogBody>
70
+ </AlertDialogContent>
71
+ </AlertDialogOverlay>
72
+ </AlertDialog>
73
+ )
74
+ }
75
+
76
+ export const DeleteButton: React.FC<
77
+ Partial<IconButtonProps> & {
78
+ onDelete: (() => Promise<any>) | (() => void)
79
+ itemName: string
80
+ actionName?: string
81
+ noConfirm?: boolean
82
+ text?: string
83
+ alertBody?: string | ReactNode
84
+ }
85
+ > = ({ onDelete, itemName, noConfirm, alertBody, text, actionName = 'Delete', ...props }) => {
86
+ const [deleteLoading, setDeleteLoading] = useState(false)
87
+ const [confirmActive, setConfirmActive] = useState(false)
88
+ const confirmTimer = useRef<any>()
89
+ const isMounted = useRef(true)
90
+ useEffect(() => {
91
+ isMounted.current = true
92
+
93
+ return () => {
94
+ isMounted.current = false
95
+ }
96
+ }, [])
97
+
98
+ const toast = useToast()
99
+ const handleDelete = useCallback(async () => {
100
+ if (confirmTimer.current) clearTimeout(confirmTimer.current)
101
+ setConfirmActive(false)
102
+ if (onDelete) {
103
+ setDeleteLoading(true)
104
+ try {
105
+ await onDelete()
106
+ } catch (err: any) {
107
+ console.error(err)
108
+ toast({
109
+ title: 'Error',
110
+ description: err.message ?? 'An error occurred',
111
+ status: 'error',
112
+ duration: 5000,
113
+ isClosable: true,
114
+ })
115
+ }
116
+ if (isMounted.current) {
117
+ setDeleteLoading(false)
118
+ }
119
+ } else {
120
+ console.error('No delete function')
121
+ }
122
+ }, [onDelete, toast])
123
+
124
+ return (
125
+ <>
126
+ {text ? (
127
+ <Button
128
+ variant="ghost"
129
+ size={props.size ?? 'md'}
130
+ aria-label="delete"
131
+ _hover={{ color: 'red.600', bg: 'red.200' }}
132
+ color="red.600"
133
+ onClick={e => {
134
+ e.stopPropagation()
135
+ if (noConfirm) handleDelete()
136
+ else setConfirmActive(true)
137
+ }}
138
+ isLoading={deleteLoading}
139
+ {...props}>
140
+ <Flex align="center" gap={1}>
141
+ <DeleteIcon />
142
+ <Text>{text}</Text>
143
+ </Flex>
144
+ </Button>
145
+ ) : (
146
+ <IconButton
147
+ variant="ghost"
148
+ size={props.size ?? 'sm'}
149
+ aria-label={actionName}
150
+ _hover={{ color: 'red.600', bg: 'red.200' }}
151
+ color="red.500"
152
+ borderRadius="full"
153
+ icon={<DeleteIcon />}
154
+ onClick={e => {
155
+ e.stopPropagation()
156
+ if (noConfirm) handleDelete()
157
+ else setConfirmActive(true)
158
+ }}
159
+ isLoading={deleteLoading}
160
+ {...props}
161
+ />
162
+ )}
163
+ {noConfirm ? null : (
164
+ <DeleteConfirmAlert
165
+ confirmActive={confirmActive}
166
+ onCancel={() => setConfirmActive(false)}
167
+ actionLoading={deleteLoading}
168
+ onConfirm={() => {
169
+ handleDelete()
170
+ }}
171
+ actionName={actionName}
172
+ body={alertBody}
173
+ itemName={itemName}
174
+ />
175
+ )}
176
+ </>
177
+ )
178
+ }
@@ -0,0 +1,88 @@
1
+ import { Box, Flex, IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react'
2
+ import { JSX } from 'react'
3
+ import { useBorderColor } from '../../hooks'
4
+
5
+ export interface ToggleOption<Option extends string = string> {
6
+ id: Option
7
+ label: string
8
+ disabledMessage?: string
9
+ Render: (selected: boolean) => JSX.Element
10
+ }
11
+ export interface ToggleProps<Option extends string> {
12
+ options: ToggleOption<Option>[]
13
+ value: Option
14
+ size?: number
15
+ onChange: (value: Option) => Promise<void> | void
16
+ }
17
+
18
+ const ToggleButton = <Option extends string>({
19
+ selected,
20
+ option: { Render, label, disabledMessage },
21
+ size,
22
+ ...props
23
+ }: Omit<IconButtonProps, 'children' | 'aria-label' | 'size'> & {
24
+ option: ToggleOption<Option>
25
+ size: number
26
+ selected: boolean
27
+ }) => {
28
+ const body = (
29
+ <Box>
30
+ <IconButton
31
+ variant="unstyled"
32
+ w={`${size}px`}
33
+ h={`${size}px`}
34
+ minW={0}
35
+ borderRadius={0}
36
+ outline="none"
37
+ display="flex"
38
+ alignItems="center"
39
+ justifyContent="center"
40
+ opacity={!disabledMessage ? 1 : 0.6}
41
+ filter={!disabledMessage ? 'none' : 'grayscale(1)'}
42
+ bg={selected ? 'whiteAlpha.500' : 'blackAlpha.100'}
43
+ _dark={{ bg: selected ? 'blackAlpha.500' : 'whiteAlpha.200' }}
44
+ icon={Render(selected)}
45
+ pointerEvents={selected ? 'none' : 'auto'}
46
+ aria-label={label}
47
+ {...props}
48
+ />
49
+ </Box>
50
+ )
51
+
52
+ return (
53
+ <Tooltip label={disabledMessage ?? label} aria-label={label}>
54
+ {body}
55
+ </Tooltip>
56
+ )
57
+ }
58
+
59
+ export const Toggle = <Option extends string>({
60
+ value,
61
+ onChange,
62
+ size = 28,
63
+ options,
64
+ }: ToggleProps<Option>) => {
65
+ const borderColor = useBorderColor()
66
+ return (
67
+ <Flex
68
+ bg="whiteAlpha.300"
69
+ _dark={{ bg: 'blackAlpha.300' }}
70
+ border={`1px solid ${borderColor}`}
71
+ borderRadius={3}>
72
+ {options.map((option, i) => (
73
+ <ToggleButton<Option>
74
+ key={option.id}
75
+ onClick={() => {
76
+ onChange(option.id)
77
+ }}
78
+ option={option}
79
+ size={size}
80
+ borderLeftRadius={i ? 0 : 3}
81
+ borderRightRadius={i === options.length - 1 ? 3 : 0}
82
+ selected={option.id === value}
83
+ borderLeft={i ? `1px solid ${borderColor}` : 'none'}
84
+ />
85
+ ))}
86
+ </Flex>
87
+ )
88
+ }