@valbuild/ui 0.13.4 → 0.17.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/dist/valbuild-ui.cjs.d.ts +13 -18
- package/dist/valbuild-ui.cjs.js +7624 -1718
- package/dist/valbuild-ui.esm.js +7625 -1719
- package/package.json +5 -2
- package/src/assets/icons/ImageIcon.tsx +15 -7
- package/src/assets/icons/Section.tsx +41 -0
- package/src/assets/icons/TextIcon.tsx +20 -0
- package/src/components/Button.tsx +18 -7
- package/src/components/DraggableList.stories.tsx +20 -0
- package/src/components/DraggableList.tsx +95 -0
- package/src/components/Dropdown.tsx +2 -0
- package/src/components/ExpandLogo.tsx +72 -0
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +1 -16
- package/src/components/RichTextEditor/RichTextEditor.tsx +2 -2
- package/src/components/User.tsx +17 -0
- package/src/components/ValMenu.tsx +40 -0
- package/src/components/ValOverlay.tsx +513 -29
- package/src/components/ValOverlayContext.tsx +63 -0
- package/src/components/ValWindow.stories.tsx +3 -3
- package/src/components/ValWindow.tsx +26 -18
- package/src/components/dashboard/DashboardButton.tsx +25 -0
- package/src/components/dashboard/DashboardDropdown.tsx +59 -0
- package/src/components/dashboard/Dropdown.stories.tsx +11 -0
- package/src/components/dashboard/Dropdown.tsx +70 -0
- package/src/components/dashboard/FormGroup.stories.tsx +37 -0
- package/src/components/dashboard/FormGroup.tsx +36 -0
- package/src/components/dashboard/Grid.stories.tsx +52 -0
- package/src/components/dashboard/Grid.tsx +126 -0
- package/src/components/dashboard/Grid2.stories.tsx +56 -0
- package/src/components/dashboard/Grid2.tsx +72 -0
- package/src/components/dashboard/Tree.stories.tsx +91 -0
- package/src/components/dashboard/Tree.tsx +72 -0
- package/src/components/dashboard/ValDashboard.tsx +148 -0
- package/src/components/dashboard/ValDashboardEditor.tsx +269 -0
- package/src/components/dashboard/ValDashboardGrid.tsx +142 -0
- package/src/components/dashboard/ValTreeNavigator.tsx +253 -0
- package/src/components/forms/Form.tsx +2 -2
- package/src/components/forms/{TextForm.tsx → TextArea.tsx} +5 -3
- package/src/dto/SerializedSchema.ts +69 -0
- package/src/dto/Session.ts +12 -0
- package/src/dto/SessionMode.ts +5 -0
- package/src/dto/Tree.ts +18 -0
- package/src/exports.ts +1 -0
- package/src/utils/Remote.ts +15 -0
- package/src/utils/resolvePath.ts +33 -0
- package/tailwind.config.js +20 -1
|
@@ -11,7 +11,6 @@ export type ValWindowProps = {
|
|
|
11
11
|
|
|
12
12
|
export function ValWindow({
|
|
13
13
|
position,
|
|
14
|
-
isInitialized: isInitializedProp,
|
|
15
14
|
onClose,
|
|
16
15
|
children,
|
|
17
16
|
}: ValWindowProps): React.ReactElement {
|
|
@@ -32,26 +31,27 @@ export function ValWindow({
|
|
|
32
31
|
|
|
33
32
|
//
|
|
34
33
|
const [size, resizeRef, onMouseDownResize] = useResize();
|
|
34
|
+
|
|
35
35
|
return (
|
|
36
36
|
<div
|
|
37
37
|
className={classNames(
|
|
38
|
-
"absolute h-[100svh] w-full tablet:w-auto tablet:h-auto tablet:min-h-fit tablet:rounded bg-base drop-shadow-2xl min-w-[320px] transition-opacity duration-300 delay-75 max-w-full",
|
|
38
|
+
"absolute inset-0 h-[100svh] w-full tablet:w-auto tablet:h-auto tablet:min-h-fit tablet:rounded bg-base text-primary drop-shadow-2xl min-w-[320px] transition-opacity duration-300 delay-75 max-w-full",
|
|
39
39
|
{
|
|
40
|
-
"opacity-0": !
|
|
41
|
-
"opacity-100": isInitialized
|
|
40
|
+
"opacity-0": !isInitialized,
|
|
41
|
+
"opacity-100": isInitialized,
|
|
42
42
|
}
|
|
43
43
|
)}
|
|
44
44
|
ref={resizeRef}
|
|
45
45
|
style={{
|
|
46
46
|
left: draggedPosition.left,
|
|
47
47
|
top: draggedPosition.top,
|
|
48
|
-
width: size?.width,
|
|
49
|
-
height: size?.height,
|
|
48
|
+
width: size?.width || 320,
|
|
49
|
+
height: size?.height || 320,
|
|
50
50
|
}}
|
|
51
51
|
>
|
|
52
52
|
<div
|
|
53
53
|
ref={dragRef}
|
|
54
|
-
className="relative flex justify-center px-2 pt-2 text-primary
|
|
54
|
+
className="relative flex justify-center px-2 pt-2 text-primary"
|
|
55
55
|
>
|
|
56
56
|
<AlignJustify
|
|
57
57
|
size={16}
|
|
@@ -69,7 +69,7 @@ export function ValWindow({
|
|
|
69
69
|
<X size={16} />
|
|
70
70
|
</button>
|
|
71
71
|
</div>
|
|
72
|
-
{children}
|
|
72
|
+
<div style={{ height: (size?.height || 320) - 64 }}>{children}</div>
|
|
73
73
|
<div
|
|
74
74
|
className="absolute bottom-0 right-0 hidden ml-auto select-none tablet:block text-border cursor-nwse-resize"
|
|
75
75
|
style={{
|
|
@@ -109,8 +109,8 @@ function useResize() {
|
|
|
109
109
|
const nextHeight =
|
|
110
110
|
startSize.height - startPosition.y + mouseMoveEvent.pageY;
|
|
111
111
|
setSize({
|
|
112
|
-
width: nextWidth,
|
|
113
|
-
height: nextHeight,
|
|
112
|
+
width: nextWidth > 320 ? nextWidth : 320,
|
|
113
|
+
height: nextHeight > 320 ? nextHeight : 320,
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -131,11 +131,20 @@ function useDrag({
|
|
|
131
131
|
const [position, setPosition] = useState({ left: 0, top: 0 });
|
|
132
132
|
useEffect(() => {
|
|
133
133
|
if (initPosition) {
|
|
134
|
+
const left =
|
|
135
|
+
initPosition.left -
|
|
136
|
+
(ref?.current?.getBoundingClientRect()?.width || 0) / 2;
|
|
137
|
+
const top = initPosition.top - 16;
|
|
138
|
+
setPosition({
|
|
139
|
+
left: left < 0 ? 0 : left,
|
|
140
|
+
top: top < 0 ? 0 : top,
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
const left = window.innerWidth / 2 - 320 / 2 - window.scrollX;
|
|
144
|
+
const top = window.innerHeight / 2 - 320 / 2 + window.scrollY;
|
|
134
145
|
setPosition({
|
|
135
|
-
left
|
|
136
|
-
|
|
137
|
-
(ref?.current?.getBoundingClientRect()?.width || 0) / 2,
|
|
138
|
-
top: initPosition.top - 16,
|
|
146
|
+
left,
|
|
147
|
+
top,
|
|
139
148
|
});
|
|
140
149
|
}
|
|
141
150
|
}, [initPosition]);
|
|
@@ -150,14 +159,13 @@ function useDrag({
|
|
|
150
159
|
if (mouseDown) {
|
|
151
160
|
e.preventDefault();
|
|
152
161
|
e.stopPropagation();
|
|
162
|
+
const left =
|
|
163
|
+
-((ref?.current?.getBoundingClientRect()?.width || 0) / 2) + e.pageX;
|
|
153
164
|
const top =
|
|
154
165
|
-((ref?.current?.getBoundingClientRect()?.height || 0) / 2) +
|
|
155
166
|
+e.pageY;
|
|
156
|
-
|
|
157
167
|
setPosition({
|
|
158
|
-
left:
|
|
159
|
-
-((ref?.current?.getBoundingClientRect()?.width || 0) / 2) +
|
|
160
|
-
e.pageX,
|
|
168
|
+
left: left < 0 ? 0 : left,
|
|
161
169
|
top: top < 0 ? 0 : top,
|
|
162
170
|
});
|
|
163
171
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FC, ReactNode } from "react";
|
|
2
|
+
import ExpandLogo from "../ExpandLogo";
|
|
3
|
+
|
|
4
|
+
export const DashboardButton: FC<{
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
expanded: boolean;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}> = ({ onClick, children, expanded }) => {
|
|
9
|
+
return (
|
|
10
|
+
<button
|
|
11
|
+
onClick={onClick}
|
|
12
|
+
className="py-2 px-3 font-serif text-[12px] tracking-[0.04em] font-[500] border rounded-md text-white dark:border-white border-warm-black bg-warm-black group dark:hover:border-highlight hover:text-highlight "
|
|
13
|
+
>
|
|
14
|
+
<span className="flex flex-row items-center justify-center gap-2">
|
|
15
|
+
{
|
|
16
|
+
<ExpandLogo
|
|
17
|
+
expanded={expanded}
|
|
18
|
+
className="fill-white group-hover:fill-highlight"
|
|
19
|
+
/>
|
|
20
|
+
}
|
|
21
|
+
{children}
|
|
22
|
+
</span>
|
|
23
|
+
</button>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SerializedModule } from "@valbuild/core";
|
|
2
|
+
import React, { FC, useRef, useState } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import Chevron from "../../assets/icons/Chevron";
|
|
5
|
+
|
|
6
|
+
interface DashboardDropdownProps {
|
|
7
|
+
selectedModule: SerializedModule;
|
|
8
|
+
setSelectedModule: React.Dispatch<React.SetStateAction<SerializedModule>>;
|
|
9
|
+
modules: SerializedModule[];
|
|
10
|
+
}
|
|
11
|
+
export const DashboardDropdown: FC<DashboardDropdownProps> = ({
|
|
12
|
+
selectedModule,
|
|
13
|
+
setSelectedModule,
|
|
14
|
+
modules,
|
|
15
|
+
}) => {
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
17
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div ref={dropdownRef} className="font-serif relative w-full max-w-[300px]">
|
|
21
|
+
<button
|
|
22
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
23
|
+
className={classNames("w-full")}
|
|
24
|
+
>
|
|
25
|
+
<span className="flex flex-row items-center justify-between w-full">
|
|
26
|
+
<p>
|
|
27
|
+
{selectedModule ? selectedModule.path : "No module selected..."}
|
|
28
|
+
</p>
|
|
29
|
+
<Chevron className={classNames({ "rotate-90": !isOpen })} />
|
|
30
|
+
</span>
|
|
31
|
+
</button>
|
|
32
|
+
<div
|
|
33
|
+
className={classNames(
|
|
34
|
+
{ block: isOpen, hidden: !isOpen },
|
|
35
|
+
"absolute right-0 flex flex-col bg-dark-gray z-10"
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{modules.map((module, idx) => (
|
|
39
|
+
<button
|
|
40
|
+
key={idx}
|
|
41
|
+
className={classNames(
|
|
42
|
+
{
|
|
43
|
+
"bg-light-gray hover:bg-dark-gray ": selectedModule === module,
|
|
44
|
+
"hover:bg-light-gray": selectedModule !== module,
|
|
45
|
+
},
|
|
46
|
+
" w-full px-4 py-4"
|
|
47
|
+
)}
|
|
48
|
+
onClick={() => {
|
|
49
|
+
setSelectedModule(module);
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{module.path}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Dropdown } from "./Dropdown";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Dropdown> = { component: Dropdown };
|
|
5
|
+
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj<typeof Dropdown>;
|
|
8
|
+
|
|
9
|
+
export const Default: Story = {
|
|
10
|
+
render: () => <Dropdown options={["/blogs", "/journals"]} />,
|
|
11
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import { ReactElement, ReactNode, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
type DropdownProps = {
|
|
5
|
+
options?: string[];
|
|
6
|
+
onClick?: (path: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export function Dropdown({
|
|
9
|
+
options = [],
|
|
10
|
+
onClick,
|
|
11
|
+
}: DropdownProps): ReactElement {
|
|
12
|
+
const [selected, setSelected] = useState<string>(options[0]);
|
|
13
|
+
const dropdownRef = useRef<HTMLSelectElement>(null);
|
|
14
|
+
|
|
15
|
+
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
16
|
+
setSelected(event.target.value);
|
|
17
|
+
if (onClick) onClick(event.target.value);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<select
|
|
22
|
+
className="relative justify-start w-full px-4 mx-2 font-serif text-xs text-white group bg-warm-black hover:bg-dark-gray"
|
|
23
|
+
onChange={handleChange}
|
|
24
|
+
ref={dropdownRef}
|
|
25
|
+
>
|
|
26
|
+
{options.map((option, index) => (
|
|
27
|
+
<option
|
|
28
|
+
key={index}
|
|
29
|
+
value={option}
|
|
30
|
+
className={classNames(
|
|
31
|
+
{ "bg-yellow": selected === option },
|
|
32
|
+
{ "bg-red": selected !== option }
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{option}
|
|
36
|
+
</option>
|
|
37
|
+
))}
|
|
38
|
+
</select>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type DropdownChildProps = {
|
|
43
|
+
children?: ReactNode | ReactNode[];
|
|
44
|
+
id?: string | number;
|
|
45
|
+
onClick?: () => void;
|
|
46
|
+
selected?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
Dropdown.Child = ({
|
|
50
|
+
children,
|
|
51
|
+
onClick,
|
|
52
|
+
selected,
|
|
53
|
+
}: DropdownChildProps): ReactElement => {
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
onClick={() => {
|
|
57
|
+
if (onClick) {
|
|
58
|
+
onClick();
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
className={classNames(
|
|
62
|
+
"flex flex-col py-2 px-3 w-full justify-start items-start bg-warm-black group-hover:bg-dark-gray hover:bg-dark-gray tracking-wider text-[12px] font-[400] font-serif hover:cursor-pointer",
|
|
63
|
+
{ "bg-yellow text-warm-black ": selected },
|
|
64
|
+
{ "text-white ": !selected }
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { FormGroup } from "./FormGroup";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof FormGroup> = { component: FormGroup };
|
|
5
|
+
|
|
6
|
+
export default meta;
|
|
7
|
+
type Story = StoryObj<typeof FormGroup>;
|
|
8
|
+
|
|
9
|
+
export const Default: Story = {
|
|
10
|
+
render: () => (
|
|
11
|
+
<FormGroup>
|
|
12
|
+
<div>Object 1</div>
|
|
13
|
+
<div>Object 2</div>
|
|
14
|
+
<div>Object 3</div>
|
|
15
|
+
<div>Object 4</div>
|
|
16
|
+
</FormGroup>
|
|
17
|
+
),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const SeveralGroups: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<div>
|
|
23
|
+
<FormGroup>
|
|
24
|
+
<div>Object 1</div>
|
|
25
|
+
<div>Object 2</div>
|
|
26
|
+
<div>Object 3</div>
|
|
27
|
+
<div>Object 4</div>
|
|
28
|
+
</FormGroup>
|
|
29
|
+
<FormGroup>
|
|
30
|
+
<div>Object 5</div>
|
|
31
|
+
<div>Object 6</div>
|
|
32
|
+
<div>Object 7</div>
|
|
33
|
+
<div>Object 8</div>
|
|
34
|
+
</FormGroup>
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import { Children, ReactNode, useState } from "react";
|
|
3
|
+
|
|
4
|
+
interface FormGroupProps {
|
|
5
|
+
children: ReactNode | ReactNode[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const FormGroup = ({ children }: FormGroupProps) => {
|
|
9
|
+
const [firstChild, ...rest] = Children.toArray(children);
|
|
10
|
+
const [expanded, setExpanded] = useState<boolean>(false);
|
|
11
|
+
const defaultClass =
|
|
12
|
+
"px-4 py-3 border-b border-dark-gray hover:bg-light-gray hover:border-light-gray";
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<div className="flex flex-col font-serif text-xs leading-4 tracking-wider text-white">
|
|
16
|
+
<button
|
|
17
|
+
className={classNames(
|
|
18
|
+
defaultClass,
|
|
19
|
+
"bg-warm-black flex justify-between items-center"
|
|
20
|
+
)}
|
|
21
|
+
onClick={() => setExpanded(!expanded)}
|
|
22
|
+
>
|
|
23
|
+
{firstChild}
|
|
24
|
+
<div>{expanded ? "Collapse" : "Expand"}</div>
|
|
25
|
+
</button>
|
|
26
|
+
{expanded && (
|
|
27
|
+
<div className="flex flex-col bg-medium-black">
|
|
28
|
+
{Children.map(rest, (child) => (
|
|
29
|
+
<div className={classNames(defaultClass)}>{child}</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Grid } from "./Grid";
|
|
3
|
+
import { Tree } from "./Tree";
|
|
4
|
+
import { Dropdown } from "./Dropdown";
|
|
5
|
+
import { FormGroup } from "./FormGroup";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Grid> = { component: Grid };
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof Grid>;
|
|
11
|
+
export const Default: Story = {
|
|
12
|
+
render: () => (
|
|
13
|
+
<Grid>
|
|
14
|
+
<Dropdown options={["/blogs", "/journals"]} />
|
|
15
|
+
<Tree>
|
|
16
|
+
<Tree.Node path="Main nav" type="section" />
|
|
17
|
+
<Tree.Node path="H1" type="string">
|
|
18
|
+
<Tree.Node path="Section 3" type="string" />
|
|
19
|
+
<Tree.Node path="Section 4" type="section">
|
|
20
|
+
<Tree.Node path="Section 5" type="string" />
|
|
21
|
+
<Tree.Node path="Section 6" type="section">
|
|
22
|
+
<Tree.Node path="Section 7" type="string" />
|
|
23
|
+
</Tree.Node>
|
|
24
|
+
</Tree.Node>
|
|
25
|
+
</Tree.Node>
|
|
26
|
+
</Tree>
|
|
27
|
+
<div className="font-serif text-xs w-full h-full flex justify-between items-center px-3 text-white">
|
|
28
|
+
<p>Content</p>
|
|
29
|
+
<button className="flex justify-between gap-1 flex-shrink-0">
|
|
30
|
+
<span className="w-fit">+</span>
|
|
31
|
+
<span className="w-fit">Add item</span>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div>
|
|
35
|
+
<FormGroup>
|
|
36
|
+
<div>Object 1</div>
|
|
37
|
+
<div>Object 2</div>
|
|
38
|
+
<div>Object 3</div>
|
|
39
|
+
<div>Object 4</div>
|
|
40
|
+
</FormGroup>
|
|
41
|
+
<FormGroup>
|
|
42
|
+
<div>Object 5</div>
|
|
43
|
+
<div>Object 6</div>
|
|
44
|
+
<div>Object 7</div>
|
|
45
|
+
<div>Object 8</div>
|
|
46
|
+
</FormGroup>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="text-white">History</div>
|
|
49
|
+
<div className="text-white">hey</div>
|
|
50
|
+
</Grid>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Children, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
type GridProps = {
|
|
4
|
+
children: React.ReactNode | React.ReactNode[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function Grid({ children }: GridProps): React.ReactElement {
|
|
8
|
+
const leftColRef = useRef<HTMLDivElement>(null);
|
|
9
|
+
const rightColRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
|
|
11
|
+
const isResizing = useRef(false);
|
|
12
|
+
const x = useRef(0);
|
|
13
|
+
const dragRef = useRef<"left" | "right" | null>(null);
|
|
14
|
+
const originalWidth = useRef(0);
|
|
15
|
+
|
|
16
|
+
const handleMouseUp = () => {
|
|
17
|
+
isResizing.current = false;
|
|
18
|
+
dragRef.current = null;
|
|
19
|
+
x.current = 0;
|
|
20
|
+
originalWidth.current = 0;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
const targetRef = dragRef.current === "left" ? leftColRef : rightColRef;
|
|
26
|
+
if (targetRef.current && isResizing.current) {
|
|
27
|
+
const dx =
|
|
28
|
+
dragRef.current === "left"
|
|
29
|
+
? event.screenX - x.current
|
|
30
|
+
: x.current - event.screenX;
|
|
31
|
+
targetRef.current.style.width = `${Math.max(
|
|
32
|
+
originalWidth.current + dx,
|
|
33
|
+
150
|
|
34
|
+
)}px`;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleMouseDown =
|
|
39
|
+
(column: "left" | "right") =>
|
|
40
|
+
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
41
|
+
const target = event.target as HTMLDivElement;
|
|
42
|
+
if (target) {
|
|
43
|
+
const columnRef =
|
|
44
|
+
column === "left"
|
|
45
|
+
? leftColRef
|
|
46
|
+
: column === "right"
|
|
47
|
+
? rightColRef
|
|
48
|
+
: null;
|
|
49
|
+
isResizing.current = true;
|
|
50
|
+
if (columnRef && columnRef.current) {
|
|
51
|
+
x.current = event.screenX;
|
|
52
|
+
dragRef.current = column;
|
|
53
|
+
if (columnRef.current) {
|
|
54
|
+
originalWidth.current = columnRef.current.offsetWidth;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const [header1, body1, header2, body2, header3, body3] =
|
|
61
|
+
Children.toArray(children);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
64
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
68
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex w-full h-screen">
|
|
74
|
+
<div
|
|
75
|
+
ref={leftColRef}
|
|
76
|
+
className="relative h-full border-r border-dark-gray"
|
|
77
|
+
style={{ width: 300 }}
|
|
78
|
+
>
|
|
79
|
+
<Grid.Column>
|
|
80
|
+
{header1}
|
|
81
|
+
{body1}
|
|
82
|
+
</Grid.Column>
|
|
83
|
+
<div
|
|
84
|
+
className="absolute inset-y-0 right-0 cursor-col-resize w-[1px] bg-dark-gray hover:w-[2px] hover:bg-light-gray"
|
|
85
|
+
onMouseDown={handleMouseDown("left")}
|
|
86
|
+
></div>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex-auto bg-warm-black">
|
|
89
|
+
<Grid.Column>
|
|
90
|
+
{header2}
|
|
91
|
+
{body2}
|
|
92
|
+
</Grid.Column>
|
|
93
|
+
</div>
|
|
94
|
+
<div
|
|
95
|
+
ref={rightColRef}
|
|
96
|
+
className="relative border-l border-dark-gray bg-warm-black"
|
|
97
|
+
style={{ width: 300 }}
|
|
98
|
+
>
|
|
99
|
+
<Grid.Column>
|
|
100
|
+
{header3}
|
|
101
|
+
{body3}
|
|
102
|
+
</Grid.Column>
|
|
103
|
+
<div
|
|
104
|
+
onMouseDown={handleMouseDown("right")}
|
|
105
|
+
className="absolute inset-y-0 left-0 cursor-col-resize w-[1px] bg-dark-gray hover:w-[2px] hover:bg-light-gray"
|
|
106
|
+
></div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type GridChildProps = {
|
|
113
|
+
children: React.ReactNode | React.ReactNode[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
Grid.Column = ({ children }: GridChildProps): React.ReactElement => {
|
|
117
|
+
const [header, body] = Children.toArray(children);
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex flex-col h-full overflow-auto bg-warm-black">
|
|
120
|
+
<div className="h-[50px] flex items-center border-b border-dark-gray">
|
|
121
|
+
{header}
|
|
122
|
+
</div>
|
|
123
|
+
{body}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Grid } from "./Grid2";
|
|
3
|
+
import { Tree } from "./Tree";
|
|
4
|
+
import { Dropdown } from "./Dropdown";
|
|
5
|
+
import { FormGroup } from "./FormGroup";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Grid> = { component: Grid };
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof Grid>;
|
|
11
|
+
export const Default: Story = {
|
|
12
|
+
render: () => (
|
|
13
|
+
<Grid>
|
|
14
|
+
<Grid.Row>
|
|
15
|
+
<Dropdown options={["/blogs", "/journals"]} />
|
|
16
|
+
<div className="font-serif text-xs w-full h-full flex justify-between items-center px-3 text-white">
|
|
17
|
+
<p>Content</p>
|
|
18
|
+
<button className="flex justify-between gap-1 flex-shrink-0">
|
|
19
|
+
<span className="w-fit">+</span>
|
|
20
|
+
<span className="w-fit">Add item</span>
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
<div className="text-white">History</div>
|
|
24
|
+
</Grid.Row>
|
|
25
|
+
<Grid.Row fill>
|
|
26
|
+
<Tree>
|
|
27
|
+
<Tree.Node path="Main nav" type="section" />
|
|
28
|
+
<Tree.Node path="H1" type="string">
|
|
29
|
+
<Tree.Node path="Section 3" type="string" />
|
|
30
|
+
<Tree.Node path="Section 4" type="section">
|
|
31
|
+
<Tree.Node path="Section 5" type="string" />
|
|
32
|
+
<Tree.Node path="Section 6" type="section">
|
|
33
|
+
<Tree.Node path="Section 7" type="string" />
|
|
34
|
+
</Tree.Node>
|
|
35
|
+
</Tree.Node>
|
|
36
|
+
</Tree.Node>
|
|
37
|
+
</Tree>
|
|
38
|
+
<div>
|
|
39
|
+
<FormGroup>
|
|
40
|
+
<div>Object 1</div>
|
|
41
|
+
<div>Object 2</div>
|
|
42
|
+
<div>Object 3</div>
|
|
43
|
+
<div>Object 4</div>
|
|
44
|
+
</FormGroup>
|
|
45
|
+
<FormGroup>
|
|
46
|
+
<div>Object 5</div>
|
|
47
|
+
<div>Object 6</div>
|
|
48
|
+
<div>Object 7</div>
|
|
49
|
+
<div>Object 8</div>
|
|
50
|
+
</FormGroup>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="text-white">hey</div>
|
|
53
|
+
</Grid.Row>
|
|
54
|
+
</Grid>
|
|
55
|
+
),
|
|
56
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import { Children, cloneElement, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
interface GridProps {
|
|
5
|
+
children: React.ReactNode | React.ReactNode[];
|
|
6
|
+
}
|
|
7
|
+
export function Grid({ children }: GridProps): React.ReactElement {
|
|
8
|
+
const leftColHeaderRef = useRef<HTMLDivElement>(null);
|
|
9
|
+
const leftColBodyRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
const rightColHeaderRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
const rightColBodyRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
|
|
13
|
+
const [headerRow, bodyRow] = Children.toArray(children);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex flex-col w-full h-screen bg-warm-black overflow-clip">
|
|
17
|
+
{cloneElement(headerRow as React.ReactElement, {
|
|
18
|
+
leftColRef: leftColHeaderRef,
|
|
19
|
+
rightColRef: rightColHeaderRef,
|
|
20
|
+
})}
|
|
21
|
+
{cloneElement(bodyRow as React.ReactElement, {
|
|
22
|
+
leftColRef: leftColBodyRef,
|
|
23
|
+
rightColRef: rightColBodyRef,
|
|
24
|
+
})}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type GridRowProps = {
|
|
30
|
+
children: React.ReactNode | React.ReactNode[];
|
|
31
|
+
leftColRef?: React.RefObject<HTMLDivElement>;
|
|
32
|
+
rightColRef?: React.RefObject<HTMLDivElement>;
|
|
33
|
+
fill?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Grid.Row = ({
|
|
37
|
+
children,
|
|
38
|
+
leftColRef,
|
|
39
|
+
rightColRef,
|
|
40
|
+
fill: bottomRow = false,
|
|
41
|
+
}: GridRowProps): React.ReactElement => {
|
|
42
|
+
const [leftCol, middleCol, rightCol] = Children.toArray(children);
|
|
43
|
+
//implement mouseover event to share hover-event between rows
|
|
44
|
+
console;
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={classNames("flex w-full py-2 border-b border-dark-gray", {
|
|
48
|
+
"h-full": bottomRow,
|
|
49
|
+
})}
|
|
50
|
+
>
|
|
51
|
+
<div className="relative" style={{ width: "300px" }} ref={leftColRef}>
|
|
52
|
+
{leftCol}
|
|
53
|
+
{bottomRow && (
|
|
54
|
+
<div
|
|
55
|
+
className="absolute inset-y-0 right-0 cursor-col-resize w-[1px] bg-dark-gray hover:w-[2px] hover:bg-light-gray -my-[100px]"
|
|
56
|
+
// onMouseDown={handleMouseDown("left")}
|
|
57
|
+
></div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex-auto">{middleCol}</div>
|
|
61
|
+
<div className="relative" style={{ width: "300px" }} ref={rightColRef}>
|
|
62
|
+
{rightCol}
|
|
63
|
+
{bottomRow && (
|
|
64
|
+
<div
|
|
65
|
+
// onMouseDown={handleMouseDown("right")}
|
|
66
|
+
className="absolute inset-y-0 left-0 cursor-col-resize w-[1px] bg-dark-gray hover:w-[2px] hover:bg-light-gray -my-2"
|
|
67
|
+
></div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|