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