@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
@@ -1,23 +1,35 @@
1
- import { Edit2, Edit3, Moon, Power, Sun } from "react-feather";
2
1
  import { useValOverlayContext } from "./ValOverlayContext";
3
2
  import { ValApi } from "@valbuild/core";
3
+ import classNames from "classnames";
4
+ import {
5
+ Maximize,
6
+ Minimize,
7
+ Moon,
8
+ Pause,
9
+ Play,
10
+ Power,
11
+ Sun,
12
+ } from "lucide-react";
13
+ import React from "react";
14
+
15
+ const className = "p-1 border rounded-full shadow border-accent";
16
+ const PREV_URL_KEY = "valbuild:urlBeforeNavigation";
4
17
 
5
18
  export function ValMenu({ api }: { api: ValApi }) {
6
19
  const { theme, setTheme, editMode, setEditMode } = useValOverlayContext();
7
20
  return (
8
- <div className="flex flex-row items-center justify-center w-full h-full space-x-4 text-primary">
9
- <button
10
- className="p-1 border rounded-full shadow bg-base border-highlight"
21
+ <div className="flex flex-row items-center justify-center w-full h-full px-1 py-2 border-2 rounded-full gap-x-3 text-primary bg-background border-fill">
22
+ <MenuButton
23
+ active={editMode === "hover"}
11
24
  onClick={() => {
12
- setEditMode((prev) => (prev === "off" ? "hover" : "off"));
25
+ setEditMode((prev) => (prev === "hover" ? "off" : "hover"));
13
26
  }}
14
27
  >
15
28
  <div className="h-[24px] w-[24px] flex justify-center items-center">
16
- {editMode === "off" ? <Edit2 size={18} /> : <Edit3 size={18} />}
29
+ {editMode === "hover" ? <Pause size={18} /> : <Play size={18} />}
17
30
  </div>
18
- </button>
19
- <button
20
- className="p-1 border rounded-full shadow bg-base border-highlight"
31
+ </MenuButton>
32
+ <MenuButton
21
33
  onClick={() => {
22
34
  setTheme(theme === "dark" ? "light" : "dark");
23
35
  }}
@@ -26,11 +38,30 @@ export function ValMenu({ api }: { api: ValApi }) {
26
38
  {theme === "dark" && <Sun size={15} />}
27
39
  {theme === "light" && <Moon size={15} />}
28
40
  </div>
29
- </button>
30
- <a
31
- className="p-1 border rounded-full shadow bg-base border-highlight"
32
- href={api.getDisableUrl()}
41
+ </MenuButton>
42
+ <MenuButton
43
+ active={editMode === "full"}
44
+ onClick={() => {
45
+ // Save the current url so we can go back to it when returning from fullscreen mode
46
+ if (editMode !== "full") {
47
+ localStorage.setItem(PREV_URL_KEY, window.location.href);
48
+ window.location.href = api.getEditUrl();
49
+ } else if (editMode === "full") {
50
+ const prevUrl = localStorage.getItem(PREV_URL_KEY);
51
+ window.location.href = prevUrl || "/";
52
+ }
53
+ }}
33
54
  >
55
+ <div className="h-[24px] w-[24px] flex justify-center items-center">
56
+ {editMode === "full" ? (
57
+ <Minimize size={15} />
58
+ ) : (
59
+ <Maximize size={15} />
60
+ )}
61
+ </div>
62
+ </MenuButton>
63
+
64
+ <a className={className} href={api.getDisableUrl()}>
34
65
  <div className="h-[24px] w-[24px] flex justify-center items-center">
35
66
  <Power size={18} />
36
67
  </div>
@@ -38,3 +69,24 @@ export function ValMenu({ api }: { api: ValApi }) {
38
69
  </div>
39
70
  );
40
71
  }
72
+
73
+ function MenuButton({
74
+ active,
75
+ onClick,
76
+ children,
77
+ }: {
78
+ active?: boolean;
79
+ children: React.ReactNode;
80
+ onClick: () => void;
81
+ }) {
82
+ return (
83
+ <button
84
+ className={classNames(className, {
85
+ "bg-accent drop-shadow-[0px_0px_12px_rgba(56,205,152,0.60)]": active,
86
+ })}
87
+ onClick={onClick}
88
+ >
89
+ {children}
90
+ </button>
91
+ );
92
+ }
@@ -5,50 +5,30 @@ import {
5
5
  SetStateAction,
6
6
  useCallback,
7
7
  useEffect,
8
- useRef,
9
8
  useState,
10
9
  } from "react";
11
10
  import { Session } from "../dto/Session";
12
11
  import { ValMenu } from "./ValMenu";
13
- import {
14
- EditMode,
15
- Theme,
16
- ValOverlayContext,
17
- WindowSize,
18
- } from "./ValOverlayContext";
12
+ import { EditMode, ValOverlayContext, WindowSize } from "./ValOverlayContext";
19
13
  import { Remote } from "../utils/Remote";
20
14
  import { ValWindow } from "./ValWindow";
21
15
  import { result } from "@valbuild/core/fp";
22
- import {
23
- AnyRichTextOptions,
24
- FileSource,
25
- Internal,
26
- RichTextSource,
27
- SerializedSchema,
28
- SourcePath,
29
- VAL_EXTENSION,
30
- } from "@valbuild/core";
16
+ import { Internal, SerializedSchema, SourcePath } from "@valbuild/core";
31
17
  import { Modules, resolvePath } from "../utils/resolvePath";
32
18
  import { ValApi } from "@valbuild/core";
33
- import { RichTextEditor } from "../exports";
34
- import { LexicalEditor } from "lexical";
35
- import { LexicalRootNode } from "../richtext/conversion/richTextSourceToLexical";
36
- import { PatchJSON } from "@valbuild/core/patch";
37
- import { readImage } from "../utils/readImage";
38
- import { lexicalToRichTextSource } from "../richtext/conversion/lexicalToRichTextSource";
19
+ import { ValFormField } from "./ValFormField";
20
+ import { usePatch } from "./usePatch";
21
+ import { Button } from "./ui/button";
22
+ import { useTheme } from "./useTheme";
23
+ import { IValStore } from "../lib/IValStore";
39
24
 
40
25
  export type ValOverlayProps = {
41
26
  defaultTheme?: "dark" | "light";
42
27
  api: ValApi;
28
+ store: IValStore;
43
29
  };
44
30
 
45
- type ImageSource = FileSource<{
46
- height: number;
47
- width: number;
48
- sha256: string;
49
- }>;
50
-
51
- export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
31
+ export function ValOverlay({ defaultTheme, api, store }: ValOverlayProps) {
52
32
  const [theme, setTheme] = useTheme(defaultTheme);
53
33
  const session = useSession(api);
54
34
 
@@ -61,37 +41,16 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
61
41
  windowTarget?.path
62
42
  );
63
43
 
64
- const [state, setState] = useState<{
65
- [path: SourcePath]: () => Promise<PatchJSON>;
66
- }>({});
67
- const initPatchCallback = useCallback((currentPath: SourcePath | null) => {
68
- return (callback: PatchCallback) => {
69
- // TODO: revaluate this logic when we have multiple paths
70
- // NOTE: see cleanup of state in useEffect below
71
- if (!currentPath) {
72
- setState({});
73
- } else {
74
- const patchPath = Internal.createPatchJSONPath(
75
- Internal.splitModuleIdAndModulePath(currentPath)[1]
76
- );
77
- setState((prev) => {
78
- return {
79
- ...prev,
80
- [currentPath]: () => callback(patchPath),
81
- };
82
- });
83
- }
84
- };
85
- }, []);
86
- useEffect(() => {
87
- setState((prev) => {
88
- return Object.fromEntries(
89
- Object.entries(prev).filter(([path]) => path === windowTarget?.path)
90
- );
91
- });
92
- }, [windowTarget?.path]);
44
+ const { initPatchCallback, onSubmitPatch } = usePatch(
45
+ windowTarget?.path ? [windowTarget.path] : [],
46
+ api,
47
+ store
48
+ );
93
49
 
94
50
  const [windowSize, setWindowSize] = useState<WindowSize>();
51
+ useEffect(() => {
52
+ store.updateAll();
53
+ }, []);
95
54
 
96
55
  return (
97
56
  <ValOverlayContext.Provider
@@ -108,7 +67,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
108
67
  setWindowSize,
109
68
  }}
110
69
  >
111
- <div data-mode={theme}>
70
+ <div data-mode={theme} className="antialiased">
112
71
  <div className="fixed -translate-x-1/2 z-overlay left-1/2 bottom-4">
113
72
  <ValMenu api={api} />
114
73
  </div>
@@ -135,61 +94,17 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
135
94
  </div>
136
95
  {loading && <div className="text-primary">Loading...</div>}
137
96
  {error && <div className="text-red">{error}</div>}
138
- {typeof selectedSource === "string" &&
139
- selectedSchema?.type === "string" && (
140
- <TextField
141
- defaultValue={selectedSource}
142
- isLoading={loading}
143
- registerPatchCallback={initPatchCallback(windowTarget.path)}
144
- />
145
- )}
146
- {selectedSource &&
147
- typeof selectedSource === "object" &&
148
- VAL_EXTENSION in selectedSource &&
149
- selectedSource[VAL_EXTENSION] === "richtext" && (
150
- <RichTextField
151
- registerPatchCallback={initPatchCallback(windowTarget.path)}
152
- defaultValue={
153
- selectedSource as RichTextSource<AnyRichTextOptions>
154
- }
155
- />
156
- )}
157
- {selectedSource &&
158
- typeof selectedSource === "object" &&
159
- VAL_EXTENSION in selectedSource &&
160
- selectedSource[VAL_EXTENSION] === "file" && (
161
- <ImageField
162
- registerPatchCallback={initPatchCallback(windowTarget.path)}
163
- defaultValue={selectedSource as ImageSource}
164
- />
165
- )}
166
- <div className="flex items-end justify-end py-2">
167
- <SubmitButton
168
- disabled={false}
169
- onClick={() => {
170
- if (state[windowTarget.path]) {
171
- const [moduleId] = Internal.splitModuleIdAndModulePath(
172
- windowTarget.path
173
- );
174
- const res = state[windowTarget.path]();
175
- if (res) {
176
- res
177
- .then((patch) => {
178
- console.log("Submitting", patch);
179
- return api.postPatches(moduleId, patch);
180
- })
181
- .then((res) => {
182
- console.log(res);
183
- })
184
- .finally(() => {
185
- console.log("done");
186
- });
187
- } else {
188
- console.error("No patch");
189
- }
190
- }
191
- }}
97
+ {selectedSchema !== undefined && selectedSource !== undefined && (
98
+ <ValFormField
99
+ path={windowTarget.path}
100
+ disabled={loading}
101
+ source={selectedSource}
102
+ schema={selectedSchema}
103
+ registerPatchCallback={initPatchCallback(windowTarget.path)}
192
104
  />
105
+ )}
106
+ <div className="flex items-end justify-end py-2">
107
+ <SubmitButton disabled={false} onClick={onSubmitPatch} />
193
108
  </div>
194
109
  </ValWindow>
195
110
  )}
@@ -198,183 +113,6 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
198
113
  );
199
114
  }
200
115
 
201
- type PatchCallback = (modulePath: string) => Promise<PatchJSON>;
202
-
203
- function ImageField({
204
- defaultValue,
205
- registerPatchCallback,
206
- }: {
207
- registerPatchCallback: (callback: PatchCallback) => void;
208
- defaultValue?: ImageSource;
209
- }) {
210
- const [data, setData] = useState<string | null>(null);
211
- const [metadata, setMetadata] = useState<{
212
- width?: number;
213
- height?: number;
214
- sha256: string;
215
- } | null>(null);
216
- const url = defaultValue && Internal.convertFileSource(defaultValue).url;
217
- useEffect(() => {
218
- registerPatchCallback(async (path) => {
219
- const pathParts = path.split("/");
220
- if (!data) {
221
- return [];
222
- }
223
- return [
224
- {
225
- value: {
226
- ...defaultValue,
227
- metadata,
228
- },
229
- op: "replace",
230
- path,
231
- },
232
- // update the contents of the file:
233
- {
234
- value: data,
235
- op: "replace",
236
- path: `${pathParts.slice(0, -1).join("/")}/$${
237
- pathParts[pathParts.length - 1]
238
- }`,
239
- },
240
- ];
241
- });
242
- }, [data]);
243
-
244
- return (
245
- <div>
246
- <label htmlFor="img_input" className="">
247
- <img src={data || url} />
248
- <input
249
- id="img_input"
250
- type="file"
251
- hidden
252
- onChange={(ev) => {
253
- readImage(ev)
254
- .then((res) => {
255
- setData(res.src);
256
- setMetadata({
257
- sha256: res.sha256,
258
- width: res.width,
259
- height: res.height,
260
- });
261
- })
262
- .catch((err) => {
263
- console.error(err.message);
264
- setData(null);
265
- setMetadata(null);
266
- });
267
- }}
268
- />
269
- </label>
270
- </div>
271
- );
272
- }
273
-
274
- function RichTextField({
275
- defaultValue,
276
- registerPatchCallback,
277
- }: {
278
- registerPatchCallback: (callback: PatchCallback) => void;
279
- defaultValue?: RichTextSource<AnyRichTextOptions>;
280
- }) {
281
- const [editor, setEditor] = useState<LexicalEditor | null>(null);
282
- useEffect(() => {
283
- if (editor) {
284
- registerPatchCallback(async (path) => {
285
- const { templateStrings, exprs, files } = editor?.toJSON()?.editorState
286
- ? await lexicalToRichTextSource(
287
- editor?.toJSON()?.editorState.root as LexicalRootNode
288
- )
289
- : ({
290
- [VAL_EXTENSION]: "richtext",
291
- templateStrings: [""],
292
- exprs: [],
293
- files: {},
294
- } as RichTextSource<AnyRichTextOptions> & {
295
- files: Record<string, string>;
296
- });
297
- return [
298
- {
299
- op: "replace" as const,
300
- path,
301
- value: {
302
- templateStrings,
303
- exprs,
304
- [VAL_EXTENSION]: "richtext",
305
- },
306
- },
307
- ...Object.entries(files).map(([path, value]) => {
308
- return {
309
- op: "file" as const,
310
- path,
311
- value,
312
- };
313
- }),
314
- ];
315
- });
316
- }
317
- }, [editor]);
318
- return (
319
- <RichTextEditor
320
- onEditor={(editor) => {
321
- setEditor(editor);
322
- }}
323
- richtext={
324
- defaultValue ||
325
- ({
326
- children: [],
327
- [VAL_EXTENSION]: "root",
328
- } as unknown as RichTextSource<AnyRichTextOptions>)
329
- }
330
- />
331
- );
332
- }
333
-
334
- function TextField({
335
- isLoading,
336
- defaultValue,
337
- registerPatchCallback,
338
- }: {
339
- registerPatchCallback: (callback: PatchCallback) => void;
340
- isLoading: boolean;
341
- defaultValue?: string;
342
- }) {
343
- const [value, setValue] = useState(defaultValue || "");
344
-
345
- // ref is used to get the value of the textarea without closing over the value field
346
- // to avoid registering a new callback every time the value changes
347
- const ref = useRef<HTMLTextAreaElement>(null);
348
- useEffect(() => {
349
- registerPatchCallback(async (path) => {
350
- return [
351
- {
352
- op: "replace",
353
- path,
354
- value: ref.current?.value || "",
355
- },
356
- ];
357
- });
358
- }, []);
359
-
360
- return (
361
- <div className="flex flex-col justify-between h-full px-4">
362
- <div
363
- className="w-full h-full py-2 overflow-y-scroll grow-wrap"
364
- data-replicated-value={value} /* see grow-wrap */
365
- >
366
- <textarea
367
- ref={ref}
368
- disabled={isLoading}
369
- className="p-2 border outline-none resize-none bg-fill text-primary border-border focus-visible:border-highlight"
370
- defaultValue={value}
371
- onChange={(e) => setValue(e.target.value)}
372
- />
373
- </div>
374
- </div>
375
- );
376
- }
377
-
378
116
  function SubmitButton({
379
117
  disabled,
380
118
  onClick,
@@ -383,13 +121,13 @@ function SubmitButton({
383
121
  onClick?: () => void;
384
122
  }) {
385
123
  return (
386
- <button
124
+ <Button
387
125
  className="px-4 py-2 border border-highlight disabled:border-border"
388
126
  disabled={disabled}
389
127
  onClick={onClick}
390
128
  >
391
129
  Submit
392
- </button>
130
+ </Button>
393
131
  );
394
132
  }
395
133
 
@@ -503,7 +241,7 @@ function ValHover({
503
241
  return (
504
242
  <div
505
243
  id="val-hover"
506
- className="fixed border-2 cursor-pointer z-overlay-hover border-base"
244
+ className="fixed border-2 cursor-pointer z-overlay-hover border-highlight drop-shadow-[0px_0px_12px_rgba(56,205,152,0.60)]"
507
245
  style={{
508
246
  top: rect?.top,
509
247
  left: rect?.left,
@@ -521,7 +259,7 @@ function ValHover({
521
259
  >
522
260
  <div className="flex items-center justify-end w-full text-xs">
523
261
  <div
524
- className="flex items-center justify-center px-3 py-1 text-primary bg-base"
262
+ className="flex items-center justify-center px-3 py-1 text-primary bg-highlight"
525
263
  style={{
526
264
  maxHeight: rect?.height && rect.height - 4,
527
265
  fontSize:
@@ -548,7 +286,6 @@ function useHoverTarget(editMode: EditMode) {
548
286
  // TODO: use .contains?
549
287
  do {
550
288
  if (curr?.dataset.valPath) {
551
- console.log("setter target");
552
289
  setTargetElement(curr);
553
290
  setTargetPath(curr.dataset.valPath as SourcePath);
554
291
  setTargetRect(curr.getBoundingClientRect());
@@ -668,49 +405,6 @@ function useInitEditMode() {
668
405
  return [editMode, setEditMode] as const;
669
406
  }
670
407
 
671
- function useTheme(defaultTheme: Theme = "dark") {
672
- const [theme, setTheme] = useState<Theme>(defaultTheme);
673
-
674
- useEffect(() => {
675
- if (localStorage.getItem("val-theme") === "light") {
676
- setTheme("light");
677
- } else if (localStorage.getItem("val-theme") === "dark") {
678
- setTheme("dark");
679
- } else if (
680
- window.matchMedia &&
681
- window.matchMedia("(prefers-color-scheme: dark)").matches
682
- ) {
683
- setTheme("dark");
684
- } else if (
685
- window.matchMedia &&
686
- window.matchMedia("(prefers-color-scheme: light)").matches
687
- ) {
688
- setTheme("light");
689
- }
690
- const themeListener = (e: MediaQueryListEvent) => {
691
- if (!localStorage.getItem("val-theme")) {
692
- setTheme(e.matches ? "dark" : "light");
693
- }
694
- };
695
- window
696
- .matchMedia("(prefers-color-scheme: dark)")
697
- .addEventListener("change", themeListener);
698
- return () => {
699
- window
700
- .matchMedia("(prefers-color-scheme: dark)")
701
- .removeEventListener("change", themeListener);
702
- };
703
- }, []);
704
-
705
- return [
706
- theme,
707
- (theme: Theme) => {
708
- localStorage.setItem("val-theme", theme);
709
- setTheme(theme);
710
- },
711
- ] as const;
712
- }
713
-
714
408
  function useSession(api: ValApi) {
715
409
  const [session, setSession] = useState<Remote<Session>>({
716
410
  status: "not-asked",
@@ -1,8 +1,9 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
- import { AlignJustify, X } from "react-feather";
3
2
  import classNames from "classnames";
4
3
  import { Resizable } from "react-resizable";
5
4
  import { useValOverlayContext } from "./ValOverlayContext";
5
+ import { AlignJustifyIcon, XIcon } from "lucide-react";
6
+ import { ScrollArea } from "./ui/scroll-area";
6
7
 
7
8
  export type ValWindowProps = {
8
9
  children:
@@ -54,11 +55,13 @@ export function ValWindow({
54
55
 
55
56
  return (
56
57
  <Resizable
58
+ minConstraints={[MIN_WIDTH, MIN_HEIGHT]}
57
59
  width={windowSize?.width || MIN_WIDTH}
58
60
  height={windowSize?.height || MIN_HEIGHT}
59
61
  onResize={(_, { size }) =>
60
62
  setWindowSize({
61
63
  ...size,
64
+
62
65
  innerHeight:
63
66
  (windowSize?.height || MIN_HEIGHT) -
64
67
  (64 + (bottomRef.current?.getBoundingClientRect()?.height || 0)),
@@ -81,7 +84,7 @@ export function ValWindow({
81
84
  }
82
85
  draggableOpts={{}}
83
86
  className={classNames(
84
- "absolute inset-0 tablet:w-auto tablet:h-auto tablet:min-h-fit tablet:rounded bg-base text-primary drop-shadow-2xl min-w-[320px] transition-opacity duration-300 delay-75",
87
+ "absolute inset-0 tablet:w-auto tablet:h-auto tablet:min-h-fit rounded-lg bg-background text-primary drop-shadow-2xl min-w-[320px]",
85
88
  {
86
89
  "opacity-0": !isInitialized,
87
90
  "opacity-100": isInitialized,
@@ -100,9 +103,9 @@ export function ValWindow({
100
103
  ref={dragRef}
101
104
  className="relative flex items-center justify-center px-2 pt-2 text-primary"
102
105
  >
103
- <AlignJustify
106
+ <AlignJustifyIcon
104
107
  size={16}
105
- className="hidden w-full cursor-grab tablet:block"
108
+ className="w-full cursor-grab tablet:block"
106
109
  onMouseDown={(e) => {
107
110
  e.preventDefault();
108
111
  e.stopPropagation();
@@ -110,10 +113,10 @@ export function ValWindow({
110
113
  }}
111
114
  />
112
115
  <button
113
- className="absolute top-0 right-0 px-4 py-2 focus:outline-none focus-visible:outline-highlight"
116
+ className="absolute top-0 right-0 px-4 py-2 focus:outline-none focus-visible:outline-accent"
114
117
  onClick={onClose}
115
118
  >
116
- <X size={16} />
119
+ <XIcon size={16} />
117
120
  </button>
118
121
  </div>
119
122
  <form
@@ -123,14 +126,14 @@ export function ValWindow({
123
126
  }}
124
127
  >
125
128
  {Array.isArray(children) && children.slice(0, 1)}
126
- <div
127
- className="relative overflow-scroll"
129
+ <ScrollArea
130
+ className="relative"
128
131
  style={{
129
132
  height: windowSize?.innerHeight,
130
133
  }}
131
134
  >
132
135
  {Array.isArray(children) ? children.slice(1, -1) : children}
133
- </div>
136
+ </ScrollArea>
134
137
  <div ref={bottomRef} className="w-full px-4 pb-0">
135
138
  {Array.isArray(children) && children.slice(-1)}
136
139
  </div>
@@ -3,20 +3,26 @@ import { Children, ReactNode, useState } from "react";
3
3
 
4
4
  interface FormGroupProps {
5
5
  children: ReactNode | ReactNode[];
6
+ className?: string;
7
+ defaultExpanded?: boolean;
6
8
  }
7
9
 
8
- export const FormGroup = ({ children }: FormGroupProps) => {
10
+ export const FormGroup = ({
11
+ children,
12
+ className,
13
+ defaultExpanded,
14
+ }: FormGroupProps) => {
9
15
  const [firstChild, ...rest] = Children.toArray(children);
10
- const [expanded, setExpanded] = useState<boolean>(false);
11
- const defaultClass =
12
- "px-4 py-3 border-b border-dark-gray hover:bg-light-gray hover:border-light-gray";
16
+ const [expanded, setExpanded] = useState<boolean>(defaultExpanded ?? true);
17
+ const defaultClass = "py-3 " + (className ? ` ${className}` : "");
13
18
  return (
14
19
  <div>
15
20
  <div className="flex flex-col font-serif text-xs leading-4 tracking-wider text-white">
16
21
  <button
17
22
  className={classNames(
18
23
  defaultClass,
19
- "bg-warm-black flex justify-between items-center"
24
+ "bg-warm-black flex justify-between items-center",
25
+ { "border-y border-highlight": !expanded }
20
26
  )}
21
27
  onClick={() => setExpanded(!expanded)}
22
28
  >
@@ -24,7 +30,7 @@ export const FormGroup = ({ children }: FormGroupProps) => {
24
30
  <div>{expanded ? "Collapse" : "Expand"}</div>
25
31
  </button>
26
32
  {expanded && (
27
- <div className="flex flex-col bg-medium-black">
33
+ <div className="flex flex-col bg-background">
28
34
  {Children.map(rest, (child) => (
29
35
  <div className={classNames(defaultClass)}>{child}</div>
30
36
  ))}