Browse Source

feat: New Component - Resizable (#2458)

* feat: init resizable

* chore: add null check

* feat: implement resizable

* feat: implement adapted F/A

* feat: custom handler

* feat: add stories to present

* refactor: remove useless code

* refactor: extract single resizable constants

* feat: init resize group

* feat: init group resizable using single foundation

* feat: create test group stories

* feat: modify resize group

* feat: implement resize group

* fix: fix extra squash bug

* fix: fix extra squash caused by min/max prop

* fix: extra squash in corner and handler shrink

* chore: init resizable docs

* chore: create chinese document demo

* fix: extra squash caused by handler width

* chore: doc add onResizeStart

* chore: add resizable ts stories

* chore: update API in doc

* fix: vertical constraint bug

* chore: add api in doc and english doc

* feat: add default style for resize group handler

* chore: change according to the review

* fix: fix code according to PR review

* feat: implement calculate in group

* feat: compliment constraints

* feat: implement callbacks

* refactor: refactor to F/A structure

* feat: sync for resize group

* feat: allocate undefined size for item

* feat: auto resize item and warning

* feat: modify onChange of resizable

* feat: manage item size of group as percentage

* feat: support px for resizeItem

* feat: refactor constraints

* feat: fix extra dragging step

* feat: fix all extra dragging bug

* feat: refactor css to scss

* chore:update doc and story using semi token

* fix: The context value of resizeGroup is saved using variables to prevent unexpected rendering caused by context value updates

* chore: remove useless console.log

* chore: refactor scss and constants file

* feat: add new unit to allocate surplus space

* refactor: move the events operations to react adapter

* feat: support number type for defaultSize of resizeItem and sync doc

* chore: Restore original settings

---------

Co-authored-by: yanzhuoran <[email protected]>
Co-authored-by: zhangyumei.0319 <[email protected]>
Nathon2Y 1 year ago
parent
commit
e17825ed06
56 changed files with 4651 additions and 34 deletions
  1. 1 1
      content/feedback/banner/index-en-US.md
  2. 1 1
      content/feedback/banner/index.md
  3. 1 1
      content/feedback/notification/index-en-US.md
  4. 1 1
      content/feedback/notification/index.md
  5. 1 1
      content/feedback/popconfirm/index-en-US.md
  6. 1 1
      content/feedback/popconfirm/index.md
  7. 1 1
      content/feedback/progress/index-en-US.md
  8. 1 1
      content/feedback/progress/index.md
  9. 1 1
      content/feedback/skeleton/index-en-US.md
  10. 1 1
      content/feedback/skeleton/index.md
  11. 1 1
      content/feedback/spin/index-en-US.md
  12. 1 1
      content/feedback/spin/index.md
  13. 1 1
      content/feedback/toast/index-en-US.md
  14. 1 1
      content/feedback/toast/index.md
  15. 1 0
      content/order.js
  16. 1 1
      content/other/configprovider/index-en-US.md
  17. 1 1
      content/other/configprovider/index.md
  18. 1 1
      content/other/locale/index-en-US.md
  19. 1 1
      content/other/locale/index.md
  20. 1 1
      content/plus/chat/index-en-US.md
  21. 1 1
      content/plus/chat/index.md
  22. 1 1
      content/show/chart/index-en-US.md
  23. 1 1
      content/show/chart/index.md
  24. 736 0
      content/show/resizable/index-en-US.md
  25. 735 0
      content/show/resizable/index.md
  26. 1 1
      content/show/scrolllist/index-en-US.md
  27. 1 1
      content/show/scrolllist/index.md
  28. 1 1
      content/show/sidesheet/index-en-US.md
  29. 1 1
      content/show/sidesheet/index.md
  30. 1 1
      content/show/table/index-en-US.md
  31. 1 1
      content/show/table/index.md
  32. 1 1
      content/show/tag/index-en-US.md
  33. 1 1
      content/show/tag/index.md
  34. 1 1
      content/show/timeline/index-en-US.md
  35. 1 1
      content/show/timeline/index.md
  36. 1 1
      content/show/tooltip/index-en-US.md
  37. 1 1
      content/show/tooltip/index.md
  38. 13 0
      packages/semi-foundation/resizable/constants.ts
  39. 31 0
      packages/semi-foundation/resizable/foundation.ts
  40. 293 0
      packages/semi-foundation/resizable/group/index.ts
  41. 25 0
      packages/semi-foundation/resizable/groupConstants.ts
  42. 39 0
      packages/semi-foundation/resizable/index.scss
  43. 629 0
      packages/semi-foundation/resizable/single/index.ts
  44. 127 0
      packages/semi-foundation/resizable/singleConstants.ts
  45. 145 0
      packages/semi-foundation/resizable/utils.ts
  46. 1 0
      packages/semi-theme-default/scss/variables.scss
  47. 7 0
      packages/semi-ui/index.ts
  48. 518 0
      packages/semi-ui/resizable/_story/resizable.stories.jsx
  49. 508 0
      packages/semi-ui/resizable/_story/resizable.stories.tsx
  50. 18 0
      packages/semi-ui/resizable/group/resizeContext.ts
  51. 204 0
      packages/semi-ui/resizable/group/resizeGroup.tsx
  52. 107 0
      packages/semi-ui/resizable/group/resizeHandler.tsx
  53. 98 0
      packages/semi-ui/resizable/group/resizeItem.tsx
  54. 19 0
      packages/semi-ui/resizable/index.tsx
  55. 273 0
      packages/semi-ui/resizable/single/resizable.tsx
  56. 90 0
      packages/semi-ui/resizable/single/resizableHandler.tsx

+ 1 - 1
content/feedback/banner/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 75
+order: 76
 category: Feedback
 title:  Banner
 subTitle: Banner

+ 1 - 1
content/feedback/banner/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 75
+order: 76
 category: 反馈类
 title:  Banner 通知横幅
 icon: doc-banner

+ 1 - 1
content/feedback/notification/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 76
+order: 77
 category: Feedback
 title:  Notification
 subTitle: Notification

+ 1 - 1
content/feedback/notification/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 76
+order: 77
 category: 反馈类
 title: Notification 通知
 icon: doc-notification

+ 1 - 1
content/feedback/popconfirm/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 77
+order: 78
 category: Feedback
 title:  Popconfirm
 subTitle: Popconfirm

+ 1 - 1
content/feedback/popconfirm/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 77
+order: 78
 category: 反馈类
 title:  Popconfirm 气泡确认框
 icon: doc-popconfirm

+ 1 - 1
content/feedback/progress/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 78
+order: 79
 category: Feedback
 title: Progress
 subTitle: Progress

+ 1 - 1
content/feedback/progress/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 78
+order: 79
 category: 反馈类
 title: Progress 进度条
 icon: doc-progress

+ 1 - 1
content/feedback/skeleton/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 79
+order: 80
 category: Feedback
 title: Skeleton
 subTitle: Skeleton

+ 1 - 1
content/feedback/skeleton/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 79
+order: 80
 category: 反馈类
 title: Skeleton 骨架屏
 icon: doc-skeleton

+ 1 - 1
content/feedback/spin/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 80
+order: 81
 category: Feedback
 title: Spin
 subTitle: Spin

+ 1 - 1
content/feedback/spin/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 80
+order: 81
 category: 反馈类
 title: Spin 加载器
 icon: doc-spin

+ 1 - 1
content/feedback/toast/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 81
+order: 82
 category: Feedback
 title: Toast
 subTitle: Toast

+ 1 - 1
content/feedback/toast/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 81
+order: 82
 category: 反馈类
 title: Toast 提示
 icon: doc-toast

+ 1 - 0
content/order.js

@@ -66,6 +66,7 @@ const order = [
     'modal',
     'overflowlist',
     'popover',
+    'resizable',
     'scrolllist',
     'sidesheet',
     'table',

+ 1 - 1
content/other/configprovider/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 82
+order: 83
 category: Other
 title: ConfigProvider
 icon: doc-configprovider

+ 1 - 1
content/other/configprovider/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 82
+order: 83
 category: 其他
 title:  ConfigProvider 全局配置
 icon: doc-configprovider

+ 1 - 1
content/other/locale/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 83
+order: 84
 category: Other
 title: LocaleProvider
 subTitle: LocaleProvider

+ 1 - 1
content/other/locale/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 83
+order: 84
 category: 其他
 title:  LocaleProvider 多语言
 icon: doc-i18n

+ 1 - 1
content/plus/chat/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 84
+order: 85
 category: Plus
 title:  Chat
 icon: doc-chat

+ 1 - 1
content/plus/chat/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 84
+order: 85
 category: Plus
 title:  Chat 对话
 icon: doc-chat

+ 1 - 1
content/show/chart/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 74
+order: 75
 category: Show
 title: Data Visualization
 icon: doc-vchart

+ 1 - 1
content/show/chart/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 74
+order: 75
 category: 展示类
 title:  Data Visualization 数据可视化
 icon: doc-vchart

+ 736 - 0
content/show/resizable/index-en-US.md

@@ -0,0 +1,736 @@
+---
+localeCode: en-US
+order: 68
+category: Show
+title: Resizable
+icon: doc-steps
+dir: column
+brief: The component size is adjusted based on the user's mouse drag, supporting both resizing of a single component and combined resizing.
+---
+
+## Demos
+
+### How to import
+
+```jsx 
+import { Resizable } from '@douyinfe/semi-ui';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui'
+```
+
+### Single Component
+Basic Usage and Callbacks
+You can set the initial size using defaultSize, and set drag callbacks with onResizeStart, onResize, and onResizeEnd.
+
+```tsx
+interface Size {
+    width: string | number;
+    height: string | number;
+}
+```
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag edge to resize')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts_2 = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        onChange={() => { setText('resizing') }}
+        onResizeStart={() => Toast.info(opts_1)}
+        onResizeEnd={() => { Toast.info(opts_2); setText('Drag edge to resize') }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          {text}
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+
+### Controlling Resize Directions
+You can enable or disable specific resizing directions by setting the value of enable. All directions are enabled by default.
+
+```tsx
+interface Enable {
+  left: Boolean;
+  right: Boolean;
+  top: Boolean;
+  bottom: Boolean;
+  topLeft: Boolean;
+  topRight: Boolean;
+  bottomLeft: Boolean;
+  bottomRight: Boolean;
+}
+```
+
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable, Switch, Typography } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [b, setB] = useState(false)
+  const { Title } = Typography;
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+        <div style={{ display: 'flex', alignItems: 'center', margin: 8 }}>
+          <Switch checked={b} onChange={setB}></Switch>
+            <Title heading={6} style={{ margin: 8 }}>
+                {b ? 'able' : 'disable'}
+            </Title>
+        </div>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        enable={{
+          left: b
+        }}
+        defaultSize={{
+          width: 200,
+          height: 200,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          {'enable.left:' + b}
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+
+### Setting Resizing Ratio
+
+You can set the drag and resize ratio using ratio.
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        ratio={2}
+        defaultSize={{
+          width: 200,
+          height: 200,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          ratio=2
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### Locking Aspect Ratio
+You can lock the aspect ratio by setting lockAspectRatio. It can be a boolean or a number. If true, it locks to the initial aspect ratio; if a number, it locks to the given ratio.
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginBottom: '10px' }}
+        defaultSize={{
+          width: 400,
+          height: 300,
+        }}
+        lockAspectRatio
+      >
+        <div style={{ marginLeft: '20%' }}>
+          lock
+        </div>
+      </Resizable>
+      <Resizable
+        style={{backgroundColor: 'rgba(var(--semi-grey-1), 1)'}}
+        defaultSize={{
+          width: 200,
+          height: 200 * 9 / 16,
+        }}
+        lockAspectRatio={16 / 9}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          16 / 9
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### Setting Maximum and Minimum Width/Height
+
+You can set the maximum and minimum width and height using maxHeight, maxWidth, minHeight, and minWidth.
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        maxWidth={200}
+        maxHeight={300}
+        minWidth={50}
+        minHeight={50}
+        defaultSize={{
+          width: 100,
+          height: 100,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          width is between 50 and 200, height is between 50 and 300
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### Control Width/Height
+You can control the size of the element through the size prop.
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [size, setSize] = useState({ width: 200, height: 300 });
+
+  const onChange = (() => {
+    let realSize = { width: size.width + 10, height: size.height + 10 };
+    setSize(realSize);
+  })
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={onChange}>set += 10</Button>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginTop: '10px' }}
+        defaultSize={{
+          width: 100,
+          height: 100,
+        }}
+        size={size}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          Control Width/Height
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+
+### Setting Scale
+You can scale the entire element by setting the scale prop.
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%', transform: 'scale(0.5)', transformOrigin: '0 0' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        defaultSize={{
+          width: '60%',
+          height: '60%',
+        }}
+        scale={0.5}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          scale 0.5
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+
+### Restricting Width/Height by an Element
+You can restrict the width and height by setting the boundElement, which supports string values like 'parent' or 'window'.
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '300px', height: '300px', border: 'var(--semi-color-border) 1px solid' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        defaultSize={{
+          width: '60%',
+          height: 200,
+        }}
+        boundElement={'parent'}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          bound:parent
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### Customizing Corner Handler Styles
+You can customize the drag handles for each direction using handleNode, and apply different styles using handleStyle and handleClassName.
+```jsx
+type HandleNode = {
+  left: ReactNode;
+  right: ReactNode;
+  top: ReactNode;
+  bottom: ReactNode;
+  topLeft: ReactNode;
+  topRight: ReactNode;
+  bottomLeft: ReactNode;
+  bottomRight: ReactNode;
+}
+
+type HandleStyle = {
+  left: React.CSSProperties;
+  right: React.CSSProperties;
+  top: React.CSSProperties;
+  bottom: React.CSSProperties;
+  topLeft: React.CSSProperties;
+  topRight: React.CSSProperties;
+  bottomLeft: React.CSSProperties;
+  bottomRight: React.CSSProperties;
+}
+
+type HandleClass = {
+  left: string;
+  right: string;
+  top: string;
+  bottom: string;
+  topLeft: string;
+  topRight: string;
+  bottomLeft: string;
+  bottomRight: string;
+}
+```
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable, Button } from '@douyinfe/semi-ui';
+function Demo() {
+    return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        handleNode={{
+          right: <div style={{
+            height: '100%',
+            display: 'flex',
+            alignItems: 'center',
+            width: 'fit-content',
+          }}><IconTransfer /></div>
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          right
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+```
+
+
+### Allowing Incremental Width and Height Adjustment
+You can allow gradual adjustments in width and height using the grid and snap properties. The grid property specifies the increments to which resizing should snap. The default value is [1, 1]. The snap property specifies the absolute pixel values to which resizing should snap. Both x and y are optional, allowing you to define only the desired axis. These two parameters can be combined with the snapGap property, which specifies the minimum gap required to move to the next target. The default is 0, meaning the target defined by grid/snap is always used.
+
+```tsx
+interface Snap {
+    x: number[];
+    y: number[];
+}
+```
+
+```jsx live=true 
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        grid={100}
+        snapGap={20}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          snap
+        </div>
+      </Resizable>
+    </div >
+  );
+}
+```
+
+### Group Component 
+<Notice type='primary' title='notice'>
+The parent element of `ResizeGroup` needs to have a size in the main axis direction.
+It's best not to set padding for ResizeItem, as it may cause the minimum size to not match the expected value. You can set padding for child elements instead.
+</Notice>
+
+
+
+Use the direction prop to set the resizing direction. Options are horizontal and vertical. Supports onResizeStart, onResize, and onResizeEnd callbacks, as well as setting min and max to control the maximum and minimum width/height.
+
+```jsx live=true dir="column"
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup, Toast } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag to resize')
+  return (
+    <div style={{ width: '1000px', height: '100px' }}>
+      <ResizeGroup direction='horizontal'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={'400px'}
+          min={'10%'}
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('Drag to resize') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text + " min:10%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={'20%'}
+          min={'10%'}
+          max={'30%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text + " min:10% max:30%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={'0.5'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={1}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+```
+
+### Nested
+Set the resizing direction using the direction prop. Options are horizontal and vertical.
+
+```jsx live=true dir="column"
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag to resize')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '1000px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => Toast.info(opts_1)}
+          onResizeEnd={() => { Toast.info(opts); setText('Drag to resize') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {'header'}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"80%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"25%"}
+              onChange={() => { setText('resizing') }}
+              onResizeStart={() => Toast.info(opts_1)}
+              onResizeEnd={() => { Toast.info(opts); setText('Drag to resize') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"75%"}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+```
+
+```jsx live=true dir="column"
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag to resize')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '1000px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          defaultSize={"80%"}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"25%"}
+              min={'10%'}
+              max={'30%'}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text + ' min:10% max:30%'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ height: '100%' }}>
+                <ResizeGroup direction='vertical'>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                    defaultSize={'33%'}
+                    min={'10%'}
+                    onChange={() => { setText('resizing') }}
+                    onResizeEnd={() => { setText('Drag to resize') }}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text + " min:10%"}
+                    </div>
+                  </ResizeItem>
+                  <ResizeHandler></ResizeHandler>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                    defaultSize={'33%'}
+                    min={'10%'}
+                    max={'40%'}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text + " min:10% max:40%"}
+                    </div>
+                  </ResizeItem>
+                  <ResizeHandler></ResizeHandler>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text}
+                    </div>
+                  </ResizeItem>
+                </ResizeGroup>
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"1"}
+              max={'30%'}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text + ' max:30%'}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'content'}
+              </div>
+            </ResizeItem>
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+```
+
+
+## API
+
+### Resizable
+
+单个伸缩框组件。
+
+| 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
+| --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
+| className | 类名                                                                          | string                  |            |        |
+| size   | Controls the size of the resizable box, supports both numeric and string (px/vw/vh/%) formats | [Size](#basic-usage-and-callbacks)                  |           |        |
+| defaultSize   | Sets the initial width and height, supports both numeric and string (px/vw/vh/%) formats | [Size](#basic-usage-and-callbacks)                  |           |        |
+| minWidth | Specifies the minimum width of the resizable box      |  string \| number                  |   |        |
+| maxWidth | Specifies the maximum width of the resizable box      |  string \| number                  |   |        |
+| minHeight | Specifies the minimum height of the resizable box      |  string \| number                  |   |        |
+| maxHeight | Specifies the maximum height of the resizable box      |  string \| number                  |   |     
+| lockAspectRatio | Locks the aspect ratio of the resizable box when true, using the initial width and height as the ratio    |  boolean \| number                  |   |        |
+| enable | Specifies the directions in which the resizable box can be resized. If not set, all directions are enabled by default      |    [Enable](#controlling-resize-directions) 
+| scale | The scale ratio of the resizable element      |   number                  |  1 |        |   
+| boundElement | Restricts the size of the resizable element within a specific element. Pass "parent" to set the parent element as the bounding element    | string                  |            |        |
+| handleNode     | Custom nodes for the drag handles in each direction             | [HandleNode](#customizing-corner-handler-styles)          |            |        |
+| handleStyle    | Styles for the drag handles in each direction             | [HandleNode](#customizing-corner-handler-styles)            |            |        |
+| handleClass   | Class names for the drag handles in each direction              | [HandleNode](#customizing-corner-handler-styles)            |            |        |
+| style |  | CSSProperties |      |
+| snapGap      | Specifies the minimum gap required to snap to the next target                        | number                  | 0       |  |
+| snap      | Specifies the pixel values to snap to during resizing. Both x and y are optional, allowing the definition of specific axes only                        | [Snap](#allowing-incremental-width-and-height-adjustment)                  | null       |  |
+| grid      | Specifies the increment to align to when resizing                          | \[number, number\]                  | \[1,1\]       |  |
+| onChange  | Callback during the dragging process                                                    | (e: Event; direction: String;size: Size) => void | -          |  |
+| onResizeStart  | Callback when resizing starts                                                  | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | Callback when resizing ends                                                   | (e: Event; direction: String) => void | -          |  |
+
+### ResizeGroup
+
+| 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
+| className   |                                                                                                                         | string                             |        |      |
+| direction | Specifies the resize direction within the group  | 'horizontal' \| 'vertical' | 'horizontal' |      |
+
+### ResizeHandler
+
+| 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
+| className   |                                                                                                                         | string                             |        |      |
+| style |  | CSSProperties |      |
+
+### ResizeItem
+
+| 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
+| --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
+| className |                                                                           | string                  |            |        |
+| defaultSize   | Used to set the initial width and height. **The string supports % and px units, and when the string is a pure number or a number is set directly, it represents the proportional allocation of the remaining space based on the value.**  | string \| number                  |           |        |
+| min | Specifies the minimum size of the resizable box (as percentage or pixel)     |  string                  |   |        |
+| max | Specifies the maximum size of the resizable box (as percentage or pixel)     |  string                  |   |        |   
+| style |  | CSSProperties |      |
+| onChange  | Callback during the dragging process                                                    | (e: Event; direction: String;size: Size) => void | -          |  |
+| onResizeStart  | Callback when resizing starts                                                  | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | Callback when resizing ends                                                   | (e: Event; direction: String) => void | -          |  |
+
+
+## Design Tokens
+
+<DesignToken/>

+ 735 - 0
content/show/resizable/index.md

@@ -0,0 +1,735 @@
+---
+localeCode: zh-CN
+order: 68
+category: 展示类
+title:  Resizable 伸缩框
+icon: doc-steps
+brief: 根据用户的鼠标拖拽,改变组件的大小,支持单个组件伸缩与组合伸缩
+---
+
+## 代码演示
+
+### 如何引入
+
+```jsx 
+import { Resizable } from '@douyinfe/semi-ui';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui'
+```
+
+### 单个组件 基本使用
+通过`defaultSize`设置初始大小,可以通过`onResizeStart` `onResize` `onResizeEnd`设置拖拽的回调
+
+```tsx
+interface Size {
+    width: string | number;
+    height: string | number;
+}
+```
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag edge to resize')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts_2 = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        onChange={() => { setText('resizing') }}
+        onResizeStart={() => Toast.info(opts_1)}
+        onResizeEnd={() => { Toast.info(opts_2); setText('Drag edge to resize') }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          {text}
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 控制伸缩方向
+通过设置`enable`的值开启/关闭特定伸缩方向,默认值均为`true`
+
+```tsx
+interface Enable {
+  left: Boolean;
+  right: Boolean;
+  top: Boolean;
+  bottom: Boolean;
+  topLeft: Boolean;
+  topRight: Boolean;
+  bottomLeft: Boolean;
+  bottomRight: Boolean;
+}
+```
+
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable, Switch, Typography } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [b, setB] = useState(false)
+  const { Title } = Typography;
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+        <div style={{ display: 'flex', alignItems: 'center', margin: 8 }}>
+          <Switch checked={b} onChange={setB}></Switch>
+            <Title heading={6} style={{ margin: 8 }}>
+                {b ? 'able' : 'disable'}
+            </Title>
+        </div>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        enable={{
+          left: b
+        }}
+        defaultSize={{
+          width: 200,
+          height: 200,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          {'enable.left:' + b}
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 设置变化比例
+
+通过`ratio`设置拖动和实际变化的比例 
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        ratio={2}
+        defaultSize={{
+          width: 200,
+          height: 200,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          ratio=2
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 锁定横纵比
+
+通过`lockAspectRatio`设置锁定横纵比,可以为`boolean`或`number`,为`number`时表示横纵比为`number`,为`true`时锁定初始横纵比
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginBottom: '10px' }}
+        defaultSize={{
+          width: 400,
+          height: 300,
+        }}
+        lockAspectRatio
+      >
+        <div style={{ marginLeft: '20%' }}>
+          lock
+        </div>
+      </Resizable>
+      <Resizable
+        style={{backgroundColor: 'rgba(var(--semi-grey-1), 1)'}}
+        defaultSize={{
+          width: 200,
+          height: 200 * 9 / 16,
+        }}
+        lockAspectRatio={16 / 9}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          16 / 9
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 设置最大,最小宽高 
+可通过 `maxHeight`,`maxWidth`,`minHeight`,`minWidth` 设置最大,最小宽高
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        maxWidth={200}
+        maxHeight={300}
+        minWidth={50}
+        minHeight={50}
+        defaultSize={{
+          width: 100,
+          height: 100,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          width在50到200之间,height在50到300之间
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 受控宽高
+
+可通过 `size` 控制元素的宽高
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [size, setSize] = useState({ width: 200, height: 300 });
+
+  const onChange = (() => {
+    let realSize = { width: size.width + 10, height: size.height + 10 };
+    setSize(realSize);
+  })
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={onChange}>set += 10</Button>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', marginTop: '10px' }}
+        defaultSize={{
+          width: 100,
+          height: 100,
+        }}
+        size={size}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          受控
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 设置缩放值
+
+通过设置 `scale`,整体缩放元素
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%', transform: 'scale(0.5)', transformOrigin: '0 0' }}>
+      <Resizable
+        style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        defaultSize={{
+          width: '60%',
+          height: '60%',
+        }}
+        scale={0.5}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          scale 0.5
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 根据元素限制元素宽高
+
+通过 boundElement 设置用于限制宽高的元素,支持 string('parent'|'window')
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '300px', height: '300px', border: 'var(--semi-color-border) 1px solid' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+        defaultSize={{
+          width: '60%',
+          height: 200,
+        }}
+        boundElement={'parent'}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          bound:parent
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+```
+
+### 自定义边角handler样式
+
+可通过 handleNode设置不同方向的拖动元素节点,可通过 handleStyle,handleClassName 设置不同方向上的样式
+
+```jsx
+type HandleNode = {
+  left: ReactNode;
+  right: ReactNode;
+  top: ReactNode;
+  bottom: ReactNode;
+  topLeft: ReactNode;
+  topRight: ReactNode;
+  bottomLeft: ReactNode;
+  bottomRight: ReactNode;
+}
+
+type HandleStyle = {
+  left: React.CSSProperties;
+  right: React.CSSProperties;
+  top: React.CSSProperties;
+  bottom: React.CSSProperties;
+  topLeft: React.CSSProperties;
+  topRight: React.CSSProperties;
+  bottomLeft: React.CSSProperties;
+  bottomRight: React.CSSProperties;
+}
+
+type HandleClass = {
+  left: string;
+  right: string;
+  top: string;
+  bottom: string;
+  topLeft: string;
+  topRight: string;
+  bottomLeft: string;
+  bottomRight: string;
+}
+```
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Resizable, Button } from '@douyinfe/semi-ui';
+function Demo() {
+    return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        handleNode={{
+          right: <div style={{
+            height: '100%',
+            display: 'flex',
+            alignItems: 'center',
+            width: 'fit-content',
+          }}><IconTransfer /></div>
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          right
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+```
+
+### 允许阶段性调整宽高
+
+可通过 grid ,snap 属性允许逐渐调整宽高。
+grid 属性用于指定调整大小应对齐的增量。默认为 [1, 1]。
+snap 属性用于指定调整大小时应对齐的绝对像素值。 x 和 y 都是可选的,允许仅包含要定义的轴。默认为空。
+以上两个参数可结合 snapGap使用,该参数用于指定移动到下一个目标所需的最小间隙。默认为 0,这意味着始终使用grid/snap 设定的目标。
+
+```tsx
+interface Snap {
+    x: number[];
+    y: number[];
+}
+```
+
+```jsx live=true 
+import React, { useState } from 'react';
+import { Resizable } from '@douyinfe/semi-ui';
+
+function Demo() {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        grid={100}
+        snapGap={20}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          snap
+        </div>
+      </Resizable>
+    </div >
+  );
+}
+```
+
+### 组合组件 基本使用
+<Notice type='primary' title='注意事项'>
+`ResizeGroup`的父元素需要具有主轴方向上的尺寸 
+最好不要为`ResizeItem`设置`padding`,会导致最小尺寸不符合预期,可以为子元素设置`padding`
+</Notice>
+
+
+通过`direction`设置伸缩方向,可选值为`horizontal`和`vertical`
+支持`onResizeStart` `onResize` `onResizeEnd`回调,支持`min` `max`设置最大最小宽高
+
+```jsx live=true dir="column"
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup, Toast } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag to resize')
+  return (
+    <div style={{ width: '1000px', height: '100px' }}>
+      <ResizeGroup direction='horizontal'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={'400px'}
+          min={'10%'}
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('Drag to resize') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text + " min:10%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={'20%'}
+          min={'10%'}
+          max={'30%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text + " min:10% max:30%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={'0.5'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+          defaultSize={1}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+```
+
+### 嵌套使用
+通过`direction`设置伸缩方向,可选值为`horizontal`和`vertical`
+
+```jsx live=true dir="column"
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag to resize')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '1000px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => Toast.info(opts_1)}
+          onResizeEnd={() => { Toast.info(opts); setText('Drag to resize') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {'header'}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"80%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"25%"}
+              onChange={() => { setText('resizing') }}
+              onResizeStart={() => Toast.info(opts_1)}
+              onResizeEnd={() => { Toast.info(opts); setText('Drag to resize') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"75%"}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+```
+```jsx live=true dir="column"
+import React, { useState } from 'react';
+import { ResizeItem, ResizeHandler, ResizeGroup } from '@douyinfe/semi-ui';
+
+function Demo() {
+  const [text, setText] = useState('Drag to resize')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '1000px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          defaultSize={"80%"}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"25%"}
+              min={'10%'}
+              max={'30%'}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text + ' min:10% max:30%'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ height: '100%' }}>
+                <ResizeGroup direction='vertical'>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                    defaultSize={'33%'}
+                    min={'10%'}
+                    onChange={() => { setText('resizing') }}
+                    onResizeEnd={() => { setText('Drag to resize') }}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text + " min:10%"}
+                    </div>
+                  </ResizeItem>
+                  <ResizeHandler></ResizeHandler>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                    defaultSize={'33%'}
+                    min={'10%'}
+                    max={'40%'}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text + " min:10% max:40%"}
+                    </div>
+                  </ResizeItem>
+                  <ResizeHandler></ResizeHandler>
+                  <ResizeItem
+                    style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+                  >
+                    <div style={{ marginLeft: '20%' }}>
+                      {text}
+                    </div>
+                  </ResizeItem>
+                </ResizeGroup>
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"1"}
+              max={'30%'}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text + ' max:30%'}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"50%"}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'content'}
+              </div>
+            </ResizeItem>
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+```
+
+## API 参考
+
+### Resizable
+
+单个伸缩框组件。
+
+| 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
+| --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
+| className | 类名                                                                          | string                  |            |        |
+| size   | 控制伸缩框的大小,支持数字和字符串(px/vw/vh/%)两种格式 | [Size](#基本使用与回调)                  |           |        |
+| defaultSize   | 用于设置初始宽高,支持数字和字符串(px/vw/vh/%)两种格式 | [Size](#基本使用与回调)                  |           |        |
+| minWidth | 指定伸缩框最小宽度      |  string \| number                  |   |        |
+| maxWidth | 指定伸缩框最大宽度      |  string \| number                  |   |        |
+| minHeight | 指定伸缩框最小高度      |  string \| number                  |   |        |
+| maxHeight | 指定伸缩框最大高度      |  string \| number                  |   |     
+| lockAspectRatio | 设置伸缩框横纵比,当为`true`时按照初始宽高锁定    |  boolean \| number                  |   |        |
+| enable | 指定伸缩框可以伸缩的方向,没有设置为 false,则默认允许该方向的拖动      |    [Enable](#控制伸缩方向) 
+| scale | 可伸缩元素被缩放的比例      |   number                  |  1 |        |   
+| boundElement | 用于限制可伸缩元素宽高的元素,传入 `parent` 设置父节点为限制节点    | string                  |            |        |
+| handleNode     | 用于设置拖拽处理元素各个方向的自定义节点             | [HandleNode](#自定义边角handler样式)          |            |        |
+| handleStyle    | 用于设置拖拽处理元素各个方向的样式              | [HandleStyles](#自定义边角handler样式)            |            |        |
+| handleClass  | 用于设置拖拽处理元素各个方向的类名称              | [HandleClasses](#自定义边角handler样式)            |            |        |
+| style | 样式 | CSSProperties |      |
+| snapGap      | 用于指定移动到下一个目标所需的最小间隙。                        | number                  | 0       |  |
+| snap      | 指定调整大小时应对齐的绝对像素值。 x 和 y 都是可选的,允许仅包含要定义的轴                        | [Snap](#允许阶段性调整宽高)                  | null       |  |
+| grid      | 指定调整大小应对齐的增量                           | \[number, number\]                  | \[1,1\]       |  |
+| onChange  | 拖拽过程中的回调                                                    | (e: Event; direction: String;size: Size) => void | -          |  |
+| onResizeStart  | 开始伸缩的回调                                                   | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | 结束伸缩的回调                                                    | (e: Event; direction: String) => void | -          |  |
+
+### ResizeGroup
+
+| 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
+| className   | 类名                                                                                                                        | string                             |        |      |
+| direction | 指定Group内的伸缩方向  | 'horizontal' \| 'vertical' | 'horizontal' |      |
+
+### ResizeHandler
+
+| 参数        | 说明                                                                                                                        | 类型                               | 默认值 | 版本 |
+| ----------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |
+| className   | 类名                                                                                                                        | string                             |        |      |
+| style | 样式 | CSSProperties |      |
+
+### ResizeItem
+
+
+| 参数      | 说明                                                                          | 类型                    | 默认值     | 版本   |
+| --------- | ----------------------------------------------------------------------------- | ----------------------- | ---------- | ------ |
+| className | 类名                                                                          | string                  |            |        |
+| defaultSize   | 用于设置初始宽高,**字符串支持%和px单位,当字符串为纯数字或直接设置数字时表示按照值的比例分配剩余空间** | string \| number                 |           |        |
+| min | 指定伸缩框最小尺寸(百分比或像素值)      |  string                   |   |        |
+| max | 指定伸缩框最大尺寸(百分比或像素值)     |  string                   |   |        |
+| style | 样式 | CSSProperties |      |
+| onChange  | 拖拽过程中的回调                                                    | (e: Event; direction: String;size: Size) => void | -          |  |
+| onResizeStart  | 开始伸缩的回调                                                   | (e: Event; direction: String) => void | -          |  |
+| onResizeEnd  | 结束伸缩的回调                                                    | (e: Event; direction: String) => void | -          |  |
+
+
+## 设计变量
+<DesignToken/>

+ 1 - 1
content/show/scrolllist/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 68
+order: 69
 category: Show
 title:  ScrollList
 subTitle: ScrollList

+ 1 - 1
content/show/scrolllist/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 68
+order: 69
 category: 展示类
 title: ScrollList 滚动列表
 icon: doc-scrolllist

+ 1 - 1
content/show/sidesheet/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 69
+order: 70
 category: Show
 title: SideSheet
 subTitle: SideSheet

+ 1 - 1
content/show/sidesheet/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 69
+order: 70
 category: 展示类
 title: SideSheet 滑动侧边栏
 icon: doc-sidesheet

+ 1 - 1
content/show/table/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 70
+order: 71
 category: Show
 title: Table
 subTitle: Table

+ 1 - 1
content/show/table/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 70
+order: 71
 category: 展示类
 title: Table 表格
 icon: doc-table

+ 1 - 1
content/show/tag/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 71
+order: 72
 category: Show
 title: Tag
 subTitle: Tag

+ 1 - 1
content/show/tag/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 71
+order: 72
 category: 展示类
 title: Tag 标签
 icon: doc-tag

+ 1 - 1
content/show/timeline/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 72
+order: 73
 category: Show
 title:  Timeline
 subTitle: Timeline

+ 1 - 1
content/show/timeline/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 72
+order: 73
 category: 展示类
 title: Timeline 时间轴
 icon: doc-timeline

+ 1 - 1
content/show/tooltip/index-en-US.md

@@ -1,6 +1,6 @@
 ---
 localeCode: en-US
-order: 73
+order: 74
 category: Show
 title: Tooltip
 subTitle: Tooltip

+ 1 - 1
content/show/tooltip/index.md

@@ -1,6 +1,6 @@
 ---
 localeCode: zh-CN
-order: 73
+order: 74
 category: 展示类
 title: Tooltip 工具提示
 icon: doc-tooltip

+ 13 - 0
packages/semi-foundation/resizable/constants.ts

@@ -0,0 +1,13 @@
+import { BASE_CLASS_PREFIX } from "../base/constants";
+
+const cssClasses = {
+    PREFIX: `${BASE_CLASS_PREFIX}-resizable`,
+} as const;
+
+const strings = {
+};
+
+export { cssClasses, strings };
+
+
+

+ 31 - 0
packages/semi-foundation/resizable/foundation.ts

@@ -0,0 +1,31 @@
+import { 
+    ResizableHandlerAdapter, 
+    ResizableHandlerFoundation, 
+    ResizableFoundation, 
+    ResizableAdapter } 
+    from './single';
+
+export { 
+    ResizableHandlerAdapter, 
+    ResizableHandlerFoundation, 
+    ResizableFoundation, 
+    ResizableAdapter 
+}; 
+
+import { 
+    ResizeGroupAdapter,
+    ResizeItemAdapter,
+    ResizeHandlerAdapter,
+    ResizeGroupFoundation,
+    ResizeItemFoundation,
+    ResizeHandlerFoundation
+} from './group';
+
+export {
+    ResizeGroupAdapter,
+    ResizeItemAdapter,
+    ResizeHandlerAdapter,
+    ResizeGroupFoundation,
+    ResizeItemFoundation,
+    ResizeHandlerFoundation
+};

+ 293 - 0
packages/semi-foundation/resizable/group/index.ts

@@ -0,0 +1,293 @@
+import { getItemDirection, getPixelSize } from "../utils";
+import BaseFoundation, { DefaultAdapter } from '../../base/foundation';
+import { ResizeStartCallback, ResizeCallback } from "../singleConstants";
+import { adjustNewSize, judgeConstraint, getOffset } from "../utils";
+export interface ResizeHandlerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    registerEvents: () => void;
+    unregisterEvents: () => void
+}
+
+export class ResizeHandlerFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizeHandlerAdapter<P, S>, P, S> {
+    constructor(adapter: ResizeHandlerAdapter<P, S>) {
+        super({ ...adapter });
+    }
+
+    init(): void {
+        this._adapter.registerEvents();
+    }
+
+    destroy(): void {
+        this._adapter.unregisterEvents();
+    }
+}
+
+export interface ResizeItemAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+}
+
+export class ResizeItemFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizeItemAdapter<P, S>, P, S> {
+    constructor(adapter: ResizeItemAdapter<P, S>) {
+        super({ ...adapter });
+    }
+
+    init(): void {
+    }
+
+    destroy(): void {
+    }
+}
+
+export interface ResizeGroupAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    getGroupRef: () => HTMLDivElement | null;
+    getItem: (index: number) => HTMLDivElement;
+    getItemCount: () => number;
+    getHandler: (index: number) => HTMLDivElement;
+    getHandlerCount: () => number;
+    getItemMin: (index: number) => string;
+    getItemMax: (index: number) => string;
+    getItemStart: (index: number) => ResizeStartCallback;
+    getItemChange: (index: number) => ResizeCallback;
+    getItemEnd: (index: number) => ResizeCallback;
+    getItemDefaultSize: (index: number) => string | number;
+    registerEvents: () => void;
+    unregisterEvents: () => void
+}
+
+export class ResizeGroupFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizeGroupAdapter<P, S>, P, S> {
+    constructor(adapter: ResizeGroupAdapter<P, S>) {
+        super({ ...adapter });
+    }
+
+    get groupRef(): HTMLDivElement | null {
+        return this._adapter.getGroupRef();
+    }
+
+    direction: 'horizontal' | 'vertical'
+    itemMinusMap: Map<number, number>;
+    totalMinus: number;
+    avaliableSize: number;
+
+
+    init(): void {
+        this.direction = this.getProp('direction');
+        this.itemMinusMap = new Map();
+        this.calculateSpace();
+    }
+    get window(): Window | null {
+        return this.groupRef.ownerDocument.defaultView as Window ?? null;
+    }
+
+    
+    registerEvents = () => {
+        this._adapter.registerEvents();
+    }
+
+    unregisterEvents = () => {
+        this._adapter.unregisterEvents();
+    }
+
+    onResizeStart = (handlerIndex: number, e: MouseEvent) => { // handler ref
+        let { clientX, clientY } = e;
+        let lastItem = this._adapter.getItem(handlerIndex), nextItem = this._adapter.getItem(handlerIndex + 1);
+        let lastOffset: number, nextOffset: number;
+        // offset caused by padding and border
+        const lastStyle = this.window.getComputedStyle(lastItem);
+        const nextStyle = this.window.getComputedStyle(nextItem);
+
+        lastOffset = getOffset(lastStyle, this.direction);
+        nextOffset = getOffset(nextStyle, this.direction);
+        const states = this.getStates();
+        this.setState({
+            isResizing: true,
+            originalPosition: {
+                x: clientX,
+                y: clientY,
+                lastItemSize: (this.direction === 'horizontal' ? lastItem.offsetWidth : lastItem.offsetHeight),
+                nextItemSize: (this.direction === 'horizontal' ? nextItem.offsetWidth : nextItem.offsetHeight),
+                lastOffset,
+                nextOffset,
+            },
+            backgroundStyle: {
+                ...states.backgroundStyle,
+                cursor: this.window.getComputedStyle(e.target as HTMLElement).cursor || 'auto',
+            },
+            curHandler: handlerIndex
+        } as any);
+        this.registerEvents();
+
+        let lastStart = this._adapter.getItemStart(handlerIndex), 
+            nextStart = this._adapter.getItemStart(handlerIndex + 1);
+        let [lastDir, nextDir] = getItemDirection(this.direction);
+        if (lastStart) {
+            lastStart(e, lastDir as any);
+        }
+        if (nextStart) {
+            nextStart(e, nextDir as any);
+        }
+    }
+
+
+    onResizing = (e: MouseEvent) => {
+        const state = this.getStates();
+        if (!state.isResizing) {
+            return;
+        }
+        const { curHandler, originalPosition } = state;
+        let { x: initX, y: initY, lastItemSize, nextItemSize, lastOffset, nextOffset } = originalPosition;
+        let { clientX, clientY } = e;
+
+        const props = this.getProps();
+        const { direction } = props;
+        let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
+        let parentSize = this.direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
+        let availableSize = parentSize - this.totalMinus;
+
+        let delta = direction === 'horizontal' ? (clientX - initX) : (clientY - initY);
+        let lastNewSize = lastItemSize + delta;
+        let nextNewSize = nextItemSize - delta;
+
+        // 判断是否超出限制
+        let lastFlag = judgeConstraint(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), availableSize, lastOffset),
+            nextFlag = judgeConstraint(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), availableSize, nextOffset);
+
+        if (lastFlag) {
+            lastNewSize = adjustNewSize(lastNewSize, this._adapter.getItemMin(curHandler), this._adapter.getItemMax(curHandler), availableSize, lastOffset);
+            nextNewSize = lastItemSize + nextItemSize - lastNewSize;
+        }
+
+        if (nextFlag) {
+            nextNewSize = adjustNewSize(nextNewSize, this._adapter.getItemMin(curHandler + 1), this._adapter.getItemMax(curHandler + 1), availableSize, nextOffset);
+            lastNewSize = lastItemSize + nextItemSize - nextNewSize;
+        }
+
+        if (direction === 'horizontal') {     
+            lastItem.style.width = (lastNewSize) / parentSize * 100 + '%';
+            nextItem.style.width = (nextNewSize) / parentSize * 100 + '%';
+        } else if (direction === 'vertical') {
+            lastItem.style.height = (lastNewSize) / parentSize * 100 + '%';
+            nextItem.style.height = (nextNewSize) / parentSize * 100 + '%';
+        }
+
+        let lastFunc = this._adapter.getItemChange(curHandler),
+            nextFunc = this._adapter.getItemChange(curHandler + 1);
+        let [lastDir, nextDir] = getItemDirection(this.direction);
+        if (lastFunc) {
+            lastFunc( { width: lastItem.offsetWidth, height: lastItem.offsetHeight }, e, lastDir as any);
+        }
+        if (nextFunc) {
+            nextFunc( { width: nextItem.offsetWidth, height: nextItem.offsetHeight }, e, nextDir as any);
+        }
+    }
+
+    onResizeEnd = (e: MouseEvent) => {
+        const { curHandler } = this.getStates();
+        let lastItem = this._adapter.getItem(curHandler), nextItem = this._adapter.getItem(curHandler + 1);
+        let lastFunc = this._adapter.getItemEnd(curHandler),
+            nextFunc = this._adapter.getItemEnd(curHandler + 1);
+        let [lastDir, nextDir] = getItemDirection(this.direction);
+        if (lastFunc) {
+            lastFunc( { width: lastItem.offsetWidth, height: lastItem.offsetHeight }, e, lastDir as any);
+        }
+        if (nextFunc) {
+            nextFunc( { width: nextItem.offsetWidth, height: nextItem.offsetHeight }, e, nextDir as any);
+        }
+        this.setState({
+            isResizing: false,
+            curHandler: null
+        } as any);
+        this.unregisterEvents();
+    }
+
+    calculateSpace = () => {
+        const props = this.getProps();
+        const { direction } = props;
+
+        // calculate accurate space for group item
+        let handlerSizes = new Array(this._adapter.getHandlerCount()).fill(0);
+        let groupSize = direction === 'horizontal' ? this.groupRef.offsetWidth : this.groupRef.offsetHeight;
+        this.totalMinus = 0;
+        for (let i = 0; i < this._adapter.getHandlerCount(); i++) {
+            let handlerSize = direction === 'horizontal' ? this._adapter.getHandler(i).offsetWidth : this._adapter.getHandler(i).offsetHeight;
+            handlerSizes[i] = handlerSize;
+            this.totalMinus += handlerSize;
+        }
+        
+        // allocate size for items which don't have default size
+        let totalSizePercent = 0;
+        let undefineLoc: Map<number, number> = new Map(), undefinedTotal = 0; // proportion
+
+        for (let i = 0; i < this._adapter.getItemCount(); i++) {
+            if (i === 0) {
+                this.itemMinusMap.set(i, handlerSizes[i] / 2);
+            } else if (i === this._adapter.getItemCount() - 1) {
+                this.itemMinusMap.set(i, handlerSizes[i - 1] / 2);
+            } else {
+                this.itemMinusMap.set(i, handlerSizes[i - 1] / 2 + handlerSizes[i] / 2);
+            }
+            const child = this._adapter.getItem(i);
+            let minSize = this._adapter.getItemMin(i), maxSize = this._adapter.getItemMax(i);
+            let minSizePercent = minSize ? getPixelSize(minSize, groupSize) / groupSize * 100 : 0,
+                maxSizePercent = maxSize ? getPixelSize(maxSize, groupSize) / groupSize * 100 : 100;
+            if (minSizePercent > maxSizePercent) {
+                console.warn('[Semi ResizableItem]: min size bigger than max size');
+            }    
+
+            let defaultSize = this._adapter.getItemDefaultSize(i);
+            if (defaultSize) {
+                let itemSizePercent: number;
+                if (typeof defaultSize === 'string') {
+                    if (defaultSize.endsWith('%')) {
+                        itemSizePercent = parseFloat(defaultSize.slice(0, -1));
+                    } else if (defaultSize.endsWith('px')) {
+                        itemSizePercent = parseFloat(defaultSize.slice(0, -2)) / groupSize * 100;
+                    } else if (/^-?\d+(\.\d+)?$/.test(defaultSize)) {
+                        // 仅由数字组成,表示按比例分配剩下空间
+                        undefineLoc.set(i, parseFloat(defaultSize));
+                        undefinedTotal += parseFloat(defaultSize);
+                        continue;
+                    }
+                } else {
+                    undefineLoc.set(i, defaultSize);
+                    undefinedTotal += defaultSize;
+                    continue;
+                }
+                
+
+                totalSizePercent += itemSizePercent;
+                
+                if (direction === 'horizontal') {
+                    child.style.width = `calc(${itemSizePercent}% - ${this.itemMinusMap.get(i)}px)`;
+                } else {
+                    child.style.height = `calc(${itemSizePercent}% - ${this.itemMinusMap.get(i)}px)`;
+                }
+                
+                if (itemSizePercent < minSizePercent) {
+                    console.warn('[Semi ResizableGroup]: item size smaller than min size');
+                } 
+                if (itemSizePercent > maxSizePercent) {
+                    console.warn('[Semi ResizableGroup]: item size bigger than max size');
+                }
+            } else {
+                undefineLoc.set(i, 1);
+                undefinedTotal += 1;
+            }
+        }
+        let undefineSizePercent = 100 - totalSizePercent;
+        if (totalSizePercent > 100) {
+            console.warn('[Semi ResizableGroup]: total Size bigger than 100%');
+            undefineSizePercent = 10; // 如果总和超过100%,则保留10%的空间均分给未定义的item
+        }
+    
+        undefineLoc.forEach((value, key) => {
+            const child = this._adapter.getItem(key);
+            if (direction === 'horizontal') {
+                child.style.width = `calc(${undefineSizePercent / undefinedTotal * value}% - ${this.itemMinusMap.get(key)}px)`;
+            } else {
+                child.style.height = `calc(${undefineSizePercent / undefinedTotal * value}% - ${this.itemMinusMap.get(key)}px)`;
+            }
+        });
+    }
+
+    destroy(): void {
+        
+    }
+}

+ 25 - 0
packages/semi-foundation/resizable/groupConstants.ts

@@ -0,0 +1,25 @@
+// group
+const rowStyleBase = {
+    width: '100%',
+    height: '8px',
+    flexShrink: 0,
+    margin: '0',
+    cursor: 'row-resize',
+} as const;
+const colStyleBase = {
+    width: '8px',
+    flexShrink: 0,
+    height: '100%',
+    margin: '0',
+    cursor: 'col-resize',
+} as const;
+
+export const directionStyles = {
+    horizontal: {
+        ...colStyleBase,
+    },
+    vertical: {
+        ...rowStyleBase,
+    }
+} as const;
+

+ 39 - 0
packages/semi-foundation/resizable/index.scss

@@ -0,0 +1,39 @@
+$module: #{$prefix}-resizable;
+
+.#{$module} {
+    &-resizable {
+        position: relative;
+        box-sizing: border-box;
+        flex-shrink: 0;
+    }
+
+    &-resizableHandler {
+        position: absolute;
+        user-select: none;
+        z-index: $z-resizable_handler;
+    }
+    
+    &-group {
+        display: flex;
+        position: relative;
+        box-sizing: border-box;
+        height: 100%;
+        width: 100%;
+    }
+
+    &-item {
+        position: relative;
+        box-sizing: border-box;
+        flex-shrink: 0;
+    }
+
+    &-handler {
+        user-select: none;
+        z-index: $z-resizable_handler;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background-color: var(--semi-color-fill-0);
+        opacity: 1;
+    }
+}

+ 629 - 0
packages/semi-foundation/resizable/single/index.ts

@@ -0,0 +1,629 @@
+import BaseFoundation, { DefaultAdapter } from '../../base/foundation';
+import { DEFAULT_SIZE, Size, NumberSize, Direction, NewSize } from "../singleConstants";
+import { getStringSize, getNumberSize, has, calculateNewMax, findNextSnap, snap, clamp } from "../utils";
+export interface ResizableHandlerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    registerEvent: () => void;
+    unregisterEvent: () => void
+}
+
+export class ResizableHandlerFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizableHandlerAdapter<P, S>, P, S> {
+    constructor(adapter: ResizableHandlerAdapter<P, S>) {
+        super({ ...adapter });
+    }
+
+    init(): void {
+        this._adapter.registerEvent();
+    }
+
+    onMouseDown = (e: MouseEvent) => {
+        this.getProp('onResizeStart')(e, this.getProp('direction'));
+    };
+
+    destroy(): void {
+        this._adapter.unregisterEvent();
+    }
+}
+
+export interface ResizableAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    getResizable: () => HTMLDivElement | null;
+    registerEvent: () => void;
+    unregisterEvent: () => void
+}
+
+export class ResizableFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ResizableAdapter<P, S>, P, S> {
+    constructor(adapter: ResizableAdapter<P, S>) {
+        super({ ...adapter });
+    }
+
+    init(): void {
+        if (!this.resizable || !this.window) {
+            return;
+        }
+        const flexBasis = this.window.getComputedStyle(this.resizable).flexBasis;
+        
+        this.setState({
+            width: this.propSize.width,
+            height: this.propSize.height,
+            flexBasis: flexBasis !== 'auto' ? flexBasis : undefined,
+        } as any);
+
+        this.onResizeStart = this.onResizeStart.bind(this);
+        this.onMouseMove = this.onMouseMove.bind(this);
+        this.onMouseUp = this.onMouseUp.bind(this);
+    }
+
+    flexDirection?: 'row' | 'column';
+
+    lockAspectRatio = 1;
+    resizable: HTMLElement | null = null;
+
+    parentLeft = 0;
+    parentTop = 0;
+
+    boundaryLeft = 0;
+    boundaryRight = 0;
+    boundaryTop = 0;
+    boundaryBottom = 0;
+
+    targetLeft = 0;
+    targetTop = 0;
+
+    get parent(): HTMLElement | null {
+        if (!this.resizable) {
+            return null;
+        }
+        return this.resizable.parentNode as HTMLElement;
+    }
+
+    get window(): Window | null {
+        if (!this.resizable) {
+            return null;
+        }
+        if (!this.resizable.ownerDocument) {
+            return null;
+        }
+        return this.resizable.ownerDocument.defaultView as Window;
+    }
+
+    get propSize(): Size {
+        const porps = this.getProps();
+        return porps.size || porps.defaultSize || DEFAULT_SIZE;
+    }
+
+    get size(): NumberSize {
+        let width = 0;
+        let height = 0;
+        if (this.resizable && this.window) {
+            width = this.resizable.offsetWidth ;
+            height = this.resizable.offsetHeight ;    
+        }
+        return { width, height };
+    }
+
+    get sizeStyle(): { width: string; height: string } {
+        const size = this.getProp('size');
+        const getSize = (property: 'width' | 'height'): string => {
+            const value = this.getStates()[property];
+            if (typeof value === 'undefined' || value === 'auto') {
+                return 'auto';
+            }
+            const propSizeValue = this.propSize?.[property];
+
+            if (propSizeValue?.toString().endsWith('%')) {
+                if (value.toString().endsWith('%')) {
+                    return value.toString();
+                }
+
+                const parentSize = this.getParentSize();
+                const numberValue = Number(value.toString().replace('px', ''));
+                const percentValue = (numberValue / parentSize[property]) * 100;
+
+                return `${percentValue}%`;
+            }
+
+            return getStringSize(value);
+        };
+
+        const isResizing = this.getStates().isResizing;
+
+        const width = size && typeof size.width !== 'undefined' && !isResizing
+            ? getStringSize(size.width)
+            : getSize('width');
+
+        const height = size && typeof size.height !== 'undefined' && !isResizing
+            ? getStringSize(size.height)
+            : getSize('height');
+
+        return { width, height };
+    }
+
+    getParentSize(): { width: number; height: number } {
+        const appendPseudo = () => {
+            if (!this.resizable || !this.window) {
+                return null;
+            }
+            const parent = this.parent;
+            if (!parent) {
+                return null;
+            }
+            const pseudoEle = this.window.document.createElement('div');
+            pseudoEle.style.width = '100%';
+            pseudoEle.style.height = '100%';
+            pseudoEle.style.position = 'absolute';
+            pseudoEle.style.transform = 'scale(0, 0)';
+            pseudoEle.style.left = '0';
+            pseudoEle.style.flex = '0 0 100%';
+            parent.appendChild(pseudoEle);
+            return pseudoEle;
+        };
+
+        const removePseudo = (pseudo: HTMLElement) => {
+            const parent = this.parent;
+            if (!parent) {
+                return;
+            }
+            parent.removeChild(pseudo);
+        };
+        if (!this.parent) {
+            if (!this.window) {
+                return { width: 0, height: 0 };
+            }
+            return { width: this.window.innerWidth, height: this.window.innerHeight };
+        }
+        const pseudoElement = appendPseudo();
+
+        if (!pseudoElement) {
+            return { width: 0, height: 0 };
+        }
+
+        let flexWrapChanged = false;
+        const originalFlexWrap = this.parent.style.flexWrap;
+
+        if (originalFlexWrap !== 'wrap') {
+            flexWrapChanged = true;
+            this.parent.style.flexWrap = 'wrap';
+        }
+
+        pseudoElement.style.position = 'relative';
+        pseudoElement.style.minWidth = '100%';
+        pseudoElement.style.minHeight = '100%';
+
+        const size = {
+            width: pseudoElement.offsetWidth,
+            height: pseudoElement.offsetHeight,
+        };
+
+        if (flexWrapChanged) {
+            this.parent.style.flexWrap = originalFlexWrap;
+        }
+
+        removePseudo(pseudoElement);
+        return size;
+    }
+
+    registerEvents() {
+        this._adapter.registerEvent();
+    }
+
+    unregisterEvents() {
+        this._adapter.unregisterEvent();
+    }
+
+    getCssPropertySize(newSize: number | string, property: 'width' | 'height'): number | string {
+        const propSizeValue = this.propSize?.[property];
+        const state = this.getStates();
+
+        const isAutoSize =
+            state[property] === 'auto' &&
+            state.original[property] === newSize &&
+            (typeof propSizeValue === 'undefined' || propSizeValue === 'auto');
+
+        return isAutoSize ? 'auto' : newSize;
+    }
+
+
+    calBoundaryMax(maxWidth?: number, maxHeight?: number) {
+        const { boundsByDirection } = this.getProps();
+        const { direction } = this.getStates();
+
+        const isWidthConstrained = boundsByDirection && has('left', direction);
+        const isHeightConstrained = boundsByDirection && has('top', direction);
+
+        let maxWidthConstraint: number;
+        let maxHeightConstraint: number;
+
+        const props = this.getProps();
+
+        if (props.boundElement === 'parent') {
+            const parentElement = this.parent;
+            if (parentElement) {
+                maxWidthConstraint = isWidthConstrained
+                    ? this.boundaryRight - this.parentLeft
+                    : parentElement.offsetWidth + (this.parentLeft - this.boundaryLeft);
+
+                maxHeightConstraint = isHeightConstrained
+                    ? this.boundaryBottom - this.parentTop
+                    : parentElement.offsetHeight + (this.parentTop - this.boundaryTop);
+            }
+        } else if (props.boundElement === 'window' && this.window) {
+            maxWidthConstraint = isWidthConstrained
+                ? this.boundaryRight
+                : this.window.innerWidth - this.boundaryLeft;
+            maxHeightConstraint = isHeightConstrained
+                ? this.boundaryBottom
+                : this.window.innerHeight - this.boundaryTop;
+        } else if (props.boundElement) {
+            const boundary = props.boundElement;
+
+            maxWidthConstraint = isWidthConstrained
+                ? this.boundaryRight - this.targetLeft
+                : boundary.offsetWidth + (this.targetLeft - this.boundaryLeft);
+            maxHeightConstraint = isHeightConstrained
+                ? this.boundaryBottom - this.targetTop
+                : boundary.offsetHeight + (this.targetTop - this.boundaryTop);
+        }
+
+        if (maxWidthConstraint && Number.isFinite(maxWidthConstraint)) {
+            maxWidth = maxWidth && maxWidth < maxWidthConstraint ? maxWidth : maxWidthConstraint;
+        }
+        if (maxHeightConstraint && Number.isFinite(maxHeightConstraint)) {
+            maxHeight = maxHeight && maxHeight < maxHeightConstraint ? maxHeight : maxHeightConstraint;
+        }
+
+        return { maxWidth, maxHeight };
+    }
+
+    calDirectionSize(clientX: number, clientY: number) {
+        const props = this.getProps();
+        const scale = props.scale || 1;
+        let aspectRatio = props.ratio;
+        const [resizeRatioX, resizeRatioY] = Array.isArray(aspectRatio) ? aspectRatio : [aspectRatio, aspectRatio];
+
+        const { direction, original } = this.getStates();
+        const { lockAspectRatio, lockAspectRatioExtraHeight = 0, lockAspectRatioExtraWidth = 0 } = props;
+
+        let newWidth = original.width;
+        let newHeight = original.height;
+
+        const calculateNewWidth = (deltaX: number) => original.width + (deltaX * resizeRatioX) / scale;
+        const calculateNewHeight = (deltaY: number) => original.height + (deltaY * resizeRatioY) / scale;
+
+        if (has('top', direction)) {
+            newHeight = calculateNewHeight(original.y - clientY);
+            if (lockAspectRatio) {
+                newWidth = (newHeight - lockAspectRatioExtraHeight) * this.lockAspectRatio + lockAspectRatioExtraWidth;
+            }
+        }
+        if (has('bottom', direction)) {
+            newHeight = calculateNewHeight(clientY - original.y);
+            if (lockAspectRatio) {
+                newWidth = (newHeight - lockAspectRatioExtraHeight) * this.lockAspectRatio + lockAspectRatioExtraWidth;
+            }
+        }
+        if (has('right', direction)) {
+            newWidth = calculateNewWidth(clientX - original.x);
+            if (lockAspectRatio) {
+                newHeight = (newWidth - lockAspectRatioExtraWidth) / this.lockAspectRatio + lockAspectRatioExtraHeight;
+            }
+        }
+        if (has('left', direction)) {
+            newWidth = calculateNewWidth(original.x - clientX);
+            if (lockAspectRatio) {
+                newHeight = (newWidth - lockAspectRatioExtraWidth) / this.lockAspectRatio + lockAspectRatioExtraHeight;
+            }
+        }
+
+        return { newWidth, newHeight };
+    }
+
+    calAspectRatioSize(
+        newWidth: number,
+        newHeight: number,
+        max: { width?: number; height?: number },
+        min: { width?: number; height?: number },
+    ) {
+        const { lockAspectRatio, lockAspectRatioExtraHeight = 0, lockAspectRatioExtraWidth = 0 } = this.getProps();
+
+        const minWidth = typeof min.width === 'undefined' ? 10 : min.width;
+        const maxWidth = typeof max.width === 'undefined' || max.width < 0 ? newWidth : max.width;
+        const minHeight = typeof min.height === 'undefined' ? 10 : min.height;
+        const maxHeight = typeof max.height === 'undefined' || max.height < 0 ? newHeight : max.height;
+
+        if (lockAspectRatio) {
+            const adjustedMinWidth = (minHeight - lockAspectRatioExtraHeight) * this.lockAspectRatio + lockAspectRatioExtraWidth;
+            const adjustedMaxWidth = (maxHeight - lockAspectRatioExtraHeight) * this.lockAspectRatio + lockAspectRatioExtraWidth;
+            const adjustedMinHeight = (minWidth - lockAspectRatioExtraWidth) / this.lockAspectRatio + lockAspectRatioExtraHeight;
+            const adjustedMaxHeight = (maxWidth - lockAspectRatioExtraWidth) / this.lockAspectRatio + lockAspectRatioExtraHeight;
+
+            const lockedMinWidth = Math.max(minWidth, adjustedMinWidth);
+            const lockedMaxWidth = Math.min(maxWidth, adjustedMaxWidth);
+            const lockedMinHeight = Math.max(minHeight, adjustedMinHeight);
+            const lockedMaxHeight = Math.min(maxHeight, adjustedMaxHeight);
+
+            newWidth = clamp(newWidth, lockedMinWidth, lockedMaxWidth);
+            newHeight = clamp(newHeight, lockedMinHeight, lockedMaxHeight);
+        } else {
+            newWidth = clamp(newWidth, minWidth, maxWidth);
+            newHeight = clamp(newHeight, minHeight, maxHeight);
+        }
+        return { newWidth, newHeight };
+    }
+
+    setBoundary() {
+        const props = this.getProps();
+
+        // Set parent boundary
+        if (props.boundElement === 'parent') {
+            const parentElement = this.parent;
+            if (parentElement) {
+                const parentRect = parentElement.getBoundingClientRect();
+                this.parentLeft = parentRect.left;
+                this.parentTop = parentRect.top;
+            }
+        }
+
+        // Set target (HTML element) boundary
+        if (props.boundElement && typeof props.boundElement !== 'string') {
+            const targetRect = props.boundElement.getBoundingClientRect();
+            this.targetLeft = targetRect.left;
+            this.targetTop = targetRect.top;
+        }
+
+        // Set resizable boundary
+        if (this.resizable) {
+            const { left, top, right, bottom } = this.resizable.getBoundingClientRect();
+            this.boundaryLeft = left;
+            this.boundaryRight = right;
+            this.boundaryTop = top;
+            this.boundaryBottom = bottom;
+        }
+    }
+
+
+    onResizeStart = (e: MouseEvent, direction: Direction) => {
+        this.resizable = this._adapter.getResizable();
+        if (!this.resizable || !this.window) {
+            return;
+        }
+
+        const { clientX, clientY } = e;
+        const props = this.getProps();
+        const states = this.getStates();
+
+        // Call onResizeStart callback if defined
+        if (props.onResizeStart) {
+            const shouldContinue = props.onResizeStart(e, direction);
+            if (shouldContinue === false) {
+                return;
+            }
+        }
+
+        // Update state with new size if defined
+        const { size } = props;
+        if (size) {
+            const { height, width } = size;
+            const { height: currentHeight, width: currentWidth } = states;
+
+            if (height !== undefined && height !== currentHeight) {
+                this.setState({ height } as any);
+            }
+
+            if (width !== undefined && width !== currentWidth) {
+                this.setState({ width } as any);
+            }
+        }
+
+        // Handle aspect ratio locking
+        this.lockAspectRatio = typeof props.lockAspectRatio === 'number'
+            ? props.lockAspectRatio
+            : this.size.width / this.size.height;
+
+        // Determine flexBasis if applicable
+        let flexBasis: string | undefined;
+        const computedStyle = this.window.getComputedStyle(this.resizable);
+        if (computedStyle.flexBasis !== 'auto') {
+            const parent = this.parent;
+            if (parent) {
+                const parentStyle = this.window.getComputedStyle(parent);
+                this.flexDirection = parentStyle.flexDirection.startsWith('row') ? 'row' : 'column';
+                flexBasis = computedStyle.flexBasis;
+            }
+        }
+
+        // Set bounding rectangle and register events
+        this.setBoundary();
+        this.registerEvents();
+
+        // Update state with initial resize values
+        const state = {
+            original: {
+                x: clientX,
+                y: clientY,
+                width: this.size.width,
+                height: this.size.height,
+            },
+            isResizing: true,
+            backgroundStyle: {
+                ...states.backgroundStyle,
+                cursor: this.window.getComputedStyle(e.target as HTMLElement).cursor || 'auto',
+            },
+            direction,
+            flexBasis,
+        };
+
+        this.setState(state as any);
+    }
+
+
+    onMouseMove = (event: MouseEvent) => {
+        const states = this.getStates();
+        const props = this.getProps();
+
+        if (!states.isResizing || !this.resizable || !this.window) {
+            return;
+        }
+
+        const { clientX, clientY } = event;
+        const { direction, original, width, height } = states;
+        const parentSize = this.getParentSize();
+        let { maxWidth, maxHeight, minWidth, minHeight } = props;
+
+        // Calculate max and min dimensions
+        const maxBounds = calculateNewMax(
+            parentSize,
+            this.window.innerWidth,
+            this.window.innerHeight,
+            maxWidth,
+            maxHeight,
+            minWidth,
+            minHeight
+        );
+
+        maxWidth = maxBounds.maxWidth;
+        maxHeight = maxBounds.maxHeight;
+        minWidth = maxBounds.minWidth;
+        minHeight = maxBounds.minHeight;
+
+        // Calculate new size based on direction
+        let { newWidth, newHeight }: NewSize = this.calDirectionSize(clientX, clientY);
+
+        // Apply boundary constraints
+        const boundaryMax = this.calBoundaryMax(maxWidth, maxHeight);
+        newWidth = getNumberSize(newWidth, parentSize.width, this.window.innerWidth, this.window.innerHeight);
+        newHeight = getNumberSize(newHeight, parentSize.height, this.window.innerWidth, this.window.innerHeight);
+
+        // Apply snapping
+        if (props.snap) {
+            if (props.snap.x) {
+                newWidth = findNextSnap(newWidth, props.snap.x, props.snapGap);
+            }
+            if (props.snap.y) {
+                newHeight = findNextSnap(newHeight, props.snap.y, props.snapGap);
+            }
+        }
+
+        // Adjust size based on aspect ratio
+        const sizeFromAspectRatio = this.calAspectRatioSize(
+            newWidth,
+            newHeight,
+            { width: boundaryMax.maxWidth, height: boundaryMax.maxHeight },
+            { width: minWidth, height: minHeight }
+        );
+        newWidth = sizeFromAspectRatio.newWidth;
+        newHeight = sizeFromAspectRatio.newHeight;
+
+        // Apply grid snapping if defined
+        if (props.grid) {
+            const [gridW, gridH] = Array.isArray(props.grid) ? props.grid : [props.grid, props.grid];
+            const gap = props.snapGap || 0;
+            const newGridWidth = snap(newWidth, gridW);
+            const newGridHeight = snap(newHeight, gridH);
+            newWidth = gap === 0 || Math.abs(newGridWidth - newWidth) <= gap ? newGridWidth : newWidth;
+            newHeight = gap === 0 || Math.abs(newGridHeight - newHeight) <= gap ? newGridHeight : newHeight;
+        }
+
+        // Convert width and height to CSS units if needed
+        const convertToCssUnit = (size: number, originalSize: number, unit: string): string | number => {
+
+            if (unit.endsWith('%')) {
+                return `${(size / originalSize) * 100}%`;
+            } else if (unit.endsWith('vw')) {
+                return `${(size / this.window.innerWidth) * 100}vw`;
+            } else if (unit.endsWith('vh')) {
+                return `${(size / this.window.innerHeight) * 100}vh`;
+            }
+            return size;
+        };
+
+        if (typeof width === 'string') {
+            newWidth = convertToCssUnit(newWidth, parentSize.width, width || '');
+        }
+
+        if (typeof height === 'string') {
+            newHeight = convertToCssUnit(newHeight, parentSize.height, height || '');
+        }
+
+        // Create new state
+        const newState: { width: string | number; height: string | number; flexBasis?: string | number } = {
+            width: this.getCssPropertySize(newWidth, 'width'),
+            height: this.getCssPropertySize(newHeight, 'height')
+        };
+
+        if (this.flexDirection === 'row') {
+            newState.flexBasis = newState.width;
+        } else if (this.flexDirection === 'column') {
+            newState.flexBasis = newState.height;
+        }
+
+        // Check for changes
+        const widthChanged = states.width !== newState.width;
+        const heightChanged = states.height !== newState.height;
+        const flexBaseChanged = states.flexBasis !== newState.flexBasis;
+        const hasChanges = widthChanged || heightChanged || flexBaseChanged;
+
+        if (hasChanges) {
+            this.setState(newState as any);
+
+            // Call onChange callback if defined
+            if (props.onChange) {
+                let newSize = {
+                    width: newState.width,
+                    height: newState.height
+                };
+                props.onChange(newSize, event, direction);
+            }
+            const size = props.size;
+            if (size) {
+                this.setState({
+                    width: size.width ?? 'auto',
+                    height: size.height ?? 'auto'
+                } as any);
+            }
+        }
+    }
+
+
+    onMouseUp = (event: MouseEvent) => {
+        const { isResizing, direction, original } = this.getStates();
+
+        if (!isResizing || !this.resizable) {
+            return;
+        }
+
+        const { width: currentWidth, height: currentHeight } = this.size;
+        const delta = {
+            width: currentWidth - original.width,
+            height: currentHeight - original.height,
+        };
+
+        const { onResizeEnd, size } = this.getProps();
+
+        // Call onResizeEnd callback if defined
+        if (onResizeEnd) {
+            onResizeEnd(this.size, event, direction);
+        }
+
+        // Update state with new size if provided
+        if (size) {
+            this.setState({
+                width: size.width ?? 'auto',
+                height: size.height ?? 'auto'
+            } as any);
+        }
+
+        // Unregister events and update state
+        this.unregisterEvents();
+        this.setState({
+            isResizing: false,
+            backgroundStyle: {
+                ...this.getStates().backgroundStyle,
+                cursor: 'auto'
+            }
+        } as any);
+    }
+
+
+    destroy(): void {
+        this.unregisterEvents();
+    }
+}

+ 127 - 0
packages/semi-foundation/resizable/singleConstants.ts

@@ -0,0 +1,127 @@
+// single
+const rowStyleBase = {
+    width: '100%',
+    height: '10px',
+    top: '0px',
+    left: '0px',
+    cursor: 'row-resize',
+} as const;
+const colStyleBase = {
+    width: '10px',
+    height: '100%',
+    top: '0px',
+    left: '0px',
+    cursor: 'col-resize',
+} as const;
+const edgeStyleBase = {
+    width: '20px',
+    height: '20px',
+    position: 'absolute',
+} as const;
+
+export const directions = ['top', 'right', 'bottom', 'left', 'topRight', 'bottomRight', 'bottomLeft', 'topLeft'] as const;
+
+export const directionStyles = {
+    top: {
+        ...rowStyleBase,
+        top: '-5px',
+    },
+    right: {
+        ...colStyleBase,
+        left: undefined,
+        right: '-5px',
+    },
+    bottom: {
+        ...rowStyleBase,
+        top: undefined,
+        bottom: '-5px',
+    },
+    left: {
+        ...colStyleBase,
+        left: '-5px',
+    },
+    topRight: {
+        ...edgeStyleBase,
+        right: '-10px',
+        top: '-10px',
+        cursor: 'ne-resize',
+    },
+    bottomRight: {
+        ...edgeStyleBase,
+        right: '-10px',
+        bottom: '-10px',
+        cursor: 'se-resize',
+    },
+    bottomLeft: {
+        ...edgeStyleBase,
+        left: '-10px',
+        bottom: '-10px',
+        cursor: 'sw-resize',
+    },
+    topLeft: {
+        ...edgeStyleBase,
+        left: '-10px',
+        top: '-10px',
+        cursor: 'nw-resize',
+    },
+} as const;
+
+export type Direction = 'top' | 'right' | 'bottom' | 'left' | 'topRight' | 'bottomRight' | 'bottomLeft' | 'topLeft';
+
+export interface HandleClassName {
+    top?: string;
+    right?: string;
+    bottom?: string;
+    left?: string;
+    topRight?: string;
+    bottomRight?: string;
+    bottomLeft?: string;
+    topLeft?: string
+}
+
+export type HandlerCallback = (
+    e: MouseEvent,
+    direction: Direction
+) => void;
+
+export interface Enable {
+    top?: boolean;
+    right?: boolean;
+    bottom?: boolean;
+    left?: boolean;
+    topRight?: boolean;
+    bottomRight?: boolean;
+    bottomLeft?: boolean;
+    topLeft?: boolean
+}
+
+export interface Size {
+    width?: string | number;
+    height?: string | number
+}
+
+export interface NumberSize {
+    width: number;
+    height: number
+}
+export interface NewSize {
+    newHeight: number | string;
+    newWidth: number | string
+}
+
+export const DEFAULT_SIZE = {
+    width: 'auto',
+    height: 'auto',
+};
+
+export type ResizeCallback = (
+    size: Size,
+    event: MouseEvent,
+    direction: Direction,
+) => void;
+
+export type ResizeStartCallback = (
+    e: MouseEvent,
+    dir: Direction,
+) => void | boolean;
+

+ 145 - 0
packages/semi-foundation/resizable/utils.ts

@@ -0,0 +1,145 @@
+
+export const clamp = (n: number, min: number, max: number): number => Math.max(Math.min(n, max), min);
+export const snap = (n: number, size: number): number => Math.round(n / size) * size;
+export const has = (dir: 'top' | 'right' | 'bottom' | 'left', target: string): boolean => new RegExp(dir, 'i').test(target);
+export const findNextSnap = (n: number, snapArray: number[], snapGap: number = 0): number => {
+    const closestGapIndex = snapArray.reduce(
+        (prev, curr, index) => (Math.abs(curr - n) < Math.abs(snapArray[prev] - n) ? index : prev),
+        0
+    );
+    const gap = Math.abs(snapArray[closestGapIndex] - n);
+
+    return snapGap === 0 || gap < snapGap ? snapArray[closestGapIndex] : n;
+};
+export const getStringSize = (n: number | string): string => {
+    n = n.toString();
+    if (n === 'auto') {
+        return n;
+    }
+    if (n.endsWith('px')) {
+        return n;
+    }
+    if (n.endsWith('%')) {
+        return n;
+    }
+    if (n.endsWith('vh')) {
+        return n;
+    }
+    if (n.endsWith('vw')) {
+        return n;
+    }
+    if (n.endsWith('vmax')) {
+        return n;
+    }
+    if (n.endsWith('vmin')) {
+        return n;
+    }
+    return `${n}px`;
+};
+export const getNumberSize = (
+    size: undefined | string | number,
+    parentSize: number,
+    innerWidth: number,
+    innerHeight: number
+) => {
+    if (size && typeof size === 'string') {
+        if (size.endsWith('px')) {
+            return Number(size.replace('px', ''));
+        }
+        if (size.endsWith('%')) {
+            const ratio = Number(size.replace('%', '')) / 100;
+            return parentSize * ratio;
+        }
+        if (size.endsWith('vw')) {
+            const ratio = Number(size.replace('vw', '')) / 100;
+            return innerWidth * ratio;
+        }
+        if (size.endsWith('vh')) {
+            const ratio = Number(size.replace('vh', '')) / 100;
+            return innerHeight * ratio;
+        }
+    }
+    return typeof size === 'undefined' ? size : Number(size);
+};
+export const calculateNewMax = (
+    parentSize: { width: number; height: number },
+    innerWidth: number,
+    innerHeight: number,
+    maxWidth?: string | number,
+    maxHeight?: string | number,
+    minWidth?: string | number,
+    minHeight?: string | number
+) => {
+    maxWidth = getNumberSize(maxWidth, parentSize.width, innerWidth, innerHeight);
+    maxHeight = getNumberSize(maxHeight, parentSize.height, innerWidth, innerHeight);
+    minWidth = getNumberSize(minWidth, parentSize.width, innerWidth, innerHeight);
+    minHeight = getNumberSize(minHeight, parentSize.height, innerWidth, innerHeight);
+    return {
+        maxWidth,
+        maxHeight,
+        minWidth,
+        minHeight,
+    };
+};export const getItemDirection = (dir: 'vertical' | 'horizontal') => {
+    if (dir === 'vertical') {
+        return ['bottom', 'top'];
+    } else {
+        return ['right', 'left'];
+    }
+};
+
+export const getPixelSize = (size: string, parentSize: number): number => {
+    if (size.endsWith('px')) {
+        return Number(size.replace('px', ''));
+    }
+    if (size.endsWith('%')) {
+        return Number(size.replace('%', '')) / 100 * parentSize;
+    }
+
+    return typeof size === 'undefined' ? size : Number(size);
+};
+
+export const judgeConstraint = (newSize: number, min: string, max: string, parentSize: number, offset: number = 0) => {
+    min = min ?? "0%";
+    max = max ?? "100%";
+    const minSize = getPixelSize(min, parentSize);
+    const maxSize = getPixelSize(max, parentSize);
+    if (newSize <= minSize + offset) {
+        return true;
+    }
+    if (newSize >= maxSize - offset) {
+        return true;
+    }
+    return false;
+};
+
+export const adjustNewSize = (newSize: number, min: string, max: string, parentSize: number, offset: number) => {
+    min = min ?? "0%";
+    max = max ?? "100%";
+    const minSize = getPixelSize(min, parentSize);
+    const maxSize = getPixelSize(max, parentSize);
+    if (newSize <= minSize + offset) {
+        return minSize + offset;
+    }
+    if (newSize >= maxSize - offset) {
+        return maxSize - offset;
+    }
+    return newSize;
+};
+
+export const getOffset = (style: CSSStyleDeclaration, direction: 'horizontal' | 'vertical') => {
+    if (direction === 'horizontal') {
+        const paddingLeft = parseFloat(style.paddingLeft);
+        const paddingRight = parseFloat(style.paddingRight);
+        const borderLeftWidth = parseFloat(style.borderLeftWidth);
+        const borderRightWidth = parseFloat(style.borderRightWidth);
+        return paddingLeft + paddingRight + borderLeftWidth + borderRightWidth;
+    } else {
+        const paddingTop = parseFloat(style.paddingTop);
+        const paddingBottom = parseFloat(style.paddingBottom);
+        const borderTopWidth = parseFloat(style.borderTopWidth);
+        const borderBottomWidth = parseFloat(style.borderBottomWidth);
+        return paddingTop + paddingBottom + borderTopWidth + borderBottomWidth;
+    }
+};
+

+ 1 - 0
packages/semi-theme-default/scss/variables.scss

@@ -48,6 +48,7 @@ $z-image_preview_header: 1; // Image 组件预览层中 header 部分 z-index
 // 正在拖拽中的元素的 z-index,需要高于所有的弹出层组件 z-index
 $z-transfer_right_item_drag_item_move: 2000; // 穿梭框右侧面板中正在拖拽元素的z-index
 $z-tagInput_drag_item_move: 2000; // 标签输入框中正在拖拽元素的z-index
+$z-resizable_handler: 2000; // 伸缩框组件中handler的z-index   
 
 // font
 $font-family-regular: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',

+ 7 - 0
packages/semi-ui/index.ts

@@ -115,3 +115,10 @@ export { default as Lottie } from "./lottie";
 export { default as Chat } from './chat';
 
 export { default as HotKeys } from './hotKeys'; 
+
+export {
+    Resizable,
+    ResizeItem,
+    ResizeHandler,
+    ResizeGroup
+} from './resizable';

+ 518 - 0
packages/semi-ui/resizable/_story/resizable.stories.jsx

@@ -0,0 +1,518 @@
+import React, { createRef, useState } from 'react';
+import { Resizable } from '../../index';
+import { Toast, Button, Tag } from '@douyinfe/semi-ui'
+export default {
+  title: 'Resizable'
+}
+
+import { ResizeItem, ResizeHandler, ResizeGroup } from '../../index'
+
+export const Group_layout = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '1000px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+          defaultSize={"20%"}
+          // onChange={() => { setText('resizing') }}
+          // onResizeStart={() => {{Toast.info(opts_1)}}}
+          // onResizeEnd={() => { Toast.info(opts); setText('test') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {'header'}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"80%"}
+          // onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"20%"}
+              // onChange={() => { setText('resizing') }}
+              // onResizeStart={() => {Toast.info(opts_1)}}
+              // onResizeEnd={() => { Toast.info(opts); setText('test') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              // onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              // defaultSize={"90%"}
+              // onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_nested = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => {{Toast.info(opts_1)}}}
+          onResizeEnd={() => { Toast.info(opts); setText('test') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler><div>{'hahaha, man'}</div></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={'20%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid', 
+                padding: '10px'  }}
+              defaultSize={"25%"}
+              onChange={() => { setText('resizing') }}
+              onResizeStart={() => {Toast.info(opts_1)}}
+              onResizeEnd={() => { Toast.info(opts); setText('test') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+              defaultSize={"25%"}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+          </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_vertical = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={"20%"}
+          min={'10%'}
+          max={'30%'}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => {Toast.info(opts_1)}}
+          onResizeEnd={() => { Toast.info(opts); setText('test') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text + " min:10% max:30%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={'20%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={'20%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_horizontal = () => {
+  const [text, setText] = useState('test')
+  return (
+    <div style={{ width: '100%', height: '100px' }}>
+      <ResizeGroup direction='horizontal'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+          defaultSize={2}
+          min={'10%'}
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('test') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text + " min:10%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+          defaultSize={'20%'}
+          min={'10%'}
+          max={'30%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text + " min:10% max:30%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
+          defaultSize={'600px'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
+          defaultSize={1.3}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+
+export const Single_defaultSize = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        onChange={(e, d, s) => { setText('resizing'); }}
+        onResizeStart={() => {Toast.info(opts_1)}}
+        onResizeEnd={() => { Toast.info(opts); setText('test') }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          <Tag>{text}</Tag>
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_Enabel = () => {
+  const [b, setB] = useState(false)
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={() => (setB(!b))}>{'left:' + b}</Button>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        enable={{
+          left: b
+        }}
+        defaultSize={{
+          width: 200,
+          height: 200,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          test
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_ratio = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        ratio={2}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          test
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_lock_aspect = () => {
+  const aspectRatio = 16 / 9
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        lockAspectRatio
+      >
+        <div style={{ marginLeft: '20%' }}>
+          lock
+        </div>
+      </Resizable>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: 200,
+          height: 1800 / 16,
+        }}
+        lockAspectRatio={16 / 9}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          16 / 9
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const singleMaxMin = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        maxWidth={500}
+        maxHeight={600}
+        minWidth={50}
+        minHeight={50}
+        defaultSize={{
+          width: 200,
+          height: 300,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          width在50到500之间,height在50到600之间
+        </div>
+      </Resizable>
+    </div>
+  )
+}
+
+export const Single_change = () => {
+  const [size, setSize] = useState({ width: 200, height: 300 });
+  const ref = createRef()
+  const onChange = (() => {
+    let realSize = { width: size.width + 10, height: size.height + 10 };
+    setSize(realSize);
+  })
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={onChange}>set += 10</Button>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        onChange={(s) => { setSize(s); }}
+        size={size}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          受控
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_scale = () => {
+
+  return (
+    <div style={{ width: '500px', height: '60%', transform: 'scale(0.5)', transformOrigin: '0 0' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'light blue', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        scale={0.5}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          scale 0.5
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_bound = () => {
+  return (
+    <div style={{ width: '500px', height: '600px', border: 'var(--semi-color-border) 5px solid' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        boundElement={'parent'}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          受控
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_handler = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        handleNode={{
+          bottomRight: <Button type="primary">hi</Button>
+        }}
+        handleStyle={{
+          bottomRight: {
+            width: '100px',
+            height: '100px',
+            backgroundColor: 'red'
+          }
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          bottomRight
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_grid = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        grid={[100, 100]}
+        snapGap={20}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          snap
+        </div>
+      </Resizable>
+    </div >
+  );
+}

+ 508 - 0
packages/semi-ui/resizable/_story/resizable.stories.tsx

@@ -0,0 +1,508 @@
+import React, { createRef, useState } from 'react';
+import { Resizable } from '../../index';
+import { Toast, Button } from '@douyinfe/semi-ui'
+export default {
+  title: 'Resizable'
+}
+
+import { ResizeItem, ResizeHandler, ResizeGroup } from '../../index'
+
+export const Group_layout = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '1000px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => {{Toast.info(opts_1)}}}
+          onResizeEnd={() => { Toast.info(opts); setText('test') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {'header'}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          defaultSize={"80%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              defaultSize={"20%"}
+              onChange={() => { setText('resizing') }}
+              onResizeStart={() => {Toast.info(opts_1)}}
+              onResizeEnd={() => { Toast.info(opts); setText('test') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {'tab'}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 1px solid' }}
+              // defaultSize={"90%"}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            
+          </ResizeGroup>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_nested = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => {{Toast.info(opts_1)}}}
+          onResizeEnd={() => { Toast.info(opts); setText('test') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler><div>{'hahaha, man'}</div></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={'20%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <ResizeGroup direction='horizontal'>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid', 
+                padding: '10px'  }}
+              defaultSize={"25%"}
+              onChange={() => { setText('resizing') }}
+              onResizeStart={() => {Toast.info(opts_1)}}
+              onResizeEnd={() => { Toast.info(opts); setText('test') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+              defaultSize={"25%"}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+            <ResizeHandler></ResizeHandler>
+            <ResizeItem
+              style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+              onChange={() => { setText('resizing') }}
+            >
+              <div style={{ marginLeft: '20%' }}>
+                {text}
+              </div>
+            </ResizeItem>
+          </ResizeGroup>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={"20%"}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_vertical = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px', height: '600px' }}>
+      <ResizeGroup direction='vertical'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={"20%"}
+          min={'10%'}
+          max={'30%'}
+          onChange={() => { setText('resizing') }}
+          onResizeStart={() => {Toast.info(opts_1)}}
+          onResizeEnd={() => { Toast.info(opts); setText('test') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text + " min:10% max:30%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={'20%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          defaultSize={'20%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+          
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+export const Group_horizontal = () => {
+  const [text, setText] = useState('test')
+  return (
+    <div style={{ width: '500px', height: '100px' }}>
+      <ResizeGroup direction='horizontal'>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+          defaultSize={'20%'}
+          min={'10%'}
+          onChange={() => { setText('resizing') }}
+          onResizeEnd={() => { setText('test') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text + " min:10%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)', }}
+          defaultSize={'20%'}
+          min={'10%'}
+          max={'30%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text + " min:10% max:30%"}
+          </div>
+        </ResizeItem>
+        <ResizeHandler></ResizeHandler>
+        <ResizeItem
+          style={{ backgroundColor: 'rgba(var(--semi-grey-1), 1)',  }}
+          defaultSize={'30%'}
+          onChange={() => { setText('resizing') }}
+        >
+          <div style={{ marginLeft: '20%', border: 'var(--semi-color-border) solid 1px', padding:'5px' }}>
+            {text}
+          </div>
+        </ResizeItem>
+      </ResizeGroup>
+    </div>
+  );
+}
+
+
+export const Single_defaultSize = () => {
+  const [text, setText] = useState('test')
+  const opts_1 = {
+    content: 'resize start',
+    duration: 1,
+    stack: true,
+  };
+  const opts = {
+    content: 'resize end',
+    duration: 1,
+    stack: true,
+  };
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        onChange={(e, d, s) => { setText('resizing'); }}
+        onResizeStart={() => {Toast.info(opts_1)}}
+        onResizeEnd={() => { Toast.info(opts); setText('test') }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          {text}
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_Enabel = () => {
+  const [b, setB] = useState(false)
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={() => (setB(!b))}>{'left:' + b}</Button>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        enable={{
+          left: b
+        }}
+        defaultSize={{
+          width: 200,
+          height: 200,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          test
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_ratio = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        ratio={2}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          test
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_lock_aspect = () => {
+  const aspectRatio = 16 / 9
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        lockAspectRatio
+      >
+        <div style={{ marginLeft: '20%' }}>
+          lock
+        </div>
+      </Resizable>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: 200,
+          height: 1800 / 16,
+        }}
+        lockAspectRatio={16 / 9}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          16 / 9
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const singleMaxMin = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        maxWidth={500}
+        maxHeight={600}
+        minWidth={50}
+        minHeight={50}
+        defaultSize={{
+          width: 200,
+          height: 300,
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          width在50到500之间,height在50到600之间
+        </div>
+      </Resizable>
+    </div>
+  )
+}
+
+export const Single_change = () => {
+  const [size, setSize] = useState({ width: 200, height: 300 });
+  const ref = createRef()
+  const onChange = (() => {
+    let realSize = { width: size.width + 10, height: size.height + 10 };
+    setSize(realSize);
+  })
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Button onClick={onChange}>set += 10</Button>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        onChange={(s) => { setSize(s); }}
+        size={size}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          受控
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_scale = () => {
+
+  return (
+    <div style={{ width: '500px', height: '60%', transform: 'scale(0.5)', transformOrigin: '0 0' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'light blue', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        scale={0.5}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          scale 0.5
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_bound = () => {
+  return (
+    <div style={{ width: '500px', height: '600px', border: 'var(--semi-color-border) 5px solid' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        boundElement={'parent'}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          受控
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_handler = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        handleNode={{
+          bottomRight: <Button type="primary">hi</Button>
+        }}
+        handleStyle={{
+          bottomRight: {
+            width: '100px',
+            height: '100px',
+            backgroundColor: 'red'
+          }
+        }}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          bottomRight
+        </div>
+      </Resizable>
+    </div>
+  );
+}
+
+export const Single_grid = () => {
+  return (
+    <div style={{ width: '500px', height: '60%' }}>
+      <Resizable
+        style={{ marginLeft: '20%', backgroundColor: 'rgba(var(--semi-grey-1), 1)', border: 'var(--semi-color-border) 5px solid' }}
+        defaultSize={{
+          width: '60%',
+          height: 300,
+        }}
+        grid={[100, 100]}
+        snapGap={20}
+      >
+        <div style={{ marginLeft: '20%' }}>
+          snap
+        </div>
+      </Resizable>
+    </div >
+  );
+}

+ 18 - 0
packages/semi-ui/resizable/group/resizeContext.ts

@@ -0,0 +1,18 @@
+import React, { createContext, RefObject } from 'react';
+import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+
+export interface ResizeContextProps {
+    direction: 'horizontal' | 'vertical';
+    registerItem: (ref: RefObject<HTMLDivElement>, 
+        min: string, max: string, defaultSize: string|number,
+        onResizeStart: ResizeStartCallback,
+        onChange: ResizeCallback,
+        onResizeEnd: ResizeCallback
+    ) => number;
+    registerHandler: (ref: RefObject<HTMLDivElement>) => number;
+    notifyResizeStart: (handlerIndex: number, e: MouseEvent) => void;
+    getGroupSize: () => number
+}
+
+export const ResizeContext = createContext<ResizeContextProps>(undefined);
+

+ 204 - 0
packages/semi-ui/resizable/group/resizeGroup.tsx

@@ -0,0 +1,204 @@
+import React, { createContext, createRef, ReactNode, Ref, RefObject } from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import { ResizeGroupFoundation, ResizeGroupAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
+import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
+import BaseComponent from '../../_base/baseComponent';
+import { ResizeContext, ResizeContextProps } from './resizeContext';
+import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import "@douyinfe/semi-foundation/resizable/index.scss";
+
+const prefixCls = cssClasses.PREFIX;
+
+export interface ResizeGroupProps {
+    children: ReactNode;
+    direction: 'horizontal' | 'vertical';
+    className?: string
+}
+
+export interface ResizeGroupState {
+    isResizing: boolean;
+    originalPosition: {
+        x: number;
+        y: number;
+        lastItemSize: number;
+        nextItemSize: number;
+        lastOffset: number;
+        nextOffset: number
+    };
+    backgroundStyle: React.CSSProperties;
+    curHandler: number
+}
+
+class ResizeGroup extends BaseComponent<ResizeGroupProps, ResizeGroupState> {
+
+    static propTypes = {
+    };
+
+    static defaultProps: Partial<ResizeGroupProps> = {
+        direction: 'horizontal'
+    };
+
+    constructor(props: ResizeGroupProps) {
+        super(props);
+        this.state = {
+            isResizing: false,
+            originalPosition: {
+                x: 0,
+                y: 0,
+                lastItemSize: 0,
+                nextItemSize: 0,
+                lastOffset: 0,
+                nextOffset: 0,
+            },
+            backgroundStyle: {
+                height: '100%',
+                width: '100%',
+                backgroundColor: 'rgba(0,0,0,0)',
+                cursor: 'auto',
+                opacity: 0,
+                position: 'fixed',
+                zIndex: 9999,
+                top: '0',
+                left: '0',
+                bottom: '0',
+                right: '0',
+            },
+            curHandler: null,
+        };
+        
+        this.groupRef = createRef();
+        this.foundation = new ResizeGroupFoundation(this.adapter);
+        this.contextValue = {
+            direction: props.direction,
+            registerItem: this.registerItem,
+            registerHandler: this.registerHandler,
+            notifyResizeStart: this.foundation.onResizeStart,
+            getGroupSize: this.getGroupSize,
+        };
+    }
+
+    contextValue: ResizeContextProps;
+    foundation: ResizeGroupFoundation;
+    groupRef: React.RefObject<HTMLDivElement>;
+    groupSize: number;
+    availableSize: number;
+    static contextType = ResizeContext;
+    context: ResizeGroupProps;
+    itemRefs: RefObject<HTMLDivElement>[] = [];
+    itemMinMap: Map<number, string> = new Map();
+    itemMaxMap: Map<number, string> = new Map();
+    itemMinusMap: Map<number, number> = new Map();
+    itemDefaultSizeList: (string|number)[] = []
+    itemResizeStart: Map<number, ResizeStartCallback> = new Map();
+    itemResizing: Map<number, ResizeCallback> = new Map();
+    itemResizeEnd: Map<number, ResizeCallback> = new Map();
+    handlerRefs: RefObject<HTMLDivElement>[] = [];
+
+    componentDidMount() {
+        this.foundation.init();
+    }
+
+    componentDidUpdate(_prevProps: ResizeGroupProps) {
+    }
+
+    componentWillUnmount() {
+        this.foundation.destroy();
+    }
+
+    get adapter(): ResizeGroupAdapter<ResizeGroupProps, ResizeGroupState> {
+        return {
+            ...super.adapter,
+            getGroupRef: () => this.groupRef.current,
+            getItem: (id: number) => this.itemRefs[id].current,
+            getItemCount: () => this.itemRefs.length,
+            getHandler: (id: number) => this.handlerRefs[id].current,
+            getHandlerCount: () => this.handlerRefs.length,
+            getItemMin: (index) => {
+                return this.itemMinMap.get(index);
+            },
+            getItemMax: (index) => {
+                return this.itemMaxMap.get(index);
+            },
+            getItemChange: (index) => {
+                return this.itemResizing.get(index);
+            },
+            getItemEnd: (index) => {
+                return this.itemResizeEnd.get(index);   
+            },
+            getItemStart: (index) => {
+                return this.itemResizeStart.get(index);
+            },
+            getItemDefaultSize: (index) => {
+                return this.itemDefaultSizeList[index];
+            },
+            registerEvents: this.registerEvent,
+            unregisterEvents: this.unregisterEvent,
+        };
+    }
+
+    get window(): Window | null {
+        return this.groupRef.current.ownerDocument.defaultView as Window ?? null;
+    }
+
+    registerEvent = () => {
+        if (this.window) {
+            this.window.addEventListener('mousemove', this.foundation.onResizing);
+            this.window.addEventListener('mouseup', this.foundation.onResizeEnd);
+            this.window.addEventListener('mouseleave', this.foundation.onResizeEnd);
+        }
+    }
+
+    unregisterEvent = () => {
+        if (this.window) {
+            this.window.removeEventListener('mousemove', this.foundation.onResizing);
+            this.window.removeEventListener('mouseup', this.foundation.onResizeEnd);
+            this.window.removeEventListener('mouseleave', this.foundation.onResizeEnd);
+        }
+    }
+
+    registerItem = (ref: RefObject<HTMLDivElement>,
+        min: string, max: string, defaultSize: string|number,
+        onResizeStart: ResizeStartCallback, onChange: ResizeCallback, onResizeEnd: ResizeCallback
+    ) => {
+        this.itemRefs.push(ref);
+        let index = this.itemRefs.length - 1;
+        this.itemMinMap.set(index, min);
+        this.itemMaxMap.set(index, max);
+        this.itemDefaultSizeList.push(defaultSize);
+        this.itemResizeStart.set(index, onResizeStart);
+        this.itemResizing.set(index, onChange);
+        this.itemResizeEnd.set(index, onResizeEnd);
+        return index;
+    }
+
+    registerHandler = (ref: RefObject<HTMLDivElement>) => {
+        this.handlerRefs.push(ref);
+        return this.handlerRefs.length - 1;
+    }
+
+    getGroupSize = () => {
+        return this.groupSize;
+    }
+
+    render() {
+        const { children, direction, className, ...rest } = this.props;
+        return (
+            <ResizeContext.Provider value={this.contextValue}>
+                <div
+                    style={{
+                        flexDirection: direction === 'vertical' ? 'column' : 'row',
+                    }}
+                    ref={this.groupRef}
+                    className={classNames(className, prefixCls + '-group')}
+                    {...rest}
+                >
+                    {this.state.isResizing && <div style={this.state.backgroundStyle} />}
+                    {children}
+                </div>
+            </ResizeContext.Provider>
+        );
+    }
+}
+
+export default ResizeGroup;

+ 107 - 0
packages/semi-ui/resizable/group/resizeHandler.tsx

@@ -0,0 +1,107 @@
+import React, { Children, createRef, ReactNode, useContext } from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import { ResizeHandlerFoundation, ResizeHandlerAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
+import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
+import { Direction, HandlerCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { directionStyles } from '@douyinfe/semi-foundation/resizable/groupConstants';
+import BaseComponent from '../../_base/baseComponent';
+import { ResizeContext, ResizeContextProps } from './resizeContext';
+import { IconHandle } from '@douyinfe/semi-icons';
+
+
+const prefixCls = cssClasses.PREFIX;
+
+export interface ResizeHandlerProps {
+    children?: ReactNode;
+    direction?: Direction;
+    onResizeStart?: HandlerCallback;
+    className?: string;
+    disabled?: boolean;
+    style?: React.CSSProperties
+}
+
+export interface ResizeHandlerState {
+}
+
+class ResizeHandler extends BaseComponent<ResizeHandlerProps, ResizeHandlerState> {
+    static propTypes = {
+        children: PropTypes.node,
+        direction: PropTypes.string,
+        onResizeStart: PropTypes.func,
+        className: PropTypes.string,
+        disabled: PropTypes.bool,
+        style: PropTypes.object,
+    };
+
+    static defaultProps: Partial<ResizeHandlerProps> = {
+    };
+
+    constructor(props: ResizeHandlerProps) {
+        super(props);
+        this.state = {
+        };
+        this.handlerRef = createRef();
+        this.foundation = new ResizeHandlerFoundation(this.adapter);
+    }
+
+    componentDidMount() {
+        this.foundation.init();
+        this.handlerIndex = this.context.registerHandler(this.handlerRef);
+    }
+
+    componentDidUpdate(_prevProps: ResizeHandlerProps) {
+    }
+
+    componentWillUnmount() {
+        this.foundation.destroy();
+    }
+
+    foundation: ResizeHandlerFoundation;
+    onMouseDown = (e: MouseEvent) => {
+        const { notifyResizeStart } = this.context;
+        notifyResizeStart(this.handlerIndex, e);
+    }
+    
+    get adapter(): ResizeHandlerAdapter<ResizeHandlerProps, ResizeHandlerState> {
+        return {
+            ...super.adapter,
+            registerEvents: () => {
+                this.handlerRef.current.addEventListener('mousedown', this.onMouseDown);
+            },
+            unregisterEvents: () => {
+                this.handlerRef.current.removeEventListener('mousedown', this.onMouseDown);
+            },
+        };
+    }
+
+    getHandler: () => HTMLElement = () => {
+        return this.handlerRef.current;
+    }
+
+    static contextType = ResizeContext;
+    context: ResizeContextProps;
+    handlerRef: React.RefObject<HTMLDivElement>
+    handlerIndex: number;
+
+    render() {
+        
+        const { style, className, children } = this.props;
+        return (
+            <div
+                className={classNames(className, prefixCls + '-handler')}
+                style={{
+                    ...directionStyles[this.context.direction],
+                    ...style
+                }}
+                ref={this.handlerRef}
+            >
+                {children ?? <IconHandle size='inherit' style={{
+                    rotate: this.context.direction === 'horizontal' ? '0deg' : '90deg',
+                }}/>}
+            </div>
+        );
+    }
+}
+
+export default ResizeHandler;

+ 98 - 0
packages/semi-ui/resizable/group/resizeItem.tsx

@@ -0,0 +1,98 @@
+import React, { createRef, ReactNode, useContext } from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import { ResizeItemFoundation, ResizeItemAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
+import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
+import BaseComponent from '../../_base/baseComponent';
+import { ResizeCallback, ResizeStartCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import { ResizeContext, ResizeContextProps } from './resizeContext';
+import { noop } from 'lodash';
+
+const prefixCls = cssClasses.PREFIX;
+
+export interface ResizeItemProps {
+    style?: React.CSSProperties;
+    className?: string;
+    min?: string;
+    max?: string;
+    children?: React.ReactNode;
+    onResizeStart?: ResizeStartCallback;
+    onChange?: ResizeCallback;
+    onResizeEnd?: ResizeCallback;
+    defaultSize?: string | number
+}
+
+export interface ResizeItemState {
+}
+
+class ResizeItem extends BaseComponent<ResizeItemProps, ResizeItemState> {
+    static propTypes = {
+        style: PropTypes.object,
+        className: PropTypes.string,
+        min: PropTypes.string,
+        max: PropTypes.string,
+        children: PropTypes.object,
+        onResizeStart: PropTypes.func,
+        onChange: PropTypes.func,
+        onResizeEnd: PropTypes.func,
+        defaultSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    };
+
+    static defaultProps: Partial<ResizeItemProps> = {
+        onResizeStart: noop,
+        onChange: noop,
+        onResizeEnd: noop,
+    };
+
+    constructor(props: ResizeItemProps) {
+        super(props);
+        this.itemRef = createRef<HTMLDivElement | null>();
+        this.foundation = new ResizeItemFoundation(this.adapter);
+        this.state = {
+            isResizing: false,
+        };
+
+    }
+
+    componentDidMount() {
+        this.foundation.init();
+        const { min, max, onResizeStart, onChange, onResizeEnd, defaultSize } = this.props;
+        this.itemIndex = this.context.registerItem(this.itemRef, min, max, defaultSize, onResizeStart, onChange, onResizeEnd);
+    }
+
+    componentDidUpdate(_prevProps: ResizeItemProps) {
+    }
+
+    componentWillUnmount() {
+        this.foundation.destroy();
+    }
+
+    get adapter(): ResizeItemAdapter<ResizeItemProps, ResizeItemState> {
+        return {
+            ...super.adapter,
+        };
+    }
+    static contextType = ResizeContext;
+    context: ResizeContextProps;
+    itemRef: React.RefObject<HTMLDivElement | null>;
+    itemIndex: number;
+
+    render() {
+        const style: React.CSSProperties = {
+            ...this.props.style,
+        };
+
+        return (
+            <div
+                style={style}
+                className={classNames(this.props.className, prefixCls + '-item')}
+                ref={this.itemRef}
+            >
+                {this.props.children}
+            </div>
+        );
+    }
+}
+
+
+export default ResizeItem;

+ 19 - 0
packages/semi-ui/resizable/index.tsx

@@ -0,0 +1,19 @@
+/**
+ * reference:https://github.com/bokuweb/re-resizable
+ * Resizable组件的api与功能代码均参考了v6.10.0,将逻辑部分放在Foundation中,react部分放在组件中
+ * ResizeGroup的伸缩逻辑也有同上的参考
+ */
+import Resizable from "./single/resizable";
+export {
+    Resizable
+};
+
+import ResizeItem from "./group/resizeItem";
+import ResizeHandler from "./group/resizeHandler";
+import ResizeGroup from "./group/resizeGroup";
+
+export {
+    ResizeItem, 
+    ResizeHandler,
+    ResizeGroup
+};

+ 273 - 0
packages/semi-ui/resizable/single/resizable.tsx

@@ -0,0 +1,273 @@
+import React, { createRef, CSSProperties, ReactNode, useRef } from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import { ResizableFoundation, ResizableAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
+
+import { cssClasses, } from '@douyinfe/semi-foundation/resizable/constants';
+import { Direction, Size, Enable, ResizeStartCallback, ResizeCallback, HandleClassName, directions } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import BaseComponent from '../../_base/baseComponent';
+import ResizableHandler from './resizableHandler';
+import '@douyinfe/semi-foundation/resizable/index.scss';
+
+const prefixCls = cssClasses.PREFIX;
+export interface HandleComponent {
+    top?: ReactNode;
+    right?: ReactNode;
+    bottom?: ReactNode;
+    left?: ReactNode;
+    topRight?: ReactNode;
+    bottomRight?: ReactNode;
+    bottomLeft?: ReactNode;
+    topLeft?: ReactNode
+}
+
+export interface HandleStyle {
+    top?: CSSProperties;
+    right?: CSSProperties;
+    bottom?: CSSProperties;
+    left?: CSSProperties;
+    topRight?: CSSProperties;
+    bottomRight?: CSSProperties;
+    bottomLeft?: CSSProperties;
+    topLeft?: CSSProperties
+}
+
+export interface ResizableProps {
+    style?: React.CSSProperties;
+    className?: string;
+    grid?: [number, number];
+    snap?: {
+        x?: number[];
+        y?: number[]
+    };
+    snapGap?: number;
+    boundElement?: 'parent' | 'window' | HTMLElement;
+    boundsByDirection?: boolean;
+    size?: Size;
+    minWidth?: string | number;
+    minHeight?: string | number;
+    maxWidth?: string | number;
+    maxHeight?: string | number;
+    lockAspectRatio?: boolean | number;
+    lockAspectRatioExtraWidth?: number;
+    lockAspectRatioExtraHeight?: number;
+    enable?: Enable | false;
+    handleStyle?: HandleStyle;
+    handleClass?: HandleClassName;
+    handleWrapperStyle?: React.CSSProperties;
+    handleWrapperClass?: string;
+    handleNode?: HandleComponent;
+    children?: React.ReactNode;
+    onResizeStart?: ResizeStartCallback;
+    onChange?: ResizeCallback;
+    onResizeEnd?: ResizeCallback;
+    defaultSize?: Size;
+    scale?: number;
+    ratio?: number | [number, number]
+}
+
+export interface ResizableState {
+    isResizing: boolean;
+    direction: Direction;
+    original: {
+        x: number;
+        y: number;
+        width: number;
+        height: number
+    };
+    width: number | string;
+    height: number | string;
+
+    backgroundStyle: React.CSSProperties;
+    flexBasis?: string | number
+}
+
+class Resizable extends BaseComponent<ResizableProps, ResizableState> {
+    static propTypes = {
+        style: PropTypes.object,
+        className: PropTypes.string,
+        grid: PropTypes.arrayOf(PropTypes.number),
+        snap: PropTypes.shape({
+            x: PropTypes.arrayOf(PropTypes.number),
+            y: PropTypes.arrayOf(PropTypes.number),
+        }),        
+        snapGap: PropTypes.number,
+        bounds: PropTypes.oneOf(['parent', 'window', PropTypes.node]),
+        boundsByDirection: PropTypes.bool,
+        size: PropTypes.object,
+        minWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+        minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+        maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+        maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+        lockAspectRatio: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
+        lockAspectRatioExtraWidth: PropTypes.number,
+        lockAspectRatioExtraHeight: PropTypes.number,
+        enable: PropTypes.object,
+        handleStyle: PropTypes.object,
+        handleClass: PropTypes.object,
+        handleWrapperStyle: PropTypes.object,
+        handleWrapperClass: PropTypes.string,
+        handleNode: PropTypes.object,
+        children: PropTypes.object,
+        onResizeStart: PropTypes.func,
+        onChange: PropTypes.func,
+        onResizeEnd: PropTypes.func,
+        defaultSize: PropTypes.object,
+        scale: PropTypes.number,
+        ratio: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]),
+    };
+
+    static defaultProps: Partial<ResizableProps> = {
+        onResizeStart: () => {},
+        onChange: () => {},
+        onResizeEnd: () => {},
+        enable: {
+            top: true,
+            right: true,
+            bottom: true,
+            left: true,
+            topRight: true,
+            bottomRight: true,
+            bottomLeft: true,
+            topLeft: true,
+        },
+        style: {},
+        grid: [1, 1],
+        lockAspectRatio: false,
+        lockAspectRatioExtraWidth: 0,
+        lockAspectRatioExtraHeight: 0,
+        scale: 1,
+        ratio: 1,
+        snapGap: 0,
+    };
+
+    foundation: ResizableFoundation;
+    resizableRef: React.RefObject<HTMLDivElement | null>;
+    constructor(props: ResizableProps) {
+        super(props);
+        this.resizableRef = createRef<HTMLDivElement | null>();
+        this.foundation = new ResizableFoundation(this.adapter);
+        this.state = {
+            isResizing: false,
+            width: this.foundation.propSize.width ?? 'auto',
+            height: this.foundation.propSize.height ?? 'auto',
+            direction: 'right',
+            original: {
+                x: 0,
+                y: 0,
+                width: 0,
+                height: 0,
+            },
+            backgroundStyle: {
+                height: '100%',
+                width: '100%',
+                backgroundColor: 'rgba(0,0,0,0)',
+                cursor: 'auto',
+                opacity: 0,
+                position: 'fixed',
+                zIndex: 9999,
+                top: '0',
+                left: '0',
+                bottom: '0',
+                right: '0',
+            },
+            flexBasis: undefined,
+        };        
+    }
+
+
+    componentDidMount() {
+        this.foundation.init();
+    }
+
+    componentDidUpdate(_prevProps: ResizableProps) {
+    }
+
+    componentWillUnmount() {
+        this.foundation.destroy();
+    }
+
+    getResizable = () => {
+        return this.resizableRef?.current;
+    }
+
+    get adapter(): ResizableAdapter<ResizableProps, ResizableState> {
+        return {
+            ...super.adapter,
+            getResizable: this.getResizable,
+            registerEvent: () => {
+                let window = this.foundation.window;
+                window?.addEventListener('mouseup', this.foundation.onMouseUp);
+                window?.addEventListener('mousemove', this.foundation.onMouseMove);
+                window?.addEventListener('mouseleave', this.foundation.onMouseUp);
+            },
+            unregisterEvent: () => {
+                let window = this.foundation.window;
+                window?.removeEventListener('mouseup', this.foundation.onMouseUp);
+                window?.removeEventListener('mousemove', this.foundation.onMouseMove);
+                window?.removeEventListener('mouseleave', this.foundation.onMouseUp);
+            },
+        };
+    }
+
+    renderResizeHandler = () => {
+        const { enable, handleStyle, handleClass, handleNode, handleWrapperStyle, handleWrapperClass } = this.props;
+        if (!enable) {
+            return null;
+        }
+        const handlers = directions.map(dir => {
+            if (enable[dir as Direction] !== false) {
+                return (
+                    <ResizableHandler
+                        key={dir}
+                        direction={dir as Direction}
+                        onResizeStart={this.foundation.onResizeStart}
+                        style={handleStyle && handleStyle[dir]}
+                        className={handleClass && handleClass[dir]}
+                    >
+                        {handleNode?.[dir] ?? null}
+                    </ResizableHandler>
+                );
+            }
+            return null;
+        });
+ 
+        return (
+            <div className={handleWrapperClass} style={handleWrapperStyle}>
+                {handlers}
+            </div>
+        );
+    }
+
+    render() {
+        const { className, style, children, maxHeight, maxWidth, minHeight, minWidth } = this.props;
+        const resizeStyle: React.CSSProperties = {
+            userSelect: this.state.isResizing ? 'none' : 'auto',
+            maxWidth: maxWidth,
+            maxHeight: maxHeight,
+            minWidth: minWidth,
+            minHeight: minHeight,
+            ...style,
+            ...this.foundation.sizeStyle,
+        };
+
+        if (this.state?.flexBasis) {
+            style.flexBasis = this.state.flexBasis;
+        }
+
+        return (
+            <div
+                style={resizeStyle}
+                className={classNames(className, prefixCls + '-resizable')}
+                ref={this.resizableRef}
+                {...this.getDataAttr(this.props)}
+            >
+                {this.state.isResizing && <div style={this.state.backgroundStyle} />}
+                {children}
+                {this.renderResizeHandler()}
+            </div>
+        );
+    }
+}
+
+export default Resizable;

+ 90 - 0
packages/semi-ui/resizable/single/resizableHandler.tsx

@@ -0,0 +1,90 @@
+import React, { createRef, ReactNode } from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import { ResizableHandlerFoundation, ResizableHandlerAdapter } from '@douyinfe/semi-foundation/resizable/foundation';
+
+import { cssClasses } from '@douyinfe/semi-foundation/resizable/constants';
+import { directionStyles, Direction, HandlerCallback } from '@douyinfe/semi-foundation/resizable/singleConstants';
+import BaseComponent from '../../_base/baseComponent';
+
+const prefixCls = cssClasses.PREFIX;
+
+export interface ResizableHandlerProps {
+    children?: ReactNode;
+    direction?: Direction;
+    onResizeStart?: HandlerCallback;
+    className?: string;
+    disabled?: boolean;
+    style?: React.CSSProperties
+}
+
+export interface ResizableHandlerState {
+    direction: Direction
+}
+
+class ResizableHandler extends BaseComponent<ResizableHandlerProps, ResizableHandlerState> {
+    static propTypes = {
+        children: PropTypes.node,
+        direction: PropTypes.string,
+        onResizeStart: PropTypes.func,
+        className: PropTypes.string,
+        disabled: PropTypes.bool,
+        style: PropTypes.object,
+    };
+
+    static defaultProps: Partial<ResizableHandlerProps> = {
+    };
+
+    constructor(props: ResizableHandlerProps) {
+        super(props);
+        this.state = {
+            direction: this.props.direction
+        };
+        this.resizeHandlerRef = createRef();
+        this.foundation = new ResizableHandlerFoundation(this.adapter); 
+    }
+
+    componentDidMount() {
+        this.foundation.init();
+    }
+
+    componentDidUpdate(_prevProps: ResizableHandlerProps) {
+    }
+
+    componentWillUnmount() {
+        this.foundation.destroy();
+    }
+
+    foundation: ResizableHandlerFoundation;
+
+    get adapter(): ResizableHandlerAdapter<ResizableHandlerProps, ResizableHandlerState> {
+        return {
+            ...super.adapter,
+            registerEvent: () => {
+                this.resizeHandlerRef.current.addEventListener('mousedown', this.foundation.onMouseDown);
+            },
+            unregisterEvent: () => {
+                this.resizeHandlerRef.current.removeEventListener('mousedown', this.foundation.onMouseDown);
+            },
+        };
+    }
+
+    resizeHandlerRef: React.RefObject<HTMLDivElement>
+    render() {
+        const { children, style, className } = this.props;
+        return (
+            <div 
+                className={classNames(className, prefixCls + '-resizableHandler')}
+                style={{
+                    ...directionStyles[this.props.direction],
+                    ...style
+                }} 
+                ref={this.resizeHandlerRef}
+            >
+                { children }
+            </div>
+        ); 
+    }
+}
+
+export default ResizableHandler;