@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.
Files changed (46) hide show
  1. package/dist/valbuild-ui.cjs.d.ts +13 -18
  2. package/dist/valbuild-ui.cjs.js +7624 -1718
  3. package/dist/valbuild-ui.esm.js +7625 -1719
  4. package/package.json +5 -2
  5. package/src/assets/icons/ImageIcon.tsx +15 -7
  6. package/src/assets/icons/Section.tsx +41 -0
  7. package/src/assets/icons/TextIcon.tsx +20 -0
  8. package/src/components/Button.tsx +18 -7
  9. package/src/components/DraggableList.stories.tsx +20 -0
  10. package/src/components/DraggableList.tsx +95 -0
  11. package/src/components/Dropdown.tsx +2 -0
  12. package/src/components/ExpandLogo.tsx +72 -0
  13. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +1 -16
  14. package/src/components/RichTextEditor/RichTextEditor.tsx +2 -2
  15. package/src/components/User.tsx +17 -0
  16. package/src/components/ValMenu.tsx +40 -0
  17. package/src/components/ValOverlay.tsx +513 -29
  18. package/src/components/ValOverlayContext.tsx +63 -0
  19. package/src/components/ValWindow.stories.tsx +3 -3
  20. package/src/components/ValWindow.tsx +26 -18
  21. package/src/components/dashboard/DashboardButton.tsx +25 -0
  22. package/src/components/dashboard/DashboardDropdown.tsx +59 -0
  23. package/src/components/dashboard/Dropdown.stories.tsx +11 -0
  24. package/src/components/dashboard/Dropdown.tsx +70 -0
  25. package/src/components/dashboard/FormGroup.stories.tsx +37 -0
  26. package/src/components/dashboard/FormGroup.tsx +36 -0
  27. package/src/components/dashboard/Grid.stories.tsx +52 -0
  28. package/src/components/dashboard/Grid.tsx +126 -0
  29. package/src/components/dashboard/Grid2.stories.tsx +56 -0
  30. package/src/components/dashboard/Grid2.tsx +72 -0
  31. package/src/components/dashboard/Tree.stories.tsx +91 -0
  32. package/src/components/dashboard/Tree.tsx +72 -0
  33. package/src/components/dashboard/ValDashboard.tsx +148 -0
  34. package/src/components/dashboard/ValDashboardEditor.tsx +269 -0
  35. package/src/components/dashboard/ValDashboardGrid.tsx +142 -0
  36. package/src/components/dashboard/ValTreeNavigator.tsx +253 -0
  37. package/src/components/forms/Form.tsx +2 -2
  38. package/src/components/forms/{TextForm.tsx → TextArea.tsx} +5 -3
  39. package/src/dto/SerializedSchema.ts +69 -0
  40. package/src/dto/Session.ts +12 -0
  41. package/src/dto/SessionMode.ts +5 -0
  42. package/src/dto/Tree.ts +18 -0
  43. package/src/exports.ts +1 -0
  44. package/src/utils/Remote.ts +15 -0
  45. package/src/utils/resolvePath.ts +33 -0
  46. 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": !(isInitialized || isInitializedProp),
41
- "opacity-100": isInitialized || isInitializedProp,
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 pb-[16px]"
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
- initPosition.left -
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
+ };