@threadlabs/looma 0.31.5 → 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.
@@ -1,5 +1,5 @@
1
1
  import { n as TABLE_CELL_BACKGROUND_PRESETS } from "./table-backgrounds.js";
2
- import { _ as parseLoomaAttributes, b as splitLoomaFrontmatter, c as LOOMA_CONTAINER_NAMES, d as formatLoomaAttributes, f as loomaCodeFence, g as normalizeLoomaChipTone, h as normalizeLoomaCalloutTone, i as referenceDisplay, l as applyLoomaCodeNotation, m as loomaHeadingSlug, n as LoomaReferencePresentation, p as loomaContainerMarker, r as getReferenceDisplay, s as LOOMA_CHIP_TONES, t as LoomaReference, u as assignLoomaHeadingIds, v as parseLoomaFenceInfo, y as readLoomaAttributes } from "./reference-display.js";
2
+ import { _ as normalizeLoomaChipTone, b as readLoomaAttributes, c as LOOMA_CONTAINER_NAMES, d as assignLoomaHeadingIds, f as formatLoomaAttributes, g as normalizeLoomaCalloutTone, h as loomaHeadingSlug, i as referenceDisplay, l as LOOMA_MENTION_HANDLE, m as loomaContainerMarker, n as LoomaReferencePresentation, p as loomaCodeFence, r as getReferenceDisplay, s as LOOMA_CHIP_TONES, t as LoomaReference, u as applyLoomaCodeNotation, v as parseLoomaAttributes, x as splitLoomaFrontmatter, y as parseLoomaFenceInfo } from "./reference-display.js";
3
3
  import { Extension, InputRule, Mark, Node as Node$1, PasteRule, callOrReturn, canInsertNode, combineTransactionSteps, escapeForRegEx, findChildren, findChildrenInRange, findParentNodeClosestToPos, getAttributes, getChangedRanges, getExtensionField, getMarksBetween, getText, getTextSerializersFromSchema, isNodeEmpty, isNodeSelection, markInputRule, markPasteRule, mergeAttributes, nodeInputRule, textInputRule, textblockTypeInputRule, wrappingInputRule } from "@tiptap/core";
4
4
  import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
5
5
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
@@ -10072,7 +10072,7 @@ function taskLists(state) {
10072
10072
  function directive(state, silent) {
10073
10073
  const { src, pos } = state;
10074
10074
  if (src.charCodeAt(pos) !== 58) return false;
10075
- const name = /^:(chip|mention)\[/.exec(src.slice(pos, pos + 10))?.[1];
10075
+ const name = /^:(chip)\[/.exec(src.slice(pos, pos + 10))?.[1];
10076
10076
  if (!name) return false;
10077
10077
  let label = "";
10078
10078
  let index = pos + name.length + 2;
@@ -10086,7 +10086,6 @@ function directive(state, silent) {
10086
10086
  end: index + 1
10087
10087
  };
10088
10088
  if (!read || read.end > state.posMax) return false;
10089
- if (name === "mention" && typeof read.attributes.id !== "string") return false;
10090
10089
  if (!silent) {
10091
10090
  const token = state.push(`looma_${name}`, "span", 0);
10092
10091
  token.meta = {
@@ -10097,6 +10096,37 @@ function directive(state, silent) {
10097
10096
  state.pos = read.end;
10098
10097
  return true;
10099
10098
  }
10099
+ function mention(state, silent) {
10100
+ const { src, pos } = state;
10101
+ if (src.charCodeAt(pos) !== 64 || pos > 0 && !/\s/u.test(src[pos - 1])) return false;
10102
+ let index = pos + 1;
10103
+ let label = "";
10104
+ if (src[index] === "[") {
10105
+ for (index++; index < state.posMax && src[index] !== "]"; index++) {
10106
+ if (src[index] === "\\" && index + 1 < state.posMax) index++;
10107
+ label += src[index];
10108
+ }
10109
+ if (src[index] !== "]" || !label) return false;
10110
+ index++;
10111
+ } else {
10112
+ label = LOOMA_MENTION_HANDLE.exec(src.slice(index, state.posMax))?.[0] ?? "";
10113
+ if (!label) return false;
10114
+ index += label.length;
10115
+ }
10116
+ let ref = src[index] === "#" ? LOOMA_MENTION_HANDLE.exec(src.slice(index + 1, state.posMax))?.[0] : void 0;
10117
+ if (ref) index += ref.length + 1;
10118
+ const read = src[index] === "{" ? readLoomaAttributes(src, index) : null;
10119
+ if (read && read.end <= state.posMax) {
10120
+ if (typeof read.attributes.ref === "string") ref ??= read.attributes.ref;
10121
+ index = read.end;
10122
+ }
10123
+ if (!silent) state.push("looma_mention", "span", 0).meta = {
10124
+ label,
10125
+ ref
10126
+ };
10127
+ state.pos = index;
10128
+ return true;
10129
+ }
10100
10130
  function inlineText(token) {
10101
10131
  return (token?.children ?? []).map((child) => child.type === "image" ? inlineText(child) : child.content).join("");
10102
10132
  }
@@ -10318,6 +10348,7 @@ function loomaMarkdown$1(md, { codeNotation = "apply" } = {}) {
10318
10348
  "list"
10319
10349
  ] });
10320
10350
  md.inline.ruler.before("link", "looma_directive", directive);
10351
+ md.inline.ruler.before("link", "looma_mention", mention);
10321
10352
  md.inline.ruler.before("link", "looma_footnote_ref", footnoteReference);
10322
10353
  md.core.ruler.after("block", "looma_task_lists", taskLists);
10323
10354
  md.core.ruler.before("inline", "looma_alerts", alerts);
@@ -10367,9 +10398,8 @@ function loomaMarkdown$1(md, { codeNotation = "apply" } = {}) {
10367
10398
  return `<span data-looma-chip="" data-label="${escape(label)}" data-color="${tone}"${label ? "" : " data-placeholder=\"\""}>${escape(label)}</span>`;
10368
10399
  };
10369
10400
  rules.looma_mention = (tokens, index) => {
10370
- const { label, attributes } = tokens[index].meta;
10371
- const id = String(attributes.id);
10372
- return `<span data-type="mention" class="looma-mention" data-id="${escape(id)}" data-label="${escape(label || id)}">@${escape(label || id)}</span>`;
10401
+ const { label, ref } = tokens[index].meta;
10402
+ return `<span data-type="mention" class="looma-mention"${ref === void 0 ? "" : ` data-id="${escape(ref)}"`} data-label="${escape(label)}">@${escape(label)}</span>`;
10373
10403
  };
10374
10404
  const code = (content, info) => {
10375
10405
  const { language, meta } = parseLoomaFenceInfo(info);
@@ -10525,8 +10555,9 @@ var NODES = {
10525
10555
  state.write(`:chip[${label(String(node.attrs.label ?? ""))}]${formatLoomaAttributes({ tone: tone !== "neutral" ? tone : null })}`);
10526
10556
  },
10527
10557
  mention: (state, node) => {
10528
- const id = String(node.attrs.id ?? "");
10529
- state.write(`:mention[${label(String(node.attrs.label ?? id))}]${formatLoomaAttributes({ id }, { hashId: false })}`);
10558
+ const id = node.attrs.id == null ? "" : String(node.attrs.id);
10559
+ const reference = !id ? "" : LOOMA_MENTION_HANDLE.exec(id)?.[0] === id ? `#${id}` : formatLoomaAttributes({ ref: id });
10560
+ state.write(`@[${label(String(node.attrs.label || id))}]${reference}`);
10530
10561
  },
10531
10562
  loomaReference: (state, node) => {
10532
10563
  const wrapper = document.createElement("span");
@@ -11955,7 +11986,7 @@ var CALLOUT_COMMANDS = [
11955
11986
  * @ownership Returned command objects belong to the caller and are recreated
11956
11987
  * per call so one editor cannot mutate another editor's command inventory.
11957
11988
  */
11958
- function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor) {
11989
+ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor, onOpenFilePicker) {
11959
11990
  return [
11960
11991
  {
11961
11992
  title: "Text",
@@ -12178,6 +12209,21 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLink
12178
12209
  onOpenLinkEditor();
12179
12210
  }
12180
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
+ }] : [],
12181
12227
  ...onOpenImagePicker ? [{
12182
12228
  title: "Image",
12183
12229
  description: "Upload an image",
@@ -12197,6 +12243,7 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLink
12197
12243
  ...command,
12198
12244
  id: command.title.toLowerCase().replaceAll(" ", "-"),
12199
12245
  group: CALLOUT_COMMANDS.some((callout) => callout.title === command.title) ? "Callouts" : [
12246
+ "File attachment",
12200
12247
  "Chip",
12201
12248
  "Inline code",
12202
12249
  "Link",
@@ -44,6 +44,11 @@ function normalizeLoomaChipTone(value) {
44
44
  return LOOMA_CHIP_TONES.includes(value) ? value : "neutral";
45
45
  }
46
46
  /**
47
+ * A mention handle, as `@handle` and the `#ref` of `@[Label]#ref` spell it: a letter, number or
48
+ * underscore, then letters, marks, numbers, underscores and hyphens.
49
+ */
50
+ var LOOMA_MENTION_HANDLE = /^[\p{L}\p{N}_][\p{L}\p{M}\p{N}_-]*/u;
51
+ /**
47
52
  * Names that open a `:::` container, matched without regard to case. A container with any other
48
53
  * name is ordinary text.
49
54
  */
@@ -445,4 +450,4 @@ var LoomaReferencePresentation = Extension.create({
445
450
  }
446
451
  });
447
452
  //#endregion
448
- export { parseLoomaAttributes as _, setReferenceDisplay as a, splitLoomaFrontmatter as b, LOOMA_CONTAINER_NAMES as c, formatLoomaAttributes as d, loomaCodeFence as f, normalizeLoomaChipTone as g, normalizeLoomaCalloutTone as h, referenceDisplay as i, applyLoomaCodeNotation as l, loomaHeadingSlug as m, LoomaReferencePresentation as n, LOOMA_CALLOUT_TONES as o, loomaContainerMarker as p, getReferenceDisplay as r, LOOMA_CHIP_TONES as s, LoomaReference as t, assignLoomaHeadingIds as u, parseLoomaFenceInfo as v, readLoomaAttributes as y };
453
+ export { normalizeLoomaChipTone as _, setReferenceDisplay as a, readLoomaAttributes as b, LOOMA_CONTAINER_NAMES as c, assignLoomaHeadingIds as d, formatLoomaAttributes as f, normalizeLoomaCalloutTone as g, loomaHeadingSlug as h, referenceDisplay as i, LOOMA_MENTION_HANDLE as l, loomaContainerMarker as m, LoomaReferencePresentation as n, LOOMA_CALLOUT_TONES as o, loomaCodeFence as p, getReferenceDisplay as r, LOOMA_CHIP_TONES as s, LoomaReference as t, applyLoomaCodeNotation as u, parseLoomaAttributes as v, splitLoomaFrontmatter as x, parseLoomaFenceInfo as y };
@@ -1,4 +1,4 @@
1
- import { m as loomaHeadingSlug, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, s as LOOMA_CHIP_TONES } from "./chunks/reference-display.js";
1
+ import { h as loomaHeadingSlug, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, s as LOOMA_CHIP_TONES } from "./chunks/reference-display.js";
2
2
  import { parse, stringify } from "expressmark";
3
3
  //#region src/editor/expressmark.ts
4
4
  /**
@@ -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,
@@ -96,7 +96,7 @@ var files = /* #__PURE__ */ Object.assign({
96
96
  "../../markdown-fixtures/lists/blocks.html": "<ul>\n<li>One\n<ul>\n<li>Nested</li>\n</ul>\n</li>\n<li>Two</li>\n</ul>\n<ol start=\"3\">\n<li>Three</li>\n<li>Four</li>\n</ol>\n<blockquote>\n<p>Quoted</p>\n</blockquote>\n<hr>\n",
97
97
  "../../markdown-fixtures/lists/blocks.md": "- One\n - Nested\n- Two\n\n3. Three\n4. Four\n\n> Quoted\n\n---\n",
98
98
  "../../markdown-fixtures/mentions/person.html": "<p>Thanks, <span data-type=\"mention\" class=\"looma-mention\" data-id=\"u1\" data-label=\"Ada Lovelace\">@Ada Lovelace</span>.</p>\n",
99
- "../../markdown-fixtures/mentions/person.md": "Thanks, :mention[Ada Lovelace]{id=u1}.\n",
99
+ "../../markdown-fixtures/mentions/person.md": "Thanks, @[Ada Lovelace]#u1.\n",
100
100
  "../../markdown-fixtures/tables/gfm.html": "<table>\n<thead>\n<tr>\n<th>Package</th>\n<th style=\"text-align:right\">Version</th>\n<th style=\"text-align:center\">Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>looma</td>\n<td style=\"text-align:right\">0.28</td>\n<td style=\"text-align:center\">Components</td>\n</tr>\n</tbody>\n</table>\n",
101
101
  "../../markdown-fixtures/tables/gfm.md": "| Package | Version | Notes |\n| --- | ---: | :---: |\n| looma | 0.28 | Components |\n",
102
102
  "../../markdown-fixtures/tables/left-aligned.html": "<table>\n<thead>\n<tr>\n<th style=\"text-align:left\">Name</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:left\">a | b</td>\n<td><code>x | y</code></td>\n</tr>\n</tbody>\n</table>\n",
@@ -32,6 +32,11 @@ export type LoomaChipTone = (typeof LOOMA_CHIP_TONES)[number];
32
32
  * @failure Unknown values become `neutral`.
33
33
  */
34
34
  export declare function normalizeLoomaChipTone(value: unknown): LoomaChipTone;
35
+ /**
36
+ * A mention handle, as `@handle` and the `#ref` of `@[Label]#ref` spell it: a letter, number or
37
+ * underscore, then letters, marks, numbers, underscores and hyphens.
38
+ */
39
+ export declare const LOOMA_MENTION_HANDLE: RegExp;
35
40
  /**
36
41
  * Names that open a `:::` container, matched without regard to case. A container with any other
37
42
  * name is ordinary text.
package/markdown/index.js CHANGED
@@ -66,6 +66,11 @@ function normalizeLoomaChipTone(value) {
66
66
  return LOOMA_CHIP_TONES.includes(value) ? value : "neutral";
67
67
  }
68
68
  /**
69
+ * A mention handle, as `@handle` and the `#ref` of `@[Label]#ref` spell it: a letter, number or
70
+ * underscore, then letters, marks, numbers, underscores and hyphens.
71
+ */
72
+ var LOOMA_MENTION_HANDLE = /^[\p{L}\p{N}_][\p{L}\p{M}\p{N}_-]*/u;
73
+ /**
69
74
  * Names that open a `:::` container, matched without regard to case. A container with any other
70
75
  * name is ordinary text.
71
76
  */
@@ -263,4 +268,4 @@ function loomaCodeFence(code) {
263
268
  return "`".repeat(Math.max(3, longest + 1));
264
269
  }
265
270
  //#endregion
266
- export { LOOMA_BADGE_TONES, LOOMA_CALLOUT_TONES, LOOMA_CALLOUT_TONE_ALIASES, LOOMA_CHIP_TONES, LOOMA_CONTAINER_NAMES, LOOMA_MEANING_TONES, applyLoomaCodeNotation, assignLoomaHeadingIds, formatLoomaAttributes, loomaCodeFence, loomaContainerMarker, loomaHeadingSlug, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter };
271
+ export { LOOMA_BADGE_TONES, LOOMA_CALLOUT_TONES, LOOMA_CALLOUT_TONE_ALIASES, LOOMA_CHIP_TONES, LOOMA_CONTAINER_NAMES, LOOMA_MEANING_TONES, LOOMA_MENTION_HANDLE, applyLoomaCodeNotation, assignLoomaHeadingIds, formatLoomaAttributes, loomaCodeFence, loomaContainerMarker, loomaHeadingSlug, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter };
@@ -1,4 +1,4 @@
1
- import { LOOMA_CONTAINER_NAMES, applyLoomaCodeNotation, assignLoomaHeadingIds, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter } from "./index.js";
1
+ import { LOOMA_CONTAINER_NAMES, LOOMA_MENTION_HANDLE, applyLoomaCodeNotation, assignLoomaHeadingIds, normalizeLoomaCalloutTone, normalizeLoomaChipTone, parseLoomaAttributes, parseLoomaFenceInfo, readLoomaAttributes, splitLoomaFrontmatter } from "./index.js";
2
2
  import MarkdownIt from "markdown-it";
3
3
  //#region src/markdown/markdown-it.ts
4
4
  var CONTAINER_OPEN = /^(:{3,})[ \t]*([A-Za-z][\w-]*)(?:[ \t]+(.*?))?[ \t]*$/;
@@ -210,7 +210,7 @@ function taskLists(state) {
210
210
  function directive(state, silent) {
211
211
  const { src, pos } = state;
212
212
  if (src.charCodeAt(pos) !== 58) return false;
213
- const name = /^:(chip|mention)\[/.exec(src.slice(pos, pos + 10))?.[1];
213
+ const name = /^:(chip)\[/.exec(src.slice(pos, pos + 10))?.[1];
214
214
  if (!name) return false;
215
215
  let label = "";
216
216
  let index = pos + name.length + 2;
@@ -224,7 +224,6 @@ function directive(state, silent) {
224
224
  end: index + 1
225
225
  };
226
226
  if (!read || read.end > state.posMax) return false;
227
- if (name === "mention" && typeof read.attributes.id !== "string") return false;
228
227
  if (!silent) {
229
228
  const token = state.push(`looma_${name}`, "span", 0);
230
229
  token.meta = {
@@ -235,6 +234,37 @@ function directive(state, silent) {
235
234
  state.pos = read.end;
236
235
  return true;
237
236
  }
237
+ function mention(state, silent) {
238
+ const { src, pos } = state;
239
+ if (src.charCodeAt(pos) !== 64 || pos > 0 && !/\s/u.test(src[pos - 1])) return false;
240
+ let index = pos + 1;
241
+ let label = "";
242
+ if (src[index] === "[") {
243
+ for (index++; index < state.posMax && src[index] !== "]"; index++) {
244
+ if (src[index] === "\\" && index + 1 < state.posMax) index++;
245
+ label += src[index];
246
+ }
247
+ if (src[index] !== "]" || !label) return false;
248
+ index++;
249
+ } else {
250
+ label = LOOMA_MENTION_HANDLE.exec(src.slice(index, state.posMax))?.[0] ?? "";
251
+ if (!label) return false;
252
+ index += label.length;
253
+ }
254
+ let ref = src[index] === "#" ? LOOMA_MENTION_HANDLE.exec(src.slice(index + 1, state.posMax))?.[0] : void 0;
255
+ if (ref) index += ref.length + 1;
256
+ const read = src[index] === "{" ? readLoomaAttributes(src, index) : null;
257
+ if (read && read.end <= state.posMax) {
258
+ if (typeof read.attributes.ref === "string") ref ??= read.attributes.ref;
259
+ index = read.end;
260
+ }
261
+ if (!silent) state.push("looma_mention", "span", 0).meta = {
262
+ label,
263
+ ref
264
+ };
265
+ state.pos = index;
266
+ return true;
267
+ }
238
268
  function inlineText(token) {
239
269
  return (token?.children ?? []).map((child) => child.type === "image" ? inlineText(child) : child.content).join("");
240
270
  }
@@ -456,6 +486,7 @@ function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
456
486
  "list"
457
487
  ] });
458
488
  md.inline.ruler.before("link", "looma_directive", directive);
489
+ md.inline.ruler.before("link", "looma_mention", mention);
459
490
  md.inline.ruler.before("link", "looma_footnote_ref", footnoteReference);
460
491
  md.core.ruler.after("block", "looma_task_lists", taskLists);
461
492
  md.core.ruler.before("inline", "looma_alerts", alerts);
@@ -505,9 +536,8 @@ function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
505
536
  return `<span data-looma-chip="" data-label="${escape(label)}" data-color="${tone}"${label ? "" : " data-placeholder=\"\""}>${escape(label)}</span>`;
506
537
  };
507
538
  rules.looma_mention = (tokens, index) => {
508
- const { label, attributes } = tokens[index].meta;
509
- const id = String(attributes.id);
510
- return `<span data-type="mention" class="looma-mention" data-id="${escape(id)}" data-label="${escape(label || id)}">@${escape(label || id)}</span>`;
539
+ const { label, ref } = tokens[index].meta;
540
+ return `<span data-type="mention" class="looma-mention"${ref === void 0 ? "" : ` data-id="${escape(ref)}"`} data-label="${escape(label)}">@${escape(label)}</span>`;
511
541
  };
512
542
  const code = (content, info) => {
513
543
  const { language, meta } = parseLoomaFenceInfo(info);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.31.5",
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, {