@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.
- package/dist/valbuild-ui.cjs.d.ts +13 -18
- package/dist/valbuild-ui.cjs.js +7624 -1718
- package/dist/valbuild-ui.esm.js +7625 -1719
- package/package.json +5 -2
- package/src/assets/icons/ImageIcon.tsx +15 -7
- package/src/assets/icons/Section.tsx +41 -0
- package/src/assets/icons/TextIcon.tsx +20 -0
- package/src/components/Button.tsx +18 -7
- package/src/components/DraggableList.stories.tsx +20 -0
- package/src/components/DraggableList.tsx +95 -0
- package/src/components/Dropdown.tsx +2 -0
- package/src/components/ExpandLogo.tsx +72 -0
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +1 -16
- package/src/components/RichTextEditor/RichTextEditor.tsx +2 -2
- package/src/components/User.tsx +17 -0
- package/src/components/ValMenu.tsx +40 -0
- package/src/components/ValOverlay.tsx +513 -29
- package/src/components/ValOverlayContext.tsx +63 -0
- package/src/components/ValWindow.stories.tsx +3 -3
- package/src/components/ValWindow.tsx +26 -18
- package/src/components/dashboard/DashboardButton.tsx +25 -0
- package/src/components/dashboard/DashboardDropdown.tsx +59 -0
- package/src/components/dashboard/Dropdown.stories.tsx +11 -0
- package/src/components/dashboard/Dropdown.tsx +70 -0
- package/src/components/dashboard/FormGroup.stories.tsx +37 -0
- package/src/components/dashboard/FormGroup.tsx +36 -0
- package/src/components/dashboard/Grid.stories.tsx +52 -0
- package/src/components/dashboard/Grid.tsx +126 -0
- package/src/components/dashboard/Grid2.stories.tsx +56 -0
- package/src/components/dashboard/Grid2.tsx +72 -0
- package/src/components/dashboard/Tree.stories.tsx +91 -0
- package/src/components/dashboard/Tree.tsx +72 -0
- package/src/components/dashboard/ValDashboard.tsx +148 -0
- package/src/components/dashboard/ValDashboardEditor.tsx +269 -0
- package/src/components/dashboard/ValDashboardGrid.tsx +142 -0
- package/src/components/dashboard/ValTreeNavigator.tsx +253 -0
- package/src/components/forms/Form.tsx +2 -2
- package/src/components/forms/{TextForm.tsx → TextArea.tsx} +5 -3
- package/src/dto/SerializedSchema.ts +69 -0
- package/src/dto/Session.ts +12 -0
- package/src/dto/SessionMode.ts +5 -0
- package/src/dto/Tree.ts +18 -0
- package/src/exports.ts +1 -0
- package/src/utils/Remote.ts +15 -0
- package/src/utils/resolvePath.ts +33 -0
- package/tailwind.config.js +20 -1
|
@@ -1,41 +1,525 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
}
|
|
222
|
+
}
|
|
11
223
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
closeValWindow: () => void;
|
|
224
|
+
type WindowTarget = {
|
|
225
|
+
element?: HTMLElement | undefined;
|
|
226
|
+
mouse: { x: number; y: number };
|
|
227
|
+
path: SourcePath;
|
|
17
228
|
};
|
|
18
229
|
|
|
19
|
-
|
|
20
|
-
|
|
230
|
+
type HoverTarget = {
|
|
231
|
+
element?: HTMLElement | undefined;
|
|
232
|
+
path: SourcePath;
|
|
233
|
+
};
|
|
234
|
+
function ValHover({
|
|
235
|
+
hoverTarget,
|
|
21
236
|
setEditMode,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}:
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
54
|
+
<TextArea
|
|
55
55
|
name="/apps/blogs.0.title"
|
|
56
56
|
text={EXAMPLE_TEXT}
|
|
57
57
|
onChange={() => {
|