@usefui/components 1.5.2 → 1.6.0
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 +246 -3
- package/dist/index.d.ts +246 -3
- package/dist/index.js +740 -307
- package/dist/index.mjs +708 -288
- package/package.json +12 -12
- package/src/__tests__/MessageBubble.test.tsx +179 -0
- package/src/__tests__/Shimmer.test.tsx +122 -0
- package/src/__tests__/Tree.test.tsx +275 -0
- package/src/accordion/hooks/index.tsx +3 -1
- package/src/badge/index.tsx +2 -3
- package/src/checkbox/hooks/index.tsx +5 -1
- package/src/collapsible/hooks/index.tsx +3 -1
- package/src/dialog/hooks/index.tsx +5 -1
- package/src/dropdown/hooks/index.tsx +3 -1
- package/src/dropdown/index.tsx +9 -9
- package/src/field/hooks/index.tsx +5 -1
- package/src/field/styles/index.ts +1 -0
- package/src/index.ts +6 -0
- package/src/message-bubble/MessageBubble.stories.tsx +91 -0
- package/src/message-bubble/hooks/index.tsx +41 -0
- package/src/message-bubble/index.tsx +153 -0
- package/src/message-bubble/styles/index.ts +61 -0
- package/src/otp-field/hooks/index.tsx +3 -1
- package/src/otp-field/index.tsx +5 -3
- package/src/sheet/hooks/index.tsx +5 -1
- package/src/shimmer/Shimmer.stories.tsx +95 -0
- package/src/shimmer/index.tsx +64 -0
- package/src/shimmer/styles/index.ts +33 -0
- package/src/switch/hooks/index.tsx +5 -1
- package/src/tabs/hooks/index.tsx +5 -1
- package/src/text-area/Textarea.stories.tsx +7 -2
- package/src/text-area/index.tsx +30 -14
- package/src/text-area/styles/index.ts +32 -72
- package/src/toolbar/hooks/index.tsx +5 -1
- package/src/tooltip/index.tsx +4 -3
- package/src/tree/Tree.stories.tsx +139 -0
- package/src/tree/hooks/tree-node-provider.tsx +50 -0
- package/src/tree/hooks/tree-provider.tsx +75 -0
- package/src/tree/index.tsx +231 -0
- package/src/tree/styles/index.ts +23 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +1 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { TextShimmerWrapper } from "./styles";
|
|
5
|
+
import { IComponentStyling } from "../../../../types";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DURATION = 2;
|
|
8
|
+
const DEFAULT_SPREAD = 200;
|
|
9
|
+
const DEFAULT_SHIMMER_COLOR = "var(--font-color-alpha-60)";
|
|
10
|
+
const DEFAULT_BASE_COLOR = "var(--font-color-alpha-30)";
|
|
11
|
+
|
|
12
|
+
export interface ITextShimmerProperties
|
|
13
|
+
extends IComponentStyling, React.HTMLAttributes<HTMLSpanElement> {
|
|
14
|
+
duration?: number;
|
|
15
|
+
spread?: number;
|
|
16
|
+
shimmerColor?: string;
|
|
17
|
+
baseColor?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Shimmer applies an animated shimmer gradient effect to inline text content.
|
|
22
|
+
*
|
|
23
|
+
* **Best practices:**
|
|
24
|
+
*
|
|
25
|
+
* - Use to indicate loading states for text content.
|
|
26
|
+
* - Prefer CSS custom properties for `shimmerColor` and `baseColor` to stay consistent with your design tokens.
|
|
27
|
+
* - Avoid using on large blocks of text; favour short labels or headings.
|
|
28
|
+
*
|
|
29
|
+
* @param {ITextShimmerProperties} props - The props for the Shimmer component.
|
|
30
|
+
* @param {ReactNode} props.children - The text content to apply the shimmer effect to.
|
|
31
|
+
* @param {boolean} props.raw - Whether the component is unstyled.
|
|
32
|
+
* @param {number} props.duration - Animation cycle duration in seconds. Defaults to 2.
|
|
33
|
+
* @param {number} props.spread - Gradient spread width as a percentage. Defaults to 200.
|
|
34
|
+
* @param {string} props.shimmerColor - Highlight color of the shimmer. Defaults to `--font-color-alpha-60`.
|
|
35
|
+
* @param {string} props.baseColor - Base text gradient color. Defaults to `--font-color-alpha-30`.
|
|
36
|
+
* @returns {ReactElement} The Shimmer component.
|
|
37
|
+
*/
|
|
38
|
+
export const Shimmer = (props: ITextShimmerProperties) => {
|
|
39
|
+
const {
|
|
40
|
+
children,
|
|
41
|
+
raw,
|
|
42
|
+
duration = DEFAULT_DURATION,
|
|
43
|
+
spread = DEFAULT_SPREAD,
|
|
44
|
+
shimmerColor = DEFAULT_SHIMMER_COLOR,
|
|
45
|
+
baseColor = DEFAULT_BASE_COLOR,
|
|
46
|
+
...restProps
|
|
47
|
+
} = props;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<TextShimmerWrapper
|
|
51
|
+
data-raw={Boolean(raw)}
|
|
52
|
+
data-duration={duration}
|
|
53
|
+
data-spread={spread}
|
|
54
|
+
data-shimmer-color={shimmerColor}
|
|
55
|
+
data-base-color={baseColor}
|
|
56
|
+
aria-label={restProps["aria-label"] ?? "shimmer-text"}
|
|
57
|
+
{...restProps}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</TextShimmerWrapper>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
Shimmer.displayName = "Shimmer";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import styled, { keyframes } from "styled-components";
|
|
2
|
+
|
|
3
|
+
const shimmer = keyframes`
|
|
4
|
+
0% {
|
|
5
|
+
background-position: 200% center;
|
|
6
|
+
}
|
|
7
|
+
100% {
|
|
8
|
+
background-position: -200% center;
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export const TextShimmerWrapper = styled.span<{
|
|
13
|
+
"data-duration": number;
|
|
14
|
+
"data-spread": number;
|
|
15
|
+
"data-shimmer-color": string;
|
|
16
|
+
"data-base-color": string;
|
|
17
|
+
}>`
|
|
18
|
+
background: linear-gradient(
|
|
19
|
+
90deg,
|
|
20
|
+
${({ "data-base-color": baseColor }) => baseColor} 0%,
|
|
21
|
+
${({ "data-shimmer-color": shimmerColor }) => shimmerColor} 40%,
|
|
22
|
+
${({ "data-base-color": baseColor }) => baseColor} 60%,
|
|
23
|
+
${({ "data-base-color": baseColor }) => baseColor} 100%
|
|
24
|
+
);
|
|
25
|
+
background-size: ${({ "data-spread": spread }) => spread}% auto;
|
|
26
|
+
background-clip: text;
|
|
27
|
+
-webkit-background-clip: text;
|
|
28
|
+
-webkit-text-fill-color: transparent;
|
|
29
|
+
color: transparent;
|
|
30
|
+
animation: ${shimmer} ${({ "data-duration": duration }) => duration}s linear
|
|
31
|
+
infinite;
|
|
32
|
+
display: inline-block;
|
|
33
|
+
`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import React, { useState, createContext, useContext } from "react";
|
|
2
4
|
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
3
5
|
|
|
@@ -9,7 +11,9 @@ const defaultComponentAPI = {
|
|
|
9
11
|
const SwitchContext = createContext<IComponentAPI>(defaultComponentAPI);
|
|
10
12
|
export const useSwitch = () => useContext(SwitchContext);
|
|
11
13
|
|
|
12
|
-
export const SwitchProvider = ({
|
|
14
|
+
export const SwitchProvider = ({
|
|
15
|
+
children,
|
|
16
|
+
}: IReactChildren): React.JSX.Element => {
|
|
13
17
|
const context = useSwitchProvider();
|
|
14
18
|
|
|
15
19
|
return (
|
package/src/tabs/hooks/index.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import React, { createContext, useContext, useState } from "react";
|
|
2
4
|
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
3
5
|
|
|
@@ -9,7 +11,9 @@ const defaultComponentAPI = {
|
|
|
9
11
|
const TabsContext = createContext<IComponentAPI>(defaultComponentAPI);
|
|
10
12
|
export const useTabs = () => useContext(TabsContext);
|
|
11
13
|
|
|
12
|
-
export const TabsProvider = ({
|
|
14
|
+
export const TabsProvider = ({
|
|
15
|
+
children,
|
|
16
|
+
}: IReactChildren): React.JSX.Element => {
|
|
13
17
|
const context = useTabsProvider();
|
|
14
18
|
|
|
15
19
|
return (
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
|
|
3
4
|
import { Page, Textarea } from "..";
|
|
5
|
+
import { TComponentSize } from "../../../../types";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Textarea are used to allow users to write large chunks of text.
|
|
@@ -16,11 +18,14 @@ type Story = StoryObj<typeof meta>;
|
|
|
16
18
|
export const Default: Story = {
|
|
17
19
|
args: {
|
|
18
20
|
variant: "secondary",
|
|
21
|
+
resizable: false,
|
|
19
22
|
},
|
|
20
23
|
render: ({ ...args }) => (
|
|
21
24
|
<Page>
|
|
22
|
-
<Page.Content className="p-large-30">
|
|
23
|
-
|
|
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
|
+
))}
|
|
24
29
|
</Page.Content>
|
|
25
30
|
</Page>
|
|
26
31
|
),
|
package/src/text-area/index.tsx
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
|
|
5
5
|
import { TextAreaContainer } from "./styles";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
IComponentSize,
|
|
8
|
+
IComponentStyling,
|
|
9
|
+
IComponentVariant,
|
|
10
|
+
TComponentShape,
|
|
11
|
+
ComponentSizeEnum,
|
|
12
|
+
} from "../../../../types";
|
|
9
13
|
|
|
10
14
|
export type ScrollContainerProps = {
|
|
11
15
|
$height?: string;
|
|
@@ -18,15 +22,26 @@ export type ScrollContainerProps = {
|
|
|
18
22
|
export interface TextareaProps
|
|
19
23
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
20
24
|
IComponentStyling,
|
|
21
|
-
|
|
25
|
+
IComponentSize,
|
|
26
|
+
IComponentVariant {
|
|
27
|
+
resizable?: boolean;
|
|
28
|
+
shape?: "square" | "smooth";
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* Textarea are used to allow users to write large chunks of text.
|
|
25
33
|
*
|
|
26
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
|
+
*
|
|
27
40
|
* @returns {ReactElement} The TextareaProps component.
|
|
28
41
|
*/
|
|
29
|
-
export const Textarea = (
|
|
42
|
+
export const Textarea = (props: TextareaProps) => {
|
|
43
|
+
const { raw, shape, sizing, variant, resizable, onChange } = props;
|
|
44
|
+
|
|
30
45
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
31
46
|
|
|
32
47
|
const adjustHeight = () => {
|
|
@@ -48,15 +63,16 @@ export const Textarea = ({ onChange, ...props }: TextareaProps) => {
|
|
|
48
63
|
React.useEffect(() => adjustHeight(), [props.value]);
|
|
49
64
|
|
|
50
65
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
/>
|
|
60
76
|
);
|
|
61
77
|
};
|
|
62
78
|
Textarea.displayName = "Textarea";
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import styled, { css } from "styled-components";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FieldShapeStyles,
|
|
6
|
+
FieldDefaultStyles,
|
|
7
|
+
FieldVariantsStyles,
|
|
8
|
+
} from "../../field/styles";
|
|
5
9
|
|
|
6
10
|
import type { ScrollContainerProps } from "text-area";
|
|
7
11
|
|
|
@@ -42,81 +46,37 @@ const CustomScrollbar = css<ScrollContainerProps>`
|
|
|
42
46
|
$trackColor ?? "transparent"
|
|
43
47
|
}`};
|
|
44
48
|
`;
|
|
49
|
+
const TextareaSizeStyles = css`
|
|
50
|
+
padding: var(--measurement-medium-30);
|
|
51
|
+
max-height: var(--measurement-large-60);
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
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"] {
|
|
48
66
|
resize: vertical;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
outline: none;
|
|
56
|
-
cursor: pointer;
|
|
57
|
-
display: flex;
|
|
58
|
-
align-items: center;
|
|
59
|
-
justify-content: center;
|
|
60
|
-
|
|
61
|
-
font-size: var(--fontsize-small-80);
|
|
62
|
-
padding: var(--measurement-medium-30) var(--measurement-medium-30)
|
|
63
|
-
var(--measurement-large-10) var(--measurement-medium-30);
|
|
64
|
-
|
|
65
|
-
font-weight: 500;
|
|
66
|
-
line-height: 1.1;
|
|
67
|
-
letter-spacing: calc(
|
|
68
|
-
var(--fontsize-small-10) - ((var(--fontsize-small-10) * 1.066))
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
border: var(--measurement-small-10) solid transparent;
|
|
72
|
-
border-radius: var(--measurement-medium-30);
|
|
73
|
-
backdrop-filter: blur(var(--measurement-small-10));
|
|
74
|
-
color: var(--font-color-alpha-60);
|
|
75
|
-
|
|
76
|
-
transition: all ease-in-out 0.2s;
|
|
77
|
-
|
|
78
|
-
svg,
|
|
79
|
-
span,
|
|
80
|
-
img {
|
|
81
|
-
opacity: 0.6;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
&:hover,
|
|
85
|
-
&:focus,
|
|
86
|
-
&:active {
|
|
87
|
-
color: var(--font-color);
|
|
88
|
-
|
|
89
|
-
svg,
|
|
90
|
-
span,
|
|
91
|
-
img {
|
|
92
|
-
opacity: 1;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
&::placeholder {
|
|
96
|
-
color: var(--font-color-alpha-30);
|
|
97
|
-
}
|
|
98
|
-
&:disabled {
|
|
99
|
-
cursor: not-allowed;
|
|
100
|
-
opacity: 0.6;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
&::placeholder {
|
|
104
|
-
color: var(--font-color-alpha-30);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
&:focus-visible {
|
|
108
|
-
outline: none;
|
|
109
|
-
}
|
|
67
|
+
}
|
|
68
|
+
&[data-resizable="false"] {
|
|
69
|
+
resize: none;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
110
72
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
73
|
+
export const TextAreaContainer = styled.textarea<ScrollContainerProps>`
|
|
74
|
+
&[data-raw="false"] {
|
|
75
|
+
${FieldDefaultStyles}
|
|
76
|
+
${FieldShapeStyles}
|
|
114
77
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
color: var(--alpha-red-30);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
78
|
+
${TextareaSizeStyles}
|
|
79
|
+
${TextareaResizableStyles}
|
|
120
80
|
|
|
121
81
|
${CustomScrollbar}
|
|
122
82
|
${FieldVariantsStyles}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import React, { useState, createContext, useContext } from "react";
|
|
2
4
|
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
3
5
|
|
|
@@ -9,7 +11,9 @@ const defaultComponentAPI = {
|
|
|
9
11
|
const ToolbarContext = createContext<IComponentAPI>(defaultComponentAPI);
|
|
10
12
|
export const useToolbar = () => useContext(ToolbarContext);
|
|
11
13
|
|
|
12
|
-
export const ToolbarProvider = ({
|
|
14
|
+
export const ToolbarProvider = ({
|
|
15
|
+
children,
|
|
16
|
+
}: IReactChildren): React.JSX.Element => {
|
|
13
17
|
const context = useToolbarProvider();
|
|
14
18
|
|
|
15
19
|
return (
|
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)))`,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
|
|
4
|
+
import { Tree } from "..";
|
|
5
|
+
import { ComponentSizeEnum, ComponentVariantEnum } from "../../../../types";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Components/Tree",
|
|
9
|
+
component: Tree,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div className="m-medium-30">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
} satisfies Meta<typeof Tree>;
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
argTypes: {
|
|
25
|
+
spacing: {
|
|
26
|
+
options: [
|
|
27
|
+
ComponentSizeEnum.Small,
|
|
28
|
+
ComponentSizeEnum.Medium,
|
|
29
|
+
ComponentSizeEnum.Large,
|
|
30
|
+
],
|
|
31
|
+
control: { type: "radio" },
|
|
32
|
+
},
|
|
33
|
+
variant: {
|
|
34
|
+
options: [
|
|
35
|
+
ComponentVariantEnum.Primary,
|
|
36
|
+
ComponentVariantEnum.Secondary,
|
|
37
|
+
ComponentVariantEnum.Tertiary,
|
|
38
|
+
ComponentVariantEnum.Mono,
|
|
39
|
+
ComponentVariantEnum.Border,
|
|
40
|
+
ComponentVariantEnum.Ghost,
|
|
41
|
+
],
|
|
42
|
+
control: { type: "radio" },
|
|
43
|
+
},
|
|
44
|
+
sizing: {
|
|
45
|
+
options: [
|
|
46
|
+
ComponentSizeEnum.Small,
|
|
47
|
+
ComponentSizeEnum.Medium,
|
|
48
|
+
ComponentSizeEnum.Large,
|
|
49
|
+
],
|
|
50
|
+
control: { type: "radio" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
render: ({ ...args }) => (
|
|
54
|
+
<Tree.Root>
|
|
55
|
+
<Tree>
|
|
56
|
+
<Tree.Node nodeId="src">
|
|
57
|
+
<Tree.Trigger nodeId="src">src</Tree.Trigger>
|
|
58
|
+
<Tree.Content nodeId="src">
|
|
59
|
+
<Tree.Node level={1} nodeId="index.ts">
|
|
60
|
+
<Tree.Trigger nodeId="index.ts">index.ts</Tree.Trigger>
|
|
61
|
+
</Tree.Node>
|
|
62
|
+
</Tree.Content>
|
|
63
|
+
</Tree.Node>
|
|
64
|
+
</Tree>
|
|
65
|
+
</Tree.Root>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const DefaultOpen: Story = {
|
|
70
|
+
render: ({ ...args }) => (
|
|
71
|
+
<Tree.Root>
|
|
72
|
+
<Tree>
|
|
73
|
+
<Tree.Node nodeId="src">
|
|
74
|
+
<Tree.Trigger nodeId="src">src</Tree.Trigger>
|
|
75
|
+
<Tree.Content nodeId="src" defaultOpen>
|
|
76
|
+
<Tree.Node level={1} nodeId="index.ts">
|
|
77
|
+
<Tree.Trigger nodeId="index.ts">index.ts</Tree.Trigger>
|
|
78
|
+
</Tree.Node>
|
|
79
|
+
</Tree.Content>
|
|
80
|
+
</Tree.Node>
|
|
81
|
+
</Tree>
|
|
82
|
+
</Tree.Root>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const Nested: Story = {
|
|
87
|
+
render: ({ ...args }) => (
|
|
88
|
+
<Tree.Root>
|
|
89
|
+
<Tree>
|
|
90
|
+
<Tree.Node nodeId="src">
|
|
91
|
+
<Tree.Trigger nodeId="src">src</Tree.Trigger>
|
|
92
|
+
<Tree.Content nodeId="src" defaultOpen>
|
|
93
|
+
<Tree.Node level={1} nodeId="components">
|
|
94
|
+
<Tree.Trigger nodeId="components">components</Tree.Trigger>
|
|
95
|
+
<Tree.Content nodeId="components" defaultOpen>
|
|
96
|
+
<Tree.Node level={2} nodeId="button.ts">
|
|
97
|
+
<Tree.Trigger nodeId="button.ts">button.ts</Tree.Trigger>
|
|
98
|
+
</Tree.Node>
|
|
99
|
+
<Tree.Node level={2} nodeId="card.ts" isLast>
|
|
100
|
+
<Tree.Trigger nodeId="card.ts">card.ts</Tree.Trigger>
|
|
101
|
+
</Tree.Node>
|
|
102
|
+
</Tree.Content>
|
|
103
|
+
</Tree.Node>
|
|
104
|
+
<Tree.Node level={1} nodeId="index.ts" isLast>
|
|
105
|
+
<Tree.Trigger nodeId="index.ts">index.ts</Tree.Trigger>
|
|
106
|
+
</Tree.Node>
|
|
107
|
+
</Tree.Content>
|
|
108
|
+
</Tree.Node>
|
|
109
|
+
</Tree>
|
|
110
|
+
</Tree.Root>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Group: Story = {
|
|
115
|
+
render: ({ ...args }) => (
|
|
116
|
+
<Tree.Root>
|
|
117
|
+
<Tree>
|
|
118
|
+
{["src", "public", "tests", "docs", "config"].map(
|
|
119
|
+
(item, index, array) => (
|
|
120
|
+
<Tree.Node
|
|
121
|
+
key={item}
|
|
122
|
+
nodeId={item}
|
|
123
|
+
isLast={index === array.length - 1}
|
|
124
|
+
>
|
|
125
|
+
<Tree.Trigger nodeId={item}>{item}</Tree.Trigger>
|
|
126
|
+
<Tree.Content nodeId={item}>
|
|
127
|
+
<Tree.Node level={1} nodeId={`${item}/index.ts`} isLast>
|
|
128
|
+
<Tree.Trigger nodeId={`${item}/index.ts`}>
|
|
129
|
+
index.ts
|
|
130
|
+
</Tree.Trigger>
|
|
131
|
+
</Tree.Node>
|
|
132
|
+
</Tree.Content>
|
|
133
|
+
</Tree.Node>
|
|
134
|
+
),
|
|
135
|
+
)}
|
|
136
|
+
</Tree>
|
|
137
|
+
</Tree.Root>
|
|
138
|
+
),
|
|
139
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext } from "react";
|
|
4
|
+
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
5
|
+
|
|
6
|
+
const defaultTreeNodeAPI: IComponentAPI = {
|
|
7
|
+
id: "",
|
|
8
|
+
states: {},
|
|
9
|
+
methods: {},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const TreeNodeContext = createContext<IComponentAPI>(defaultTreeNodeAPI);
|
|
13
|
+
export const useTreeNode = () => useContext(TreeNodeContext);
|
|
14
|
+
|
|
15
|
+
export interface ITreeNodeProviderProperties extends IReactChildren {
|
|
16
|
+
nodeId: string;
|
|
17
|
+
level: number;
|
|
18
|
+
isLast: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TreeNodeProvider = ({
|
|
22
|
+
children,
|
|
23
|
+
nodeId,
|
|
24
|
+
level,
|
|
25
|
+
isLast,
|
|
26
|
+
}: ITreeNodeProviderProperties): React.JSX.Element => {
|
|
27
|
+
const context = useTreeNodeProviderContext({ nodeId, level, isLast });
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<TreeNodeContext.Provider value={context}>
|
|
31
|
+
{children}
|
|
32
|
+
</TreeNodeContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function useTreeNodeProviderContext({
|
|
37
|
+
nodeId,
|
|
38
|
+
level,
|
|
39
|
+
isLast,
|
|
40
|
+
}: Omit<ITreeNodeProviderProperties, "children">): IComponentAPI {
|
|
41
|
+
return {
|
|
42
|
+
id: nodeId,
|
|
43
|
+
states: {
|
|
44
|
+
nodeId,
|
|
45
|
+
level,
|
|
46
|
+
isLast,
|
|
47
|
+
},
|
|
48
|
+
methods: {},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, createContext, useContext } from "react";
|
|
4
|
+
import { IReactChildren, IComponentAPI } from "../../../../../types";
|
|
5
|
+
|
|
6
|
+
// ─── Tree Context ─────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const defaultTreeAPI: IComponentAPI = {
|
|
9
|
+
id: "",
|
|
10
|
+
states: {},
|
|
11
|
+
methods: {},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const TreeContext = createContext<IComponentAPI>(defaultTreeAPI);
|
|
15
|
+
export const useTree = () => useContext(TreeContext);
|
|
16
|
+
|
|
17
|
+
export interface ITreeProviderProperties extends IReactChildren {
|
|
18
|
+
defaultExpandedIds?: string[];
|
|
19
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const TreeProvider = ({
|
|
23
|
+
children,
|
|
24
|
+
defaultExpandedIds = [],
|
|
25
|
+
onSelectionChange,
|
|
26
|
+
}: ITreeProviderProperties): React.JSX.Element => {
|
|
27
|
+
const context = useTreeProviderContext({
|
|
28
|
+
defaultExpandedIds,
|
|
29
|
+
onSelectionChange,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TreeContext.Provider value={context}>{children}</TreeContext.Provider>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function useTreeProviderContext({
|
|
38
|
+
defaultExpandedIds,
|
|
39
|
+
onSelectionChange,
|
|
40
|
+
}: Omit<ITreeProviderProperties, "children">): IComponentAPI {
|
|
41
|
+
const treeId = React.useId();
|
|
42
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(
|
|
43
|
+
() => new Set(defaultExpandedIds),
|
|
44
|
+
);
|
|
45
|
+
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: treeId,
|
|
49
|
+
states: {
|
|
50
|
+
expandedIds,
|
|
51
|
+
selectedIds,
|
|
52
|
+
},
|
|
53
|
+
methods: {
|
|
54
|
+
isExpanded: (id: string): boolean => expandedIds.has(id),
|
|
55
|
+
isSelected: (id: string): boolean => selectedIds.has(id),
|
|
56
|
+
toggleExpanded: (id: string): void => {
|
|
57
|
+
setExpandedIds((prev) => {
|
|
58
|
+
const next = new Set(prev);
|
|
59
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
toggleSelected: (id: string): void => {
|
|
64
|
+
setSelectedIds((prev) => {
|
|
65
|
+
const next = new Set(prev);
|
|
66
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
67
|
+
onSelectionChange?.(Array.from(next));
|
|
68
|
+
return next;
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
getTreeId: ({ nodeId, type }: Record<string, string>): string =>
|
|
72
|
+
`${treeId}-${type}-${nodeId}`,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|