@ctrl-ui/react 0.8.17 → 0.8.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/blocks/coding-agent.js +1 -1
  2. package/dist/blocks/design-canvas-inspector.js +1 -1
  3. package/dist/blocks/design-canvas.js +9 -5
  4. package/dist/blocks/discussion-markdown-input.d.ts +12 -0
  5. package/dist/blocks/discussion-markdown-input.js +34 -0
  6. package/dist/blocks/discussion.d.ts +7 -3
  7. package/dist/blocks/discussion.js +77 -16
  8. package/dist/blocks/file-explorer.js +4 -4
  9. package/dist/blocks/settings.js +5 -3
  10. package/dist/knob-contracts/label-knobs.d.ts +4 -0
  11. package/dist/knob-contracts/label-knobs.js +4 -0
  12. package/dist/knob-contracts/markdown-editor-knobs.d.ts +4 -0
  13. package/dist/knob-contracts/markdown-editor-knobs.js +16 -0
  14. package/dist/knob-contracts/switch-knobs.d.ts +1 -1
  15. package/dist/knob-contracts/switch-knobs.js +1 -0
  16. package/dist/markdown-editor/context.d.ts +22 -0
  17. package/dist/markdown-editor/context.js +10 -0
  18. package/dist/markdown-editor/extensions.d.ts +10 -0
  19. package/dist/markdown-editor/extensions.js +139 -0
  20. package/dist/markdown-editor/toolbar.d.ts +9 -0
  21. package/dist/markdown-editor/toolbar.js +336 -0
  22. package/dist/markdown-editor/uploads.d.ts +23 -0
  23. package/dist/markdown-editor/uploads.js +32 -0
  24. package/dist/markdown-editor/use-image-uploads.d.ts +12 -0
  25. package/dist/markdown-editor/use-image-uploads.js +120 -0
  26. package/dist/markdown-editor.d.ts +27 -0
  27. package/dist/markdown-editor.js +192 -0
  28. package/dist/styles/index.css +4 -2
  29. package/dist/styles/recipes/code.css +4 -0
  30. package/dist/styles/recipes/field.css +1 -1
  31. package/dist/styles/recipes/label.css +19 -0
  32. package/dist/styles/recipes/markdown-editor.css +196 -0
  33. package/dist/styles/recipes/switch.css +17 -5
  34. package/dist/styles/theme.css +25 -0
  35. package/dist/styles/type-scale.css +28 -2
  36. package/dist/ui/app-shell.js +1 -1
  37. package/dist/ui/autocomplete.js +1 -1
  38. package/dist/ui/code.js +1 -0
  39. package/dist/ui/combobox.js +1 -1
  40. package/dist/ui/command.js +1 -1
  41. package/dist/ui/dockable-panel.js +1 -1
  42. package/dist/ui/label.d.ts +8 -0
  43. package/dist/ui/label.js +14 -0
  44. package/dist/ui/markdown.d.ts +2 -1
  45. package/dist/ui/markdown.js +3 -3
  46. package/dist/ui/responsive-dialog.js +1 -1
  47. package/dist/ui/sidebar-menu.js +1 -1
  48. package/dist/ui/sidebar.js +1 -1
  49. package/dist/ui/typography.d.ts +44 -0
  50. package/dist/ui/typography.js +59 -0
  51. package/dist/ui/use-switch-drag.js +1 -1
  52. package/package.json +37 -1
@@ -0,0 +1,336 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { ChatComposerAttachment, ChatComposerAttachments } from "../chat-composer-attachment.js";
4
+ import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "../ui/popover.js";
5
+ import { Input } from "../ui/input.js";
6
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
7
+ import { useMarkdownEditor } from "./context.js";
8
+ import { isEditorUrl } from "./extensions.js";
9
+ import { markdownImageTypes } from "./uploads.js";
10
+ import { Label } from "../ui/label.js";
11
+ import { Toolbar, ToolbarButton, ToolbarSeparator } from "../ui/toolbar.js";
12
+ import { useId, useRef, useState } from "react";
13
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
14
+ import { BoldIcon, CodeIcon, Heading2Icon, ImageIcon, ItalicIcon, LinkIcon, ListChecksIcon, ListIcon, ListOrderedIcon, QuoteIcon, RedoIcon, SquareCodeIcon, StrikethroughIcon, UndoIcon } from "lucide-react";
15
+ import { useEditorState } from "@tiptap/react";
16
+ //#region src/markdown-editor/toolbar.tsx
17
+ function MarkdownEditorToolbar({ children }) {
18
+ const { editor, disabled, source, sourceRequired, setSource, uploads } = useMarkdownEditor();
19
+ const state = useEditorState({
20
+ editor,
21
+ selector: ({ editor: current }) => ({
22
+ bold: current?.isActive("bold"),
23
+ italic: current?.isActive("italic"),
24
+ strike: current?.isActive("strike"),
25
+ code: current?.isActive("code"),
26
+ heading: current?.isActive("heading", { level: 2 }),
27
+ bulletList: current?.isActive("bulletList"),
28
+ orderedList: current?.isActive("orderedList"),
29
+ taskList: current?.isActive("taskList"),
30
+ blockquote: current?.isActive("blockquote"),
31
+ codeBlock: current?.isActive("codeBlock"),
32
+ undo: current?.can().undo(),
33
+ redo: current?.can().redo()
34
+ })
35
+ });
36
+ const actions = [
37
+ {
38
+ label: "Bold",
39
+ Icon: BoldIcon,
40
+ active: state?.bold,
41
+ run: () => editor?.chain().focus().toggleBold().run()
42
+ },
43
+ {
44
+ label: "Italic",
45
+ Icon: ItalicIcon,
46
+ active: state?.italic,
47
+ run: () => editor?.chain().focus().toggleItalic().run()
48
+ },
49
+ {
50
+ label: "Strikethrough",
51
+ Icon: StrikethroughIcon,
52
+ active: state?.strike,
53
+ run: () => editor?.chain().focus().toggleStrike().run()
54
+ },
55
+ {
56
+ label: "Inline code",
57
+ Icon: CodeIcon,
58
+ active: state?.code,
59
+ run: () => editor?.chain().focus().toggleCode().run()
60
+ },
61
+ {
62
+ label: "Heading",
63
+ Icon: Heading2Icon,
64
+ active: state?.heading,
65
+ run: () => editor?.chain().focus().toggleHeading({ level: 2 }).run()
66
+ },
67
+ {
68
+ label: "Bullet list",
69
+ Icon: ListIcon,
70
+ active: state?.bulletList,
71
+ run: () => editor?.chain().focus().toggleBulletList().run()
72
+ },
73
+ {
74
+ label: "Numbered list",
75
+ Icon: ListOrderedIcon,
76
+ active: state?.orderedList,
77
+ run: () => editor?.chain().focus().toggleOrderedList().run()
78
+ },
79
+ {
80
+ label: "Task list",
81
+ Icon: ListChecksIcon,
82
+ active: state?.taskList,
83
+ run: () => editor?.chain().focus().toggleTaskList().run()
84
+ },
85
+ {
86
+ label: "Quote",
87
+ Icon: QuoteIcon,
88
+ active: state?.blockquote,
89
+ run: () => editor?.chain().focus().toggleBlockquote().run()
90
+ },
91
+ {
92
+ label: "Code block",
93
+ Icon: SquareCodeIcon,
94
+ active: state?.codeBlock,
95
+ run: () => editor?.chain().focus().toggleCodeBlock().run()
96
+ }
97
+ ];
98
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Toolbar, {
99
+ "aria-label": "Markdown formatting",
100
+ className: "flex w-full flex-wrap",
101
+ children: [
102
+ actions.map(({ label, Icon, active, run }) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
103
+ render: /* @__PURE__ */ jsx(ToolbarButton, {
104
+ type: "button",
105
+ iconOnly: true,
106
+ "aria-label": label,
107
+ "aria-pressed": Boolean(active),
108
+ disabled: disabled || source || !editor,
109
+ onMouseDown: (event) => event.preventDefault(),
110
+ onClick: run
111
+ }),
112
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
113
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: label })] }, label)),
114
+ /* @__PURE__ */ jsx(MarkdownEditorLink, {}),
115
+ /* @__PURE__ */ jsx(MarkdownEditorImage, {}),
116
+ /* @__PURE__ */ jsx(ToolbarSeparator, {}),
117
+ /* @__PURE__ */ jsx(ToolbarButton, {
118
+ type: "button",
119
+ iconOnly: true,
120
+ "aria-label": "Undo",
121
+ disabled: disabled || source || !state?.undo,
122
+ onClick: () => editor?.chain().focus().undo().run(),
123
+ children: /* @__PURE__ */ jsx(UndoIcon, { "aria-hidden": "true" })
124
+ }),
125
+ /* @__PURE__ */ jsx(ToolbarButton, {
126
+ type: "button",
127
+ iconOnly: true,
128
+ "aria-label": "Redo",
129
+ disabled: disabled || source || !state?.redo,
130
+ onClick: () => editor?.chain().focus().redo().run(),
131
+ children: /* @__PURE__ */ jsx(RedoIcon, { "aria-hidden": "true" })
132
+ }),
133
+ /* @__PURE__ */ jsx(ToolbarButton, {
134
+ type: "button",
135
+ "aria-pressed": source,
136
+ disabled: disabled || !editor || sourceRequired || uploads.length > 0,
137
+ onClick: () => setSource(!source),
138
+ children: "Markdown source"
139
+ }),
140
+ children
141
+ ]
142
+ }) });
143
+ }
144
+ function MarkdownEditorLink() {
145
+ const { editor, disabled, source } = useMarkdownEditor();
146
+ const [open, setOpen] = useState(false);
147
+ const [url, setUrl] = useState("");
148
+ const inputId = useId();
149
+ const valid = isEditorUrl(url.trim());
150
+ function apply() {
151
+ if (!valid || disabled || source) return;
152
+ editor?.chain().focus().extendMarkRange("link").setLink({ href: url.trim() }).run();
153
+ setOpen(false);
154
+ }
155
+ return /* @__PURE__ */ jsxs(Popover, {
156
+ open,
157
+ onOpenChange: (next) => {
158
+ setOpen(next);
159
+ if (next) setUrl(editor?.getAttributes("link").href ?? "");
160
+ },
161
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
162
+ disabled: disabled || source || !editor,
163
+ render: /* @__PURE__ */ jsx(ToolbarButton, {
164
+ type: "button",
165
+ iconOnly: true,
166
+ "aria-label": "Edit link"
167
+ }),
168
+ children: /* @__PURE__ */ jsx(LinkIcon, { "aria-hidden": "true" })
169
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
170
+ className: "grid gap-3",
171
+ onKeyDown: (event) => {
172
+ if (event.key === "Enter") {
173
+ event.preventDefault();
174
+ event.stopPropagation();
175
+ apply();
176
+ }
177
+ },
178
+ children: [
179
+ /* @__PURE__ */ jsx(PopoverTitle, { children: "Edit link" }),
180
+ /* @__PURE__ */ jsx(Label, {
181
+ htmlFor: inputId,
182
+ children: "URL"
183
+ }),
184
+ /* @__PURE__ */ jsx(Input, {
185
+ id: inputId,
186
+ value: url,
187
+ onChange: (event) => setUrl(event.target.value),
188
+ placeholder: "https://example.com"
189
+ }),
190
+ /* @__PURE__ */ jsxs("div", {
191
+ className: "flex gap-2",
192
+ children: [/* @__PURE__ */ jsx(Button, {
193
+ type: "button",
194
+ disabled: !valid || disabled,
195
+ onClick: apply,
196
+ children: "Apply link"
197
+ }), /* @__PURE__ */ jsx(Button, {
198
+ type: "button",
199
+ variant: "ghost",
200
+ disabled,
201
+ onClick: () => {
202
+ editor?.chain().focus().extendMarkRange("link").unsetLink().run();
203
+ setOpen(false);
204
+ },
205
+ children: "Remove link"
206
+ })]
207
+ })
208
+ ]
209
+ })]
210
+ });
211
+ }
212
+ function MarkdownEditorImage() {
213
+ const { editor, disabled, source, canUpload, upload } = useMarkdownEditor();
214
+ const [open, setOpen] = useState(false);
215
+ const [url, setUrl] = useState("");
216
+ const [alt, setAlt] = useState("");
217
+ const inputId = useId();
218
+ const altId = useId();
219
+ const fileInput = useRef(null);
220
+ const valid = isEditorUrl(url.trim(), true);
221
+ function insert() {
222
+ if (!valid || disabled || source) return;
223
+ editor?.chain().focus().setImage({
224
+ src: url.trim(),
225
+ alt
226
+ }).run();
227
+ setOpen(false);
228
+ setUrl("");
229
+ setAlt("");
230
+ }
231
+ return /* @__PURE__ */ jsxs(Popover, {
232
+ open,
233
+ onOpenChange: setOpen,
234
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
235
+ disabled: disabled || source || !editor,
236
+ render: /* @__PURE__ */ jsx(ToolbarButton, {
237
+ type: "button",
238
+ iconOnly: true,
239
+ "aria-label": "Insert image"
240
+ }),
241
+ children: /* @__PURE__ */ jsx(ImageIcon, { "aria-hidden": "true" })
242
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
243
+ className: "grid gap-3",
244
+ onKeyDown: (event) => {
245
+ if (event.key === "Enter") {
246
+ event.preventDefault();
247
+ event.stopPropagation();
248
+ insert();
249
+ }
250
+ },
251
+ children: [
252
+ /* @__PURE__ */ jsx(PopoverTitle, { children: "Insert image" }),
253
+ /* @__PURE__ */ jsx(Label, {
254
+ htmlFor: inputId,
255
+ children: "Image URL"
256
+ }),
257
+ /* @__PURE__ */ jsx(Input, {
258
+ id: inputId,
259
+ value: url,
260
+ onChange: (event) => setUrl(event.target.value),
261
+ placeholder: "https://example.com/image.png"
262
+ }),
263
+ /* @__PURE__ */ jsx(Label, {
264
+ htmlFor: altId,
265
+ children: "Image description"
266
+ }),
267
+ /* @__PURE__ */ jsx(Input, {
268
+ id: altId,
269
+ value: alt,
270
+ onChange: (event) => setAlt(event.target.value)
271
+ }),
272
+ /* @__PURE__ */ jsx(Button, {
273
+ type: "button",
274
+ disabled: !valid || disabled,
275
+ onClick: insert,
276
+ children: "Insert image"
277
+ }),
278
+ canUpload ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("input", {
279
+ ref: fileInput,
280
+ type: "file",
281
+ accept: markdownImageTypes.join(","),
282
+ multiple: true,
283
+ hidden: true,
284
+ onChange: (event) => {
285
+ upload(Array.from(event.target.files ?? []));
286
+ event.target.value = "";
287
+ setOpen(false);
288
+ }
289
+ }), /* @__PURE__ */ jsx(Button, {
290
+ type: "button",
291
+ variant: "ghost",
292
+ disabled,
293
+ onClick: () => fileInput.current?.click(),
294
+ children: "Upload images"
295
+ })] }) : null
296
+ ]
297
+ })]
298
+ });
299
+ }
300
+ function MarkdownEditorUploads() {
301
+ const { uploads, uploadError, retryUpload, removeUpload, disabled } = useMarkdownEditor();
302
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [uploadError ? /* @__PURE__ */ jsx("p", {
303
+ role: "alert",
304
+ children: uploadError
305
+ }) : null, uploads.length > 0 ? /* @__PURE__ */ jsxs("div", {
306
+ "data-control-ui": "markdown-editor",
307
+ "data-control-family": "markdown-editor",
308
+ "data-slot": "uploads",
309
+ children: [/* @__PURE__ */ jsx(ChatComposerAttachments, {
310
+ label: "Image uploads",
311
+ children: uploads.map((item) => /* @__PURE__ */ jsx(ChatComposerAttachment, {
312
+ file: item.file,
313
+ status: item.status,
314
+ progress: item.progress,
315
+ onRemove: disabled ? void 0 : () => removeUpload(item.id),
316
+ removeLabel: `Remove ${item.file.name}`
317
+ }, item.id))
318
+ }), uploads.filter((item) => item.status === "error").map((item) => /* @__PURE__ */ jsxs("div", {
319
+ role: "alert",
320
+ className: "flex items-center gap-2",
321
+ children: [/* @__PURE__ */ jsxs("span", { children: [
322
+ item.file.name,
323
+ ": ",
324
+ item.error
325
+ ] }), /* @__PURE__ */ jsx(Button, {
326
+ type: "button",
327
+ size: "xs",
328
+ disabled,
329
+ onClick: () => retryUpload(item.id),
330
+ children: "Retry upload"
331
+ })]
332
+ }, item.id))]
333
+ }) : null] });
334
+ }
335
+ //#endregion
336
+ export { MarkdownEditorImage, MarkdownEditorLink, MarkdownEditorToolbar, MarkdownEditorUploads };
@@ -0,0 +1,23 @@
1
+ import { Node } from "@tiptap/pm/model";
2
+ import { Editor } from "@tiptap/core";
3
+ //#region src/markdown-editor/uploads.d.ts
4
+ export type MarkdownImage = {
5
+ url: string;
6
+ alt?: string;
7
+ title?: string;
8
+ };
9
+ export type MarkdownImageUploader = (file: File, options: {
10
+ signal: AbortSignal;
11
+ onProgress: (progress: number) => void;
12
+ }) => Promise<MarkdownImage>;
13
+ export type MarkdownImageUpload = {
14
+ id: string;
15
+ file: File;
16
+ status: "uploading" | "error";
17
+ progress?: number;
18
+ error?: string;
19
+ };
20
+ export declare function findImageUpload(document: Node, id: string): number | undefined;
21
+ export declare function completeImageUpload(editor: Editor, id: string, image: MarkdownImage, fallbackAlt: string): void;
22
+ export declare const markdownImageTypes: string[];
23
+ //#endregion
@@ -0,0 +1,32 @@
1
+ import { isEditorUrl } from "./extensions.js";
2
+ //#region src/markdown-editor/uploads.ts
3
+ function findImageUpload(document, id) {
4
+ let position;
5
+ document.descendants((node, pos) => {
6
+ if (node.type.name === "imageUpload" && node.attrs.id === id) position = pos;
7
+ });
8
+ return position;
9
+ }
10
+ function completeImageUpload(editor, id, image, fallbackAlt) {
11
+ if (!isEditorUrl(image.url, true)) throw new Error("The upload must return a permanent HTTP(S) or root-relative image URL.");
12
+ const position = findImageUpload(editor.state.doc, id);
13
+ if (position === void 0) return;
14
+ const imageType = editor.schema.nodes.image;
15
+ const paragraphType = editor.schema.nodes.paragraph;
16
+ if (!imageType || !paragraphType) throw new Error("Image uploads require image and paragraph nodes.");
17
+ const node = imageType.create({
18
+ src: image.url,
19
+ alt: image.alt ?? fallbackAlt,
20
+ title: image.title ?? null
21
+ });
22
+ editor.view.dispatch(editor.state.tr.replaceWith(position, position + 1, paragraphType.create(null, node)).setMeta("addToHistory", false));
23
+ }
24
+ const markdownImageTypes = [
25
+ "image/png",
26
+ "image/jpeg",
27
+ "image/gif",
28
+ "image/webp",
29
+ "image/avif"
30
+ ];
31
+ //#endregion
32
+ export { completeImageUpload, findImageUpload, markdownImageTypes };
@@ -0,0 +1,12 @@
1
+ import { MarkdownImageUpload, MarkdownImageUploader } from "./uploads.js";
2
+ import { Editor } from "@tiptap/core";
3
+ //#region src/markdown-editor/use-image-uploads.d.ts
4
+ export declare function useImageUploads(editor: Editor | null, uploader: MarkdownImageUploader | undefined, maxImageBytes: number): {
5
+ uploads: MarkdownImageUpload[];
6
+ uploadError: string | null;
7
+ upload: (files: File[], position?: number) => void;
8
+ retryUpload: (id: string) => void;
9
+ removeUpload: (id: string) => void;
10
+ cancelAll: () => void;
11
+ };
12
+ //#endregion
@@ -0,0 +1,120 @@
1
+ import { completeImageUpload, findImageUpload, markdownImageTypes } from "./uploads.js";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ //#region src/markdown-editor/use-image-uploads.ts
4
+ function useImageUploads(editor, uploader, maxImageBytes) {
5
+ const [uploads, setUploads] = useState([]);
6
+ const [uploadError, setUploadError] = useState(null);
7
+ const uploadPrefix = useId();
8
+ const nextId = useRef(0);
9
+ const jobs = useRef(/* @__PURE__ */ new Map());
10
+ function discard(id) {
11
+ jobs.current.get(id)?.controller.abort();
12
+ jobs.current.delete(id);
13
+ setUploads((current) => current.filter((item) => item.id !== id));
14
+ }
15
+ function cancelAll() {
16
+ for (const job of jobs.current.values()) job.controller.abort();
17
+ jobs.current.clear();
18
+ setUploads([]);
19
+ }
20
+ useEffect(() => {
21
+ const activeJobs = jobs.current;
22
+ return () => {
23
+ for (const job of activeJobs.values()) job.controller.abort();
24
+ activeJobs.clear();
25
+ };
26
+ }, []);
27
+ useEffect(() => {
28
+ if (!editor) return;
29
+ const prune = () => {
30
+ for (const id of jobs.current.keys()) if (findImageUpload(editor.state.doc, id) === void 0) {
31
+ jobs.current.get(id)?.controller.abort();
32
+ jobs.current.delete(id);
33
+ setUploads((current) => current.filter((item) => item.id !== id));
34
+ }
35
+ };
36
+ editor.on("update", prune);
37
+ return () => {
38
+ editor.off("update", prune);
39
+ };
40
+ }, [editor]);
41
+ async function run(id, file) {
42
+ if (!editor || !uploader) return;
43
+ const controller = new AbortController();
44
+ jobs.current.set(id, {
45
+ file,
46
+ controller
47
+ });
48
+ setUploads((current) => [...current.filter((item) => item.id !== id), {
49
+ id,
50
+ file,
51
+ status: "uploading"
52
+ }]);
53
+ try {
54
+ const image = await uploader(file, {
55
+ signal: controller.signal,
56
+ onProgress: (progress) => {
57
+ if (controller.signal.aborted || !Number.isFinite(progress)) return;
58
+ setUploads((current) => current.map((item) => item.id === id ? {
59
+ ...item,
60
+ progress: Math.max(0, Math.min(100, progress))
61
+ } : item));
62
+ }
63
+ });
64
+ if (controller.signal.aborted || editor.isDestroyed) return;
65
+ completeImageUpload(editor, id, image, file.name);
66
+ discard(id);
67
+ } catch (error) {
68
+ if (controller.signal.aborted) return;
69
+ setUploads((current) => current.map((item) => item.id === id ? {
70
+ ...item,
71
+ status: "error",
72
+ error: error instanceof Error ? error.message : "Image upload failed."
73
+ } : item));
74
+ }
75
+ }
76
+ function upload(files, position) {
77
+ if (!editor || !uploader || !editor.isEditable) return;
78
+ setUploadError(null);
79
+ let insertAt = position ?? editor.state.selection.to;
80
+ for (const file of files) {
81
+ if (!markdownImageTypes.includes(file.type) || file.size > maxImageBytes) {
82
+ setUploadError(`Choose a PNG, JPEG, GIF, WebP or AVIF image up to ${Math.round(maxImageBytes / 1024 / 1024)} MB.`);
83
+ continue;
84
+ }
85
+ const id = `${uploadPrefix}-${nextId.current++}`;
86
+ editor.chain().focus().insertContentAt(insertAt, {
87
+ type: "imageUpload",
88
+ attrs: {
89
+ id,
90
+ name: file.name
91
+ }
92
+ }).run();
93
+ insertAt = editor.state.selection.to;
94
+ run(id, file);
95
+ }
96
+ }
97
+ function removeUpload(id) {
98
+ discard(id);
99
+ if (!editor) return;
100
+ const position = findImageUpload(editor.state.doc, id);
101
+ if (position !== void 0) editor.view.dispatch(editor.state.tr.delete(position, position + 1).setMeta("addToHistory", false));
102
+ }
103
+ function retryUpload(id) {
104
+ const job = jobs.current.get(id);
105
+ if (job) {
106
+ job.controller.abort();
107
+ run(id, job.file);
108
+ }
109
+ }
110
+ return {
111
+ uploads,
112
+ uploadError,
113
+ upload,
114
+ retryUpload,
115
+ removeUpload,
116
+ cancelAll
117
+ };
118
+ }
119
+ //#endregion
120
+ export { useImageUploads };
@@ -0,0 +1,27 @@
1
+ import { MarkdownEditorKnobStyle } from "./knob-contracts/markdown-editor-knobs.js";
2
+ import { MarkdownImageUploader } from "./markdown-editor/uploads.js";
3
+ import { CSSProperties, ComponentProps } from "react";
4
+ //#region src/markdown-editor.d.ts
5
+ export type MarkdownEditorStatus = {
6
+ isEmpty: boolean;
7
+ hasPendingUploads: boolean;
8
+ };
9
+ export type MarkdownEditorProps = Omit<ComponentProps<"div">, "defaultValue" | "onChange" | "style"> & {
10
+ value?: string;
11
+ defaultValue?: string;
12
+ onValueChange?: (markdown: string) => void;
13
+ disabled?: boolean;
14
+ onUploadImage?: MarkdownImageUploader;
15
+ maxImageBytes?: number;
16
+ onStatusChange?: (status: MarkdownEditorStatus) => void;
17
+ style?: CSSProperties & MarkdownEditorKnobStyle;
18
+ };
19
+ export declare function MarkdownEditor({ value, defaultValue, onValueChange, disabled, onUploadImage, maxImageBytes, onStatusChange, children, className, ...props }: MarkdownEditorProps): import("react").JSX.Element;
20
+ export type MarkdownEditorContentProps = {
21
+ label: string;
22
+ placeholder?: string;
23
+ autoFocus?: boolean;
24
+ className?: string;
25
+ };
26
+ export declare function MarkdownEditorContent({ label, placeholder, autoFocus, className }: MarkdownEditorContentProps): import("react").JSX.Element;
27
+ //#endregion