@valbuild/ui 0.13.4 → 0.17.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 (46) hide show
  1. package/dist/valbuild-ui.cjs.d.ts +13 -18
  2. package/dist/valbuild-ui.cjs.js +7624 -1718
  3. package/dist/valbuild-ui.esm.js +7625 -1719
  4. package/package.json +5 -2
  5. package/src/assets/icons/ImageIcon.tsx +15 -7
  6. package/src/assets/icons/Section.tsx +41 -0
  7. package/src/assets/icons/TextIcon.tsx +20 -0
  8. package/src/components/Button.tsx +18 -7
  9. package/src/components/DraggableList.stories.tsx +20 -0
  10. package/src/components/DraggableList.tsx +95 -0
  11. package/src/components/Dropdown.tsx +2 -0
  12. package/src/components/ExpandLogo.tsx +72 -0
  13. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +1 -16
  14. package/src/components/RichTextEditor/RichTextEditor.tsx +2 -2
  15. package/src/components/User.tsx +17 -0
  16. package/src/components/ValMenu.tsx +40 -0
  17. package/src/components/ValOverlay.tsx +513 -29
  18. package/src/components/ValOverlayContext.tsx +63 -0
  19. package/src/components/ValWindow.stories.tsx +3 -3
  20. package/src/components/ValWindow.tsx +26 -18
  21. package/src/components/dashboard/DashboardButton.tsx +25 -0
  22. package/src/components/dashboard/DashboardDropdown.tsx +59 -0
  23. package/src/components/dashboard/Dropdown.stories.tsx +11 -0
  24. package/src/components/dashboard/Dropdown.tsx +70 -0
  25. package/src/components/dashboard/FormGroup.stories.tsx +37 -0
  26. package/src/components/dashboard/FormGroup.tsx +36 -0
  27. package/src/components/dashboard/Grid.stories.tsx +52 -0
  28. package/src/components/dashboard/Grid.tsx +126 -0
  29. package/src/components/dashboard/Grid2.stories.tsx +56 -0
  30. package/src/components/dashboard/Grid2.tsx +72 -0
  31. package/src/components/dashboard/Tree.stories.tsx +91 -0
  32. package/src/components/dashboard/Tree.tsx +72 -0
  33. package/src/components/dashboard/ValDashboard.tsx +148 -0
  34. package/src/components/dashboard/ValDashboardEditor.tsx +269 -0
  35. package/src/components/dashboard/ValDashboardGrid.tsx +142 -0
  36. package/src/components/dashboard/ValTreeNavigator.tsx +253 -0
  37. package/src/components/forms/Form.tsx +2 -2
  38. package/src/components/forms/{TextForm.tsx → TextArea.tsx} +5 -3
  39. package/src/dto/SerializedSchema.ts +69 -0
  40. package/src/dto/Session.ts +12 -0
  41. package/src/dto/SessionMode.ts +5 -0
  42. package/src/dto/Tree.ts +18 -0
  43. package/src/exports.ts +1 -0
  44. package/src/utils/Remote.ts +15 -0
  45. package/src/utils/resolvePath.ts +33 -0
  46. package/tailwind.config.js +20 -1
@@ -1,41 +1,525 @@
1
- import { EditButton } from "./EditButton";
2
- import { Form, FormProps } from "./forms/Form";
1
+ import {
2
+ Dispatch,
3
+ SetStateAction,
4
+ useCallback,
5
+ useEffect,
6
+ useState,
7
+ } from "react";
8
+ import { Session } from "../dto/Session";
9
+ import { ValMenu } from "./ValMenu";
10
+ import { EditMode, Theme, ValOverlayContext } from "./ValOverlayContext";
11
+ import { Remote } from "../utils/Remote";
3
12
  import { ValWindow } from "./ValWindow";
13
+ import { result } from "@valbuild/core/fp";
14
+ import { TextArea } from "./forms/TextArea";
15
+ import { Internal, SerializedSchema, SourcePath } from "@valbuild/core";
16
+ import { Modules, resolvePath } from "../utils/resolvePath";
17
+ import { ValApi } from "@valbuild/core";
4
18
 
5
- type ValWindow = {
6
- position: {
7
- left: number;
8
- top: number;
19
+ export type ValOverlayProps = {
20
+ defaultTheme?: "dark" | "light";
21
+ api: ValApi;
22
+ };
23
+
24
+ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
25
+ const [theme, setTheme] = useTheme(defaultTheme);
26
+ const session = useSession(api);
27
+
28
+ const [editMode, setEditMode] = useInitEditMode();
29
+ const [hoverTarget, setHoverTarget] = useHoverTarget(editMode);
30
+ const [windowTarget, setWindowTarget] = useState<WindowTarget | null>(null);
31
+ const [highlight, setHighlight] = useState(false);
32
+ const { selectedSchema, selectedSource, error, loading } = useValModules(
33
+ api,
34
+ windowTarget?.path
35
+ );
36
+
37
+ return (
38
+ <ValOverlayContext.Provider
39
+ value={{
40
+ api,
41
+ theme,
42
+ session,
43
+ editMode,
44
+ setEditMode,
45
+ highlight,
46
+ setHighlight,
47
+ setTheme,
48
+ }}
49
+ >
50
+ <div data-mode={theme}>
51
+ <div className="fixed -translate-x-1/2 z-overlay left-1/2 bottom-4">
52
+ <ValMenu api={api} />
53
+ </div>
54
+ {editMode === "hover" && hoverTarget && (
55
+ <ValHover
56
+ hoverTarget={hoverTarget}
57
+ setHoverTarget={setHoverTarget}
58
+ setEditMode={setEditMode}
59
+ setWindowTarget={setWindowTarget}
60
+ />
61
+ )}
62
+ {editMode === "window" && windowTarget && (
63
+ <ValWindow
64
+ onClose={() => {
65
+ setWindowTarget(null);
66
+ setEditMode("hover");
67
+ }}
68
+ >
69
+ <div className="px-4 text-sm">
70
+ <WindowHeader
71
+ path={windowTarget.path}
72
+ type={selectedSchema?.type}
73
+ />
74
+ </div>
75
+ {loading && <div className="text-primary">Loading...</div>}
76
+ {error && <div className="text-red">{error}</div>}
77
+ {typeof selectedSource === "string" &&
78
+ selectedSchema?.type === "string" && (
79
+ <TextForm
80
+ api={api}
81
+ path={windowTarget.path}
82
+ defaultValue={selectedSource}
83
+ />
84
+ )}
85
+ </ValWindow>
86
+ )}
87
+ </div>
88
+ </ValOverlayContext.Provider>
89
+ );
90
+ }
91
+
92
+ function TextForm({
93
+ path,
94
+ defaultValue,
95
+ api,
96
+ }: {
97
+ path: SourcePath;
98
+ defaultValue?: string;
99
+ api: ValApi;
100
+ }) {
101
+ const [text, setText] = useState(defaultValue || "");
102
+ const [moduleId, modulePath] = Internal.splitModuleIdAndModulePath(path);
103
+ const [isPatching, setIsPatching] = useState(false);
104
+ return (
105
+ <form
106
+ className="flex flex-col justify-between h-full px-4"
107
+ onSubmit={(ev) => {
108
+ ev.preventDefault();
109
+ setIsPatching(true);
110
+ api
111
+ .postPatches(moduleId, [
112
+ {
113
+ op: "replace",
114
+ path: Internal.createPatchJSONPath(modulePath),
115
+ value: text,
116
+ },
117
+ ])
118
+ .finally(() => {
119
+ setIsPatching(false);
120
+ });
121
+ }}
122
+ >
123
+ <TextArea
124
+ name={path}
125
+ text={text}
126
+ disabled={isPatching}
127
+ onChange={setText}
128
+ />
129
+ <button
130
+ className="px-4 py-2 border border-highlight disabled:border-border"
131
+ disabled={isPatching}
132
+ >
133
+ Submit
134
+ </button>
135
+ </form>
136
+ );
137
+ }
138
+
139
+ function useValModules(api: ValApi, path: string | undefined) {
140
+ const [modules, setModules] = useState<Remote<Modules>>();
141
+ const moduleId =
142
+ path && Internal.splitModuleIdAndModulePath(path as SourcePath)[0];
143
+
144
+ useEffect(() => {
145
+ if (path) {
146
+ setModules({ status: "loading" });
147
+ api
148
+ .getModules({
149
+ patch: true,
150
+ includeSchema: true,
151
+ includeSource: true,
152
+ treePath: moduleId,
153
+ })
154
+ .then((res) => {
155
+ if (result.isOk(res)) {
156
+ setModules({ status: "success", data: res.value.modules });
157
+ } else {
158
+ console.error({ status: "error", error: res.error });
159
+ setModules({ status: "error", error: res.error.message });
160
+ }
161
+ });
162
+ }
163
+ }, [path]);
164
+ if (!path || modules?.status === "not-asked") {
165
+ return {
166
+ error: null,
167
+ selectedSource: undefined,
168
+ selectedSchema: undefined,
169
+ loading: false,
170
+ };
171
+ }
172
+ if (modules?.status === "loading") {
173
+ return {
174
+ error: null,
175
+ selectedSource: undefined,
176
+ selectedSchema: undefined,
177
+ loading: true,
178
+ };
179
+ }
180
+ if (modules?.status === "error") {
181
+ return {
182
+ error: modules.error,
183
+ selectedSource: undefined,
184
+ selectedSchema: undefined,
185
+ loading: false,
186
+ };
187
+ }
188
+ if (!modules?.data) {
189
+ return {
190
+ error: "No modules",
191
+ selectedSource: undefined,
192
+ selectedSchema: undefined,
193
+ loading: false,
194
+ };
195
+ }
196
+
197
+ const resolvedModulePath = resolvePath(path as SourcePath, modules.data);
198
+
199
+ const {
200
+ error,
201
+ source: selectedSource,
202
+ schema: selectedSchema,
203
+ } = resolvedModulePath && result.isOk(resolvedModulePath)
204
+ ? {
205
+ ...resolvedModulePath.value,
206
+ error: null,
207
+ }
208
+ : {
209
+ error:
210
+ resolvedModulePath && result.isErr(resolvedModulePath)
211
+ ? resolvedModulePath.error.message
212
+ : null,
213
+ source: undefined,
214
+ schema: undefined,
215
+ };
216
+ return {
217
+ error,
218
+ selectedSource,
219
+ selectedSchema,
220
+ loading: false,
9
221
  };
10
- } & FormProps;
222
+ }
11
223
 
12
- export type ValOverlayProps = {
13
- editMode: boolean;
14
- setEditMode: (editMode: boolean) => void;
15
- valWindow?: ValWindow;
16
- closeValWindow: () => void;
224
+ type WindowTarget = {
225
+ element?: HTMLElement | undefined;
226
+ mouse: { x: number; y: number };
227
+ path: SourcePath;
17
228
  };
18
229
 
19
- export function ValOverlay({
20
- editMode,
230
+ type HoverTarget = {
231
+ element?: HTMLElement | undefined;
232
+ path: SourcePath;
233
+ };
234
+ function ValHover({
235
+ hoverTarget,
21
236
  setEditMode,
22
- valWindow,
23
- closeValWindow,
24
- }: ValOverlayProps) {
237
+ setWindowTarget,
238
+ setHoverTarget,
239
+ }: {
240
+ hoverTarget: HoverTarget;
241
+ setEditMode: Dispatch<EditMode>;
242
+ setHoverTarget: Dispatch<HoverTarget | null>;
243
+ setWindowTarget: Dispatch<WindowTarget | null>;
244
+ }) {
245
+ const rect = hoverTarget.element?.getBoundingClientRect();
25
246
  return (
26
- <>
27
- <div className="fixed -translate-x-1/2 left-1/2 bottom-4">
28
- <EditButton
29
- onClick={() => {
30
- setEditMode(!editMode);
247
+ <div
248
+ id="val-hover"
249
+ className="fixed border-2 cursor-pointer z-overlay-hover border-base"
250
+ style={{
251
+ top: rect?.top,
252
+ left: rect?.left,
253
+ width: rect?.width,
254
+ height: rect?.height,
255
+ }}
256
+ onClick={(ev) => {
257
+ setWindowTarget({
258
+ ...hoverTarget,
259
+ mouse: { x: ev.pageX, y: ev.pageY },
260
+ });
261
+ setEditMode("window");
262
+ setHoverTarget(null);
263
+ }}
264
+ >
265
+ <div className="flex items-center justify-end w-full text-xs">
266
+ <div
267
+ className="flex items-center justify-center px-3 py-1 text-primary bg-base"
268
+ style={{
269
+ maxHeight: rect?.height && rect.height - 4,
270
+ fontSize:
271
+ rect?.height && rect.height <= 16 ? rect.height - 4 : undefined,
31
272
  }}
32
- />
273
+ >
274
+ Edit
275
+ </div>
33
276
  </div>
34
- {editMode && valWindow && (
35
- <ValWindow onClose={closeValWindow} position={valWindow.position}>
36
- <Form onSubmit={valWindow.onSubmit} inputs={valWindow.inputs} />
37
- </ValWindow>
38
- )}
39
- </>
277
+ </div>
278
+ );
279
+ }
280
+
281
+ // TODO: do something fun on highlight?
282
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
283
+ function useHighlight(
284
+ highlight: boolean,
285
+ setTarget: Dispatch<HoverTarget | null>
286
+ ) {
287
+ useEffect(() => {
288
+ if (highlight) {
289
+ const elements =
290
+ document.querySelectorAll<HTMLElement>("[data-val-path]");
291
+ let index = 0;
292
+ let timeout: NodeJS.Timeout | null = null;
293
+
294
+ const highlight = () => {
295
+ const element = elements[index];
296
+ const path = element.dataset.valPath as SourcePath;
297
+ if (path) {
298
+ setTarget({
299
+ path,
300
+ element,
301
+ });
302
+ }
303
+ index++;
304
+ if (index >= elements.length) {
305
+ index = 0;
306
+ }
307
+ timeout = setTimeout(highlight, 1000);
308
+ };
309
+ highlight();
310
+ return () => {
311
+ if (timeout) {
312
+ clearTimeout(timeout);
313
+ }
314
+ };
315
+ }
316
+ }, [highlight]);
317
+ }
318
+
319
+ const LOCAL_STORAGE_EDIT_MODE_KEY = "val-edit-mode";
320
+
321
+ function useInitEditMode() {
322
+ const [editMode, setEditModeRaw] = useState<EditMode>("off");
323
+ useEffect(() => {
324
+ try {
325
+ const storedEditMode = localStorage.getItem(LOCAL_STORAGE_EDIT_MODE_KEY);
326
+ if (
327
+ storedEditMode === "off" ||
328
+ storedEditMode === "hover" ||
329
+ storedEditMode === "window" ||
330
+ storedEditMode === "full"
331
+ ) {
332
+ setEditModeRaw(storedEditMode === "window" ? "hover" : storedEditMode);
333
+ } else {
334
+ localStorage.removeItem(LOCAL_STORAGE_EDIT_MODE_KEY);
335
+ setEditModeRaw("off");
336
+ }
337
+ } catch (err) {
338
+ setEditModeRaw("off");
339
+ }
340
+ }, []);
341
+
342
+ const setEditMode: Dispatch<SetStateAction<EditMode>> = useCallback((v) => {
343
+ if (typeof v === "function") {
344
+ setEditModeRaw((prev) => {
345
+ const next = v(prev);
346
+ localStorage.setItem(LOCAL_STORAGE_EDIT_MODE_KEY, next);
347
+ return next;
348
+ });
349
+ } else {
350
+ localStorage.setItem(LOCAL_STORAGE_EDIT_MODE_KEY, v);
351
+ setEditModeRaw(v);
352
+ }
353
+ }, []);
354
+ return [editMode, setEditMode] as const;
355
+ }
356
+
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
+ function useTheme(defaultTheme: Theme = "dark") {
403
+ const [theme, setTheme] = useState<Theme>(defaultTheme);
404
+
405
+ useEffect(() => {
406
+ if (localStorage.getItem("val-theme") === "light") {
407
+ setTheme("light");
408
+ } else if (localStorage.getItem("val-theme") === "dark") {
409
+ setTheme("dark");
410
+ } else if (
411
+ window.matchMedia &&
412
+ window.matchMedia("(prefers-color-scheme: dark)").matches
413
+ ) {
414
+ setTheme("dark");
415
+ } else if (
416
+ window.matchMedia &&
417
+ window.matchMedia("(prefers-color-scheme: light)").matches
418
+ ) {
419
+ setTheme("light");
420
+ }
421
+ const themeListener = (e: MediaQueryListEvent) => {
422
+ if (!localStorage.getItem("val-theme")) {
423
+ setTheme(e.matches ? "dark" : "light");
424
+ }
425
+ };
426
+ window
427
+ .matchMedia("(prefers-color-scheme: dark)")
428
+ .addEventListener("change", themeListener);
429
+ return () => {
430
+ window
431
+ .matchMedia("(prefers-color-scheme: dark)")
432
+ .removeEventListener("change", themeListener);
433
+ };
434
+ }, []);
435
+
436
+ return [
437
+ theme,
438
+ (theme: Theme) => {
439
+ localStorage.setItem("val-theme", theme);
440
+ setTheme(theme);
441
+ },
442
+ ] as const;
443
+ }
444
+
445
+ function useSession(api: ValApi) {
446
+ const [session, setSession] = useState<Remote<Session>>({
447
+ status: "not-asked",
448
+ });
449
+ const [sessionResetId, setSessionResetId] = useState(0);
450
+ useEffect(() => {
451
+ setSession({ status: "loading" });
452
+ api.getSession().then(async (res) => {
453
+ try {
454
+ if (result.isOk(res)) {
455
+ const session = res.value;
456
+ setSession({ status: "success", data: Session.parse(session) });
457
+ } else {
458
+ if (sessionResetId < 3) {
459
+ setTimeout(() => {
460
+ setSessionResetId(sessionResetId + 1);
461
+ }, 200 * sessionResetId);
462
+ } else {
463
+ setSession({ status: "error", error: "Could not fetch session" });
464
+ }
465
+ }
466
+ } catch (e) {
467
+ setSession({
468
+ status: "error",
469
+ error: "Got an error while trying to get session",
470
+ });
471
+ }
472
+ });
473
+ }, [sessionResetId]);
474
+ return session;
475
+ }
476
+
477
+ function WindowHeader({
478
+ path,
479
+ type,
480
+ }: {
481
+ path: SourcePath;
482
+ type?: SerializedSchema["type"];
483
+ }) {
484
+ const segments = path.split("/").slice(1);
485
+ return (
486
+ <span className="flex items-center justify-between">
487
+ <span>
488
+ <span className="pr-1 text-xs opacity-50">/</span>
489
+ {segments.map((segment, i) => {
490
+ if (i === segments.length - 1) {
491
+ return (
492
+ <span key={i} className="text-primary">
493
+ {segment.split(".").map((s, i) => {
494
+ let name = s;
495
+ if (i === 0) {
496
+ return (
497
+ <span key={i + "."}>
498
+ <span>{name}</span>
499
+ </span>
500
+ );
501
+ } else {
502
+ name = JSON.parse(s);
503
+ }
504
+ return (
505
+ <span key={i + "."}>
506
+ <span className="px-1 text-xs text-highlight">/</span>
507
+ <span>{name}</span>
508
+ </span>
509
+ );
510
+ })}
511
+ </span>
512
+ );
513
+ }
514
+ return (
515
+ <span key={i}>
516
+ <span>{segment}</span>
517
+ <span className="px-1 text-xs opacity-50">/</span>
518
+ </span>
519
+ );
520
+ })}
521
+ </span>
522
+ {type && <span className="ml-4">({type})</span>}
523
+ </span>
40
524
  );
41
525
  }
@@ -0,0 +1,63 @@
1
+ import React, { Dispatch, SetStateAction } from "react";
2
+ import type { Remote } from "../utils/Remote";
3
+ import type { Session } from "../dto/Session";
4
+ import { ValApi } from "@valbuild/core";
5
+
6
+ export type Theme = "dark" | "light";
7
+ export type EditMode = "off" | "hover" | "window" | "full";
8
+
9
+ export const ValOverlayContext = React.createContext<{
10
+ api: ValApi;
11
+ session: Remote<Session>;
12
+ editMode: EditMode;
13
+ highlight: boolean;
14
+ setHighlight: Dispatch<SetStateAction<boolean>>;
15
+ setEditMode: Dispatch<SetStateAction<EditMode>>;
16
+ theme: Theme;
17
+ setTheme: (theme: Theme) => void;
18
+ }>({
19
+ get api(): never {
20
+ throw Error(
21
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
22
+ );
23
+ },
24
+ get session(): never {
25
+ throw Error(
26
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
27
+ );
28
+ },
29
+ get theme(): never {
30
+ throw Error(
31
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
32
+ );
33
+ },
34
+ get setTheme(): never {
35
+ throw Error(
36
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
37
+ );
38
+ },
39
+ get editMode(): never {
40
+ throw Error(
41
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
42
+ );
43
+ },
44
+ get setEditMode(): never {
45
+ throw Error(
46
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
47
+ );
48
+ },
49
+ get highlight(): never {
50
+ throw Error(
51
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
52
+ );
53
+ },
54
+ get setHighlight(): never {
55
+ throw Error(
56
+ "ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
57
+ );
58
+ },
59
+ });
60
+
61
+ export function useValOverlayContext() {
62
+ return React.useContext(ValOverlayContext);
63
+ }
@@ -3,7 +3,7 @@ import { RichText as RichTextType, SourcePath } from "@valbuild/core";
3
3
  import { RichTextEditor } from "../exports";
4
4
  import { FormContainer } from "./forms/FormContainer";
5
5
  import { ImageForm } from "./forms/ImageForm";
6
- import { TextForm } from "./forms/TextForm";
6
+ import { TextArea } from "./forms/TextArea";
7
7
 
8
8
  import { ValWindow } from "./ValWindow";
9
9
 
@@ -31,7 +31,7 @@ export const ShortText: Story = {
31
31
  /* */
32
32
  }}
33
33
  >
34
- <TextForm
34
+ <TextArea
35
35
  name="/apps/blogs.0.title"
36
36
  text="Hva skjer'a, Bagera?"
37
37
  onChange={() => {
@@ -51,7 +51,7 @@ export const LongText: Story = {
51
51
  /* */
52
52
  }}
53
53
  >
54
- <TextForm
54
+ <TextArea
55
55
  name="/apps/blogs.0.title"
56
56
  text={EXAMPLE_TEXT}
57
57
  onChange={() => {