@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
@@ -0,0 +1,91 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Tree } from "./Tree";
3
+
4
+ const meta: Meta<typeof Tree> = { component: Tree };
5
+
6
+ export default meta;
7
+ type Story = StoryObj<typeof Tree>;
8
+
9
+ type Node = {
10
+ path: string;
11
+ type: "string" | "image" | "section";
12
+ children: Node[];
13
+ };
14
+
15
+ const nodes: Node[] = [
16
+ {
17
+ path: "Main nav",
18
+ type: "section",
19
+ children: [],
20
+ },
21
+ {
22
+ path: "H1",
23
+ type: "string",
24
+ children: [
25
+ {
26
+ path: "Section 3",
27
+ type: "string",
28
+ children: [],
29
+ },
30
+ {
31
+ path: "Section 4",
32
+ type: "section",
33
+ children: [
34
+ {
35
+ path: "Section 5",
36
+ type: "string",
37
+ children: [],
38
+ },
39
+ {
40
+ path: "Section 6",
41
+ type: "section",
42
+ children: [
43
+ {
44
+ path: "Section 7",
45
+ type: "string",
46
+ children: [],
47
+ },
48
+ {
49
+ path: "Image 1",
50
+ type: "image",
51
+ children: [],
52
+ },
53
+ {
54
+ path: "Image 2",
55
+ type: "image",
56
+ children: [],
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ },
64
+ ];
65
+
66
+ const renderNodes = (node: Node) => (
67
+ <Tree.Node path={node.path} type={node.type}>
68
+ {node.children.map(renderNodes)}
69
+ </Tree.Node>
70
+ );
71
+
72
+ export const Default: Story = {
73
+ render: () => (
74
+ <Tree rootPath="root">
75
+ <Tree.Node path="Main nav" type="section" />
76
+ <Tree.Node path="H1" type="string">
77
+ <Tree.Node path="Section 3" type="string" />
78
+ <Tree.Node path="Section 4" type="section">
79
+ <Tree.Node path="Section 5" type="string" />
80
+ <Tree.Node path="Section 6" type="section">
81
+ <Tree.Node path="Section 7" type="string" />
82
+ </Tree.Node>
83
+ </Tree.Node>
84
+ </Tree.Node>
85
+ </Tree>
86
+ ),
87
+ };
88
+
89
+ export const TestData: Story = {
90
+ render: () => <Tree rootPath="root">{nodes.map(renderNodes)}</Tree>,
91
+ };
@@ -0,0 +1,72 @@
1
+ import { Children, cloneElement } from "react";
2
+ import ImageIcon from "../../assets/icons/ImageIcon";
3
+ import Section from "../../assets/icons/Section";
4
+ import TextIcon from "../../assets/icons/TextIcon";
5
+
6
+ type TreeProps = {
7
+ children: React.ReactNode | React.ReactNode[];
8
+ rootPath?: string;
9
+ };
10
+ export function Tree({ children, rootPath }: TreeProps): React.ReactElement {
11
+ return (
12
+ <div className="flex flex-col bg-warm-black text-white font-sans text-xs w-full py-2">
13
+ {Children.map(children, (child) => {
14
+ return cloneElement(child as React.ReactElement, {
15
+ paths: [rootPath],
16
+ });
17
+ })}
18
+ </div>
19
+ );
20
+ }
21
+ type TreeNodeProps = {
22
+ children?: React.ReactNode | React.ReactNode[];
23
+ path: string;
24
+ paths?: string[];
25
+ level?: number;
26
+ type: "string" | "image" | "section";
27
+ setActivePath?: (path: string) => void;
28
+ };
29
+ Tree.Node = ({
30
+ children,
31
+ paths = [],
32
+ path,
33
+ level = 1,
34
+ type,
35
+ setActivePath,
36
+ }: TreeNodeProps): React.ReactElement => {
37
+ const paddingLeft = level * 30;
38
+ const logo =
39
+ type === "string" ? (
40
+ <TextIcon />
41
+ ) : type === "image" ? (
42
+ <ImageIcon className="h-[9px] w-[9px]" />
43
+ ) : (
44
+ <Section />
45
+ );
46
+ return (
47
+ <div className="w-full">
48
+ <button
49
+ className="flex justify-between w-full text-white hover:bg-dark-gray group py-2 hover:text-warm-black text-xs font-[400] shrink-0"
50
+ onClick={() => {
51
+ setActivePath && setActivePath(path);
52
+ }}
53
+ style={{ paddingLeft: paddingLeft }}
54
+ >
55
+ <div className="flex items-center justify-center gap-2">
56
+ {logo}
57
+ <p>{path}</p>
58
+ </div>
59
+ </button>
60
+ {children && (
61
+ <>
62
+ {Children.map(children, (child) => {
63
+ return cloneElement(child as React.ReactElement, {
64
+ level: level + 1,
65
+ paths: [...paths, path],
66
+ });
67
+ })}
68
+ </>
69
+ )}
70
+ </div>
71
+ );
72
+ };
@@ -0,0 +1,148 @@
1
+ import { SerializedModule } from "@valbuild/core";
2
+ import { Json } from "@valbuild/core/src/Json";
3
+ import { ValApi } from "@valbuild/core";
4
+ import { FC, useEffect, useState } from "react";
5
+ import { Dropdown } from "./Dropdown";
6
+ import { FormGroup } from "./FormGroup";
7
+ import { Grid } from "./Grid";
8
+ import { Tree } from "./Tree";
9
+
10
+ interface ValDashboardProps {
11
+ showDashboard: boolean;
12
+ editMode: boolean;
13
+ valApi: ValApi;
14
+ }
15
+ export const ValDashboard: FC<ValDashboardProps> = ({
16
+ showDashboard,
17
+ editMode,
18
+ }) => {
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ const [modules, setModules] = useState<SerializedModule[]>([]);
21
+ const [selectedPath, setSelectedPath] = useState<string>("");
22
+ const [selectedModule, setSelectedModule] = useState<
23
+ | {
24
+ [key: string]: {
25
+ path: string;
26
+ type: string;
27
+ };
28
+ }[]
29
+ | undefined
30
+ >();
31
+
32
+ useEffect(() => {
33
+ // valApi.getModules({ patch: true, includeSource: true }).then((modules) => {
34
+ // // TODO:
35
+ // });
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ const newModule = modules.find((module) => module.path === selectedPath);
40
+ if (newModule) {
41
+ const children = mapChildren(newModule);
42
+ console.log("children", children);
43
+ setSelectedModule(children);
44
+ }
45
+ }, [selectedPath]);
46
+
47
+ const mapChildren = (module: SerializedModule) => {
48
+ if (module) {
49
+ if (module.schema.type === "array") {
50
+ return (module.source as Json[]).map((child) => {
51
+ const newModule: {
52
+ [key: string]: {
53
+ path: string;
54
+ type: string;
55
+ };
56
+ } = {};
57
+ if (child) {
58
+ for (const key of Object.keys(child)) {
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const type = (module.schema as any).item.items[key]
61
+ .type as string;
62
+ if (key !== "rank" && type !== "richtext") {
63
+ newModule[key] = {
64
+ path: key,
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ type: (module.schema as any).item.items[key].type as string,
67
+ };
68
+ }
69
+ }
70
+ }
71
+ return newModule;
72
+ });
73
+ } else {
74
+ const child = module.source as Json;
75
+ const newModule: {
76
+ [key: string]: {
77
+ path: string;
78
+ type: string;
79
+ };
80
+ } = {};
81
+ if (child) {
82
+ for (const key of Object.keys(child)) {
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ const type = (module.schema as any).items[key].type as string;
85
+ if (key !== "rank" && type !== "richtext") {
86
+ newModule[key] = {
87
+ path: key,
88
+ type: type,
89
+ };
90
+ }
91
+ }
92
+ }
93
+ return [newModule];
94
+ }
95
+ }
96
+ };
97
+
98
+ return (
99
+ <>
100
+ {showDashboard && editMode && (
101
+ <div className="bg-base w-screen fixed z-10 top-[68.54px] text-white overflow-hidden">
102
+ <Grid>
103
+ {modules && (
104
+ <Dropdown
105
+ options={modules.map((module) => module.path)}
106
+ onClick={(path) => setSelectedPath(path)}
107
+ />
108
+ )}
109
+ {selectedModule && (
110
+ <Tree>
111
+ {selectedModule.map((child, idx) => {
112
+ return (
113
+ <Tree.Node
114
+ key={idx}
115
+ path={`Section ${idx + 1}`}
116
+ type="section"
117
+ >
118
+ {Object.values(child).map((value, idx2) => (
119
+ <Tree.Node
120
+ key={idx2}
121
+ path={value.path}
122
+ type={value.type as "string" | "image" | "section"}
123
+ />
124
+ ))}
125
+ </Tree.Node>
126
+ );
127
+ })}
128
+ </Tree>
129
+ )}
130
+ <div className="flex items-center justify-between w-full h-full px-3 font-serif text-xs text-white">
131
+ <p>Content</p>
132
+ <button className="flex justify-between flex-shrink-0 gap-1">
133
+ <span className="w-fit">+</span>
134
+ <span className="w-fit">Add item</span>
135
+ </button>
136
+ </div>
137
+ <FormGroup>
138
+ <div>test</div>
139
+ <div>test</div>
140
+ <div>test</div>
141
+ </FormGroup>
142
+ <div>content</div>
143
+ </Grid>
144
+ </div>
145
+ )}
146
+ </>
147
+ );
148
+ };
@@ -0,0 +1,269 @@
1
+ import { SerializedModule } from "@valbuild/core";
2
+ import { ValApi } from "@valbuild/core";
3
+ import { LexicalEditor } from "lexical";
4
+ import { FC, useEffect, useState } from "react";
5
+ import { Inputs, RichTextEditor } from "../../exports";
6
+ import Button from "../Button";
7
+ import { ImageForm } from "../forms/ImageForm";
8
+ import { TextArea } from "../forms/TextArea";
9
+
10
+ interface ValDashboardEditorProps {
11
+ selectedPath: string;
12
+ valApi: ValApi;
13
+ }
14
+
15
+ export const ValDashboardEditor: FC<ValDashboardEditorProps> = ({
16
+ selectedPath,
17
+ }) => {
18
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
+ const [selectedModule, setSelectedModule] = useState<SerializedModule>();
20
+ const [inputs, setInputs] = useState<Inputs>({});
21
+ const [inputIsDirty, setInputIsDirty] = useState<{ [path: string]: boolean }>(
22
+ {}
23
+ );
24
+ const [richTextEditor, setRichTextEditor] = useState<{
25
+ [path: string]: LexicalEditor;
26
+ }>();
27
+
28
+ useEffect(() => {
29
+ // if (selectedPath) {
30
+ // valApi.getModule(selectedPath).then((module) => {
31
+ // setSelectedModule(module);
32
+ // });
33
+ // }
34
+ }, [selectedPath]);
35
+
36
+ useEffect(() => {
37
+ // if (selectedModule && selectedModule?.source) {
38
+ // setInputs({});
39
+ // for (const key of Object.keys(selectedModule?.source)) {
40
+ // if (key !== "rank") {
41
+ // valApi
42
+ // .getModule(`${selectedModule.path}.${key}`)
43
+ // .then((serializedModule) => {
44
+ // let input: Inputs[string] | undefined;
45
+ // if (
46
+ // serializedModule.schema.type === "string" &&
47
+ // typeof serializedModule.source === "string"
48
+ // ) {
49
+ // input = {
50
+ // status: "completed",
51
+ // type: "text",
52
+ // data: serializedModule.source,
53
+ // };
54
+ // } else if (
55
+ // serializedModule.schema.type === "richtext" &&
56
+ // typeof serializedModule.source === "object"
57
+ // ) {
58
+ // input = {
59
+ // status: "completed",
60
+ // type: "richtext",
61
+ // data: serializedModule.source as RichText, // TODO: validate
62
+ // };
63
+ // } else if (
64
+ // serializedModule.schema.type === "image" &&
65
+ // serializedModule.source &&
66
+ // typeof serializedModule.source === "object" &&
67
+ // FILE_REF_PROP in serializedModule.source &&
68
+ // typeof serializedModule.source[FILE_REF_PROP] === "string" &&
69
+ // VAL_EXTENSION in serializedModule.source &&
70
+ // typeof serializedModule.source[VAL_EXTENSION] === "string"
71
+ // ) {
72
+ // input = {
73
+ // status: "completed",
74
+ // type: "image",
75
+ // data: Internal.convertImageSource(
76
+ // serializedModule.source as FileSource<ImageMetadata>
77
+ // ),
78
+ // };
79
+ // }
80
+ // if (!input) {
81
+ // throw new Error(
82
+ // `Unsupported module type: ${serializedModule.schema.type}`
83
+ // );
84
+ // }
85
+ // setInputs((inputs) => {
86
+ // return {
87
+ // ...inputs,
88
+ // [serializedModule.path]: input,
89
+ // } as Inputs;
90
+ // });
91
+ // });
92
+ // }
93
+ // }
94
+ // }
95
+ }, [selectedModule]);
96
+
97
+ useEffect(() => {
98
+ for (const key of Object.keys(inputs)) {
99
+ if (!Object.keys(inputIsDirty).includes(key)) {
100
+ setInputIsDirty({
101
+ ...inputIsDirty,
102
+ [key]: false,
103
+ });
104
+ }
105
+ }
106
+ }, [inputs]);
107
+
108
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
109
+ const patchElement = async (path: string) => {
110
+ // Promise.all(
111
+ // Object.entries(inputs)
112
+ // .filter(([k]) => k === key)
113
+ // .map(([path, input]) => {
114
+ // if (input.status === "completed") {
115
+ // const [moduleId, modulePath] = Internal.splitModuleIdAndModulePath(
116
+ // path as SourcePath
117
+ // );
118
+ // if (input.type === "text") {
119
+ // const patch: PatchJSON = [
120
+ // {
121
+ // value: input.data,
122
+ // op: "replace",
123
+ // path: `/${modulePath
124
+ // .split(".")
125
+ // .map((p) => {
126
+ // return JSON.parse(p);
127
+ // })
128
+ // .join("/")}`,
129
+ // },
130
+ // ];
131
+ // console.log("patch", patch);
132
+ // return valApi.patchModuleContent(moduleId, patch);
133
+ // } else if (input.type === "image") {
134
+ // const pathParts = modulePath.split(".").map((p) => JSON.parse(p));
135
+ // if (!input?.data || !("src" in input.data)) {
136
+ // // TODO: We probably need to have an Output type that is different from the Input: we have a union of both cases in Input right now, and we believe we do not want that
137
+ // console.warn(
138
+ // "No .src on input provided - this might mean no changes was made"
139
+ // );
140
+ // return;
141
+ // }
142
+ // const patch: PatchJSON = [
143
+ // {
144
+ // value: input.data.src,
145
+ // op: "replace",
146
+ // path: `/${pathParts.slice(0, -1).join("/")}/$${
147
+ // pathParts[pathParts.length - 1]
148
+ // }`,
149
+ // },
150
+ // ];
151
+ // if (input.data.metadata) {
152
+ // if (input.data.addMetadata) {
153
+ // patch.push({
154
+ // value: input.data.metadata,
155
+ // op: "add",
156
+ // path: `/${pathParts.join("/")}/metadata`,
157
+ // });
158
+ // } else {
159
+ // patch.push({
160
+ // value: input.data.metadata,
161
+ // op: "replace",
162
+ // path: `/${pathParts.join("/")}/metadata`,
163
+ // });
164
+ // }
165
+ // }
166
+ // console.log("patch", patch);
167
+ // return valApi.patchModuleContent(moduleId, patch);
168
+ // } else if (input.type === "richtext") {
169
+ // const patch: PatchJSON = [
170
+ // {
171
+ // value: input.data,
172
+ // op: "replace",
173
+ // path: `/${modulePath
174
+ // .split(".")
175
+ // .map((p) => JSON.parse(p))
176
+ // .join("/")}`,
177
+ // },
178
+ // ];
179
+ // return valApi.patchModuleContent(moduleId, patch);
180
+ // }
181
+ // // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ // throw new Error(`Unsupported input type: ${(input as any).type}`);
183
+ // } else {
184
+ // console.error("Submitted incomplete input, ignoring...");
185
+ // return Promise.resolve();
186
+ // }
187
+ // })
188
+ // ).then((res) => {
189
+ // console.log("patched", res);
190
+ // });
191
+ };
192
+
193
+ return (
194
+ <div className="flex flex-col items-start px-4">
195
+ {selectedModule ? (
196
+ <div className="flex flex-col items-start w-full py-3 gap-[36px] font-normal">
197
+ {Object.entries(inputs).map(([path, input]) => {
198
+ return (
199
+ <div key={path} className={"flex flex-col justify-start "}>
200
+ {input.status === "requested" && (
201
+ <div className="p-2 text-center text-primary">Loading...</div>
202
+ )}
203
+ <div className="flex flex-col gap-1 font-[550]">
204
+ <p>{path.split(".").slice(-1)[0].split('"').join("")}</p>
205
+ {input.status === "completed" && input.type === "image" && (
206
+ <ImageForm
207
+ name={path}
208
+ data={input.data}
209
+ onChange={(data) => {
210
+ if (data.value) {
211
+ setInputs({
212
+ ...inputs,
213
+ [path]: {
214
+ status: "completed",
215
+ type: "image",
216
+ data: data.value,
217
+ },
218
+ });
219
+ setInputIsDirty({ ...inputIsDirty, [path]: true });
220
+ }
221
+ }}
222
+ error={null}
223
+ />
224
+ )}
225
+ {input.status === "completed" && input.type === "text" && (
226
+ <TextArea
227
+ name={path}
228
+ text={input.data}
229
+ onChange={(data) => {
230
+ setInputs({
231
+ ...inputs,
232
+ [path]: {
233
+ status: "completed",
234
+ type: "text",
235
+ data: data,
236
+ },
237
+ });
238
+ setInputIsDirty({ ...inputIsDirty, [path]: true });
239
+ }}
240
+ />
241
+ )}
242
+ {input.status === "completed" &&
243
+ input.type === "richtext" && (
244
+ <RichTextEditor
245
+ richtext={input.data}
246
+ onEditor={(editor) => {
247
+ setRichTextEditor({
248
+ ...richTextEditor,
249
+ [path]: editor,
250
+ });
251
+ }}
252
+ />
253
+ )}
254
+ {inputIsDirty[path] && (
255
+ <Button onClick={() => patchElement(path)}>
256
+ Save changes
257
+ </Button>
258
+ )}
259
+ </div>
260
+ </div>
261
+ );
262
+ })}
263
+ </div>
264
+ ) : (
265
+ <h1 className="px-4 py-3">No module selected</h1>
266
+ )}
267
+ </div>
268
+ );
269
+ };