@ctrl-ui/react 0.8.18 → 0.8.20

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 (106) hide show
  1. package/README.md +7 -0
  2. package/dist/action-bar.js +2 -2
  3. package/dist/audio-device-select.js +5 -2
  4. package/dist/blocks/coding-agent.js +22 -12
  5. package/dist/blocks/design-canvas-inspector.js +6 -3
  6. package/dist/blocks/design-canvas.js +20 -10
  7. package/dist/blocks/discussion-markdown-input.d.ts +14 -0
  8. package/dist/blocks/discussion-markdown-input.js +36 -0
  9. package/dist/blocks/discussion.d.ts +9 -3
  10. package/dist/blocks/discussion.js +82 -16
  11. package/dist/blocks/file-explorer.js +46 -24
  12. package/dist/blocks/settings.js +10 -6
  13. package/dist/chat-composer-editor/extensions/mention.d.ts +1 -1
  14. package/dist/chat-message.d.ts +13 -0
  15. package/dist/chat-message.js +198 -3
  16. package/dist/filter-bar/editor.js +5 -2
  17. package/dist/knob-contracts/flow-knobs.d.ts +4 -0
  18. package/dist/knob-contracts/flow-knobs.js +32 -0
  19. package/dist/knob-contracts/item-knobs.d.ts +1 -1
  20. package/dist/knob-contracts/item-knobs.js +4 -1
  21. package/dist/knob-contracts/label-knobs.d.ts +4 -0
  22. package/dist/knob-contracts/label-knobs.js +4 -0
  23. package/dist/knob-contracts/markdown-editor-knobs.d.ts +4 -0
  24. package/dist/knob-contracts/markdown-editor-knobs.js +17 -0
  25. package/dist/knob-contracts/morphing-panel-knobs.d.ts +1 -1
  26. package/dist/knob-contracts/morphing-panel-knobs.js +2 -1
  27. package/dist/knob-contracts/resize-handle-knobs.d.ts +4 -0
  28. package/dist/knob-contracts/resize-handle-knobs.js +12 -0
  29. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  30. package/dist/knob-contracts/sidebar-knobs.js +10 -0
  31. package/dist/knob-contracts/switch-knobs.d.ts +1 -1
  32. package/dist/knob-contracts/switch-knobs.js +1 -0
  33. package/dist/lib/canvas-grid.d.ts +4 -0
  34. package/dist/lib/canvas-grid.js +5 -0
  35. package/dist/markdown-editor/context.d.ts +22 -0
  36. package/dist/markdown-editor/context.js +10 -0
  37. package/dist/markdown-editor/drag-handle.d.ts +6 -0
  38. package/dist/markdown-editor/drag-handle.js +126 -0
  39. package/dist/markdown-editor/extensions.d.ts +10 -0
  40. package/dist/markdown-editor/extensions.js +139 -0
  41. package/dist/markdown-editor/suggestions.d.ts +12 -0
  42. package/dist/markdown-editor/suggestions.js +235 -0
  43. package/dist/markdown-editor/toolbar.d.ts +9 -0
  44. package/dist/markdown-editor/toolbar.js +339 -0
  45. package/dist/markdown-editor/uploads.d.ts +23 -0
  46. package/dist/markdown-editor/uploads.js +32 -0
  47. package/dist/markdown-editor/use-image-uploads.d.ts +12 -0
  48. package/dist/markdown-editor/use-image-uploads.js +120 -0
  49. package/dist/markdown-editor.d.ts +27 -0
  50. package/dist/markdown-editor.js +202 -0
  51. package/dist/styles/index.css +9 -4
  52. package/dist/styles/recipes/chat-message-edit.css +105 -0
  53. package/dist/styles/recipes/code.css +4 -0
  54. package/dist/styles/recipes/field.css +1 -1
  55. package/dist/styles/recipes/flow.css +304 -0
  56. package/dist/styles/recipes/infinite-canvas.css +1 -1
  57. package/dist/styles/recipes/item.css +39 -2
  58. package/dist/styles/recipes/label.css +19 -0
  59. package/dist/styles/recipes/markdown-editor.css +234 -0
  60. package/dist/styles/recipes/morphing-panel.css +53 -12
  61. package/dist/styles/recipes/page-layout.css +11 -1
  62. package/dist/styles/recipes/resize-handle.css +126 -0
  63. package/dist/styles/recipes/sidebar-mobile.css +171 -0
  64. package/dist/styles/recipes/switch.css +17 -5
  65. package/dist/styles/recipes/toolbar.css +8 -1
  66. package/dist/styles/theme.css +1 -0
  67. package/dist/ui/autocomplete.js +1 -1
  68. package/dist/ui/code.js +1 -0
  69. package/dist/ui/color-picker.js +1 -1
  70. package/dist/ui/combobox.d.ts +1 -1
  71. package/dist/ui/combobox.js +1 -1
  72. package/dist/ui/dockable-panel.js +1 -1
  73. package/dist/ui/flow.d.ts +51 -0
  74. package/dist/ui/flow.js +153 -0
  75. package/dist/ui/infinite-canvas.js +3 -3
  76. package/dist/ui/item.d.ts +5 -3
  77. package/dist/ui/item.js +2 -1
  78. package/dist/ui/label.d.ts +8 -0
  79. package/dist/ui/label.js +14 -0
  80. package/dist/ui/markdown.d.ts +2 -1
  81. package/dist/ui/markdown.js +3 -3
  82. package/dist/ui/morphing-panel.d.ts +21 -4
  83. package/dist/ui/morphing-panel.js +152 -36
  84. package/dist/ui/resize-handle.d.ts +11 -0
  85. package/dist/ui/resize-handle.js +26 -0
  86. package/dist/ui/sidebar-mobile.d.ts +17 -0
  87. package/dist/ui/sidebar-mobile.js +70 -0
  88. package/dist/ui/sidebar-provider.d.ts +1 -0
  89. package/dist/ui/sidebar-provider.js +3 -0
  90. package/dist/ui/sidebar.d.ts +3 -1
  91. package/dist/ui/sidebar.js +101 -49
  92. package/dist/ui/toolbar.d.ts +3 -1
  93. package/dist/ui/toolbar.js +2 -1
  94. package/dist/ui/typography.d.ts +47 -0
  95. package/dist/ui/typography.js +61 -0
  96. package/dist/ui/use-switch-drag.js +1 -1
  97. package/package.json +41 -1
  98. package/dist/environment-variables.d.ts +0 -125
  99. package/dist/environment-variables.js +0 -512
  100. package/dist/hooks/use-environment-variables.d.ts +0 -50
  101. package/dist/hooks/use-environment-variables.js +0 -188
  102. package/dist/knob-contracts/environment-variables-knobs.d.ts +0 -4
  103. package/dist/knob-contracts/environment-variables-knobs.js +0 -12
  104. package/dist/lib/env-file.d.ts +0 -27
  105. package/dist/lib/env-file.js +0 -218
  106. package/dist/styles/recipes/environment-variables.css +0 -170
@@ -0,0 +1,339 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { Text } from "../ui/typography.js";
4
+ import { ChatComposerAttachment, ChatComposerAttachments } from "../chat-composer-attachment.js";
5
+ import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "../ui/popover.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 { Input } from "../ui/input.js";
11
+ import { Toolbar, ToolbarButton, ToolbarSeparator } from "../ui/toolbar.js";
12
+ import { Label } from "../ui/label.js";
13
+ import { useId, useRef, useState } from "react";
14
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
15
+ import { BoldIcon, CodeIcon, Heading2Icon, ImageIcon, ItalicIcon, LinkIcon, ListChecksIcon, ListIcon, ListOrderedIcon, QuoteIcon, RedoIcon, SquareCodeIcon, StrikethroughIcon, UndoIcon } from "lucide-react";
16
+ import { useEditorState } from "@tiptap/react";
17
+ //#region src/markdown-editor/toolbar.tsx
18
+ function MarkdownEditorToolbar({ children }) {
19
+ const { editor, disabled, source, sourceRequired, setSource, uploads } = useMarkdownEditor();
20
+ const state = useEditorState({
21
+ editor,
22
+ selector: ({ editor: current }) => ({
23
+ bold: current?.isActive("bold"),
24
+ italic: current?.isActive("italic"),
25
+ strike: current?.isActive("strike"),
26
+ code: current?.isActive("code"),
27
+ heading: current?.isActive("heading", { level: 2 }),
28
+ bulletList: current?.isActive("bulletList"),
29
+ orderedList: current?.isActive("orderedList"),
30
+ taskList: current?.isActive("taskList"),
31
+ blockquote: current?.isActive("blockquote"),
32
+ codeBlock: current?.isActive("codeBlock"),
33
+ undo: current?.can().undo(),
34
+ redo: current?.can().redo()
35
+ })
36
+ });
37
+ const actions = [
38
+ {
39
+ label: "Bold",
40
+ Icon: BoldIcon,
41
+ active: state?.bold,
42
+ run: () => editor?.chain().focus().toggleBold().run()
43
+ },
44
+ {
45
+ label: "Italic",
46
+ Icon: ItalicIcon,
47
+ active: state?.italic,
48
+ run: () => editor?.chain().focus().toggleItalic().run()
49
+ },
50
+ {
51
+ label: "Strikethrough",
52
+ Icon: StrikethroughIcon,
53
+ active: state?.strike,
54
+ run: () => editor?.chain().focus().toggleStrike().run()
55
+ },
56
+ {
57
+ label: "Inline code",
58
+ Icon: CodeIcon,
59
+ active: state?.code,
60
+ run: () => editor?.chain().focus().toggleCode().run()
61
+ },
62
+ {
63
+ label: "Heading",
64
+ Icon: Heading2Icon,
65
+ active: state?.heading,
66
+ run: () => editor?.chain().focus().toggleHeading({ level: 2 }).run()
67
+ },
68
+ {
69
+ label: "Bullet list",
70
+ Icon: ListIcon,
71
+ active: state?.bulletList,
72
+ run: () => editor?.chain().focus().toggleBulletList().run()
73
+ },
74
+ {
75
+ label: "Numbered list",
76
+ Icon: ListOrderedIcon,
77
+ active: state?.orderedList,
78
+ run: () => editor?.chain().focus().toggleOrderedList().run()
79
+ },
80
+ {
81
+ label: "Task list",
82
+ Icon: ListChecksIcon,
83
+ active: state?.taskList,
84
+ run: () => editor?.chain().focus().toggleTaskList().run()
85
+ },
86
+ {
87
+ label: "Quote",
88
+ Icon: QuoteIcon,
89
+ active: state?.blockquote,
90
+ run: () => editor?.chain().focus().toggleBlockquote().run()
91
+ },
92
+ {
93
+ label: "Code block",
94
+ Icon: SquareCodeIcon,
95
+ active: state?.codeBlock,
96
+ run: () => editor?.chain().focus().toggleCodeBlock().run()
97
+ }
98
+ ];
99
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(Toolbar, {
100
+ "aria-label": "Markdown formatting",
101
+ chrome: "embedded",
102
+ className: "flex w-full flex-wrap",
103
+ children: [
104
+ actions.map(({ label, Icon, active, run }) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
105
+ render: /* @__PURE__ */ jsx(ToolbarButton, {
106
+ type: "button",
107
+ iconOnly: true,
108
+ "aria-label": label,
109
+ "aria-pressed": Boolean(active),
110
+ disabled: disabled || source || !editor,
111
+ onMouseDown: (event) => event.preventDefault(),
112
+ onClick: run
113
+ }),
114
+ children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true" })
115
+ }), /* @__PURE__ */ jsx(TooltipContent, { children: label })] }, label)),
116
+ /* @__PURE__ */ jsx(MarkdownEditorLink, {}),
117
+ /* @__PURE__ */ jsx(MarkdownEditorImage, {}),
118
+ /* @__PURE__ */ jsx(ToolbarSeparator, {}),
119
+ /* @__PURE__ */ jsx(ToolbarButton, {
120
+ type: "button",
121
+ iconOnly: true,
122
+ "aria-label": "Undo",
123
+ disabled: disabled || source || !state?.undo,
124
+ onClick: () => editor?.chain().focus().undo().run(),
125
+ children: /* @__PURE__ */ jsx(UndoIcon, { "aria-hidden": "true" })
126
+ }),
127
+ /* @__PURE__ */ jsx(ToolbarButton, {
128
+ type: "button",
129
+ iconOnly: true,
130
+ "aria-label": "Redo",
131
+ disabled: disabled || source || !state?.redo,
132
+ onClick: () => editor?.chain().focus().redo().run(),
133
+ children: /* @__PURE__ */ jsx(RedoIcon, { "aria-hidden": "true" })
134
+ }),
135
+ /* @__PURE__ */ jsx(ToolbarButton, {
136
+ type: "button",
137
+ "aria-pressed": source,
138
+ disabled: disabled || !editor || sourceRequired || uploads.length > 0,
139
+ onClick: () => setSource(!source),
140
+ children: "Markdown source"
141
+ }),
142
+ children
143
+ ]
144
+ }) });
145
+ }
146
+ function MarkdownEditorLink() {
147
+ const { editor, disabled, source } = useMarkdownEditor();
148
+ const [open, setOpen] = useState(false);
149
+ const [url, setUrl] = useState("");
150
+ const inputId = useId();
151
+ const valid = isEditorUrl(url.trim());
152
+ function apply() {
153
+ if (!valid || disabled || source) return;
154
+ editor?.chain().focus().extendMarkRange("link").setLink({ href: url.trim() }).run();
155
+ setOpen(false);
156
+ }
157
+ return /* @__PURE__ */ jsxs(Popover, {
158
+ open,
159
+ onOpenChange: (next) => {
160
+ setOpen(next);
161
+ if (next) setUrl(editor?.getAttributes("link").href ?? "");
162
+ },
163
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
164
+ disabled: disabled || source || !editor,
165
+ render: /* @__PURE__ */ jsx(ToolbarButton, {
166
+ type: "button",
167
+ iconOnly: true,
168
+ "aria-label": "Edit link"
169
+ }),
170
+ children: /* @__PURE__ */ jsx(LinkIcon, { "aria-hidden": "true" })
171
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
172
+ className: "grid gap-3",
173
+ onKeyDown: (event) => {
174
+ if (event.key === "Enter") {
175
+ event.preventDefault();
176
+ event.stopPropagation();
177
+ apply();
178
+ }
179
+ },
180
+ children: [
181
+ /* @__PURE__ */ jsx(PopoverTitle, { children: "Edit link" }),
182
+ /* @__PURE__ */ jsx(Label, {
183
+ htmlFor: inputId,
184
+ children: "URL"
185
+ }),
186
+ /* @__PURE__ */ jsx(Input, {
187
+ id: inputId,
188
+ value: url,
189
+ onChange: (event) => setUrl(event.target.value),
190
+ placeholder: "https://example.com"
191
+ }),
192
+ /* @__PURE__ */ jsxs("div", {
193
+ className: "flex gap-2",
194
+ children: [/* @__PURE__ */ jsx(Button, {
195
+ type: "button",
196
+ disabled: !valid || disabled,
197
+ onClick: apply,
198
+ children: "Apply link"
199
+ }), /* @__PURE__ */ jsx(Button, {
200
+ type: "button",
201
+ variant: "ghost",
202
+ disabled,
203
+ onClick: () => {
204
+ editor?.chain().focus().extendMarkRange("link").unsetLink().run();
205
+ setOpen(false);
206
+ },
207
+ children: "Remove link"
208
+ })]
209
+ })
210
+ ]
211
+ })]
212
+ });
213
+ }
214
+ function MarkdownEditorImage() {
215
+ const { editor, disabled, source, canUpload, upload } = useMarkdownEditor();
216
+ const [open, setOpen] = useState(false);
217
+ const [url, setUrl] = useState("");
218
+ const [alt, setAlt] = useState("");
219
+ const inputId = useId();
220
+ const altId = useId();
221
+ const fileInput = useRef(null);
222
+ const valid = isEditorUrl(url.trim(), true);
223
+ function insert() {
224
+ if (!valid || disabled || source) return;
225
+ editor?.chain().focus().setImage({
226
+ src: url.trim(),
227
+ alt
228
+ }).run();
229
+ setOpen(false);
230
+ setUrl("");
231
+ setAlt("");
232
+ }
233
+ return /* @__PURE__ */ jsxs(Popover, {
234
+ open,
235
+ onOpenChange: setOpen,
236
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
237
+ disabled: disabled || source || !editor,
238
+ render: /* @__PURE__ */ jsx(ToolbarButton, {
239
+ type: "button",
240
+ iconOnly: true,
241
+ "aria-label": "Insert image"
242
+ }),
243
+ children: /* @__PURE__ */ jsx(ImageIcon, { "aria-hidden": "true" })
244
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
245
+ className: "grid gap-3",
246
+ onKeyDown: (event) => {
247
+ if (event.key === "Enter") {
248
+ event.preventDefault();
249
+ event.stopPropagation();
250
+ insert();
251
+ }
252
+ },
253
+ children: [
254
+ /* @__PURE__ */ jsx(PopoverTitle, { children: "Insert image" }),
255
+ /* @__PURE__ */ jsx(Label, {
256
+ htmlFor: inputId,
257
+ children: "Image URL"
258
+ }),
259
+ /* @__PURE__ */ jsx(Input, {
260
+ id: inputId,
261
+ value: url,
262
+ onChange: (event) => setUrl(event.target.value),
263
+ placeholder: "https://example.com/image.png"
264
+ }),
265
+ /* @__PURE__ */ jsx(Label, {
266
+ htmlFor: altId,
267
+ children: "Image description"
268
+ }),
269
+ /* @__PURE__ */ jsx(Input, {
270
+ id: altId,
271
+ value: alt,
272
+ onChange: (event) => setAlt(event.target.value)
273
+ }),
274
+ /* @__PURE__ */ jsx(Button, {
275
+ type: "button",
276
+ disabled: !valid || disabled,
277
+ onClick: insert,
278
+ children: "Insert image"
279
+ }),
280
+ canUpload ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("input", {
281
+ ref: fileInput,
282
+ type: "file",
283
+ accept: markdownImageTypes.join(","),
284
+ multiple: true,
285
+ hidden: true,
286
+ onChange: (event) => {
287
+ upload(Array.from(event.target.files ?? []));
288
+ event.target.value = "";
289
+ setOpen(false);
290
+ }
291
+ }), /* @__PURE__ */ jsx(Button, {
292
+ type: "button",
293
+ variant: "ghost",
294
+ disabled,
295
+ onClick: () => fileInput.current?.click(),
296
+ children: "Upload images"
297
+ })] }) : null
298
+ ]
299
+ })]
300
+ });
301
+ }
302
+ function MarkdownEditorUploads() {
303
+ const { uploads, uploadError, retryUpload, removeUpload, disabled } = useMarkdownEditor();
304
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [uploadError ? /* @__PURE__ */ jsx(Text, {
305
+ as: "p",
306
+ role: "alert",
307
+ children: uploadError
308
+ }) : null, uploads.length > 0 ? /* @__PURE__ */ jsxs("div", {
309
+ "data-control-ui": "markdown-editor",
310
+ "data-control-family": "markdown-editor",
311
+ "data-slot": "uploads",
312
+ children: [/* @__PURE__ */ jsx(ChatComposerAttachments, {
313
+ label: "Image uploads",
314
+ children: uploads.map((item) => /* @__PURE__ */ jsx(ChatComposerAttachment, {
315
+ file: item.file,
316
+ status: item.status,
317
+ progress: item.progress,
318
+ onRemove: disabled ? void 0 : () => removeUpload(item.id),
319
+ removeLabel: `Remove ${item.file.name}`
320
+ }, item.id))
321
+ }), uploads.filter((item) => item.status === "error").map((item) => /* @__PURE__ */ jsxs("div", {
322
+ role: "alert",
323
+ className: "flex items-center gap-2",
324
+ children: [/* @__PURE__ */ jsxs("span", { children: [
325
+ item.file.name,
326
+ ": ",
327
+ item.error
328
+ ] }), /* @__PURE__ */ jsx(Button, {
329
+ type: "button",
330
+ size: "xs",
331
+ disabled,
332
+ onClick: () => retryUpload(item.id),
333
+ children: "Retry upload"
334
+ })]
335
+ }, item.id))]
336
+ }) : null] });
337
+ }
338
+ //#endregion
339
+ 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