@chem-po/react-web 0.0.5 → 0.0.6

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 +22 -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,379 @@
1
+ import {
2
+ Center,
3
+ Flex,
4
+ HStack,
5
+ IconButton,
6
+ Image,
7
+ ImageProps,
8
+ Text,
9
+ useColorModeValue,
10
+ VStack,
11
+ } from '@chakra-ui/react'
12
+
13
+ import { FileValue, generateId, ImageViewOptions, InputRef, WithId } from '@chem-po/core'
14
+ import { FileField, useImageSize, useObjectUrl } from '@chem-po/react'
15
+ import {
16
+ ChangeEventHandler,
17
+ forwardRef,
18
+ useCallback,
19
+ useImperativeHandle,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from 'react'
24
+ import { useBorderColor } from '../../../hooks'
25
+ import { LoadingImage } from '../../loading/LoadingImage'
26
+ import { PlayButton } from '../../media/PlayButton'
27
+ import { FieldProps } from './types'
28
+
29
+ const generateAccept = (field: FileField) => {
30
+ if (!field.accept) return undefined
31
+ const accept: string[] = []
32
+ if (field.accept.includes('image')) {
33
+ accept.push('image/jpg', 'image/jpeg', 'image/png', 'image/svg', 'image/gif')
34
+ }
35
+ if (field.accept.includes('pdf')) accept.push('application/pdf')
36
+ if (field.accept.includes('audio')) {
37
+ accept.push('audio/mp3', 'audio/wav', 'audio/x-wav', 'audio/webm', 'audio/ogg')
38
+ }
39
+ return accept.join(',')
40
+ }
41
+
42
+ export const AudioFileView = ({
43
+ hasUpload,
44
+ value,
45
+ noLabel,
46
+ }: {
47
+ value: FileValue
48
+ noLabel?: boolean
49
+ hasUpload?: boolean
50
+ }) => {
51
+ const media = useMemo<WithId<any>>(
52
+ () => ({
53
+ id: generateId(),
54
+ title: value.filename || 'Uploaded file',
55
+ artistName: '',
56
+ artistId: '',
57
+ storageDir: '',
58
+ producerId: '',
59
+ songwriterId: '',
60
+ }),
61
+ [value],
62
+ )
63
+
64
+ return (
65
+ <VStack>
66
+ <HStack spacing={4}>
67
+ <PlayButton withThumbnail media={media} />
68
+ {hasUpload ? (
69
+ <IconButton
70
+ w="40px"
71
+ h="40px"
72
+ p={2}
73
+ borderRadius="full"
74
+ boxShadow="0 0 4px black"
75
+ aria-label="upload"
76
+ icon={<Image filter="invert(100%)" src="/svg/upload.svg" opacity={0.8} />}
77
+ />
78
+ ) : null}
79
+ </HStack>
80
+ {!noLabel && value.filename ? (
81
+ <Text fontSize="md">{value.filename || 'Uploaded file'}</Text>
82
+ ) : null}
83
+ </VStack>
84
+ )
85
+ }
86
+
87
+ const ImageFileView = ({
88
+ value,
89
+ options,
90
+ hasUpload,
91
+ loading: loadingProp,
92
+ }: {
93
+ value: FileValue
94
+ options?: ImageViewOptions
95
+ hasUpload?: boolean
96
+ loading?: boolean
97
+ }) => {
98
+ const [loaded, setLoaded] = useState(false)
99
+ const { url, loading } = useObjectUrl(value)
100
+ const [imageSize, setImageSize] = useState({ width: 0, height: 0 })
101
+ const { shape, placeholder, objectFit, noFullView, background } = options ?? {}
102
+ const {
103
+ src: placeholderSrc,
104
+ opacity: placeholderOpacity,
105
+ scale: placeholderScale,
106
+ } = placeholder ?? {}
107
+
108
+ const size = useImageSize(options, imageSize)
109
+ const { width: optWidth, height: optHeight } = size ?? {}
110
+
111
+ const opacity = useMemo(() => {
112
+ if (loaded) return url ? 1 : (placeholderOpacity ?? 0.8)
113
+ return 0
114
+ }, [loaded, url, placeholderOpacity])
115
+
116
+ const imageProps = useMemo<ImageProps>(() => {
117
+ if (!url) {
118
+ return {
119
+ width: size ? `${size.width * (placeholderScale ?? 0.9)}px` : 'auto',
120
+ height: size ? `${size.height * (placeholderScale ?? 0.9)}px` : '80px',
121
+ }
122
+ }
123
+ return {
124
+ objectFit: objectFit ?? (loaded && shape ? 'cover' : 'contain'),
125
+ onLoad: e => {
126
+ setLoaded(true)
127
+ setImageSize({
128
+ width: e.currentTarget.naturalWidth,
129
+ height: e.currentTarget.naturalHeight,
130
+ })
131
+ },
132
+ }
133
+ }, [url, size, placeholderScale, loaded, shape, objectFit])
134
+ const isLoading = !!loadingProp || loading
135
+ const width = typeof optWidth === 'number' ? `${optWidth}px` : 'auto'
136
+ const height = typeof optHeight === 'number' ? `${optHeight}px` : '100px'
137
+ return (
138
+ <LoadingImage
139
+ width={width}
140
+ height={height}
141
+ noFullView={hasUpload ?? noFullView}
142
+ overflow="hidden"
143
+ borderRadius={shape === 'circle' ? 'full' : 4}
144
+ boxShadow={shape ? '1px 1px 3px #00000066' : 'none'}
145
+ opacity={opacity}
146
+ background={background}
147
+ transition="opacity 333ms ease"
148
+ loadingOverride={isLoading}
149
+ imageProps={imageProps}
150
+ src={url ?? placeholderSrc}
151
+ />
152
+ )
153
+ }
154
+
155
+ const extensionIcons: Record<string, string> = {
156
+ txt: '/icons/description.svg',
157
+ pdf: '/icons/pdf.svg',
158
+ zip: '/icons/zip.svg',
159
+ '7z': '/icons/zip.svg',
160
+ rar: '/icons/zip.svg',
161
+ }
162
+
163
+ const ExtensionIcon = ({ value }: { value: FileValue | null | undefined }) => {
164
+ const extension = useMemo(() => {
165
+ if (!value) return ''
166
+ const parts = value.filename.split('.')
167
+ return parts[parts.length - 1]
168
+ }, [value])
169
+ const icon = extensionIcons[extension]
170
+ const filter = useColorModeValue('invert(0)', 'invert(1)')
171
+ if (!icon) return null
172
+
173
+ return <Image filter={extension !== 'pdf' ? filter : 'none'} h="24px" src={icon} />
174
+ }
175
+
176
+ const FileViewBody = ({
177
+ value,
178
+ noLabel,
179
+ imageOptions,
180
+ loading,
181
+ hasUpload,
182
+ }: {
183
+ value: FileValue
184
+ imageOptions?: ImageViewOptions
185
+ loading?: boolean
186
+ noLabel?: boolean
187
+ hasUpload?: boolean
188
+ }) => {
189
+ const fileType = useMemo(() => value.type?.split('/')?.[0], [value])
190
+ switch (fileType) {
191
+ case 'image':
192
+ return (
193
+ <ImageFileView
194
+ hasUpload={hasUpload}
195
+ loading={loading}
196
+ options={imageOptions}
197
+ value={value}
198
+ />
199
+ )
200
+ case 'audio':
201
+ return <AudioFileView noLabel={noLabel} value={value} hasUpload={hasUpload} />
202
+ case 'pdf':
203
+ return <Image src="/icons/pdf.svg" />
204
+ default:
205
+ return (
206
+ <Flex gap={2}>
207
+ <ExtensionIcon value={value} />
208
+ <Text>{value.filename}</Text>
209
+ </Flex>
210
+ )
211
+ }
212
+ }
213
+
214
+ const NoFileView = ({
215
+ hasUpload,
216
+ imageOptions,
217
+ }: {
218
+ hasUpload?: boolean
219
+ imageOptions?: ImageViewOptions
220
+ }) => {
221
+ const imageSize = useImageSize(imageOptions)
222
+ const { shape, placeholder } = imageOptions ?? {}
223
+ const {
224
+ src: placeholderSrc,
225
+ scale: placeholderScale,
226
+ opacity: placeholderOpacity,
227
+ } = placeholder ?? {}
228
+ const { width, height } = imageSize ?? {}
229
+ const filter = useColorModeValue('invert(0)', 'invert(1)')
230
+
231
+ const bg = useColorModeValue('whiteAlpha.500', 'whiteAlpha.100')
232
+ const borderColor = useBorderColor()
233
+
234
+ const body = imageOptions ? (
235
+ <Center
236
+ height={shape ? `${height}px` : 'auto'}
237
+ width={shape ? `${width}px` : 'auto'}
238
+ bg={shape ? bg : 'transparent'}
239
+ borderRadius={shape === 'circle' ? 'full' : 4}
240
+ overflow="hidden"
241
+ boxShadow={shape ? '1px 1px 4px #00000055' : 'none'}>
242
+ <Image
243
+ filter={filter}
244
+ opacity={placeholderOpacity}
245
+ height={typeof height === 'number' ? `${height * (placeholderScale ?? 0.6)}px` : '50px'}
246
+ src={placeholderSrc ?? '/icons/image.svg'}
247
+ />
248
+ </Center>
249
+ ) : (
250
+ <Text
251
+ border="1px dashed"
252
+ borderColor={borderColor}
253
+ p={3}
254
+ fontSize="sm"
255
+ textAlign="center"
256
+ w="100%"
257
+ opacity={0.8}>
258
+ {hasUpload ? 'Click to upload or drop file here' : 'No file uploaded'}
259
+ </Text>
260
+ )
261
+ return (
262
+ <Center
263
+ width={typeof width === 'number' ? `${width}px` : 'auto'}
264
+ height={typeof height === 'number' ? `${height}px` : 'auto'}>
265
+ {body}
266
+ </Center>
267
+ )
268
+ }
269
+
270
+ export const FileView = ({
271
+ value,
272
+ hasUpload,
273
+ noLabel,
274
+ loading,
275
+ imageOptions,
276
+ }: {
277
+ value?: FileValue | null
278
+ hasUpload?: boolean
279
+ noLabel?: boolean
280
+ loading?: boolean
281
+ imageOptions?: ImageViewOptions
282
+ }) => {
283
+ const { storagePath, dataUrl } = value ?? {}
284
+
285
+ const missingFile = useMemo(() => !dataUrl && !storagePath, [dataUrl, storagePath])
286
+ if (!value || missingFile) {
287
+ return <NoFileView hasUpload={hasUpload} imageOptions={imageOptions} />
288
+ }
289
+ return (
290
+ <FileViewBody
291
+ loading={loading}
292
+ imageOptions={imageOptions}
293
+ noLabel={noLabel}
294
+ hasUpload={hasUpload}
295
+ value={value}
296
+ />
297
+ )
298
+ }
299
+ export const FileComponent = forwardRef<InputRef, FieldProps<FileField>>(
300
+ ({ input: { value, onChange }, field }, ref) => {
301
+ const { imageOptions } = field || {}
302
+ const inputRef = useRef<HTMLInputElement>(null)
303
+
304
+ const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
305
+ e => {
306
+ const file = e.target.files?.[0]
307
+ if (!file) return
308
+ if (value?.dataUrl) URL.revokeObjectURL(value.dataUrl)
309
+
310
+ onChange({
311
+ ...value,
312
+ dataUrl: URL.createObjectURL(file),
313
+ type: file.type,
314
+ filename: file.name,
315
+ })
316
+ },
317
+ [onChange, value],
318
+ )
319
+
320
+ useImperativeHandle(ref, () => ({
321
+ focus: () => {
322
+ inputRef.current?.click()
323
+ },
324
+ blur: () => {
325
+ inputRef.current?.blur()
326
+ },
327
+ }))
328
+ const isImageField = useMemo(
329
+ () => field.accept && field.accept.length === 1 && field.accept[0] === 'image',
330
+ [field],
331
+ )
332
+ const imgOptions = useMemo<ImageViewOptions | undefined>(
333
+ () =>
334
+ isImageField || value?.type?.startsWith('image/') || value?.type?.startsWith('video/')
335
+ ? {
336
+ height: 120,
337
+ ...imageOptions,
338
+ objectFit: 'contain',
339
+ }
340
+ : undefined,
341
+ [imageOptions, isImageField, value],
342
+ )
343
+ return (
344
+ <Center
345
+ flexFlow="column"
346
+ position="relative"
347
+ cursor="pointer"
348
+ onClick={() => inputRef.current?.click()}
349
+ width="100%">
350
+ <Text fontSize="sm" opacity={0.8} mb={1}>
351
+ {field.placeholder}
352
+ </Text>
353
+ <Center
354
+ // maxW='160px'
355
+ width="100%"
356
+ position="relative"
357
+ p={2}
358
+ overflow="hidden">
359
+ <FileView hasUpload imageOptions={imgOptions} value={value} />
360
+ <input
361
+ onChange={handleChange}
362
+ ref={inputRef}
363
+ accept={generateAccept(field)}
364
+ type="file"
365
+ style={{
366
+ position: 'absolute',
367
+ pointerEvents: 'none',
368
+ height: 0.1,
369
+ width: 0.1,
370
+ opacity: 0,
371
+ }}
372
+ />
373
+ </Center>
374
+ </Center>
375
+ )
376
+ },
377
+ )
378
+
379
+ FileComponent.displayName = 'FileComponent'
@@ -0,0 +1,2 @@
1
+ export * from './useInputImperativeHandle'
2
+ export * from './useInputStyle'
@@ -0,0 +1,16 @@
1
+ import { InputRef } from '@chem-po/core'
2
+ import { Ref, useImperativeHandle, useRef } from 'react'
3
+
4
+ export const useInputImperativeHandle = (
5
+ ref: Ref<InputRef>,
6
+ onFocus?: () => void,
7
+ onBlur?: () => void,
8
+ ) => {
9
+ const inputRef = useRef<HTMLInputElement>(null)
10
+
11
+ useImperativeHandle(ref, () => ({
12
+ focus: onFocus ?? (() => inputRef.current?.focus()),
13
+ blur: onBlur ?? (() => inputRef.current?.blur()),
14
+ }))
15
+ return inputRef
16
+ }
@@ -0,0 +1,39 @@
1
+ import { InputSize } from '@chem-po/react'
2
+ import { CSSProperties, useMemo } from 'react'
3
+
4
+ const inputPaddingY: Record<InputSize, number> = {
5
+ xs: 0.25,
6
+ sm: 0.35,
7
+ md: 0.5,
8
+ lg: 0.5,
9
+ xl: 0.5,
10
+ }
11
+
12
+ const inputPaddingX: Record<InputSize, number> = {
13
+ xs: 0.5,
14
+ sm: 0.75,
15
+ md: 0.75,
16
+ lg: 0.75,
17
+ xl: 0.75,
18
+ }
19
+ const getInputPadding = (size: InputSize) => {
20
+ const paddingY = inputPaddingY[size]
21
+ const paddingX = inputPaddingX[size]
22
+ return `${paddingY}rem ${paddingX}rem`
23
+ }
24
+ const fontSize: Record<InputSize, string> = {
25
+ xs: '0.75rem',
26
+ sm: '0.875rem',
27
+ md: '1rem',
28
+ lg: '1.125rem',
29
+ xl: '1.25rem',
30
+ }
31
+
32
+ export const useInputStyle = (size?: InputSize) =>
33
+ useMemo<Partial<CSSProperties>>(
34
+ () => ({
35
+ padding: getInputPadding(size ?? 'md'),
36
+ fontSize: fontSize[size ?? 'md'],
37
+ }),
38
+ [size],
39
+ )
@@ -0,0 +1,2 @@
1
+ export * from './Editable'
2
+ export * from './StandaloneInput'
@@ -0,0 +1,44 @@
1
+ .react-date-picker,
2
+ .react-datetime-picker,
3
+ .react-time-picker {
4
+ border: none;
5
+ width: 100%;
6
+ box-sizing: border-box;
7
+ margin: 0.25rem 0.25rem;
8
+ padding: 0.5rem 0.5rem;
9
+ /* box-shadow: 0 0 7px rgba(0, 0, 0, 0.1); */
10
+ border: 1px solid #cdcdcd;
11
+ /* background-color: white; */
12
+ }
13
+
14
+ .react-date-picker__wrapper,
15
+ .react-datetime-picker__wrapper,
16
+ .react-time-picker__wrapper {
17
+ width: 100%;
18
+ display: flex;
19
+ border: none !important;
20
+ }
21
+
22
+ .react-date-picker__clear-button__icon,
23
+ .react-date-picker__button__icon,
24
+ .react-time-picker__clear-button,
25
+ .react-time-picker__button__icon {
26
+ margin-left: auto;
27
+ }
28
+
29
+ .react-date-picker__inputGroup,
30
+ .react-datetime-picker__inputGroup {
31
+ flex: 1;
32
+ }
33
+
34
+ .react-date-picker__inputGroup > input,
35
+ .react-time-picker__inputGroup > input,
36
+ .react-time-picker__inputGroup > select {
37
+ font-size: 1rem;
38
+ color: #000000aa;
39
+ background-color: transparent;
40
+ width: 50px;
41
+ outline: none;
42
+ margin: 0 0.3rem;
43
+ font-family: 'Public Sans';
44
+ }
@@ -0,0 +1,130 @@
1
+ import { Flex, FlexProps, Text, VStack } from '@chakra-ui/react'
2
+ import { FieldType, InputRef } from '@chem-po/core'
3
+ import { Field, TypedField } from '@chem-po/react'
4
+ import { ForwardedRef, forwardRef, ForwardRefExoticComponent, useMemo } from 'react'
5
+ import { BooleanComponent } from './boolean'
6
+ import { ColorComponent } from './color'
7
+ import { DateInput } from './date'
8
+ import { DateTimeInput } from './datetime'
9
+ import { FileComponent } from './file'
10
+ import { MultipleSelectComponent } from './multipleSelect'
11
+ import { CurrencyAmountComponent, NumberComponent } from './number'
12
+ import { OptionalTag } from './OptionalTag'
13
+ import { SelectComponent } from './select'
14
+ import { SocialMediaComponent } from './socialMedia'
15
+ import { TextComponent } from './text'
16
+ import { TimeInput } from './time'
17
+ import { FieldProps } from './types'
18
+
19
+ type ComponentType<T extends Field> = ForwardRefExoticComponent<
20
+ FieldProps<T> & React.RefAttributes<InputRef>
21
+ >
22
+
23
+ const Components: { [Key in FieldType]: ComponentType<TypedField<Key>> } = {
24
+ text: TextComponent,
25
+ file: FileComponent,
26
+ boolean: BooleanComponent,
27
+ select: SelectComponent,
28
+ multipleSelect: MultipleSelectComponent,
29
+ currency: CurrencyAmountComponent,
30
+ number: NumberComponent,
31
+ date: DateInput,
32
+ datetime: DateTimeInput,
33
+ time: TimeInput,
34
+ socialMedia: SocialMediaComponent,
35
+ color: ColorComponent,
36
+ }
37
+
38
+ const InputBase = <T extends Field>(props: FieldProps<T>, ref: ForwardedRef<InputRef>) => {
39
+ const {
40
+ field,
41
+ meta: { error, active, touched },
42
+ input,
43
+ inEditable,
44
+ } = props
45
+
46
+ const { value } = input
47
+
48
+ const color = useMemo(() => {
49
+ if (error && touched) return '#ff7777'
50
+ if (active) return 'rgba(0,0,0,0.7)'
51
+ return 'rgba(0,0,0,0.3)'
52
+ }, [error, active, touched])
53
+ const { _type, optional, label } = field
54
+ const showPlaceholder = useMemo(
55
+ () => _type === 'multipleSelect' || (!!value && _type !== 'file' && _type !== 'boolean'),
56
+ [value, _type],
57
+ )
58
+ const styles = useMemo<FlexProps>(() => {
59
+ if (inEditable) return {}
60
+ switch (_type) {
61
+ case 'text':
62
+ case 'number':
63
+ case 'currency':
64
+ case 'select':
65
+ return { boxShadow: `0 0 7px ${color}`, transition: 'all 300ms', bg: 'background.50' }
66
+ default:
67
+ return {}
68
+ }
69
+ }, [_type, color, inEditable])
70
+ const Component = useMemo(() => Components[_type] as ComponentType<T>, [_type])
71
+
72
+ const pb = useMemo(() => {
73
+ if (inEditable) return 0
74
+ return error && touched ? 6 : 2
75
+ }, [inEditable, error, touched])
76
+ return (
77
+ <VStack
78
+ position="relative"
79
+ align="flex-start"
80
+ width="100%"
81
+ py={inEditable ? 0 : 1}
82
+ px={1}
83
+ spacing={0}
84
+ pb={pb}
85
+ transition="all 500ms"
86
+ pt={showPlaceholder && !inEditable ? 4 : 0}>
87
+ {label && !inEditable ? (
88
+ <Text color="gray.800" fontSize="sm" px={2}>
89
+ {label}
90
+ </Text>
91
+ ) : null}
92
+ <Flex width="100%" borderRadius={4} py={0.5} overflow="hidden" {...styles}>
93
+ <Component ref={ref} {...props} />
94
+ </Flex>
95
+ {inEditable ? null : (
96
+ <>
97
+ <Text
98
+ opacity={error && touched ? 1 : 0}
99
+ transition={`opacity 500ms ease ${error && touched ? 250 : 0}ms`}
100
+ position="absolute"
101
+ bottom="0px"
102
+ fontSize="sm"
103
+ px={1}
104
+ pointerEvents="none"
105
+ color="red.600">
106
+ {error}
107
+ </Text>
108
+ <Text
109
+ opacity={showPlaceholder ? 1 : 0}
110
+ transition={`opacity 500ms ease ${showPlaceholder ? 250 : 0}ms`}
111
+ position="absolute"
112
+ top="-4px"
113
+ pointerEvents="none"
114
+ fontSize="sm"
115
+ fontFamily="fonts.heading"
116
+ px={1}
117
+ color="blackAlpha.600"
118
+ _dark={{
119
+ color: 'whiteAlpha.600',
120
+ }}>
121
+ {field.placeholder || ''}
122
+ </Text>
123
+ </>
124
+ )}
125
+ {optional && !inEditable ? <OptionalTag field={field} value={value} /> : null}
126
+ </VStack>
127
+ )
128
+ }
129
+
130
+ export const Input = forwardRef<InputRef, FieldProps<Field>>(InputBase)
@@ -0,0 +1,55 @@
1
+ import { Box, Button, Flex, Text, useColorMode } from '@chakra-ui/react'
2
+ import { InputRef } from '@chem-po/core'
3
+ import { MultipleSelectField } from '@chem-po/react'
4
+ import { forwardRef, useImperativeHandle } from 'react'
5
+ import { FieldProps } from '../types'
6
+
7
+ export const MultipleSelectComponent = forwardRef<InputRef, FieldProps<MultipleSelectField>>(
8
+ ({ field, input, inEditable }, ref) => {
9
+ const { options, renderOption, getOptionKey } = field
10
+ const { onChange, value, onFocus, onBlur } = input
11
+ const { colorMode } = useColorMode()
12
+ useImperativeHandle(ref, () => ({
13
+ focus: () => {
14
+ onFocus()
15
+ },
16
+ blur: () => {
17
+ onBlur()
18
+ },
19
+ }))
20
+
21
+ const body = (
22
+ <Flex w="100%" flexFlow="row wrap">
23
+ {options.map(o => (
24
+ <Box key={getOptionKey ? getOptionKey(o) : o} p={0.5}>
25
+ <Button
26
+ w="100%"
27
+ minH={0}
28
+ size="xs"
29
+ p={0}
30
+ opacity={value?.includes(o) ? 1 : 0.7}
31
+ variant="unstyled"
32
+ onClick={e => {
33
+ e.stopPropagation()
34
+ onChange(value?.includes(o) ? value.filter(v => v !== o) : [...(value ?? []), o])
35
+ }}
36
+ _hover={{ opacity: 0.8 }}>
37
+ {renderOption(o, colorMode, !!value?.includes(o))}
38
+ </Button>
39
+ </Box>
40
+ ))}
41
+ </Flex>
42
+ )
43
+
44
+ return inEditable ? (
45
+ <Flex py={0.5} flexFlow="column" w="100%">
46
+ <Text lineHeight={1} px={2} opacity={0.8} fontSize="sm" fontWeight={600}>
47
+ {field.placeholder}
48
+ </Text>
49
+ {body}
50
+ </Flex>
51
+ ) : (
52
+ body
53
+ )
54
+ },
55
+ )