@overmap-ai/forms 0.0.1-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/.husky/pre-commit +6 -0
- package/.prettierrc.json +10 -0
- package/.storybook/StoryDecorator.tsx +22 -0
- package/.storybook/main.ts +20 -0
- package/.storybook/palettes/green.css +66 -0
- package/.storybook/palettes/red.css +66 -0
- package/.storybook/preview.css +39 -0
- package/.storybook/preview.tsx +31 -0
- package/.storybook/tailwind-theme/accentPalette.css +181 -0
- package/.storybook/tailwind-theme/backgrounds.css +11 -0
- package/.storybook/tailwind-theme/basePalette.css +178 -0
- package/dev/publish-alpha.sh +13 -0
- package/dev/publish-patch.sh +3 -0
- package/eslint.config.js +56 -0
- package/package.json +93 -0
- package/src/ColorPicker/ColorPicker.tsx +47 -0
- package/src/ColorPicker/index.ts +1 -0
- package/src/FileBadge/FileBadge.tsx +27 -0
- package/src/FileBadge/index.ts +1 -0
- package/src/FileCard/FileCard.stories.tsx +69 -0
- package/src/FileCard/FileCard.tsx +53 -0
- package/src/FileCard/index.ts +1 -0
- package/src/FileIcon/FileIcon.tsx +31 -0
- package/src/FileIcon/index.ts +1 -0
- package/src/FileViewer/FileViewerProvider.stories.tsx +50 -0
- package/src/FileViewer/FileViewerProvider.tsx +72 -0
- package/src/FileViewer/context.ts +11 -0
- package/src/FileViewer/index.ts +3 -0
- package/src/FileViewer/typings.ts +5 -0
- package/src/ImageCard/ImageCard.stories.tsx +94 -0
- package/src/ImageCard/ImageCard.tsx +82 -0
- package/src/ImageCard/index.ts +1 -0
- package/src/ImageMarkup/ImageMarkup.stories.tsx +65 -0
- package/src/ImageMarkup/ImageMarkup.tsx +268 -0
- package/src/ImageMarkup/index.ts +1 -0
- package/src/ImageViewer/ImageViewer.stories.tsx +57 -0
- package/src/ImageViewer/ImageViewer.tsx +124 -0
- package/src/ImageViewer/constants.ts +1 -0
- package/src/ImageViewer/index.ts +2 -0
- package/src/PDFViewer/PDFViewer.stories.tsx +55 -0
- package/src/PDFViewer/PDFViewer.tsx +170 -0
- package/src/PDFViewer/constants.ts +1 -0
- package/src/PDFViewer/index.ts +2 -0
- package/src/SpreadsheetViewer/SpreadsheetViewer.stories.tsx +55 -0
- package/src/SpreadsheetViewer/SpreadsheetViewer.tsx +162 -0
- package/src/SpreadsheetViewer/constants.ts +8 -0
- package/src/SpreadsheetViewer/index.ts +2 -0
- package/src/forms/builder/DropDispatch.ts +84 -0
- package/src/forms/builder/FieldActions.tsx +155 -0
- package/src/forms/builder/FieldBuilder.tsx +386 -0
- package/src/forms/builder/FieldSectionWithActions.tsx +260 -0
- package/src/forms/builder/FieldWithActions.tsx +129 -0
- package/src/forms/builder/FieldsEditor.tsx +180 -0
- package/src/forms/builder/FormBuilder.stories.tsx +105 -0
- package/src/forms/builder/FormBuilder.tsx +237 -0
- package/src/forms/builder/constants.ts +18 -0
- package/src/forms/builder/hooks.tsx +24 -0
- package/src/forms/builder/index.ts +2 -0
- package/src/forms/builder/typings.ts +18 -0
- package/src/forms/builder/utils.ts +229 -0
- package/src/forms/constants.ts +9 -0
- package/src/forms/constantsJsx.tsx +67 -0
- package/src/forms/fields/BaseField/BaseField.ts +152 -0
- package/src/forms/fields/BaseField/hooks.tsx +60 -0
- package/src/forms/fields/BaseField/index.ts +4 -0
- package/src/forms/fields/BaseField/layouts.tsx +100 -0
- package/src/forms/fields/BaseField/typings.ts +9 -0
- package/src/forms/fields/BooleanField/BooleanField.tsx +48 -0
- package/src/forms/fields/BooleanField/BooleanInput.tsx +54 -0
- package/src/forms/fields/BooleanField/index.ts +2 -0
- package/src/forms/fields/CustomField/CustomField.tsx +45 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputCloner.tsx +25 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputClonerField.tsx +26 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/index.ts +3 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/typings.ts +8 -0
- package/src/forms/fields/CustomField/index.ts +1 -0
- package/src/forms/fields/DateField/DateField.tsx +42 -0
- package/src/forms/fields/DateField/DateInput.tsx +39 -0
- package/src/forms/fields/DateField/index.ts +2 -0
- package/src/forms/fields/FieldSection/FieldSection.tsx +173 -0
- package/src/forms/fields/FieldSection/FieldSectionLayout.tsx +56 -0
- package/src/forms/fields/FieldSection/index.ts +1 -0
- package/src/forms/fields/MultiStringField/MultiStringField.tsx +90 -0
- package/src/forms/fields/MultiStringField/MultiStringInput.tsx +207 -0
- package/src/forms/fields/MultiStringField/index.ts +2 -0
- package/src/forms/fields/NumberField/NumberField.tsx +173 -0
- package/src/forms/fields/NumberField/NumberInput.tsx +44 -0
- package/src/forms/fields/NumberField/index.ts +2 -0
- package/src/forms/fields/QrField/QrField.tsx +38 -0
- package/src/forms/fields/QrField/QrInput.module.sass +5 -0
- package/src/forms/fields/QrField/QrInput.tsx +144 -0
- package/src/forms/fields/QrField/index.ts +2 -0
- package/src/forms/fields/SelectField/BaseSelectField.ts +73 -0
- package/src/forms/fields/SelectField/MultiSelectField.tsx +53 -0
- package/src/forms/fields/SelectField/MultiSelectInput.tsx +80 -0
- package/src/forms/fields/SelectField/SelectField.tsx +49 -0
- package/src/forms/fields/SelectField/SelectInput.tsx +69 -0
- package/src/forms/fields/SelectField/index.ts +4 -0
- package/src/forms/fields/StringOrTextFields/StringField/StringField.tsx +61 -0
- package/src/forms/fields/StringOrTextFields/StringField/StringInput.tsx +41 -0
- package/src/forms/fields/StringOrTextFields/StringField/index.ts +2 -0
- package/src/forms/fields/StringOrTextFields/StringOrTextField.ts +143 -0
- package/src/forms/fields/StringOrTextFields/TextField/TextField.tsx +52 -0
- package/src/forms/fields/StringOrTextFields/TextField/TextInput.tsx +42 -0
- package/src/forms/fields/StringOrTextFields/TextField/index.ts +2 -0
- package/src/forms/fields/StringOrTextFields/index.ts +2 -0
- package/src/forms/fields/UploadField/UploadField.tsx +156 -0
- package/src/forms/fields/UploadField/UploadInput.tsx +220 -0
- package/src/forms/fields/UploadField/index.ts +2 -0
- package/src/forms/fields/UploadField/utils.ts +17 -0
- package/src/forms/fields/constants.ts +43 -0
- package/src/forms/fields/hooks.tsx +26 -0
- package/src/forms/fields/index.ts +12 -0
- package/src/forms/fields/typings.ts +45 -0
- package/src/forms/fields/utils.ts +125 -0
- package/src/forms/index.ts +5 -0
- package/src/forms/renderer/FormRenderer/FormRenderer.stories.tsx +142 -0
- package/src/forms/renderer/FormRenderer/FormRenderer.tsx +135 -0
- package/src/forms/renderer/PatchForm/Field.tsx +41 -0
- package/src/forms/renderer/PatchForm/PatchForm.stories.tsx +91 -0
- package/src/forms/renderer/PatchForm/Provider.tsx +119 -0
- package/src/forms/renderer/PatchForm/index.ts +2 -0
- package/src/forms/renderer/index.ts +2 -0
- package/src/forms/typings.ts +162 -0
- package/src/forms/utils.ts +69 -0
- package/src/index.ts +11 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +8 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +23 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import pluginJs from "@eslint/js"
|
|
2
|
+
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
|
|
3
|
+
import pluginReact from "eslint-plugin-react"
|
|
4
|
+
import simpleImportSort from "eslint-plugin-simple-import-sort"
|
|
5
|
+
import pluginStorybook from "eslint-plugin-storybook"
|
|
6
|
+
import globals from "globals"
|
|
7
|
+
import tseslint from "typescript-eslint"
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Linter.Config[]} */
|
|
10
|
+
export default [
|
|
11
|
+
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
|
|
12
|
+
{ languageOptions: { globals: globals.browser } },
|
|
13
|
+
pluginJs.configs.recommended,
|
|
14
|
+
...tseslint.configs.recommended,
|
|
15
|
+
{
|
|
16
|
+
rules: {
|
|
17
|
+
"no-unused-vars": "off",
|
|
18
|
+
"@typescript-eslint/no-unused-vars": [
|
|
19
|
+
"error",
|
|
20
|
+
{
|
|
21
|
+
varsIgnorePattern: "^_",
|
|
22
|
+
argsIgnorePattern: "^_",
|
|
23
|
+
caughtErrorsIgnorePattern: "^_",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
plugins: {
|
|
30
|
+
"simple-import-sort": simpleImportSort,
|
|
31
|
+
},
|
|
32
|
+
rules: {
|
|
33
|
+
"simple-import-sort/imports": "error",
|
|
34
|
+
"simple-import-sort/exports": "error",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
...pluginReact.configs.flat.recommended,
|
|
39
|
+
settings: {
|
|
40
|
+
react: { version: "detect" },
|
|
41
|
+
},
|
|
42
|
+
rules: {
|
|
43
|
+
"react/react-in-jsx-scope": "off",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
...pluginStorybook.configs["flat/recommended"],
|
|
47
|
+
eslintPluginPrettierRecommended,
|
|
48
|
+
{
|
|
49
|
+
linterOptions: {
|
|
50
|
+
reportUnusedDisableDirectives: "error",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
ignores: ["!.storybook", "dist"],
|
|
55
|
+
},
|
|
56
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@overmap-ai/forms",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1-master.3",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"prepare": "husky",
|
|
12
|
+
"lint:fix": "yarn lint --fix",
|
|
13
|
+
"lint": "tsc --noEmit && eslint . --max-warnings 0",
|
|
14
|
+
"storybook": "storybook dev -p 6006",
|
|
15
|
+
"build-storybook": "storybook build"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@overmap-ai/blocks": "1.0.31-tailwind-components.22",
|
|
19
|
+
"@overmap-ai/core": "1.0.60-forms-removal.3",
|
|
20
|
+
"react": ">=18.2.0 <20.0.0",
|
|
21
|
+
"react-dom": ">=18.2.0 <20.0.0",
|
|
22
|
+
"tailwindcss": "^4.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@chromatic-com/storybook": "^3.2.4",
|
|
26
|
+
"@eslint/js": "^9.19.0",
|
|
27
|
+
"@overmap-ai/blocks": "1.0.31-tailwind-components.25",
|
|
28
|
+
"@overmap-ai/core": "1.0.60-forms-removal.3",
|
|
29
|
+
"@radix-ui/colors": "^3.0.0",
|
|
30
|
+
"@storybook/addon-a11y": "^8.5.3",
|
|
31
|
+
"@storybook/addon-essentials": "^8.5.3",
|
|
32
|
+
"@storybook/addon-interactions": "^8.5.3",
|
|
33
|
+
"@storybook/addon-onboarding": "^8.5.3",
|
|
34
|
+
"@storybook/addon-themes": "^8.5.3",
|
|
35
|
+
"@storybook/addon-viewport": "^8.5.3",
|
|
36
|
+
"@storybook/blocks": "^8.5.3",
|
|
37
|
+
"@storybook/react": "^8.5.3",
|
|
38
|
+
"@storybook/react-vite": "^8.5.3",
|
|
39
|
+
"@storybook/test": "^8.5.3",
|
|
40
|
+
"@storybook/types": "^8.5.3",
|
|
41
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
42
|
+
"@types/file-saver": "^2.0.7",
|
|
43
|
+
"@types/lodash.clonedeep": "^4.5.9",
|
|
44
|
+
"@types/lodash.get": "^4.4.9",
|
|
45
|
+
"@types/lodash.set": "^4.3.9",
|
|
46
|
+
"@types/node": "^22.13.0",
|
|
47
|
+
"@types/react": "^19.0.0",
|
|
48
|
+
"@types/react-dom": "^19.0.0",
|
|
49
|
+
"@types/react-icons": "^3.0.0",
|
|
50
|
+
"@types/xlsx": "^0.0.36",
|
|
51
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
52
|
+
"eslint": "^9.19.0",
|
|
53
|
+
"eslint-config-prettier": "^10.0.1",
|
|
54
|
+
"eslint-plugin-prettier": "^5.2.3",
|
|
55
|
+
"eslint-plugin-react": "^7.37.4",
|
|
56
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
57
|
+
"eslint-plugin-storybook": "^0.11.2",
|
|
58
|
+
"file-saver": "^2.0.5",
|
|
59
|
+
"globals": "^15.14.0",
|
|
60
|
+
"husky": "^9.1.7",
|
|
61
|
+
"prettier": "^3.4.2",
|
|
62
|
+
"react": "^19.0.0",
|
|
63
|
+
"react-dom": "^19.0.0",
|
|
64
|
+
"storybook": "^8.5.3",
|
|
65
|
+
"tailwindcss": "^4.0.0",
|
|
66
|
+
"typescript": "~5.6.2",
|
|
67
|
+
"typescript-eslint": "^8.22.0",
|
|
68
|
+
"vite": "^6.0.5",
|
|
69
|
+
"vite-plugin-dts": "^4.5.0",
|
|
70
|
+
"vite-plugin-externalize-deps": "^0.9.0"
|
|
71
|
+
},
|
|
72
|
+
"eslintConfig": {
|
|
73
|
+
"extends": [
|
|
74
|
+
"plugin:storybook/recommended"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@hello-pangea/dnd": "^17.0.0",
|
|
79
|
+
"formik": "^2.4.6",
|
|
80
|
+
"lodash.clonedeep": "^4.5.0",
|
|
81
|
+
"lodash.get": "^4.4.2",
|
|
82
|
+
"lodash.set": "^4.3.2",
|
|
83
|
+
"qr-scanner": "^1.4.2",
|
|
84
|
+
"react-icons": "^5.4.0",
|
|
85
|
+
"react-pdf": "^9.2.1",
|
|
86
|
+
"react-sketch-canvas": "^6.2.0",
|
|
87
|
+
"react-spreadsheet": "^0.9.5",
|
|
88
|
+
"xlsx": "^0.18.5"
|
|
89
|
+
},
|
|
90
|
+
"resolutions": {
|
|
91
|
+
"@types/react": "^19.0.0"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IconButton, Popover, RiIcon } from "@overmap-ai/blocks"
|
|
2
|
+
import { CSSColor } from "@overmap-ai/core"
|
|
3
|
+
import { memo, ReactElement, useCallback } from "react"
|
|
4
|
+
|
|
5
|
+
interface ColorPickerProps {
|
|
6
|
+
selectedColor: CSSColor | null
|
|
7
|
+
allColors: CSSColor[]
|
|
8
|
+
onFinish: (color: CSSColor) => void
|
|
9
|
+
trigger: ReactElement
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ColorPicker = memo((props: ColorPickerProps) => {
|
|
13
|
+
const { selectedColor, allColors, onFinish, trigger } = props
|
|
14
|
+
|
|
15
|
+
const handleSelectedColorChange = useCallback(
|
|
16
|
+
(color: CSSColor) => () => {
|
|
17
|
+
onFinish(color)
|
|
18
|
+
},
|
|
19
|
+
[onFinish],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Popover.Root>
|
|
24
|
+
<Popover.Trigger>{trigger}</Popover.Trigger>
|
|
25
|
+
<Popover.Content size="sm">
|
|
26
|
+
<div className="grid w-max grid-cols-7 gap-x-1 gap-y-1">
|
|
27
|
+
{allColors.map((color) => (
|
|
28
|
+
<IconButton
|
|
29
|
+
key={color}
|
|
30
|
+
onClick={handleSelectedColorChange(color)}
|
|
31
|
+
style={{
|
|
32
|
+
backgroundColor: color,
|
|
33
|
+
}}
|
|
34
|
+
type="button"
|
|
35
|
+
variant="solid"
|
|
36
|
+
aria-label={color}
|
|
37
|
+
>
|
|
38
|
+
{selectedColor === color && <RiIcon icon="RiCheckLine" />}
|
|
39
|
+
</IconButton>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</Popover.Content>
|
|
43
|
+
</Popover.Root>
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
ColorPicker.displayName = "ColorPicker"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ColorPicker"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Badge, BadgeProps } from "@overmap-ai/blocks"
|
|
2
|
+
import { truncate } from "@overmap-ai/core"
|
|
3
|
+
import { memo, useMemo } from "react"
|
|
4
|
+
|
|
5
|
+
import { FileIcon } from "../FileIcon"
|
|
6
|
+
|
|
7
|
+
export interface FileBadgeProps extends Omit<BadgeProps, "children"> {
|
|
8
|
+
file: File
|
|
9
|
+
truncateLength?: number
|
|
10
|
+
hideName?: boolean
|
|
11
|
+
}
|
|
12
|
+
export const FileBadge = memo((props: FileBadgeProps) => {
|
|
13
|
+
const { file, truncateLength, hideName = false, ...rest } = props
|
|
14
|
+
|
|
15
|
+
const fileName = useMemo(
|
|
16
|
+
() => (truncateLength !== undefined ? truncate(file.name, truncateLength) : file.name),
|
|
17
|
+
[file.name, truncateLength],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Badge {...rest}>
|
|
22
|
+
<FileIcon fileType={file.type} />
|
|
23
|
+
{!hideName && fileName}
|
|
24
|
+
</Badge>
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
FileBadge.displayName = "FileBadge"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./FileBadge"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta } from "@storybook/react"
|
|
2
|
+
import { ChangeEvent, useCallback, useRef, useState } from "react"
|
|
3
|
+
|
|
4
|
+
import { FileCard } from "./FileCard"
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/FileCard",
|
|
8
|
+
component: FileCard,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
disablePanel: true,
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof FileCard>
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
export const Basic = () => {
|
|
18
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
19
|
+
const [files, setFiles] = useState<File[]>([])
|
|
20
|
+
|
|
21
|
+
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
if (e.target.files) {
|
|
23
|
+
const files = Array.from(e.target.files)
|
|
24
|
+
if (files.length > 0) {
|
|
25
|
+
setFiles(files)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<input ref={inputRef} multiple type="file" onChange={handleChange} />
|
|
33
|
+
<div className="flex flex-col gap-1">
|
|
34
|
+
{files.map((file, index) => (
|
|
35
|
+
<FileCard key={index} file={file} />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const Loading = () => {
|
|
43
|
+
return <FileCard file={null} />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Errored = () => {
|
|
47
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
48
|
+
const [files, setFiles] = useState<File[]>([])
|
|
49
|
+
|
|
50
|
+
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
51
|
+
if (e.target.files) {
|
|
52
|
+
const files = Array.from(e.target.files)
|
|
53
|
+
if (files.length > 0) {
|
|
54
|
+
setFiles(files)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<input ref={inputRef} multiple type="file" onChange={handleChange} />
|
|
62
|
+
<div className="flex flex-col gap-1">
|
|
63
|
+
{files.map((file, index) => (
|
|
64
|
+
<FileCard key={index} file={file} error={file.name} />
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { RiIcon, Spinner } from "@overmap-ai/blocks"
|
|
2
|
+
import { truncate } from "@overmap-ai/core"
|
|
3
|
+
import { cx } from "class-variance-authority"
|
|
4
|
+
import { ComponentProps, forwardRef, memo, PropsWithoutRef, ReactNode, useMemo } from "react"
|
|
5
|
+
|
|
6
|
+
import { FileIcon } from "../FileIcon"
|
|
7
|
+
|
|
8
|
+
export interface FileCardProps extends Omit<PropsWithoutRef<ComponentProps<"div">>, "children"> {
|
|
9
|
+
file: File | null
|
|
10
|
+
truncateLength?: number
|
|
11
|
+
error?: string
|
|
12
|
+
rightSlot?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FileCard = memo(
|
|
16
|
+
forwardRef<HTMLDivElement, FileCardProps>((props, ref) => {
|
|
17
|
+
const { file, className, error, truncateLength, rightSlot, ...rest } = props
|
|
18
|
+
|
|
19
|
+
const fileName = useMemo(() => {
|
|
20
|
+
if (!file) return
|
|
21
|
+
return truncateLength !== undefined ? truncate(file.name, truncateLength) : file.name
|
|
22
|
+
}, [file, truncateLength])
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={cx(
|
|
27
|
+
className,
|
|
28
|
+
"flex h-5 w-full items-center gap-2 rounded-md border border-(--base-a6) bg-(--base-2) text-sm py-1 px-2 h-max",
|
|
29
|
+
{
|
|
30
|
+
"text-(--gray-11)": error,
|
|
31
|
+
},
|
|
32
|
+
)}
|
|
33
|
+
ref={ref}
|
|
34
|
+
{...rest}
|
|
35
|
+
>
|
|
36
|
+
{error ? <RiIcon icon="RiFileWarningLine" /> : <FileIcon fileType={file?.type ?? ""} />}
|
|
37
|
+
{!error ? (
|
|
38
|
+
file ? (
|
|
39
|
+
fileName
|
|
40
|
+
) : (
|
|
41
|
+
<div className="flex w-full justify-center">
|
|
42
|
+
<Spinner />
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
) : (
|
|
46
|
+
error
|
|
47
|
+
)}
|
|
48
|
+
{rightSlot}
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
FileCard.displayName = "FileCard"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./FileCard"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RiIcon } from "@overmap-ai/blocks"
|
|
2
|
+
import { memo, useMemo } from "react"
|
|
3
|
+
|
|
4
|
+
import { SUPPORTED_IMAGE_FILE_TYPES } from "../ImageViewer"
|
|
5
|
+
import { SUPPORTED_PDF_FILE_TYPES } from "../PDFViewer"
|
|
6
|
+
import { SUPPORTED_SPREADSHEET_FILE_EXTENSIONS } from "../SpreadsheetViewer"
|
|
7
|
+
|
|
8
|
+
// TODO: create a mapping for each supported file extensions to a appropriate icon.
|
|
9
|
+
// May need to outsource some icons for this.
|
|
10
|
+
|
|
11
|
+
export interface FileIconProps {
|
|
12
|
+
fileType: string
|
|
13
|
+
}
|
|
14
|
+
export const FileIcon = memo((props: FileIconProps) => {
|
|
15
|
+
const { fileType } = props
|
|
16
|
+
const icon = useMemo(() => {
|
|
17
|
+
if (SUPPORTED_SPREADSHEET_FILE_EXTENSIONS.includes(fileType)) {
|
|
18
|
+
return <RiIcon icon="RiFileExcelLine" />
|
|
19
|
+
}
|
|
20
|
+
if (SUPPORTED_PDF_FILE_TYPES.includes(fileType)) {
|
|
21
|
+
return <RiIcon icon="RiFilePdfLine" />
|
|
22
|
+
}
|
|
23
|
+
if (SUPPORTED_IMAGE_FILE_TYPES.includes(fileType)) {
|
|
24
|
+
return <RiIcon icon="RiFileImageLine" />
|
|
25
|
+
}
|
|
26
|
+
return <RiIcon icon="RiFileLine" />
|
|
27
|
+
}, [fileType])
|
|
28
|
+
|
|
29
|
+
return <div className="h-max w-max">{icon}</div>
|
|
30
|
+
})
|
|
31
|
+
FileIcon.displayName = "FileIcon"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./FileIcon"
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { useFileViewer } from "./context"
|
|
6
|
+
import { FileViewerProvider } from "./FileViewerProvider"
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Contexts/FileViewer",
|
|
10
|
+
component: FileViewerProvider,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
parameters: {
|
|
13
|
+
disablePanel: true,
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof FileViewerProvider>
|
|
16
|
+
|
|
17
|
+
export default meta
|
|
18
|
+
|
|
19
|
+
export const Basic = () => {
|
|
20
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
21
|
+
const [file, setFile] = useState<File | null>(null)
|
|
22
|
+
const openFileViewer = useFileViewer()
|
|
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
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<input ref={inputRef} type="file" onChange={handleChange} />
|
|
36
|
+
{file && (
|
|
37
|
+
<Button
|
|
38
|
+
onClick={() => {
|
|
39
|
+
openFileViewer((close) => ({
|
|
40
|
+
file: file,
|
|
41
|
+
onDelete: close,
|
|
42
|
+
}))
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
Open
|
|
46
|
+
</Button>
|
|
47
|
+
)}
|
|
48
|
+
</>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { memo, PropsWithChildren, useCallback, useEffect, useMemo, useState } from "react"
|
|
2
|
+
|
|
3
|
+
import { ImageViewer, SUPPORTED_IMAGE_FILE_TYPES } from "../ImageViewer"
|
|
4
|
+
import { PDFViewer, SUPPORTED_PDF_FILE_TYPES } from "../PDFViewer"
|
|
5
|
+
import { SpreadsheetViewer, SUPPORTED_SPREADSHEET_FILE_EXTENSIONS } from "../SpreadsheetViewer"
|
|
6
|
+
import { FileViewerContext } from "./context"
|
|
7
|
+
import { FileViewerConfig } from "./typings"
|
|
8
|
+
|
|
9
|
+
type FileViewerType = "image" | "spreadsheet" | "pdf"
|
|
10
|
+
export const FileViewerProvider = memo((props: PropsWithChildren) => {
|
|
11
|
+
const { children } = props
|
|
12
|
+
const [config, setConfig] = useState<FileViewerConfig | null>(null)
|
|
13
|
+
const [fileType, setFileType] = useState<FileViewerType | null>(null)
|
|
14
|
+
|
|
15
|
+
const closeFileViewer = useCallback(() => {
|
|
16
|
+
setConfig(null)
|
|
17
|
+
setFileType(null)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
const openFileViewer = useCallback(
|
|
21
|
+
(func: (closeFileViewer: () => void) => FileViewerConfig) => {
|
|
22
|
+
setConfig(func(closeFileViewer))
|
|
23
|
+
},
|
|
24
|
+
[closeFileViewer],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const handleClose = useCallback(() => {
|
|
28
|
+
if (!config) return
|
|
29
|
+
if (config.onClose) config.onClose()
|
|
30
|
+
closeFileViewer()
|
|
31
|
+
}, [closeFileViewer, config])
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!config) return
|
|
35
|
+
const { file } = config
|
|
36
|
+
|
|
37
|
+
if (SUPPORTED_SPREADSHEET_FILE_EXTENSIONS.includes(file.type)) {
|
|
38
|
+
setFileType("spreadsheet")
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (SUPPORTED_PDF_FILE_TYPES.includes(file.type)) {
|
|
42
|
+
setFileType("pdf")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
if (SUPPORTED_IMAGE_FILE_TYPES.includes(file.type)) {
|
|
46
|
+
setFileType("image")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
}, [config])
|
|
50
|
+
|
|
51
|
+
const value = useMemo(() => openFileViewer, [openFileViewer])
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<FileViewerContext.Provider value={value}>
|
|
55
|
+
{children}
|
|
56
|
+
{config && (
|
|
57
|
+
<>
|
|
58
|
+
{fileType === "spreadsheet" && (
|
|
59
|
+
<SpreadsheetViewer file={config.file} onDelete={config.onDelete} onClose={handleClose} />
|
|
60
|
+
)}
|
|
61
|
+
{fileType === "pdf" && (
|
|
62
|
+
<PDFViewer file={config.file} onDelete={config.onDelete} onClose={handleClose} />
|
|
63
|
+
)}
|
|
64
|
+
{fileType === "image" && (
|
|
65
|
+
<ImageViewer file={config.file} onDelete={config.onDelete} onClose={handleClose} />
|
|
66
|
+
)}
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
</FileViewerContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
FileViewerProvider.displayName = "FileViewerProvider"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
|
|
3
|
+
import { FileViewerConfig } from "./typings"
|
|
4
|
+
|
|
5
|
+
export type FileViewerContextType = (func: (close: () => void) => FileViewerConfig) => void
|
|
6
|
+
|
|
7
|
+
export const FileViewerContext = createContext<FileViewerContextType>(() => {
|
|
8
|
+
throw new Error("No FileViewerProvider found")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const useFileViewer = () => useContext(FileViewerContext)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Meta } from "@storybook/react"
|
|
2
|
+
import { ChangeEvent, useCallback, useRef, useState } from "react"
|
|
3
|
+
|
|
4
|
+
import { ImageCard } from "./ImageCard"
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/ImageCard",
|
|
8
|
+
component: ImageCard,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
disablePanel: true,
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof ImageCard>
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
|
|
17
|
+
export const Basic = () => {
|
|
18
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
19
|
+
const [files, setFiles] = useState<File[]>([])
|
|
20
|
+
|
|
21
|
+
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
22
|
+
if (e.target.files) {
|
|
23
|
+
const files = Array.from(e.target.files)
|
|
24
|
+
if (files.length > 0) {
|
|
25
|
+
setFiles(files)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<input ref={inputRef} multiple type="file" onChange={handleChange} />
|
|
33
|
+
<div className="flex flex-col gap-1">
|
|
34
|
+
{files.map((file, index) => (
|
|
35
|
+
<ImageCard key={index} file={file} />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const Loading = () => {
|
|
43
|
+
return <ImageCard file={null} />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Errored = () => {
|
|
47
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
48
|
+
const [files, setFiles] = useState<File[]>([])
|
|
49
|
+
|
|
50
|
+
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
51
|
+
if (e.target.files) {
|
|
52
|
+
const files = Array.from(e.target.files)
|
|
53
|
+
if (files.length > 0) {
|
|
54
|
+
setFiles(files)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<input ref={inputRef} multiple type="file" onChange={handleChange} />
|
|
62
|
+
<div className="flex flex-col gap-1">
|
|
63
|
+
{files.map((file, index) => (
|
|
64
|
+
<ImageCard key={index} file={file} error={file.name} />
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const Responsive = () => {
|
|
72
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
73
|
+
const [files, setFiles] = useState<File[]>([])
|
|
74
|
+
|
|
75
|
+
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
76
|
+
if (e.target.files) {
|
|
77
|
+
const files = Array.from(e.target.files)
|
|
78
|
+
if (files.length > 0) {
|
|
79
|
+
setFiles(files)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<input ref={inputRef} multiple type="file" onChange={handleChange} />
|
|
87
|
+
<div className="flex flex-col gap-1">
|
|
88
|
+
{files.map((file, index) => (
|
|
89
|
+
<ImageCard key={index} file={file} />
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</>
|
|
93
|
+
)
|
|
94
|
+
}
|