@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.
- package/dist/valbuild-ui.cjs.js +66 -10
- package/dist/valbuild-ui.esm.js +66 -10
- package/package.json +1 -1
- package/src/assets/icons/Bold.tsx +23 -0
- package/src/assets/icons/Chevron.tsx +28 -0
- package/src/assets/icons/FontColor.tsx +30 -0
- package/src/assets/icons/ImageIcon.tsx +21 -0
- package/src/assets/icons/Italic.tsx +24 -0
- package/src/assets/icons/Strikethrough.tsx +22 -0
- package/src/assets/icons/Underline.tsx +22 -0
- package/src/assets/icons/Undo.tsx +20 -0
- package/src/components/Button.tsx +58 -0
- package/src/components/Checkbox.tsx +51 -0
- package/src/components/Dropdown.tsx +92 -0
- package/src/components/EditButton.tsx +10 -0
- package/src/components/ErrorText.tsx +3 -0
- package/src/components/RichTextEditor/ContentEditable.tsx +9 -0
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +117 -0
- package/src/components/RichTextEditor/Plugins/AutoFocus.tsx +12 -0
- package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +46 -0
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +381 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +176 -0
- package/src/components/UploadModal.tsx +109 -0
- package/src/components/ValOverlay.tsx +41 -0
- package/src/components/ValWindow.stories.tsx +182 -0
- package/src/components/ValWindow.tsx +192 -0
- package/src/components/forms/Form.tsx +122 -0
- package/src/components/forms/FormContainer.tsx +24 -0
- package/src/components/forms/ImageForm.tsx +195 -0
- package/src/components/forms/TextForm.tsx +22 -0
- package/src/exports.ts +3 -0
- package/src/index.css +79 -0
- package/src/index.tsx +14 -0
- package/src/server.ts +41 -0
- package/src/stories/Button.stories.tsx +20 -0
- package/src/stories/Checkbox.stories.tsx +14 -0
- package/src/stories/Dropdown.stories.tsx +23 -0
- package/src/stories/Introduction.mdx +221 -0
- package/src/stories/RichTextEditor.stories.tsx +314 -0
- package/src/stories/assets/code-brackets.svg +1 -0
- package/src/stories/assets/colors.svg +1 -0
- package/src/stories/assets/comments.svg +1 -0
- package/src/stories/assets/direction.svg +1 -0
- package/src/stories/assets/flow.svg +1 -0
- package/src/stories/assets/plugin.svg +1 -0
- package/src/stories/assets/repo.svg +1 -0
- package/src/stories/assets/stackalt.svg +1 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vite-index.tsx +7 -0
- 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
|
+
}
|