@valbuild/ui 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/valbuild-ui.cjs.js +41 -15
- package/dist/valbuild-ui.esm.js +41 -15
- package/package.json +7 -3
- package/server/.tmp/assets/index-18cfa26c.css +1 -0
- package/server/.tmp/assets/index-513f7a9c.js +197 -0
- package/{index.html → server/.tmp/index.html} +3 -1
- package/server/dist/style.css +0 -3
- package/server/dist/valbuild-ui-main.cjs.js +60 -34
- package/server/dist/valbuild-ui-main.esm.js +60 -34
- package/server/dist/valbuild-ui-server.cjs.js +1 -1
- package/server/dist/valbuild-ui-server.esm.js +1 -1
- package/.babelrc.json +0 -10
- package/.storybook/main.js +0 -25
- package/.storybook/preview-head.html +0 -6
- package/.storybook/preview.js +0 -33
- package/.storybook/theme.css +0 -34
- package/CHANGELOG.md +0 -0
- package/components.json +0 -16
- package/fix-server-hack.js +0 -54
- package/fullscreen.vite.config.ts +0 -9
- package/jest.config.js +0 -4
- package/postcss.config.js +0 -6
- package/rollup.config.js +0 -23
- package/server.vite.config.ts +0 -31
- package/src/App.tsx +0 -73
- package/src/assets/icons/Bold.tsx +0 -23
- package/src/assets/icons/Chevron.tsx +0 -28
- package/src/assets/icons/FontColor.tsx +0 -30
- package/src/assets/icons/ImageIcon.tsx +0 -29
- package/src/assets/icons/Italic.tsx +0 -24
- package/src/assets/icons/Logo.tsx +0 -103
- package/src/assets/icons/Section.tsx +0 -41
- package/src/assets/icons/Strikethrough.tsx +0 -22
- package/src/assets/icons/TextIcon.tsx +0 -20
- package/src/assets/icons/Underline.tsx +0 -22
- package/src/assets/icons/Undo.tsx +0 -20
- package/src/components/Button.tsx +0 -68
- package/src/components/Checkbox.tsx +0 -51
- package/src/components/DraggableList.stories.tsx +0 -20
- package/src/components/DraggableList.tsx +0 -95
- package/src/components/Dropdown.tsx +0 -101
- package/src/components/EditButton.tsx +0 -10
- package/src/components/ErrorText.tsx +0 -3
- package/src/components/ExpandLogo.tsx +0 -72
- package/src/components/Grid.stories.tsx +0 -43
- package/src/components/Grid.tsx +0 -139
- package/src/components/RichTextEditor/ContentEditable.tsx +0 -117
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +0 -100
- package/src/components/RichTextEditor/Plugins/AutoFocus.tsx +0 -12
- package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +0 -45
- package/src/components/RichTextEditor/Plugins/LinkEditorPlugin.tsx +0 -58
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +0 -412
- package/src/components/RichTextEditor/RichTextEditor.tsx +0 -105
- package/src/components/UploadModal.tsx +0 -109
- package/src/components/User.tsx +0 -17
- package/src/components/ValFormField.tsx +0 -574
- package/src/components/ValFullscreen.tsx +0 -1278
- package/src/components/ValMenu.tsx +0 -92
- package/src/components/ValOverlay.tsx +0 -488
- package/src/components/ValOverlayContext.tsx +0 -80
- package/src/components/ValWindow.stories.tsx +0 -146
- package/src/components/ValWindow.tsx +0 -220
- package/src/components/dashboard/DashboardButton.tsx +0 -25
- package/src/components/dashboard/DashboardDropdown.tsx +0 -59
- package/src/components/dashboard/Dropdown.stories.tsx +0 -11
- package/src/components/dashboard/Dropdown.tsx +0 -70
- package/src/components/dashboard/FormGroup.stories.tsx +0 -37
- package/src/components/dashboard/FormGroup.tsx +0 -42
- package/src/components/dashboard/Grid2.stories.tsx +0 -56
- package/src/components/dashboard/Grid2.tsx +0 -72
- package/src/components/dashboard/Tree.stories.tsx +0 -91
- package/src/components/dashboard/Tree.tsx +0 -72
- package/src/components/dashboard/ValDashboardEditor.tsx +0 -269
- package/src/components/dashboard/ValDashboardGrid.tsx +0 -142
- package/src/components/dashboard/ValTreeNavigator.tsx +0 -253
- package/src/components/forms/Form.tsx +0 -126
- package/src/components/forms/FormContainer.tsx +0 -24
- package/src/components/forms/ImageForm.tsx +0 -195
- package/src/components/forms/TextArea.tsx +0 -24
- package/src/components/ui/accordion.tsx +0 -58
- package/src/components/ui/alert-dialog.tsx +0 -139
- package/src/components/ui/avatar.tsx +0 -48
- package/src/components/ui/button.tsx +0 -56
- package/src/components/ui/calendar.tsx +0 -62
- package/src/components/ui/card.tsx +0 -86
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/command.tsx +0 -153
- package/src/components/ui/dialog.tsx +0 -120
- package/src/components/ui/dropdown-menu.tsx +0 -198
- package/src/components/ui/form.tsx +0 -177
- package/src/components/ui/input.tsx +0 -24
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -26
- package/src/components/ui/radio-group.tsx +0 -42
- package/src/components/ui/scroll-area.tsx +0 -51
- package/src/components/ui/select.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/toggle.tsx +0 -43
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/components/usePatch.ts +0 -86
- package/src/components/useTheme.ts +0 -45
- package/src/dto/SerializedSchema.ts +0 -69
- package/src/dto/Session.ts +0 -12
- package/src/dto/SessionMode.ts +0 -5
- package/src/dto/Tree.ts +0 -18
- package/src/exports.ts +0 -6
- package/src/index.css +0 -115
- package/src/index.tsx +0 -14
- package/src/lib/IValStore.ts +0 -6
- package/src/lib/utils.ts +0 -6
- package/src/main.jsx +0 -10
- package/src/richtext/conversion/conversion.test.ts +0 -146
- package/src/richtext/conversion/lexicalToRichTextSource.test.ts +0 -89
- package/src/richtext/conversion/lexicalToRichTextSource.ts +0 -285
- package/src/richtext/conversion/parseRichTextSource.test.ts +0 -469
- package/src/richtext/conversion/parseRichTextSource.ts +0 -233
- package/src/richtext/conversion/richTextSourceToLexical.test.ts +0 -381
- package/src/richtext/conversion/richTextSourceToLexical.ts +0 -293
- package/src/richtext/shadowRootPolyFill.js +0 -115
- package/src/server.ts +0 -70
- package/src/stories/Button.stories.tsx +0 -20
- package/src/stories/Checkbox.stories.tsx +0 -14
- package/src/stories/Dropdown.stories.tsx +0 -23
- package/src/stories/Introduction.mdx +0 -221
- package/src/stories/RichTextEditor.stories.tsx +0 -24
- package/src/stories/assets/code-brackets.svg +0 -1
- package/src/stories/assets/colors.svg +0 -1
- package/src/stories/assets/comments.svg +0 -1
- package/src/stories/assets/direction.svg +0 -1
- package/src/stories/assets/flow.svg +0 -1
- package/src/stories/assets/plugin.svg +0 -1
- package/src/stories/assets/repo.svg +0 -1
- package/src/stories/assets/stackalt.svg +0 -1
- package/src/utils/Remote.ts +0 -15
- package/src/utils/imageMimeType.ts +0 -23
- package/src/utils/readImage.ts +0 -54
- package/src/utils/resolvePath.ts +0 -32
- package/src/vite-env.d.ts +0 -1
- package/src/vite-index.tsx +0 -7
- package/src/vite-server.ts +0 -42
- package/tailwind.config.js +0 -83
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -43
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { Internal, SerializedModule, SourcePath } from "@valbuild/core";
|
|
2
|
-
import { Json, JsonArray } from "@valbuild/core/src/Json";
|
|
3
|
-
import { ValApi } from "@valbuild/core";
|
|
4
|
-
import classNames from "classnames";
|
|
5
|
-
import { Dispatch, FC, SetStateAction, useState } from "react";
|
|
6
|
-
import Chevron from "../../assets/icons/Chevron";
|
|
7
|
-
import { DraggableList, DraggableResult } from "../DraggableList";
|
|
8
|
-
|
|
9
|
-
const ValTreeArrayModuleItem: FC<{
|
|
10
|
-
submodule: Json;
|
|
11
|
-
selectedModule: string;
|
|
12
|
-
setSelectedModule: Dispatch<SetStateAction<string>>;
|
|
13
|
-
idx: number;
|
|
14
|
-
path: string;
|
|
15
|
-
reOrder: (oldIdx: number, newIdx: number) => void;
|
|
16
|
-
reorderMode: boolean;
|
|
17
|
-
}> = ({
|
|
18
|
-
submodule,
|
|
19
|
-
selectedModule,
|
|
20
|
-
setSelectedModule,
|
|
21
|
-
idx,
|
|
22
|
-
path,
|
|
23
|
-
reOrder,
|
|
24
|
-
reorderMode,
|
|
25
|
-
}) => {
|
|
26
|
-
const title = resolveTitle(submodule);
|
|
27
|
-
return (
|
|
28
|
-
<div className="w-fit" draggable id={idx.toString()}>
|
|
29
|
-
<div className="flex gap-4">
|
|
30
|
-
<button
|
|
31
|
-
onClick={() => setSelectedModule(path + idx.toString())}
|
|
32
|
-
className={classNames(
|
|
33
|
-
"px-4 py-2 text-start hover:bg-light-gray/20 hover:rounded-lg",
|
|
34
|
-
{
|
|
35
|
-
"font-extrabold ":
|
|
36
|
-
module.path + idx.toString() === selectedModule,
|
|
37
|
-
}
|
|
38
|
-
)}
|
|
39
|
-
>
|
|
40
|
-
{title}
|
|
41
|
-
</button>
|
|
42
|
-
{reorderMode && (
|
|
43
|
-
<div className="flex gap-2">
|
|
44
|
-
<button
|
|
45
|
-
onClick={() => reOrder(idx, idx + 1)}
|
|
46
|
-
className="disabled:text-dark-gray"
|
|
47
|
-
>
|
|
48
|
-
<Chevron className="rotate-90" />
|
|
49
|
-
</button>
|
|
50
|
-
<button
|
|
51
|
-
onClick={() => reOrder(idx, idx - 1)}
|
|
52
|
-
className="disabled:text-dark-gray"
|
|
53
|
-
>
|
|
54
|
-
<Chevron className="-rotate-90" />
|
|
55
|
-
</button>
|
|
56
|
-
</div>
|
|
57
|
-
)}
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const DEFAULT_TITLE = "Untitled";
|
|
64
|
-
function resolveTitle(submodule: Json): string {
|
|
65
|
-
if (!submodule) {
|
|
66
|
-
return DEFAULT_TITLE;
|
|
67
|
-
}
|
|
68
|
-
if (typeof submodule === "string") {
|
|
69
|
-
return submodule;
|
|
70
|
-
}
|
|
71
|
-
if (typeof submodule === "object") {
|
|
72
|
-
if ("title" in submodule && typeof submodule.title === "string") {
|
|
73
|
-
return submodule.title;
|
|
74
|
-
}
|
|
75
|
-
if ("name" in submodule && typeof submodule.name === "string") {
|
|
76
|
-
return submodule.name;
|
|
77
|
-
}
|
|
78
|
-
const firstStringField = Object.entries(submodule).find(([, value]) => {
|
|
79
|
-
return typeof value === "string";
|
|
80
|
-
})?.[1];
|
|
81
|
-
if (typeof firstStringField === "string") {
|
|
82
|
-
return firstStringField;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return DEFAULT_TITLE;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const ValTreeNavigatorArrayModule: FC<{
|
|
89
|
-
module: SerializedModule;
|
|
90
|
-
selectedModule: string;
|
|
91
|
-
setSelectedModule: Dispatch<SetStateAction<string>>;
|
|
92
|
-
valApi: ValApi;
|
|
93
|
-
}> = ({ module, selectedModule, setSelectedModule }) => {
|
|
94
|
-
const [collapsed, setCollapsed] = useState(true);
|
|
95
|
-
const [items, setItems] = useState<Json[]>(
|
|
96
|
-
(module.source as JsonArray).map((submodule) => submodule as Json)
|
|
97
|
-
);
|
|
98
|
-
const [reOrderMode, setReOrderMode] = useState(false);
|
|
99
|
-
|
|
100
|
-
const reOrder = async (oldIndex: number, newIndex: number) => {
|
|
101
|
-
const sanitizedNewIndex =
|
|
102
|
-
newIndex < 0 ? items.length - 1 : newIndex % items.length;
|
|
103
|
-
const path = module.path + oldIndex.toString();
|
|
104
|
-
const newPath = module.path + sanitizedNewIndex.toString();
|
|
105
|
-
// const [moduleId, modulePath] = Internal.splitModuleIdAndModulePath(
|
|
106
|
-
// path as SourcePath
|
|
107
|
-
// );
|
|
108
|
-
|
|
109
|
-
const [newModuleId, newModulePath] = Internal.splitModuleIdAndModulePath(
|
|
110
|
-
newPath as SourcePath
|
|
111
|
-
);
|
|
112
|
-
// const patch: PatchJSON = [
|
|
113
|
-
// {
|
|
114
|
-
// op: "move",
|
|
115
|
-
// from: `/${modulePath
|
|
116
|
-
// .split(".")
|
|
117
|
-
// .map((p) => {
|
|
118
|
-
// return JSON.parse(p);
|
|
119
|
-
// })
|
|
120
|
-
// .join("/")}`,
|
|
121
|
-
// path: `/${newModulePath
|
|
122
|
-
// .split(".")
|
|
123
|
-
// .map((p) => {
|
|
124
|
-
// return JSON.parse(p);
|
|
125
|
-
// })
|
|
126
|
-
// .join("/")}`,
|
|
127
|
-
// },
|
|
128
|
-
// ];
|
|
129
|
-
// await valApi.patchModuleContent(moduleId, patch);
|
|
130
|
-
if (selectedModule === path) {
|
|
131
|
-
setSelectedModule(`${newModuleId}.${newModulePath}`);
|
|
132
|
-
}
|
|
133
|
-
setItems((items) => {
|
|
134
|
-
const newItems = [...items];
|
|
135
|
-
const item = newItems.splice(oldIndex, 1)[0];
|
|
136
|
-
newItems.splice(sanitizedNewIndex, 0, item);
|
|
137
|
-
return newItems;
|
|
138
|
-
});
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const toggleReorderMode = () => {
|
|
142
|
-
setReOrderMode(!reOrderMode);
|
|
143
|
-
if (collapsed) setCollapsed(false);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<div className="relative flex flex-col gap-3">
|
|
148
|
-
<div className="flex items-center justify-between ">
|
|
149
|
-
<button
|
|
150
|
-
className="flex items-center justify-between px-4 py-2 hover:bg-light-gray/20 hover:rounded-lg "
|
|
151
|
-
onClick={() => {
|
|
152
|
-
setSelectedModule(`${module.path.toString()}`);
|
|
153
|
-
setCollapsed(!collapsed);
|
|
154
|
-
}}
|
|
155
|
-
>
|
|
156
|
-
<div className="flex items-center gap-2">
|
|
157
|
-
<Chevron className={classNames({ "rotate-90": !collapsed })} />
|
|
158
|
-
<h1 className="text-xl">{module.path}</h1>
|
|
159
|
-
</div>
|
|
160
|
-
</button>
|
|
161
|
-
<div className="flex gap-2">
|
|
162
|
-
<button
|
|
163
|
-
className="relative w-[20px] h-[20px] flex flex-col justify-between items-center"
|
|
164
|
-
onClick={toggleReorderMode}
|
|
165
|
-
>
|
|
166
|
-
<Chevron className="top-0 -rotate-90" />
|
|
167
|
-
<Chevron className="rotate-90 " />
|
|
168
|
-
</button>
|
|
169
|
-
<button className="text-2xl w-[20px] h-[20px] rounded-full flex justify-center items-center">
|
|
170
|
-
+
|
|
171
|
-
</button>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
{!collapsed && (
|
|
176
|
-
<DraggableList
|
|
177
|
-
onDragEnd={(res: DraggableResult) => reOrder(res.from, res.to)}
|
|
178
|
-
>
|
|
179
|
-
{items.map((submodule, idx) => (
|
|
180
|
-
<ValTreeArrayModuleItem
|
|
181
|
-
submodule={submodule}
|
|
182
|
-
idx={idx}
|
|
183
|
-
selectedModule={selectedModule}
|
|
184
|
-
setSelectedModule={setSelectedModule}
|
|
185
|
-
path={module.path}
|
|
186
|
-
key={idx}
|
|
187
|
-
reOrder={reOrder}
|
|
188
|
-
reorderMode={reOrderMode}
|
|
189
|
-
/>
|
|
190
|
-
))}
|
|
191
|
-
</DraggableList>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const ValTreeNavigatorModule: FC<{
|
|
198
|
-
module: SerializedModule;
|
|
199
|
-
setSelectedModule: Dispatch<SetStateAction<string>>;
|
|
200
|
-
}> = ({ module, setSelectedModule }) => {
|
|
201
|
-
return (
|
|
202
|
-
<div className="relative flex flex-col gap-3 px-4 py-2 hover:bg-light-gray/20 hover:rounded-lg ">
|
|
203
|
-
<button
|
|
204
|
-
className="flex items-center justify-between"
|
|
205
|
-
onClick={() => setSelectedModule(module.path)}
|
|
206
|
-
>
|
|
207
|
-
<div className="flex items-center gap-2">
|
|
208
|
-
<Chevron className={classNames("opacity-0")} />
|
|
209
|
-
<h1 className="text-xl">{module.path}</h1>
|
|
210
|
-
</div>
|
|
211
|
-
</button>
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
interface ValTreeNavigator {
|
|
217
|
-
modules: SerializedModule[];
|
|
218
|
-
selectedModule: string;
|
|
219
|
-
setSelectedModule: Dispatch<SetStateAction<string>>;
|
|
220
|
-
valApi: ValApi;
|
|
221
|
-
}
|
|
222
|
-
export const ValTreeNavigator: FC<ValTreeNavigator> = ({
|
|
223
|
-
modules,
|
|
224
|
-
selectedModule,
|
|
225
|
-
setSelectedModule,
|
|
226
|
-
valApi,
|
|
227
|
-
}) => {
|
|
228
|
-
console.log("hello?????????????", modules);
|
|
229
|
-
return (
|
|
230
|
-
<div
|
|
231
|
-
className={classNames("flex flex-col gap-4 font-serif text-lg px-4 py-3")}
|
|
232
|
-
>
|
|
233
|
-
{modules.map((module, idx) => (
|
|
234
|
-
<div key={idx}>
|
|
235
|
-
{module.schema.type === "array" ? (
|
|
236
|
-
<ValTreeNavigatorArrayModule
|
|
237
|
-
module={module}
|
|
238
|
-
key={idx}
|
|
239
|
-
selectedModule={selectedModule}
|
|
240
|
-
setSelectedModule={setSelectedModule}
|
|
241
|
-
valApi={valApi}
|
|
242
|
-
/>
|
|
243
|
-
) : (
|
|
244
|
-
<ValTreeNavigatorModule
|
|
245
|
-
module={module}
|
|
246
|
-
setSelectedModule={setSelectedModule}
|
|
247
|
-
/>
|
|
248
|
-
)}
|
|
249
|
-
</div>
|
|
250
|
-
))}
|
|
251
|
-
</div>
|
|
252
|
-
);
|
|
253
|
-
};
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { AnyRichTextOptions, RichTextSource } from "@valbuild/core";
|
|
2
|
-
import { LexicalEditor } from "lexical";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { RichTextEditor } from "../RichTextEditor/RichTextEditor";
|
|
5
|
-
import { FormContainer } from "./FormContainer";
|
|
6
|
-
import { ImageForm, ImageData } from "./ImageForm";
|
|
7
|
-
import { TextData, TextArea } from "./TextArea";
|
|
8
|
-
|
|
9
|
-
export type Inputs = {
|
|
10
|
-
[path: string]:
|
|
11
|
-
| { status: "requested" }
|
|
12
|
-
| {
|
|
13
|
-
status: "completed";
|
|
14
|
-
type: "text";
|
|
15
|
-
data: TextData;
|
|
16
|
-
}
|
|
17
|
-
| { status: "completed"; type: "image"; data: ImageData }
|
|
18
|
-
| {
|
|
19
|
-
status: "completed";
|
|
20
|
-
type: "richtext";
|
|
21
|
-
data: RichTextSource<AnyRichTextOptions>;
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type FormProps = {
|
|
26
|
-
onSubmit: (nextInputs: Inputs) => void;
|
|
27
|
-
inputs: Inputs;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export function Form({ onSubmit, inputs }: FormProps): React.ReactElement {
|
|
31
|
-
const [currentInputs, setCurrentInputs] = useState<Inputs>();
|
|
32
|
-
const [richTextEditor, setRichTextEditor] = useState<{
|
|
33
|
-
[path: string]: LexicalEditor;
|
|
34
|
-
}>();
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
setCurrentInputs(inputs);
|
|
38
|
-
}, [inputs]);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<FormContainer
|
|
42
|
-
onSubmit={() => {
|
|
43
|
-
if (currentInputs) {
|
|
44
|
-
onSubmit(
|
|
45
|
-
Object.fromEntries(
|
|
46
|
-
Object.entries(currentInputs).map(([path, input]) => {
|
|
47
|
-
if (input.status === "completed" && input.type === "richtext") {
|
|
48
|
-
if (!richTextEditor) {
|
|
49
|
-
throw Error(
|
|
50
|
-
"Cannot save rich text - editor not initialized"
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
return [
|
|
54
|
-
path,
|
|
55
|
-
{
|
|
56
|
-
status: "completed",
|
|
57
|
-
type: "richtext",
|
|
58
|
-
data: richTextEditor[path].getEditorState().toJSON()
|
|
59
|
-
?.root,
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
}
|
|
63
|
-
return [path, input];
|
|
64
|
-
})
|
|
65
|
-
)
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
{currentInputs &&
|
|
71
|
-
Object.entries(currentInputs).map(([path, input]) => (
|
|
72
|
-
<div key={path}>
|
|
73
|
-
{input.status === "requested" && (
|
|
74
|
-
<div className="p-2 text-center text-primary">Loading...</div>
|
|
75
|
-
)}
|
|
76
|
-
{input.status === "completed" && input.type === "image" && (
|
|
77
|
-
<ImageForm
|
|
78
|
-
name={path}
|
|
79
|
-
data={input.data}
|
|
80
|
-
onChange={(data) => {
|
|
81
|
-
if (data.value) {
|
|
82
|
-
setCurrentInputs({
|
|
83
|
-
...currentInputs,
|
|
84
|
-
[path]: {
|
|
85
|
-
status: "completed",
|
|
86
|
-
type: "image",
|
|
87
|
-
data: data.value,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}}
|
|
92
|
-
error={null}
|
|
93
|
-
/>
|
|
94
|
-
)}
|
|
95
|
-
{input.status === "completed" && input.type === "text" && (
|
|
96
|
-
<TextArea
|
|
97
|
-
name={path}
|
|
98
|
-
text={input.data}
|
|
99
|
-
onChange={(data) => {
|
|
100
|
-
setCurrentInputs({
|
|
101
|
-
...currentInputs,
|
|
102
|
-
[path]: {
|
|
103
|
-
status: "completed",
|
|
104
|
-
type: "text",
|
|
105
|
-
data: data,
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}}
|
|
109
|
-
/>
|
|
110
|
-
)}
|
|
111
|
-
{input.status === "completed" && input.type === "richtext" && (
|
|
112
|
-
<RichTextEditor
|
|
113
|
-
richtext={input.data}
|
|
114
|
-
onEditor={(editor) => {
|
|
115
|
-
setRichTextEditor({
|
|
116
|
-
...richTextEditor,
|
|
117
|
-
[path]: editor,
|
|
118
|
-
});
|
|
119
|
-
}}
|
|
120
|
-
/>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
))}
|
|
124
|
-
</FormContainer>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { PrimaryButton } from "../Button";
|
|
2
|
-
|
|
3
|
-
export function FormContainer({
|
|
4
|
-
children,
|
|
5
|
-
onSubmit,
|
|
6
|
-
}: {
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
onSubmit: () => void;
|
|
9
|
-
}) {
|
|
10
|
-
return (
|
|
11
|
-
<form
|
|
12
|
-
className="flex flex-col justify-between w-full px-4 py-2"
|
|
13
|
-
onSubmit={(ev) => {
|
|
14
|
-
ev.preventDefault();
|
|
15
|
-
onSubmit();
|
|
16
|
-
}}
|
|
17
|
-
>
|
|
18
|
-
{children}
|
|
19
|
-
<div className="flex justify-end">
|
|
20
|
-
<PrimaryButton>Save</PrimaryButton>
|
|
21
|
-
</div>
|
|
22
|
-
</form>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { UploadCloud } from "react-feather";
|
|
3
|
-
import { ErrorText } from "../ErrorText";
|
|
4
|
-
|
|
5
|
-
type Error = "invalid-file" | "file-too-large";
|
|
6
|
-
export type ImageData =
|
|
7
|
-
| {
|
|
8
|
-
src: string;
|
|
9
|
-
addMetadata: boolean;
|
|
10
|
-
metadata?: { width: number; height: number; sha256: string };
|
|
11
|
-
}
|
|
12
|
-
| {
|
|
13
|
-
url: string;
|
|
14
|
-
metadata?: { width: number; height: number; sha256: string };
|
|
15
|
-
}
|
|
16
|
-
| null;
|
|
17
|
-
export type ImageInputProps = {
|
|
18
|
-
name: string;
|
|
19
|
-
data: ImageData;
|
|
20
|
-
maxSize?: number;
|
|
21
|
-
error: Error | null;
|
|
22
|
-
onChange: (
|
|
23
|
-
result: { error: null; value: ImageData } | { error: Error; value: null }
|
|
24
|
-
) => void;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const textEncoder = new TextEncoder();
|
|
28
|
-
// TODO: handle hashes some other way
|
|
29
|
-
const getSHA256Hash = async (bits: Uint8Array) => {
|
|
30
|
-
const hashBuffer = await window.crypto.subtle.digest("SHA-256", bits);
|
|
31
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
32
|
-
const hash = hashArray
|
|
33
|
-
.map((item) => item.toString(16).padStart(2, "0"))
|
|
34
|
-
.join("");
|
|
35
|
-
return hash;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export function ImageForm({
|
|
39
|
-
name,
|
|
40
|
-
data: data,
|
|
41
|
-
maxSize,
|
|
42
|
-
error,
|
|
43
|
-
onChange,
|
|
44
|
-
}: ImageInputProps) {
|
|
45
|
-
const [currentData, setCurrentData] = useState<ImageData>(null);
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
setCurrentData(data);
|
|
48
|
-
}, [data]);
|
|
49
|
-
const [currentError, setCurrentError] = useState<Error | null>(null);
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
setCurrentError(error);
|
|
52
|
-
}, [data]);
|
|
53
|
-
|
|
54
|
-
// TODO: we should update the Input type - we should never have a url here?
|
|
55
|
-
const src =
|
|
56
|
-
(currentData &&
|
|
57
|
-
(("src" in currentData && currentData.src) ||
|
|
58
|
-
("url" in currentData && currentData.url))) ||
|
|
59
|
-
undefined;
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<div className="w-full py-2 max-w-[90vw] object-contain">
|
|
63
|
-
<label htmlFor={name}>
|
|
64
|
-
<div className="flex items-center justify-center w-full h-full min-h-[200px] cursor-pointer">
|
|
65
|
-
{currentData !== null && (
|
|
66
|
-
<img className="object-contain max-h-[300px]" src={src} />
|
|
67
|
-
)}
|
|
68
|
-
{!src && <UploadCloud size={24} className="text-primary" />}
|
|
69
|
-
</div>
|
|
70
|
-
<input
|
|
71
|
-
hidden
|
|
72
|
-
type="file"
|
|
73
|
-
id={name}
|
|
74
|
-
name={name}
|
|
75
|
-
onChange={(e) => {
|
|
76
|
-
const file = e.target.files?.[0];
|
|
77
|
-
if (file) {
|
|
78
|
-
if (maxSize && file.size > maxSize) {
|
|
79
|
-
onChange({ error: "file-too-large", value: null });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const reader = new FileReader();
|
|
83
|
-
reader.addEventListener("load", () => {
|
|
84
|
-
const result = reader.result;
|
|
85
|
-
if (typeof result === "string") {
|
|
86
|
-
const image = new Image();
|
|
87
|
-
image.onload = async () => {
|
|
88
|
-
const nextSource = {
|
|
89
|
-
src: result,
|
|
90
|
-
metadata: {
|
|
91
|
-
width: image.naturalWidth,
|
|
92
|
-
height: image.naturalHeight,
|
|
93
|
-
sha256: await getSHA256Hash(textEncoder.encode(result)),
|
|
94
|
-
},
|
|
95
|
-
addMetadata: !currentData?.metadata,
|
|
96
|
-
};
|
|
97
|
-
setCurrentData(nextSource);
|
|
98
|
-
setCurrentError(null);
|
|
99
|
-
onChange({
|
|
100
|
-
error: null,
|
|
101
|
-
value: nextSource,
|
|
102
|
-
});
|
|
103
|
-
};
|
|
104
|
-
image.src = result;
|
|
105
|
-
} else {
|
|
106
|
-
onChange({ error: "invalid-file", value: null });
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
reader.readAsDataURL(file);
|
|
110
|
-
}
|
|
111
|
-
}}
|
|
112
|
-
/>
|
|
113
|
-
</label>
|
|
114
|
-
{currentData &&
|
|
115
|
-
(!currentData.metadata ||
|
|
116
|
-
typeof currentData.metadata.height === "undefined" ||
|
|
117
|
-
typeof currentData.metadata.width === "undefined" ||
|
|
118
|
-
typeof currentData.metadata.sha256 === "undefined") && (
|
|
119
|
-
<ErrorText>
|
|
120
|
-
<div className="">
|
|
121
|
-
<div>Validation failed: missing metadata</div>
|
|
122
|
-
<button
|
|
123
|
-
className="px-4 py-2 border border-dark-gray"
|
|
124
|
-
onClick={async (ev) => {
|
|
125
|
-
ev.preventDefault();
|
|
126
|
-
|
|
127
|
-
const image = new Image();
|
|
128
|
-
image.onload = async () => {
|
|
129
|
-
const nextSource = {
|
|
130
|
-
src: image.src,
|
|
131
|
-
metadata: {
|
|
132
|
-
width: image.naturalWidth,
|
|
133
|
-
height: image.naturalHeight,
|
|
134
|
-
sha256: await getSHA256Hash(
|
|
135
|
-
textEncoder.encode(image.src)
|
|
136
|
-
),
|
|
137
|
-
},
|
|
138
|
-
addMetadata: !currentData?.metadata,
|
|
139
|
-
};
|
|
140
|
-
setCurrentData(nextSource);
|
|
141
|
-
setCurrentError(null);
|
|
142
|
-
onChange({
|
|
143
|
-
error: null,
|
|
144
|
-
value: nextSource,
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if ("url" in currentData) {
|
|
149
|
-
const src = await fetch(currentData.url)
|
|
150
|
-
.then((response) => response.blob())
|
|
151
|
-
.then((blob) => {
|
|
152
|
-
return new Promise<string>((resolve, reject) => {
|
|
153
|
-
const reader = new FileReader();
|
|
154
|
-
reader.onload = function () {
|
|
155
|
-
if (typeof this.result !== "string") {
|
|
156
|
-
return reject(
|
|
157
|
-
Error(
|
|
158
|
-
`Could not read file from url: ${currentData.url}`
|
|
159
|
-
)
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
resolve(this.result);
|
|
163
|
-
}; // <--- `this.result` contains a base64 data URI
|
|
164
|
-
reader.readAsDataURL(blob);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
image.src = src;
|
|
168
|
-
} else {
|
|
169
|
-
image.src = currentData.src;
|
|
170
|
-
}
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
Fix
|
|
174
|
-
</button>
|
|
175
|
-
</div>
|
|
176
|
-
</ErrorText>
|
|
177
|
-
)}
|
|
178
|
-
{currentData?.metadata && (
|
|
179
|
-
<div className="ml-auto text-primary">
|
|
180
|
-
Dimensions: {currentData.metadata.width}x{currentData.metadata.height}
|
|
181
|
-
</div>
|
|
182
|
-
)}
|
|
183
|
-
{currentError && <ImageError error={currentError} />}
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function ImageError({ error }: { error: Error }): React.ReactElement | null {
|
|
189
|
-
if (error === "invalid-file") {
|
|
190
|
-
return <ErrorText>Invalid file</ErrorText>;
|
|
191
|
-
} else if (error === "file-too-large") {
|
|
192
|
-
return <ErrorText>File is too large</ErrorText>;
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export type TextData = string;
|
|
2
|
-
export type TextInputProps = {
|
|
3
|
-
name?: string;
|
|
4
|
-
text: TextData;
|
|
5
|
-
disabled?: boolean;
|
|
6
|
-
onChange: (value: string) => void;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function TextArea({ name, disabled, text, onChange }: TextInputProps) {
|
|
10
|
-
return (
|
|
11
|
-
<div
|
|
12
|
-
className="w-full h-full py-2 overflow-y-scroll grow-wrap"
|
|
13
|
-
data-replicated-value={text} /* see grow-wrap */
|
|
14
|
-
>
|
|
15
|
-
<textarea
|
|
16
|
-
disabled={disabled}
|
|
17
|
-
name={name}
|
|
18
|
-
className="p-2 border outline-none resize-none bg-fill text-primary border-border focus-visible:border-highlight"
|
|
19
|
-
defaultValue={text}
|
|
20
|
-
onChange={(e) => onChange(e.target.value)}
|
|
21
|
-
/>
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
3
|
-
import { ChevronDown } from "lucide-react";
|
|
4
|
-
|
|
5
|
-
import { cn } from "../../lib/utils";
|
|
6
|
-
|
|
7
|
-
const Accordion = AccordionPrimitive.Root;
|
|
8
|
-
|
|
9
|
-
const AccordionItem = React.forwardRef<
|
|
10
|
-
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
11
|
-
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
12
|
-
>(({ className, ...props }, ref) => (
|
|
13
|
-
<AccordionPrimitive.Item
|
|
14
|
-
ref={ref}
|
|
15
|
-
className={cn("border-b", className)}
|
|
16
|
-
{...props}
|
|
17
|
-
/>
|
|
18
|
-
));
|
|
19
|
-
AccordionItem.displayName = "AccordionItem";
|
|
20
|
-
|
|
21
|
-
const AccordionTrigger = React.forwardRef<
|
|
22
|
-
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
23
|
-
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
24
|
-
>(({ className, children, ...props }, ref) => (
|
|
25
|
-
<AccordionPrimitive.Header className="flex">
|
|
26
|
-
<AccordionPrimitive.Trigger
|
|
27
|
-
ref={ref}
|
|
28
|
-
className={cn(
|
|
29
|
-
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
30
|
-
className
|
|
31
|
-
)}
|
|
32
|
-
{...props}
|
|
33
|
-
>
|
|
34
|
-
{children}
|
|
35
|
-
<ChevronDown className="w-4 h-4 transition-transform duration-200 shrink-0" />
|
|
36
|
-
</AccordionPrimitive.Trigger>
|
|
37
|
-
</AccordionPrimitive.Header>
|
|
38
|
-
));
|
|
39
|
-
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
|
40
|
-
|
|
41
|
-
const AccordionContent = React.forwardRef<
|
|
42
|
-
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
43
|
-
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
44
|
-
>(({ className, children, ...props }, ref) => (
|
|
45
|
-
<AccordionPrimitive.Content
|
|
46
|
-
ref={ref}
|
|
47
|
-
className={cn(
|
|
48
|
-
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
|
49
|
-
className
|
|
50
|
-
)}
|
|
51
|
-
{...props}
|
|
52
|
-
>
|
|
53
|
-
<div className="pt-0 pb-4">{children}</div>
|
|
54
|
-
</AccordionPrimitive.Content>
|
|
55
|
-
));
|
|
56
|
-
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
|
57
|
-
|
|
58
|
-
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|