@usefui/components 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +237 -9
- package/dist/index.d.ts +237 -9
- package/dist/index.js +1095 -311
- package/dist/index.mjs +1049 -278
- package/package.json +6 -6
- package/src/avatar/index.tsx +1 -1
- package/src/badge/index.tsx +3 -3
- package/src/breadcrumb/Breadcrumb.stories.tsx +36 -0
- package/src/breadcrumb/index.tsx +117 -0
- package/src/breadcrumb/styles/index.ts +19 -0
- package/src/button/Button.stories.tsx +80 -8
- package/src/button/index.tsx +67 -5
- package/src/button/styles/index.ts +82 -10
- package/src/card/Card.stories.tsx +57 -0
- package/src/card/index.tsx +55 -0
- package/src/card/styles/index.ts +72 -0
- package/src/copy-button/CopyButton.stories.tsx +29 -0
- package/src/copy-button/index.tsx +101 -0
- package/src/dialog/index.tsx +1 -1
- package/src/dropdown/index.tsx +1 -1
- package/src/field/Field.stories.tsx +39 -8
- package/src/field/index.tsx +5 -0
- package/src/field/styles/index.ts +38 -12
- package/src/index.ts +8 -0
- package/src/page/index.tsx +1 -1
- package/src/privacy-field/PrivacyField.stories.tsx +29 -0
- package/src/privacy-field/index.tsx +56 -0
- package/src/privacy-field/styles/index.ts +17 -0
- package/src/resizable/Resizable.stories.tsx +40 -0
- package/src/resizable/index.tsx +108 -0
- package/src/resizable/styles/index.ts +65 -0
- package/src/skeleton/Skeleton.stories.tsx +32 -0
- package/src/skeleton/index.tsx +43 -0
- package/src/skeleton/styles/index.ts +56 -0
- package/src/spinner/Spinner.stories.tsx +27 -0
- package/src/spinner/index.tsx +19 -0
- package/src/spinner/styles/index.ts +43 -0
- package/src/text-area/Textarea.stories.tsx +32 -0
- package/src/text-area/index.tsx +78 -0
- package/src/text-area/styles/index.ts +84 -0
- package/src/tooltip/index.tsx +4 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Page, Resizable } from "..";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resizable are used to render children into separated sections that can be resized by users.
|
|
7
|
+
*/
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Components/Resizable",
|
|
10
|
+
component: Resizable,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
} satisfies Meta<typeof Resizable>;
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
defaultWidth: 30,
|
|
19
|
+
left: (
|
|
20
|
+
<div
|
|
21
|
+
className="h-100 w-100 flex align-center justify-center"
|
|
22
|
+
style={{ borderRight: "1px solid var(--font-color-alpha-10)" }}
|
|
23
|
+
>
|
|
24
|
+
<p className="fs-medium-20">One</p>
|
|
25
|
+
</div>
|
|
26
|
+
),
|
|
27
|
+
right: (
|
|
28
|
+
<div className="h-100 w-100 flex align-center justify-center">
|
|
29
|
+
<p className="fs-medium-20">Two</p>
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
render: ({ ...args }) => (
|
|
34
|
+
<Page>
|
|
35
|
+
<Page.Content>
|
|
36
|
+
<Resizable {...args} />
|
|
37
|
+
</Page.Content>
|
|
38
|
+
</Page>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SplitContainer,
|
|
7
|
+
Panel,
|
|
8
|
+
Divider,
|
|
9
|
+
DragHandle,
|
|
10
|
+
DragIndicator,
|
|
11
|
+
DragOverlay,
|
|
12
|
+
} from "./styles";
|
|
13
|
+
|
|
14
|
+
type ResizableEditorProperties = {
|
|
15
|
+
defaultWidth?: number;
|
|
16
|
+
left: React.ReactNode;
|
|
17
|
+
right: React.ReactNode;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resizable are used to render children into separated sections that can be resized by users.
|
|
22
|
+
*
|
|
23
|
+
* @param {ResizableEditorProperties} props - The props for the Resizable component.
|
|
24
|
+
* @param {number} props.defaultWidth - The default width of the Resizable left section.
|
|
25
|
+
* @param {ReactNode} props.left - The content to be rendered inside the Left panel of the Resizable component.
|
|
26
|
+
* @param {ReactNode} props.right - The content to be rendered inside the Right panel of the Resizable component.
|
|
27
|
+
* @returns {ReactElement} The Resizable component.
|
|
28
|
+
*/
|
|
29
|
+
export const Resizable = ({
|
|
30
|
+
defaultWidth,
|
|
31
|
+
left,
|
|
32
|
+
right,
|
|
33
|
+
}: ResizableEditorProperties) => {
|
|
34
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
35
|
+
|
|
36
|
+
const [leftWidth, setLeftWidth] = React.useState(defaultWidth ?? 50); // Percentage
|
|
37
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
38
|
+
|
|
39
|
+
const handleMouseDown = React.useCallback(() => setIsDragging(true), []);
|
|
40
|
+
const handleMouseUp = React.useCallback(() => setIsDragging(false), []);
|
|
41
|
+
|
|
42
|
+
const handleMouseMove = React.useCallback(
|
|
43
|
+
(e: MouseEvent) => {
|
|
44
|
+
if (!isDragging || !containerRef.current) return;
|
|
45
|
+
|
|
46
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
47
|
+
const newLeftWidth =
|
|
48
|
+
((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
49
|
+
|
|
50
|
+
// Constrain between 20% and 80%
|
|
51
|
+
const threshold = { min: 30, max: 70 };
|
|
52
|
+
|
|
53
|
+
const constrainedWidth = Math.min(
|
|
54
|
+
Math.max(newLeftWidth, threshold.min),
|
|
55
|
+
threshold.max
|
|
56
|
+
);
|
|
57
|
+
setLeftWidth(constrainedWidth);
|
|
58
|
+
},
|
|
59
|
+
[isDragging]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (isDragging) {
|
|
64
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
65
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
66
|
+
document.body.style.cursor = "col-resize";
|
|
67
|
+
document.body.style.userSelect = "none";
|
|
68
|
+
} else {
|
|
69
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
70
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
71
|
+
document.body.style.cursor = "";
|
|
72
|
+
document.body.style.userSelect = "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
77
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
78
|
+
document.body.style.cursor = "";
|
|
79
|
+
document.body.style.userSelect = "";
|
|
80
|
+
};
|
|
81
|
+
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<React.Fragment>
|
|
85
|
+
<SplitContainer ref={containerRef} className="h-100 flex">
|
|
86
|
+
<Panel width={leftWidth}>{left}</Panel>
|
|
87
|
+
|
|
88
|
+
<Divider
|
|
89
|
+
$dragging={isDragging}
|
|
90
|
+
onMouseDown={handleMouseDown}
|
|
91
|
+
onTouchStart={handleMouseDown}
|
|
92
|
+
>
|
|
93
|
+
<DragHandle
|
|
94
|
+
className="flex align-center justify-center"
|
|
95
|
+
id="drag-handle"
|
|
96
|
+
>
|
|
97
|
+
<DragIndicator className="drag-indicator-handle" />
|
|
98
|
+
</DragHandle>
|
|
99
|
+
</Divider>
|
|
100
|
+
|
|
101
|
+
<Panel width={100 - leftWidth}>{right}</Panel>
|
|
102
|
+
</SplitContainer>
|
|
103
|
+
|
|
104
|
+
{isDragging && <DragOverlay />}
|
|
105
|
+
</React.Fragment>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
Resizable.displayName = "Resizable";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
|
|
5
|
+
export const SplitContainer = styled.div`
|
|
6
|
+
position: relative;
|
|
7
|
+
`;
|
|
8
|
+
export const Panel = styled.div<{ width: number }>`
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
width: ${(props) => props.width}%;
|
|
11
|
+
`;
|
|
12
|
+
export const Divider = styled.div<{ $dragging: boolean }>`
|
|
13
|
+
width: var(--measurement-medium-10);
|
|
14
|
+
height: 100%;
|
|
15
|
+
top: 0;
|
|
16
|
+
|
|
17
|
+
border-radius: var(--measurement-medium-60);
|
|
18
|
+
background-color: transparent;
|
|
19
|
+
|
|
20
|
+
/* background-color: ${(props) =>
|
|
21
|
+
props.$dragging ? "var(--font-color-alpha-10)" : "transparent"}; */
|
|
22
|
+
|
|
23
|
+
cursor: col-resize;
|
|
24
|
+
transition: background-color 0.2s;
|
|
25
|
+
position: relative;
|
|
26
|
+
|
|
27
|
+
/** Shows DragIndicator on hover */
|
|
28
|
+
&:hover .drag-indicator-handle,
|
|
29
|
+
&:active .drag-indicator-handle {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.drag-indicator-handle {
|
|
34
|
+
height: ${(props) =>
|
|
35
|
+
props.$dragging
|
|
36
|
+
? "var(--measurement-large-10)"
|
|
37
|
+
: "var(--measurement-medium-60)"};
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
export const DragHandle = styled.div`
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 0;
|
|
43
|
+
bottom: 0;
|
|
44
|
+
left: calc(var(--measurement-medium-10) * -1);
|
|
45
|
+
right: calc(var(--measurement-medium-10) * -1);
|
|
46
|
+
`;
|
|
47
|
+
export const DragIndicator = styled.div`
|
|
48
|
+
position: fixed;
|
|
49
|
+
width: var(--measurement-medium-10);
|
|
50
|
+
/* height: var(--measurement-medium-60); */
|
|
51
|
+
background-color: var(--font-color-alpha-60);
|
|
52
|
+
border-radius: var(--measurement-large-10);
|
|
53
|
+
|
|
54
|
+
opacity: 0;
|
|
55
|
+
transition: all 0.2s;
|
|
56
|
+
`;
|
|
57
|
+
export const DragOverlay = styled.div`
|
|
58
|
+
position: fixed;
|
|
59
|
+
top: 0;
|
|
60
|
+
left: 0;
|
|
61
|
+
right: 0;
|
|
62
|
+
bottom: 0;
|
|
63
|
+
z-index: var(--depth-default-90);
|
|
64
|
+
cursor: col-resize;
|
|
65
|
+
`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Page, Skeleton } from "..";
|
|
4
|
+
|
|
5
|
+
/** */
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Skeleton",
|
|
8
|
+
component: Skeleton,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof Skeleton>;
|
|
11
|
+
export default meta;
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {},
|
|
16
|
+
render: ({ ...args }) => (
|
|
17
|
+
<Page>
|
|
18
|
+
<Page.Content className="flex flex-column g-medium-30">
|
|
19
|
+
<div className="flex align-center justify-center ">
|
|
20
|
+
<Skeleton sizing="small" />
|
|
21
|
+
<Skeleton sizing="medium" />
|
|
22
|
+
<Skeleton sizing="large" />
|
|
23
|
+
</div>
|
|
24
|
+
<div className="flex align-center justify-center">
|
|
25
|
+
<Skeleton shape="square" />
|
|
26
|
+
<Skeleton shape="smooth" />
|
|
27
|
+
<Skeleton shape="round" />
|
|
28
|
+
</div>
|
|
29
|
+
</Page.Content>
|
|
30
|
+
</Page>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { SkeletonLoader } from "./styles";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ComponentSizeEnum,
|
|
8
|
+
type IComponentSize,
|
|
9
|
+
type TComponentShape,
|
|
10
|
+
} from "../../../../types";
|
|
11
|
+
|
|
12
|
+
export interface SkeletonProperties
|
|
13
|
+
extends IComponentSize,
|
|
14
|
+
React.ComponentPropsWithRef<"span"> {
|
|
15
|
+
shape?: TComponentShape;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Skeletons are used to convoy a loading state information.
|
|
20
|
+
*
|
|
21
|
+
* @param {IButtonProperties} props - The props for the Skeleton component.
|
|
22
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to `medium`.
|
|
23
|
+
* @param {TComponentShape} props.shape - The size of the component. Defaults to `smooth`.
|
|
24
|
+
* @returns {ReactElement} The Skeleton component.
|
|
25
|
+
*/
|
|
26
|
+
export const Skeleton = (props: SkeletonProperties): React.ReactElement => {
|
|
27
|
+
const {
|
|
28
|
+
sizing = ComponentSizeEnum.Medium,
|
|
29
|
+
shape = "smooth",
|
|
30
|
+
...restProps
|
|
31
|
+
} = props;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<SkeletonLoader
|
|
35
|
+
data-size={sizing}
|
|
36
|
+
data-shape={shape}
|
|
37
|
+
tabIndex={0}
|
|
38
|
+
{...restProps}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Skeleton.displayName = "Skeleton";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import styled, { css, keyframes } from "styled-components";
|
|
4
|
+
|
|
5
|
+
const SkeletonBlink = keyframes`
|
|
6
|
+
0% {
|
|
7
|
+
opacity: 0.3;
|
|
8
|
+
}
|
|
9
|
+
100% {
|
|
10
|
+
opacity: 0.1;
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const SkeletonBaseStyles = css`
|
|
15
|
+
background: linear-gradient(
|
|
16
|
+
45deg,
|
|
17
|
+
var(--font-color-alpha-10),
|
|
18
|
+
var(--font-color-alpha-20)
|
|
19
|
+
);
|
|
20
|
+
animation: ${SkeletonBlink} 1s ease-in-out alternate-reverse infinite;
|
|
21
|
+
`;
|
|
22
|
+
const SkeletonSizeStyles = css`
|
|
23
|
+
&[data-size="small"] {
|
|
24
|
+
width: 100%;
|
|
25
|
+
|
|
26
|
+
min-width: var(--measurement-medium-60);
|
|
27
|
+
min-height: var(--measurement-medium-70);
|
|
28
|
+
}
|
|
29
|
+
&[data-size="medium"] {
|
|
30
|
+
width: 100%;
|
|
31
|
+
min-width: var(--measurement-medium-90);
|
|
32
|
+
min-height: var(--measurement-medium-80);
|
|
33
|
+
}
|
|
34
|
+
&[data-size="large"] {
|
|
35
|
+
width: 100%;
|
|
36
|
+
min-width: var(--measurement-medium-90);
|
|
37
|
+
min-height: var(--measurement-medium-90);
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
const SkeletonShapeStyles = css`
|
|
41
|
+
&[data-shape="square"] {
|
|
42
|
+
border-radius: 0;
|
|
43
|
+
}
|
|
44
|
+
&[data-shape="smooth"] {
|
|
45
|
+
border-radius: var(--measurement-medium-20);
|
|
46
|
+
}
|
|
47
|
+
&[data-shape="round"] {
|
|
48
|
+
border-radius: var(--measurement-large-90);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
export const SkeletonLoader = styled.span`
|
|
53
|
+
${SkeletonBaseStyles}
|
|
54
|
+
${SkeletonSizeStyles}
|
|
55
|
+
${SkeletonShapeStyles}
|
|
56
|
+
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Page, Spinner } from "..";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Spinners are used to convey a pending state.
|
|
7
|
+
*/
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Components/Spinner",
|
|
10
|
+
component: Spinner,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
} satisfies Meta<typeof Spinner>;
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
args: {},
|
|
18
|
+
render: ({ ...args }) => (
|
|
19
|
+
<Page>
|
|
20
|
+
<Page.Content className="g-medium-60 flex align-center justify-center h-100 w-100">
|
|
21
|
+
<Spinner sizing="small" {...args} />
|
|
22
|
+
<Spinner sizing="medium" {...args} />
|
|
23
|
+
<Spinner sizing="large" {...args} />
|
|
24
|
+
</Page.Content>
|
|
25
|
+
</Page>
|
|
26
|
+
),
|
|
27
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import { RotatingSpinner } from "./styles";
|
|
6
|
+
import type { IComponentSize } from "../../../../types";
|
|
7
|
+
|
|
8
|
+
interface SpinnerProperties extends IComponentSize {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Spinners are used to convoy a loading state information.
|
|
12
|
+
*
|
|
13
|
+
* @param {TextareaProps} props - The props for the Spinner component.
|
|
14
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to `medium`.
|
|
15
|
+
* @returns {ReactElement} The Spinner component.
|
|
16
|
+
*/
|
|
17
|
+
export const Spinner = (props: SpinnerProperties) => {
|
|
18
|
+
return <RotatingSpinner data-size={props.sizing ?? "medium"} />;
|
|
19
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import styled, { css, keyframes } from "styled-components";
|
|
4
|
+
|
|
5
|
+
const Rotate = keyframes`
|
|
6
|
+
0% {
|
|
7
|
+
transform: rotate(0deg);
|
|
8
|
+
}
|
|
9
|
+
100% {
|
|
10
|
+
transform: rotate(360deg);
|
|
11
|
+
}
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const FieldSizeStyles = css`
|
|
15
|
+
&[data-size="small"] {
|
|
16
|
+
width: var(--measurement-medium-40);
|
|
17
|
+
height: var(--measurement-medium-40);
|
|
18
|
+
}
|
|
19
|
+
&[data-size="medium"] {
|
|
20
|
+
width: var(--measurement-medium-50);
|
|
21
|
+
height: var(--measurement-medium-50);
|
|
22
|
+
}
|
|
23
|
+
&[data-size="large"] {
|
|
24
|
+
width: var(--measurement-medium-60);
|
|
25
|
+
height: var(--measurement-medium-60);
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export const RotatingSpinner = styled.span`
|
|
30
|
+
padding: 0;
|
|
31
|
+
margin: 0;
|
|
32
|
+
|
|
33
|
+
display: inline-block;
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
|
|
36
|
+
border: var(--measurement-small-60) solid var(--font-color-alpha-30);
|
|
37
|
+
border-bottom-color: transparent;
|
|
38
|
+
border-radius: var(--measurement-large-90);
|
|
39
|
+
|
|
40
|
+
animation: ${Rotate} 1s linear infinite;
|
|
41
|
+
|
|
42
|
+
${FieldSizeStyles}
|
|
43
|
+
`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
|
|
4
|
+
import { Page, Textarea } from "..";
|
|
5
|
+
import { TComponentSize } from "../../../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Textarea are used to allow users to write large chunks of text.
|
|
9
|
+
*/
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Components/Textarea",
|
|
12
|
+
component: Textarea,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
} satisfies Meta<typeof Textarea>;
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
variant: "secondary",
|
|
21
|
+
resizable: false,
|
|
22
|
+
},
|
|
23
|
+
render: ({ ...args }) => (
|
|
24
|
+
<Page>
|
|
25
|
+
<Page.Content className="p-large-30 flex flex-column g-large-10">
|
|
26
|
+
{["small", "medium", "large"].map((size) => (
|
|
27
|
+
<Textarea key={size} sizing={size as TComponentSize} {...args} />
|
|
28
|
+
))}
|
|
29
|
+
</Page.Content>
|
|
30
|
+
</Page>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import { TextAreaContainer } from "./styles";
|
|
6
|
+
import {
|
|
7
|
+
IComponentSize,
|
|
8
|
+
IComponentStyling,
|
|
9
|
+
IComponentVariant,
|
|
10
|
+
TComponentShape,
|
|
11
|
+
ComponentSizeEnum,
|
|
12
|
+
} from "../../../../types";
|
|
13
|
+
|
|
14
|
+
export type ScrollContainerProps = {
|
|
15
|
+
$height?: string;
|
|
16
|
+
$width?: string;
|
|
17
|
+
$thumbColor?: string;
|
|
18
|
+
$trackColor?: string;
|
|
19
|
+
$thumbHoverColor?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface TextareaProps
|
|
23
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
24
|
+
IComponentStyling,
|
|
25
|
+
IComponentSize,
|
|
26
|
+
IComponentVariant {
|
|
27
|
+
resizable?: boolean;
|
|
28
|
+
shape?: "square" | "smooth";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Textarea are used to allow users to write large chunks of text.
|
|
33
|
+
*
|
|
34
|
+
* @param {TextareaProps} props - The props for the Textarea component.
|
|
35
|
+
* @param {string} props.variant - The style definition used by the component. Defaults to `secondary`.
|
|
36
|
+
* @param {TComponentShape} props.shape - The size of the component. Defaults to `smooth`.
|
|
37
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to `medium`.
|
|
38
|
+
* @param {boolean} props.resizable - Define whether the component is resizable or not. Defaults to 'true'.
|
|
39
|
+
*
|
|
40
|
+
* @returns {ReactElement} The TextareaProps component.
|
|
41
|
+
*/
|
|
42
|
+
export const Textarea = (props: TextareaProps) => {
|
|
43
|
+
const { raw, shape, sizing, variant, resizable, onChange } = props;
|
|
44
|
+
|
|
45
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
46
|
+
|
|
47
|
+
const adjustHeight = () => {
|
|
48
|
+
const textarea = textareaRef.current;
|
|
49
|
+
if (textarea) {
|
|
50
|
+
textarea.style.height = "auto";
|
|
51
|
+
textarea.style.height = `${Math.min(
|
|
52
|
+
textarea.scrollHeight,
|
|
53
|
+
parseInt(getComputedStyle(textarea).maxHeight)
|
|
54
|
+
)}px`;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
59
|
+
adjustHeight();
|
|
60
|
+
onChange?.(e);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => adjustHeight(), [props.value]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<TextAreaContainer
|
|
67
|
+
ref={textareaRef}
|
|
68
|
+
onChange={handleChange}
|
|
69
|
+
data-variant={variant ?? "secondary"}
|
|
70
|
+
data-shape={shape ?? "smooth"}
|
|
71
|
+
data-size={sizing ?? ComponentSizeEnum.Medium}
|
|
72
|
+
data-resizable={resizable}
|
|
73
|
+
data-raw={String(Boolean(raw))}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
Textarea.displayName = "Textarea";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import styled, { css } from "styled-components";
|
|
4
|
+
import {
|
|
5
|
+
FieldShapeStyles,
|
|
6
|
+
FieldDefaultStyles,
|
|
7
|
+
FieldVariantsStyles,
|
|
8
|
+
} from "../../field/styles";
|
|
9
|
+
|
|
10
|
+
import type { ScrollContainerProps } from "text-area";
|
|
11
|
+
|
|
12
|
+
const CustomScrollbar = css<ScrollContainerProps>`
|
|
13
|
+
height: ${({ $height }) => $height ?? "100%"};
|
|
14
|
+
width: ${({ $width }) => $width ?? "100%"};
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
overflow-x: hidden;
|
|
17
|
+
|
|
18
|
+
&::-webkit-scrollbar {
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
|
|
21
|
+
width: var(--measurement-medium-10);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&::-webkit-scrollbar-track {
|
|
25
|
+
background: ${({ $trackColor }) => $trackColor ?? "transparent"};
|
|
26
|
+
border-radius: var(--measurement-medium-30);
|
|
27
|
+
border: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&::-webkit-scrollbar-thumb {
|
|
31
|
+
background: ${({ $thumbColor }) =>
|
|
32
|
+
$thumbColor ?? "var(--font-color-alpha-10)"};
|
|
33
|
+
border-radius: var(--measurement-medium-30);
|
|
34
|
+
transition: background-color 0.2s ease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&::-webkit-scrollbar-thumb:hover {
|
|
38
|
+
background: ${({ $thumbHoverColor, $thumbColor }) =>
|
|
39
|
+
$thumbHoverColor ?? $thumbColor ?? "var(--font-color-alpha-20)"};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Firefox
|
|
43
|
+
scrollbar-width: thin;
|
|
44
|
+
scrollbar-color: ${({ $thumbColor, $trackColor }) =>
|
|
45
|
+
`${$thumbColor ?? "var(--font-color-alpha-10)"} ${
|
|
46
|
+
$trackColor ?? "transparent"
|
|
47
|
+
}`};
|
|
48
|
+
`;
|
|
49
|
+
const TextareaSizeStyles = css`
|
|
50
|
+
padding: var(--measurement-medium-30);
|
|
51
|
+
max-height: var(--measurement-large-60);
|
|
52
|
+
|
|
53
|
+
&[data-size="small"] {
|
|
54
|
+
min-height: var(--measurement-large-30);
|
|
55
|
+
}
|
|
56
|
+
&[data-size="medium"] {
|
|
57
|
+
min-height: var(--measurement-large-50);
|
|
58
|
+
}
|
|
59
|
+
&[data-size="large"] {
|
|
60
|
+
min-height: var(--measurement-large-60);
|
|
61
|
+
max-height: var(--measurement-large-80);
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
const TextareaResizableStyles = css`
|
|
65
|
+
&[data-resizable="true"] {
|
|
66
|
+
resize: vertical;
|
|
67
|
+
}
|
|
68
|
+
&[data-resizable="false"] {
|
|
69
|
+
resize: none;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export const TextAreaContainer = styled.textarea<ScrollContainerProps>`
|
|
74
|
+
&[data-raw="false"] {
|
|
75
|
+
${FieldDefaultStyles}
|
|
76
|
+
${FieldShapeStyles}
|
|
77
|
+
|
|
78
|
+
${TextareaSizeStyles}
|
|
79
|
+
${TextareaResizableStyles}
|
|
80
|
+
|
|
81
|
+
${CustomScrollbar}
|
|
82
|
+
${FieldVariantsStyles}
|
|
83
|
+
}
|
|
84
|
+
`;
|
package/src/tooltip/index.tsx
CHANGED
|
@@ -59,11 +59,12 @@ const Tooltip = ({
|
|
|
59
59
|
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
60
60
|
|
|
61
61
|
const contentRect = () => contentRef?.current?.getBoundingClientRect();
|
|
62
|
-
const bodyRect =
|
|
62
|
+
const bodyRect = (): DOMRect | undefined => {
|
|
63
63
|
if (typeof document !== "undefined") {
|
|
64
|
-
return document
|
|
64
|
+
return document?.body?.getBoundingClientRect();
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
return undefined;
|
|
67
|
+
};
|
|
67
68
|
|
|
68
69
|
const positions = {
|
|
69
70
|
btt: `calc((${triggerProps?.top}px - ${contentProps?.height}px) - (var(--measurement-medium-10)))`,
|