@threadlabs/looma 0.32.0 → 0.32.1

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.
@@ -11986,7 +11986,7 @@ var CALLOUT_COMMANDS = [
11986
11986
  * @ownership Returned command objects belong to the caller and are recreated
11987
11987
  * per call so one editor cannot mutate another editor's command inventory.
11988
11988
  */
11989
- function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor) {
11989
+ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor, onOpenFilePicker) {
11990
11990
  return [
11991
11991
  {
11992
11992
  title: "Text",
@@ -12209,6 +12209,21 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLink
12209
12209
  onOpenLinkEditor();
12210
12210
  }
12211
12211
  }] : [],
12212
+ ...onOpenFilePicker ? [{
12213
+ title: "File attachment",
12214
+ description: "Attach a file",
12215
+ icon: "paperclip",
12216
+ keywords: [
12217
+ "file",
12218
+ "attachment",
12219
+ "upload",
12220
+ "pdf"
12221
+ ],
12222
+ command: ({ editor, range }) => {
12223
+ editor.chain().focus().deleteRange(range).run();
12224
+ onOpenFilePicker();
12225
+ }
12226
+ }] : [],
12212
12227
  ...onOpenImagePicker ? [{
12213
12228
  title: "Image",
12214
12229
  description: "Upload an image",
@@ -12228,6 +12243,7 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLink
12228
12243
  ...command,
12229
12244
  id: command.title.toLowerCase().replaceAll(" ", "-"),
12230
12245
  group: CALLOUT_COMMANDS.some((callout) => callout.title === command.title) ? "Callouts" : [
12246
+ "File attachment",
12231
12247
  "Chip",
12232
12248
  "Inline code",
12233
12249
  "Link",
@@ -79,7 +79,7 @@ declare module "@tiptap/core" {
79
79
  * @ownership Returned command objects belong to the caller and are recreated
80
80
  * per call so one editor cannot mutate another editor's command inventory.
81
81
  */
82
- export declare function getDefaultSlashCommands(onOpenImagePicker?: () => void, onOpenChipEditor?: (editor: Editor, position: number) => void, onOpenLinkEditor?: () => void): LoomaSlashCommand[];
82
+ export declare function getDefaultSlashCommands(onOpenImagePicker?: () => void, onOpenChipEditor?: (editor: Editor, position: number) => void, onOpenLinkEditor?: () => void, onOpenFilePicker?: () => void): LoomaSlashCommand[];
83
83
  /**
84
84
  * Ranks exact names/aliases before partial matches, then matching categories.
85
85
  * Other ties retain inventory order.
@@ -0,0 +1,31 @@
1
+ import type { Editor } from '@tiptap/core';
2
+ /** A retained file, represented as a readable ordinary link in the document. */
3
+ export interface LoomaFileDescriptor {
4
+ href: string;
5
+ name: string;
6
+ size: number;
7
+ contentType: string;
8
+ }
9
+ /** The application retains the bytes and returns their authorized download URL. */
10
+ export type LoomaFileUpload = (file: File, init: {
11
+ signal: AbortSignal;
12
+ progress: (loaded: number, total: number) => void;
13
+ }) => PromiseLike<LoomaFileDescriptor | null> | LoomaFileDescriptor | null;
14
+ /** File insertion status, independent of the document and its undo history. */
15
+ export interface LoomaFileUploadsState {
16
+ uploading: boolean;
17
+ progress: {
18
+ loaded: number;
19
+ total: number;
20
+ } | null;
21
+ }
22
+ /** Captures the caret before asynchronous transport; later transactions map that caret. */
23
+ export declare function createFileUploads(options: {
24
+ editor: Editor;
25
+ upload: () => LoomaFileUpload | undefined;
26
+ onError: (error: unknown, file: File) => void;
27
+ onState?: (state: LoomaFileUploadsState) => void;
28
+ }): {
29
+ cancel(): void;
30
+ insert(file: File): Promise<void>;
31
+ };
package/editor/icons.d.ts CHANGED
@@ -38,6 +38,7 @@ export declare const LOOMA_ICONS: {
38
38
  highlighter: IconNode;
39
39
  history: IconNode;
40
40
  image: IconNode;
41
+ paperclip: IconNode;
41
42
  info: IconNode;
42
43
  italic: IconNode;
43
44
  'layout-dashboard': IconNode;
package/editor/index.js CHANGED
@@ -2,7 +2,7 @@ import { n as TABLE_CELL_BACKGROUND_PRESETS, t as TABLE_CELL_BACKGROUND_OPTIONS
2
2
  import { n as resolveTableCellAt, t as measureTableOverlayGeometry } from "./chunks/ui.js";
3
3
  import { $ as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, A as filterLoomaMentionItems, B as LOOMA_FOOTNOTE_LABEL, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaFigcaption, H as LoomaFootnoteReference, I as getTableOfContentsEntries, J as revealLoomaExpandAt, K as LoomaFigure, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaCallout, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as LoomaFootnotes, V as LoomaFootnote, W as findLoomaFootnote, X as LoomaChip, Y as LOOMA_CHIP_COLORS, Z as normalizeLoomaChipColor, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, et as LOOMA_ACTIVE_BLOCK_CLASS, f as getDefaultEditorExtensions, g as LoomaTableHeader, h as LoomaTableCell, i as getDefaultSlashCommands, j as normalizeMentionResultLimit, k as MAX_MENTION_RESULT_LIMIT, l as normalizeActiveTableColumnWidths, m as LoomaTable, n as createLoomaSlashCommandExtension, nt as LoomaActiveBlock, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaExpand, r as filterLoomaSlashCommands, rt as LoomaActiveBlockPluginKey, s as handleTableOverlayAction, t as LoomaSlashCommand, tt as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "./chunks/extensions.js";
4
4
  import { a as setReferenceDisplay, i as referenceDisplay, n as LoomaReferencePresentation, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, t as LoomaReference } from "./chunks/reference-display.js";
5
- import { AlignCenter, AlignLeft, AlignRight, ArrowLeft, Bell, Bold, BookUser, Braces, Calculator, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDot, CircleQuestionMark, CircleX, CodeXml, Columns3, Compass, CreditCard, Ellipsis, Eraser, ExternalLink, FileText, Folder, GripHorizontal, GripVertical, Heading1, Heading2, Heading3, Highlighter, History, Image, Info, Italic, LayoutDashboard, Lightbulb, Link, List, ListOrdered, ListTodo, LoaderCircle, Merge, MessageSquareWarning, Minus, NotebookPen, PaintBucket, PanelBottom, PanelLeft, PanelRight, PanelTop, Pencil, Pilcrow, Plus, Quote, Receipt, Redo2, Rows3, Search, Settings, Split, Strikethrough, Superscript, Table2, Tag, Trash2, TriangleAlert, Truck, Underline, Undo2, Unlink, UserRound, Users, X } from "lucide";
5
+ import { AlignCenter, AlignLeft, AlignRight, ArrowLeft, Bell, Bold, BookUser, Braces, Calculator, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDot, CircleQuestionMark, CircleX, CodeXml, Columns3, Compass, CreditCard, Ellipsis, Eraser, ExternalLink, FileText, Folder, GripHorizontal, GripVertical, Heading1, Heading2, Heading3, Highlighter, History, Image, Info, Italic, LayoutDashboard, Lightbulb, Link, List, ListOrdered, ListTodo, LoaderCircle, Merge, MessageSquareWarning, Minus, NotebookPen, PaintBucket, PanelBottom, PanelLeft, PanelRight, PanelTop, Paperclip, Pencil, Pilcrow, Plus, Quote, Receipt, Redo2, Rows3, Search, Settings, Split, Strikethrough, Superscript, Table2, Tag, Trash2, TriangleAlert, Truck, Underline, Undo2, Unlink, UserRound, Users, X } from "lucide";
6
6
  //#region src/editor/icons.ts
7
7
  /** The opinionated Lucide set used by Looma's shipped interaction surfaces. */
8
8
  var LOOMA_ICONS = {
@@ -43,6 +43,7 @@ var LOOMA_ICONS = {
43
43
  highlighter: Highlighter,
44
44
  history: History,
45
45
  image: Image,
46
+ paperclip: Paperclip,
46
47
  info: Info,
47
48
  italic: Italic,
48
49
  "layout-dashboard": LayoutDashboard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -1,3 +1,4 @@
1
+ import { type LoomaFileDescriptor } from "../../editor/file-uploads.js";
1
2
  import "../../components/ui-editor-surface/ui-editor-surface.css";
2
3
  import "./looma-editor.css";
3
4
  import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
@@ -77,6 +78,10 @@ export type LoomaMentionQuery = LoomaResponder<LoomaMentionQueryDetail, LoomaMen
77
78
  * @failure A rejection leaves the document unchanged and emits `uploadError`.
78
79
  */
79
80
  export type LoomaImageUploader = LoomaResponder<LoomaImageUploadDetail, LoomaImageDescriptor>;
81
+ /** Uploads a file and returns its retained link; rejection leaves authored content unchanged. */
82
+ export type LoomaFileUploader = LoomaResponder<{
83
+ file: File;
84
+ }, LoomaFileDescriptor>;
80
85
  /**
81
86
  * Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
82
87
  * with declarative menus, image delivery, and table interaction chrome.
@@ -174,7 +179,12 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
174
179
  type: StringConstructor;
175
180
  default: string;
176
181
  };
177
- /** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
182
+ /** Answers `file-upload` with a retained ordinary file link. */
183
+ fileUpload: {
184
+ type: PropType<LoomaFileUploader | undefined>;
185
+ default: undefined;
186
+ };
187
+ /** Answers `image-upload` with the image to insert. */
178
188
  imageUpload: {
179
189
  type: PropType<LoomaImageUploader | undefined>;
180
190
  default: undefined;
@@ -324,7 +334,12 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
324
334
  type: StringConstructor;
325
335
  default: string;
326
336
  };
327
- /** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
337
+ /** Answers `file-upload` with a retained ordinary file link. */
338
+ fileUpload: {
339
+ type: PropType<LoomaFileUploader | undefined>;
340
+ default: undefined;
341
+ };
342
+ /** Answers `image-upload` with the image to insert. */
328
343
  imageUpload: {
329
344
  type: PropType<LoomaImageUploader | undefined>;
330
345
  default: undefined;
@@ -402,6 +417,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
402
417
  linkSearch: LoomaLinkSearch | undefined;
403
418
  linkResolve: LoomaLinkResolve | undefined;
404
419
  imageUpload: LoomaImageUploader | undefined;
420
+ fileUpload: LoomaFileUploader | undefined;
405
421
  modelValue: JSONContent;
406
422
  contentDensity: "comfortable" | "compact";
407
423
  mentionItems: LoomaMentionItem[];
@@ -1,4 +1,5 @@
1
1
  export * from "@threadlabs/looma/editor";
2
2
  export * from "./primitives.js";
3
- export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorExtensions, type LoomaEditorSlashCommands, type LoomaSlashCommandsDetail, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkSearchDetail, type LoomaLinkResolve, type LoomaLinkResolveDetail, type LoomaMentionQuery, type LoomaMentionQueryDetail, type LoomaImageUploader, type LoomaImageUploadDetail, type LoomaRequestInit, type LoomaResponder, } from "./LoomaEditor.js";
3
+ export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorExtensions, type LoomaEditorSlashCommands, type LoomaSlashCommandsDetail, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkSearchDetail, type LoomaLinkResolve, type LoomaLinkResolveDetail, type LoomaMentionQuery, type LoomaMentionQueryDetail, type LoomaFileUploader, type LoomaImageUploader, type LoomaImageUploadDetail, type LoomaRequestInit, type LoomaResponder, } from "./LoomaEditor.js";
4
4
  export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageSourceResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "../../editor/image-delivery.js";
5
+ export type { LoomaFileDescriptor } from "../../editor/file-uploads.js";
@@ -47,6 +47,73 @@ var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
47
47
  EditorFootnoteEditor: "ui-editor-footnote-editor"
48
48
  };
49
49
  //#endregion
50
+ //#region src/editor/file-uploads.ts
51
+ /** Captures the caret before asynchronous transport; later transactions map that caret. */
52
+ function createFileUploads(options) {
53
+ let active;
54
+ const state = (uploading, progress = null) => options.onState?.({
55
+ uploading,
56
+ progress
57
+ });
58
+ return {
59
+ cancel() {
60
+ active?.abort();
61
+ state(false);
62
+ },
63
+ async insert(file) {
64
+ const upload = options.upload();
65
+ if (!upload || options.editor.isDestroyed) return;
66
+ active?.abort();
67
+ const controller = new AbortController();
68
+ active = controller;
69
+ const { editor } = options;
70
+ let position = editor.state.selection.from;
71
+ const map = ({ transaction }) => {
72
+ position = transaction.mapping.map(position);
73
+ };
74
+ editor.on("transaction", map);
75
+ state(true);
76
+ try {
77
+ const answer = await upload(file, {
78
+ signal: controller.signal,
79
+ progress: (loaded, total) => {
80
+ if (!controller.signal.aborted) state(true, {
81
+ loaded,
82
+ total
83
+ });
84
+ }
85
+ });
86
+ if (!answer || controller.signal.aborted || editor.isDestroyed || !editor.isEditable) return;
87
+ if (!/^(?:https?:\/\/|\/(?!\/))/.test(answer.href) || !Number.isSafeInteger(answer.size) || answer.size < 0) throw new Error("Invalid file descriptor");
88
+ const size = answer.size < 1e3 ? `${answer.size} B` : answer.size < 1e6 ? `${Math.ceil(answer.size / 1e3)} KB` : `${Number((answer.size / 1e6).toFixed(1))} MB`;
89
+ editor.chain().focus().insertContentAt(position, [{
90
+ type: "text",
91
+ text: answer.name || file.name,
92
+ marks: [{
93
+ type: "link",
94
+ attrs: {
95
+ href: answer.href,
96
+ target: "_blank",
97
+ rel: "noopener noreferrer"
98
+ }
99
+ }]
100
+ }, {
101
+ type: "text",
102
+ text: ` (${answer.contentType || "application/octet-stream"}, ${size}) `
103
+ }]).run();
104
+ } catch (error) {
105
+ if (!controller.signal.aborted) options.onError(error, file);
106
+ } finally {
107
+ editor.off("transaction", map);
108
+ if (active === controller) {
109
+ active = void 0;
110
+ state(false);
111
+ }
112
+ }
113
+ }
114
+ };
115
+ }
116
+ //#endregion
50
117
  //#region src/components/shared/overlay.js
51
118
  function stateFor$1(document) {
52
119
  const key = Symbol.for("@threadlabs/looma/overlay-stack/v1");
@@ -14546,7 +14613,8 @@ var EDITOR_REQUESTS = {
14546
14613
  "mention-query": "mentionQuery",
14547
14614
  "link-search": "linkSearch",
14548
14615
  "link-resolve": "linkResolve",
14549
- "image-upload": "imageUpload"
14616
+ "image-upload": "imageUpload",
14617
+ "file-upload": "fileUpload"
14550
14618
  };
14551
14619
  var EDITOR_REQUEST_NAMES = Object.keys(EDITOR_REQUESTS);
14552
14620
  var EMPTY_DOCUMENT = {
@@ -14680,7 +14748,12 @@ var LoomaEditor = defineComponent({
14680
14748
  type: String,
14681
14749
  default: "Item"
14682
14750
  },
14683
- /** Answers `image-upload`: uploads a pasted, dropped, or chosen image and returns the image to insert. */
14751
+ /** Answers `file-upload` with a retained ordinary file link. */
14752
+ fileUpload: {
14753
+ type: Function,
14754
+ default: void 0
14755
+ },
14756
+ /** Answers `image-upload` with the image to insert. */
14684
14757
  imageUpload: {
14685
14758
  type: Function,
14686
14759
  default: void 0
@@ -14750,6 +14823,12 @@ var LoomaEditor = defineComponent({
14750
14823
  const formatAnchorId = `looma-editor-format-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14751
14824
  const formatOpen = ref(false);
14752
14825
  const fileInput = ref(null);
14826
+ const attachmentInput = ref(null);
14827
+ const fileUploadState = shallowRef({
14828
+ uploading: false,
14829
+ progress: null
14830
+ });
14831
+ let attachmentUploads;
14753
14832
  const tablePickerAnchorId = `looma-editor-table-picker-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14754
14833
  const blockActionAnchorId = `ui-block-tools-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
14755
14834
  const linkAnchorId = `looma-editor-link-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
@@ -14918,9 +14997,11 @@ var LoomaEditor = defineComponent({
14918
14997
  let slashDefaults;
14919
14998
  const defaultSlashCommands = () => {
14920
14999
  const uploads = answers("image-upload");
14921
- if (slashDefaults?.uploads !== uploads) slashDefaults = {
15000
+ const files = answers("file-upload");
15001
+ if (slashDefaults?.uploads !== uploads || slashDefaults?.files !== files) slashDefaults = {
14922
15002
  uploads,
14923
- commands: getDefaultSlashCommands(uploads ? () => fileInput.value?.click() : void 0, openChipEditor, () => openLinkEditor())
15003
+ files,
15004
+ commands: getDefaultSlashCommands(uploads ? () => fileInput.value?.click() : void 0, openChipEditor, () => openLinkEditor(), files ? () => attachmentInput.value?.click() : void 0)
14924
15005
  };
14925
15006
  return slashDefaults.commands;
14926
15007
  };
@@ -14946,7 +15027,7 @@ var LoomaEditor = defineComponent({
14946
15027
  const current = new Set(EDITOR_REQUEST_NAMES.filter(answers));
14947
15028
  const arrived = (name) => current.has(name) && !responders.has(name);
14948
15029
  const extensionsArrived = arrived("extensions");
14949
- const slashChanged = arrived("slash-commands") || current.has("image-upload") !== responders.has("image-upload");
15030
+ const slashChanged = arrived("slash-commands") || current.has("image-upload") !== responders.has("image-upload") || current.has("file-upload") !== responders.has("file-upload");
14950
15031
  responders = current;
14951
15032
  if (extensionsArrived) askExtensions();
14952
15033
  if (slashChanged) askSlashCommands();
@@ -15122,6 +15203,14 @@ var LoomaEditor = defineComponent({
15122
15203
  uploadState.value = imageUploads.state;
15123
15204
  });
15124
15205
  uploads = imageUploads;
15206
+ attachmentUploads = createFileUploads({
15207
+ editor: session.editor,
15208
+ upload: () => (file, init) => ask("file-upload", { file }, init),
15209
+ onError: (error, file) => emit("uploadError", error, file),
15210
+ onState: (value) => {
15211
+ fileUploadState.value = value;
15212
+ }
15213
+ });
15125
15214
  const instance = session.editor;
15126
15215
  formatting = createFormattingSelection({
15127
15216
  editor: instance,
@@ -15142,6 +15231,7 @@ var LoomaEditor = defineComponent({
15142
15231
  footnoteEditing?.close();
15143
15232
  formatting?.destroy();
15144
15233
  uploads?.cancel();
15234
+ attachmentUploads?.cancel();
15145
15235
  previous.destroy();
15146
15236
  build(content);
15147
15237
  };
@@ -15167,6 +15257,7 @@ var LoomaEditor = defineComponent({
15167
15257
  onBeforeUnmount(() => {
15168
15258
  const element = session?.editor.options.element;
15169
15259
  element?.parentNode?.replaceChild(element.cloneNode(true), element);
15260
+ attachmentUploads?.cancel();
15170
15261
  connection.abort();
15171
15262
  stopAnnouncements?.();
15172
15263
  linkEditing?.destroy();
@@ -15712,6 +15803,12 @@ var LoomaEditor = defineComponent({
15712
15803
  if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
15713
15804
  if (tablePointerFrame !== null) cancelAnimationFrame(tablePointerFrame);
15714
15805
  });
15806
+ const onAttachmentChange = async (event) => {
15807
+ const input = event.target;
15808
+ const file = input.files?.[0];
15809
+ input.value = "";
15810
+ if (file) await attachmentUploads?.insert(file);
15811
+ };
15715
15812
  const onFileChange = async (event) => {
15716
15813
  const input = event.target;
15717
15814
  const files = Array.from(input.files ?? []);
@@ -15954,6 +16051,7 @@ var LoomaEditor = defineComponent({
15954
16051
  variant: tablePickerOpen.value ? "solid" : "ghost",
15955
16052
  "aria-expanded": tablePickerOpen.value ? "true" : "false"
15956
16053
  }, () => loomaIcon("table")),
16054
+ commandButton("Attach file", "paperclip", false, fileUploadState.value.uploading || !answers("file-upload"), () => attachmentInput.value?.click()),
15957
16055
  commandButton(uploadState.value.uploading ? "Uploading image" : "Insert image", "image", false, uploadState.value.uploading || !answers("image-upload"), () => fileInput.value?.click()),
15958
16056
  h("span", {
15959
16057
  class: "divider",
@@ -16143,6 +16241,22 @@ var LoomaEditor = defineComponent({
16143
16241
  "aria-hidden": "true",
16144
16242
  onChange: onFileChange
16145
16243
  }),
16244
+ h("input", {
16245
+ ref: attachmentInput,
16246
+ class: "looma-editor__file-input",
16247
+ type: "file",
16248
+ tabindex: -1,
16249
+ "aria-hidden": "true",
16250
+ onChange: onAttachmentChange
16251
+ }),
16252
+ fileUploadState.value.uploading ? h(StatusMessage, { kind: "loading" }, {
16253
+ default: () => "Attaching file…",
16254
+ action: () => h(Button, {
16255
+ size: "sm",
16256
+ variant: "ghost",
16257
+ onClick: () => attachmentUploads?.cancel()
16258
+ }, () => "Cancel")
16259
+ }) : null,
16146
16260
  uploadState.value.uploading ? h(StatusMessage, { kind: "loading" }, {
16147
16261
  default: () => uploadProgressText(uploadState.value.progress),
16148
16262
  action: () => h(Button, {