@overmap-ai/forms 1.0.17-master.1 → 1.0.17-master.3
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.
- package/package.json +4 -1
- package/.husky/pre-commit +0 -6
- package/.prettierrc.json +0 -10
- package/.storybook/StoryDecorator.tsx +0 -22
- package/.storybook/main.ts +0 -20
- package/.storybook/palettes/green.css +0 -66
- package/.storybook/palettes/red.css +0 -66
- package/.storybook/preview.css +0 -39
- package/.storybook/preview.tsx +0 -31
- package/.storybook/tailwind-theme/accentPalette.css +0 -181
- package/.storybook/tailwind-theme/backgrounds.css +0 -11
- package/.storybook/tailwind-theme/basePalette.css +0 -178
- package/dev/publish-alpha.sh +0 -13
- package/dev/publish-patch.sh +0 -3
- package/eslint.config.js +0 -56
- package/src/ColorPicker/ColorPicker.tsx +0 -47
- package/src/ColorPicker/index.ts +0 -1
- package/src/FileBadge/FileBadge.tsx +0 -27
- package/src/FileBadge/index.ts +0 -1
- package/src/FileCard/FileCard.stories.tsx +0 -69
- package/src/FileCard/FileCard.tsx +0 -53
- package/src/FileCard/index.ts +0 -1
- package/src/FileIcon/FileIcon.tsx +0 -31
- package/src/FileIcon/index.ts +0 -1
- package/src/FileViewer/FileViewerProvider.stories.tsx +0 -50
- package/src/FileViewer/FileViewerProvider.tsx +0 -72
- package/src/FileViewer/context.ts +0 -11
- package/src/FileViewer/index.ts +0 -3
- package/src/FileViewer/typings.ts +0 -5
- package/src/ImageCard/ImageCard.stories.tsx +0 -94
- package/src/ImageCard/ImageCard.tsx +0 -82
- package/src/ImageCard/index.ts +0 -1
- package/src/ImageMarkup/ImageMarkup.stories.tsx +0 -65
- package/src/ImageMarkup/ImageMarkup.tsx +0 -268
- package/src/ImageMarkup/index.ts +0 -1
- package/src/ImageViewer/ImageViewer.stories.tsx +0 -57
- package/src/ImageViewer/ImageViewer.tsx +0 -124
- package/src/ImageViewer/constants.ts +0 -1
- package/src/ImageViewer/index.ts +0 -2
- package/src/PDFViewer/PDFViewer.stories.tsx +0 -55
- package/src/PDFViewer/PDFViewer.tsx +0 -170
- package/src/PDFViewer/constants.ts +0 -1
- package/src/PDFViewer/index.ts +0 -2
- package/src/SpreadsheetViewer/SpreadsheetViewer.stories.tsx +0 -55
- package/src/SpreadsheetViewer/SpreadsheetViewer.tsx +0 -162
- package/src/SpreadsheetViewer/constants.ts +0 -8
- package/src/SpreadsheetViewer/index.ts +0 -2
- package/src/forms/builder/DropDispatch.ts +0 -84
- package/src/forms/builder/FieldActions.tsx +0 -155
- package/src/forms/builder/FieldBuilder.tsx +0 -386
- package/src/forms/builder/FieldSectionWithActions.tsx +0 -260
- package/src/forms/builder/FieldWithActions.tsx +0 -129
- package/src/forms/builder/FieldsEditor.tsx +0 -180
- package/src/forms/builder/FormBuilder.stories.tsx +0 -105
- package/src/forms/builder/FormBuilder.tsx +0 -237
- package/src/forms/builder/constants.ts +0 -18
- package/src/forms/builder/hooks.tsx +0 -24
- package/src/forms/builder/index.ts +0 -2
- package/src/forms/builder/typings.ts +0 -18
- package/src/forms/builder/utils.ts +0 -229
- package/src/forms/constants.ts +0 -9
- package/src/forms/constantsJsx.tsx +0 -67
- package/src/forms/fields/BaseField/BaseField.ts +0 -152
- package/src/forms/fields/BaseField/hooks.tsx +0 -60
- package/src/forms/fields/BaseField/index.ts +0 -4
- package/src/forms/fields/BaseField/layouts.tsx +0 -100
- package/src/forms/fields/BaseField/typings.ts +0 -9
- package/src/forms/fields/BooleanField/BooleanField.tsx +0 -48
- package/src/forms/fields/BooleanField/BooleanInput.tsx +0 -54
- package/src/forms/fields/BooleanField/index.ts +0 -2
- package/src/forms/fields/CustomField/CustomField.tsx +0 -45
- package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputCloner.tsx +0 -25
- package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputClonerField.tsx +0 -26
- package/src/forms/fields/CustomField/FieldInputClonerField/index.ts +0 -3
- package/src/forms/fields/CustomField/FieldInputClonerField/typings.ts +0 -8
- package/src/forms/fields/CustomField/index.ts +0 -1
- package/src/forms/fields/DateField/DateField.tsx +0 -42
- package/src/forms/fields/DateField/DateInput.tsx +0 -39
- package/src/forms/fields/DateField/index.ts +0 -2
- package/src/forms/fields/FieldSection/FieldSection.tsx +0 -173
- package/src/forms/fields/FieldSection/FieldSectionLayout.tsx +0 -56
- package/src/forms/fields/FieldSection/index.ts +0 -1
- package/src/forms/fields/MultiStringField/MultiStringField.tsx +0 -90
- package/src/forms/fields/MultiStringField/MultiStringInput.tsx +0 -207
- package/src/forms/fields/MultiStringField/index.ts +0 -2
- package/src/forms/fields/NumberField/NumberField.tsx +0 -173
- package/src/forms/fields/NumberField/NumberInput.tsx +0 -44
- package/src/forms/fields/NumberField/index.ts +0 -2
- package/src/forms/fields/QrField/QrField.tsx +0 -38
- package/src/forms/fields/QrField/QrInput.module.sass +0 -5
- package/src/forms/fields/QrField/QrInput.tsx +0 -144
- package/src/forms/fields/QrField/index.ts +0 -2
- package/src/forms/fields/SelectField/BaseSelectField.ts +0 -73
- package/src/forms/fields/SelectField/MultiSelectField.tsx +0 -53
- package/src/forms/fields/SelectField/MultiSelectInput.tsx +0 -80
- package/src/forms/fields/SelectField/SelectField.tsx +0 -49
- package/src/forms/fields/SelectField/SelectInput.tsx +0 -69
- package/src/forms/fields/SelectField/index.ts +0 -4
- package/src/forms/fields/StringOrTextFields/StringField/StringField.tsx +0 -61
- package/src/forms/fields/StringOrTextFields/StringField/StringInput.tsx +0 -41
- package/src/forms/fields/StringOrTextFields/StringField/index.ts +0 -2
- package/src/forms/fields/StringOrTextFields/StringOrTextField.ts +0 -143
- package/src/forms/fields/StringOrTextFields/TextField/TextField.tsx +0 -52
- package/src/forms/fields/StringOrTextFields/TextField/TextInput.tsx +0 -42
- package/src/forms/fields/StringOrTextFields/TextField/index.ts +0 -2
- package/src/forms/fields/StringOrTextFields/index.ts +0 -2
- package/src/forms/fields/UploadField/UploadField.tsx +0 -156
- package/src/forms/fields/UploadField/UploadInput.tsx +0 -220
- package/src/forms/fields/UploadField/index.ts +0 -2
- package/src/forms/fields/UploadField/utils.ts +0 -17
- package/src/forms/fields/constants.ts +0 -43
- package/src/forms/fields/hooks.tsx +0 -26
- package/src/forms/fields/index.ts +0 -12
- package/src/forms/fields/typings.ts +0 -45
- package/src/forms/fields/utils.ts +0 -125
- package/src/forms/index.ts +0 -5
- package/src/forms/renderer/FormRenderer/FormRenderer.stories.tsx +0 -142
- package/src/forms/renderer/FormRenderer/FormRenderer.tsx +0 -135
- package/src/forms/renderer/PatchForm/Field.tsx +0 -41
- package/src/forms/renderer/PatchForm/PatchForm.stories.tsx +0 -91
- package/src/forms/renderer/PatchForm/Provider.tsx +0 -119
- package/src/forms/renderer/PatchForm/index.ts +0 -2
- package/src/forms/renderer/index.ts +0 -2
- package/src/forms/typings.ts +0 -162
- package/src/forms/utils.ts +0 -69
- package/src/index.ts +0 -11
- package/src/vite-env.d.ts +0 -1
- package/tailwind.config.ts +0 -8
- package/tsconfig.json +0 -26
- package/tsconfig.node.json +0 -10
- package/vite.config.ts +0 -23
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { ButtonGroup, IconButton, IconToggleButton, RiIcon, Separator, Spinner, useSize } from "@overmap-ai/blocks"
|
|
2
|
-
import { CSSColor } from "@overmap-ai/core"
|
|
3
|
-
import { Colors, DeferredPromise, downloadFile, fileToBlob, hashFile } from "@overmap-ai/core"
|
|
4
|
-
import * as RadixDialog from "@radix-ui/react-dialog"
|
|
5
|
-
import React, { memo, RefObject, useCallback, useEffect, useRef, useState } from "react"
|
|
6
|
-
import { ReactSketchCanvas, ReactSketchCanvasRef } from "react-sketch-canvas"
|
|
7
|
-
|
|
8
|
-
import { ColorPicker } from "../ColorPicker"
|
|
9
|
-
|
|
10
|
-
type FileWithObjectURL = File & { objectURL?: string }
|
|
11
|
-
export interface AttachmentMarkupProps {
|
|
12
|
-
file: FileWithObjectURL
|
|
13
|
-
onClose: () => void
|
|
14
|
-
onSave: (imageWithMarkup: FileWithObjectURL) => void
|
|
15
|
-
onDelete: (imageFile: FileWithObjectURL) => void
|
|
16
|
-
dirty: boolean
|
|
17
|
-
onDirty: (dirty: boolean) => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const colors: CSSColor[] = Object.values(Colors)
|
|
21
|
-
|
|
22
|
-
// 0-1 range, 1 is best quality.
|
|
23
|
-
const COMPRESSION_QUALITY = 1
|
|
24
|
-
|
|
25
|
-
async function combineImages(
|
|
26
|
-
canvas: HTMLCanvasElement,
|
|
27
|
-
image1: File,
|
|
28
|
-
image2: File,
|
|
29
|
-
finalSize: { width: number; height: number },
|
|
30
|
-
): Promise<FileWithObjectURL> {
|
|
31
|
-
const context = canvas.getContext("2d", {} satisfies CanvasRenderingContext2DSettings)!
|
|
32
|
-
const imageObj1 = new Image(finalSize.width, finalSize.height)
|
|
33
|
-
const imageObj2 = new Image(finalSize.width, finalSize.height)
|
|
34
|
-
const ret = new DeferredPromise<FileWithObjectURL>()
|
|
35
|
-
// Add an onload handler to the image
|
|
36
|
-
imageObj1.onload = function () {
|
|
37
|
-
context.drawImage(imageObj1, 0, 0, finalSize.width, finalSize.height)
|
|
38
|
-
imageObj2.src = URL.createObjectURL(image2)
|
|
39
|
-
imageObj2.onload = async function () {
|
|
40
|
-
context.drawImage(imageObj2, 0, 0, finalSize.width, finalSize.height)
|
|
41
|
-
const combinedDataUrl = canvas.toDataURL("image/jpeg", COMPRESSION_QUALITY)
|
|
42
|
-
const asBlob = await fileToBlob(combinedDataUrl)
|
|
43
|
-
const asFile = new File([asBlob], image1.name, { type: "image/jpeg" })
|
|
44
|
-
ret.resolve(asFile as FileWithObjectURL)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Load the image
|
|
48
|
-
imageObj1.src = URL.createObjectURL(image1)
|
|
49
|
-
return ret
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// TODO: Make this similar to the Alert Dialog setup with a context
|
|
53
|
-
export const ImageMarkup = memo((props: AttachmentMarkupProps) => {
|
|
54
|
-
const { file, onClose, onSave, onDelete, dirty, onDirty } = props
|
|
55
|
-
if (!file.objectURL) {
|
|
56
|
-
file.objectURL = URL.createObjectURL(file)
|
|
57
|
-
}
|
|
58
|
-
const canvasRef = useRef<ReactSketchCanvasRef>(null)
|
|
59
|
-
const imageCombinerRef = useRef<HTMLCanvasElement>(null)
|
|
60
|
-
const [color, setColor] = useState<CSSColor>(Colors.red!)
|
|
61
|
-
const [originalImageSize, setOriginalImageSize] = useState<{ width: number; height: number } | null>(null)
|
|
62
|
-
const [loading, setLoading] = useState(false)
|
|
63
|
-
const [reset, setReset] = useState(false)
|
|
64
|
-
|
|
65
|
-
// We'll be watching for resize events of the inner container, so we know what to animate the height to.
|
|
66
|
-
const watchResizeTarget = useRef<HTMLImageElement>(null)
|
|
67
|
-
const currentImageSize = useSize(watchResizeTarget as RefObject<HTMLImageElement>)
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
const image = new Image()
|
|
71
|
-
image.onload = function () {
|
|
72
|
-
setOriginalImageSize({ width: image.naturalWidth, height: image.naturalHeight })
|
|
73
|
-
}
|
|
74
|
-
image.src = URL.createObjectURL(file)
|
|
75
|
-
}, [file])
|
|
76
|
-
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (!reset && currentImageSize?.width && currentImageSize.height) {
|
|
79
|
-
setReset(true)
|
|
80
|
-
}
|
|
81
|
-
}, [currentImageSize, reset])
|
|
82
|
-
|
|
83
|
-
const saveMarkup = useCallback(async () => {
|
|
84
|
-
if (!dirty) onClose()
|
|
85
|
-
|
|
86
|
-
const currentCanvasRef = canvasRef.current
|
|
87
|
-
if (!currentCanvasRef) return
|
|
88
|
-
|
|
89
|
-
if (!originalImageSize) {
|
|
90
|
-
throw new Error("Original image size not loaded yet")
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const markupDataUrl: string = await currentCanvasRef.exportImage("png")
|
|
94
|
-
const markupImage = new Image(originalImageSize.width, originalImageSize.height)
|
|
95
|
-
|
|
96
|
-
markupImage.onload = async function () {
|
|
97
|
-
const markupBlob: Blob = await fileToBlob(markupDataUrl)
|
|
98
|
-
const markupFile: File = new File([markupBlob], file.name, { type: file.type })
|
|
99
|
-
const canvas = imageCombinerRef.current!
|
|
100
|
-
const combinedFile = await combineImages(canvas, file, markupFile, originalImageSize)
|
|
101
|
-
combinedFile.objectURL = URL.createObjectURL(combinedFile)
|
|
102
|
-
const oldHash = await hashFile(file)
|
|
103
|
-
const newHash = await hashFile(combinedFile)
|
|
104
|
-
// If the markup didn't change the image, we don't need to save it. In fact, doing so results in an error.
|
|
105
|
-
if (newHash !== oldHash) {
|
|
106
|
-
onSave(combinedFile)
|
|
107
|
-
}
|
|
108
|
-
setLoading(false)
|
|
109
|
-
onClose()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
setLoading(true)
|
|
113
|
-
// Initiates the loading of image process
|
|
114
|
-
markupImage.src = markupDataUrl
|
|
115
|
-
}, [dirty, file, onClose, onSave, originalImageSize])
|
|
116
|
-
|
|
117
|
-
const handleDelete = useCallback(() => {
|
|
118
|
-
onDelete(file)
|
|
119
|
-
}, [file, onDelete])
|
|
120
|
-
|
|
121
|
-
const handleCancel = useCallback(() => {
|
|
122
|
-
onClose()
|
|
123
|
-
}, [onClose])
|
|
124
|
-
|
|
125
|
-
const handleCanvasChange = useCallback(() => {
|
|
126
|
-
onDirty(true)
|
|
127
|
-
}, [onDirty])
|
|
128
|
-
|
|
129
|
-
const handleRedoLast = useCallback(() => {
|
|
130
|
-
if (!canvasRef.current) return
|
|
131
|
-
canvasRef.current.redo()
|
|
132
|
-
}, [])
|
|
133
|
-
|
|
134
|
-
const handleUndoLast = useCallback(() => {
|
|
135
|
-
if (!canvasRef.current) return
|
|
136
|
-
canvasRef.current.undo()
|
|
137
|
-
}, [])
|
|
138
|
-
|
|
139
|
-
const handleUndoAll = useCallback(() => {
|
|
140
|
-
if (!canvasRef.current) return
|
|
141
|
-
onDirty(false)
|
|
142
|
-
canvasRef.current.clearCanvas()
|
|
143
|
-
}, [onDirty])
|
|
144
|
-
|
|
145
|
-
const handleToggleEraseMode = useCallback((erasing: boolean) => {
|
|
146
|
-
if (!canvasRef.current) return
|
|
147
|
-
canvasRef.current.eraseMode(erasing)
|
|
148
|
-
}, [])
|
|
149
|
-
|
|
150
|
-
const handleSave = useCallback(() => {
|
|
151
|
-
void saveMarkup()
|
|
152
|
-
}, [saveMarkup])
|
|
153
|
-
|
|
154
|
-
const handleDownload: React.MouseEventHandler<HTMLButtonElement> = useCallback(() => {
|
|
155
|
-
downloadFile(file)
|
|
156
|
-
}, [file])
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<RadixDialog.Root open onOpenChange={onClose}>
|
|
160
|
-
<RadixDialog.Portal>
|
|
161
|
-
<RadixDialog.Overlay className="light:bg-(--black-a6) fixed inset-0 dark:bg-(--black-a8)" />
|
|
162
|
-
<RadixDialog.Content className="fixed inset-0">
|
|
163
|
-
<>
|
|
164
|
-
{/* Invisible canvas for combining background image and markup */}
|
|
165
|
-
{originalImageSize && (
|
|
166
|
-
<canvas
|
|
167
|
-
style={{ display: "none" }}
|
|
168
|
-
id="attachment-markup-canvas"
|
|
169
|
-
ref={imageCombinerRef}
|
|
170
|
-
width={originalImageSize.width}
|
|
171
|
-
height={originalImageSize.height}
|
|
172
|
-
/>
|
|
173
|
-
)}
|
|
174
|
-
<div className="relative flex h-full w-full flex-col">
|
|
175
|
-
<div className="flex h-8 w-full items-center bg-(--color-background) px-2">
|
|
176
|
-
<ButtonGroup accentColor="base" size="md" variant="soft">
|
|
177
|
-
<div className="grid w-full grid-cols-3">
|
|
178
|
-
<div className="flex gap-2">
|
|
179
|
-
<IconButton aria-label="close" onClick={handleCancel}>
|
|
180
|
-
<RiIcon icon="RiCloseLine" />
|
|
181
|
-
</IconButton>
|
|
182
|
-
<IconButton aria-label={`Download ${file.name}`} onClick={handleDownload}>
|
|
183
|
-
<RiIcon icon="RiDownload2Line" />
|
|
184
|
-
</IconButton>
|
|
185
|
-
</div>
|
|
186
|
-
<div className="flex items-center justify-center gap-2">
|
|
187
|
-
<ColorPicker
|
|
188
|
-
selectedColor={color}
|
|
189
|
-
allColors={colors}
|
|
190
|
-
onFinish={setColor}
|
|
191
|
-
trigger={
|
|
192
|
-
<IconButton
|
|
193
|
-
aria-label="Markup color picker"
|
|
194
|
-
type="button"
|
|
195
|
-
variant="solid"
|
|
196
|
-
style={{
|
|
197
|
-
backgroundColor: color,
|
|
198
|
-
}}
|
|
199
|
-
>
|
|
200
|
-
{" "}
|
|
201
|
-
</IconButton>
|
|
202
|
-
}
|
|
203
|
-
/>
|
|
204
|
-
<IconToggleButton
|
|
205
|
-
defaultPressed={false}
|
|
206
|
-
aria-label="erase"
|
|
207
|
-
onPressedChange={handleToggleEraseMode}
|
|
208
|
-
>
|
|
209
|
-
<RiIcon icon="RiEraserFill" />
|
|
210
|
-
</IconToggleButton>
|
|
211
|
-
<Separator orientation="vertical" size="full" />
|
|
212
|
-
<IconButton aria-label="undo" onClick={handleUndoLast}>
|
|
213
|
-
<RiIcon icon="RiArrowGoBackLine" />
|
|
214
|
-
</IconButton>
|
|
215
|
-
<IconButton aria-label="undo" onClick={handleRedoLast}>
|
|
216
|
-
<RiIcon icon="RiArrowGoForwardLine" />
|
|
217
|
-
</IconButton>
|
|
218
|
-
<IconButton aria-label="undo all" onClick={handleUndoAll}>
|
|
219
|
-
<RiIcon icon="RiLoopLeftLine" />
|
|
220
|
-
</IconButton>
|
|
221
|
-
</div>
|
|
222
|
-
<div className="flex justify-end gap-2">
|
|
223
|
-
<IconButton
|
|
224
|
-
aria-label="Save markup"
|
|
225
|
-
accentColor="primary"
|
|
226
|
-
onClick={handleSave}
|
|
227
|
-
>
|
|
228
|
-
<RiIcon icon="RiSaveLine" />
|
|
229
|
-
</IconButton>
|
|
230
|
-
<IconButton aria-label="Delete attachment" onClick={handleDelete}>
|
|
231
|
-
<RiIcon icon="RiDeleteBin2Line" />
|
|
232
|
-
</IconButton>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</ButtonGroup>
|
|
236
|
-
</div>
|
|
237
|
-
<div className="relative flex w-full grow items-center justify-center p-4">
|
|
238
|
-
{/**NOTE: Invisible. Its only purpose is for us to measure its size (which is automatically
|
|
239
|
-
set for img tags */}
|
|
240
|
-
<img
|
|
241
|
-
className="translate-xmax-h-[calc(100%-130px)] pointer-events-none invisible absolute top-[50%] left-[50%] max-w-svw translate-x-[-50%] translate-y-[-50%] object-contain"
|
|
242
|
-
alt="Photo attachment"
|
|
243
|
-
ref={watchResizeTarget}
|
|
244
|
-
src={file.objectURL}
|
|
245
|
-
/>
|
|
246
|
-
|
|
247
|
-
{loading ? (
|
|
248
|
-
<Spinner />
|
|
249
|
-
) : (
|
|
250
|
-
<ReactSketchCanvas
|
|
251
|
-
key={reset ? "1" : "0"}
|
|
252
|
-
backgroundImage={file.objectURL}
|
|
253
|
-
ref={canvasRef}
|
|
254
|
-
width={`${currentImageSize?.width || 0}px`}
|
|
255
|
-
height={`${currentImageSize?.height || 0}px`}
|
|
256
|
-
onStroke={handleCanvasChange}
|
|
257
|
-
strokeColor={color}
|
|
258
|
-
/>
|
|
259
|
-
)}
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</>
|
|
263
|
-
</RadixDialog.Content>
|
|
264
|
-
</RadixDialog.Portal>
|
|
265
|
-
</RadixDialog.Root>
|
|
266
|
-
)
|
|
267
|
-
})
|
|
268
|
-
ImageMarkup.displayName = "AttachmentEditor"
|
package/src/ImageMarkup/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./ImageMarkup"
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { Button } from "@overmap-ai/blocks"
|
|
2
|
-
import type { Meta } from "@storybook/react"
|
|
3
|
-
import { ChangeEvent, useCallback, useRef, useState } from "react"
|
|
4
|
-
|
|
5
|
-
import { ImageViewer } from "./ImageViewer"
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Components/ImageViewer",
|
|
9
|
-
component: ImageViewer,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
parameters: {
|
|
12
|
-
disablePanel: true,
|
|
13
|
-
},
|
|
14
|
-
} satisfies Meta<typeof ImageViewer>
|
|
15
|
-
|
|
16
|
-
export default meta
|
|
17
|
-
// type Story = StoryObj<typeof meta>
|
|
18
|
-
|
|
19
|
-
export const Basic = () => {
|
|
20
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
21
|
-
const [file, setFile] = useState<File | null>(null)
|
|
22
|
-
const [open, setOpen] = useState(false)
|
|
23
|
-
|
|
24
|
-
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
25
|
-
if (e.target.files) {
|
|
26
|
-
const files = Array.from(e.target.files)
|
|
27
|
-
if (files.length > 0) {
|
|
28
|
-
setFile(files[0] || null)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}, [])
|
|
32
|
-
|
|
33
|
-
console.log(file)
|
|
34
|
-
return (
|
|
35
|
-
<>
|
|
36
|
-
<input ref={inputRef} type="file" onChange={handleChange} />
|
|
37
|
-
<Button
|
|
38
|
-
onClick={() => {
|
|
39
|
-
setOpen(!open)
|
|
40
|
-
}}
|
|
41
|
-
>
|
|
42
|
-
open
|
|
43
|
-
</Button>
|
|
44
|
-
{file && open && (
|
|
45
|
-
<ImageViewer
|
|
46
|
-
file={file}
|
|
47
|
-
onClose={() => {
|
|
48
|
-
setOpen(false)
|
|
49
|
-
}}
|
|
50
|
-
onDelete={() => {
|
|
51
|
-
setFile(null)
|
|
52
|
-
}}
|
|
53
|
-
/>
|
|
54
|
-
)}
|
|
55
|
-
</>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { ButtonGroup, IconButton, RiIcon, useViewportSize } from "@overmap-ai/blocks"
|
|
2
|
-
import { downloadFile } from "@overmap-ai/core"
|
|
3
|
-
import * as RadixDialog from "@radix-ui/react-dialog"
|
|
4
|
-
import { memo, useCallback, useLayoutEffect, useRef } from "react"
|
|
5
|
-
|
|
6
|
-
import { FileBadge } from "../FileBadge"
|
|
7
|
-
|
|
8
|
-
interface ImageViewerProps {
|
|
9
|
-
file: File
|
|
10
|
-
onClose: () => void
|
|
11
|
-
onDelete?: (file: File) => void
|
|
12
|
-
}
|
|
13
|
-
export const ImageViewer = memo((props: ImageViewerProps) => {
|
|
14
|
-
const { file, onClose, onDelete } = props
|
|
15
|
-
const { md } = useViewportSize()
|
|
16
|
-
const imageRef = useRef<HTMLImageElement>(null)
|
|
17
|
-
const imageContainerRef = useRef<HTMLDivElement>(null)
|
|
18
|
-
const originalImageSize = useRef<{ width: number; height: number }>(null)
|
|
19
|
-
|
|
20
|
-
const adjustSize = useCallback(() => {
|
|
21
|
-
if (!imageContainerRef.current || !imageRef.current || !originalImageSize.current) return
|
|
22
|
-
|
|
23
|
-
const heightDifference = originalImageSize.current.height - imageContainerRef.current.clientHeight
|
|
24
|
-
const widthDifference = originalImageSize.current.width - imageContainerRef.current.clientWidth
|
|
25
|
-
|
|
26
|
-
if (heightDifference >= 0 || widthDifference >= 0) {
|
|
27
|
-
if (widthDifference > heightDifference) {
|
|
28
|
-
imageRef.current.style.width = `${imageContainerRef.current.clientWidth}px`
|
|
29
|
-
imageRef.current.style.height = "unset"
|
|
30
|
-
} else {
|
|
31
|
-
imageRef.current.style.height = `${imageContainerRef.current.clientHeight}px`
|
|
32
|
-
imageRef.current.style.width = "unset"
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
imageRef.current.style.width = `${originalImageSize.current.width}px`
|
|
36
|
-
imageRef.current.style.height = `${originalImageSize.current.height}px`
|
|
37
|
-
}
|
|
38
|
-
}, [])
|
|
39
|
-
|
|
40
|
-
const handleDelete = useCallback(() => {
|
|
41
|
-
if (onDelete) onDelete(file)
|
|
42
|
-
}, [file, onDelete])
|
|
43
|
-
|
|
44
|
-
const handleDownload = useCallback(() => {
|
|
45
|
-
downloadFile(file)
|
|
46
|
-
}, [file])
|
|
47
|
-
|
|
48
|
-
useLayoutEffect(() => {
|
|
49
|
-
const image = new Image()
|
|
50
|
-
image.onload = () => {
|
|
51
|
-
originalImageSize.current = {
|
|
52
|
-
width: image.width,
|
|
53
|
-
height: image.height,
|
|
54
|
-
}
|
|
55
|
-
// once we have stored the original image size, adjust the size of the image to the appropriate size
|
|
56
|
-
adjustSize()
|
|
57
|
-
}
|
|
58
|
-
image.src = URL.createObjectURL(file)
|
|
59
|
-
/*NOTE: adjustSize function has no dependencies, so the only thing causing this hook to rerun
|
|
60
|
-
* is if file changes, which is as desired since that will require loading a new image.
|
|
61
|
-
*/
|
|
62
|
-
}, [adjustSize, file])
|
|
63
|
-
|
|
64
|
-
useLayoutEffect(() => {
|
|
65
|
-
if (!imageContainerRef.current) return
|
|
66
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
67
|
-
// we want to adjust the size of the image as its container is resized
|
|
68
|
-
adjustSize()
|
|
69
|
-
})
|
|
70
|
-
resizeObserver.observe(imageContainerRef.current)
|
|
71
|
-
return () => {
|
|
72
|
-
resizeObserver.disconnect()
|
|
73
|
-
}
|
|
74
|
-
}, [adjustSize])
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<RadixDialog.Root open onOpenChange={onClose}>
|
|
78
|
-
<RadixDialog.Portal>
|
|
79
|
-
<RadixDialog.Overlay className="light:bg-(--black-a6) fixed inset-0 dark:bg-(--black-a8)" />
|
|
80
|
-
<RadixDialog.Content className="fixed inset-0">
|
|
81
|
-
<div className="flex h-full w-full flex-col">
|
|
82
|
-
<div className="flex h-max w-full items-center bg-(--color-background) p-2">
|
|
83
|
-
<ButtonGroup className="w-full" accentColor="base" variant="soft">
|
|
84
|
-
<div className="grid w-full grid-cols-3">
|
|
85
|
-
<div className="flex justify-start gap-2 items-center">
|
|
86
|
-
<IconButton onClick={onClose} aria-label="close">
|
|
87
|
-
<RiIcon icon="RiCloseLine" />
|
|
88
|
-
</IconButton>
|
|
89
|
-
<IconButton onClick={handleDownload} aria-label="close">
|
|
90
|
-
<RiIcon icon="RiDownload2Line" />
|
|
91
|
-
</IconButton>
|
|
92
|
-
</div>
|
|
93
|
-
<div className="flex justify-center">
|
|
94
|
-
<FileBadge
|
|
95
|
-
file={file}
|
|
96
|
-
accentColor="base"
|
|
97
|
-
truncateLength={!md ? 25 : undefined}
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
<div className="flex justify-end">
|
|
101
|
-
{!!onDelete && (
|
|
102
|
-
<IconButton onClick={handleDelete} aria-label="close">
|
|
103
|
-
<RiIcon icon="RiDeleteBin2Line" />
|
|
104
|
-
</IconButton>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</ButtonGroup>
|
|
109
|
-
</div>
|
|
110
|
-
<div className="h-full w-full overflow-hidden p-5">
|
|
111
|
-
<div
|
|
112
|
-
className="flex h-full w-full items-center justify-center overflow-hidden"
|
|
113
|
-
ref={imageContainerRef}
|
|
114
|
-
>
|
|
115
|
-
<img ref={imageRef} src={URL.createObjectURL(file)} alt="" />
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</RadixDialog.Content>
|
|
120
|
-
</RadixDialog.Portal>
|
|
121
|
-
</RadixDialog.Root>
|
|
122
|
-
)
|
|
123
|
-
})
|
|
124
|
-
ImageViewer.displayName = "ImageViewer"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const SUPPORTED_IMAGE_FILE_TYPES = ["image/jpeg", "image/png", "image/svg+xml"]
|
package/src/ImageViewer/index.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Button } from "@overmap-ai/blocks"
|
|
2
|
-
import type { Meta } from "@storybook/react"
|
|
3
|
-
import { ChangeEvent, useCallback, useRef, useState } from "react"
|
|
4
|
-
|
|
5
|
-
import { PDFViewer } from "./PDFViewer"
|
|
6
|
-
|
|
7
|
-
const meta = {
|
|
8
|
-
title: "Components/PDFViewer",
|
|
9
|
-
component: PDFViewer,
|
|
10
|
-
tags: ["autodocs"],
|
|
11
|
-
parameters: {
|
|
12
|
-
disablePanel: true,
|
|
13
|
-
},
|
|
14
|
-
} satisfies Meta<typeof PDFViewer>
|
|
15
|
-
|
|
16
|
-
export default meta
|
|
17
|
-
|
|
18
|
-
export const Basic = () => {
|
|
19
|
-
const inputRef = useRef<HTMLInputElement>(null)
|
|
20
|
-
const [file, setFile] = useState<File | null>(null)
|
|
21
|
-
const [open, setOpen] = useState(false)
|
|
22
|
-
|
|
23
|
-
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
24
|
-
if (e.target.files) {
|
|
25
|
-
const files = Array.from(e.target.files)
|
|
26
|
-
if (files.length > 0) {
|
|
27
|
-
setFile(files[0] || null)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<>
|
|
34
|
-
<input ref={inputRef} type="file" onChange={handleChange} />
|
|
35
|
-
<Button
|
|
36
|
-
onClick={() => {
|
|
37
|
-
setOpen(!open)
|
|
38
|
-
}}
|
|
39
|
-
>
|
|
40
|
-
open
|
|
41
|
-
</Button>
|
|
42
|
-
{file && open && (
|
|
43
|
-
<PDFViewer
|
|
44
|
-
file={file}
|
|
45
|
-
onClose={() => {
|
|
46
|
-
setOpen(false)
|
|
47
|
-
}}
|
|
48
|
-
onDelete={() => {
|
|
49
|
-
setFile(null)
|
|
50
|
-
}}
|
|
51
|
-
/>
|
|
52
|
-
)}
|
|
53
|
-
</>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import "react-pdf/dist/Page/AnnotationLayer.css"
|
|
2
|
-
import "react-pdf/dist/Page/TextLayer.css"
|
|
3
|
-
|
|
4
|
-
import { Badge, ButtonGroup, IconButton, RiIcon, Spinner, useViewportSize, ViewportSizes } from "@overmap-ai/blocks"
|
|
5
|
-
import { downloadFile } from "@overmap-ai/core"
|
|
6
|
-
import * as RadixDialog from "@radix-ui/react-dialog"
|
|
7
|
-
import type { PDFDocumentProxy } from "pdfjs-dist"
|
|
8
|
-
import { memo, useCallback, useEffect, useState } from "react"
|
|
9
|
-
import { Document, Page, pdfjs } from "react-pdf"
|
|
10
|
-
|
|
11
|
-
import { FileBadge } from "../FileBadge"
|
|
12
|
-
|
|
13
|
-
pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.js", import.meta.url).toString()
|
|
14
|
-
|
|
15
|
-
const ViewportSizeToScaleMapping: Record<ViewportSizes, number> = {
|
|
16
|
-
initial: 0.6,
|
|
17
|
-
xs: 0.8,
|
|
18
|
-
sm: 0.9,
|
|
19
|
-
md: 1,
|
|
20
|
-
lg: 1.1,
|
|
21
|
-
xl: 1.1,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// TODO: Add swiping functionality to navigate through pages on both desktop and mobile
|
|
25
|
-
interface PDFViewerProps {
|
|
26
|
-
file: File
|
|
27
|
-
onDelete?: (file: File) => void
|
|
28
|
-
onClose: () => void
|
|
29
|
-
}
|
|
30
|
-
export const PDFViewer = memo((props: PDFViewerProps) => {
|
|
31
|
-
const { file, onDelete, onClose } = props
|
|
32
|
-
|
|
33
|
-
// loaded pdf from file if the load was successful
|
|
34
|
-
const [pdf, setPdf] = useState<PDFDocumentProxy | null>(null)
|
|
35
|
-
// current page being viewed
|
|
36
|
-
const [currentPageIndex, setCurrentPageIndex] = useState<number>(0)
|
|
37
|
-
// boolean used to track if an error occured when loading pdf from file
|
|
38
|
-
const [error, setError] = useState<boolean>(false)
|
|
39
|
-
const { lg, size } = useViewportSize()
|
|
40
|
-
|
|
41
|
-
/** when passed in file changes, have to restart the loading pdf process */
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
setCurrentPageIndex(0)
|
|
44
|
-
setError(false)
|
|
45
|
-
setPdf(null)
|
|
46
|
-
}, [file])
|
|
47
|
-
|
|
48
|
-
const handleLoadSuccess = useCallback((pdf: PDFDocumentProxy) => {
|
|
49
|
-
setCurrentPageIndex(0)
|
|
50
|
-
setPdf(pdf)
|
|
51
|
-
}, [])
|
|
52
|
-
|
|
53
|
-
const handleLoadError = useCallback(() => {
|
|
54
|
-
setError(true)
|
|
55
|
-
}, [])
|
|
56
|
-
|
|
57
|
-
const handleNextPage = useCallback(() => {
|
|
58
|
-
if (!pdf || currentPageIndex === pdf.numPages - 1) return
|
|
59
|
-
setCurrentPageIndex((prevPageIndex) => prevPageIndex + 1)
|
|
60
|
-
}, [currentPageIndex, pdf])
|
|
61
|
-
|
|
62
|
-
const handleDownload = useCallback(() => {
|
|
63
|
-
downloadFile(file)
|
|
64
|
-
}, [file])
|
|
65
|
-
|
|
66
|
-
const handlePrevPage = useCallback(() => {
|
|
67
|
-
if (currentPageIndex === 0) return
|
|
68
|
-
setCurrentPageIndex((prevPageIndex) => prevPageIndex - 1)
|
|
69
|
-
}, [currentPageIndex])
|
|
70
|
-
|
|
71
|
-
const handleDelete = useCallback(() => {
|
|
72
|
-
if (onDelete) onDelete(file)
|
|
73
|
-
}, [file, onDelete])
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<RadixDialog.Root open onOpenChange={onClose}>
|
|
77
|
-
<RadixDialog.Portal>
|
|
78
|
-
<RadixDialog.Overlay className="light:bg-(--black-a6) fixed inset-0 dark:bg-(--black-a8)" />
|
|
79
|
-
<RadixDialog.Content className="fixed inset-0">
|
|
80
|
-
<div className="flex h-full w-full flex-col items-center">
|
|
81
|
-
<div className="h-max w-full shrink-0 items-center bg-(--color-background) p-2">
|
|
82
|
-
<ButtonGroup className="flex items-center gap-1" variant="soft" accentColor="base">
|
|
83
|
-
<div className="grid w-full grid-cols-3">
|
|
84
|
-
<div className="w-full gap-2 flex items-center">
|
|
85
|
-
<IconButton onClick={onClose} aria-label="close">
|
|
86
|
-
<RiIcon icon="RiCloseLine" />
|
|
87
|
-
</IconButton>
|
|
88
|
-
<IconButton onClick={handleDownload} aria-label="download">
|
|
89
|
-
<RiIcon icon="RiDownload2Line" />
|
|
90
|
-
</IconButton>
|
|
91
|
-
</div>
|
|
92
|
-
<div className="flex justify-center">
|
|
93
|
-
<FileBadge
|
|
94
|
-
file={file}
|
|
95
|
-
accentColor="base"
|
|
96
|
-
truncateLength={!lg ? 25 : undefined}
|
|
97
|
-
/>
|
|
98
|
-
</div>
|
|
99
|
-
<div className="flex justify-end">
|
|
100
|
-
{!!onDelete && (
|
|
101
|
-
<IconButton onClick={handleDelete} aria-label="delete">
|
|
102
|
-
<RiIcon icon="RiDeleteBin2Line" />
|
|
103
|
-
</IconButton>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</ButtonGroup>
|
|
108
|
-
</div>
|
|
109
|
-
<div className="relative flex h-full w-full flex-col items-center gap-2 p-3">
|
|
110
|
-
{!pdf && !error && <SpinnerComponent />}
|
|
111
|
-
<div className="flex h-max max-h-full w-max max-w-full flex-col items-center justify-center overflow-auto [scrollbar-color:var(--base-6)_transparent] [scrollbar-width:thin]">
|
|
112
|
-
<Document
|
|
113
|
-
className="relative"
|
|
114
|
-
file={file}
|
|
115
|
-
onLoadSuccess={handleLoadSuccess}
|
|
116
|
-
onLoadError={handleLoadError}
|
|
117
|
-
// get rid of default error component
|
|
118
|
-
error=""
|
|
119
|
-
// get rid of default loading component
|
|
120
|
-
loading=""
|
|
121
|
-
>
|
|
122
|
-
<Page scale={ViewportSizeToScaleMapping[size]} pageIndex={currentPageIndex} />
|
|
123
|
-
{!!pdf && !error && (
|
|
124
|
-
<div className="absolute top-0 z-[2] flex w-full justify-center py-2">
|
|
125
|
-
<ButtonGroup variant="solid" accentColor="base" size="sm">
|
|
126
|
-
<IconButton onClick={handlePrevPage} aria-label="previous page">
|
|
127
|
-
<RiIcon icon="RiArrowLeftLine" />
|
|
128
|
-
</IconButton>
|
|
129
|
-
<Badge accentColor="base" style={{ borderRadius: 0 }} variant="solid">
|
|
130
|
-
{currentPageIndex + 1}/{pdf.numPages}
|
|
131
|
-
</Badge>
|
|
132
|
-
<IconButton onClick={handleNextPage} aria-label="next-page">
|
|
133
|
-
<RiIcon icon="RiArrowRightLine" />
|
|
134
|
-
</IconButton>
|
|
135
|
-
</ButtonGroup>
|
|
136
|
-
</div>
|
|
137
|
-
)}
|
|
138
|
-
</Document>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{error && <ErrorComponent />}
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
</RadixDialog.Content>
|
|
145
|
-
</RadixDialog.Portal>
|
|
146
|
-
</RadixDialog.Root>
|
|
147
|
-
)
|
|
148
|
-
})
|
|
149
|
-
PDFViewer.displayName = "PDFViewer"
|
|
150
|
-
|
|
151
|
-
const SpinnerComponent = memo(() => {
|
|
152
|
-
return (
|
|
153
|
-
<div className="absolute flex h-full w-full items-center justify-center">
|
|
154
|
-
<Spinner />
|
|
155
|
-
</div>
|
|
156
|
-
)
|
|
157
|
-
})
|
|
158
|
-
SpinnerComponent.displayName = "SpinnerComponent"
|
|
159
|
-
|
|
160
|
-
const ErrorComponent = memo(() => {
|
|
161
|
-
return (
|
|
162
|
-
<div className="flex h-[70%] w-[40%] flex-col items-center justify-center rounded-md border border-(--base-a6) bg-(--base-2)">
|
|
163
|
-
<RiIcon icon="RiFileWarningLine" size={40} />
|
|
164
|
-
<span className="text-sm font-light text-(--accent-a11)" data-accent-color="base">
|
|
165
|
-
Failed to load
|
|
166
|
-
</span>
|
|
167
|
-
</div>
|
|
168
|
-
)
|
|
169
|
-
})
|
|
170
|
-
ErrorComponent.displayName = "ErrorComponent"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const SUPPORTED_PDF_FILE_TYPES = ["application/pdf"]
|
package/src/PDFViewer/index.ts
DELETED