@valbuild/ui 0.20.0 → 0.21.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 (29) hide show
  1. package/dist/valbuild-ui.cjs.d.ts +6 -4
  2. package/dist/valbuild-ui.cjs.js +13528 -12255
  3. package/dist/valbuild-ui.esm.js +8313 -7040
  4. package/package.json +4 -2
  5. package/src/components/Button.tsx +0 -9
  6. package/src/components/Dropdown.tsx +32 -25
  7. package/src/components/RichTextEditor/Nodes/ImageNode.tsx +0 -8
  8. package/src/components/RichTextEditor/Plugins/LinkEditorPlugin.tsx +58 -0
  9. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +70 -2
  10. package/src/components/RichTextEditor/RichTextEditor.tsx +33 -8
  11. package/src/components/ValOverlay.tsx +55 -31
  12. package/src/components/ValOverlayContext.tsx +17 -0
  13. package/src/components/ValWindow.stories.tsx +5 -51
  14. package/src/components/ValWindow.tsx +27 -12
  15. package/src/components/dashboard/ValDashboard.tsx +2 -0
  16. package/src/components/forms/Form.tsx +2 -2
  17. package/src/exports.ts +1 -0
  18. package/src/richtext/conversion/conversion.test.ts +146 -0
  19. package/src/richtext/conversion/lexicalToRichTextSource.test.ts +89 -0
  20. package/src/richtext/conversion/lexicalToRichTextSource.ts +286 -0
  21. package/src/richtext/conversion/parseRichTextSource.test.ts +424 -0
  22. package/src/richtext/conversion/parseRichTextSource.ts +228 -0
  23. package/src/richtext/conversion/richTextSourceToLexical.test.ts +381 -0
  24. package/src/richtext/conversion/richTextSourceToLexical.ts +293 -0
  25. package/src/stories/RichTextEditor.stories.tsx +3 -47
  26. package/src/utils/imageMimeType.ts +23 -0
  27. package/src/utils/readImage.ts +1 -25
  28. package/src/components/RichTextEditor/conversion.test.ts +0 -132
  29. package/src/components/RichTextEditor/conversion.ts +0 -389
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valbuild/ui",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "sideEffects": false,
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
@@ -9,6 +9,7 @@
9
9
  "build-storybook": "storybook build"
10
10
  },
11
11
  "dependencies": {
12
+ "@lexical/link": "^0.10.0",
12
13
  "@lexical/list": "^0.10.0",
13
14
  "@lexical/react": "^0.10.0",
14
15
  "@lexical/rich-text": "^0.10.0",
@@ -16,10 +17,11 @@
16
17
  "@lexical/utils": "^0.10.0",
17
18
  "@types/express": "^4.17.17",
18
19
  "@types/react": "^18.0.26",
19
- "@valbuild/core": "~0.20.0",
20
+ "@valbuild/core": "~0.21.0",
20
21
  "classnames": "^2.3.2",
21
22
  "esbuild": "^0.17.19",
22
23
  "lexical": "^0.10.0",
24
+ "marked": "^9.0.3",
23
25
  "react-feather": "^2.0.10",
24
26
  "react-resizable": "^3.0.5",
25
27
  "rollup-plugin-peer-deps-external": "^2.2.4",
@@ -7,7 +7,6 @@ export interface ButtonProps
7
7
  icon?: React.ReactElement<SVGProps<SVGSVGElement>>;
8
8
  active?: boolean;
9
9
  disabled?: boolean;
10
- tooltip?: string;
11
10
  }
12
11
 
13
12
  export function PrimaryButton({
@@ -34,7 +33,6 @@ const Button: FC<ButtonProps> = ({
34
33
  icon,
35
34
  // active = false,
36
35
  disabled = false,
37
- tooltip,
38
36
  }) => {
39
37
  return (
40
38
  <button
@@ -51,13 +49,6 @@ const Button: FC<ButtonProps> = ({
51
49
  )}
52
50
  onClick={onClick}
53
51
  >
54
- {tooltip && (
55
- <div
56
- className={`absolute bottom-[-75%] left-0 z-20 bg-black w-fit h-fit text-primary hidden group-hover:block`}
57
- >
58
- <div>{tooltip}</div>
59
- </div>
60
- )}
61
52
  <span className="flex flex-row items-center justify-center gap-2">
62
53
  {icon && icon}
63
54
  {children}
@@ -1,6 +1,7 @@
1
1
  import React, { SVGProps, useEffect, useRef, useState } from "react";
2
2
  import Chevron from "../assets/icons/Chevron";
3
3
  import Button from "./Button";
4
+ import { useValOverlayContext } from "./ValOverlayContext";
4
5
 
5
6
  export interface DropdownProps {
6
7
  options: string[];
@@ -15,11 +16,11 @@ const Dropdown: React.FC<DropdownProps> = ({
15
16
  onChange,
16
17
  label,
17
18
  icon,
18
- // variant = "primary",
19
19
  }) => {
20
20
  const [isOpen, setIsOpen] = useState<boolean>(false);
21
21
  const dropdownRef = useRef<HTMLDivElement>(null);
22
22
  const [selectedOption, setSelectedOption] = useState<number>(0);
23
+ const { windowSize } = useValOverlayContext();
23
24
 
24
25
  const handleToggle = () => {
25
26
  setIsOpen(!isOpen);
@@ -62,31 +63,37 @@ const Dropdown: React.FC<DropdownProps> = ({
62
63
  />
63
64
  }
64
65
  >
65
- <span className="flex flex-row items-center justify-center gap-1">
66
- {label}
67
- {icon && icon}
68
- </span>
69
- </Button>
70
- {isOpen && (
71
- <div className="absolute left-0 mt-2 w-48 shadow-lg font-mono text-[14px] text-primary bg-border z-overlay">
72
- <div className="py-1 rounded-md">
73
- {options?.map((option, idx) => (
74
- <button
75
- key={option}
76
- onClick={(ev) => {
77
- ev.preventDefault();
78
- handleSelect(option, idx);
79
- }}
80
- className={`w-full text-left px-4 py-2 hover:bg-base hover:text-highlight ${
81
- idx === selectedOption && "font-bold bg-base hover:bg-base"
82
- }`}
83
- >
84
- {option}
85
- </button>
86
- ))}
87
- </div>
66
+ <div className="relative">
67
+ <span className="flex flex-row items-center justify-center gap-1">
68
+ {label}
69
+ {icon && icon}
70
+ </span>
71
+ {isOpen && (
72
+ <div
73
+ className="absolute -top-[4px] overflow-scroll shadow-lg -left-2 text-primary bg-border w-fit z-overlay"
74
+ style={{ maxHeight: windowSize?.innerHeight }}
75
+ >
76
+ <div className="flex flex-col ">
77
+ {options?.map((option, idx) => (
78
+ <button
79
+ key={option}
80
+ onClick={(ev) => {
81
+ ev.preventDefault();
82
+ handleSelect(option, idx);
83
+ }}
84
+ className={`text-left px-2 py-1 hover:bg-base hover:text-highlight ${
85
+ idx === selectedOption &&
86
+ "font-bold bg-base hover:bg-base truncate"
87
+ }`}
88
+ >
89
+ {option}
90
+ </button>
91
+ ))}
92
+ </div>
93
+ </div>
94
+ )}
88
95
  </div>
89
- )}
96
+ </Button>
90
97
  </div>
91
98
  );
92
99
  };
@@ -8,8 +8,6 @@ import {
8
8
 
9
9
  export type ImagePayload = {
10
10
  src: string;
11
- sha256?: string;
12
- fileExt?: string;
13
11
  altText?: string;
14
12
  height?: number;
15
13
  width?: number;
@@ -32,11 +30,9 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
32
30
  static clone(node: ImageNode): ImageNode {
33
31
  return new ImageNode({
34
32
  src: node.__src,
35
- sha256: node.__sha256,
36
33
  altText: node.__altText,
37
34
  width: node.__width,
38
35
  height: node.__height,
39
- fileExt: node.__fileExt,
40
36
  });
41
37
  }
42
38
 
@@ -46,8 +42,6 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
46
42
  this.__altText = payload.altText;
47
43
  this.__width = payload.width;
48
44
  this.__height = payload.height;
49
- this.__imageFileExt = payload.fileExt;
50
- this.__sha256 = payload.sha256;
51
45
  }
52
46
 
53
47
  exportJSON(): SerializedImageNode {
@@ -57,9 +51,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
57
51
  src: this.__src,
58
52
  type: "image",
59
53
  version: 1,
60
- fileExt: this.__imageFileExt,
61
54
  width: this.__width,
62
- sha256: this.__sha256,
63
55
  };
64
56
  }
65
57
 
@@ -0,0 +1,58 @@
1
+ import { $isLinkNode } from "@lexical/link";
2
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
3
+ import { $isAtNodeEnd } from "@lexical/selection";
4
+ import { $findMatchingParent, mergeRegister } from "@lexical/utils";
5
+ import {
6
+ $getSelection,
7
+ $isRangeSelection,
8
+ CLICK_COMMAND,
9
+ COMMAND_PRIORITY_LOW,
10
+ ElementNode,
11
+ RangeSelection,
12
+ TextNode,
13
+ } from "lexical";
14
+ import { useEffect } from "react";
15
+
16
+ export function getSelectedNode(
17
+ selection: RangeSelection
18
+ ): TextNode | ElementNode {
19
+ const anchor = selection.anchor;
20
+ const focus = selection.focus;
21
+ const anchorNode = selection.anchor.getNode();
22
+ const focusNode = selection.focus.getNode();
23
+ if (anchorNode === focusNode) {
24
+ return anchorNode;
25
+ }
26
+ const isBackward = selection.isBackward();
27
+ if (isBackward) {
28
+ return $isAtNodeEnd(focus) ? anchorNode : focusNode;
29
+ } else {
30
+ return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
31
+ }
32
+ }
33
+
34
+ export default function LinkEditorPlugin() {
35
+ const [editor] = useLexicalComposerContext();
36
+
37
+ useEffect(() => {
38
+ return mergeRegister(
39
+ editor.registerCommand(
40
+ CLICK_COMMAND,
41
+ (payload) => {
42
+ const selection = $getSelection();
43
+ if ($isRangeSelection(selection)) {
44
+ const node = getSelectedNode(selection);
45
+ const linkNode = $findMatchingParent(node, $isLinkNode);
46
+ if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) {
47
+ window.open(linkNode.getURL(), "_blank");
48
+ return true;
49
+ }
50
+ }
51
+ return false;
52
+ },
53
+ COMMAND_PRIORITY_LOW
54
+ )
55
+ );
56
+ }, []);
57
+ return null;
58
+ }
@@ -53,6 +53,9 @@ import Dropdown from "../../Dropdown";
53
53
  import UploadModal from "../../UploadModal";
54
54
  import { INSERT_IMAGE_COMMAND } from "./ImagePlugin";
55
55
  import { readImage } from "../../../utils/readImage";
56
+ import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
57
+ import { getSelectedNode } from "./LinkEditorPlugin";
58
+ import { Check, Link, X } from "react-feather";
56
59
 
57
60
  export interface ToolbarSettingsProps {
58
61
  fontsFamilies?: string[];
@@ -91,7 +94,12 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
91
94
  const [inputUrl, setInputUrl] = useState<boolean>(false);
92
95
  const [uploadMode, setUploadMode] = useState<"url" | "file">("url");
93
96
  const [file, setFile] = useState<File | null>(null);
94
- const [url, setUrl] = useState<string>("");
97
+
98
+ const [url, setUrl] = useState<string | null>(null);
99
+
100
+ const dispatchLinkChange = (url: string | null) => {
101
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
102
+ };
95
103
 
96
104
  const blockTypes: { [key: string]: string } = {
97
105
  paragraph: "Normal",
@@ -126,6 +134,20 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
126
134
  element = anchorNode.getTopLevelElementOrThrow();
127
135
  }
128
136
 
137
+ // LINK STUFF
138
+ const node = getSelectedNode(selection);
139
+ const linkParent = $findMatchingParent(
140
+ node,
141
+ $isLinkNode
142
+ ) as LinkNode | null;
143
+ if (linkParent !== null) {
144
+ const href = linkParent.getURL();
145
+ setUrl(href);
146
+ } else {
147
+ setUrl(null);
148
+ }
149
+ // ====================
150
+
129
151
  const elementKey = element.getKey();
130
152
  const elementDOM = activeEditor.getElementByKey(elementKey);
131
153
  setIsBold(selection.hasFormat("bold"));
@@ -278,7 +300,7 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
278
300
  };
279
301
 
280
302
  return (
281
- <div className="sticky top-0 border-b bg-base border-highlight">
303
+ <div className="sticky top-0 border-b bg-base border-highlight flex flex-col">
282
304
  <div className="flex flex-row gap-1">
283
305
  <Dropdown
284
306
  options={Object.values(blockTypes)}
@@ -316,6 +338,21 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
316
338
  }}
317
339
  icon={<Italic className={`${isItalic && "stroke-[3px]"}`} />}
318
340
  />
341
+ <Button
342
+ active={url !== null}
343
+ onClick={(ev) => {
344
+ ev.preventDefault();
345
+ setUrl("");
346
+ dispatchLinkChange("");
347
+ }}
348
+ icon={
349
+ <Link
350
+ width={12}
351
+ height={12}
352
+ className={`${url !== null && "stroke-[3px]"}`}
353
+ />
354
+ }
355
+ />
319
356
  <label className="flex items-center justify-center">
320
357
  <ImageIcon />
321
358
  <input
@@ -337,6 +374,37 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
337
374
  />
338
375
  </label>
339
376
  </div>
377
+ {url !== null && (
378
+ <div className="flex flex-row p-2">
379
+ <input
380
+ type="text"
381
+ placeholder="Enter URL"
382
+ className="w-1/3 text-primary bg-base px-2"
383
+ value={url}
384
+ onChange={(ev) => {
385
+ ev.preventDefault();
386
+ setUrl(ev.target.value);
387
+ }}
388
+ ></input>
389
+ <Button
390
+ variant="primary"
391
+ onClick={(ev) => {
392
+ ev.preventDefault();
393
+ // empty url will remove link
394
+ dispatchLinkChange(url || null);
395
+ }}
396
+ icon={<Check size={14} />}
397
+ />
398
+ <Button
399
+ variant="primary"
400
+ onClick={(ev) => {
401
+ ev.preventDefault();
402
+ dispatchLinkChange(null);
403
+ }}
404
+ icon={<X size={14} />}
405
+ />
406
+ </div>
407
+ )}
340
408
  </div>
341
409
  );
342
410
  };
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
4
  import { LexicalEditor } from "lexical";
3
5
  import { ListItemNode, ListNode } from "@lexical/list";
@@ -6,18 +8,23 @@ import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
6
8
  import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
7
9
  import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
8
10
  import { ListPlugin } from "@lexical/react/LexicalListPlugin";
11
+ import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
9
12
  import { FC } from "react";
10
13
  import LexicalContentEditable from "./ContentEditable";
11
14
  import { ImageNode } from "./Nodes/ImageNode";
12
15
  import { AutoFocus } from "./Plugins/AutoFocus";
13
16
  import ImagesPlugin from "./Plugins/ImagePlugin";
14
17
  import Toolbar from "./Plugins/Toolbar";
15
- import { AnyRichTextOptions, RichText } from "@valbuild/core";
18
+ import { AnyRichTextOptions, RichTextSource } from "@valbuild/core";
16
19
  import { HeadingNode } from "@lexical/rich-text";
17
- import { toLexical } from "./conversion";
20
+ import { richTextSourceToLexical } from "../../richtext/conversion/richTextSourceToLexical";
21
+ import { useValOverlayContext } from "../ValOverlayContext";
22
+ import { parseRichTextSource } from "../../exports";
23
+ import { LinkNode } from "@lexical/link";
24
+ import LinkEditorPlugin from "./Plugins/LinkEditorPlugin";
18
25
 
19
26
  export interface RichTextEditorProps {
20
- richtext: RichText<AnyRichTextOptions>;
27
+ richtext: RichTextSource<AnyRichTextOptions>;
21
28
  onEditor?: (editor: LexicalEditor) => void; // Not the ideal way of passing the editor to the upper context, we need it to be able to save
22
29
  }
23
30
 
@@ -25,21 +32,25 @@ function onError(error: any) {
25
32
  console.error(error);
26
33
  }
27
34
 
35
+ const TOOLBAR_HEIGHT = 28;
36
+
28
37
  export const RichTextEditor: FC<RichTextEditorProps> = ({
29
38
  richtext,
30
39
  onEditor,
31
40
  }) => {
41
+ const { windowSize } = useValOverlayContext();
32
42
  const prePopulatedState = (editor: LexicalEditor) => {
33
43
  editor.setEditorState(
34
- editor.parseEditorState({ root: toLexical(richtext) })
44
+ editor.parseEditorState({
45
+ root: richTextSourceToLexical(parseRichTextSource(richtext)),
46
+ })
35
47
  );
36
48
  };
37
49
  const initialConfig = {
38
50
  namespace: "val",
39
51
  editorState: prePopulatedState,
40
- nodes: [HeadingNode, ImageNode, ListNode, ListItemNode],
52
+ nodes: [HeadingNode, ImageNode, ListNode, ListItemNode, LinkNode],
41
53
  theme: {
42
- root: "p-4 bg-fill text-white font-roboto border-b border-highlight",
43
54
  text: {
44
55
  bold: "font-semibold",
45
56
  underline: "underline",
@@ -58,8 +69,9 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
58
69
  h3: "text-2xl font-bold",
59
70
  h4: "text-xl font-bold",
60
71
  h5: "text-lg font-bold",
61
- h6: "text-base font-bold",
72
+ h6: "text-md font-bold",
62
73
  },
74
+ link: "text-highlight underline",
63
75
  },
64
76
  onError,
65
77
  };
@@ -68,10 +80,23 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
68
80
  <AutoFocus />
69
81
  <Toolbar onEditor={onEditor} />
70
82
  <RichTextPlugin
71
- contentEditable={<LexicalContentEditable className="outline-none" />}
83
+ contentEditable={
84
+ <div
85
+ className="text-white border-b border-highlight font-roboto"
86
+ style={{
87
+ minHeight: windowSize?.innerHeight
88
+ ? windowSize?.innerHeight - TOOLBAR_HEIGHT
89
+ : undefined,
90
+ }}
91
+ >
92
+ <LexicalContentEditable className="p-4 outline-none bg-fill" />
93
+ </div>
94
+ }
72
95
  placeholder={<div className="">Enter some text...</div>}
73
96
  ErrorBoundary={LexicalErrorBoundary}
74
97
  />
98
+ <LinkPlugin />
99
+ <LinkEditorPlugin />
75
100
  <ListPlugin />
76
101
  <ImagesPlugin />
77
102
  <HistoryPlugin />
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  Dispatch,
3
5
  SetStateAction,
@@ -8,7 +10,12 @@ import {
8
10
  } from "react";
9
11
  import { Session } from "../dto/Session";
10
12
  import { ValMenu } from "./ValMenu";
11
- import { EditMode, Theme, ValOverlayContext } from "./ValOverlayContext";
13
+ import {
14
+ EditMode,
15
+ Theme,
16
+ ValOverlayContext,
17
+ WindowSize,
18
+ } from "./ValOverlayContext";
12
19
  import { Remote } from "../utils/Remote";
13
20
  import { ValWindow } from "./ValWindow";
14
21
  import { result } from "@valbuild/core/fp";
@@ -16,7 +23,7 @@ import {
16
23
  AnyRichTextOptions,
17
24
  FileSource,
18
25
  Internal,
19
- RichText,
26
+ RichTextSource,
20
27
  SerializedSchema,
21
28
  SourcePath,
22
29
  VAL_EXTENSION,
@@ -25,9 +32,10 @@ import { Modules, resolvePath } from "../utils/resolvePath";
25
32
  import { ValApi } from "@valbuild/core";
26
33
  import { RichTextEditor } from "../exports";
27
34
  import { LexicalEditor } from "lexical";
28
- import { LexicalRootNode, fromLexical } from "./RichTextEditor/conversion";
35
+ import { LexicalRootNode } from "../richtext/conversion/richTextSourceToLexical";
29
36
  import { PatchJSON } from "@valbuild/core/patch";
30
37
  import { readImage } from "../utils/readImage";
38
+ import { lexicalToRichTextSource } from "../richtext/conversion/lexicalToRichTextSource";
31
39
 
32
40
  export type ValOverlayProps = {
33
41
  defaultTheme?: "dark" | "light";
@@ -54,7 +62,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
54
62
  );
55
63
 
56
64
  const [state, setState] = useState<{
57
- [path: SourcePath]: () => PatchJSON;
65
+ [path: SourcePath]: () => Promise<PatchJSON>;
58
66
  }>({});
59
67
  const initPatchCallback = useCallback((currentPath: SourcePath | null) => {
60
68
  return (callback: PatchCallback) => {
@@ -83,6 +91,8 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
83
91
  });
84
92
  }, [windowTarget?.path]);
85
93
 
94
+ const [windowSize, setWindowSize] = useState<WindowSize>();
95
+
86
96
  return (
87
97
  <ValOverlayContext.Provider
88
98
  value={{
@@ -94,6 +104,8 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
94
104
  highlight,
95
105
  setHighlight,
96
106
  setTheme,
107
+ windowSize,
108
+ setWindowSize,
97
109
  }}
98
110
  >
99
111
  <div data-mode={theme}>
@@ -137,7 +149,9 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
137
149
  selectedSource[VAL_EXTENSION] === "richtext" && (
138
150
  <RichTextField
139
151
  registerPatchCallback={initPatchCallback(windowTarget.path)}
140
- defaultValue={selectedSource as RichText<AnyRichTextOptions>}
152
+ defaultValue={
153
+ selectedSource as RichTextSource<AnyRichTextOptions>
154
+ }
141
155
  />
142
156
  )}
143
157
  {selectedSource &&
@@ -157,16 +171,22 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
157
171
  const [moduleId] = Internal.splitModuleIdAndModulePath(
158
172
  windowTarget.path
159
173
  );
160
- const patch = state[windowTarget.path]();
161
- console.log("Submitting", patch);
162
- api
163
- .postPatches(moduleId, patch)
164
- .then((res) => {
165
- console.log(res);
166
- })
167
- .finally(() => {
168
- console.log("done");
169
- });
174
+ const res = state[windowTarget.path]();
175
+ if (res) {
176
+ res
177
+ .then((patch) => {
178
+ console.log("Submitting", patch);
179
+ return api.postPatches(moduleId, patch);
180
+ })
181
+ .then((res) => {
182
+ console.log(res);
183
+ })
184
+ .finally(() => {
185
+ console.log("done");
186
+ });
187
+ } else {
188
+ console.error("No patch");
189
+ }
170
190
  }
171
191
  }}
172
192
  />
@@ -178,7 +198,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
178
198
  );
179
199
  }
180
200
 
181
- type PatchCallback = (modulePath: string) => PatchJSON;
201
+ type PatchCallback = (modulePath: string) => Promise<PatchJSON>;
182
202
 
183
203
  function ImageField({
184
204
  defaultValue,
@@ -195,7 +215,7 @@ function ImageField({
195
215
  } | null>(null);
196
216
  const url = defaultValue && Internal.convertFileSource(defaultValue).url;
197
217
  useEffect(() => {
198
- registerPatchCallback((path) => {
218
+ registerPatchCallback(async (path) => {
199
219
  const pathParts = path.split("/");
200
220
  if (!data) {
201
221
  return [];
@@ -256,27 +276,31 @@ function RichTextField({
256
276
  registerPatchCallback,
257
277
  }: {
258
278
  registerPatchCallback: (callback: PatchCallback) => void;
259
- defaultValue?: RichText<AnyRichTextOptions>;
279
+ defaultValue?: RichTextSource<AnyRichTextOptions>;
260
280
  }) {
261
281
  const [editor, setEditor] = useState<LexicalEditor | null>(null);
262
282
  useEffect(() => {
263
283
  if (editor) {
264
- registerPatchCallback((path) => {
265
- const { node, files } = editor?.toJSON()?.editorState
266
- ? fromLexical(editor?.toJSON()?.editorState.root as LexicalRootNode)
267
- : {
268
- node: {
269
- [VAL_EXTENSION]: "richtext",
270
- children: [],
271
- } as RichText<AnyRichTextOptions>,
284
+ registerPatchCallback(async (path) => {
285
+ const { templateStrings, exprs, files } = editor?.toJSON()?.editorState
286
+ ? await lexicalToRichTextSource(
287
+ editor?.toJSON()?.editorState.root as LexicalRootNode
288
+ )
289
+ : ({
290
+ [VAL_EXTENSION]: "richtext",
291
+ templateStrings: [""],
292
+ exprs: [],
272
293
  files: {},
273
- };
294
+ } as RichTextSource<AnyRichTextOptions> & {
295
+ files: Record<string, string>;
296
+ });
274
297
  return [
275
298
  {
276
- op: "replace",
299
+ op: "replace" as const,
277
300
  path,
278
301
  value: {
279
- ...node,
302
+ templateStrings,
303
+ exprs,
280
304
  [VAL_EXTENSION]: "richtext",
281
305
  },
282
306
  },
@@ -301,7 +325,7 @@ function RichTextField({
301
325
  ({
302
326
  children: [],
303
327
  [VAL_EXTENSION]: "root",
304
- } as unknown as RichText<AnyRichTextOptions>)
328
+ } as unknown as RichTextSource<AnyRichTextOptions>)
305
329
  }
306
330
  />
307
331
  );
@@ -322,7 +346,7 @@ function TextField({
322
346
  // to avoid registering a new callback every time the value changes
323
347
  const ref = useRef<HTMLTextAreaElement>(null);
324
348
  useEffect(() => {
325
- registerPatchCallback((path) => {
349
+ registerPatchCallback(async (path) => {
326
350
  return [
327
351
  {
328
352
  op: "replace",
@@ -5,6 +5,11 @@ import { ValApi } from "@valbuild/core";
5
5
 
6
6
  export type Theme = "dark" | "light";
7
7
  export type EditMode = "off" | "hover" | "window" | "full";
8
+ export type WindowSize = {
9
+ width: number;
10
+ height: number;
11
+ innerHeight: number;
12
+ };
8
13
 
9
14
  export const ValOverlayContext = React.createContext<{
10
15
  api: ValApi;
@@ -15,6 +20,8 @@ export const ValOverlayContext = React.createContext<{
15
20
  setEditMode: Dispatch<SetStateAction<EditMode>>;
16
21
  theme: Theme;
17
22
  setTheme: (theme: Theme) => void;
23
+ setWindowSize: (size: WindowSize) => void;
24
+ windowSize?: WindowSize;
18
25
  }>({
19
26
  get api(): never {
20
27
  throw Error(
@@ -56,6 +63,16 @@ export const ValOverlayContext = React.createContext<{
56
63
  "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
57
64
  );
58
65
  },
66
+ get setWindowSize(): never {
67
+ throw Error(
68
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
69
+ );
70
+ },
71
+ get windowSize(): never {
72
+ throw Error(
73
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
74
+ );
75
+ },
59
76
  });
60
77
 
61
78
  export function useValOverlayContext() {