@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,425 @@
1
+ import { CloseIcon, DeleteIcon, DragHandleIcon, EditIcon } from '@chakra-ui/icons'
2
+ import {
3
+ Box,
4
+ Button,
5
+ Flex,
6
+ IconButton,
7
+ Stack,
8
+ Text,
9
+ useColorModeValue,
10
+ VStack,
11
+ } from '@chakra-ui/react'
12
+ import { isField, isListField } from '@chem-po/core'
13
+ import {
14
+ ChempoFormProvider,
15
+ DataViewProps,
16
+ DataViewProvider,
17
+ Field,
18
+ FieldFormProps,
19
+ FieldMap,
20
+ FieldMapFormProps,
21
+ FormProps,
22
+ IFormElement,
23
+ ListField,
24
+ useDataView,
25
+ useFormSubmit,
26
+ UseFormSubmitProps,
27
+ } from '@chem-po/react'
28
+ import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
29
+ import { FC, useMemo, useState } from 'react'
30
+ import { useFieldArray, UseFieldArrayMove, useFormContext } from 'react-hook-form'
31
+ import { useBorderColor } from '../../hooks'
32
+ import { Expandable } from '../box'
33
+ import { ExpandOnMount } from '../box/ExpandOnMount'
34
+ import { DeleteButton } from '../button/DeleteButton'
35
+ import { Condition } from './Condition'
36
+ import { FieldComponent } from './Field'
37
+ import { FormFooter } from './FormFooter'
38
+ import { Editable } from './input/Editable'
39
+ import { FieldView } from './view'
40
+
41
+ const makeOnDragEndFunction = (move: UseFieldArrayMove) => (result: DropResult) => {
42
+ // dropped outside the list
43
+ if (!result.destination) {
44
+ return
45
+ }
46
+
47
+ move(result.source.index, result.destination.index)
48
+ }
49
+
50
+ export const ListFieldInput: FC<{ field: ListField; name: string }> = ({ field, name }) => {
51
+ const emptyData = useMemo(() => {
52
+ if (isField(field.itemField)) return ''
53
+ if (isListField(field.itemField)) return []
54
+ return {}
55
+ }, [field])
56
+ const draggingBg = useColorModeValue('#efefef', '#2b2b2b')
57
+ const borderColor = useBorderColor()
58
+ const { control } = useFormContext()
59
+ const { fields, append, remove, move } = useFieldArray({
60
+ control,
61
+ name,
62
+ })
63
+ const onDragEnd = useMemo(() => makeOnDragEndFunction(move), [move])
64
+ return (
65
+ <Flex overflow="hidden" gap={2} flexFlow="column" w="100%" px={2} py={1}>
66
+ <Text lineHeight={1} opacity={0.7} fontSize="sm">
67
+ {field.placeholder}
68
+ </Text>
69
+ <DragDropContext onDragEnd={onDragEnd}>
70
+ <Droppable droppableId="droppable">
71
+ {(droppableProvided, { draggingFromThisWith }) => (
72
+ <Flex
73
+ ref={droppableProvided.innerRef}
74
+ flexFlow="column"
75
+ w="100%"
76
+ py={1}
77
+ borderRadius={4}
78
+ minH="30px"
79
+ align="flex"
80
+ {...droppableProvided.droppableProps}>
81
+ {fields.length ? (
82
+ fields.map((formField, idx) => (
83
+ <Draggable key={formField.id} draggableId={formField.id} index={idx}>
84
+ {({ dragHandleProps, draggableProps, innerRef }, { isDragging }) => (
85
+ <Flex
86
+ transition="all 300ms"
87
+ boxShadow={`2px 2px 4px #000000${isDragging ? '55' : '00'}`}
88
+ borderRadius={4}
89
+ pt={2}
90
+ px={2}
91
+ border={`1px solid ${borderColor}`}
92
+ bg={`${draggingBg}${isDragging ? 'ff' : '00'}`}
93
+ ref={innerRef}
94
+ w="100%"
95
+ {...draggableProps}>
96
+ <ExpandOnMount>
97
+ <Flex
98
+ opacity={draggingFromThisWith && !isDragging ? 0.5 : 1}
99
+ gap={1}
100
+ align="center"
101
+ w="100%">
102
+ <IconButton
103
+ aria-label="drag"
104
+ size="xs"
105
+ opacity={0.8}
106
+ variant="ghost"
107
+ icon={<DragHandleIcon />}
108
+ {...dragHandleProps}
109
+ />
110
+ <Box minW="0" flex={1}>
111
+ <FormElement name={formField.id} field={field.itemField} />
112
+ </Box>
113
+ <IconButton
114
+ aria-label="delete"
115
+ size="xs"
116
+ bg="red.600"
117
+ color="white"
118
+ icon={<DeleteIcon filter="drop-shadow(1px 1px 3px #00000088)" />}
119
+ onClick={e => {
120
+ e.stopPropagation()
121
+ remove(idx)
122
+ }}
123
+ />
124
+ </Flex>
125
+ </ExpandOnMount>
126
+ </Flex>
127
+ )}
128
+ </Draggable>
129
+ ))
130
+ ) : (
131
+ <Text fontSize="sm" opacity={0.7} py={1} px={2}>
132
+ No items
133
+ </Text>
134
+ )}
135
+ {droppableProvided.placeholder}
136
+ </Flex>
137
+ )}
138
+ </Droppable>
139
+ </DragDropContext>
140
+ <Flex>
141
+ <Button mr="auto" width="auto" size="xs" onClick={() => append(emptyData)}>
142
+ + NEW
143
+ </Button>
144
+ </Flex>
145
+ </Flex>
146
+ )
147
+ }
148
+
149
+ export const FormElement = ({
150
+ field,
151
+ name,
152
+ // validate,
153
+ }: {
154
+ field: IFormElement
155
+ name: string
156
+ // validate?: boolean
157
+ }) => {
158
+ const fields = useMemo<Array<{ name: string; field: IFormElement }>>(() => {
159
+ if (isField(field) || isListField(field)) return [{ name: 'value', field }]
160
+
161
+ return Object.entries((field as FieldMap).children).map(([childName, childField]) => ({
162
+ name: `${name ? `${name}.` : ''}${childName}`,
163
+ field: childField,
164
+ }))
165
+ }, [field, name])
166
+ return (
167
+ <VStack spacing={1} w="100%">
168
+ {fields.map(f => {
169
+ if (isListField(f.field)) {
170
+ const b = <ListFieldInput key={f.name} name={f.name} field={f.field} />
171
+ return f.field.condition ? (
172
+ <Condition path={name} condition={f.field.condition} key={f.name}>
173
+ {b}
174
+ </Condition>
175
+ ) : (
176
+ b
177
+ )
178
+ }
179
+ if (isField(f.field)) {
180
+ const b = <FieldComponent key={f.name} name={f.name} field={f.field} />
181
+ return f.field.condition ? (
182
+ <Condition path={name} condition={f.field.condition} key={f.name}>
183
+ {b}
184
+ </Condition>
185
+ ) : (
186
+ b
187
+ )
188
+ }
189
+
190
+ const b = <FormElement key={f.name} name={f.name} field={f.field} />
191
+ return f.field.condition ? (
192
+ <Condition path={name} condition={f.field.condition} key={f.name}>
193
+ {b}
194
+ </Condition>
195
+ ) : (
196
+ b
197
+ )
198
+ })}
199
+ </VStack>
200
+ )
201
+ }
202
+ const ListFieldView = ({
203
+ field,
204
+ value,
205
+ path,
206
+ }: {
207
+ field: ListField
208
+ value: any[]
209
+ path: string
210
+ }) => {
211
+ const { itemField, placeholder } = field
212
+ const borderColor = useBorderColor()
213
+ return (
214
+ <Box w="100%" p={1}>
215
+ <Expandable
216
+ border={`1px solid ${borderColor}`}
217
+ borderRadius={4}
218
+ initExpanded
219
+ header={() => <Text py={1}>{placeholder}</Text>}>
220
+ <Flex flexFlow="column" bg="background.200" gap={2} px={2} py={1}>
221
+ {value?.length ? (
222
+ value.map((item, index) => (
223
+ <Box
224
+ bg="background.100"
225
+ key={`${path}.${index}`}
226
+ border={`1px solid ${borderColor}`}
227
+ borderRadius={3}>
228
+ <FormElementView
229
+ path={`${path}.${index}`}
230
+ field={{ ...itemField, placeholder: `${placeholder} ${index + 1}` }}
231
+ value={item}
232
+ />
233
+ </Box>
234
+ ))
235
+ ) : (
236
+ <Text fontSize="sm" opacity={0.7} py={1} px={2}>
237
+ No items
238
+ </Text>
239
+ )}
240
+ </Flex>
241
+ </Expandable>
242
+ </Box>
243
+ )
244
+ }
245
+
246
+ const FormElementView = ({
247
+ field,
248
+ value,
249
+ path,
250
+ storagePath,
251
+ }: {
252
+ field: IFormElement
253
+ value: any
254
+ path: string
255
+ storagePath?: string
256
+ }) => {
257
+ const { updateField } = useDataView()
258
+ if (isField(field)) {
259
+ return updateField ? (
260
+ <Editable
261
+ storagePath={storagePath}
262
+ field={field}
263
+ value={value}
264
+ onSubmit={v => updateField(path, v)}
265
+ />
266
+ ) : (
267
+ <FieldView field={field} value={value} />
268
+ )
269
+ }
270
+ if (isListField(field)) {
271
+ return <ListFieldView path={path} field={field} value={value} />
272
+ }
273
+ return (
274
+ <DataView
275
+ storagePath={storagePath ? `${storagePath}.${path}` : undefined}
276
+ path={path}
277
+ field={field}
278
+ />
279
+ )
280
+ }
281
+
282
+ export const DataView = ({
283
+ field,
284
+ value,
285
+ onClose,
286
+ onDelete,
287
+ onSubmit,
288
+ itemName,
289
+ storagePath,
290
+ path = '',
291
+ }: DataViewProps & { path?: string }) => {
292
+ const { name: fieldName, children } = field
293
+ const [isEditing, setIsEditing] = useState(false)
294
+ return (
295
+ <DataViewProvider value={value} onSubmit={onSubmit}>
296
+ <Expandable
297
+ alwaysExpanded
298
+ header={() => (
299
+ <Flex align="center" px={2} w="100%">
300
+ <Text py={2} fontSize="lg" fontFamily="fonts.heading">
301
+ {fieldName}
302
+ </Text>
303
+ <Flex gap={2} align="center" ml="auto">
304
+ {onDelete ? <DeleteButton onDelete={onDelete} itemName={itemName ?? 'item'} /> : null}
305
+ {onSubmit ? (
306
+ <IconButton
307
+ size="sm"
308
+ borderRadius="full"
309
+ minW={0}
310
+ w={7}
311
+ h={7}
312
+ ml="auto"
313
+ aria-label="edit"
314
+ icon={<EditIcon />}
315
+ onClick={() => setIsEditing(true)}
316
+ />
317
+ ) : null}
318
+ {onClose ? (
319
+ <IconButton
320
+ size="sm"
321
+ borderRadius="full"
322
+ minW={0}
323
+ w={7}
324
+ h={7}
325
+ variant="ghost"
326
+ aria-label="close"
327
+ icon={<CloseIcon opacity={0.8} w={3} h={3} />}
328
+ onClick={onClose}
329
+ />
330
+ ) : null}
331
+ </Flex>
332
+ </Flex>
333
+ )}>
334
+ {isEditing && onSubmit ? (
335
+ <Form
336
+ storagePath={storagePath}
337
+ field={field}
338
+ value={value}
339
+ onSubmit={onSubmit}
340
+ onBack={() => setIsEditing(false)}
341
+ />
342
+ ) : (
343
+ <Flex flexFlow="column" px={4} py={2}>
344
+ {Object.entries(children).map(([key, childField]) => (
345
+ <FormElementView
346
+ storagePath={storagePath ? `${storagePath}/${key}` : undefined}
347
+ path={path ? `${path}.${key}` : key}
348
+ key={key}
349
+ field={childField}
350
+ value={value?.[key]}
351
+ />
352
+ ))}
353
+ </Flex>
354
+ )}
355
+ </Expandable>
356
+ </DataViewProvider>
357
+ )
358
+ }
359
+
360
+ export const FieldMapForm = <F extends FieldMap>({
361
+ onSubmit: submit,
362
+ onBack,
363
+ field,
364
+ value,
365
+ buttonText = 'SUBMIT',
366
+ renderFooter,
367
+ storagePath,
368
+ }: FieldMapFormProps<F>) => {
369
+ const submitDataProps = useMemo<UseFormSubmitProps<F>>(
370
+ () => ({
371
+ field,
372
+ value,
373
+ submit,
374
+ storagePath,
375
+ }),
376
+ [field, value, submit, storagePath],
377
+ )
378
+
379
+ const { onSubmit, uploads } = useFormSubmit(submitDataProps)
380
+
381
+ return (
382
+ <Stack w="100%" spacing={3}>
383
+ <form>
384
+ <Box pt={1} px={2} w="100%">
385
+ <FormElement name="" field={field} />
386
+ </Box>
387
+ <FormFooter
388
+ uploads={uploads}
389
+ onSubmit={onSubmit}
390
+ renderFooter={renderFooter}
391
+ onBack={onBack}
392
+ buttonText={buttonText}
393
+ />
394
+ </form>
395
+ </Stack>
396
+ )
397
+ }
398
+
399
+ export const FieldForm = <F extends Field | ListField>({
400
+ onSubmit,
401
+ field,
402
+ value,
403
+ ...props
404
+ }: FieldFormProps<F>) => {
405
+ const fieldMap = useMemo<FieldMap>(() => ({ children: { value: field } }), [field])
406
+ return (
407
+ <FieldMapForm
408
+ field={fieldMap}
409
+ value={{ value }}
410
+ onSubmit={data => onSubmit(data?.value)}
411
+ {...props}
412
+ />
413
+ )
414
+ }
415
+
416
+ export const Form = <F extends IFormElement>({ field, ...props }: FormProps<F>) => {
417
+ const body =
418
+ isField(field) || isListField(field) ? (
419
+ <FieldForm field={field} {...props} />
420
+ ) : (
421
+ <FieldMapForm field={field} {...props} />
422
+ )
423
+
424
+ return <ChempoFormProvider>{body}</ChempoFormProvider>
425
+ }
@@ -0,0 +1,82 @@
1
+ import { Button, ButtonProps, Collapse, Flex, HStack, Progress, Text } from '@chakra-ui/react'
2
+
3
+ import { FC, PropsWithChildren } from 'react'
4
+
5
+ import { FormProps, IFormElement, useChempoForm, UseFormSubmit } from '@chem-po/react'
6
+ import { useFormState } from 'react-hook-form'
7
+ import { UploadProgress } from './UploadProgress'
8
+
9
+ const CancelButton = ({ onBack, children }: PropsWithChildren<{ onBack: () => void }>) => (
10
+ <Button
11
+ flex={1}
12
+ transition="all 500ms"
13
+ onClick={onBack}
14
+ variant="outline"
15
+ border="1px solid #cdcdcd"
16
+ color="#777"
17
+ ml="auto">
18
+ {children}
19
+ </Button>
20
+ )
21
+
22
+ export const SubmitButton: FC<
23
+ { onSubmitClick: () => Promise<void>; submitting: boolean } & ButtonProps
24
+ > = ({ onSubmitClick, submitting, children, filter, opacity }) => (
25
+ <Button
26
+ isLoading={submitting}
27
+ filter={filter}
28
+ flex={1}
29
+ transition="all 500ms"
30
+ opacity={opacity}
31
+ onClick={() => {
32
+ onSubmitClick()
33
+ }}
34
+ ml="auto"
35
+ variant="solid">
36
+ {children}
37
+ </Button>
38
+ )
39
+
40
+ const FormErrorView = () => {
41
+ const { formError } = useChempoForm()
42
+ return (
43
+ <Collapse in={!!formError}>
44
+ <Text color="red">{formError ?? ''}</Text>
45
+ </Collapse>
46
+ )
47
+ }
48
+
49
+ export const FormFooter = <F extends IFormElement>({
50
+ renderFooter,
51
+ ...props
52
+ }: Pick<FormProps<F>, 'renderFooter' | 'onBack' | 'buttonText'> &
53
+ Pick<UseFormSubmit, 'uploads'> & {
54
+ onSubmit: () => Promise<void>
55
+ }) => {
56
+ const { uploads, onBack, buttonText, onSubmit } = props
57
+ const { isSubmitting: submitting, isValid: valid } = useFormState()
58
+ return (
59
+ <Flex w="100%" flexFlow="column">
60
+ <Collapse endingHeight={5} style={{ width: '100%' }} in={submitting}>
61
+ <Progress w="100%" h="5px" isIndeterminate />
62
+ </Collapse>
63
+ <UploadProgress uploads={uploads} />
64
+ <FormErrorView />
65
+ {renderFooter ? (
66
+ renderFooter(props)
67
+ ) : (
68
+ <HStack py={2} borderTop="1px solid" borderColor="background.200" px={3} w="100%">
69
+ {onBack ? <CancelButton onBack={onBack}>Cancel</CancelButton> : null}
70
+ <SubmitButton
71
+ size="sm"
72
+ filter={`grayscale(${!valid ? 100 : 0}%)`}
73
+ opacity={!valid ? 0.5 : 1}
74
+ onSubmitClick={onSubmit}
75
+ submitting={submitting}>
76
+ {buttonText}
77
+ </SubmitButton>
78
+ </HStack>
79
+ )}
80
+ </Flex>
81
+ )
82
+ }
@@ -0,0 +1,38 @@
1
+ import { Center, Flex, Progress, Text } from '@chakra-ui/react'
2
+ import { UploadsState } from '@chem-po/core'
3
+ import { useMemo } from 'react'
4
+ import { TransitionGroup } from 'react-transition-group'
5
+ import { ExpandOnMount } from '../../box'
6
+ import './styles.css'
7
+
8
+ export const UploadProgress = ({ uploads }: { uploads: UploadsState }) => {
9
+ const asArr = useMemo(() => Object.values(uploads), [uploads])
10
+ return (
11
+ <TransitionGroup component={Flex} width="100%" flexFlow="column">
12
+ {asArr.map(upload => (
13
+ <ExpandOnMount key={upload.label}>
14
+ <Center px={2} height="26px" position="relative" w="100%" flexDirection="column">
15
+ <Progress
16
+ borderRadius="full"
17
+ value={upload.percent * 100}
18
+ mx={2}
19
+ my={1}
20
+ size="md"
21
+ height="100%"
22
+ width="100%"
23
+ />
24
+ <Text
25
+ fontFamily="fonts.heading"
26
+ textShadow="1px 1px 3px #000000aa"
27
+ color="white"
28
+ position="absolute"
29
+ fontSize="sm"
30
+ fontWeight={500}>
31
+ {upload.label.toUpperCase()}
32
+ </Text>
33
+ </Center>
34
+ </ExpandOnMount>
35
+ ))}
36
+ </TransitionGroup>
37
+ )
38
+ }
@@ -0,0 +1,23 @@
1
+ .upload-enter {
2
+ opacity: 0;
3
+ height: 0;
4
+ overflow: hidden;
5
+ }
6
+ .upload-enter-active {
7
+ opacity: 1;
8
+ height: 26px;
9
+ overflow: hidden;
10
+ transition: opacity 300ms, height 300ms;
11
+ }
12
+
13
+ .upload-exit {
14
+ opacity: 1;
15
+ height: 26px;
16
+ overflow: hidden;
17
+ }
18
+ .upload-exit-active {
19
+ opacity: 0;
20
+ height: 0;
21
+ overflow: hidden;
22
+ transition: opacity 300ms, height 300ms;
23
+ }
@@ -0,0 +1,4 @@
1
+ export * from './Form'
2
+ export * from './input'
3
+ export { FileView } from './input/file'
4
+ export { FieldView } from './view'
@@ -0,0 +1,129 @@
1
+ import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons'
2
+ import { Flex, IconButton } from '@chakra-ui/react'
3
+ import { palette } from '@chem-po/core'
4
+ import {
5
+ ChempoFormProvider,
6
+ EditableProps,
7
+ Field,
8
+ useColorModeValue,
9
+ useEditable,
10
+ } from '@chem-po/react'
11
+ import { CSSProperties, useEffect, useMemo } from 'react'
12
+ import { LoadingOverlay } from '../../loading/Loading'
13
+ import { UploadProgress } from '../UploadProgress'
14
+ import { FieldView } from '../view'
15
+ import { StandaloneInput } from './StandaloneInput'
16
+
17
+ export const Editable = <T extends Field>({
18
+ value: initValue,
19
+ field,
20
+ onSubmit,
21
+ storagePath,
22
+ style,
23
+ onEditClose,
24
+ onEditOpen,
25
+ }: EditableProps<CSSProperties, T>) => {
26
+ const {
27
+ formattedValue,
28
+ inputRef,
29
+ setValue,
30
+ isLoading,
31
+ uploads,
32
+ editHovered,
33
+ handleEditOpen,
34
+ handleEditClose,
35
+ isEditing,
36
+ value,
37
+ setEditHovered,
38
+ submit,
39
+ submitValue,
40
+ parse,
41
+ } = useEditable({
42
+ value: initValue,
43
+ field,
44
+ onSubmit,
45
+ storagePath,
46
+ onEditOpen,
47
+ onEditClose,
48
+ })
49
+ const alwaysEditing = useMemo(() => field._type === 'file' || field._type === 'boolean', [field])
50
+ useEffect(() => {
51
+ if (isEditing) {
52
+ inputRef.current?.focus()
53
+ }
54
+ }, [isEditing, inputRef])
55
+
56
+ const editingBorderColor = useColorModeValue('#00000055', '#ffffff55')
57
+
58
+ return (
59
+ <ChempoFormProvider>
60
+ <Flex
61
+ borderRadius={4}
62
+ border={`1px dashed ${editHovered && !isEditing ? editingBorderColor : 'transparent'}`}
63
+ flexFlow="column"
64
+ w="100%">
65
+ <Flex align="center" px={1} position="relative" w="100%">
66
+ <Flex
67
+ opacity={isLoading ? 0 : 1}
68
+ transition="all 300ms"
69
+ mr={1}
70
+ border={`1px dashed ${isEditing ? editingBorderColor : 'transparent'}`}
71
+ flex={1}
72
+ minW="0">
73
+ {isEditing || alwaysEditing ? (
74
+ <StandaloneInput
75
+ ref={inputRef}
76
+ value={formattedValue}
77
+ inEditable
78
+ field={field}
79
+ style={{ padding: 0, ...style }}
80
+ onChange={alwaysEditing ? v => submitValue(parse(v)) : v => setValue(parse(v))}
81
+ />
82
+ ) : (
83
+ <FieldView style={style} field={field} value={value} />
84
+ )}
85
+ </Flex>
86
+ {alwaysEditing ? null : (
87
+ <>
88
+ <IconButton
89
+ aria-label="Edit"
90
+ size="xs"
91
+ icon={isEditing ? <CloseIcon width={3} /> : <EditIcon />}
92
+ onMouseEnter={() => setEditHovered(true)}
93
+ onMouseLeave={() => setEditHovered(false)}
94
+ onClick={() => {
95
+ if (isEditing) {
96
+ setValue(value)
97
+ handleEditClose()
98
+ } else handleEditOpen()
99
+ }}
100
+ />
101
+ <Flex
102
+ transition="all 300ms"
103
+ justify="flex-end"
104
+ opacity={isEditing ? 1 : 0}
105
+ overflow="hidden"
106
+ w={isEditing ? '30px' : '0px'}>
107
+ <IconButton
108
+ aria-label="Edit"
109
+ size="xs"
110
+ bg={palette.cyan.light}
111
+ _dark={{ bg: palette.cyan.light, _hover: { bg: palette.cyan.lighter } }}
112
+ _hover={{
113
+ bg: palette.cyan.medium,
114
+ }}
115
+ icon={<CheckIcon filter="drop-shadow(1px 1px 3px #000000aa)" />}
116
+ onClick={() => {
117
+ submit()
118
+ }}
119
+ />
120
+ </Flex>
121
+ </>
122
+ )}
123
+ <LoadingOverlay isLoading={isLoading} />
124
+ </Flex>
125
+ <UploadProgress uploads={uploads} />
126
+ </Flex>
127
+ </ChempoFormProvider>
128
+ )
129
+ }