@valbuild/ui 0.21.2 → 0.22.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 (70) hide show
  1. package/.storybook/theme.css +5 -1
  2. package/components.json +16 -0
  3. package/dist/valbuild-ui.cjs.d.ts +11 -7
  4. package/dist/valbuild-ui.cjs.js +43607 -33216
  5. package/dist/valbuild-ui.esm.js +48313 -37938
  6. package/fix-server-hack.js +45 -0
  7. package/fullscreen.vite.config.ts +11 -0
  8. package/index.html +13 -0
  9. package/package.json +52 -13
  10. package/server/dist/manifest.json +16 -0
  11. package/server/dist/style.css +2145 -0
  12. package/server/dist/valbuild-ui-main.cjs.js +74441 -0
  13. package/server/dist/valbuild-ui-main.esm.js +74442 -0
  14. package/server/dist/valbuild-ui-server.cjs.js +19 -2
  15. package/server/dist/valbuild-ui-server.esm.js +19 -2
  16. package/server.vite.config.ts +2 -0
  17. package/src/App.tsx +73 -0
  18. package/src/assets/icons/Logo.tsx +103 -0
  19. package/src/components/Button.tsx +10 -2
  20. package/src/components/Dropdown.tsx +2 -2
  21. package/src/components/{dashboard/Grid.stories.tsx → Grid.stories.tsx} +8 -17
  22. package/src/components/{dashboard/Grid.tsx → Grid.tsx} +36 -23
  23. package/src/components/RichTextEditor/ContentEditable.tsx +109 -1
  24. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +2 -2
  25. package/src/components/RichTextEditor/RichTextEditor.tsx +1 -1
  26. package/src/components/ValFormField.tsx +576 -0
  27. package/src/components/ValFullscreen.tsx +1283 -0
  28. package/src/components/ValMenu.tsx +65 -13
  29. package/src/components/ValOverlay.tsx +32 -338
  30. package/src/components/ValWindow.tsx +12 -9
  31. package/src/components/dashboard/FormGroup.tsx +12 -6
  32. package/src/components/dashboard/Tree.tsx +2 -2
  33. package/src/components/ui/accordion.tsx +58 -0
  34. package/src/components/ui/alert-dialog.tsx +139 -0
  35. package/src/components/ui/avatar.tsx +48 -0
  36. package/src/components/ui/button.tsx +56 -0
  37. package/src/components/ui/calendar.tsx +62 -0
  38. package/src/components/ui/card.tsx +86 -0
  39. package/src/components/ui/checkbox.tsx +28 -0
  40. package/src/components/ui/command.tsx +153 -0
  41. package/src/components/ui/dialog.tsx +120 -0
  42. package/src/components/ui/dropdown-menu.tsx +198 -0
  43. package/src/components/ui/form.tsx +177 -0
  44. package/src/components/ui/input.tsx +24 -0
  45. package/src/components/ui/label.tsx +24 -0
  46. package/src/components/ui/popover.tsx +29 -0
  47. package/src/components/ui/progress.tsx +26 -0
  48. package/src/components/ui/radio-group.tsx +42 -0
  49. package/src/components/ui/scroll-area.tsx +51 -0
  50. package/src/components/ui/select.tsx +119 -0
  51. package/src/components/ui/switch.tsx +27 -0
  52. package/src/components/ui/tabs.tsx +53 -0
  53. package/src/components/ui/toggle.tsx +43 -0
  54. package/src/components/ui/tooltip.tsx +28 -0
  55. package/src/components/usePatch.ts +86 -0
  56. package/src/components/useTheme.ts +45 -0
  57. package/src/exports.ts +2 -1
  58. package/src/index.css +96 -60
  59. package/src/lib/IValStore.ts +6 -0
  60. package/src/lib/utils.ts +6 -0
  61. package/src/main.jsx +10 -0
  62. package/src/richtext/conversion/lexicalToRichTextSource.ts +0 -1
  63. package/src/richtext/shadowRootPolyFill.js +115 -0
  64. package/src/server.ts +39 -2
  65. package/src/utils/resolvePath.ts +0 -1
  66. package/src/vite-server.ts +20 -3
  67. package/tailwind.config.js +63 -51
  68. package/tsconfig.json +2 -1
  69. package/vite.config.ts +10 -0
  70. package/src/components/dashboard/ValDashboard.tsx +0 -150
@@ -0,0 +1,576 @@
1
+ import {
2
+ AnyRichTextOptions,
3
+ FileSource,
4
+ ImageMetadata,
5
+ Internal,
6
+ Json,
7
+ RichTextSource,
8
+ SerializedSchema,
9
+ SourcePath,
10
+ VAL_EXTENSION,
11
+ } from "@valbuild/core";
12
+ import type { PatchJSON } from "@valbuild/core/patch";
13
+ import { LexicalEditor } from "lexical";
14
+ import { useState, useEffect, useRef, useContext } from "react";
15
+ import { RichTextEditor } from "../exports";
16
+ import { lexicalToRichTextSource } from "../richtext/conversion/lexicalToRichTextSource";
17
+ import { LexicalRootNode } from "../richtext/conversion/richTextSourceToLexical";
18
+ import { readImage } from "../utils/readImage";
19
+ import { Button } from "./ui/button";
20
+ import { Input } from "./ui/input";
21
+ import {
22
+ Select,
23
+ SelectContent,
24
+ SelectItem,
25
+ SelectLabel,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from "./ui/select";
29
+ import { PatchCallback } from "./usePatch";
30
+ import { useValModuleFromPath, ValModulesContext } from "./ValFullscreen";
31
+
32
+ type ImageSource = FileSource<ImageMetadata>;
33
+ export type OnSubmit = (callback: PatchCallback) => Promise<void>;
34
+
35
+ export function ValFormField({
36
+ path,
37
+ disabled,
38
+ source: source,
39
+ schema: schema,
40
+ registerPatchCallback,
41
+ onSubmit,
42
+ }: {
43
+ path: string;
44
+ disabled: boolean;
45
+ source: Json;
46
+ schema: SerializedSchema;
47
+ onSubmit?: OnSubmit;
48
+ registerPatchCallback?: (callback: PatchCallback) => void;
49
+ }) {
50
+ if (
51
+ (typeof source === "string" || source === null) &&
52
+ schema?.type === "string"
53
+ ) {
54
+ return (
55
+ <StringField
56
+ defaultValue={source}
57
+ disabled={disabled}
58
+ registerPatchCallback={registerPatchCallback}
59
+ onSubmit={onSubmit}
60
+ />
61
+ );
62
+ }
63
+ if (
64
+ (typeof source === "number" || source === null) &&
65
+ schema?.type === "number"
66
+ ) {
67
+ return (
68
+ <NumberField
69
+ defaultValue={source}
70
+ disabled={disabled}
71
+ registerPatchCallback={registerPatchCallback}
72
+ onSubmit={onSubmit}
73
+ />
74
+ );
75
+ }
76
+ if (
77
+ (typeof source === "number" ||
78
+ typeof source === "string" ||
79
+ source === null) &&
80
+ schema?.type === "keyOf"
81
+ ) {
82
+ return (
83
+ <KeyOfField
84
+ defaultValue={source}
85
+ disabled={disabled}
86
+ registerPatchCallback={registerPatchCallback}
87
+ onSubmit={onSubmit}
88
+ selector={schema.selector}
89
+ />
90
+ );
91
+ }
92
+ if (
93
+ (typeof source === "object" || source === null) &&
94
+ schema?.type === "richtext"
95
+ ) {
96
+ return (
97
+ <RichTextField
98
+ registerPatchCallback={registerPatchCallback}
99
+ onSubmit={onSubmit}
100
+ defaultValue={source as RichTextSource<AnyRichTextOptions>}
101
+ />
102
+ );
103
+ }
104
+ if (
105
+ (typeof source === "object" || source === null) &&
106
+ schema?.type === "image"
107
+ ) {
108
+ return (
109
+ <ImageField
110
+ path={path}
111
+ registerPatchCallback={registerPatchCallback}
112
+ onSubmit={onSubmit}
113
+ defaultValue={source as ImageSource}
114
+ />
115
+ );
116
+ }
117
+ return <div>Unsupported schema: {schema.type}</div>;
118
+ }
119
+
120
+ async function createImagePatch(
121
+ path: string,
122
+ data: string | null,
123
+ metadata: ImageMetadata,
124
+ defaultValue?: ImageSource
125
+ ): Promise<PatchJSON> {
126
+ const pathParts = path.split("/");
127
+ if (!data || !metadata) {
128
+ return [];
129
+ }
130
+ return [
131
+ {
132
+ value: {
133
+ ...defaultValue,
134
+ metadata,
135
+ },
136
+ op: "replace",
137
+ path,
138
+ },
139
+ // update the contents of the file:
140
+ {
141
+ value: data,
142
+ op: "replace",
143
+ path: `${pathParts.slice(0, -1).join("/")}/$${
144
+ pathParts[pathParts.length - 1]
145
+ }`,
146
+ },
147
+ ];
148
+ }
149
+
150
+ function ImageField({
151
+ path,
152
+ defaultValue,
153
+ onSubmit,
154
+ registerPatchCallback,
155
+ }: {
156
+ path: string;
157
+ onSubmit?: OnSubmit;
158
+ registerPatchCallback?: (callback: PatchCallback) => void;
159
+ defaultValue?: ImageSource;
160
+ }) {
161
+ const [data, setData] = useState<string | null>(null);
162
+ const [loading, setLoading] = useState(false);
163
+ const [metadata, setMetadata] = useState<ImageMetadata>();
164
+ const [url, setUrl] = useState<string>();
165
+ useEffect(() => {
166
+ setUrl(defaultValue && Internal.convertFileSource(defaultValue).url);
167
+ }, [defaultValue]);
168
+
169
+ useEffect(() => {
170
+ if (registerPatchCallback) {
171
+ registerPatchCallback(async (path) => {
172
+ return createImagePatch(path, data, metadata, defaultValue);
173
+ });
174
+ }
175
+ }, [data, defaultValue]);
176
+
177
+ return (
178
+ <div className="max-w-4xl p-4" key={path}>
179
+ <label htmlFor={`img_input:${path}`} className="">
180
+ {data || url ? <img src={data || url} /> : <div>Empty</div>}
181
+ <input
182
+ id={`img_input:${path}`}
183
+ type="file"
184
+ hidden
185
+ onChange={(ev) => {
186
+ readImage(ev)
187
+ .then((res) => {
188
+ setData(res.src);
189
+ if (res.width && res.height) {
190
+ setMetadata({
191
+ sha256: res.sha256,
192
+ width: res.width,
193
+ height: res.height,
194
+ });
195
+ } else {
196
+ setMetadata(undefined);
197
+ }
198
+ })
199
+ .catch((err) => {
200
+ console.error(err.message);
201
+ setData(null);
202
+ setMetadata(undefined);
203
+ });
204
+ }}
205
+ />
206
+ </label>
207
+ {onSubmit && (
208
+ <div>
209
+ {data && (
210
+ <Button
211
+ disabled={loading}
212
+ onClick={() => {
213
+ setLoading(true);
214
+ onSubmit((path) =>
215
+ createImagePatch(path, data, metadata, defaultValue)
216
+ ).finally(() => {
217
+ setLoading(false);
218
+ setData(null);
219
+ setMetadata(undefined);
220
+ });
221
+ }}
222
+ >
223
+ {loading ? "Saving..." : "Submit"}
224
+ </Button>
225
+ )}
226
+ </div>
227
+ )}
228
+ </div>
229
+ );
230
+ }
231
+
232
+ async function createRichTextPatch(path: string, editor: LexicalEditor) {
233
+ const { templateStrings, exprs, files } = editor
234
+ ? await lexicalToRichTextSource(
235
+ editor.getEditorState().toJSON().root as LexicalRootNode
236
+ )
237
+ : ({
238
+ [VAL_EXTENSION]: "richtext",
239
+ templateStrings: [""],
240
+ exprs: [],
241
+ files: {},
242
+ } as RichTextSource<AnyRichTextOptions> & {
243
+ files: Record<string, string>;
244
+ });
245
+ return [
246
+ {
247
+ op: "replace" as const,
248
+ path,
249
+ value: {
250
+ templateStrings,
251
+ exprs,
252
+ [VAL_EXTENSION]: "richtext",
253
+ },
254
+ },
255
+ ...Object.entries(files).map(([filePath, value]) => {
256
+ return {
257
+ op: "file" as const,
258
+ path,
259
+ filePath,
260
+ value,
261
+ };
262
+ }),
263
+ ];
264
+ }
265
+ function RichTextField({
266
+ defaultValue,
267
+ onSubmit,
268
+ registerPatchCallback,
269
+ }: {
270
+ onSubmit?: OnSubmit;
271
+ registerPatchCallback?: (callback: PatchCallback) => void;
272
+ defaultValue?: RichTextSource<AnyRichTextOptions>;
273
+ }) {
274
+ const [editor, setEditor] = useState<LexicalEditor | null>(null);
275
+ const [didChange, setDidChange] = useState(false);
276
+ const [loading, setLoading] = useState(false);
277
+ useEffect(() => {
278
+ if (editor) {
279
+ setDidChange(false);
280
+ editor.registerTextContentListener(() => {
281
+ setDidChange(true);
282
+ });
283
+ editor.registerDecoratorListener(() => {
284
+ setDidChange(true);
285
+ });
286
+ }
287
+ }, [editor]);
288
+ useEffect(() => {
289
+ if (editor && registerPatchCallback) {
290
+ registerPatchCallback((path) => createRichTextPatch(path, editor));
291
+ }
292
+ }, [editor]);
293
+ return (
294
+ <div className="p-4 border rounded border-card">
295
+ <RichTextEditor
296
+ onEditor={(editor) => {
297
+ setEditor(editor);
298
+ }}
299
+ richtext={
300
+ defaultValue ||
301
+ ({
302
+ children: [],
303
+ [VAL_EXTENSION]: "root",
304
+ } as unknown as RichTextSource<AnyRichTextOptions>)
305
+ }
306
+ />
307
+ {onSubmit && (
308
+ <div>
309
+ {didChange && (
310
+ <Button
311
+ disabled={loading || !editor}
312
+ onClick={() => {
313
+ if (editor) {
314
+ setLoading(true);
315
+ onSubmit((path) => createRichTextPatch(path, editor)).finally(
316
+ () => {
317
+ setLoading(false);
318
+ setDidChange(false);
319
+ }
320
+ );
321
+ }
322
+ }}
323
+ >
324
+ {loading ? "Saving..." : "Submit"}
325
+ </Button>
326
+ )}
327
+ </div>
328
+ )}
329
+ </div>
330
+ );
331
+ }
332
+
333
+ function KeyOfField({
334
+ disabled,
335
+ defaultValue,
336
+ registerPatchCallback,
337
+ onSubmit,
338
+ selector,
339
+ }: {
340
+ registerPatchCallback?: (callback: PatchCallback) => void;
341
+ onSubmit?: OnSubmit;
342
+ disabled: boolean;
343
+ defaultValue?: string | number | null;
344
+ selector: SourcePath;
345
+ }) {
346
+ const valModule = useValModuleFromPath(selector);
347
+ const getValuesFromModule = (module: typeof valModule) => {
348
+ if (Array.isArray(module.moduleSource)) {
349
+ return {
350
+ type: "number",
351
+ values: Object.keys(module.moduleSource).map((key) => parseInt(key)),
352
+ };
353
+ }
354
+ return {
355
+ type: "string",
356
+ values: Object.keys(module.moduleSource ?? ["ERROR fetching source"]),
357
+ };
358
+ };
359
+ const typeAndValues = getValuesFromModule(valModule);
360
+ const [value, setValue] = useState(defaultValue || typeAndValues.values[0]);
361
+ const [loading, setLoading] = useState(false);
362
+ useEffect(() => {
363
+ setLoading(disabled);
364
+ }, [disabled]);
365
+
366
+ const parse = (value: string) => {
367
+ if (typeAndValues.type === "number") {
368
+ if (value === "") {
369
+ throw new Error("Value cannot be empty");
370
+ }
371
+ if (Number.isNaN(Number(value))) {
372
+ throw new Error("Value was not a number: " + JSON.stringify(value));
373
+ }
374
+ return Number(value);
375
+ }
376
+ return value;
377
+ };
378
+
379
+ useEffect(() => {
380
+ if (registerPatchCallback) {
381
+ registerPatchCallback(async (path) => {
382
+ return [
383
+ {
384
+ op: "replace",
385
+ path,
386
+ value: value,
387
+ },
388
+ ];
389
+ });
390
+ }
391
+ }, [value]);
392
+
393
+ return (
394
+ <div className="flex flex-col justify-between h-full gap-y-4">
395
+ <Select
396
+ defaultValue={value.toString()}
397
+ disabled={loading}
398
+ onValueChange={(value) => {
399
+ setValue(parse(value));
400
+ }}
401
+ >
402
+ <SelectTrigger>
403
+ <SelectValue placeholder="Select a value" />
404
+ </SelectTrigger>
405
+ <SelectContent>
406
+ {typeAndValues.values.map((value) => (
407
+ <SelectItem key={value} value={value.toString()}>
408
+ {value}
409
+ </SelectItem>
410
+ ))}
411
+ </SelectContent>
412
+ </Select>
413
+ {onSubmit && (
414
+ <div>
415
+ {defaultValue !== value && (
416
+ <Button
417
+ disabled={loading}
418
+ onClick={() => {
419
+ setLoading(true);
420
+ onSubmit(async (path) => [
421
+ {
422
+ op: "replace",
423
+ path,
424
+ value: value,
425
+ },
426
+ ]).finally(() => {
427
+ setLoading(false);
428
+ });
429
+ }}
430
+ >
431
+ {loading ? "Saving..." : "Submit"}
432
+ </Button>
433
+ )}
434
+ </div>
435
+ )}
436
+ </div>
437
+ );
438
+ }
439
+ function NumberField({
440
+ disabled,
441
+ defaultValue,
442
+ registerPatchCallback,
443
+ onSubmit,
444
+ }: {
445
+ registerPatchCallback?: (callback: PatchCallback) => void;
446
+ onSubmit?: OnSubmit;
447
+ disabled: boolean;
448
+ defaultValue?: number | null;
449
+ }) {
450
+ const [value, setValue] = useState(defaultValue || 0);
451
+ const [loading, setLoading] = useState(false);
452
+ useEffect(() => {
453
+ setLoading(disabled);
454
+ }, [disabled]);
455
+
456
+ // ref is used to get the value of the textarea without closing over the value field
457
+ // to avoid registering a new callback every time the value changes
458
+ const ref = useRef<HTMLInputElement>(null);
459
+ useEffect(() => {
460
+ if (registerPatchCallback) {
461
+ registerPatchCallback(async (path) => {
462
+ return [
463
+ {
464
+ op: "replace",
465
+ path,
466
+ value: Number(ref.current?.value) || 0,
467
+ },
468
+ ];
469
+ });
470
+ }
471
+ }, []);
472
+
473
+ return (
474
+ <div className="flex flex-col justify-between h-full gap-y-4">
475
+ <Input
476
+ ref={ref}
477
+ disabled={loading}
478
+ defaultValue={value ?? 0}
479
+ onChange={(e) => setValue(Number(e.target.value))}
480
+ type="number"
481
+ />
482
+ {onSubmit && (
483
+ <div>
484
+ {defaultValue !== value && (
485
+ <Button
486
+ disabled={loading}
487
+ onClick={() => {
488
+ setLoading(true);
489
+ onSubmit(async (path) => [
490
+ {
491
+ op: "replace",
492
+ path,
493
+ value: Number(ref.current?.value) || 0,
494
+ },
495
+ ]).finally(() => {
496
+ setLoading(false);
497
+ });
498
+ }}
499
+ >
500
+ {loading ? "Saving..." : "Submit"}
501
+ </Button>
502
+ )}
503
+ </div>
504
+ )}
505
+ </div>
506
+ );
507
+ }
508
+
509
+ function StringField({
510
+ disabled,
511
+ defaultValue,
512
+ registerPatchCallback,
513
+ onSubmit,
514
+ }: {
515
+ registerPatchCallback?: (callback: PatchCallback) => void;
516
+ onSubmit?: OnSubmit;
517
+ disabled: boolean;
518
+ defaultValue?: string | null;
519
+ }) {
520
+ const [value, setValue] = useState(defaultValue || "");
521
+ const [loading, setLoading] = useState(false);
522
+ useEffect(() => {
523
+ setLoading(disabled);
524
+ }, [disabled]);
525
+
526
+ // ref is used to get the value of the textarea without closing over the value field
527
+ // to avoid registering a new callback every time the value changes
528
+ const ref = useRef<HTMLInputElement>(null);
529
+ useEffect(() => {
530
+ if (registerPatchCallback) {
531
+ registerPatchCallback(async (path) => {
532
+ return [
533
+ {
534
+ op: "replace",
535
+ path,
536
+ value: ref.current?.value || "",
537
+ },
538
+ ];
539
+ });
540
+ }
541
+ }, []);
542
+
543
+ return (
544
+ <div className="flex flex-col justify-between h-full gap-y-4">
545
+ <Input
546
+ ref={ref}
547
+ disabled={loading}
548
+ defaultValue={value ?? ""}
549
+ onChange={(e) => setValue(e.target.value)}
550
+ />
551
+ {onSubmit && (
552
+ <div>
553
+ {defaultValue !== value && (
554
+ <Button
555
+ disabled={loading}
556
+ onClick={() => {
557
+ setLoading(true);
558
+ onSubmit(async (path) => [
559
+ {
560
+ op: "replace",
561
+ path,
562
+ value: ref.current?.value || "",
563
+ },
564
+ ]).finally(() => {
565
+ setLoading(false);
566
+ });
567
+ }}
568
+ >
569
+ {loading ? "Saving..." : "Submit"}
570
+ </Button>
571
+ )}
572
+ </div>
573
+ )}
574
+ </div>
575
+ );
576
+ }