diff --git a/apps/docs/.storybook/preview.ts b/apps/docs/.storybook/preview.ts index 2496ab0..5250d6a 100644 --- a/apps/docs/.storybook/preview.ts +++ b/apps/docs/.storybook/preview.ts @@ -1,23 +1,23 @@ -import type { Preview } from "@storybook/react"; +import type { Preview } from '@storybook/react'; -import "../src/index.css"; -import "@sopt-makers/ui/dist/index.css"; +import '../src/index.css'; +import '@sopt-makers/ui/dist/index.css'; const preview: Preview = { parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, - layout: "centered", + layout: 'centered', backgrounds: { - default: "dark", // 기본 배경을 'dark'로 설정 + default: 'dark', // 기본 배경을 'dark'로 설정 values: [ - { name: "dark", value: "#0F1012" }, // 'dark' 배경의 색상을 검정색으로 지정 - { name: "white", value: "#ffffff" }, + { name: 'dark', value: '#0F1012' }, // 'dark' 배경의 색상을 검정색으로 지정 + { name: 'white', value: '#ffffff' }, ], }, }, diff --git a/apps/docs/src/stories/Dropzone.stories.tsx b/apps/docs/src/stories/Dropzone.stories.tsx new file mode 100644 index 0000000..f8ce006 --- /dev/null +++ b/apps/docs/src/stories/Dropzone.stories.tsx @@ -0,0 +1,36 @@ +import { Button, Dropzone, DropzoneProps, FieldBox } from '@sopt-makers/ui'; +import { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/Dropzone', + component: Dropzone, + tags: ['autodocs'], + args: { + width: '300px', + }, +}; + +export default meta; + +export const Default: StoryObj = { + render: (args) => , +}; + +export const WithFieldbox: StoryObj = { + render: (args) => ( + } + rightAddon={ + + } + /> + } + > + + + ), +}; diff --git a/packages/ui/DropZone/DropZone.tsx b/packages/ui/DropZone/DropZone.tsx new file mode 100644 index 0000000..5659540 --- /dev/null +++ b/packages/ui/DropZone/DropZone.tsx @@ -0,0 +1,75 @@ +import { forwardRef, useState } from 'react'; +import { IconImagePlus } from '@sopt-makers/icons'; +import type { DragEvent, DragEventHandler, HTMLAttributes, ReactNode } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { dropzoneVariants } from './style.css'; + +export interface DropzoneProps extends Omit, 'onDrop'> { + width?: string; + height?: string; + icon?: ReactNode; + disabled?: boolean; + accept?: string; + onDrop?: (event: DragEvent, files: File[]) => void; +} + +export const Dropzone = forwardRef((props, forwardedRef) => { + const { + width = '100%', + height = '170px', + icon = , + disabled = false, + accept, + onDragOver, + onDragLeave, + onDrop, + } = props; + + const [isDragOver, setIsDragOver] = useState(false); + const { getRootProps, getInputProps } = useDropzone(); + + const handleDragOver: DragEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (!disabled) { + setIsDragOver(true); + e.dataTransfer.dropEffect = 'copy'; + } else { + e.dataTransfer.dropEffect = 'none'; + } + + onDragOver?.(e); + }; + + const handleDragLeave: DragEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + onDragLeave?.(e); + }; + + const handleDrop: DragEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + onDrop?.(e, Array.from(e.dataTransfer.files)); + }; + + return ( +
+ {icon} + +
+ ); +}); + +Dropzone.displayName = 'Dropzone'; diff --git a/packages/ui/DropZone/index.ts b/packages/ui/DropZone/index.ts new file mode 100644 index 0000000..d832ddf --- /dev/null +++ b/packages/ui/DropZone/index.ts @@ -0,0 +1 @@ +export * from './DropZone'; diff --git a/packages/ui/DropZone/style.css.ts b/packages/ui/DropZone/style.css.ts new file mode 100644 index 0000000..55d6a87 --- /dev/null +++ b/packages/ui/DropZone/style.css.ts @@ -0,0 +1,26 @@ +import { style, styleVariants } from '@vanilla-extract/css'; +import theme from '../theme.css'; + +export const dropzoneBase = style({ + position: 'relative', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.gray700, + borderRadius: '10px', + cursor: 'pointer', + transition: 'background-color 0.3s ease', + border: '2px dashed transparent', +}); + +export const dropzoneVariants = styleVariants({ + default: [dropzoneBase], + dragOver: [ + dropzoneBase, + { + backgroundColor: theme.colors.gray600, + borderColor: theme.colors.white, + opacity: 0.8, + }, + ], +}); diff --git a/packages/ui/index.ts b/packages/ui/index.ts index 8046228..bde4064 100644 --- a/packages/ui/index.ts +++ b/packages/ui/index.ts @@ -14,5 +14,6 @@ export { default as Tab } from './Tab'; export * from './Skeleton'; export * from './FieldBox'; export * from './Tag'; +export * from './Dropzone'; // test component export { default as Test } from './Test'; diff --git a/packages/ui/package.json b/packages/ui/package.json index 71d6fd2..9a90765 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -28,7 +28,8 @@ "@sopt-makers/fonts": "workspace:^", "@sopt-makers/icons": "workspace:^", "@vanilla-extract/css": "^1.14.0", - "@vanilla-extract/sprinkles": "^1.6.1" + "@vanilla-extract/sprinkles": "^1.6.1", + "react-dropzone": "^14.3.5" }, "devDependencies": { "@testing-library/jest-dom": "^6.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54b3763..95bef89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ importers: version: link:packages/tsconfig turbo: specifier: latest - version: 2.2.3 + version: 2.3.3 apps/docs: dependencies: @@ -190,6 +190,9 @@ importers: '@vanilla-extract/sprinkles': specifier: ^1.6.1 version: 1.6.1(@vanilla-extract/css@1.16.0) + react-dropzone: + specifier: ^14.3.5 + version: 14.3.5(react@18.3.1) devDependencies: '@testing-library/jest-dom': specifier: ^6.5.0 @@ -3350,6 +3353,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -4447,6 +4454,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + file-system-cache@2.3.0: resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} @@ -6009,6 +6020,12 @@ packages: peerDependencies: react: ^18.3.1 + react-dropzone@14.3.5: + resolution: {integrity: sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-element-to-jsx-string@15.0.0: resolution: {integrity: sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==} peerDependencies: @@ -6788,38 +6805,38 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - turbo-darwin-64@2.2.3: - resolution: {integrity: sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg==} + turbo-darwin-64@2.3.3: + resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.2.3: - resolution: {integrity: sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA==} + turbo-darwin-arm64@2.3.3: + resolution: {integrity: sha512-DYbQwa3NsAuWkCUYVzfOUBbSUBVQzH5HWUFy2Kgi3fGjIWVZOFk86ss+xsWu//rlEAfYwEmopigsPYSmW4X15A==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.2.3: - resolution: {integrity: sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ==} + turbo-linux-64@2.3.3: + resolution: {integrity: sha512-eHj9OIB0dFaP6BxB88jSuaCLsOQSYWBgmhy2ErCu6D2GG6xW3b6e2UWHl/1Ho9FsTg4uVgo4DB9wGsKa5erjUA==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.2.3: - resolution: {integrity: sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ==} + turbo-linux-arm64@2.3.3: + resolution: {integrity: sha512-NmDE/NjZoDj1UWBhMtOPmqFLEBKhzGS61KObfrDEbXvU3lekwHeoPvAMfcovzswzch+kN2DrtbNIlz+/rp8OCg==} cpu: [arm64] os: [linux] - turbo-windows-64@2.2.3: - resolution: {integrity: sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==} + turbo-windows-64@2.3.3: + resolution: {integrity: sha512-O2+BS4QqjK3dOERscXqv7N2GXNcqHr9hXumkMxDj/oGx9oCatIwnnwx34UmzodloSnJpgSqjl8iRWiY65SmYoQ==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.2.3: - resolution: {integrity: sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw==} + turbo-windows-arm64@2.3.3: + resolution: {integrity: sha512-dW4ZK1r6XLPNYLIKjC4o87HxYidtRRcBeo/hZ9Wng2XM/MqqYkAyzJXJGgRMsc0MMEN9z4+ZIfnSNBrA0b08ag==} cpu: [arm64] os: [win32] - turbo@2.2.3: - resolution: {integrity: sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ==} + turbo@2.3.3: + resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==} hasBin: true tween-functions@1.2.0: @@ -11104,6 +11121,8 @@ snapshots: asynckit@0.4.0: {} + attr-accept@2.2.5: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 @@ -11760,7 +11779,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.0 dotenv-expand@10.0.0: {} @@ -12527,6 +12546,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-selector@2.1.2: + dependencies: + tslib: 2.8.0 + file-system-cache@2.3.0: dependencies: fs-extra: 11.1.1 @@ -13476,7 +13499,7 @@ snapshots: lower-case@2.0.2: dependencies: - tslib: 2.7.0 + tslib: 2.8.0 lru-cache@10.4.3: {} @@ -13652,7 +13675,7 @@ snapshots: no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.7.0 + tslib: 2.8.0 node-dir@0.1.17: dependencies: @@ -14177,6 +14200,13 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dropzone@14.3.5(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 18.3.1 + react-element-to-jsx-string@15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@base2/pretty-print-object': 1.0.1 @@ -15104,32 +15134,32 @@ snapshots: tslib: 1.14.1 typescript: 5.6.3 - turbo-darwin-64@2.2.3: + turbo-darwin-64@2.3.3: optional: true - turbo-darwin-arm64@2.2.3: + turbo-darwin-arm64@2.3.3: optional: true - turbo-linux-64@2.2.3: + turbo-linux-64@2.3.3: optional: true - turbo-linux-arm64@2.2.3: + turbo-linux-arm64@2.3.3: optional: true - turbo-windows-64@2.2.3: + turbo-windows-64@2.3.3: optional: true - turbo-windows-arm64@2.2.3: + turbo-windows-arm64@2.3.3: optional: true - turbo@2.2.3: + turbo@2.3.3: optionalDependencies: - turbo-darwin-64: 2.2.3 - turbo-darwin-arm64: 2.2.3 - turbo-linux-64: 2.2.3 - turbo-linux-arm64: 2.2.3 - turbo-windows-64: 2.2.3 - turbo-windows-arm64: 2.2.3 + turbo-darwin-64: 2.3.3 + turbo-darwin-arm64: 2.3.3 + turbo-linux-64: 2.3.3 + turbo-linux-arm64: 2.3.3 + turbo-windows-64: 2.3.3 + turbo-windows-arm64: 2.3.3 tween-functions@1.2.0: {}