diff --git a/.changeset-pending/README.md b/.changeset-pending/README.md
new file mode 100644
index 000000000..a0665e70f
--- /dev/null
+++ b/.changeset-pending/README.md
@@ -0,0 +1 @@
+暂时不发版的变更记录文件放在此目录
diff --git a/.changeset-pending/sixty-elephants-join.md b/.changeset-pending/sixty-elephants-join.md
new file mode 100644
index 000000000..cb29c087f
--- /dev/null
+++ b/.changeset-pending/sixty-elephants-join.md
@@ -0,0 +1,6 @@
+---
+"@hi-ui/hiui": minor
+"@hi-ui/resize-box": minor
+---
+
+feat: 增加 ResizeBox 组件
diff --git a/.changeset/eleven-actors-beg.md b/.changeset/eleven-actors-beg.md
new file mode 100644
index 000000000..0f2ea8ab9
--- /dev/null
+++ b/.changeset/eleven-actors-beg.md
@@ -0,0 +1,5 @@
+---
+"@hi-ui/icons": patch
+---
+
+增加 DragDot 图标
diff --git a/packages/icons/icon-resources/common/outlined/drag-dot.svg b/packages/icons/icon-resources/common/outlined/drag-dot.svg
new file mode 100644
index 000000000..fa2bfdc6a
--- /dev/null
+++ b/packages/icons/icon-resources/common/outlined/drag-dot.svg
@@ -0,0 +1 @@
+
diff --git a/packages/icons/src/components/common/drag-dot-outlined.tsx b/packages/icons/src/components/common/drag-dot-outlined.tsx
new file mode 100644
index 000000000..ccb44a946
--- /dev/null
+++ b/packages/icons/src/components/common/drag-dot-outlined.tsx
@@ -0,0 +1,23 @@
+
+import React, { forwardRef } from 'react'
+import { cx, getPrefixCls } from '@hi-ui/classname'
+import { __DEV__ } from '@hi-ui/env'
+import { IconProps } from '../../@types/props'
+
+const _prefix = getPrefixCls('icon-drag-dot-outlined')
+
+export const DragDotOutlined = forwardRef(
+ ({ prefixCls = _prefix, className, children, size, style: styleProp, ...rest }, ref) => {
+ const cls = cx(prefixCls, className)
+ const style = { fontSize: size, ...styleProp }
+
+ return (
+
+ )
+ }
+)
+
+if (__DEV__) {
+ DragDotOutlined.displayName = 'DragDotOutlined'
+}
+
\ No newline at end of file
diff --git a/packages/icons/src/icon-summation.ts b/packages/icons/src/icon-summation.ts
index 9c690a274..e8dc77b67 100644
--- a/packages/icons/src/icon-summation.ts
+++ b/packages/icons/src/icon-summation.ts
@@ -144,6 +144,7 @@ export { DislikeOutlined } from './components/common/dislike-outlined'
export { DocumentOutlined } from './components/common/document-outlined'
export { DocumentExclamationOutlined } from './components/common/document-exclamation-outlined'
export { DownloadOutlined } from './components/common/download-outlined'
+export { DragDotOutlined } from './components/common/drag-dot-outlined'
export { EndDateOutlined } from './components/common/end-date-outlined'
export { ExportOutlined } from './components/common/export-outlined'
export { ExpressionOutlined } from './components/common/expression-outlined'
diff --git a/packages/icons/stories/basic.stories.tsx b/packages/icons/stories/basic.stories.tsx
index c8de37c98..b36d1a097 100644
--- a/packages/icons/stories/basic.stories.tsx
+++ b/packages/icons/stories/basic.stories.tsx
@@ -335,6 +335,8 @@ export const Basic = () => {
{ component: Icons.DownloadOutlined, tagName: 'DownloadOutlined' },
+ { component: Icons.DragDotOutlined, tagName: 'DragDotOutlined' },
+
{ component: Icons.EndDateOutlined, tagName: 'EndDateOutlined' },
{ component: Icons.ExportOutlined, tagName: 'ExportOutlined' },
diff --git a/packages/ui/hiui/package.json b/packages/ui/hiui/package.json
index 67d208977..b6f4be8ee 100644
--- a/packages/ui/hiui/package.json
+++ b/packages/ui/hiui/package.json
@@ -87,6 +87,7 @@
"@hi-ui/provider": "^4.0.5",
"@hi-ui/radio": "^4.0.4",
"@hi-ui/rating": "^4.0.5",
+ "@hi-ui/resize-box": "^4.0.0",
"@hi-ui/result": "^4.0.4",
"@hi-ui/scrollbar": "^4.0.1",
"@hi-ui/search": "^4.0.8",
diff --git a/packages/ui/hiui/src/index.ts b/packages/ui/hiui/src/index.ts
index 9bd2077c6..9b5168556 100644
--- a/packages/ui/hiui/src/index.ts
+++ b/packages/ui/hiui/src/index.ts
@@ -139,6 +139,9 @@ export { default as Radio } from '@hi-ui/radio'
export * from '@hi-ui/rating'
export { default as Rating } from '@hi-ui/rating'
+export * from '@hi-ui/resize-box'
+export { default as ResizeBox } from '@hi-ui/resize-box'
+
export * from '@hi-ui/result'
export { default as Result } from '@hi-ui/result'
diff --git a/packages/ui/resize-box/README.md b/packages/ui/resize-box/README.md
new file mode 100644
index 000000000..0c5cb050d
--- /dev/null
+++ b/packages/ui/resize-box/README.md
@@ -0,0 +1,11 @@
+# `@hi-ui/resize-box`
+
+> TODO: description
+
+## Usage
+
+```
+const ResizeBox = require('@hi-ui/resize-box');
+
+// TODO: DEMONSTRATE API
+```
diff --git a/packages/ui/resize-box/__tests__/resize-box.test.js b/packages/ui/resize-box/__tests__/resize-box.test.js
new file mode 100644
index 000000000..ec3ed8c08
--- /dev/null
+++ b/packages/ui/resize-box/__tests__/resize-box.test.js
@@ -0,0 +1,5 @@
+const ResizeBox = require('../src')
+
+describe('@hi-ui/resize-box', () => {
+ it('needs tests', () => {})
+})
diff --git a/packages/ui/resize-box/hi-docs.config.mdx b/packages/ui/resize-box/hi-docs.config.mdx
new file mode 100644
index 000000000..1f1c95531
--- /dev/null
+++ b/packages/ui/resize-box/hi-docs.config.mdx
@@ -0,0 +1,11 @@
+# ResizeBox 伸缩框
+
+用于可调整大小的布局
+
+## 使用示例
+
+
+
+## Props
+
+
diff --git a/packages/ui/resize-box/jest.config.js b/packages/ui/resize-box/jest.config.js
new file mode 100644
index 000000000..e33c14b5d
--- /dev/null
+++ b/packages/ui/resize-box/jest.config.js
@@ -0,0 +1 @@
+module.exports = require('../../../jest.config')
diff --git a/packages/ui/resize-box/package.json b/packages/ui/resize-box/package.json
new file mode 100644
index 000000000..8f53191b1
--- /dev/null
+++ b/packages/ui/resize-box/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@hi-ui/resize-box",
+ "version": "4.0.0-alpha.0",
+ "description": "A sub-package for @hi-ui/hiui.",
+ "keywords": [],
+ "author": "HiUI ",
+ "homepage": "https://github.com/XiaoMi/hiui/tree/master/packages/ui/resize-box#readme",
+ "license": "MIT",
+ "directories": {
+ "lib": "lib",
+ "test": "__tests__"
+ },
+ "files": [
+ "lib"
+ ],
+ "main": "lib/cjs/index.js",
+ "module": "lib/esm/index.js",
+ "types": "lib/types/index.d.ts",
+ "typings": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "require": "./lib/cjs/index.js",
+ "default": "./lib/esm/index.js"
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/XiaoMi/hiui.git"
+ },
+ "scripts": {
+ "test": "jest",
+ "clean": "rimraf lib",
+ "prebuild": "yarn clean",
+ "build:esm": "hi-build ./src/index.ts --format esm -d ./lib/esm",
+ "build:cjs": "hi-build ./src/index.ts --format cjs -d ./lib/cjs",
+ "build:types": "tsc --emitDeclarationOnly --declaration --declarationDir lib/types",
+ "build": "concurrently yarn:build:*"
+ },
+ "bugs": {
+ "url": "https://github.com/XiaoMi/hiui/issues"
+ },
+ "dependencies": {
+ "@hi-ui/classname": "^4.0.0",
+ "@hi-ui/env": "^4.0.0",
+ "@hi-ui/use-merge-refs": "^4.0.1",
+ "@hi-ui/use-uncontrolled-state": "^4.0.1",
+ "react-resizable": "^3.0.5"
+ },
+ "peerDependencies": {
+ "@hi-ui/core": ">=4.0.0",
+ "react": ">=16.8.6",
+ "react-dom": ">=16.8.6"
+ },
+ "devDependencies": {
+ "@hi-ui/core": "^4.0.0",
+ "@hi-ui/core-css": "^4.0.0",
+ "react": "^17.0.1",
+ "react-dom": "^17.0.1"
+ }
+}
diff --git a/packages/ui/resize-box/src/ResizeBox.tsx b/packages/ui/resize-box/src/ResizeBox.tsx
new file mode 100644
index 000000000..ab61296a0
--- /dev/null
+++ b/packages/ui/resize-box/src/ResizeBox.tsx
@@ -0,0 +1,223 @@
+import React, { forwardRef } from 'react'
+import { Resizable } from 'react-resizable'
+import { cx, getPrefixCls } from '@hi-ui/classname'
+import { __DEV__ } from '@hi-ui/env'
+import { HiBaseHTMLProps } from '@hi-ui/core'
+import { useMergeRefs } from '@hi-ui/use-merge-refs'
+import { useUncontrolledState } from '@hi-ui/use-uncontrolled-state'
+import { ResizeBoxPane, ResizeBoxPaneProps } from './ResizeBoxPane'
+import { Separator, SeparatorProps } from './Separator'
+
+const RESIZE_BOX_PREFIX = getPrefixCls('resize-box')
+
+export const ResizeBox = forwardRef(
+ (
+ {
+ prefixCls = RESIZE_BOX_PREFIX,
+ role = 'resize-box',
+ className,
+ children,
+ separatorProps,
+ ...rest
+ },
+ ref
+ ) => {
+ const cls = cx(prefixCls, className)
+
+ const containerRef = React.useRef(null)
+ const mergedRef = useMergeRefs(ref, containerRef)
+
+ const [colWidths, tryChangeColWidths] = useUncontrolledState([])
+ const minColWidthsRef = React.useRef([])
+
+ const draggableRef = React.useRef(true)
+
+ /**
+ * 计算内容面板宽度
+ * 如果有设置默认宽度,则使用默认宽度,否则使用平均宽度
+ * 如果有设置最小宽度,则使用最小宽度,否则使用默认宽度的一半
+ */
+ const calcPaneWidth = React.useCallback(() => {
+ const container = containerRef.current
+ const containerWidth = container?.getBoundingClientRect().width ?? 0
+ const minColWidths: number[] = []
+ let defaultColWidths: number[] = []
+ let calcWidth = 0
+ let avgWidth = 0
+
+ React.Children.forEach(children, (child) => {
+ const {
+ props: { defaultWidth = 0, minWidth = 0 },
+ } = child as React.ReactElement
+
+ defaultColWidths.push(defaultWidth)
+ minColWidths?.push(minWidth)
+
+ calcWidth += defaultWidth
+ })
+
+ if (calcWidth > containerWidth) {
+ console.error('default width is greater than container width')
+ return
+ }
+
+ if (calcWidth < containerWidth) {
+ const noDefaultWidthLength = defaultColWidths.filter((item) => !item).length
+
+ avgWidth = Math.floor((containerWidth - calcWidth) / noDefaultWidthLength)
+
+ defaultColWidths = defaultColWidths.map((item) => {
+ if (!item) {
+ return avgWidth
+ } else {
+ return item
+ }
+ })
+ }
+
+ tryChangeColWidths(defaultColWidths)
+
+ minColWidthsRef.current = minColWidths.map((item, index) => {
+ // 如果没有设置最小宽度,则最小宽度是默认宽度的一半
+ return item || defaultColWidths[index] * 0.5
+ })
+ }, [children, tryChangeColWidths])
+
+ const panesContent = React.useMemo(() => {
+ if (!children) {
+ console.error('children is required')
+ return null
+ }
+
+ if (!Array.isArray(children)) {
+ console.error('children must be array')
+ return children
+ }
+
+ return React.Children.map(
+ children as React.ReactElement[],
+ (child, index) => {
+ if (!React.isValidElement(child)) {
+ console.error('child is not valid element')
+ return
+ }
+
+ const { type, props } = child
+ const { style, onResizeStart, onResizeEnd, onResize, ...rest } = props
+
+ if (type !== ResizeBoxPane) {
+ console.error('ResizeBox children must be ResizeBoxPane')
+ return
+ }
+
+ if (index !== children?.length - 1) {
+ return (
+ }
+ height={0}
+ width={colWidths[index] ?? 0}
+ onResizeStart={() => {
+ draggableRef.current = true
+ onResizeStart?.()
+ }}
+ onResizeStop={onResizeEnd}
+ onResize={(evt, data) => {
+ const mouseEvent = evt as React.MouseEvent
+
+ mouseEvent.stopPropagation()
+ mouseEvent.preventDefault()
+
+ const { width: resizedWidth } = data.size
+
+ // 向左或向右拖动到最小宽度时禁止拖拽
+ if (
+ resizedWidth < minColWidthsRef.current[index] ||
+ colWidths[index] + colWidths[index + 1] - resizedWidth <
+ minColWidthsRef.current[index + 1]
+ ) {
+ draggableRef.current = false
+ }
+
+ if (!draggableRef.current) return
+
+ tryChangeColWidths((prev) => {
+ const nextColWidths = [...prev]
+ const currentWidth = nextColWidths[index]
+ const siblingWidth = nextColWidths[index + 1]
+ const minColWidth = minColWidthsRef.current[index]
+ const siblingMinColWidth = minColWidthsRef.current[index + 1]
+ const width =
+ resizedWidth <= minColWidth
+ ? // 显示最小宽度
+ minColWidth
+ : currentWidth + siblingWidth - resizedWidth < siblingMinColWidth
+ ? // 能够显示的最大宽度
+ currentWidth + siblingWidth - siblingMinColWidth
+ : // 显示拖拽后的宽度
+ resizedWidth
+ const resizeWidth = width - currentWidth
+
+ nextColWidths[index] = width
+ nextColWidths[index + 1] = siblingWidth - resizeWidth
+
+ onResize?.(width)
+
+ return nextColWidths
+ })
+ }}
+ >
+ {React.cloneElement(child, {
+ ...rest,
+ style: {
+ ...style,
+ width: colWidths[index],
+ },
+ })}
+
+ )
+ } else {
+ return React.cloneElement(child, {
+ ...rest,
+ style: {
+ ...style,
+ width: colWidths[index],
+ },
+ })
+ }
+ }
+ )
+ }, [children, colWidths, prefixCls, separatorProps, tryChangeColWidths])
+
+ React.useLayoutEffect(() => {
+ if (containerRef.current) {
+ calcPaneWidth()
+
+ const resizeObserver = new ResizeObserver(() => {
+ calcPaneWidth()
+ })
+
+ resizeObserver.observe(containerRef.current)
+
+ return () => {
+ resizeObserver.disconnect()
+ }
+ }
+ }, [calcPaneWidth])
+
+ return (
+
+ {panesContent}
+
+ )
+ }
+)
+
+export interface ResizeBoxProps extends HiBaseHTMLProps<'div'> {
+ separatorProps?: SeparatorProps
+}
+
+if (__DEV__) {
+ ResizeBox.displayName = 'ResizeBox'
+}
diff --git a/packages/ui/resize-box/src/ResizeBoxPane.tsx b/packages/ui/resize-box/src/ResizeBoxPane.tsx
new file mode 100644
index 000000000..1660c1d97
--- /dev/null
+++ b/packages/ui/resize-box/src/ResizeBoxPane.tsx
@@ -0,0 +1,45 @@
+import React, { forwardRef } from 'react'
+import { cx, getPrefixCls } from '@hi-ui/classname'
+import { __DEV__ } from '@hi-ui/env'
+import { HiBaseHTMLProps } from '@hi-ui/core'
+
+const RESIZE_BOX_PANE_PREFIX = getPrefixCls('resize-box-pane')
+
+export const ResizeBoxPane = forwardRef(
+ (
+ {
+ prefixCls = RESIZE_BOX_PANE_PREFIX,
+ role = 'resize-box-pane',
+ className,
+ children,
+ defaultWidth,
+ minWidth,
+ onResizeStart,
+ onResizeEnd,
+ onResize,
+ ...rest
+ },
+ ref
+ ) => {
+ const cls = cx(prefixCls, className)
+
+ return (
+
+ {children}
+
+ )
+ }
+)
+
+export interface ResizeBoxPaneProps extends HiBaseHTMLProps<'div'> {
+ defaultWidth?: number
+ width?: number
+ minWidth?: number
+ onResizeStart?: () => void
+ onResizeEnd?: () => void
+ onResize?: (width: number) => void
+}
+
+if (__DEV__) {
+ ResizeBoxPane.displayName = 'ResizeBoxPane'
+}
diff --git a/packages/ui/resize-box/src/Separator.tsx b/packages/ui/resize-box/src/Separator.tsx
new file mode 100644
index 000000000..117857d4f
--- /dev/null
+++ b/packages/ui/resize-box/src/Separator.tsx
@@ -0,0 +1,27 @@
+import React, { forwardRef } from 'react'
+import { cx, getPrefixCls } from '@hi-ui/classname'
+import { __DEV__ } from '@hi-ui/env'
+import { HiBaseHTMLProps } from '@hi-ui/core'
+
+const SEPARATOR_PREFIX = getPrefixCls('resize-box-separator')
+
+export const Separator = forwardRef(
+ (
+ { prefixCls = SEPARATOR_PREFIX, role = 'resize-box-separator', className, children, ...rest },
+ ref
+ ) => {
+ const cls = cx(prefixCls, className)
+
+ return (
+
+
+
+ )
+ }
+)
+
+export interface SeparatorProps extends HiBaseHTMLProps<'div'> {}
+
+if (__DEV__) {
+ Separator.displayName = 'SEPARATOR'
+}
diff --git a/packages/ui/resize-box/src/index.ts b/packages/ui/resize-box/src/index.ts
new file mode 100644
index 000000000..9c6ec5311
--- /dev/null
+++ b/packages/ui/resize-box/src/index.ts
@@ -0,0 +1,6 @@
+import './styles/index.scss'
+
+export * from './ResizeBox'
+export { ResizeBox as default } from './ResizeBox'
+
+export * from './ResizeBoxPane'
diff --git a/packages/ui/resize-box/src/styles/index.scss b/packages/ui/resize-box/src/styles/index.scss
new file mode 100644
index 000000000..846141425
--- /dev/null
+++ b/packages/ui/resize-box/src/styles/index.scss
@@ -0,0 +1 @@
+@import './resize-box.scss';
diff --git a/packages/ui/resize-box/src/styles/resize-box.scss b/packages/ui/resize-box/src/styles/resize-box.scss
new file mode 100644
index 000000000..9200b87f3
--- /dev/null
+++ b/packages/ui/resize-box/src/styles/resize-box.scss
@@ -0,0 +1,38 @@
+@import '~@hi-ui/core-css/lib/index.scss';
+
+$prefix: '#{$component-prefix}-resize-box' !default;
+
+.#{$prefix} {
+ display: flex;
+ box-sizing: border-box;
+ user-select: auto;
+
+ &-pane {
+ display: flex;
+ justify-content: space-between;
+ overflow: hidden;
+ }
+
+ &-separator {
+ position: relative;
+ height: 100%;
+ padding: 0 2px;
+ cursor: col-resize;
+ flex-shrink: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &-content {
+ width: 2px;
+ height: 100%;
+ background-color: use-color('gray', 300);
+ }
+
+ &:hover {
+ .#{$prefix}-separator-content {
+ background-color: use-color('brandblue', 300);
+ }
+ }
+ }
+}
diff --git a/packages/ui/resize-box/stories/basic.stories.tsx b/packages/ui/resize-box/stories/basic.stories.tsx
new file mode 100644
index 000000000..633ab13ba
--- /dev/null
+++ b/packages/ui/resize-box/stories/basic.stories.tsx
@@ -0,0 +1,43 @@
+import React from 'react'
+import ResizeBox, { ResizeBoxPane } from '../src'
+
+/**
+ * @title 基础用法
+ */
+export const Basic = () => {
+ return (
+ <>
+ Basic
+
+
+
+
+ left content
+
+
+
+
+ right content
+
+
+
+
+ >
+ )
+}
diff --git a/packages/ui/resize-box/stories/index.stories.tsx b/packages/ui/resize-box/stories/index.stories.tsx
new file mode 100644
index 000000000..d7a14925c
--- /dev/null
+++ b/packages/ui/resize-box/stories/index.stories.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import ResizeBox from '../src'
+
+export * from './basic.stories'
+export * from './min-width.stories'
+
+export default {
+ title: 'Others/ResizeBox',
+ component: ResizeBox,
+ decorators: [(story: Function) => {story()}
],
+}
diff --git a/packages/ui/resize-box/stories/min-width.stories.tsx b/packages/ui/resize-box/stories/min-width.stories.tsx
new file mode 100644
index 000000000..db99a32f1
--- /dev/null
+++ b/packages/ui/resize-box/stories/min-width.stories.tsx
@@ -0,0 +1,43 @@
+import React from 'react'
+import ResizeBox, { ResizeBoxPane } from '../src'
+
+/**
+ * @title 设置 pane 最小宽度
+ */
+export const MinWidth = () => {
+ return (
+ <>
+ MinWidth
+
+
+
+
+ left content
+
+
+
+
+ right content
+
+
+
+
+ >
+ )
+}
diff --git a/packages/ui/resize-box/tsconfig.json b/packages/ui/resize-box/tsconfig.json
new file mode 100644
index 000000000..f7bbdb2fe
--- /dev/null
+++ b/packages/ui/resize-box/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "../../../tsconfig.json",
+ "include": ["./src"]
+}
diff --git a/yarn.lock b/yarn.lock
index d194e712d..55f06d8b1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15601,6 +15601,14 @@ react-resizable@^3.0.4:
prop-types "15.x"
react-draggable "^4.0.3"
+react-resizable@^3.0.5:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1"
+ integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
+ dependencies:
+ prop-types "15.x"
+ react-draggable "^4.0.3"
+
react-resize-detector@^6.7.6:
version "6.7.6"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.6.tgz#4416994e5ead7eba76606e3a248a1dfca49b67a3"