@bbki.ng/ui 0.1.1

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 (97) hide show
  1. package/.storybook/main.ts +21 -0
  2. package/.storybook/preview-head.html +10 -0
  3. package/.storybook/preview.tsx +30 -0
  4. package/CHANGELOG.md +8 -0
  5. package/README.md +124 -0
  6. package/package.json +57 -0
  7. package/scripts/build-tokens.ts +170 -0
  8. package/src/atoms/blink-dot/BlinkDot.stories.tsx +44 -0
  9. package/src/atoms/blink-dot/BlinkDot.tsx +45 -0
  10. package/src/atoms/blink-dot/index.ts +2 -0
  11. package/src/atoms/button/Button.stories.tsx +84 -0
  12. package/src/atoms/button/Button.tsx +59 -0
  13. package/src/atoms/button/Button.types.ts +20 -0
  14. package/src/atoms/button/Button.variants.ts +58 -0
  15. package/src/atoms/button/index.ts +3 -0
  16. package/src/atoms/link/Link.stories.tsx +121 -0
  17. package/src/atoms/link/Link.tsx +69 -0
  18. package/src/atoms/link/Link.types.ts +26 -0
  19. package/src/atoms/link/Link.variants.ts +55 -0
  20. package/src/atoms/link/index.ts +3 -0
  21. package/src/atoms/logo/Logo.stories.tsx +37 -0
  22. package/src/atoms/logo/Logo.tsx +49 -0
  23. package/src/atoms/logo/Logo.types.ts +4 -0
  24. package/src/atoms/logo/index.ts +2 -0
  25. package/src/index.ts +54 -0
  26. package/src/layout/container/Container.stories.tsx +73 -0
  27. package/src/layout/container/Container.tsx +57 -0
  28. package/src/layout/container/index.ts +2 -0
  29. package/src/layout/grid/Grid.stories.tsx +106 -0
  30. package/src/layout/grid/Grid.tsx +86 -0
  31. package/src/layout/grid/index.ts +2 -0
  32. package/src/layout/index.ts +4 -0
  33. package/src/molecules/article/Article.stories.tsx +45 -0
  34. package/src/molecules/article/Article.tsx +25 -0
  35. package/src/molecules/article/Article.types.ts +11 -0
  36. package/src/molecules/article/index.ts +2 -0
  37. package/src/molecules/breadcrumb/Breadcrumb.stories.tsx +60 -0
  38. package/src/molecules/breadcrumb/Breadcrumb.tsx +43 -0
  39. package/src/molecules/breadcrumb/Breadcrumb.types.ts +19 -0
  40. package/src/molecules/breadcrumb/index.ts +2 -0
  41. package/src/molecules/list/List.stories.tsx +84 -0
  42. package/src/molecules/list/List.tsx +79 -0
  43. package/src/molecules/list/List.types.ts +23 -0
  44. package/src/molecules/list/index.ts +2 -0
  45. package/src/molecules/nav/Nav.stories.tsx +45 -0
  46. package/src/molecules/nav/Nav.tsx +29 -0
  47. package/src/molecules/nav/Nav.types.ts +10 -0
  48. package/src/molecules/nav/index.ts +2 -0
  49. package/src/molecules/panel/Panel.stories.tsx +42 -0
  50. package/src/molecules/panel/Panel.tsx +27 -0
  51. package/src/molecules/panel/Panel.types.ts +6 -0
  52. package/src/molecules/panel/index.ts +2 -0
  53. package/src/molecules/table/Table.stories.tsx +54 -0
  54. package/src/molecules/table/Table.tsx +31 -0
  55. package/src/molecules/table/Table.types.ts +20 -0
  56. package/src/molecules/table/index.ts +2 -0
  57. package/src/organisms/canvas/Canvas.tsx +79 -0
  58. package/src/organisms/canvas/Canvas.types.ts +25 -0
  59. package/src/organisms/canvas/index.ts +3 -0
  60. package/src/organisms/canvas/useRenderer.ts +44 -0
  61. package/src/organisms/drop-image/DropImage.stories.tsx +36 -0
  62. package/src/organisms/drop-image/DropImage.tsx +193 -0
  63. package/src/organisms/drop-image/DropImage.types.ts +26 -0
  64. package/src/organisms/drop-image/index.ts +3 -0
  65. package/src/organisms/drop-image/useDropImage.ts +124 -0
  66. package/src/organisms/drop-image/utils.ts +1 -0
  67. package/src/organisms/drop-zone/DropZone.tsx +58 -0
  68. package/src/organisms/drop-zone/DropZone.types.ts +9 -0
  69. package/src/organisms/drop-zone/index.ts +2 -0
  70. package/src/organisms/loading-spiral/LoadingSpiral.stories.tsx +30 -0
  71. package/src/organisms/loading-spiral/LoadingSpiral.tsx +44 -0
  72. package/src/organisms/loading-spiral/LoadingSpiral.types.ts +4 -0
  73. package/src/organisms/loading-spiral/constants.ts +62 -0
  74. package/src/organisms/loading-spiral/createOptions.ts +31 -0
  75. package/src/organisms/loading-spiral/createSettings.ts +26 -0
  76. package/src/organisms/loading-spiral/index.ts +2 -0
  77. package/src/organisms/loading-spiral/useCanvasRef.ts +23 -0
  78. package/src/organisms/loading-spiral/utils.ts +5 -0
  79. package/src/organisms/page/Page.stories.tsx +65 -0
  80. package/src/organisms/page/Page.tsx +71 -0
  81. package/src/organisms/page/Page.types.ts +23 -0
  82. package/src/organisms/page/index.ts +8 -0
  83. package/src/styles.css +151 -0
  84. package/src/theme/ThemeContext.tsx +20 -0
  85. package/src/theme/ThemeProvider.tsx +93 -0
  86. package/src/theme/index.ts +3 -0
  87. package/src/tokens/css/dark.css +111 -0
  88. package/src/tokens/css/light.css +111 -0
  89. package/src/tokens/index.ts +127 -0
  90. package/tokens/base/colors.json +54 -0
  91. package/tokens/base/shadows.json +34 -0
  92. package/tokens/base/spacing.json +21 -0
  93. package/tokens/base/typography.json +35 -0
  94. package/tokens/semantic/dark.json +50 -0
  95. package/tokens/semantic/light.json +54 -0
  96. package/tsconfig.json +22 -0
  97. package/vite.config.ts +44 -0
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ import { PathObj } from '../breadcrumb';
3
+
4
+ export interface NavProps {
5
+ paths: PathObj[];
6
+ loading?: boolean;
7
+ mini?: boolean;
8
+ className?: string;
9
+ customLogo?: ReactNode;
10
+ }
@@ -0,0 +1,2 @@
1
+ export { Nav } from './Nav';
2
+ export type { NavProps } from './Nav.types';
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Panel } from './Panel';
3
+
4
+ const meta: Meta<typeof Panel> = {
5
+ title: 'Molecules/Panel',
6
+ component: Panel,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default: Story = {
17
+ args: {
18
+ children: (
19
+ <div className="p-4">
20
+ <h3 className="text-lg font-medium text-content-primary">面板标题</h3>
21
+ <p className="text-content-secondary mt-2">这是面板内容</p>
22
+ </div>
23
+ ),
24
+ },
25
+ };
26
+
27
+ export const WithContent: Story = {
28
+ args: {
29
+ children: (
30
+ <div className="space-y-4">
31
+ <div className="flex items-center justify-between">
32
+ <span className="text-content-primary font-medium">项目</span>
33
+ <span className="text-content-secondary">12</span>
34
+ </div>
35
+ <div className="flex items-center justify-between">
36
+ <span className="text-content-primary font-medium">任务</span>
37
+ <span className="text-content-secondary">48</span>
38
+ </div>
39
+ </div>
40
+ ),
41
+ },
42
+ };
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { PanelProps } from './Panel.types';
4
+
5
+ export const Panel = (props: PanelProps) => {
6
+ const { className = '', children } = props;
7
+ const [show, setShow] = useState(false);
8
+
9
+ useEffect(() => {
10
+ setShow(true);
11
+ }, []);
12
+
13
+ return (
14
+ <div
15
+ className={twMerge(
16
+ 'transition-all ease-in-out duration-500',
17
+ 'p-4 rounded-sm',
18
+ show ? 'shadow-[var(--shadow-panel)]' : 'shadow-none',
19
+ className
20
+ )}
21
+ >
22
+ {children}
23
+ </div>
24
+ );
25
+ };
26
+
27
+ Panel.displayName = 'Panel';
@@ -0,0 +1,6 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export interface PanelProps {
4
+ children: ReactNode;
5
+ className?: string;
6
+ }
@@ -0,0 +1,2 @@
1
+ export { Panel } from './Panel';
2
+ export type { PanelProps } from './Panel.types';
@@ -0,0 +1,54 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Table } from './Table';
3
+
4
+ const meta: Meta<typeof Table> = {
5
+ title: 'Molecules/Table',
6
+ component: Table,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ const sampleData = [
17
+ { name: 'Item 1', value: 100, status: 'Active' },
18
+ { name: 'Item 2', value: 200, status: 'Inactive' },
19
+ { name: 'Item 3', value: 300, status: 'Active' },
20
+ ];
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ rowCount: sampleData.length,
25
+ headerRenderer: () => (
26
+ <>
27
+ <Table.HCell className="text-left p-2 text-content-primary">Name</Table.HCell>
28
+ <Table.HCell className="text-left p-2 text-content-primary">Value</Table.HCell>
29
+ <Table.HCell className="text-left p-2 text-content-primary">Status</Table.HCell>
30
+ </>
31
+ ),
32
+ rowRenderer: (index: number) => (
33
+ <>
34
+ <Table.Cell className="p-2 text-content-primary">{sampleData[index].name}</Table.Cell>
35
+ <Table.Cell className="p-2 text-content-secondary">{sampleData[index].value}</Table.Cell>
36
+ <Table.Cell className="p-2 text-content-secondary">{sampleData[index].status}</Table.Cell>
37
+ </>
38
+ ),
39
+ className: 'w-full border-collapse',
40
+ },
41
+ };
42
+
43
+ export const WithoutHeader: Story = {
44
+ args: {
45
+ rowCount: sampleData.length,
46
+ rowRenderer: (index: number) => (
47
+ <>
48
+ <Table.Cell className="p-2 text-content-primary">{sampleData[index].name}</Table.Cell>
49
+ <Table.Cell className="p-2 text-content-secondary">{sampleData[index].value}</Table.Cell>
50
+ </>
51
+ ),
52
+ className: 'w-full',
53
+ },
54
+ };
@@ -0,0 +1,31 @@
1
+ import { TableProps, TableHCellProps, TableCellProps } from './Table.types';
2
+
3
+ export const Table = (props: TableProps) => {
4
+ const { rowCount, rowRenderer, headerRenderer, className } = props;
5
+ const rows = [];
6
+
7
+ for (let i = 0; i < rowCount; i++) {
8
+ rows.push(<tr key={i}>{rowRenderer(i)}</tr>);
9
+ }
10
+
11
+ return (
12
+ <table className={className}>
13
+ {headerRenderer && (
14
+ <thead>
15
+ <tr>{headerRenderer()}</tr>
16
+ </thead>
17
+ )}
18
+ <tbody>{rows}</tbody>
19
+ </table>
20
+ );
21
+ };
22
+
23
+ Table.displayName = 'Table';
24
+
25
+ const HCell = (props: TableHCellProps) => <th {...props}>{props.children}</th>;
26
+ HCell.displayName = 'Table.HCell';
27
+ Table.HCell = HCell;
28
+
29
+ const Cell = (props: TableCellProps) => <td {...props}>{props.children}</td>;
30
+ Cell.displayName = 'Table.Cell';
31
+ Table.Cell = Cell;
@@ -0,0 +1,20 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+
3
+ export interface TableProps {
4
+ rowCount: number;
5
+ rowRenderer: (index: number) => ReactNode;
6
+ headerRenderer?: () => ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ export interface TableHCellProps {
11
+ children: ReactNode;
12
+ style?: CSSProperties;
13
+ className?: string;
14
+ }
15
+
16
+ export interface TableCellProps {
17
+ children: ReactNode;
18
+ style?: CSSProperties;
19
+ className?: string;
20
+ }
@@ -0,0 +1,2 @@
1
+ export { Table } from './Table';
2
+ export type { TableProps, TableHCellProps, TableCellProps } from './Table.types';
@@ -0,0 +1,79 @@
1
+ import { useEffect } from 'react';
2
+ import { useRenderer } from './useRenderer';
3
+ import { CanvasProps, UniformProps } from './Canvas.types';
4
+
5
+ export const Canvas = (props: CanvasProps) => {
6
+ const { fragment, vertex, uniforms = {}, name, attributes = [], onRender, ...rest } = props;
7
+
8
+ const { canvasRef, renderer } = useRenderer(uniforms);
9
+ const instName = name ?? 'default';
10
+
11
+ useEffect(() => {
12
+ if (renderer == null || canvasRef.current == null) {
13
+ return;
14
+ }
15
+
16
+ const aCopy = attributes.map(attr => ({
17
+ ...attr,
18
+ data: (i: number, total: number) => attr.data?.(i, total),
19
+ }));
20
+
21
+ const uCopy = Object.keys(uniforms).reduce((acc: Record<string, UniformProps>, key) => {
22
+ acc[key] = {
23
+ ...uniforms[key],
24
+ value: [...uniforms[key].value],
25
+ };
26
+ return acc;
27
+ }, {});
28
+
29
+ const vertices: Array<{ x: number; y: number; z: number }> = [
30
+ { x: -100, y: 100, z: 0 },
31
+ { x: -100, y: -100, z: 0 },
32
+ { x: 100, y: 100, z: 0 },
33
+ { x: 100, y: -100, z: 0 },
34
+ { x: -100, y: -100, z: 0 },
35
+ { x: 100, y: 100, z: 0 },
36
+ ];
37
+
38
+ (renderer.add as (name: string, props: unknown) => void)(instName, {
39
+ uniforms: {
40
+ ...uCopy,
41
+ uProgress: {
42
+ type: 'float',
43
+ value: [0.0],
44
+ },
45
+ },
46
+ attributes: aCopy,
47
+ fragment,
48
+ vertex,
49
+ mode: 4,
50
+ geometry: { vertices: vertices as unknown as object[][] },
51
+ onRender: (inst: { uniforms: { uProgress?: { value: number[] } } }) => {
52
+ if (inst.uniforms.uProgress != undefined) {
53
+ inst.uniforms.uProgress.value[0] += 0.09;
54
+ }
55
+
56
+ if (onRender) {
57
+ onRender(inst);
58
+ }
59
+ },
60
+ });
61
+
62
+ return () => {
63
+ renderer.remove(instName);
64
+ };
65
+ }, [renderer]);
66
+
67
+ return (
68
+ <canvas
69
+ style={{
70
+ ...props.style,
71
+ imageRendering: 'pixelated',
72
+ }}
73
+ ref={canvasRef}
74
+ {...rest}
75
+ />
76
+ );
77
+ };
78
+
79
+ Canvas.displayName = 'Canvas';
@@ -0,0 +1,25 @@
1
+ import { HTMLAttributes } from 'react';
2
+
3
+ export interface AttributeProps {
4
+ name: string;
5
+ size: number;
6
+ data?: (index: number, total: number) => number;
7
+ }
8
+
9
+ export interface UniformProps {
10
+ type: string;
11
+ value: number[];
12
+ location?: WebGLUniformLocation;
13
+ }
14
+
15
+ export interface CanvasProps extends HTMLAttributes<HTMLCanvasElement> {
16
+ fragment: string;
17
+ vertex?: string;
18
+ uniforms?: {
19
+ [key: string]: UniformProps;
20
+ };
21
+ attributes?: Array<AttributeProps>;
22
+ onRender?: (r: unknown) => void;
23
+ onInstRender?: (r: UniformProps) => void;
24
+ name?: string;
25
+ }
@@ -0,0 +1,3 @@
1
+ export { Canvas } from './Canvas';
2
+ export type { CanvasProps, AttributeProps, UniformProps } from './Canvas.types';
3
+ export { useRenderer } from './useRenderer';
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import Phenomenon from 'phenomenon';
3
+ import { UniformProps } from './Canvas.types';
4
+
5
+ type UniformType = {
6
+ [key: string]: UniformProps;
7
+ };
8
+
9
+ export const useRenderer = (uniform: UniformType) => {
10
+ const canvasRef = useRef<HTMLCanvasElement>(null);
11
+ const [renderer, setRenderer] = useState<Phenomenon | null>(null);
12
+
13
+ useEffect(() => {
14
+ if (!canvasRef.current) {
15
+ return;
16
+ }
17
+
18
+ const phenomenon = new Phenomenon({
19
+ settings: {
20
+ uniforms: uniform,
21
+ devicePixelRatio: window.devicePixelRatio,
22
+ shouldRender: true,
23
+ canvas: canvasRef.current,
24
+ mode: 4,
25
+ },
26
+ context: {
27
+ antialias: true,
28
+ alpha: true,
29
+ },
30
+ });
31
+
32
+ setRenderer(phenomenon);
33
+
34
+ return () => {
35
+ phenomenon?.destroy();
36
+ window.removeEventListener('resize', phenomenon?.resize);
37
+ };
38
+ }, []);
39
+
40
+ return {
41
+ canvasRef,
42
+ renderer,
43
+ };
44
+ };
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { DropImage } from './DropImage';
3
+
4
+ const meta: Meta<typeof DropImage> = {
5
+ title: 'Organisms/DropImage',
6
+ component: DropImage,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ const mockUploader = async (file: File) => {
17
+ await new Promise(resolve => setTimeout(resolve, 1000));
18
+ return { success: true, fileName: file.name };
19
+ };
20
+
21
+ export const Default: Story = {
22
+ args: {
23
+ uploader: mockUploader,
24
+ placeholder: <div className="text-content-secondary text-center">拖拽图片到此处</div>,
25
+ },
26
+ };
27
+
28
+ export const Ghost: Story = {
29
+ args: {
30
+ ghost: true,
31
+ uploader: mockUploader,
32
+ placeholder: (
33
+ <div className="text-content-secondary text-center">Ghost 模式 - 拖拽图片到页面任意位置</div>
34
+ ),
35
+ },
36
+ };
@@ -0,0 +1,193 @@
1
+ import { useCallback, useEffect, useState, FunctionComponent } from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { wait } from './utils';
4
+ import { useDropImage } from './useDropImage';
5
+ import { ImagePreviewerProps, DropImageProps } from './DropImage.types';
6
+
7
+ const ImagePreviewer = (props: ImagePreviewerProps) => {
8
+ const { visible: showImagePreviewer, imageRef, imageSize, imageSrc } = props;
9
+
10
+ return (
11
+ <img
12
+ className={twMerge(
13
+ 'max-w-full h-auto duration-300 transition-opacity opacity-100',
14
+ !showImagePreviewer && 'opacity-0 m-0! p-0!'
15
+ )}
16
+ ref={imageRef}
17
+ src={imageSrc}
18
+ width={imageSize.width}
19
+ height={imageSize.height}
20
+ alt="Preview"
21
+ />
22
+ );
23
+ };
24
+
25
+ ImagePreviewer.displayName = 'ImagePreviewer';
26
+
27
+ const GhostDropImage: FunctionComponent<DropImageProps<unknown>> = props => {
28
+ const {
29
+ onDrop = () => {},
30
+ onUploadFinish = () => {},
31
+ uploader,
32
+ waitTimeAfterFinish = 2000,
33
+ className = '',
34
+ children,
35
+ placeholder,
36
+ } = props;
37
+
38
+ const [coverVisible, setCoverVisible] = useState(false);
39
+ const [imageVisible, setImageVisible] = useState(false);
40
+
41
+ const handleDocDragEnter = useCallback(() => {
42
+ setCoverVisible(true);
43
+ }, []);
44
+
45
+ const { handleDragLeave, handleDragOver, handleDrop, imageRef, imageSize, imageSrc, reset } =
46
+ useDropImage({
47
+ onImageLoad: () => {
48
+ setImageVisible(true);
49
+ },
50
+ onDrop: async (e, file) => {
51
+ onDrop(e, file);
52
+ setCoverVisible(false);
53
+ const result = await uploader(file);
54
+ await wait(waitTimeAfterFinish);
55
+ onUploadFinish(result);
56
+ setImageVisible(false);
57
+ reset();
58
+ },
59
+ });
60
+
61
+ useEffect(() => {
62
+ document.addEventListener('dragenter', handleDocDragEnter);
63
+
64
+ return () => {
65
+ document.removeEventListener('dragenter', handleDocDragEnter);
66
+ };
67
+ }, [handleDocDragEnter]);
68
+
69
+ return (
70
+ <>
71
+ <div
72
+ className={twMerge(
73
+ 'fixed top-0 left-0 h-full w-full transition-all duration-200',
74
+ coverVisible
75
+ ? 'backdrop-blur-sm bg-background/50 z-[999] opacity-100'
76
+ : 'z-[-1] opacity-0 pointer-events-none'
77
+ )}
78
+ onDragLeave={() => {
79
+ handleDragLeave();
80
+ setCoverVisible(false);
81
+ }}
82
+ onDragOver={handleDragOver}
83
+ onDrop={handleDrop}
84
+ />
85
+ {!imageVisible && placeholder}
86
+ {children ? (
87
+ children({
88
+ visible: imageVisible,
89
+ imageRef,
90
+ imageSize,
91
+ imageSrc,
92
+ })
93
+ ) : (
94
+ <ImagePreviewer
95
+ className={className}
96
+ visible={imageVisible}
97
+ imageRef={imageRef}
98
+ imageSrc={imageSrc}
99
+ imageSize={imageSize}
100
+ />
101
+ )}
102
+ </>
103
+ );
104
+ };
105
+
106
+ GhostDropImage.displayName = 'GhostDropImage';
107
+
108
+ export const DropImage: FunctionComponent<DropImageProps<unknown>> = props => {
109
+ const {
110
+ uploader,
111
+ defaultBgColor = 'var(--color-muted)',
112
+ onDrop,
113
+ dragOverBgColor = 'var(--color-accent)',
114
+ waitTimeAfterFinish = 2000,
115
+ placeholder = '',
116
+ className = '',
117
+ onUploadFinish = () => {},
118
+ ghost,
119
+ children,
120
+ dropAreaStyle = {
121
+ width: 300,
122
+ height: 300,
123
+ },
124
+ } = props;
125
+
126
+ if (ghost) {
127
+ return <GhostDropImage {...props} />;
128
+ }
129
+
130
+ const [showImagePreviewer, setShowImagePreviewer] = useState(false);
131
+ const {
132
+ handleDragLeave,
133
+ handleDragOver,
134
+ handleDrop,
135
+ imageRef,
136
+ imageSize,
137
+ imageSrc,
138
+ isDragOver,
139
+ reset,
140
+ } = useDropImage({
141
+ onDrop,
142
+ onImageLoad: async (image, file) => {
143
+ await wait(500);
144
+ setShowImagePreviewer(true);
145
+ await onUploadFinish(await uploader(file, image));
146
+ await wait(waitTimeAfterFinish);
147
+ setShowImagePreviewer(false);
148
+ await wait(500);
149
+ reset();
150
+ },
151
+ });
152
+
153
+ const getDropAreaStyle = () => {
154
+ return Object.assign({}, dropAreaStyle, {
155
+ background: isDragOver ? dragOverBgColor : defaultBgColor,
156
+ width: imageSize.width || dropAreaStyle.width,
157
+ height: imageSize.height || dropAreaStyle.height,
158
+ });
159
+ };
160
+
161
+ return (
162
+ <div
163
+ className={twMerge(
164
+ 'transition-all items-center justify-center flex duration-200 ease-in-out flex-col rounded-sm',
165
+ className,
166
+ imageSrc ? 'shadow-none' : 'shadow-[var(--shadow-input)]'
167
+ )}
168
+ onDragLeave={handleDragLeave}
169
+ onDragOver={handleDragOver}
170
+ onDrop={handleDrop}
171
+ style={getDropAreaStyle()}
172
+ >
173
+ {children ? (
174
+ children({
175
+ visible: showImagePreviewer,
176
+ imageRef,
177
+ imageSize,
178
+ imageSrc,
179
+ })
180
+ ) : (
181
+ <ImagePreviewer
182
+ visible={showImagePreviewer}
183
+ imageRef={imageRef}
184
+ imageSrc={imageSrc}
185
+ imageSize={imageSize}
186
+ />
187
+ )}
188
+ {!imageSrc && placeholder}
189
+ </div>
190
+ );
191
+ };
192
+
193
+ DropImage.displayName = 'DropImage';
@@ -0,0 +1,26 @@
1
+ import { CSSProperties, ReactNode, Ref } from 'react';
2
+
3
+ export interface ImagePreviewerProps {
4
+ className?: string;
5
+ visible: boolean;
6
+ imageRef: Ref<HTMLImageElement>;
7
+ imageSrc: string;
8
+ imageSize: {
9
+ width: number;
10
+ height: number;
11
+ };
12
+ }
13
+
14
+ export interface DropImageProps<T = unknown> {
15
+ uploader: (file: File, img?: HTMLImageElement) => Promise<T>;
16
+ onDrop?: (e: React.DragEvent<Element>, file: File) => void;
17
+ onUploadFinish?: (result: T) => void;
18
+ waitTimeAfterFinish?: number;
19
+ defaultBgColor?: string;
20
+ dragOverBgColor?: string;
21
+ dropAreaStyle?: CSSProperties;
22
+ placeholder?: ReactNode;
23
+ className?: string;
24
+ ghost?: boolean;
25
+ children?: (props: ImagePreviewerProps) => ReactNode;
26
+ }
@@ -0,0 +1,3 @@
1
+ export { DropImage } from './DropImage';
2
+ export type { DropImageProps, ImagePreviewerProps } from './DropImage.types';
3
+ export { useDropImage } from './useDropImage';