@riducms/plugin-richtext 0.1.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.
@@ -0,0 +1,282 @@
1
+ <script lang="ts">
2
+ import {
3
+ useLexicalComposerContext,
4
+ useLexicalEditable,
5
+ useLexicalNodeSelection,
6
+ } from "@hvniel/lexical-svelte";
7
+ import { getAdminI18n } from "@riducms/plugin";
8
+ import type { NodeKey } from "lexical";
9
+ import { Textarea, TooltipContent, TooltipRoot, TooltipTrigger } from "@riducms/ui";
10
+
11
+ import { getRichTextAuthoringHost } from "@plugin-richtext/field/rich-text-context.svelte";
12
+ import {
13
+ OPEN_UPLOAD_BROWSER_COMMAND,
14
+ REMOVE_UPLOAD_COMMAND,
15
+ UPDATE_UPLOAD_CAPTION_COMMAND,
16
+ } from "@plugin-richtext/menu/rich-text-commands";
17
+
18
+ let {
19
+ caption,
20
+ documentID,
21
+ nodeKey,
22
+ relationTo,
23
+ }: { caption: string; documentID: string; nodeKey: NodeKey; relationTo: string } = $props();
24
+ const editor = useLexicalComposerContext()[0];
25
+ const isEditable = useLexicalEditable();
26
+ const i18n = getAdminI18n();
27
+ // A decorator node key is fixed for this component's lifetime.
28
+ // svelte-ignore state_referenced_locally
29
+ const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
30
+ const authoring = getRichTextAuthoringHost();
31
+ let document = $state.raw<Record<string, unknown>>();
32
+ let loadError = $state(false);
33
+ const collection = $derived(
34
+ authoring?.collections.find((candidate) => candidate.slug === relationTo)
35
+ );
36
+ const filename = $derived(String(document?.filename ?? documentID));
37
+ const altText = $derived(typeof document?.alt === "string" ? document.alt.trim() : "");
38
+ const extension = $derived(fileExtension(filename));
39
+ const imageURL = $derived(
40
+ typeof document?.url === "string" &&
41
+ typeof document.mimeType === "string" &&
42
+ document.mimeType.startsWith("image/")
43
+ ? document.url
44
+ : undefined
45
+ );
46
+
47
+ $effect(() => {
48
+ if (authoring === undefined) return;
49
+ void authoring.documentRevision;
50
+ const request = new AbortController();
51
+ authoring
52
+ .findDocument(relationTo, documentID, request.signal)
53
+ .then((value) => {
54
+ if (request.signal.aborted) return;
55
+ document = value;
56
+ loadError = false;
57
+ })
58
+ .catch(() => {
59
+ if (request.signal.aborted) return;
60
+ loadError = true;
61
+ });
62
+ return () => request.abort();
63
+ });
64
+
65
+ function selectNode(event?: MouseEvent) {
66
+ if (event?.shiftKey !== true) clearSelection();
67
+ setSelected(true);
68
+ }
69
+
70
+ function openDocument(event: MouseEvent) {
71
+ selectNode(event);
72
+ editor.dispatchCommand(OPEN_UPLOAD_BROWSER_COMMAND, {
73
+ collectionSlug: relationTo,
74
+ documentID,
75
+ mode: "edit",
76
+ });
77
+ }
78
+
79
+ function replaceDocument() {
80
+ editor.dispatchCommand(OPEN_UPLOAD_BROWSER_COMMAND, {
81
+ collectionSlug: relationTo,
82
+ documentID,
83
+ mode: "replace",
84
+ nodeKey,
85
+ });
86
+ }
87
+
88
+ function removeUpload() {
89
+ editor.dispatchCommand(REMOVE_UPLOAD_COMMAND, nodeKey);
90
+ queueMicrotask(() => editor.focus());
91
+ }
92
+
93
+ function updateCaption(event: Event) {
94
+ if (!(event.currentTarget instanceof HTMLTextAreaElement)) return;
95
+ editor.dispatchCommand(UPDATE_UPLOAD_CAPTION_COMMAND, {
96
+ caption: event.currentTarget.value,
97
+ nodeKey,
98
+ });
99
+ }
100
+
101
+ function handleCaptionKeydown(event: KeyboardEvent) {
102
+ event.stopPropagation();
103
+ if (event.key !== "Escape" || !(event.currentTarget instanceof HTMLTextAreaElement)) return;
104
+ event.preventDefault();
105
+ event.currentTarget.blur();
106
+ queueMicrotask(() => editor.focus());
107
+ }
108
+
109
+ function fileExtension(value: string) {
110
+ const match = /\.([a-z0-9]{1,8})$/i.exec(value);
111
+ return match?.[1]?.toLocaleUpperCase(i18n.language) ?? i18n.t("plugin.richtext:asset.file");
112
+ }
113
+ </script>
114
+
115
+ <figure
116
+ class={[
117
+ "group/upload relative my-[1.85rem] w-full overflow-hidden rounded-[4px] border border-control-border bg-control leading-[0] transition-colors duration-150 hover:border-primary/50 focus-within:border-primary/50",
118
+ isSelected() && "border-primary/50",
119
+ ]}
120
+ role="group"
121
+ aria-label={i18n.t("plugin.richtext:asset.label", {
122
+ collection: collection?.labels.singular ?? relationTo,
123
+ filename,
124
+ })}
125
+ >
126
+ <div class="relative w-full">
127
+ {#if isEditable()}
128
+ <button
129
+ type="button"
130
+ class="block w-full cursor-pointer border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-primary/65 focus-visible:ring-inset"
131
+ aria-label={i18n.t("plugin.richtext:editor.editNamed", { name: filename })}
132
+ onclick={openDocument}
133
+ >
134
+ <span
135
+ class={[
136
+ "ridu-richtext-upload-preview relative grid aspect-[16/7] w-full place-items-center overflow-hidden bg-background",
137
+ document === undefined && !loadError && "is-loading",
138
+ ]}
139
+ >
140
+ {#if imageURL !== undefined}
141
+ <img
142
+ class="absolute inset-0 block size-full object-cover"
143
+ src={imageURL}
144
+ alt={altText}
145
+ />
146
+ {:else}
147
+ <span
148
+ class="grid max-w-[80%] place-items-center gap-[0.45rem] font-mono text-[11px] leading-normal tracking-[0.12em] text-foreground-muted uppercase"
149
+ aria-hidden="true"
150
+ >
151
+ <strong class="text-[13px] font-medium">
152
+ {loadError ? i18n.t("plugin.richtext:asset.unavailable") : extension}
153
+ </strong>
154
+ <span class="max-w-full truncate text-foreground-faint">
155
+ {loadError ? i18n.t("plugin.richtext:asset.couldNotLoad") : filename}
156
+ </span>
157
+ </span>
158
+ {/if}
159
+ </span>
160
+ </button>
161
+ {:else}
162
+ <span
163
+ class={[
164
+ "ridu-richtext-upload-preview relative grid aspect-[16/7] w-full place-items-center overflow-hidden bg-background",
165
+ document === undefined && !loadError && "is-loading",
166
+ ]}
167
+ >
168
+ {#if imageURL !== undefined}
169
+ <img class="absolute inset-0 block size-full object-cover" src={imageURL} alt={altText} />
170
+ {:else}
171
+ <span
172
+ class="font-mono text-[11px] leading-normal tracking-[0.12em] text-foreground-muted uppercase"
173
+ >
174
+ {loadError ? i18n.t("plugin.richtext:asset.unavailable") : extension}
175
+ </span>
176
+ {/if}
177
+ </span>
178
+ {/if}
179
+
180
+ {#if isEditable()}
181
+ <div
182
+ class={[
183
+ "pointer-events-none absolute top-3 left-1/2 z-2 flex max-w-[calc(100%-1.5rem)] -translate-x-1/2 items-center gap-1 rounded-[4px] border border-media-foreground/10 bg-media-overlay p-1 text-media-foreground opacity-0 shadow-[var(--shadow-popover)] backdrop-blur-md transition-opacity duration-150 group-hover/upload:pointer-events-auto group-hover/upload:opacity-100 group-focus-within/upload:pointer-events-auto group-focus-within/upload:opacity-100",
184
+ isSelected() && "pointer-events-auto opacity-100",
185
+ ]}
186
+ aria-label={i18n.t("plugin.richtext:asset.actions")}
187
+ >
188
+ <button
189
+ type="button"
190
+ class="min-w-0 cursor-pointer truncate rounded-[3px] border-0 bg-transparent px-2 py-1.5 text-start text-[11.5px] leading-[1.2] text-media-foreground/88 hover:bg-media-foreground/10 focus-visible:bg-media-foreground/10 focus-visible:outline-none"
191
+ onclick={openDocument}
192
+ aria-label={i18n.t("plugin.richtext:editor.editNamed", { name: filename })}
193
+ >
194
+ {filename}
195
+ </button>
196
+ <span class="mx-0.5 h-4 w-px shrink-0 bg-media-foreground/16" aria-hidden="true"></span>
197
+ <TooltipRoot>
198
+ <TooltipTrigger>
199
+ {#snippet child({ props })}
200
+ <button
201
+ {...props}
202
+ type="button"
203
+ class="grid size-7 shrink-0 cursor-pointer place-items-center rounded-[3px] border-0 bg-transparent text-media-foreground/75 hover:bg-media-foreground/10 hover:text-media-foreground focus-visible:bg-media-foreground/10 focus-visible:text-media-foreground focus-visible:outline-none"
204
+ onclick={replaceDocument}
205
+ aria-label={i18n.t("plugin.richtext:editor.replaceNamed", { name: filename })}
206
+ >
207
+ <svg
208
+ class="size-3.5"
209
+ viewBox="0 0 24 24"
210
+ fill="none"
211
+ stroke="currentColor"
212
+ stroke-width="2"
213
+ aria-hidden="true"
214
+ >
215
+ <path
216
+ d="M20 11a8.1 8.1 0 0 0-15.5-2M4 4v5h5M4 13a8.1 8.1 0 0 0 15.5 2M20 20v-5h-5"
217
+ />
218
+ </svg>
219
+ </button>
220
+ {/snippet}
221
+ </TooltipTrigger>
222
+ <TooltipContent>{i18n.t("plugin.richtext:asset.replace")}</TooltipContent>
223
+ </TooltipRoot>
224
+ <TooltipRoot>
225
+ <TooltipTrigger>
226
+ {#snippet child({ props })}
227
+ <button
228
+ {...props}
229
+ type="button"
230
+ class="grid size-7 shrink-0 cursor-pointer place-items-center rounded-[3px] border-0 bg-transparent text-media-foreground/75 hover:bg-destructive/25 hover:text-media-foreground focus-visible:bg-destructive/25 focus-visible:text-media-foreground focus-visible:outline-none"
231
+ onclick={removeUpload}
232
+ aria-label={i18n.t("plugin.richtext:editor.removeNamed", { name: filename })}
233
+ >
234
+ <svg
235
+ class="size-3.5"
236
+ viewBox="0 0 24 24"
237
+ fill="none"
238
+ stroke="currentColor"
239
+ stroke-width="2"
240
+ aria-hidden="true"
241
+ >
242
+ <path d="M18 6 6 18M6 6l12 12" />
243
+ </svg>
244
+ </button>
245
+ {/snippet}
246
+ </TooltipTrigger>
247
+ <TooltipContent>{i18n.t("plugin.richtext:asset.remove")}</TooltipContent>
248
+ </TooltipRoot>
249
+ </div>
250
+ {/if}
251
+ </div>
252
+
253
+ {#if isEditable() || caption !== ""}
254
+ <figcaption class="border-t border-border bg-background/45 px-[0.85rem] py-[0.55rem]">
255
+ {#if isEditable()}
256
+ <div class="flex items-start gap-2">
257
+ <svg
258
+ class="mt-[0.3rem] size-3 shrink-0 text-foreground-faint"
259
+ viewBox="0 0 24 24"
260
+ fill="none"
261
+ stroke="currentColor"
262
+ stroke-width="2"
263
+ aria-hidden="true"
264
+ >
265
+ <path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4Z" />
266
+ </svg>
267
+ <Textarea
268
+ class="min-h-[1.45rem] w-full resize-none overflow-hidden border-0 bg-transparent p-0 text-start text-[13px] leading-[1.45] text-foreground-muted outline-none field-sizing-content placeholder:text-foreground-faint focus:text-foreground"
269
+ rows={1}
270
+ value={caption}
271
+ oninput={updateCaption}
272
+ onkeydown={handleCaptionKeydown}
273
+ aria-label={i18n.t("plugin.richtext:asset.captionFor", { filename })}
274
+ placeholder={i18n.t("plugin.richtext:asset.addCaption")}
275
+ />
276
+ </div>
277
+ {:else}
278
+ <p class="text-start text-[13px] leading-[1.45] text-foreground-muted">{caption}</p>
279
+ {/if}
280
+ </figcaption>
281
+ {/if}
282
+ </figure>
@@ -0,0 +1,144 @@
1
+ import type { Decorator } from "@hvniel/lexical-svelte";
2
+ import { DecoratorBlockNode } from "@hvniel/lexical-svelte";
3
+ import {
4
+ $applyNodeReplacement,
5
+ $getDocument,
6
+ type ElementFormatType,
7
+ type LexicalNode,
8
+ type LexicalUpdateJSON,
9
+ type NodeKey,
10
+ type SerializedLexicalNode,
11
+ type Spread,
12
+ } from "lexical";
13
+
14
+ import RichTextUploadNodeComponent from "@plugin-richtext/upload/rich-text-upload-node.svelte";
15
+
16
+ export type SerializedUploadNode = Spread<
17
+ {
18
+ caption?: string;
19
+ format: ElementFormatType;
20
+ id: string;
21
+ relationTo: string;
22
+ },
23
+ SerializedLexicalNode
24
+ >;
25
+
26
+ export class UploadNode extends DecoratorBlockNode {
27
+ __caption: string;
28
+ __relationTo: string;
29
+ __documentID: string;
30
+
31
+ static getType(): string {
32
+ return "upload";
33
+ }
34
+
35
+ static clone(node: UploadNode): UploadNode {
36
+ return new UploadNode(
37
+ node.__relationTo,
38
+ node.__documentID,
39
+ node.__caption,
40
+ node.__format,
41
+ node.__key
42
+ );
43
+ }
44
+
45
+ static importJSON(serializedNode: SerializedUploadNode): UploadNode {
46
+ return createUploadNode({
47
+ caption: serializedNode.caption ?? "",
48
+ documentID: serializedNode.id,
49
+ format: serializedNode.format,
50
+ relationTo: serializedNode.relationTo,
51
+ }).updateFromJSON(serializedNode);
52
+ }
53
+
54
+ constructor(
55
+ relationTo: string,
56
+ documentID: string,
57
+ caption = "",
58
+ format?: ElementFormatType,
59
+ key?: NodeKey
60
+ ) {
61
+ super(format, key);
62
+ this.__relationTo = relationTo;
63
+ this.__documentID = documentID;
64
+ this.__caption = caption;
65
+ }
66
+
67
+ updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedUploadNode>): this {
68
+ const node = super.updateFromJSON(serializedNode);
69
+ node.__relationTo = serializedNode.relationTo;
70
+ node.__documentID = serializedNode.id;
71
+ node.__caption = serializedNode.caption ?? "";
72
+ return node;
73
+ }
74
+
75
+ exportJSON(): SerializedUploadNode {
76
+ return {
77
+ ...super.exportJSON(),
78
+ ...(this.__caption === "" ? {} : { caption: this.__caption }),
79
+ id: this.__documentID,
80
+ relationTo: this.__relationTo,
81
+ };
82
+ }
83
+
84
+ createDOM(): HTMLElement {
85
+ const element = $getDocument().createElement("div");
86
+ element.className = "ridu-richtext-upload";
87
+ return element;
88
+ }
89
+
90
+ decorate(): Decorator<typeof RichTextUploadNodeComponent> {
91
+ return {
92
+ component: RichTextUploadNodeComponent,
93
+ props: {
94
+ caption: this.__caption,
95
+ documentID: this.__documentID,
96
+ nodeKey: this.getKey(),
97
+ relationTo: this.__relationTo,
98
+ },
99
+ };
100
+ }
101
+
102
+ getDocumentID(): string {
103
+ return this.getLatest().__documentID;
104
+ }
105
+
106
+ getCaption(): string {
107
+ return this.getLatest().__caption;
108
+ }
109
+
110
+ getRelationTo(): string {
111
+ return this.getLatest().__relationTo;
112
+ }
113
+
114
+ setReference(relationTo: string, documentID: string): this {
115
+ const writable = this.getWritable();
116
+ writable.__relationTo = relationTo;
117
+ writable.__documentID = documentID;
118
+ return writable;
119
+ }
120
+
121
+ setCaption(caption: string): this {
122
+ const writable = this.getWritable();
123
+ writable.__caption = caption;
124
+ return writable;
125
+ }
126
+ }
127
+
128
+ export function createUploadNode({
129
+ caption = "",
130
+ documentID,
131
+ format,
132
+ relationTo,
133
+ }: {
134
+ caption?: string;
135
+ documentID: string;
136
+ format?: ElementFormatType;
137
+ relationTo: string;
138
+ }): UploadNode {
139
+ return $applyNodeReplacement(new UploadNode(relationTo, documentID, caption, format));
140
+ }
141
+
142
+ export function isUploadNode(node: LexicalNode | null | undefined): node is UploadNode {
143
+ return node instanceof UploadNode;
144
+ }
@@ -0,0 +1,183 @@
1
+ <script lang="ts">
2
+ import type { FieldAuthoringHost } from "@riducms/plugin";
3
+ import type { SchemaCollection, SchemaField } from "@riducms/protocol";
4
+ import { $insertNodeToNearestRoot, mergeRegister } from "@lexical/utils";
5
+ import {
6
+ $createParagraphNode,
7
+ $getNodeByKey,
8
+ $getPreviousSelection,
9
+ $getRoot,
10
+ $getSelection,
11
+ $isElementNode,
12
+ $isParagraphNode,
13
+ $isRangeSelection,
14
+ COMMAND_PRIORITY_EDITOR,
15
+ type NodeKey,
16
+ } from "lexical";
17
+ import { useLexicalComposerContext } from "@hvniel/lexical-svelte";
18
+
19
+ import {
20
+ OPEN_UPLOAD_BROWSER_COMMAND,
21
+ REMOVE_UPLOAD_COMMAND,
22
+ UPDATE_UPLOAD_CAPTION_COMMAND,
23
+ } from "@plugin-richtext/menu/rich-text-commands";
24
+ import type { RichTextConfig } from "@plugin-richtext/field/rich-text-config";
25
+ import {
26
+ createUploadNode,
27
+ isUploadNode,
28
+ UploadNode,
29
+ } from "@plugin-richtext/upload/rich-text-upload-node";
30
+
31
+ let {
32
+ authoring,
33
+ config,
34
+ field,
35
+ }: {
36
+ authoring: FieldAuthoringHost | undefined;
37
+ config: RichTextConfig;
38
+ field: SchemaField;
39
+ } = $props();
40
+ const editor = useLexicalComposerContext()[0];
41
+ let browserOpen = $state(false);
42
+ let targetCollection = $state<SchemaCollection>();
43
+ let documentID = $state<string>();
44
+ let browserMode = $state<"edit" | "insert" | "replace">("insert");
45
+ let replaceNodeKey: NodeKey | undefined;
46
+ const uploadCollections = $derived(
47
+ (authoring?.collections ?? []).filter(
48
+ (collection) =>
49
+ collection.capabilities.upload &&
50
+ (config.uploadCollections.length === 0 ||
51
+ config.uploadCollections.includes(collection.slug))
52
+ )
53
+ );
54
+ const browserField = $derived(
55
+ targetCollection === undefined ? undefined : createBrowserField(field, targetCollection)
56
+ );
57
+
58
+ $effect(() => {
59
+ if (!editor.hasNodes([UploadNode])) {
60
+ throw new Error("RichTextUploadPlugin requires UploadNode to be registered");
61
+ }
62
+ return mergeRegister(
63
+ editor.registerCommand(
64
+ OPEN_UPLOAD_BROWSER_COMMAND,
65
+ (payload) => {
66
+ const collection = uploadCollections.find(
67
+ (candidate) => candidate.slug === payload.collectionSlug
68
+ );
69
+ if (collection === undefined) return false;
70
+ browserMode =
71
+ payload.mode ??
72
+ (payload.nodeKey !== undefined
73
+ ? "replace"
74
+ : payload.documentID !== undefined
75
+ ? "edit"
76
+ : "insert");
77
+ targetCollection = collection;
78
+ documentID = payload.documentID;
79
+ replaceNodeKey = browserMode === "replace" ? payload.nodeKey : undefined;
80
+ browserOpen = true;
81
+ return true;
82
+ },
83
+ COMMAND_PRIORITY_EDITOR
84
+ ),
85
+ editor.registerCommand(
86
+ UPDATE_UPLOAD_CAPTION_COMMAND,
87
+ ({ caption, nodeKey }) => {
88
+ const node = $getNodeByKey(nodeKey);
89
+ if (!isUploadNode(node)) return false;
90
+ node.setCaption(caption);
91
+ return true;
92
+ },
93
+ COMMAND_PRIORITY_EDITOR
94
+ ),
95
+ editor.registerCommand(
96
+ REMOVE_UPLOAD_COMMAND,
97
+ (nodeKey) => {
98
+ const node = $getNodeByKey(nodeKey);
99
+ if (!isUploadNode(node)) return false;
100
+ const next = node.getNextSibling();
101
+ const previous = node.getPreviousSibling();
102
+ node.remove();
103
+ if ($isElementNode(next)) next.selectStart();
104
+ else if ($isElementNode(previous)) previous.selectEnd();
105
+ else {
106
+ const paragraph = $createParagraphNode();
107
+ $getRoot().append(paragraph);
108
+ paragraph.select();
109
+ }
110
+ return true;
111
+ },
112
+ COMMAND_PRIORITY_EDITOR
113
+ )
114
+ );
115
+ });
116
+
117
+ function commit(ids: string[]) {
118
+ const id = ids[0];
119
+ const collection = targetCollection;
120
+ if (id === undefined || collection === undefined) return;
121
+ if (browserMode === "edit") {
122
+ closeBrowser();
123
+ return;
124
+ }
125
+ editor.update(() => {
126
+ if (replaceNodeKey !== undefined) {
127
+ const current = $getNodeByKey(replaceNodeKey);
128
+ if (isUploadNode(current)) current.setReference(collection.slug, id);
129
+ return;
130
+ }
131
+ const selection = $getSelection() ?? $getPreviousSelection();
132
+ if (!$isRangeSelection(selection)) $getRoot().selectEnd();
133
+ const focusNode = $isRangeSelection(selection) ? selection.focus.getNode() : undefined;
134
+ const upload = createUploadNode({ documentID: id, relationTo: collection.slug });
135
+ $insertNodeToNearestRoot(upload);
136
+ if ($isParagraphNode(focusNode) && focusNode.getChildrenSize() === 0) focusNode.remove();
137
+ const paragraph = $createParagraphNode();
138
+ upload.insertAfter(paragraph);
139
+ paragraph.select();
140
+ });
141
+ closeBrowser();
142
+ }
143
+
144
+ function closeBrowser() {
145
+ browserOpen = false;
146
+ browserMode = "insert";
147
+ documentID = undefined;
148
+ replaceNodeKey = undefined;
149
+ queueMicrotask(() => editor.focus());
150
+ }
151
+
152
+ function createBrowserField(field: SchemaField, collection: SchemaCollection): SchemaField {
153
+ return {
154
+ id: `${field.id}-richtext-upload`,
155
+ name: `${field.name}Upload`,
156
+ path: `${field.path}.__upload`,
157
+ type: "upload",
158
+ category: "upload",
159
+ required: false,
160
+ unique: false,
161
+ admin: { label: collection.labels.singular },
162
+ upload: {
163
+ collectionId: collection.id,
164
+ collectionSlug: collection.slug,
165
+ onDelete: "nullify",
166
+ },
167
+ };
168
+ }
169
+ </script>
170
+
171
+ {#if browserOpen && authoring !== undefined && targetCollection !== undefined && browserField !== undefined}
172
+ {const ReferenceBrowser = authoring.referenceBrowser}
173
+ <ReferenceBrowser
174
+ open
175
+ field={browserField}
176
+ collection={targetCollection}
177
+ hasMany={false}
178
+ selectedIDs={documentID === undefined ? [] : [documentID]}
179
+ {...browserMode === "edit" && documentID !== undefined ? { initialDocumentID: documentID } : {}}
180
+ onCommit={commit}
181
+ onClose={closeBrowser}
182
+ />
183
+ {/if}
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />