@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.
- package/.storybook/theme.css +5 -1
- package/components.json +16 -0
- package/dist/valbuild-ui.cjs.d.ts +11 -7
- package/dist/valbuild-ui.cjs.js +43607 -33216
- package/dist/valbuild-ui.esm.js +48313 -37938
- package/fix-server-hack.js +45 -0
- package/fullscreen.vite.config.ts +11 -0
- package/index.html +13 -0
- package/package.json +52 -13
- package/server/dist/manifest.json +16 -0
- package/server/dist/style.css +2145 -0
- package/server/dist/valbuild-ui-main.cjs.js +74441 -0
- package/server/dist/valbuild-ui-main.esm.js +74442 -0
- package/server/dist/valbuild-ui-server.cjs.js +19 -2
- package/server/dist/valbuild-ui-server.esm.js +19 -2
- package/server.vite.config.ts +2 -0
- package/src/App.tsx +73 -0
- package/src/assets/icons/Logo.tsx +103 -0
- package/src/components/Button.tsx +10 -2
- package/src/components/Dropdown.tsx +2 -2
- package/src/components/{dashboard/Grid.stories.tsx → Grid.stories.tsx} +8 -17
- package/src/components/{dashboard/Grid.tsx → Grid.tsx} +36 -23
- package/src/components/RichTextEditor/ContentEditable.tsx +109 -1
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +2 -2
- package/src/components/RichTextEditor/RichTextEditor.tsx +1 -1
- package/src/components/ValFormField.tsx +576 -0
- package/src/components/ValFullscreen.tsx +1283 -0
- package/src/components/ValMenu.tsx +65 -13
- package/src/components/ValOverlay.tsx +32 -338
- package/src/components/ValWindow.tsx +12 -9
- package/src/components/dashboard/FormGroup.tsx +12 -6
- package/src/components/dashboard/Tree.tsx +2 -2
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert-dialog.tsx +139 -0
- package/src/components/ui/avatar.tsx +48 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/calendar.tsx +62 -0
- package/src/components/ui/card.tsx +86 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/command.tsx +153 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/dropdown-menu.tsx +198 -0
- package/src/components/ui/form.tsx +177 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +29 -0
- package/src/components/ui/progress.tsx +26 -0
- package/src/components/ui/radio-group.tsx +42 -0
- package/src/components/ui/scroll-area.tsx +51 -0
- package/src/components/ui/select.tsx +119 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/usePatch.ts +86 -0
- package/src/components/useTheme.ts +45 -0
- package/src/exports.ts +2 -1
- package/src/index.css +96 -60
- package/src/lib/IValStore.ts +6 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.jsx +10 -0
- package/src/richtext/conversion/lexicalToRichTextSource.ts +0 -1
- package/src/richtext/shadowRootPolyFill.js +115 -0
- package/src/server.ts +39 -2
- package/src/utils/resolvePath.ts +0 -1
- package/src/vite-server.ts +20 -3
- package/tailwind.config.js +63 -51
- package/tsconfig.json +2 -1
- package/vite.config.ts +10 -0
- 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
|
|
9
|
-
<
|
|
10
|
-
|
|
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 === "
|
|
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 === "
|
|
29
|
+
{editMode === "hover" ? <Pause size={18} /> : <Play size={18} />}
|
|
17
30
|
</div>
|
|
18
|
-
</
|
|
19
|
-
<
|
|
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
|
-
</
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
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
|
-
|
|
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
|
|
65
|
-
[path:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
<
|
|
106
|
+
<AlignJustifyIcon
|
|
104
107
|
size={16}
|
|
105
|
-
className="
|
|
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-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
127
|
-
className="relative
|
|
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
|
-
</
|
|
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 = ({
|
|
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>(
|
|
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-
|
|
33
|
+
<div className="flex flex-col bg-background">
|
|
28
34
|
{Children.map(rest, (child) => (
|
|
29
35
|
<div className={classNames(defaultClass)}>{child}</div>
|
|
30
36
|
))}
|