@valbuild/ui 0.12.0 → 0.13.3

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 (50) hide show
  1. package/dist/valbuild-ui.cjs.js +66 -10
  2. package/dist/valbuild-ui.esm.js +66 -10
  3. package/package.json +1 -1
  4. package/src/assets/icons/Bold.tsx +23 -0
  5. package/src/assets/icons/Chevron.tsx +28 -0
  6. package/src/assets/icons/FontColor.tsx +30 -0
  7. package/src/assets/icons/ImageIcon.tsx +21 -0
  8. package/src/assets/icons/Italic.tsx +24 -0
  9. package/src/assets/icons/Strikethrough.tsx +22 -0
  10. package/src/assets/icons/Underline.tsx +22 -0
  11. package/src/assets/icons/Undo.tsx +20 -0
  12. package/src/components/Button.tsx +58 -0
  13. package/src/components/Checkbox.tsx +51 -0
  14. package/src/components/Dropdown.tsx +92 -0
  15. package/src/components/EditButton.tsx +10 -0
  16. package/src/components/ErrorText.tsx +3 -0
  17. package/src/components/RichTextEditor/ContentEditable.tsx +9 -0
  18. package/src/components/RichTextEditor/Nodes/ImageNode.tsx +117 -0
  19. package/src/components/RichTextEditor/Plugins/AutoFocus.tsx +12 -0
  20. package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +46 -0
  21. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +381 -0
  22. package/src/components/RichTextEditor/RichTextEditor.tsx +176 -0
  23. package/src/components/UploadModal.tsx +109 -0
  24. package/src/components/ValOverlay.tsx +41 -0
  25. package/src/components/ValWindow.stories.tsx +182 -0
  26. package/src/components/ValWindow.tsx +192 -0
  27. package/src/components/forms/Form.tsx +122 -0
  28. package/src/components/forms/FormContainer.tsx +24 -0
  29. package/src/components/forms/ImageForm.tsx +195 -0
  30. package/src/components/forms/TextForm.tsx +22 -0
  31. package/src/exports.ts +3 -0
  32. package/src/index.css +79 -0
  33. package/src/index.tsx +14 -0
  34. package/src/server.ts +41 -0
  35. package/src/stories/Button.stories.tsx +20 -0
  36. package/src/stories/Checkbox.stories.tsx +14 -0
  37. package/src/stories/Dropdown.stories.tsx +23 -0
  38. package/src/stories/Introduction.mdx +221 -0
  39. package/src/stories/RichTextEditor.stories.tsx +314 -0
  40. package/src/stories/assets/code-brackets.svg +1 -0
  41. package/src/stories/assets/colors.svg +1 -0
  42. package/src/stories/assets/comments.svg +1 -0
  43. package/src/stories/assets/direction.svg +1 -0
  44. package/src/stories/assets/flow.svg +1 -0
  45. package/src/stories/assets/plugin.svg +1 -0
  46. package/src/stories/assets/repo.svg +1 -0
  47. package/src/stories/assets/stackalt.svg +1 -0
  48. package/src/vite-env.d.ts +1 -0
  49. package/src/vite-index.tsx +7 -0
  50. package/src/vite-server.ts +8 -0
@@ -0,0 +1,182 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { RichTextEditor } from "../exports";
3
+ import { FormContainer } from "./forms/FormContainer";
4
+ import { ImageForm } from "./forms/ImageForm";
5
+ import { TextForm } from "./forms/TextForm";
6
+
7
+ import { ValWindow } from "./ValWindow";
8
+
9
+ const meta: Meta<typeof ValWindow> = { component: ValWindow };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof ValWindow>;
13
+
14
+ const EXAMPLE_IMAGE =
15
+ "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAFgAWAAD/4QDkRXhpZgAASUkqAAgAAAAJABIBAwABAAAAAQAAABoBBQABAAAAegAAABsBBQABAAAAggAAACgBAwABAAAAAgAAADEBAgANAAAAigAAADIBAgAUAAAAmAAAAGmHBAABAAAAygAAAHySAgAHAAAArAAAAIaSAgAWAAAAtAAAAAAAAAAWAAAAAQAAABYAAAABAAAAR0lNUCAyLjEwLjM0AAAyMDIzOjA1OjI0IDE0OjQ3OjA1AE9wZW5BSQAATWFkZSB3aXRoIE9wZW5BSSBMYWJzAAEAAaADAAEAAAABAAAAAAAAAP/hDM9odHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDQuNC4wLUV4aXYyIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo1NDk5MWU4Yy01YjkxLTQwYWYtYjk4ZC00ZjEwMWYwY2QxZWQiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Y2JmMjFlNjMtNGZiYi00MjI5LThlM2UtN2MzMWI0ZDNiNWFhIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6YzQ3ZWZmNDktNzg4Yy00NzdjLWJkNTEtZGM5YzJjYTY4NzBjIiBkYzpGb3JtYXQ9ImltYWdlL2pwZWciIEdJTVA6QVBJPSIyLjAiIEdJTVA6UGxhdGZvcm09IkxpbnV4IiBHSU1QOlRpbWVTdGFtcD0iMTY4NDkzMjQyNzk4NDA2MCIgR0lNUDpWZXJzaW9uPSIyLjEwLjM0IiB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMzowNToyNFQxNDo0NzowNSswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjM6MDU6MjRUMTQ6NDc6MDUrMDI6MDAiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoxNjIyMWEyMS0wZmMzLTQ1NmEtOWZlOC1hOTAyMmY0NjBhOTEiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTGludXgpIiBzdEV2dDp3aGVuPSIyMDIzLTA1LTI0VDE0OjQ3OjA3KzAyOjAwIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+/+ICsElDQ19QUk9GSUxFAAEBAAACoGxjbXMEQAAAbW50clJHQiBYWVogB+cABQAYAAwALQAJYWNzcEFQUEwAAAAAAAAA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";
16
+ const EXAMPLE_TEXT = `
17
+ Vi gjør mange ting sammen i Blank, men det vi lever av er å designe og utvikle digitale tjenester for kundene våre.
18
+
19
+ Noen av selskapene vi jobber med er små, andre er store. Alle har de høye ambisjoner for sine digitale løsninger, og stiller høye krav til hvem de jobber med.
20
+
21
+ Noen ganger starter vi nye, egne, selskaper også, mest fordi det er gøy (og fordi vi liker å bygge ting), men også fordi smarte folk har gode idéer som fortjener å bli realisert.
22
+ Ting vi har bygd for kundene våre
23
+ `;
24
+ export const ShortText: Story = {
25
+ args: {
26
+ isInitialized: true,
27
+ children: (
28
+ <FormContainer
29
+ onSubmit={() => {
30
+ /* */
31
+ }}
32
+ >
33
+ <TextForm
34
+ name="/apps/blogs.0.title"
35
+ text="Hva skjer'a, Bagera?"
36
+ onChange={() => {
37
+ console.log("onChange");
38
+ }}
39
+ />
40
+ </FormContainer>
41
+ ),
42
+ },
43
+ };
44
+ export const LongText: Story = {
45
+ args: {
46
+ isInitialized: true,
47
+ children: (
48
+ <FormContainer
49
+ onSubmit={() => {
50
+ /* */
51
+ }}
52
+ >
53
+ <TextForm
54
+ name="/apps/blogs.0.title"
55
+ text={EXAMPLE_TEXT}
56
+ onChange={() => {
57
+ console.log("onChange");
58
+ }}
59
+ />
60
+ </FormContainer>
61
+ ),
62
+ },
63
+ };
64
+
65
+ export const RichText: Story = {
66
+ args: {
67
+ isInitialized: true,
68
+ children: (
69
+ <FormContainer
70
+ onSubmit={() => {
71
+ /* */
72
+ }}
73
+ >
74
+ <RichTextEditor
75
+ richtext={{
76
+ children: [
77
+ {
78
+ children: [
79
+ {
80
+ detail: 0,
81
+ format: 0,
82
+ mode: "normal",
83
+ style: "",
84
+ text: "Heading 1",
85
+ type: "text",
86
+ version: 1,
87
+ },
88
+ ],
89
+ direction: "ltr",
90
+ format: "",
91
+ indent: 0,
92
+ type: "heading",
93
+ version: 1,
94
+ tag: "h1",
95
+ },
96
+ ],
97
+ direction: "ltr",
98
+ format: "",
99
+ indent: 0,
100
+ type: "root",
101
+ version: 1,
102
+ }}
103
+ />
104
+ </FormContainer>
105
+ ),
106
+ },
107
+ };
108
+
109
+ export const EmptyImage: Story = {
110
+ args: {
111
+ isInitialized: true,
112
+ children: (
113
+ <FormContainer
114
+ onSubmit={() => {
115
+ /* */
116
+ }}
117
+ >
118
+ <ImageForm
119
+ name="/apps/blogs.0.image"
120
+ error={null}
121
+ data={null}
122
+ onChange={() => {
123
+ console.log("onChange");
124
+ }}
125
+ />
126
+ </FormContainer>
127
+ ),
128
+ },
129
+ };
130
+
131
+ export const Image: Story = {
132
+ args: {
133
+ isInitialized: true,
134
+ children: (
135
+ <FormContainer
136
+ onSubmit={() => {
137
+ /* */
138
+ }}
139
+ >
140
+ <ImageForm
141
+ name="/apps/blogs.0.image"
142
+ error={null}
143
+ data={{
144
+ url: EXAMPLE_IMAGE,
145
+ metadata: {
146
+ width: 32,
147
+ height: 32,
148
+ sha256: "123",
149
+ },
150
+ }}
151
+ onChange={() => {
152
+ console.log("onChange");
153
+ }}
154
+ />
155
+ </FormContainer>
156
+ ),
157
+ },
158
+ };
159
+
160
+ export const ImageError: Story = {
161
+ args: {
162
+ isInitialized: true,
163
+ children: (
164
+ <FormContainer
165
+ onSubmit={() => {
166
+ /* */
167
+ }}
168
+ >
169
+ <ImageForm
170
+ name="/apps/blogs.0.image"
171
+ error={"invalid-file"}
172
+ data={{
173
+ url: EXAMPLE_IMAGE,
174
+ }}
175
+ onChange={() => {
176
+ console.log("onChange");
177
+ }}
178
+ />
179
+ </FormContainer>
180
+ ),
181
+ },
182
+ };
@@ -0,0 +1,192 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { AlignJustify, X } from "react-feather";
3
+ import classNames from "classnames";
4
+
5
+ export type ValWindowProps = {
6
+ children: React.ReactNode;
7
+ onClose: () => void;
8
+ position?: { left: number; top: number };
9
+ isInitialized?: true;
10
+ };
11
+
12
+ export function ValWindow({
13
+ position,
14
+ isInitialized: isInitializedProp,
15
+ onClose,
16
+ children,
17
+ }: ValWindowProps): React.ReactElement {
18
+ const [draggedPosition, isInitialized, dragRef, onMouseDownDrag] = useDrag({
19
+ position,
20
+ });
21
+ useEffect(() => {
22
+ const closeOnEscape = (e: KeyboardEvent) => {
23
+ if (e.key === "Escape") {
24
+ onClose();
25
+ }
26
+ };
27
+ document.addEventListener("keyup", closeOnEscape);
28
+ return () => {
29
+ document.removeEventListener("keyup", closeOnEscape);
30
+ };
31
+ }, []);
32
+
33
+ //
34
+ const [size, resizeRef, onMouseDownResize] = useResize();
35
+ return (
36
+ <div
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",
39
+ {
40
+ "opacity-0": !(isInitialized || isInitializedProp),
41
+ "opacity-100": isInitialized || isInitializedProp,
42
+ }
43
+ )}
44
+ ref={resizeRef}
45
+ style={{
46
+ left: draggedPosition.left,
47
+ top: draggedPosition.top,
48
+ width: size?.width,
49
+ height: size?.height,
50
+ }}
51
+ >
52
+ <div
53
+ ref={dragRef}
54
+ className="relative flex justify-center px-2 pt-2 text-primary pb-[16px]"
55
+ >
56
+ <AlignJustify
57
+ size={16}
58
+ className="hidden w-full cursor-grab tablet:block"
59
+ onMouseDown={(e) => {
60
+ e.preventDefault();
61
+ e.stopPropagation();
62
+ onMouseDownDrag();
63
+ }}
64
+ />
65
+ <button
66
+ className="absolute top-0 right-0 px-4 py-2 focus:outline-none focus-visible:outline-highlight"
67
+ onClick={onClose}
68
+ >
69
+ <X size={16} />
70
+ </button>
71
+ </div>
72
+ {children}
73
+ <div
74
+ className="absolute bottom-0 right-0 hidden ml-auto select-none tablet:block text-border cursor-nwse-resize"
75
+ style={{
76
+ height: 16,
77
+ width: 16,
78
+ }}
79
+ onMouseDown={onMouseDownResize}
80
+ >
81
+ <svg
82
+ height="18"
83
+ viewBox="0 0 18 18"
84
+ width="18"
85
+ xmlns="http://www.w3.org/2000/svg"
86
+ >
87
+ <path
88
+ d="m14.228 16.227a1 1 0 0 1 -.707-1.707l1-1a1 1 0 0 1 1.416 1.414l-1 1a1 1 0 0 1 -.707.293zm-5.638 0a1 1 0 0 1 -.707-1.707l6.638-6.638a1 1 0 0 1 1.416 1.414l-6.638 6.638a1 1 0 0 1 -.707.293zm-5.84 0a1 1 0 0 1 -.707-1.707l12.477-12.477a1 1 0 1 1 1.415 1.414l-12.478 12.477a1 1 0 0 1 -.707.293z"
89
+ fill="currentColor"
90
+ />
91
+ </svg>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ function useResize() {
98
+ const ref = useRef<HTMLDivElement>(null);
99
+ const [size, setSize] = useState<{ height: number; width: number }>();
100
+
101
+ const handler = (mouseDownEvent: React.MouseEvent) => {
102
+ const startSize = ref.current?.getBoundingClientRect();
103
+
104
+ const startPosition = { x: mouseDownEvent.pageX, y: mouseDownEvent.pageY };
105
+ function onMouseMove(mouseMoveEvent: MouseEvent) {
106
+ if (startSize) {
107
+ const nextWidth =
108
+ startSize.width - startPosition.x + mouseMoveEvent.pageX;
109
+ const nextHeight =
110
+ startSize.height - startPosition.y + mouseMoveEvent.pageY;
111
+ setSize({
112
+ width: nextWidth,
113
+ height: nextHeight,
114
+ });
115
+ }
116
+ }
117
+ function onMouseUp() {
118
+ document.body.removeEventListener("mousemove", onMouseMove);
119
+ }
120
+ document.body.addEventListener("mousemove", onMouseMove);
121
+ document.body.addEventListener("mouseup", onMouseUp, { once: true });
122
+ };
123
+ return [size, ref, handler] as const;
124
+ }
125
+
126
+ function useDrag({
127
+ position: initPosition,
128
+ }: {
129
+ position?: { left: number; top: number };
130
+ }) {
131
+ const [position, setPosition] = useState({ left: 0, top: 0 });
132
+ useEffect(() => {
133
+ if (initPosition) {
134
+ setPosition({
135
+ left:
136
+ initPosition.left -
137
+ (ref?.current?.getBoundingClientRect()?.width || 0) / 2,
138
+ top: initPosition.top - 16,
139
+ });
140
+ }
141
+ }, [initPosition]);
142
+
143
+ const [mouseDown, setMouseDown] = useState(false);
144
+ const ref = useRef<HTMLDivElement>(null);
145
+ useEffect(() => {
146
+ const onMouseUp = () => {
147
+ setMouseDown(false);
148
+ };
149
+ const onMouseMove = (e: MouseEvent) => {
150
+ if (mouseDown) {
151
+ e.preventDefault();
152
+ e.stopPropagation();
153
+ const top =
154
+ -((ref?.current?.getBoundingClientRect()?.height || 0) / 2) +
155
+ +e.pageY;
156
+
157
+ setPosition({
158
+ left:
159
+ -((ref?.current?.getBoundingClientRect()?.width || 0) / 2) +
160
+ e.pageX,
161
+ top: top < 0 ? 0 : top,
162
+ });
163
+ }
164
+ };
165
+
166
+ document.addEventListener("mouseup", onMouseUp);
167
+ document.addEventListener("mousemove", onMouseMove);
168
+ return () => {
169
+ document.removeEventListener("mouseup", onMouseUp);
170
+ document.removeEventListener("mousemove", onMouseMove);
171
+ };
172
+ }, [mouseDown]);
173
+
174
+ // TODO: rename hook from useDrag to usePosition or something since we also check for screen width here?
175
+ useEffect(() => {
176
+ const onResize = () => {
177
+ if (window.screen.width < 640) {
178
+ setPosition({
179
+ left: 0,
180
+ top: 0,
181
+ });
182
+ }
183
+ };
184
+ window.addEventListener("resize", onResize);
185
+ return () => {
186
+ window.removeEventListener("resize", onResize);
187
+ };
188
+ }, []);
189
+ const handleMouseDown = () => setMouseDown(true);
190
+ const isInitialized = !!ref?.current?.getBoundingClientRect()?.width;
191
+ return [position, isInitialized, ref, handleMouseDown] as const;
192
+ }
@@ -0,0 +1,122 @@
1
+ import { RichText } from "@valbuild/core";
2
+ import { LexicalEditor } from "lexical";
3
+ import { useEffect, useState } from "react";
4
+ import { RichTextEditor } from "../RichTextEditor/RichTextEditor";
5
+ import { FormContainer } from "./FormContainer";
6
+ import { ImageForm, ImageData } from "./ImageForm";
7
+ import { TextData, TextForm } from "./TextForm";
8
+
9
+ export type Inputs = {
10
+ [path: string]:
11
+ | { status: "requested" }
12
+ | {
13
+ status: "completed";
14
+ type: "text";
15
+ data: TextData;
16
+ }
17
+ | { status: "completed"; type: "image"; data: ImageData }
18
+ | { status: "completed"; type: "richtext"; data: RichText };
19
+ };
20
+
21
+ export type FormProps = {
22
+ onSubmit: (nextInputs: Inputs) => void;
23
+ inputs: Inputs;
24
+ };
25
+
26
+ export function Form({ onSubmit, inputs }: FormProps): React.ReactElement {
27
+ const [currentInputs, setCurrentInputs] = useState<Inputs>();
28
+ const [richTextEditor, setRichTextEditor] = useState<{
29
+ [path: string]: LexicalEditor;
30
+ }>();
31
+
32
+ useEffect(() => {
33
+ setCurrentInputs(inputs);
34
+ }, [inputs]);
35
+
36
+ return (
37
+ <FormContainer
38
+ onSubmit={() => {
39
+ if (currentInputs) {
40
+ onSubmit(
41
+ Object.fromEntries(
42
+ Object.entries(currentInputs).map(([path, input]) => {
43
+ if (input.status === "completed" && input.type === "richtext") {
44
+ if (!richTextEditor) {
45
+ throw Error(
46
+ "Cannot save rich text - editor not initialized"
47
+ );
48
+ }
49
+ return [
50
+ path,
51
+ {
52
+ status: "completed",
53
+ type: "richtext",
54
+ data: richTextEditor[path].getEditorState().toJSON()
55
+ ?.root,
56
+ },
57
+ ];
58
+ }
59
+ return [path, input];
60
+ })
61
+ )
62
+ );
63
+ }
64
+ }}
65
+ >
66
+ {currentInputs &&
67
+ Object.entries(currentInputs).map(([path, input]) => (
68
+ <div key={path}>
69
+ {input.status === "requested" && (
70
+ <div className="p-2 text-center text-primary">Loading...</div>
71
+ )}
72
+ {input.status === "completed" && input.type === "image" && (
73
+ <ImageForm
74
+ name={path}
75
+ data={input.data}
76
+ onChange={(data) => {
77
+ if (data.value) {
78
+ setCurrentInputs({
79
+ ...currentInputs,
80
+ [path]: {
81
+ status: "completed",
82
+ type: "image",
83
+ data: data.value,
84
+ },
85
+ });
86
+ }
87
+ }}
88
+ error={null}
89
+ />
90
+ )}
91
+ {input.status === "completed" && input.type === "text" && (
92
+ <TextForm
93
+ name={path}
94
+ text={input.data}
95
+ onChange={(data) => {
96
+ setCurrentInputs({
97
+ ...currentInputs,
98
+ [path]: {
99
+ status: "completed",
100
+ type: "text",
101
+ data: data,
102
+ },
103
+ });
104
+ }}
105
+ />
106
+ )}
107
+ {input.status === "completed" && input.type === "richtext" && (
108
+ <RichTextEditor
109
+ richtext={input.data}
110
+ onEditor={(editor) => {
111
+ setRichTextEditor({
112
+ ...richTextEditor,
113
+ [path]: editor,
114
+ });
115
+ }}
116
+ />
117
+ )}
118
+ </div>
119
+ ))}
120
+ </FormContainer>
121
+ );
122
+ }
@@ -0,0 +1,24 @@
1
+ import { PrimaryButton } from "../Button";
2
+
3
+ export function FormContainer({
4
+ children,
5
+ onSubmit,
6
+ }: {
7
+ children: React.ReactNode;
8
+ onSubmit: () => void;
9
+ }) {
10
+ return (
11
+ <form
12
+ className="flex flex-col justify-between w-full px-4 py-2"
13
+ onSubmit={(ev) => {
14
+ ev.preventDefault();
15
+ onSubmit();
16
+ }}
17
+ >
18
+ {children}
19
+ <div className="flex justify-end">
20
+ <PrimaryButton>Save</PrimaryButton>
21
+ </div>
22
+ </form>
23
+ );
24
+ }