@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +246 -3
  3. package/dist/index.d.ts +246 -3
  4. package/dist/index.js +740 -307
  5. package/dist/index.mjs +708 -288
  6. package/package.json +12 -12
  7. package/src/__tests__/MessageBubble.test.tsx +179 -0
  8. package/src/__tests__/Shimmer.test.tsx +122 -0
  9. package/src/__tests__/Tree.test.tsx +275 -0
  10. package/src/accordion/hooks/index.tsx +3 -1
  11. package/src/badge/index.tsx +2 -3
  12. package/src/checkbox/hooks/index.tsx +5 -1
  13. package/src/collapsible/hooks/index.tsx +3 -1
  14. package/src/dialog/hooks/index.tsx +5 -1
  15. package/src/dropdown/hooks/index.tsx +3 -1
  16. package/src/dropdown/index.tsx +9 -9
  17. package/src/field/hooks/index.tsx +5 -1
  18. package/src/field/styles/index.ts +1 -0
  19. package/src/index.ts +6 -0
  20. package/src/message-bubble/MessageBubble.stories.tsx +91 -0
  21. package/src/message-bubble/hooks/index.tsx +41 -0
  22. package/src/message-bubble/index.tsx +153 -0
  23. package/src/message-bubble/styles/index.ts +61 -0
  24. package/src/otp-field/hooks/index.tsx +3 -1
  25. package/src/otp-field/index.tsx +5 -3
  26. package/src/sheet/hooks/index.tsx +5 -1
  27. package/src/shimmer/Shimmer.stories.tsx +95 -0
  28. package/src/shimmer/index.tsx +64 -0
  29. package/src/shimmer/styles/index.ts +33 -0
  30. package/src/switch/hooks/index.tsx +5 -1
  31. package/src/tabs/hooks/index.tsx +5 -1
  32. package/src/text-area/Textarea.stories.tsx +7 -2
  33. package/src/text-area/index.tsx +30 -14
  34. package/src/text-area/styles/index.ts +32 -72
  35. package/src/toolbar/hooks/index.tsx +5 -1
  36. package/src/tooltip/index.tsx +4 -3
  37. package/src/tree/Tree.stories.tsx +139 -0
  38. package/src/tree/hooks/tree-node-provider.tsx +50 -0
  39. package/src/tree/hooks/tree-provider.tsx +75 -0
  40. package/src/tree/index.tsx +231 -0
  41. package/src/tree/styles/index.ts +23 -0
  42. package/tsconfig.build.json +20 -0
  43. 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 = ({ children }: IReactChildren): JSX.Element => {
14
+ export const SwitchProvider = ({
15
+ children,
16
+ }: IReactChildren): React.JSX.Element => {
13
17
  const context = useSwitchProvider();
14
18
 
15
19
  return (
@@ -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 = ({ children }: IReactChildren): JSX.Element => {
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
- <Textarea {...args} />
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
  ),
@@ -3,9 +3,13 @@
3
3
  import React from "react";
4
4
 
5
5
  import { TextAreaContainer } from "./styles";
6
- import { Field } from "../field";
7
-
8
- import type { IComponentStyling, IComponentVariant } from "../../../../types";
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
- IComponentVariant {}
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 = ({ onChange, ...props }: TextareaProps) => {
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
- <Field.Wrapper className="w-100 h-auto">
52
- <TextAreaContainer
53
- ref={textareaRef}
54
- onChange={handleChange}
55
- data-variant={props.variant ?? "secondary"}
56
- data-raw={String(Boolean(props?.raw))}
57
- {...props}
58
- />
59
- </Field.Wrapper>
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 { FieldDefaultStyles, FieldVariantsStyles } from "../../field/styles";
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
- export const TextAreaContainer = styled.textarea<ScrollContainerProps>`
47
- &[data-raw="false"] {
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
- max-height: var(--measurement-large-60);
50
- min-height: auto;
51
- width: 100%;
52
-
53
- overflow-y: auto;
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
- &:disabled {
112
- cursor: not-allowed;
113
- }
73
+ export const TextAreaContainer = styled.textarea<ScrollContainerProps>`
74
+ &[data-raw="false"] {
75
+ ${FieldDefaultStyles}
76
+ ${FieldShapeStyles}
114
77
 
115
- &[data-error="true"] {
116
- &::placeholder {
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 = ({ children }: IReactChildren): JSX.Element => {
14
+ export const ToolbarProvider = ({
15
+ children,
16
+ }: IReactChildren): React.JSX.Element => {
13
17
  const context = useToolbarProvider();
14
18
 
15
19
  return (
@@ -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 = React.useCallback(() => {
62
+ const bodyRect = (): DOMRect | undefined => {
63
63
  if (typeof document !== "undefined") {
64
- return document.body.getBoundingClientRect();
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
+ }