@valbuild/ui 0.26.0 → 0.28.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 (145) hide show
  1. package/dist/valbuild-ui.cjs.js +41 -15
  2. package/dist/valbuild-ui.esm.js +41 -15
  3. package/package.json +7 -3
  4. package/server/.tmp/assets/index-18cfa26c.css +1 -0
  5. package/server/.tmp/assets/index-513f7a9c.js +197 -0
  6. package/{index.html → server/.tmp/index.html} +3 -1
  7. package/server/dist/style.css +0 -3
  8. package/server/dist/valbuild-ui-main.cjs.js +60 -34
  9. package/server/dist/valbuild-ui-main.esm.js +60 -34
  10. package/server/dist/valbuild-ui-server.cjs.js +1 -1
  11. package/server/dist/valbuild-ui-server.esm.js +1 -1
  12. package/.babelrc.json +0 -10
  13. package/.storybook/main.js +0 -25
  14. package/.storybook/preview-head.html +0 -6
  15. package/.storybook/preview.js +0 -33
  16. package/.storybook/theme.css +0 -34
  17. package/CHANGELOG.md +0 -0
  18. package/components.json +0 -16
  19. package/fix-server-hack.js +0 -54
  20. package/fullscreen.vite.config.ts +0 -9
  21. package/jest.config.js +0 -4
  22. package/postcss.config.js +0 -6
  23. package/rollup.config.js +0 -23
  24. package/server.vite.config.ts +0 -31
  25. package/src/App.tsx +0 -73
  26. package/src/assets/icons/Bold.tsx +0 -23
  27. package/src/assets/icons/Chevron.tsx +0 -28
  28. package/src/assets/icons/FontColor.tsx +0 -30
  29. package/src/assets/icons/ImageIcon.tsx +0 -29
  30. package/src/assets/icons/Italic.tsx +0 -24
  31. package/src/assets/icons/Logo.tsx +0 -103
  32. package/src/assets/icons/Section.tsx +0 -41
  33. package/src/assets/icons/Strikethrough.tsx +0 -22
  34. package/src/assets/icons/TextIcon.tsx +0 -20
  35. package/src/assets/icons/Underline.tsx +0 -22
  36. package/src/assets/icons/Undo.tsx +0 -20
  37. package/src/components/Button.tsx +0 -68
  38. package/src/components/Checkbox.tsx +0 -51
  39. package/src/components/DraggableList.stories.tsx +0 -20
  40. package/src/components/DraggableList.tsx +0 -95
  41. package/src/components/Dropdown.tsx +0 -101
  42. package/src/components/EditButton.tsx +0 -10
  43. package/src/components/ErrorText.tsx +0 -3
  44. package/src/components/ExpandLogo.tsx +0 -72
  45. package/src/components/Grid.stories.tsx +0 -43
  46. package/src/components/Grid.tsx +0 -139
  47. package/src/components/RichTextEditor/ContentEditable.tsx +0 -117
  48. package/src/components/RichTextEditor/Nodes/ImageNode.tsx +0 -100
  49. package/src/components/RichTextEditor/Plugins/AutoFocus.tsx +0 -12
  50. package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +0 -45
  51. package/src/components/RichTextEditor/Plugins/LinkEditorPlugin.tsx +0 -58
  52. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +0 -412
  53. package/src/components/RichTextEditor/RichTextEditor.tsx +0 -105
  54. package/src/components/UploadModal.tsx +0 -109
  55. package/src/components/User.tsx +0 -17
  56. package/src/components/ValFormField.tsx +0 -574
  57. package/src/components/ValFullscreen.tsx +0 -1278
  58. package/src/components/ValMenu.tsx +0 -92
  59. package/src/components/ValOverlay.tsx +0 -488
  60. package/src/components/ValOverlayContext.tsx +0 -80
  61. package/src/components/ValWindow.stories.tsx +0 -146
  62. package/src/components/ValWindow.tsx +0 -220
  63. package/src/components/dashboard/DashboardButton.tsx +0 -25
  64. package/src/components/dashboard/DashboardDropdown.tsx +0 -59
  65. package/src/components/dashboard/Dropdown.stories.tsx +0 -11
  66. package/src/components/dashboard/Dropdown.tsx +0 -70
  67. package/src/components/dashboard/FormGroup.stories.tsx +0 -37
  68. package/src/components/dashboard/FormGroup.tsx +0 -42
  69. package/src/components/dashboard/Grid2.stories.tsx +0 -56
  70. package/src/components/dashboard/Grid2.tsx +0 -72
  71. package/src/components/dashboard/Tree.stories.tsx +0 -91
  72. package/src/components/dashboard/Tree.tsx +0 -72
  73. package/src/components/dashboard/ValDashboardEditor.tsx +0 -269
  74. package/src/components/dashboard/ValDashboardGrid.tsx +0 -142
  75. package/src/components/dashboard/ValTreeNavigator.tsx +0 -253
  76. package/src/components/forms/Form.tsx +0 -126
  77. package/src/components/forms/FormContainer.tsx +0 -24
  78. package/src/components/forms/ImageForm.tsx +0 -195
  79. package/src/components/forms/TextArea.tsx +0 -24
  80. package/src/components/ui/accordion.tsx +0 -58
  81. package/src/components/ui/alert-dialog.tsx +0 -139
  82. package/src/components/ui/avatar.tsx +0 -48
  83. package/src/components/ui/button.tsx +0 -56
  84. package/src/components/ui/calendar.tsx +0 -62
  85. package/src/components/ui/card.tsx +0 -86
  86. package/src/components/ui/checkbox.tsx +0 -28
  87. package/src/components/ui/command.tsx +0 -153
  88. package/src/components/ui/dialog.tsx +0 -120
  89. package/src/components/ui/dropdown-menu.tsx +0 -198
  90. package/src/components/ui/form.tsx +0 -177
  91. package/src/components/ui/input.tsx +0 -24
  92. package/src/components/ui/label.tsx +0 -24
  93. package/src/components/ui/popover.tsx +0 -29
  94. package/src/components/ui/progress.tsx +0 -26
  95. package/src/components/ui/radio-group.tsx +0 -42
  96. package/src/components/ui/scroll-area.tsx +0 -51
  97. package/src/components/ui/select.tsx +0 -119
  98. package/src/components/ui/switch.tsx +0 -27
  99. package/src/components/ui/tabs.tsx +0 -53
  100. package/src/components/ui/toggle.tsx +0 -43
  101. package/src/components/ui/tooltip.tsx +0 -28
  102. package/src/components/usePatch.ts +0 -86
  103. package/src/components/useTheme.ts +0 -45
  104. package/src/dto/SerializedSchema.ts +0 -69
  105. package/src/dto/Session.ts +0 -12
  106. package/src/dto/SessionMode.ts +0 -5
  107. package/src/dto/Tree.ts +0 -18
  108. package/src/exports.ts +0 -6
  109. package/src/index.css +0 -115
  110. package/src/index.tsx +0 -14
  111. package/src/lib/IValStore.ts +0 -6
  112. package/src/lib/utils.ts +0 -6
  113. package/src/main.jsx +0 -10
  114. package/src/richtext/conversion/conversion.test.ts +0 -146
  115. package/src/richtext/conversion/lexicalToRichTextSource.test.ts +0 -89
  116. package/src/richtext/conversion/lexicalToRichTextSource.ts +0 -285
  117. package/src/richtext/conversion/parseRichTextSource.test.ts +0 -469
  118. package/src/richtext/conversion/parseRichTextSource.ts +0 -233
  119. package/src/richtext/conversion/richTextSourceToLexical.test.ts +0 -381
  120. package/src/richtext/conversion/richTextSourceToLexical.ts +0 -293
  121. package/src/richtext/shadowRootPolyFill.js +0 -115
  122. package/src/server.ts +0 -70
  123. package/src/stories/Button.stories.tsx +0 -20
  124. package/src/stories/Checkbox.stories.tsx +0 -14
  125. package/src/stories/Dropdown.stories.tsx +0 -23
  126. package/src/stories/Introduction.mdx +0 -221
  127. package/src/stories/RichTextEditor.stories.tsx +0 -24
  128. package/src/stories/assets/code-brackets.svg +0 -1
  129. package/src/stories/assets/colors.svg +0 -1
  130. package/src/stories/assets/comments.svg +0 -1
  131. package/src/stories/assets/direction.svg +0 -1
  132. package/src/stories/assets/flow.svg +0 -1
  133. package/src/stories/assets/plugin.svg +0 -1
  134. package/src/stories/assets/repo.svg +0 -1
  135. package/src/stories/assets/stackalt.svg +0 -1
  136. package/src/utils/Remote.ts +0 -15
  137. package/src/utils/imageMimeType.ts +0 -23
  138. package/src/utils/readImage.ts +0 -54
  139. package/src/utils/resolvePath.ts +0 -32
  140. package/src/vite-env.d.ts +0 -1
  141. package/src/vite-index.tsx +0 -7
  142. package/src/vite-server.ts +0 -42
  143. package/tailwind.config.js +0 -83
  144. package/tsconfig.json +0 -19
  145. package/vite.config.ts +0 -43
@@ -1,1278 +0,0 @@
1
- "use client";
2
- import {
3
- AnyRichTextOptions,
4
- ApiTreeResponse,
5
- FileSource,
6
- FILE_REF_PROP,
7
- ImageMetadata,
8
- Internal,
9
- ModuleId,
10
- RichText,
11
- RichTextNode,
12
- RichTextSource,
13
- SerializedRecordSchema,
14
- SerializedSchema,
15
- SourcePath,
16
- VAL_EXTENSION,
17
- } from "@valbuild/core";
18
- import {
19
- SerializedArraySchema,
20
- SerializedObjectSchema,
21
- Json,
22
- JsonArray,
23
- JsonObject,
24
- } from "@valbuild/core";
25
- import { ValApi } from "@valbuild/core";
26
- import { FC, Fragment, useCallback, useEffect, useState } from "react";
27
- import { Grid } from "./Grid";
28
- import { result } from "@valbuild/core/fp";
29
- import { Tree } from "./dashboard/Tree";
30
- import { OnSubmit, ValFormField } from "./ValFormField";
31
- import React from "react";
32
- import { parseRichTextSource } from "../exports";
33
- import { createPortal } from "react-dom";
34
- import Logo from "../assets/icons/Logo";
35
- import { ScrollArea } from "./ui/scroll-area";
36
- import { Switch } from "./ui/switch";
37
- import { Card } from "./ui/card";
38
- import { ChevronLeft } from "lucide-react";
39
- import { ValOverlayContext } from "./ValOverlayContext";
40
- import { useNavigate, useParams } from "react-router";
41
- import { useTheme } from "./useTheme";
42
- import classNames from "classnames";
43
- import { ValMenu } from "./ValMenu";
44
-
45
- interface ValFullscreenProps {
46
- valApi: ValApi;
47
- }
48
-
49
- // TODO: move SerializedModuleContent to core
50
- type SerializedModuleContent = ApiTreeResponse["modules"][ModuleId];
51
- export const ValModulesContext = React.createContext<ValModules>(null);
52
-
53
- export const useValModuleFromPath = (
54
- sourcePath: SourcePath
55
- ): {
56
- moduleId: ModuleId;
57
- moduleSource: Json | undefined;
58
- moduleSchema: SerializedSchema | undefined;
59
- } => {
60
- const modules = React.useContext(ValModulesContext);
61
- const [moduleId, modulePath] =
62
- Internal.splitModuleIdAndModulePath(sourcePath);
63
- const moduleSource = modules?.[moduleId]?.source;
64
- const moduleSchema = modules?.[moduleId]?.schema;
65
- if (!moduleSource || !moduleSchema) {
66
- throw Error("Could not find module: " + moduleId);
67
- }
68
- const resolvedPath = Internal.resolvePath(
69
- modulePath,
70
- moduleSource,
71
- moduleSchema
72
- );
73
- return {
74
- moduleId,
75
- moduleSource: resolvedPath.source,
76
- moduleSchema: resolvedPath.schema,
77
- };
78
- };
79
-
80
- type ValModules = Record<ModuleId, SerializedModuleContent> | null;
81
-
82
- type InitOnSubmit = (path: SourcePath) => OnSubmit;
83
- export const ValFullscreen: FC<ValFullscreenProps> = ({ valApi }) => {
84
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
85
- const { "*": pathFromParams } = useParams();
86
- const [modules, setModules] = useState<ValModules>(null);
87
- const [error, setError] = useState<string | null>(null);
88
- const [selectedPath, setSelectedPath] = useState<SourcePath | ModuleId>();
89
- const [selectedModuleId] = selectedPath
90
- ? Internal.splitModuleIdAndModulePath(selectedPath as SourcePath)
91
- : [undefined, undefined];
92
- const moduleSource = selectedModuleId && modules?.[selectedModuleId]?.source;
93
- const moduleSchema = selectedModuleId && modules?.[selectedModuleId]?.schema;
94
- const fatalErrors = Object.entries(modules || {}).flatMap(([, module]) => {
95
- return module.errors
96
- ? module.errors.fatal
97
- ? module.errors.fatal
98
- : []
99
- : [];
100
- });
101
- const validationErrors = Object.entries(modules || {}).flatMap(
102
- ([, module]) => {
103
- return module.errors && module.errors.validation
104
- ? [module.errors.validation]
105
- : [];
106
- }
107
- );
108
-
109
- if (fatalErrors && fatalErrors.length > 0) {
110
- const message =
111
- fatalErrors.length === 1
112
- ? fatalErrors[0].message
113
- : `Multiple errors detected:\n${fatalErrors
114
- .map((f, i) => `${i + 1}. ${f.message}`)
115
- .join("\n")}\n\nShowing stack trace of: 0. ${
116
- fatalErrors[0].message
117
- }`;
118
- const error = new Error(message);
119
- error.stack = fatalErrors[0].stack;
120
- throw error;
121
- }
122
-
123
- if (validationErrors && validationErrors.length > 0) {
124
- console.warn("Val encountered validation errors:", validationErrors);
125
- }
126
- //
127
- useEffect(() => {
128
- setSelectedPath(
129
- pathFromParams ? (`/${pathFromParams}` as ModuleId) : selectedPath
130
- );
131
- }, [pathFromParams]);
132
-
133
- const [hmrHash, setHmrHash] = useState(null);
134
- useEffect(() => {
135
- try {
136
- // use websocket to update modules
137
- const hot = new WebSocket(
138
- `${window.location.origin.replace(
139
- "http://",
140
- "ws://"
141
- )}/_next/webpack-hmr`
142
- );
143
- hot.addEventListener("message", (e) => {
144
- let data;
145
- try {
146
- data = JSON.parse(e.data);
147
- } catch (err) {
148
- console.error("Failed to parse HMR");
149
- }
150
- if (typeof data?.hash === "string" && data?.action === "built") {
151
- setHmrHash(data.hash);
152
- }
153
- });
154
- } catch (err) {
155
- // could not set up dev mode
156
- console.warn("Failed to initialize HMR", err);
157
- }
158
- }, []);
159
- useEffect(() => {
160
- console.log("(Re)-fetching modules");
161
- valApi
162
- .getModules({ patch: true, includeSchema: true, includeSource: true })
163
- .then((res) => {
164
- if (result.isOk(res)) {
165
- setModules(res.value.modules);
166
- } else {
167
- setError("Could not load modules: " + res.error.message);
168
- console.error(res.error);
169
- }
170
- });
171
- }, [hmrHash]);
172
-
173
- const navigate = useNavigate();
174
- const [theme, setTheme] = useTheme();
175
-
176
- useEffect(() => {
177
- const popStateListener = (event: PopStateEvent) => {
178
- console.log("popstate", event);
179
- };
180
-
181
- window.addEventListener("popstate", popStateListener);
182
-
183
- return () => {
184
- window.removeEventListener("popstate", popStateListener);
185
- };
186
- }, []);
187
-
188
- const hoverElemRef = React.useRef<HTMLDivElement | null>(null);
189
-
190
- const initOnSubmit: InitOnSubmit = useCallback(
191
- (path) => async (callback) => {
192
- const [moduleId, modulePath] = Internal.splitModuleIdAndModulePath(path);
193
- const patch = await callback(Internal.createPatchJSONPath(modulePath));
194
- return valApi
195
- .postPatches(moduleId, patch)
196
- .then((res) => {
197
- if (result.isErr(res)) {
198
- throw res.error;
199
- } else {
200
- console.log("submitted", patch);
201
- // TODO: we need to revisit this a bit, HMR might not be the best solution here
202
- if (!hmrHash) {
203
- // TODO: we should only refresh the module that was updated
204
- return valApi
205
- .getModules({
206
- patch: true,
207
- includeSchema: true,
208
- includeSource: true,
209
- })
210
- .then((res) => {
211
- if (result.isOk(res)) {
212
- setModules(res.value.modules);
213
- } else {
214
- setError("Could not load modules: " + res.error.message);
215
- console.error(res.error);
216
- }
217
- });
218
- }
219
- }
220
- })
221
- .catch((e) => {
222
- console.error(e);
223
- });
224
- },
225
- []
226
- );
227
-
228
- return (
229
- <ValOverlayContext.Provider
230
- value={{
231
- theme,
232
- setTheme,
233
- api: valApi,
234
- editMode: "full",
235
- session: { status: "not-asked" },
236
- highlight: false,
237
- setHighlight: () => {
238
- //
239
- },
240
- setEditMode: () => {
241
- //
242
- },
243
- setWindowSize: () => {
244
- //
245
- },
246
- }}
247
- >
248
- <div
249
- id="val-fullscreen-container"
250
- className="relative font-serif antialiased"
251
- data-mode={theme}
252
- >
253
- <div className="fixed -translate-x-1/2 z-overlay left-1/2 bottom-4">
254
- <ValMenu api={valApi} />
255
- </div>
256
- <div id="val-fullscreen-hover" ref={hoverElemRef}></div>
257
- <ValFullscreenHoverContext.Provider
258
- value={{
259
- hoverElem: hoverElemRef?.current,
260
- }}
261
- >
262
- <div className="text-primary bg-background">
263
- <Grid>
264
- <div className="px-4 h-[50px] flex items-center justify-center">
265
- <Logo />
266
- </div>
267
- <ScrollArea className="px-4">
268
- {modules ? (
269
- <PathTree
270
- paths={Object.keys(modules)}
271
- setSelectedModuleId={(path) => {
272
- navigate(path);
273
- }}
274
- />
275
- ) : (
276
- !error && <div className="py-4">Loading...</div>
277
- )}
278
- </ScrollArea>
279
- <div className="flex items-center justify-start w-full h-[50px] gap-2 font-serif text-xs">
280
- <button
281
- onClick={() => {
282
- history.back();
283
- }}
284
- >
285
- <ChevronLeft />
286
- </button>
287
- <p>{selectedPath || "/"}</p>
288
- </div>
289
- <div className="p-4">
290
- {error && (
291
- <div className="text-lg text-destructive-foreground">
292
- ERROR: {error}
293
- </div>
294
- )}
295
- {modules &&
296
- selectedPath &&
297
- selectedModuleId &&
298
- moduleSource !== undefined &&
299
- moduleSchema !== undefined && (
300
- <ValModulesContext.Provider value={modules}>
301
- <ValModule
302
- path={selectedPath}
303
- source={moduleSource}
304
- schema={moduleSchema}
305
- setSelectedPath={setSelectedPath}
306
- initOnSubmit={initOnSubmit}
307
- />
308
- </ValModulesContext.Provider>
309
- )}
310
- </div>
311
- </Grid>
312
- </div>
313
- </ValFullscreenHoverContext.Provider>
314
- </div>
315
- </ValOverlayContext.Provider>
316
- );
317
- };
318
-
319
- const ValFullscreenHoverContext = React.createContext<{
320
- hoverElem: HTMLElement | null;
321
- }>({
322
- hoverElem: null,
323
- });
324
-
325
- const useValFullscreenHover = () => {
326
- return React.useContext(ValFullscreenHoverContext);
327
- };
328
-
329
- function ValModule({
330
- path,
331
- source: moduleSource,
332
- schema: moduleSchema,
333
- setSelectedPath,
334
- initOnSubmit,
335
- }: {
336
- path: SourcePath | ModuleId;
337
- source: Json;
338
- schema: SerializedSchema;
339
- setSelectedPath: (path: SourcePath | ModuleId) => void;
340
- initOnSubmit: InitOnSubmit;
341
- }): React.ReactElement {
342
- const [, modulePath] = Internal.splitModuleIdAndModulePath(
343
- path as SourcePath
344
- );
345
- const resolvedPath = Internal.resolvePath(
346
- modulePath,
347
- moduleSource,
348
- moduleSchema
349
- );
350
- if (!resolvedPath) {
351
- throw Error("Could not resolve module: " + path);
352
- }
353
- return (
354
- <AnyVal
355
- path={path as SourcePath}
356
- source={resolvedPath.source}
357
- schema={resolvedPath.schema as SerializedSchema}
358
- setSelectedPath={setSelectedPath}
359
- initOnSubmit={initOnSubmit}
360
- top
361
- />
362
- );
363
- }
364
-
365
- function AnyVal({
366
- path,
367
- source,
368
- schema,
369
- setSelectedPath,
370
- field,
371
- initOnSubmit,
372
- top,
373
- }: {
374
- path: SourcePath;
375
- source: Json;
376
- schema: SerializedSchema;
377
- setSelectedPath: (path: SourcePath | ModuleId) => void;
378
- field?: string;
379
- initOnSubmit: InitOnSubmit;
380
- top?: boolean;
381
- }): React.ReactElement {
382
- if (source === null || schema.opt) {
383
- return (
384
- <ValOptional
385
- path={path}
386
- source={source}
387
- schema={schema}
388
- field={field}
389
- initOnSubmit={initOnSubmit}
390
- setSelectedPath={setSelectedPath}
391
- />
392
- );
393
- }
394
- if (schema.type === "object") {
395
- if (typeof source !== "object" || isJsonArray(source)) {
396
- return <div>ERROR: expected object, but found {typeof source}</div>;
397
- }
398
- return (
399
- <ValObject
400
- source={source}
401
- path={path}
402
- schema={schema}
403
- initOnSubmit={initOnSubmit}
404
- setSelectedPath={setSelectedPath}
405
- top={top}
406
- />
407
- );
408
- } else if (schema.type === "array") {
409
- if (typeof source !== "object" || !isJsonArray(source)) {
410
- return <div>ERROR: expected array, but found {typeof source}</div>;
411
- }
412
- if (field) {
413
- <div>
414
- <div className="text-left">{field || path}</div>
415
- <ValList
416
- source={source}
417
- path={path}
418
- schema={schema}
419
- setSelectedPath={setSelectedPath}
420
- />
421
- </div>;
422
- }
423
- return (
424
- <ValList
425
- source={source}
426
- path={path}
427
- schema={schema}
428
- setSelectedPath={setSelectedPath}
429
- />
430
- );
431
- } else if (schema.type === "record") {
432
- if (typeof source !== "object") {
433
- return (
434
- <div>
435
- ERROR: expected object for {schema.type}, but found {typeof source}
436
- </div>
437
- );
438
- }
439
- if (isJsonArray(source)) {
440
- return <div>ERROR: did not expect array for {schema.type}</div>;
441
- }
442
- if (field) {
443
- <div>
444
- <div className="text-left">{field || path}</div>
445
- <ValRecord
446
- source={source}
447
- path={path}
448
- schema={schema}
449
- setSelectedPath={setSelectedPath}
450
- />
451
- </div>;
452
- }
453
- return (
454
- <ValRecord
455
- source={source}
456
- path={path}
457
- schema={schema}
458
- setSelectedPath={setSelectedPath}
459
- />
460
- );
461
- }
462
-
463
- return (
464
- <div className="py-2 gap-y-4">
465
- <div className="text-left">{field || path}</div>
466
- <ValFormField
467
- path={path}
468
- disabled={false}
469
- source={source}
470
- schema={schema}
471
- onSubmit={initOnSubmit(path)}
472
- />
473
- </div>
474
- );
475
- }
476
-
477
- function ValObject({
478
- path,
479
- source,
480
- schema,
481
- setSelectedPath,
482
- initOnSubmit,
483
- top,
484
- }: {
485
- source: JsonObject;
486
- path: SourcePath;
487
- schema: SerializedObjectSchema;
488
- setSelectedPath: (path: SourcePath | ModuleId) => void;
489
- initOnSubmit: InitOnSubmit;
490
- top?: boolean;
491
- }): React.ReactElement {
492
- return (
493
- <div
494
- key={path}
495
- className={classNames("flex flex-col gap-y-8", {
496
- "border-l-2 border-border pl-6": !top,
497
- })}
498
- >
499
- {Object.entries(schema.items).map(([key, property]) => {
500
- const subPath = createValPathOfItem(path, key);
501
- return (
502
- <AnyVal
503
- key={subPath}
504
- path={subPath}
505
- source={source[key]}
506
- schema={property}
507
- setSelectedPath={setSelectedPath}
508
- field={key}
509
- initOnSubmit={initOnSubmit}
510
- />
511
- );
512
- })}
513
- </div>
514
- );
515
- }
516
- function ValRecord({
517
- path,
518
- source,
519
- schema,
520
- setSelectedPath,
521
- }: {
522
- source: JsonObject;
523
- path: SourcePath;
524
- schema: SerializedRecordSchema;
525
- setSelectedPath: (path: SourcePath | ModuleId) => void;
526
- }): React.ReactElement {
527
- const navigate = useNavigate();
528
- return (
529
- <div key={path} className="flex flex-col gap-4 p-2">
530
- {Object.entries(source).map(([key, item]) => {
531
- const subPath = createValPathOfItem(path, key);
532
- return (
533
- <button
534
- key={subPath}
535
- onClick={() => {
536
- setSelectedPath(subPath);
537
- navigate(subPath);
538
- }}
539
- >
540
- <ValRecordItem
541
- recordKey={key}
542
- path={subPath}
543
- source={item}
544
- schema={schema.item}
545
- />
546
- </button>
547
- );
548
- })}
549
- </div>
550
- );
551
- }
552
-
553
- const RECORD_ITEM_MAX_HEIGHT = 170;
554
- function ValRecordItem({
555
- recordKey,
556
- path,
557
- source,
558
- schema,
559
- }: {
560
- recordKey: string;
561
- source: Json | null;
562
- path: SourcePath;
563
- schema: SerializedSchema;
564
- }): React.ReactElement {
565
- const ref = React.useRef<HTMLDivElement>(null);
566
- const [isTruncated, setIsTruncated] = useState<boolean>(false);
567
- useEffect(() => {
568
- if (ref.current) {
569
- const height = ref.current.getBoundingClientRect().height;
570
- if (height >= RECORD_ITEM_MAX_HEIGHT) {
571
- setIsTruncated(true);
572
- }
573
- }
574
- }, []);
575
- return (
576
- <Card
577
- key={path}
578
- ref={ref}
579
- className="relative px-4 pt-2 pb-4 overflow-hidden border gap-y-2"
580
- style={{
581
- maxHeight: RECORD_ITEM_MAX_HEIGHT,
582
- }}
583
- >
584
- <div className="pb-4 font-serif text-left text-accent">{recordKey}</div>
585
- <div className="text-xs">
586
- <ValPreview path={path} source={source} schema={schema} />
587
- </div>
588
- {isTruncated && (
589
- <div className="absolute bottom-0 left-0 w-full h-[20px] bg-gradient-to-b from-transparent to-background"></div>
590
- )}
591
- </Card>
592
- );
593
- }
594
-
595
- function ValList({
596
- path,
597
- source,
598
- schema,
599
- setSelectedPath,
600
- }: {
601
- source: JsonArray;
602
- path: SourcePath;
603
- schema: SerializedArraySchema;
604
- setSelectedPath: (path: SourcePath | ModuleId) => void;
605
- }): React.ReactElement {
606
- const navigate = useNavigate();
607
- return (
608
- <div key={path} className="flex flex-col gap-4 p-2">
609
- {source.map((item, index) => {
610
- const subPath = createValPathOfItem(path, index);
611
- return (
612
- <button
613
- key={subPath}
614
- onClick={() => {
615
- setSelectedPath(subPath);
616
- navigate(subPath);
617
- }}
618
- >
619
- <ValListItem
620
- index={index}
621
- key={subPath}
622
- path={subPath}
623
- source={item}
624
- schema={schema.item}
625
- />
626
- </button>
627
- );
628
- })}
629
- </div>
630
- );
631
- }
632
-
633
- const LIST_ITEM_MAX_HEIGHT = RECORD_ITEM_MAX_HEIGHT;
634
- function ValListItem({
635
- index,
636
- path,
637
- source,
638
- schema,
639
- }: {
640
- index: number;
641
- source: Json | null;
642
- path: SourcePath;
643
- schema: SerializedSchema;
644
- }): React.ReactElement {
645
- const ref = React.useRef<HTMLDivElement>(null);
646
- const [isTruncated, setIsTruncated] = useState<boolean>(false);
647
- useEffect(() => {
648
- if (ref.current) {
649
- const height = ref.current.getBoundingClientRect().height;
650
- if (height >= LIST_ITEM_MAX_HEIGHT) {
651
- setIsTruncated(true);
652
- }
653
- }
654
- }, []);
655
- return (
656
- <Card
657
- ref={ref}
658
- className="relative px-4 pt-2 pb-4 overflow-hidden border gap-y-2"
659
- style={{
660
- maxHeight: LIST_ITEM_MAX_HEIGHT,
661
- }}
662
- >
663
- <div className="pb-4 font-serif text-left uppercase text-accent">
664
- {index + 1 < 10 ? `0${index + 1}` : index + 1}
665
- </div>
666
- <div className="text-xs">
667
- <ValPreview path={path} source={source} schema={schema} />
668
- </div>
669
- {isTruncated && (
670
- <div className="absolute bottom-0 left-0 w-full h-[20px] bg-gradient-to-b from-transparent to-background"></div>
671
- )}
672
- </Card>
673
- );
674
- }
675
-
676
- function createValPathOfItem(
677
- arrayPath: SourcePath | undefined,
678
- prop: string | number | symbol
679
- ) {
680
- const val = Internal.createValPathOfItem(arrayPath, prop);
681
- if (!val) {
682
- // Should never happen
683
- throw Error(
684
- `Could not create val path: ${arrayPath} of ${prop?.toString()}`
685
- );
686
- }
687
- return val;
688
- }
689
-
690
- function ValPreview({
691
- path,
692
- source,
693
- schema,
694
- }: {
695
- source: Json | null;
696
- path: SourcePath;
697
- schema: SerializedSchema;
698
- }): React.ReactElement {
699
- const [isMouseOver, setIsMouseOver] = useState<{
700
- x: number;
701
- y: number;
702
- } | null>(null);
703
- const { hoverElem } = useValFullscreenHover();
704
-
705
- if (schema.type === "object") {
706
- return (
707
- <div
708
- key={path}
709
- className="grid grid-cols-[min-content_1fr] gap-2 text-left"
710
- >
711
- {Object.entries(schema.items).map(([key]) => {
712
- return (
713
- <Fragment key={createValPathOfItem(path, key)}>
714
- <span className="text-muted">{key}:</span>
715
- <span>
716
- <ValPreview
717
- source={(source as JsonObject | null)?.[key] ?? null}
718
- schema={schema.items[key]}
719
- path={createValPathOfItem(path, key)}
720
- />
721
- </span>
722
- </Fragment>
723
- );
724
- })}
725
- </div>
726
- );
727
- } else if (schema.type === "array") {
728
- if (source === null) {
729
- return (
730
- <span key={path} className="text-accent">
731
- Empty
732
- </span>
733
- );
734
- }
735
- if (Array.isArray(source)) {
736
- return (
737
- <span key={path}>
738
- <span className="text-accent">{source.length}</span>
739
- <span>{source.length === 1 ? " item" : " items"}</span>
740
- </span>
741
- );
742
- }
743
- return (
744
- <span
745
- key={path}
746
- className="px-2 bg-destructive text-destructive-foreground"
747
- >
748
- Unknown length
749
- </span>
750
- );
751
- } else if (schema.type === "richtext") {
752
- if (source === null) {
753
- return (
754
- <span key={path} className="text-accent">
755
- Empty
756
- </span>
757
- );
758
- }
759
- if (typeof source !== "object") {
760
- return (
761
- <div
762
- key={path}
763
- className="p-4 text-destructive-foreground bg-destructive"
764
- >
765
- ERROR: {typeof source} not an object
766
- </div>
767
- );
768
- }
769
- if (!(VAL_EXTENSION in source) || source[VAL_EXTENSION] !== "richtext") {
770
- return (
771
- <div
772
- key={path}
773
- className="p-4 text-destructive-foreground bg-destructive"
774
- >
775
- ERROR: object is not richtext
776
- </div>
777
- );
778
- }
779
- return (
780
- <ValRichText key={path}>
781
- {parseRichTextSource(source as RichTextSource<AnyRichTextOptions>)}
782
- </ValRichText>
783
- );
784
- } else if (schema.type === "string") {
785
- if (source === null) {
786
- return (
787
- <span key={path} className="text-accent">
788
- Empty
789
- </span>
790
- );
791
- }
792
- return <span>{source as string}</span>;
793
- } else if (schema.type === "image") {
794
- if (source === null) {
795
- return (
796
- <span key={path} className="text-accent">
797
- Empty
798
- </span>
799
- );
800
- }
801
- if (typeof source !== "object") {
802
- return (
803
- <div
804
- key={path}
805
- className="p-4 text-destructive-foreground bg-destructive"
806
- >
807
- ERROR: not an object
808
- </div>
809
- );
810
- }
811
- if (
812
- !(FILE_REF_PROP in source) ||
813
- typeof source[FILE_REF_PROP] !== "string"
814
- ) {
815
- return (
816
- <div
817
- key={path}
818
- className="p-4 text-destructive-foreground bg-destructive"
819
- >
820
- ERROR: object is not an image
821
- </div>
822
- );
823
- }
824
- const url = Internal.convertFileSource(
825
- source as FileSource<ImageMetadata>
826
- ).url;
827
- return (
828
- <span
829
- key={path}
830
- onMouseOver={(ev) => {
831
- setIsMouseOver({
832
- x: ev.clientX,
833
- y: ev.clientY,
834
- });
835
- }}
836
- onMouseLeave={() => {
837
- setIsMouseOver(null);
838
- }}
839
- className="relative flex items-center justify-start gap-1"
840
- >
841
- <a href={url} className="overflow-hidden underline truncate ">
842
- {source[FILE_REF_PROP]}
843
- </a>
844
- {isMouseOver &&
845
- hoverElem &&
846
- createPortal(
847
- <img
848
- className="absolute z-[5] max-w-[10vw]"
849
- style={{
850
- left: isMouseOver.x + 10,
851
- top: isMouseOver.y + 10,
852
- }}
853
- src={url}
854
- ></img>,
855
- hoverElem
856
- )}
857
- </span>
858
- );
859
- } else if (schema.type === "boolean") {
860
- if (source === null) {
861
- return (
862
- <span key={path} className="text-accent">
863
- Empty
864
- </span>
865
- );
866
- }
867
- return (
868
- <span key={path} className="text-accent">
869
- {source ? "true" : "false"}
870
- </span>
871
- );
872
- } else if (schema.type === "number") {
873
- if (source === null) {
874
- return (
875
- <span key={path} className="text-accent">
876
- Empty
877
- </span>
878
- );
879
- }
880
- return <span className="text-accent">{source.toString()}</span>;
881
- } else if (schema.type === "keyOf") {
882
- if (source === null) {
883
- return (
884
- <span key={path} className="text-accent">
885
- Empty
886
- </span>
887
- );
888
- }
889
- return (
890
- <span key={path} className="text-accent">
891
- {source.toString()}
892
- </span>
893
- );
894
- }
895
-
896
- return <div key={path}>TODO: {schema.type}</div>;
897
- }
898
-
899
- function ValOptional({
900
- path,
901
- source,
902
- schema,
903
- setSelectedPath,
904
- initOnSubmit,
905
- field,
906
- }: {
907
- path: SourcePath;
908
- source: Json;
909
- schema: SerializedSchema;
910
- setSelectedPath: (path: SourcePath | ModuleId) => void;
911
- initOnSubmit: InitOnSubmit;
912
- field?: string;
913
- }) {
914
- const [enable, setEnable] = useState<boolean>(source !== null);
915
-
916
- return (
917
- <div className="flex flex-col gap-y-6" key={path}>
918
- {field ? (
919
- <div className="flex items-center justify-start gap-x-4">
920
- <Switch
921
- checked={enable}
922
- onClick={() => {
923
- setEnable((prev) => !prev);
924
- }}
925
- />
926
- <span>{field}</span>
927
- </div>
928
- ) : (
929
- <Switch
930
- checked={enable}
931
- onClick={() => {
932
- setEnable((prev) => !prev);
933
- }}
934
- />
935
- )}
936
- {enable && (
937
- <ValDefaultOf
938
- source={source}
939
- schema={schema}
940
- path={path}
941
- setSelectedPath={setSelectedPath}
942
- initOnSubmit={initOnSubmit}
943
- />
944
- )}
945
- </div>
946
- );
947
- }
948
-
949
- function ValDefaultOf({
950
- source,
951
- path,
952
- schema,
953
- setSelectedPath,
954
- initOnSubmit,
955
- }: {
956
- source: Json;
957
- path: SourcePath;
958
- schema: SerializedSchema;
959
- setSelectedPath: (path: SourcePath | ModuleId) => void;
960
- initOnSubmit: InitOnSubmit;
961
- }): React.ReactElement {
962
- if (schema.type === "array") {
963
- if (
964
- typeof source === "object" &&
965
- (source === null || isJsonArray(source))
966
- ) {
967
- return (
968
- <ValList
969
- source={source === null ? [] : source}
970
- path={path}
971
- schema={schema}
972
- setSelectedPath={setSelectedPath}
973
- />
974
- );
975
- }
976
- } else if (schema.type === "object") {
977
- if (
978
- typeof source === "object" &&
979
- (source === null || !isJsonArray(source))
980
- ) {
981
- return (
982
- <ValObject
983
- source={source as JsonObject}
984
- path={path}
985
- schema={schema}
986
- setSelectedPath={setSelectedPath}
987
- initOnSubmit={initOnSubmit}
988
- />
989
- );
990
- }
991
- } else if (
992
- schema.type === "richtext" ||
993
- schema.type === "string" ||
994
- schema.type === "image" ||
995
- schema.type === "number" ||
996
- schema.type === "keyOf"
997
- ) {
998
- return (
999
- <ValFormField
1000
- key={path}
1001
- path={path}
1002
- disabled={false}
1003
- source={source}
1004
- schema={schema}
1005
- onSubmit={initOnSubmit(path)}
1006
- />
1007
- );
1008
- }
1009
-
1010
- return (
1011
- <div className="p-4 bg-destructive text-destructive-foreground">
1012
- ERROR: unexpected source type {typeof source} for schema type{" "}
1013
- {schema.type}
1014
- </div>
1015
- );
1016
- }
1017
-
1018
- function isJsonArray(source: JsonArray | JsonObject): source is JsonArray {
1019
- return Array.isArray(source);
1020
- }
1021
-
1022
- type Tree = {
1023
- [key: string]: Tree;
1024
- };
1025
- function pathsToTree(paths: string[]): Tree {
1026
- const tree: Tree = {};
1027
- paths.forEach((path) => {
1028
- const parts = path.split("/").filter((part) => part !== "");
1029
- let current = tree;
1030
- parts.forEach((part) => {
1031
- if (!current[part]) {
1032
- current[part] = {};
1033
- }
1034
- current = current[part] as Tree;
1035
- });
1036
- });
1037
- return tree;
1038
- }
1039
-
1040
- function PathTree({
1041
- paths,
1042
- setSelectedModuleId,
1043
- }: {
1044
- paths: string[];
1045
- setSelectedModuleId: (path: ModuleId | SourcePath) => void;
1046
- }): React.ReactElement {
1047
- const tree = pathsToTree(paths);
1048
- return (
1049
- <Tree>
1050
- {Object.entries(tree).map(([name, subTree]) => (
1051
- <div className="px-4 py-2" key={`/${name}`}>
1052
- <PathNode
1053
- name={name}
1054
- tree={subTree}
1055
- moduleId={`/${name}` as ModuleId}
1056
- setSelectedModuleId={setSelectedModuleId}
1057
- />
1058
- </div>
1059
- ))}
1060
- </Tree>
1061
- );
1062
- }
1063
-
1064
- function PathNode({
1065
- name,
1066
- tree,
1067
- moduleId,
1068
- setSelectedModuleId,
1069
- }: {
1070
- name: string;
1071
- tree: Tree;
1072
- moduleId: ModuleId;
1073
- setSelectedModuleId: (moduleId: ModuleId | SourcePath) => void;
1074
- }): React.ReactElement {
1075
- return (
1076
- <div>
1077
- <button
1078
- onClick={() => {
1079
- setSelectedModuleId(moduleId);
1080
- }}
1081
- >
1082
- {name}
1083
- </button>
1084
- {Object.entries(tree).map(([childName, childTree]) => (
1085
- <div className="px-4 py-1" key={`${moduleId}/${childName}` as ModuleId}>
1086
- <PathNode
1087
- name={childName}
1088
- tree={childTree}
1089
- moduleId={`${moduleId}/${childName}` as ModuleId}
1090
- setSelectedModuleId={setSelectedModuleId}
1091
- />
1092
- </div>
1093
- ))}
1094
- </div>
1095
- );
1096
- }
1097
-
1098
- const theme: { tags: Record<string, string>; classes: Record<string, string> } =
1099
- {
1100
- tags: {
1101
- h1: "font-bold",
1102
- h2: "font-bold",
1103
- h3: "font-bold",
1104
- h4: "font-bold",
1105
- h5: "font-bold",
1106
- h6: "font-bold",
1107
- p: "",
1108
- },
1109
- classes: {
1110
- bold: "font-bold",
1111
- italic: "italic",
1112
- lineThrough: "line-through",
1113
- },
1114
- };
1115
-
1116
- export function ValRichText({
1117
- children,
1118
- }: {
1119
- children: RichText<AnyRichTextOptions>;
1120
- }) {
1121
- const root = children as RichText<AnyRichTextOptions> & {
1122
- valPath: SourcePath;
1123
- };
1124
- function withRenderTag(clazz: string, current?: string) {
1125
- const renderClass = theme.tags[clazz];
1126
- if (renderClass && current) {
1127
- return [current, renderClass].join(" ");
1128
- }
1129
- if (renderClass) {
1130
- return renderClass;
1131
- }
1132
- return current;
1133
- }
1134
- function withRenderClass(clazz: string, current?: string) {
1135
- const renderClass = theme.classes[clazz];
1136
- if (renderClass && current) {
1137
- return [current, renderClass].join(" ");
1138
- }
1139
- if (renderClass) {
1140
- return renderClass;
1141
- }
1142
- return current;
1143
- }
1144
-
1145
- function toReact(
1146
- node: RichTextNode<AnyRichTextOptions>,
1147
- key: number | string
1148
- ): React.ReactNode {
1149
- if (typeof node === "string") {
1150
- return node;
1151
- }
1152
- if (node.tag === "p") {
1153
- return (
1154
- <p className={withRenderTag("p")} key={key}>
1155
- {node.children.map((child, key) => toReact(child, key))}
1156
- </p>
1157
- );
1158
- }
1159
- if (node.tag === "img") {
1160
- return <img className={withRenderTag("img")} key={key} src={node.src} />;
1161
- }
1162
- if (node.tag === "ul") {
1163
- return (
1164
- <ul className={withRenderTag("ul")} key={key}>
1165
- {node.children.map((child, key) => toReact(child, key))}
1166
- </ul>
1167
- );
1168
- }
1169
- if (node.tag === "ol") {
1170
- return (
1171
- <ol className={withRenderTag("ol")} key={key}>
1172
- {node.children.map((child, key) => toReact(child, key))}
1173
- </ol>
1174
- );
1175
- }
1176
- if (node.tag === "li") {
1177
- return (
1178
- <li className={withRenderTag("li")} key={key}>
1179
- {node.children.map((child, key) => toReact(child, key))}
1180
- </li>
1181
- );
1182
- }
1183
- if (node.tag === "span") {
1184
- return (
1185
- <span
1186
- key={key}
1187
- className={node.classes
1188
- .map((nodeClass) => {
1189
- switch (nodeClass) {
1190
- case "bold":
1191
- return withRenderClass("bold");
1192
- case "line-through":
1193
- return withRenderClass("lineThrough");
1194
- case "italic":
1195
- return withRenderClass("italic");
1196
- }
1197
- })
1198
- .join(" ")}
1199
- >
1200
- {node.children.map((child, key) => toReact(child, key))}
1201
- </span>
1202
- );
1203
- }
1204
- if (node.tag === "h1") {
1205
- return (
1206
- <h1 className={withRenderTag("h1")} key={key}>
1207
- {node.children.map((child, key) => toReact(child, key))}
1208
- </h1>
1209
- );
1210
- }
1211
- if (node.tag === "h2") {
1212
- return (
1213
- <h2 className={withRenderTag("h2")} key={key}>
1214
- {node.children.map((child, key) => toReact(child, key))}
1215
- </h2>
1216
- );
1217
- }
1218
- if (node.tag === "h3") {
1219
- return (
1220
- <h3 className={withRenderTag("h3")} key={key}>
1221
- {node.children.map((child, key) => toReact(child, key))}
1222
- </h3>
1223
- );
1224
- }
1225
- if (node.tag === "h4") {
1226
- return (
1227
- <h4 className={withRenderTag("h4")} key={key}>
1228
- {node.children.map((child, key) => toReact(child, key))}
1229
- </h4>
1230
- );
1231
- }
1232
- if (node.tag === "h5") {
1233
- return (
1234
- <h5 className={withRenderTag("h5")} key={key}>
1235
- {node.children.map((child, key) => toReact(child, key))}
1236
- </h5>
1237
- );
1238
- }
1239
- if (node.tag === "h6") {
1240
- return (
1241
- <h6 className={withRenderTag("h6")} key={key}>
1242
- {node.children.map((child, key) => toReact(child, key))}
1243
- </h6>
1244
- );
1245
- }
1246
-
1247
- if (node.tag === "br") {
1248
- return <br key={key} />;
1249
- }
1250
- if (node.tag === "a") {
1251
- return (
1252
- <a href={node.href} key={key}>
1253
- {node.children.map((child, key) => toReact(child, key))}
1254
- </a>
1255
- );
1256
- }
1257
- console.error("Unknown tag", node.tag);
1258
- const _exhaustiveCheck: never = node.tag;
1259
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1260
- const anyNode = _exhaustiveCheck as any;
1261
- if (!anyNode?.tag) {
1262
- return null;
1263
- }
1264
- return React.createElement(anyNode.tag, {
1265
- key,
1266
- className: anyNode.class?.join(" "),
1267
- children: anyNode.children?.map(toReact),
1268
- });
1269
- }
1270
-
1271
- return (
1272
- <span data-val-path={root.valPath}>
1273
- {root.children.map((child, i) => {
1274
- return toReact(child, i);
1275
- })}
1276
- </span>
1277
- );
1278
- }