@valbuild/ui 0.18.0 → 0.20.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.
- package/dist/valbuild-ui.cjs.d.ts +3 -3
- package/dist/valbuild-ui.cjs.js +7217 -2477
- package/dist/valbuild-ui.esm.js +7083 -2343
- package/jest.config.js +4 -0
- package/package.json +6 -3
- package/src/assets/icons/Bold.tsx +2 -2
- package/src/assets/icons/Chevron.tsx +2 -2
- package/src/assets/icons/ImageIcon.tsx +2 -2
- package/src/assets/icons/Italic.tsx +2 -2
- package/src/assets/icons/Strikethrough.tsx +2 -2
- package/src/assets/icons/Underline.tsx +2 -2
- package/src/components/Button.tsx +2 -2
- package/src/components/Dropdown.tsx +2 -2
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +50 -59
- package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +1 -2
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +32 -54
- package/src/components/RichTextEditor/RichTextEditor.tsx +22 -118
- package/src/components/RichTextEditor/conversion.test.ts +132 -0
- package/src/components/RichTextEditor/conversion.ts +389 -0
- package/src/components/ValOverlay.tsx +333 -88
- package/src/components/ValWindow.stories.tsx +55 -49
- package/src/components/ValWindow.tsx +80 -78
- package/src/components/forms/Form.tsx +6 -2
- package/src/components/forms/TextArea.tsx +1 -1
- package/src/stories/RichTextEditor.stories.tsx +30 -278
- package/src/utils/readImage.ts +78 -0
- package/tailwind.config.js +4 -4
- package/tsconfig.json +2 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
SetStateAction,
|
|
4
4
|
useCallback,
|
|
5
5
|
useEffect,
|
|
6
|
+
useRef,
|
|
6
7
|
useState,
|
|
7
8
|
} from "react";
|
|
8
9
|
import { Session } from "../dto/Session";
|
|
@@ -11,16 +12,34 @@ import { EditMode, Theme, ValOverlayContext } from "./ValOverlayContext";
|
|
|
11
12
|
import { Remote } from "../utils/Remote";
|
|
12
13
|
import { ValWindow } from "./ValWindow";
|
|
13
14
|
import { result } from "@valbuild/core/fp";
|
|
14
|
-
import {
|
|
15
|
-
|
|
15
|
+
import {
|
|
16
|
+
AnyRichTextOptions,
|
|
17
|
+
FileSource,
|
|
18
|
+
Internal,
|
|
19
|
+
RichText,
|
|
20
|
+
SerializedSchema,
|
|
21
|
+
SourcePath,
|
|
22
|
+
VAL_EXTENSION,
|
|
23
|
+
} from "@valbuild/core";
|
|
16
24
|
import { Modules, resolvePath } from "../utils/resolvePath";
|
|
17
25
|
import { ValApi } from "@valbuild/core";
|
|
26
|
+
import { RichTextEditor } from "../exports";
|
|
27
|
+
import { LexicalEditor } from "lexical";
|
|
28
|
+
import { LexicalRootNode, fromLexical } from "./RichTextEditor/conversion";
|
|
29
|
+
import { PatchJSON } from "@valbuild/core/patch";
|
|
30
|
+
import { readImage } from "../utils/readImage";
|
|
18
31
|
|
|
19
32
|
export type ValOverlayProps = {
|
|
20
33
|
defaultTheme?: "dark" | "light";
|
|
21
34
|
api: ValApi;
|
|
22
35
|
};
|
|
23
36
|
|
|
37
|
+
type ImageSource = FileSource<{
|
|
38
|
+
height: number;
|
|
39
|
+
width: number;
|
|
40
|
+
sha256: string;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
24
43
|
export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
25
44
|
const [theme, setTheme] = useTheme(defaultTheme);
|
|
26
45
|
const session = useSession(api);
|
|
@@ -34,6 +53,36 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
34
53
|
windowTarget?.path
|
|
35
54
|
);
|
|
36
55
|
|
|
56
|
+
const [state, setState] = useState<{
|
|
57
|
+
[path: SourcePath]: () => PatchJSON;
|
|
58
|
+
}>({});
|
|
59
|
+
const initPatchCallback = useCallback((currentPath: SourcePath | null) => {
|
|
60
|
+
return (callback: PatchCallback) => {
|
|
61
|
+
// TODO: revaluate this logic when we have multiple paths
|
|
62
|
+
// NOTE: see cleanup of state in useEffect below
|
|
63
|
+
if (!currentPath) {
|
|
64
|
+
setState({});
|
|
65
|
+
} else {
|
|
66
|
+
const patchPath = Internal.createPatchJSONPath(
|
|
67
|
+
Internal.splitModuleIdAndModulePath(currentPath)[1]
|
|
68
|
+
);
|
|
69
|
+
setState((prev) => {
|
|
70
|
+
return {
|
|
71
|
+
...prev,
|
|
72
|
+
[currentPath]: () => callback(patchPath),
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setState((prev) => {
|
|
80
|
+
return Object.fromEntries(
|
|
81
|
+
Object.entries(prev).filter(([path]) => path === windowTarget?.path)
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}, [windowTarget?.path]);
|
|
85
|
+
|
|
37
86
|
return (
|
|
38
87
|
<ValOverlayContext.Provider
|
|
39
88
|
value={{
|
|
@@ -66,7 +115,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
66
115
|
setEditMode("hover");
|
|
67
116
|
}}
|
|
68
117
|
>
|
|
69
|
-
<div className="px-4 text-sm">
|
|
118
|
+
<div className="px-4 py-2 text-sm border-b border-highlight">
|
|
70
119
|
<WindowHeader
|
|
71
120
|
path={windowTarget.path}
|
|
72
121
|
type={selectedSchema?.type}
|
|
@@ -76,12 +125,52 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
76
125
|
{error && <div className="text-red">{error}</div>}
|
|
77
126
|
{typeof selectedSource === "string" &&
|
|
78
127
|
selectedSchema?.type === "string" && (
|
|
79
|
-
<
|
|
80
|
-
api={api}
|
|
81
|
-
path={windowTarget.path}
|
|
128
|
+
<TextField
|
|
82
129
|
defaultValue={selectedSource}
|
|
130
|
+
isLoading={loading}
|
|
131
|
+
registerPatchCallback={initPatchCallback(windowTarget.path)}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
{selectedSource &&
|
|
135
|
+
typeof selectedSource === "object" &&
|
|
136
|
+
VAL_EXTENSION in selectedSource &&
|
|
137
|
+
selectedSource[VAL_EXTENSION] === "richtext" && (
|
|
138
|
+
<RichTextField
|
|
139
|
+
registerPatchCallback={initPatchCallback(windowTarget.path)}
|
|
140
|
+
defaultValue={selectedSource as RichText<AnyRichTextOptions>}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
{selectedSource &&
|
|
144
|
+
typeof selectedSource === "object" &&
|
|
145
|
+
VAL_EXTENSION in selectedSource &&
|
|
146
|
+
selectedSource[VAL_EXTENSION] === "file" && (
|
|
147
|
+
<ImageField
|
|
148
|
+
registerPatchCallback={initPatchCallback(windowTarget.path)}
|
|
149
|
+
defaultValue={selectedSource as ImageSource}
|
|
83
150
|
/>
|
|
84
151
|
)}
|
|
152
|
+
<div className="flex items-end justify-end py-2">
|
|
153
|
+
<SubmitButton
|
|
154
|
+
disabled={false}
|
|
155
|
+
onClick={() => {
|
|
156
|
+
if (state[windowTarget.path]) {
|
|
157
|
+
const [moduleId] = Internal.splitModuleIdAndModulePath(
|
|
158
|
+
windowTarget.path
|
|
159
|
+
);
|
|
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
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
85
174
|
</ValWindow>
|
|
86
175
|
)}
|
|
87
176
|
</div>
|
|
@@ -89,50 +178,194 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
89
178
|
);
|
|
90
179
|
}
|
|
91
180
|
|
|
92
|
-
|
|
93
|
-
|
|
181
|
+
type PatchCallback = (modulePath: string) => PatchJSON;
|
|
182
|
+
|
|
183
|
+
function ImageField({
|
|
94
184
|
defaultValue,
|
|
95
|
-
|
|
185
|
+
registerPatchCallback,
|
|
96
186
|
}: {
|
|
97
|
-
|
|
98
|
-
defaultValue?:
|
|
99
|
-
api: ValApi;
|
|
187
|
+
registerPatchCallback: (callback: PatchCallback) => void;
|
|
188
|
+
defaultValue?: ImageSource;
|
|
100
189
|
}) {
|
|
101
|
-
const [
|
|
102
|
-
const [
|
|
103
|
-
|
|
190
|
+
const [data, setData] = useState<string | null>(null);
|
|
191
|
+
const [metadata, setMetadata] = useState<{
|
|
192
|
+
width?: number;
|
|
193
|
+
height?: number;
|
|
194
|
+
sha256: string;
|
|
195
|
+
} | null>(null);
|
|
196
|
+
const url = defaultValue && Internal.convertFileSource(defaultValue).url;
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
registerPatchCallback((path) => {
|
|
199
|
+
const pathParts = path.split("/");
|
|
200
|
+
if (!data) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
value: {
|
|
206
|
+
...defaultValue,
|
|
207
|
+
metadata,
|
|
208
|
+
},
|
|
209
|
+
op: "replace",
|
|
210
|
+
path,
|
|
211
|
+
},
|
|
212
|
+
// update the contents of the file:
|
|
213
|
+
{
|
|
214
|
+
value: data,
|
|
215
|
+
op: "replace",
|
|
216
|
+
path: `${pathParts.slice(0, -1).join("/")}/$${
|
|
217
|
+
pathParts[pathParts.length - 1]
|
|
218
|
+
}`,
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
});
|
|
222
|
+
}, [data]);
|
|
223
|
+
|
|
104
224
|
return (
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
225
|
+
<div>
|
|
226
|
+
<label htmlFor="img_input" className="">
|
|
227
|
+
<img src={data || url} />
|
|
228
|
+
<input
|
|
229
|
+
id="img_input"
|
|
230
|
+
type="file"
|
|
231
|
+
hidden
|
|
232
|
+
onChange={(ev) => {
|
|
233
|
+
readImage(ev)
|
|
234
|
+
.then((res) => {
|
|
235
|
+
setData(res.src);
|
|
236
|
+
setMetadata({
|
|
237
|
+
sha256: res.sha256,
|
|
238
|
+
width: res.width,
|
|
239
|
+
height: res.height,
|
|
240
|
+
});
|
|
241
|
+
})
|
|
242
|
+
.catch((err) => {
|
|
243
|
+
console.error(err.message);
|
|
244
|
+
setData(null);
|
|
245
|
+
setMetadata(null);
|
|
246
|
+
});
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
</label>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function RichTextField({
|
|
255
|
+
defaultValue,
|
|
256
|
+
registerPatchCallback,
|
|
257
|
+
}: {
|
|
258
|
+
registerPatchCallback: (callback: PatchCallback) => void;
|
|
259
|
+
defaultValue?: RichText<AnyRichTextOptions>;
|
|
260
|
+
}) {
|
|
261
|
+
const [editor, setEditor] = useState<LexicalEditor | null>(null);
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
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>,
|
|
272
|
+
files: {},
|
|
273
|
+
};
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
op: "replace",
|
|
277
|
+
path,
|
|
278
|
+
value: {
|
|
279
|
+
...node,
|
|
280
|
+
[VAL_EXTENSION]: "richtext",
|
|
116
281
|
},
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
282
|
+
},
|
|
283
|
+
...Object.entries(files).map(([path, value]) => {
|
|
284
|
+
return {
|
|
285
|
+
op: "file" as const,
|
|
286
|
+
path,
|
|
287
|
+
value,
|
|
288
|
+
};
|
|
289
|
+
}),
|
|
290
|
+
];
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}, [editor]);
|
|
294
|
+
return (
|
|
295
|
+
<RichTextEditor
|
|
296
|
+
onEditor={(editor) => {
|
|
297
|
+
setEditor(editor);
|
|
121
298
|
}}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
299
|
+
richtext={
|
|
300
|
+
defaultValue ||
|
|
301
|
+
({
|
|
302
|
+
children: [],
|
|
303
|
+
[VAL_EXTENSION]: "root",
|
|
304
|
+
} as unknown as RichText<AnyRichTextOptions>)
|
|
305
|
+
}
|
|
306
|
+
/>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function TextField({
|
|
311
|
+
isLoading,
|
|
312
|
+
defaultValue,
|
|
313
|
+
registerPatchCallback,
|
|
314
|
+
}: {
|
|
315
|
+
registerPatchCallback: (callback: PatchCallback) => void;
|
|
316
|
+
isLoading: boolean;
|
|
317
|
+
defaultValue?: string;
|
|
318
|
+
}) {
|
|
319
|
+
const [value, setValue] = useState(defaultValue || "");
|
|
320
|
+
|
|
321
|
+
// ref is used to get the value of the textarea without closing over the value field
|
|
322
|
+
// to avoid registering a new callback every time the value changes
|
|
323
|
+
const ref = useRef<HTMLTextAreaElement>(null);
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
registerPatchCallback((path) => {
|
|
326
|
+
return [
|
|
327
|
+
{
|
|
328
|
+
op: "replace",
|
|
329
|
+
path,
|
|
330
|
+
value: ref.current?.value || "",
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
});
|
|
334
|
+
}, []);
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div className="flex flex-col justify-between h-full px-4">
|
|
338
|
+
<div
|
|
339
|
+
className="w-full h-full py-2 overflow-y-scroll grow-wrap"
|
|
340
|
+
data-replicated-value={value} /* see grow-wrap */
|
|
132
341
|
>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
342
|
+
<textarea
|
|
343
|
+
ref={ref}
|
|
344
|
+
disabled={isLoading}
|
|
345
|
+
className="p-2 border outline-none resize-none bg-fill text-primary border-border focus-visible:border-highlight"
|
|
346
|
+
defaultValue={value}
|
|
347
|
+
onChange={(e) => setValue(e.target.value)}
|
|
348
|
+
/>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function SubmitButton({
|
|
355
|
+
disabled,
|
|
356
|
+
onClick,
|
|
357
|
+
}: {
|
|
358
|
+
disabled?: boolean;
|
|
359
|
+
onClick?: () => void;
|
|
360
|
+
}) {
|
|
361
|
+
return (
|
|
362
|
+
<button
|
|
363
|
+
className="px-4 py-2 border border-highlight disabled:border-border"
|
|
364
|
+
disabled={disabled}
|
|
365
|
+
onClick={onClick}
|
|
366
|
+
>
|
|
367
|
+
Submit
|
|
368
|
+
</button>
|
|
136
369
|
);
|
|
137
370
|
}
|
|
138
371
|
|
|
@@ -278,6 +511,63 @@ function ValHover({
|
|
|
278
511
|
);
|
|
279
512
|
}
|
|
280
513
|
|
|
514
|
+
function useHoverTarget(editMode: EditMode) {
|
|
515
|
+
const [targetElement, setTargetElement] = useState<HTMLElement>();
|
|
516
|
+
const [targetPath, setTargetPath] = useState<SourcePath>();
|
|
517
|
+
const [targetRect, setTargetRect] = useState<DOMRect>();
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
if (editMode === "hover") {
|
|
520
|
+
let curr: HTMLElement | null = null;
|
|
521
|
+
const mouseOverListener = (e: MouseEvent) => {
|
|
522
|
+
const target = e.target as HTMLElement | null;
|
|
523
|
+
curr = target;
|
|
524
|
+
// TODO: use .contains?
|
|
525
|
+
do {
|
|
526
|
+
if (curr?.dataset.valPath) {
|
|
527
|
+
console.log("setter target");
|
|
528
|
+
setTargetElement(curr);
|
|
529
|
+
setTargetPath(curr.dataset.valPath as SourcePath);
|
|
530
|
+
setTargetRect(curr.getBoundingClientRect());
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
} while ((curr = curr?.parentElement || null));
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
document.addEventListener("mouseover", mouseOverListener);
|
|
537
|
+
|
|
538
|
+
return () => {
|
|
539
|
+
setTargetElement(undefined);
|
|
540
|
+
setTargetPath(undefined);
|
|
541
|
+
document.removeEventListener("mouseover", mouseOverListener);
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}, [editMode]);
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
const scrollListener = () => {
|
|
547
|
+
if (targetElement) {
|
|
548
|
+
setTargetRect(targetElement.getBoundingClientRect());
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
document.addEventListener("scroll", scrollListener, { passive: true });
|
|
552
|
+
return () => {
|
|
553
|
+
document.removeEventListener("scroll", scrollListener);
|
|
554
|
+
};
|
|
555
|
+
}, [targetElement]);
|
|
556
|
+
|
|
557
|
+
return [
|
|
558
|
+
{
|
|
559
|
+
path: targetPath,
|
|
560
|
+
element: targetElement,
|
|
561
|
+
rect: targetRect,
|
|
562
|
+
} as HoverTarget,
|
|
563
|
+
(target: HoverTarget | null) => {
|
|
564
|
+
setTargetElement(target?.element);
|
|
565
|
+
setTargetPath(target?.path);
|
|
566
|
+
setTargetRect(target?.element?.getBoundingClientRect());
|
|
567
|
+
},
|
|
568
|
+
] as const;
|
|
569
|
+
}
|
|
570
|
+
|
|
281
571
|
// TODO: do something fun on highlight?
|
|
282
572
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
283
573
|
function useHighlight(
|
|
@@ -354,51 +644,6 @@ function useInitEditMode() {
|
|
|
354
644
|
return [editMode, setEditMode] as const;
|
|
355
645
|
}
|
|
356
646
|
|
|
357
|
-
function useHoverTarget(editMode: EditMode) {
|
|
358
|
-
const [target, setTarget] = useState<{
|
|
359
|
-
element?: HTMLElement;
|
|
360
|
-
rect?: DOMRect;
|
|
361
|
-
path: SourcePath;
|
|
362
|
-
} | null>(null);
|
|
363
|
-
useEffect(() => {
|
|
364
|
-
if (editMode === "hover") {
|
|
365
|
-
let curr: HTMLElement | null = null;
|
|
366
|
-
const mouseOverListener = (e: MouseEvent) => {
|
|
367
|
-
const target = e.target as HTMLElement | null;
|
|
368
|
-
curr = target;
|
|
369
|
-
// TODO: use .contains?
|
|
370
|
-
do {
|
|
371
|
-
if (curr?.dataset.valPath) {
|
|
372
|
-
setTarget({
|
|
373
|
-
element: curr,
|
|
374
|
-
path: curr.dataset.valPath as SourcePath,
|
|
375
|
-
});
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
} while ((curr = curr?.parentElement || null));
|
|
379
|
-
};
|
|
380
|
-
const scrollListener = () => {
|
|
381
|
-
if (target?.element) {
|
|
382
|
-
setTarget({
|
|
383
|
-
...target,
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
document.addEventListener("mouseover", mouseOverListener);
|
|
389
|
-
document.addEventListener("scroll", scrollListener, { passive: true });
|
|
390
|
-
|
|
391
|
-
return () => {
|
|
392
|
-
setTarget(null);
|
|
393
|
-
document.removeEventListener("mouseover", mouseOverListener);
|
|
394
|
-
document.removeEventListener("scroll", scrollListener);
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
}, [editMode]);
|
|
398
|
-
|
|
399
|
-
return [target, setTarget] as const;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
647
|
function useTheme(defaultTheme: Theme = "dark") {
|
|
403
648
|
const [theme, setTheme] = useState<Theme>(defaultTheme);
|
|
404
649
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { RichText as RichTextType
|
|
2
|
+
import { AnyRichTextOptions, RichText as RichTextType } from "@valbuild/core";
|
|
3
3
|
import { RichTextEditor } from "../exports";
|
|
4
4
|
import { FormContainer } from "./forms/FormContainer";
|
|
5
5
|
import { ImageForm } from "./forms/ImageForm";
|
|
@@ -51,13 +51,15 @@ export const LongText: Story = {
|
|
|
51
51
|
/* */
|
|
52
52
|
}}
|
|
53
53
|
>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
<div className="h-full grid grid-rows-[1fr,_min-content] ">
|
|
55
|
+
<TextArea
|
|
56
|
+
name="/apps/blogs.0.title"
|
|
57
|
+
text={EXAMPLE_TEXT}
|
|
58
|
+
onChange={() => {
|
|
59
|
+
console.log("onChange");
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
61
63
|
</FormContainer>
|
|
62
64
|
),
|
|
63
65
|
},
|
|
@@ -75,34 +77,53 @@ export const RichText: Story = {
|
|
|
75
77
|
<RichTextEditor
|
|
76
78
|
richtext={
|
|
77
79
|
{
|
|
78
|
-
|
|
80
|
+
_type: "richtext",
|
|
79
81
|
children: [
|
|
82
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
83
|
+
{ tag: "h2", children: ["Title 2"] },
|
|
84
|
+
{ tag: "h3", children: ["Title 3"] },
|
|
85
|
+
{ tag: "h4", children: ["Title 4"] },
|
|
86
|
+
{ tag: "h5", children: ["Title 5"] },
|
|
87
|
+
{ tag: "h6", children: ["Title 6"] },
|
|
88
|
+
{
|
|
89
|
+
tag: "p",
|
|
90
|
+
children: [
|
|
91
|
+
{
|
|
92
|
+
tag: "span",
|
|
93
|
+
classes: ["bold", "italic", "line-through"],
|
|
94
|
+
children: ["Formatted span"],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
80
98
|
{
|
|
99
|
+
tag: "ul",
|
|
81
100
|
children: [
|
|
82
101
|
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
tag: "li",
|
|
103
|
+
children: [
|
|
104
|
+
{
|
|
105
|
+
tag: "ol",
|
|
106
|
+
dir: "rtl",
|
|
107
|
+
children: [
|
|
108
|
+
{
|
|
109
|
+
tag: "li",
|
|
110
|
+
children: [
|
|
111
|
+
{
|
|
112
|
+
tag: "span",
|
|
113
|
+
classes: ["italic"],
|
|
114
|
+
children: ["number 1.1"],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{ tag: "li", children: ["number 1.2"] },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
90
122
|
},
|
|
91
123
|
],
|
|
92
|
-
direction: "ltr",
|
|
93
|
-
format: "",
|
|
94
|
-
indent: 0,
|
|
95
|
-
type: "heading",
|
|
96
|
-
version: 1,
|
|
97
|
-
tag: "h1",
|
|
98
124
|
},
|
|
99
125
|
],
|
|
100
|
-
|
|
101
|
-
format: "",
|
|
102
|
-
indent: 0,
|
|
103
|
-
type: "root",
|
|
104
|
-
version: 1,
|
|
105
|
-
} as RichTextType
|
|
126
|
+
} as RichTextType<AnyRichTextOptions>
|
|
106
127
|
}
|
|
107
128
|
/>
|
|
108
129
|
</FormContainer>
|
|
@@ -136,27 +157,12 @@ export const Image: Story = {
|
|
|
136
157
|
args: {
|
|
137
158
|
isInitialized: true,
|
|
138
159
|
children: (
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
name="/apps/blogs.0.image"
|
|
146
|
-
error={null}
|
|
147
|
-
data={{
|
|
148
|
-
url: EXAMPLE_IMAGE,
|
|
149
|
-
metadata: {
|
|
150
|
-
width: 32,
|
|
151
|
-
height: 32,
|
|
152
|
-
sha256: "123",
|
|
153
|
-
},
|
|
154
|
-
}}
|
|
155
|
-
onChange={() => {
|
|
156
|
-
console.log("onChange");
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
</FormContainer>
|
|
160
|
+
<div className="h-full grid grid-rows-[1fr,_min-content] overflow-scroll">
|
|
161
|
+
<img src={EXAMPLE_IMAGE} />
|
|
162
|
+
<div className="flex justify-end">
|
|
163
|
+
<button className="px-4 py-2 border border-highlight">Submit</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
160
166
|
),
|
|
161
167
|
},
|
|
162
168
|
};
|