@usefui/components 1.5.1
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 +233 -0
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/babel.config.js +12 -0
- package/dist/index.d.mts +1299 -0
- package/dist/index.d.ts +1299 -0
- package/dist/index.js +3701 -0
- package/dist/index.mjs +3586 -0
- package/package.json +44 -0
- package/src/__tests__/Accordion.test.tsx +106 -0
- package/src/__tests__/Avatar.test.tsx +89 -0
- package/src/__tests__/Badge.test.tsx +58 -0
- package/src/__tests__/Button.test.tsx +88 -0
- package/src/__tests__/Checkbox.test.tsx +106 -0
- package/src/__tests__/Collapsible.test.tsx +79 -0
- package/src/__tests__/Dialog.test.tsx +109 -0
- package/src/__tests__/Dropdown.test.tsx +159 -0
- package/src/__tests__/Field.test.tsx +100 -0
- package/src/__tests__/OTPField.test.tsx +199 -0
- package/src/__tests__/Overlay.test.tsx +70 -0
- package/src/__tests__/Page.test.tsx +98 -0
- package/src/__tests__/Portal.test.tsx +28 -0
- package/src/__tests__/Sheet.test.tsx +125 -0
- package/src/__tests__/Switch.test.tsx +90 -0
- package/src/__tests__/Tabs.test.tsx +129 -0
- package/src/__tests__/Toggle.test.tsx +67 -0
- package/src/__tests__/Toolbar.test.tsx +147 -0
- package/src/__tests__/Tooltip.test.tsx +88 -0
- package/src/accordion/Accordion.stories.tsx +89 -0
- package/src/accordion/hooks/index.tsx +39 -0
- package/src/accordion/index.tsx +170 -0
- package/src/avatar/Avatar.stories.tsx +62 -0
- package/src/avatar/index.tsx +90 -0
- package/src/avatar/styles/index.ts +79 -0
- package/src/badge/Badge.stories.tsx +60 -0
- package/src/badge/index.tsx +58 -0
- package/src/badge/styles/index.ts +109 -0
- package/src/button/Button.stories.tsx +47 -0
- package/src/button/index.tsx +79 -0
- package/src/button/styles/index.ts +180 -0
- package/src/checkbox/Checkbox.stories.tsx +100 -0
- package/src/checkbox/hooks/index.tsx +40 -0
- package/src/checkbox/index.tsx +147 -0
- package/src/checkbox/styles/index.ts +139 -0
- package/src/collapsible/Collapsible.stories.tsx +95 -0
- package/src/collapsible/hooks/index.tsx +50 -0
- package/src/collapsible/index.tsx +137 -0
- package/src/dialog/Dialog.stories.tsx +73 -0
- package/src/dialog/hooks/index.tsx +35 -0
- package/src/dialog/index.tsx +221 -0
- package/src/dialog/styles/index.ts +72 -0
- package/src/divider/index.ts +10 -0
- package/src/dropdown/Dropdown.stories.tsx +100 -0
- package/src/dropdown/hooks/index.tsx +64 -0
- package/src/dropdown/index.tsx +316 -0
- package/src/dropdown/styles/index.ts +90 -0
- package/src/field/Field.stories.tsx +146 -0
- package/src/field/hooks/index.tsx +28 -0
- package/src/field/index.tsx +183 -0
- package/src/field/styles/index.ts +166 -0
- package/src/index.ts +33 -0
- package/src/otp-field/OTPField.stories.tsx +50 -0
- package/src/otp-field/hooks/index.tsx +13 -0
- package/src/otp-field/index.tsx +234 -0
- package/src/otp-field/styles/index.ts +33 -0
- package/src/otp-field/types/index.ts +23 -0
- package/src/overlay/Overlay.stories.tsx +59 -0
- package/src/overlay/index.tsx +58 -0
- package/src/overlay/styles/index.ts +26 -0
- package/src/page/Page.stories.tsx +85 -0
- package/src/page/index.tsx +265 -0
- package/src/page/styles/index.ts +59 -0
- package/src/portal/Portal.stories.tsx +27 -0
- package/src/portal/index.tsx +36 -0
- package/src/scrollarea/Scrollarea.stories.tsx +99 -0
- package/src/scrollarea/index.tsx +27 -0
- package/src/scrollarea/styles/index.ts +71 -0
- package/src/sheet/Sheet.stories.tsx +86 -0
- package/src/sheet/hooks/index.tsx +47 -0
- package/src/sheet/index.tsx +190 -0
- package/src/sheet/styles/index.ts +69 -0
- package/src/switch/Switch.stories.tsx +96 -0
- package/src/switch/hooks/index.tsx +33 -0
- package/src/switch/index.tsx +122 -0
- package/src/switch/styles/index.ts +118 -0
- package/src/table/index.tsx +138 -0
- package/src/table/styles/index.ts +48 -0
- package/src/tabs/Tabs.stories.tsx +87 -0
- package/src/tabs/hooks/index.tsx +35 -0
- package/src/tabs/index.tsx +161 -0
- package/src/tabs/styles/index.ts +9 -0
- package/src/toggle/Toggle.stories.tsx +118 -0
- package/src/toggle/index.tsx +55 -0
- package/src/toggle/styles/index.ts +0 -0
- package/src/toolbar/Toolbar.stories.tsx +89 -0
- package/src/toolbar/hooks/index.tsx +35 -0
- package/src/toolbar/index.tsx +243 -0
- package/src/toolbar/styles/index.ts +129 -0
- package/src/tooltip/Tooltip.stories.tsx +60 -0
- package/src/tooltip/index.tsx +177 -0
- package/src/tooltip/styles/index.ts +38 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface OTPFieldContextType {
|
|
2
|
+
otp: string[];
|
|
3
|
+
inputRefs: React.MutableRefObject<(HTMLInputElement | null)[]>;
|
|
4
|
+
length: number;
|
|
5
|
+
activeIndex: number;
|
|
6
|
+
handleChange: (index: number, value: string) => void;
|
|
7
|
+
handleKeyDown: (index: number, e: React.KeyboardEvent) => void;
|
|
8
|
+
handleClick: (index: number) => void;
|
|
9
|
+
handlePaste: (e: React.ClipboardEvent) => void;
|
|
10
|
+
handleFocus: (index: number) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OTPFieldProps {
|
|
14
|
+
name?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
length?: number;
|
|
18
|
+
onComplete?: (value: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OTPFieldSlotProps {
|
|
22
|
+
index: number;
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Overlay, Button } from "..";
|
|
4
|
+
|
|
5
|
+
// Duplicated doc: The JSDoc content isn't rendering on Storybook.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Overlay are used to display content on top of the current layer.
|
|
9
|
+
*
|
|
10
|
+
* **Best practices:**
|
|
11
|
+
*
|
|
12
|
+
* - Use semantic HTML elements to structure the content of the overlay.
|
|
13
|
+
* - Ensure that the overlay is visible and accessible to all users, including those using assistive technologies.
|
|
14
|
+
* - Use keyboard shortcuts to provide an alternative way of interacting with the overlay.
|
|
15
|
+
* - Ensure that the overlay is responsive and adapts to different screen sizes and orientations.
|
|
16
|
+
*
|
|
17
|
+
*/
|
|
18
|
+
const meta = {
|
|
19
|
+
title: "Components/Overlay",
|
|
20
|
+
component: Overlay,
|
|
21
|
+
tags: ["autodocs"],
|
|
22
|
+
} satisfies Meta<typeof Overlay>;
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
raw: false,
|
|
29
|
+
visible: false,
|
|
30
|
+
closeOnInteract: false,
|
|
31
|
+
},
|
|
32
|
+
render: ({ ...args }) => {
|
|
33
|
+
const [displayed, setDisplayed] = React.useState(args.visible);
|
|
34
|
+
return (
|
|
35
|
+
<React.Fragment>
|
|
36
|
+
<Button onClick={() => setDisplayed(true)}>Show</Button>
|
|
37
|
+
<Overlay visible={displayed} />
|
|
38
|
+
</React.Fragment>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export const Visible: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
raw: false,
|
|
45
|
+
visible: true,
|
|
46
|
+
closeOnInteract: false,
|
|
47
|
+
},
|
|
48
|
+
render: ({ ...args }) => <Overlay visible={args.visible} />,
|
|
49
|
+
};
|
|
50
|
+
export const CloseOnInteraction: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
raw: false,
|
|
53
|
+
visible: true,
|
|
54
|
+
closeOnInteract: true,
|
|
55
|
+
},
|
|
56
|
+
render: ({ ...args }) => (
|
|
57
|
+
<Overlay visible={args.visible} closeOnInteract={args.closeOnInteract} />
|
|
58
|
+
),
|
|
59
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { OverlayWrapper } from "./styles";
|
|
5
|
+
import { IComponentStyling } from "../../../../types";
|
|
6
|
+
|
|
7
|
+
export interface IOverlayProperties
|
|
8
|
+
extends IComponentStyling,
|
|
9
|
+
React.ComponentProps<"div"> {
|
|
10
|
+
visible?: boolean;
|
|
11
|
+
closeOnInteract?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Overlay are used to display content on top of the current layer.
|
|
16
|
+
*
|
|
17
|
+
* **Best practices:**
|
|
18
|
+
*
|
|
19
|
+
* - Use semantic HTML elements to structure the content of the overlay.
|
|
20
|
+
* - Ensure that the overlay is visible and accessible to all users, including those using assistive technologies.
|
|
21
|
+
* - Use keyboard shortcuts to provide an alternative way of interacting with the overlay.
|
|
22
|
+
* - Ensure that the overlay is responsive and adapts to different screen sizes and orientations.
|
|
23
|
+
*
|
|
24
|
+
* @param {IOverlayProperties} props - The props for the Overlay component.
|
|
25
|
+
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
26
|
+
* @param {boolean} props.visible - Whether the component is visible or not.
|
|
27
|
+
* @param {boolean} props.closeOnInteract - Whether the overlay should be closed when the user interacts with it.
|
|
28
|
+
* @param {function} props.onClick - The callback function to be invoked when the overlay is clicked.
|
|
29
|
+
* @returns {ReactElement} The Overlay component.
|
|
30
|
+
*/
|
|
31
|
+
export const Overlay = (props: IOverlayProperties) => {
|
|
32
|
+
const { raw, visible, closeOnInteract, onClick, ...restProps } = props;
|
|
33
|
+
const [mounted, setMounted] = React.useState<boolean>(Boolean(visible));
|
|
34
|
+
|
|
35
|
+
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
36
|
+
if (onClick) onClick(event);
|
|
37
|
+
if (closeOnInteract) setMounted(false);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
if (visible !== mounted) setMounted(Boolean(visible));
|
|
42
|
+
}, [visible]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{mounted && (
|
|
47
|
+
<OverlayWrapper
|
|
48
|
+
tabIndex={-1}
|
|
49
|
+
onClick={handleClick}
|
|
50
|
+
aria-hidden={true}
|
|
51
|
+
data-raw={Boolean(raw)}
|
|
52
|
+
{...restProps}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
Overlay.displayName = "Overlay";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
export const OverlayWrapper = styled.div<any>`
|
|
4
|
+
@keyframes animate-fade {
|
|
5
|
+
0% {
|
|
6
|
+
opacity: 0;
|
|
7
|
+
}
|
|
8
|
+
100% {
|
|
9
|
+
opacity: 1;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
position: fixed;
|
|
14
|
+
top: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
z-index: var(--depth-default-90);
|
|
19
|
+
|
|
20
|
+
&[data-raw="false"] {
|
|
21
|
+
background-color: rgba(0, 0, 0, 0.6); // Always forced to black
|
|
22
|
+
animation-duration: 0.2s;
|
|
23
|
+
animation-name: animate-fade;
|
|
24
|
+
animation-fill-mode: backwards;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
|
|
4
|
+
import { Page } from "..";
|
|
5
|
+
import {
|
|
6
|
+
ComponentSizeEnum,
|
|
7
|
+
ComponentSideEnum,
|
|
8
|
+
ComponentHeightEnum,
|
|
9
|
+
} from "../../../../types";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Components/Page",
|
|
13
|
+
component: Page,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
} satisfies Meta<typeof Page>;
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
export const Composed: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
raw: false,
|
|
22
|
+
defaultOpen: false,
|
|
23
|
+
showoncollapse: false,
|
|
24
|
+
showfirstchild: false,
|
|
25
|
+
shortcut: false,
|
|
26
|
+
hotkey: ":",
|
|
27
|
+
bindkey: null,
|
|
28
|
+
},
|
|
29
|
+
argTypes: {
|
|
30
|
+
side: {
|
|
31
|
+
options: [
|
|
32
|
+
ComponentSideEnum.Top,
|
|
33
|
+
ComponentSideEnum.Right,
|
|
34
|
+
ComponentSideEnum.Bottom,
|
|
35
|
+
ComponentSideEnum.Left,
|
|
36
|
+
],
|
|
37
|
+
control: { type: "radio" },
|
|
38
|
+
},
|
|
39
|
+
sizing: {
|
|
40
|
+
options: [
|
|
41
|
+
ComponentSizeEnum.Small,
|
|
42
|
+
ComponentSizeEnum.Medium,
|
|
43
|
+
ComponentSizeEnum.Large,
|
|
44
|
+
],
|
|
45
|
+
control: { type: "radio" },
|
|
46
|
+
},
|
|
47
|
+
height: {
|
|
48
|
+
options: [ComponentHeightEnum.Fullscreen, ComponentHeightEnum.Auto],
|
|
49
|
+
control: { type: "radio" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
render: ({ ...args }) => {
|
|
53
|
+
return (
|
|
54
|
+
<Page>
|
|
55
|
+
<Page.Tools
|
|
56
|
+
shortcut
|
|
57
|
+
hotkey=";"
|
|
58
|
+
showoncollapse
|
|
59
|
+
side={ComponentSideEnum.Left}
|
|
60
|
+
sizing={ComponentSizeEnum.Medium}
|
|
61
|
+
/>
|
|
62
|
+
<div style={{ height: "100dvh" }}>
|
|
63
|
+
<Page.Navigation>🐻</Page.Navigation>
|
|
64
|
+
<Page.Menu>🐻❄️</Page.Menu>
|
|
65
|
+
<Page.Content>🦊</Page.Content>
|
|
66
|
+
<Page.Panel
|
|
67
|
+
shortcut
|
|
68
|
+
hotkey=","
|
|
69
|
+
side={ComponentSideEnum.Bottom}
|
|
70
|
+
sizing={ComponentSizeEnum.Large}
|
|
71
|
+
>
|
|
72
|
+
🐱
|
|
73
|
+
</Page.Panel>
|
|
74
|
+
</div>
|
|
75
|
+
<Page.Tools
|
|
76
|
+
shortcut
|
|
77
|
+
hotkey=":"
|
|
78
|
+
defaultOpen
|
|
79
|
+
side={ComponentSideEnum.Right}
|
|
80
|
+
sizing={ComponentSizeEnum.Large}
|
|
81
|
+
/>
|
|
82
|
+
</Page>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Toolbar,
|
|
6
|
+
IToolbarBodyProperties,
|
|
7
|
+
IToolbarSectionProperties,
|
|
8
|
+
ScrollArea,
|
|
9
|
+
IScrollAreaProperties,
|
|
10
|
+
IButtonProperties,
|
|
11
|
+
} from "..";
|
|
12
|
+
import {
|
|
13
|
+
PageRootWrapper,
|
|
14
|
+
PageNavWrapper,
|
|
15
|
+
PageMenuWrapper,
|
|
16
|
+
PagePanelWrapper,
|
|
17
|
+
PageSectionWrapper,
|
|
18
|
+
PageBodyWrapper,
|
|
19
|
+
} from "./styles";
|
|
20
|
+
|
|
21
|
+
export interface IPageToolsProperties
|
|
22
|
+
extends IToolbarBodyProperties,
|
|
23
|
+
IToolbarSectionProperties {
|
|
24
|
+
trigger?: React.ReactNode | string;
|
|
25
|
+
triggerProps?: IButtonProperties;
|
|
26
|
+
fixed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface IPageWrapperProperties {
|
|
29
|
+
$menus?: number;
|
|
30
|
+
$navigations?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pages are structured components used to build User Interface layouts.
|
|
35
|
+
*
|
|
36
|
+
* @param {React.ComponentProps<"div">} props - The props for the Page component.
|
|
37
|
+
* @returns {ReactElement} The Page component.
|
|
38
|
+
*/
|
|
39
|
+
const Page = (props: React.ComponentProps<"div">) => {
|
|
40
|
+
const { children } = props;
|
|
41
|
+
return (
|
|
42
|
+
<PageRootWrapper className="flex" {...props}>
|
|
43
|
+
{children}
|
|
44
|
+
</PageRootWrapper>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
Page.displayName = "Page";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Navigations are used to hold links and actions.
|
|
51
|
+
*
|
|
52
|
+
* @param {React.ComponentProps<"div">} props - The props for the Page.Navigation component.
|
|
53
|
+
* @returns {ReactElement} The Page.Navigation component.
|
|
54
|
+
*/
|
|
55
|
+
const PageNavigation = (props: React.ComponentProps<"nav">) => {
|
|
56
|
+
const { children } = props;
|
|
57
|
+
return <PageNavWrapper {...props}>{children}</PageNavWrapper>;
|
|
58
|
+
};
|
|
59
|
+
PageNavigation.displayName = "Page.Navigation";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Toolbar are component that provides a set of tools or actions for the user.
|
|
63
|
+
*
|
|
64
|
+
* **Best practices:**
|
|
65
|
+
*
|
|
66
|
+
* - Use semantic HTML elements to structure the content of the Page.Toolbar.
|
|
67
|
+
* - Ensure that the Page.Toolbar is visible and accessible to all users, including those using assistive technologies.
|
|
68
|
+
* - Use keyboard shortcuts to provide an alternative way of interacting with the Page.Toolbar.
|
|
69
|
+
* - Ensure that the Page.Toolbar is responsive and adapts to different screen sizes and orientations.
|
|
70
|
+
*
|
|
71
|
+
* @param {IPageToolsProperties} props - The props for the Page.Toolbar component.
|
|
72
|
+
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
73
|
+
* @param {string} props.shortcut - The key combination used as keyboard shortcuts to trigger the Page.Toolbar.
|
|
74
|
+
* @param {string} props.hotkey - The key to use in the key combination for the keyboard shortcuts.
|
|
75
|
+
* @param {KeyBindingEnum} props.bindkey - The modifier key to use in the key combination.
|
|
76
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the Page.Toolbar.
|
|
77
|
+
* @param {ComponentHeightEnum} props.height - The height definition of the Page.Toolbar.
|
|
78
|
+
* @param {TComponentSide} props.side - The side of the Page.Toolbar.
|
|
79
|
+
* @param {boolean} props.defaultOpen - Whether the Page.Toolbar should be open by default.
|
|
80
|
+
* @param {ReactNode} props.children - The content to be rendered inside the Page.Toolbar.
|
|
81
|
+
* @returns {ReactElement} The Page.Toolbar component.
|
|
82
|
+
*/
|
|
83
|
+
const PageTools = (props: IPageToolsProperties) => {
|
|
84
|
+
const {
|
|
85
|
+
shortcut,
|
|
86
|
+
hotkey,
|
|
87
|
+
bindkey,
|
|
88
|
+
raw,
|
|
89
|
+
sizing,
|
|
90
|
+
side,
|
|
91
|
+
defaultOpen,
|
|
92
|
+
fixed,
|
|
93
|
+
showoncollapse,
|
|
94
|
+
onClick,
|
|
95
|
+
trigger,
|
|
96
|
+
triggerProps,
|
|
97
|
+
children,
|
|
98
|
+
} = props;
|
|
99
|
+
|
|
100
|
+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
101
|
+
if (onClick) onClick(event);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Toolbar.Root>
|
|
106
|
+
<Toolbar
|
|
107
|
+
raw={raw}
|
|
108
|
+
sizing={sizing}
|
|
109
|
+
side={side}
|
|
110
|
+
shortcut={!fixed && shortcut}
|
|
111
|
+
hotkey={hotkey}
|
|
112
|
+
bindkey={bindkey}
|
|
113
|
+
defaultOpen={defaultOpen}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
<Toolbar.Section showoncollapse={showoncollapse}>
|
|
117
|
+
{children}
|
|
118
|
+
</Toolbar.Section>
|
|
119
|
+
|
|
120
|
+
{!fixed && (
|
|
121
|
+
<Toolbar.Trigger
|
|
122
|
+
title={
|
|
123
|
+
shortcut ? `${bindkey ?? "ctrl"} + ${hotkey}` : "toolbar-trigger"
|
|
124
|
+
}
|
|
125
|
+
onClick={handleClick}
|
|
126
|
+
{...triggerProps}
|
|
127
|
+
>
|
|
128
|
+
{trigger ?? <span>↔</span>}
|
|
129
|
+
</Toolbar.Trigger>
|
|
130
|
+
)}
|
|
131
|
+
</Toolbar>
|
|
132
|
+
</Toolbar.Root>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
PageTools.displayName = "Page.Tools";
|
|
136
|
+
|
|
137
|
+
const PageContent = (
|
|
138
|
+
props: React.ComponentProps<"div"> & IScrollAreaProperties
|
|
139
|
+
) => {
|
|
140
|
+
const { children } = props;
|
|
141
|
+
return (
|
|
142
|
+
<ScrollArea as={PageSectionWrapper} {...props}>
|
|
143
|
+
{children}
|
|
144
|
+
</ScrollArea>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
PageContent.displayName = "Page.Content";
|
|
148
|
+
|
|
149
|
+
const PageWrapper = (
|
|
150
|
+
props: IPageWrapperProperties & React.ComponentProps<"div">
|
|
151
|
+
) => {
|
|
152
|
+
const { children } = props;
|
|
153
|
+
return <PageBodyWrapper {...props}>{children}</PageBodyWrapper>;
|
|
154
|
+
};
|
|
155
|
+
PageWrapper.displayName = "Page.Wrapper";
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Panel are component that provides a set of tools or actions for the user.
|
|
159
|
+
*
|
|
160
|
+
* **Best practices:**
|
|
161
|
+
*
|
|
162
|
+
* - Use semantic HTML elements to structure the content of the Page.Panel.
|
|
163
|
+
* - Ensure that the Page.Panel is visible and accessible to all users, including those using assistive technologies.
|
|
164
|
+
* - Use keyboard shortcuts to provide an alternative way of interacting with the Page.Panel.
|
|
165
|
+
* - Ensure that the Page.Panel is responsive and adapts to different screen sizes and orientations.
|
|
166
|
+
*
|
|
167
|
+
* @param {IPageToolsProperties} props - The props for the Page.Panel component.
|
|
168
|
+
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
169
|
+
* @param {string} props.shortcut - The key combination used as keyboard shortcuts to trigger the Page.Panel.
|
|
170
|
+
* @param {string} props.hotkey - The key to use in the key combination for the keyboard shortcuts.
|
|
171
|
+
* @param {KeyBindingEnum} props.bindkey - The modifier key to use in the key combination.
|
|
172
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the Page.Panel.
|
|
173
|
+
* @param {ComponentHeightEnum} props.height - The height definition of the Page.Panel.
|
|
174
|
+
* @param {TComponentSide} props.side - The side of the Page.Panel.
|
|
175
|
+
* @param {boolean} props.defaultOpen - Whether the Page.Panel should be open by default.
|
|
176
|
+
* @param {ReactNode} props.children - The content to be rendered inside the Page.Panel.
|
|
177
|
+
* @returns {ReactElement} The Page.Panel component.
|
|
178
|
+
*/
|
|
179
|
+
const PagePanel = (props: IPageToolsProperties) => {
|
|
180
|
+
const {
|
|
181
|
+
shortcut,
|
|
182
|
+
hotkey,
|
|
183
|
+
bindkey,
|
|
184
|
+
raw,
|
|
185
|
+
sizing,
|
|
186
|
+
height = "auto",
|
|
187
|
+
side,
|
|
188
|
+
defaultOpen,
|
|
189
|
+
fixed,
|
|
190
|
+
showoncollapse,
|
|
191
|
+
onClick,
|
|
192
|
+
trigger,
|
|
193
|
+
triggerProps,
|
|
194
|
+
children,
|
|
195
|
+
} = props;
|
|
196
|
+
|
|
197
|
+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
198
|
+
if (onClick) onClick(event);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Toolbar.Root>
|
|
203
|
+
<PagePanelWrapper
|
|
204
|
+
as={Toolbar}
|
|
205
|
+
raw={raw}
|
|
206
|
+
sizing={sizing}
|
|
207
|
+
height={height}
|
|
208
|
+
side={side}
|
|
209
|
+
shortcut={!fixed && shortcut}
|
|
210
|
+
hotkey={hotkey}
|
|
211
|
+
bindkey={bindkey}
|
|
212
|
+
defaultOpen={defaultOpen}
|
|
213
|
+
aria-label={props["aria-label"]}
|
|
214
|
+
>
|
|
215
|
+
{!fixed && (
|
|
216
|
+
<Toolbar.Trigger
|
|
217
|
+
title={
|
|
218
|
+
shortcut ? `${bindkey ?? "ctrl"} + ${hotkey}` : "toolbar-trigger"
|
|
219
|
+
}
|
|
220
|
+
onClick={handleClick}
|
|
221
|
+
{...triggerProps}
|
|
222
|
+
>
|
|
223
|
+
{trigger ?? (
|
|
224
|
+
<span style={{ transform: "rotate(90deg)" }}>↔</span>
|
|
225
|
+
)}
|
|
226
|
+
</Toolbar.Trigger>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
<Toolbar.Section showoncollapse={showoncollapse}>
|
|
230
|
+
{children}
|
|
231
|
+
</Toolbar.Section>
|
|
232
|
+
</PagePanelWrapper>
|
|
233
|
+
</Toolbar.Root>
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
PagePanel.displayName = "Page.Panel";
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Menus are used to hold links and actions.
|
|
240
|
+
*
|
|
241
|
+
* @param {React.ComponentProps<"menu">} props - The props for the Page.Menu component.
|
|
242
|
+
* @returns {ReactElement} The Page.Menu component.
|
|
243
|
+
*/
|
|
244
|
+
const PageMenu = (props: React.ComponentProps<"menu">) => {
|
|
245
|
+
const { children } = props;
|
|
246
|
+
return <PageMenuWrapper {...props}>{children}</PageMenuWrapper>;
|
|
247
|
+
};
|
|
248
|
+
PageMenu.displayName = "Page.Menu";
|
|
249
|
+
|
|
250
|
+
Page.Navigation = PageNavigation;
|
|
251
|
+
Page.Tools = PageTools;
|
|
252
|
+
Page.Wrapper = PageWrapper;
|
|
253
|
+
Page.Content = PageContent;
|
|
254
|
+
Page.Panel = PagePanel;
|
|
255
|
+
Page.Menu = PageMenu;
|
|
256
|
+
|
|
257
|
+
export {
|
|
258
|
+
Page,
|
|
259
|
+
PageNavigation,
|
|
260
|
+
PageTools,
|
|
261
|
+
PagePanel,
|
|
262
|
+
PageWrapper,
|
|
263
|
+
PageContent,
|
|
264
|
+
PageMenu,
|
|
265
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
import { HiddenScrollbar } from "../../scrollarea/styles";
|
|
3
|
+
|
|
4
|
+
export const PageRootWrapper = styled.div<any>`
|
|
5
|
+
height: 100dvh;
|
|
6
|
+
width: 100%;
|
|
7
|
+
`;
|
|
8
|
+
export const PageNavWrapper = styled.nav<any>`
|
|
9
|
+
background-color: var(--body-color);
|
|
10
|
+
border: var(--measurement-small-10) solid transparent;
|
|
11
|
+
border-bottom-color: var(--font-color-alpha-10);
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
max-height: var(--measurement-large-20);
|
|
15
|
+
padding: var(--measurement-medium-30);
|
|
16
|
+
`;
|
|
17
|
+
export const PageMenuWrapper = styled.menu<any>`
|
|
18
|
+
background-color: var(--body-color);
|
|
19
|
+
border: var(--measurement-small-10) solid transparent;
|
|
20
|
+
border-bottom-color: var(--font-color-alpha-10);
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
max-height: var(--measurement-large-30);
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: var(--measurement-medium-60) var(--measurement-medium-30);
|
|
26
|
+
`;
|
|
27
|
+
export const PagePanelWrapper = styled.aside<any>`
|
|
28
|
+
position: absolute;
|
|
29
|
+
bottom: 0;
|
|
30
|
+
width: 100%;
|
|
31
|
+
overflow: scroll;
|
|
32
|
+
${HiddenScrollbar}
|
|
33
|
+
`;
|
|
34
|
+
export const PageSectionWrapper = styled.div<any>`
|
|
35
|
+
background: var(--body-color);
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
`;
|
|
39
|
+
export const PageBodyWrapper = styled.div<any>`
|
|
40
|
+
--menus-height: calc(
|
|
41
|
+
var(--measurement-large-30) *
|
|
42
|
+
${({ $menus }) => ($menus ? Number($menus) : 0)}
|
|
43
|
+
);
|
|
44
|
+
--navs-height: calc(
|
|
45
|
+
var(--measurement-large-20) *
|
|
46
|
+
${({ $navigations }) => ($navigations ? Number($navigations) : 0)}
|
|
47
|
+
);
|
|
48
|
+
--page-height: calc(100dvh - (var(--menus-height) + var(--navs-height)));
|
|
49
|
+
|
|
50
|
+
outline: none;
|
|
51
|
+
display: inline-block;
|
|
52
|
+
|
|
53
|
+
height: var(--page-height);
|
|
54
|
+
max-height: var(--page-height);
|
|
55
|
+
|
|
56
|
+
width: 100%;
|
|
57
|
+
overflow: scroll;
|
|
58
|
+
${HiddenScrollbar}
|
|
59
|
+
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Portal } from "..";
|
|
4
|
+
|
|
5
|
+
// Duplicated doc: The JSDoc content isn't rendering on Storybook.
|
|
6
|
+
/**
|
|
7
|
+
* Portal are used to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
|
|
8
|
+
*/
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Components/Portal",
|
|
11
|
+
component: Portal,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
} satisfies Meta<typeof Portal>;
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
container: "",
|
|
20
|
+
},
|
|
21
|
+
render: ({ ...args }) => (
|
|
22
|
+
<React.Fragment>
|
|
23
|
+
<div id="portal-container" />
|
|
24
|
+
<Portal container="portal-container">🐻🐻❄️🦊🐱🐶</Portal>
|
|
25
|
+
</React.Fragment>
|
|
26
|
+
),
|
|
27
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
|
|
6
|
+
export interface IPortalProperties {
|
|
7
|
+
container: string;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Portal are used to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
|
|
13
|
+
*
|
|
14
|
+
* @param {IPortalProperties} props - The props for the Portal component.
|
|
15
|
+
* @param {string} props.container - The ID of the DOM element to render the portal into.
|
|
16
|
+
* @param {ReactNode} props.children - The content to be rendered inside the portal.
|
|
17
|
+
* @returns {ReactElement} The Portal component.
|
|
18
|
+
*/
|
|
19
|
+
export const Portal = (props: IPortalProperties) => {
|
|
20
|
+
// Handle SSR in Next.js and Electron's renderer process
|
|
21
|
+
if (typeof document === "undefined") return null;
|
|
22
|
+
|
|
23
|
+
const { container, children } = props;
|
|
24
|
+
|
|
25
|
+
const [hasMounted, setHasMounted] = React.useState<boolean>(false);
|
|
26
|
+
const [portalRoot, setPortalRoot] = React.useState<Element | null>(null);
|
|
27
|
+
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
setHasMounted(true);
|
|
30
|
+
setPortalRoot(document.querySelector(`#${container}`));
|
|
31
|
+
}, [container]);
|
|
32
|
+
|
|
33
|
+
if (!hasMounted || !portalRoot) return null;
|
|
34
|
+
return createPortal(children, portalRoot);
|
|
35
|
+
};
|
|
36
|
+
Portal.displayName = "Portal";
|