@threadlabs/looma 0.26.4 → 0.26.6

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.
@@ -29,6 +29,20 @@ export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUplo
29
29
  * set. Narrow viewports retain a dock; document commands/content are unchanged.
30
30
  */
31
31
  export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover" | "contextual";
32
+ /** Host-owned non-mutating commands for the selected passage. */
33
+ export interface LoomaEditorSelectionAction {
34
+ id: string;
35
+ label: string;
36
+ }
37
+ /** Exact editor range captured before focus leaves for a host action. */
38
+ export interface LoomaEditorSelectionActionDetail {
39
+ id: string;
40
+ from: number;
41
+ to: number;
42
+ text: string;
43
+ before: string;
44
+ after: string;
45
+ }
32
46
  /**
33
47
  * Replace the slash inventory, or return retained defaults plus host commands.
34
48
  * @ownership The host owns its command objects and mutations; Looma supplies
@@ -160,6 +174,16 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
160
174
  * In `popover` mode, the ID of the app's button that toggles the full toolbar and anchors it.
161
175
  * The button needs no click handler of its own.
162
176
  */
177
+ /** Keep frequent inline styles visible; Format opens the complete existing inventory. */
178
+ compactToolbar: {
179
+ type: BooleanConstructor;
180
+ default: boolean;
181
+ };
182
+ /** Optional passage actions also work on read-only selections without granting editing. */
183
+ selectionActions: {
184
+ type: PropType<LoomaEditorSelectionAction[]>;
185
+ default: () => never[];
186
+ };
163
187
  toolbarTriggerId: {
164
188
  type: StringConstructor;
165
189
  default: string;
@@ -184,6 +208,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
184
208
  "update:toolbarOpen": (_value: boolean) => true;
185
209
  update: (_value: JSONContent) => true;
186
210
  ready: (_editor: Editor) => true;
211
+ selectionAction: (_detail: LoomaEditorSelectionActionDetail) => true;
187
212
  uploadError: (_error: unknown, _file: File) => true;
188
213
  imageActivate: (_detail: LoomaImageActivationDetail) => true;
189
214
  imageRenditionError: (_detail: LoomaImageRenditionErrorDetail) => true;
@@ -281,6 +306,16 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
281
306
  * In `popover` mode, the ID of the app's button that toggles the full toolbar and anchors it.
282
307
  * The button needs no click handler of its own.
283
308
  */
309
+ /** Keep frequent inline styles visible; Format opens the complete existing inventory. */
310
+ compactToolbar: {
311
+ type: BooleanConstructor;
312
+ default: boolean;
313
+ };
314
+ /** Optional passage actions also work on read-only selections without granting editing. */
315
+ selectionActions: {
316
+ type: PropType<LoomaEditorSelectionAction[]>;
317
+ default: () => never[];
318
+ };
284
319
  toolbarTriggerId: {
285
320
  type: StringConstructor;
286
321
  default: string;
@@ -303,6 +338,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
303
338
  "onUpdate:modelValue"?: ((_value: JSONContent) => any) | undefined;
304
339
  "onUpdate:toolbarOpen"?: ((_value: boolean) => any) | undefined;
305
340
  onReady?: ((_editor: Editor) => any) | undefined;
341
+ onSelectionAction?: ((_detail: LoomaEditorSelectionActionDetail) => any) | undefined;
306
342
  onUploadError?: ((_error: unknown, _file: File) => any) | undefined;
307
343
  onImageActivate?: ((_detail: LoomaImageActivationDetail) => any) | undefined;
308
344
  onImageRenditionError?: ((_detail: LoomaImageRenditionErrorDetail) => any) | undefined;
@@ -326,6 +362,8 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
326
362
  uploadImage: LoomaImageUploader | undefined;
327
363
  resolveImageAttributes: LoomaImageAttributeResolver | undefined;
328
364
  toolbarMode: LoomaEditorToolbarMode;
365
+ compactToolbar: boolean;
366
+ selectionActions: LoomaEditorSelectionAction[];
329
367
  toolbarTriggerId: string;
330
368
  toolbarOpen: boolean;
331
369
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,4 +1,4 @@
1
1
  export * from "@threadlabs/looma/editor";
2
2
  export * from "./primitives.js";
3
- export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSlashCommands, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
3
+ export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorSlashCommands, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
4
4
  export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "./image-delivery.js";
@@ -1700,6 +1700,16 @@ var LoomaEditor = defineComponent({
1700
1700
  * In `popover` mode, the ID of the app's button that toggles the full toolbar and anchors it.
1701
1701
  * The button needs no click handler of its own.
1702
1702
  */
1703
+ /** Keep frequent inline styles visible; Format opens the complete existing inventory. */
1704
+ compactToolbar: {
1705
+ type: Boolean,
1706
+ default: false
1707
+ },
1708
+ /** Optional passage actions also work on read-only selections without granting editing. */
1709
+ selectionActions: {
1710
+ type: Array,
1711
+ default: () => []
1712
+ },
1703
1713
  toolbarTriggerId: {
1704
1714
  type: String,
1705
1715
  default: ""
@@ -1723,12 +1733,15 @@ var LoomaEditor = defineComponent({
1723
1733
  "update:toolbarOpen": (_value) => true,
1724
1734
  update: (_value) => true,
1725
1735
  ready: (_editor) => true,
1736
+ selectionAction: (_detail) => true,
1726
1737
  uploadError: (_error, _file) => true,
1727
1738
  imageActivate: (_detail) => true,
1728
1739
  imageRenditionError: (_detail) => true
1729
1740
  },
1730
1741
  setup(props, { attrs, emit, expose }) {
1731
1742
  const root = ref(null);
1743
+ const formatAnchorId = `looma-editor-format-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
1744
+ const formatOpen = ref(false);
1732
1745
  const fileInput = ref(null);
1733
1746
  const tablePickerAnchorId = `looma-editor-table-picker-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
1734
1747
  const blockActionAnchorId = `ui-block-tools-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
@@ -1806,7 +1819,7 @@ var LoomaEditor = defineComponent({
1806
1819
  let heldPressTimer;
1807
1820
  let heldPress = null;
1808
1821
  const floatingFormattingMode = () => props.toolbarMode !== "sticky" && !(props.toolbarMode === "popover" && props.toolbarOpen) && !mobile.value;
1809
- const formattingPickerOpen = () => linkOpen.value || tablePickerOpen.value;
1822
+ const formattingPickerOpen = () => linkOpen.value || tablePickerOpen.value || formatOpen.value;
1810
1823
  const cancelHeldPress = () => {
1811
1824
  clearTimeout(heldPressTimer);
1812
1825
  heldPressTimer = void 0;
@@ -2479,8 +2492,12 @@ var LoomaEditor = defineComponent({
2479
2492
  });
2480
2493
  if (!editable) chipOpen.value = false;
2481
2494
  });
2482
- watch([linkOpen, tablePickerOpen], ([link, table]) => {
2483
- if (link || table || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.popper.contains(document.activeElement)) return;
2495
+ watch([
2496
+ linkOpen,
2497
+ tablePickerOpen,
2498
+ formatOpen
2499
+ ], ([link, table, format]) => {
2500
+ if (link || table || format || !floatingFormattingMode() || editor.value?.isFocused || formattingPopup?.popper.contains(document.activeElement)) return;
2484
2501
  heldFormattingSelection = null;
2485
2502
  formattingPopup?.hide();
2486
2503
  });
@@ -3036,7 +3053,34 @@ var LoomaEditor = defineComponent({
3036
3053
  onClick: run
3037
3054
  }, () => loomaIcon(icon));
3038
3055
  };
3039
- const renderToolbar = (instance, floating = true, textOnly = false) => {
3056
+ const renderSelectionActions = (instance) => {
3057
+ const selection = instance.state.selection;
3058
+ if (selection.empty || !(selection instanceof TextSelection || selection instanceof AllSelection)) return [];
3059
+ return props.selectionActions.map((action) => h(Button, {
3060
+ key: action.id,
3061
+ "aria-label": action.label,
3062
+ type: "button",
3063
+ variant: "ghost",
3064
+ tone: "neutral",
3065
+ size: "sm",
3066
+ onPointerdown: (event) => event.preventDefault(),
3067
+ onClick: () => {
3068
+ const { from, to } = instance.state.selection;
3069
+ if (from === to) return;
3070
+ const doc = instance.state.doc;
3071
+ emit("selectionAction", {
3072
+ id: action.id,
3073
+ from,
3074
+ to,
3075
+ text: doc.textBetween(from, to, "\n"),
3076
+ before: doc.textBetween(Math.max(0, from - 48), from, "\n"),
3077
+ after: doc.textBetween(to, Math.min(doc.content.size, to + 48), "\n")
3078
+ });
3079
+ formattingPopup?.hide();
3080
+ }
3081
+ }, () => action.label));
3082
+ };
3083
+ const renderToolbar = (instance, floating = true, textOnly = false, full = false) => {
3040
3084
  const textButtons = [
3041
3085
  commandButton("Bold", "bold", instance.isActive("bold"), !instance.can().toggleBold(), () => instance.chain().focus().toggleBold().run(), "Mod-b"),
3042
3086
  commandButton("Italic", "italic", instance.isActive("italic"), !instance.can().toggleItalic(), () => instance.chain().focus().toggleItalic().run(), "Mod-i"),
@@ -3045,7 +3089,7 @@ var LoomaEditor = defineComponent({
3045
3089
  ...authorHighlight ? [commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run(), "Mod-Shift-h")] : [],
3046
3090
  commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run(), "Mod-e")
3047
3091
  ];
3048
- const linkTriggerId = textOnly ? `${linkAnchorId}-selection` : linkAnchorId;
3092
+ const linkTriggerId = full ? `${linkAnchorId}-full` : textOnly ? `${linkAnchorId}-selection` : linkAnchorId;
3049
3093
  textButtons.push(h(IconButton, {
3050
3094
  id: linkTriggerId,
3051
3095
  class: "looma-editor__toolbar-button",
@@ -3072,7 +3116,7 @@ var LoomaEditor = defineComponent({
3072
3116
  },
3073
3117
  onFocusout: hideTool
3074
3118
  }, () => loomaIcon("link")));
3075
- const buttons = textOnly ? textButtons : [
3119
+ let buttons = textOnly ? textButtons : [
3076
3120
  ...textButtons,
3077
3121
  h("span", {
3078
3122
  class: "divider",
@@ -3120,7 +3164,24 @@ var LoomaEditor = defineComponent({
3120
3164
  commandButton("Undo", "undo", false, !instance.can().undo(), () => instance.chain().focus().undo().run(), "Mod-z"),
3121
3165
  commandButton("Redo", "redo", false, !instance.can().redo(), () => instance.chain().focus().redo().run(), isApplePlatform() ? "Mod-Shift-z" : "Mod-y")
3122
3166
  ];
3123
- return h(EditorToolbar, { floating }, () => [...buttons, h(Tooltip, {
3167
+ if (!props.editable) buttons = [];
3168
+ else if (props.compactToolbar && !mobile.value && !full) buttons = [
3169
+ textButtons[0],
3170
+ textButtons[1],
3171
+ textButtons.at(-1),
3172
+ h(Button, {
3173
+ id: formatAnchorId,
3174
+ type: "button",
3175
+ variant: "ghost",
3176
+ tone: "neutral",
3177
+ size: "sm",
3178
+ "aria-label": "Format",
3179
+ "aria-expanded": formatOpen.value ? "true" : "false",
3180
+ onPointerdown: (event) => event.preventDefault()
3181
+ }, () => "Format")
3182
+ ];
3183
+ const actions = full ? [] : renderSelectionActions(instance);
3184
+ return h(EditorToolbar, { floating }, () => [...mobile.value ? [...actions, ...buttons] : [...buttons, ...actions], h(Tooltip, {
3124
3185
  class: "looma-editor__toolbar-tip",
3125
3186
  for: tooltipFor.value,
3126
3187
  open: tooltipOpen.value,
@@ -3215,11 +3276,11 @@ var LoomaEditor = defineComponent({
3215
3276
  placement: "top",
3216
3277
  inverse: true
3217
3278
  }, () => "Drag to resize column") : null,
3218
- instance && props.editable && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3279
+ instance && (props.editable || props.selectionActions.length) && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
3219
3280
  editor: instance,
3220
3281
  pluginKey: "looma-text-formatting-menu",
3221
3282
  updateDelay: 0,
3222
- shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (menuEditor.isFocused || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
3283
+ shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (!props.editable && props.selectionActions.length > 0 || menuEditor.isFocused || formattingPopup?.popper.contains(document.activeElement) || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
3223
3284
  tippyOptions: {
3224
3285
  onCreate: (popup) => {
3225
3286
  formattingPopup = popup;
@@ -3302,7 +3363,7 @@ var LoomaEditor = defineComponent({
3302
3363
  value: codeUi.language,
3303
3364
  onValueChange: (event) => chooseCodeLanguage(event.detail)
3304
3365
  }, () => codeLanguageOptions.value.map((option) => h("option", { value: option.value }, option.label)))]) : null,
3305
- instance && props.editable && mobile.value && (editorFocused.value || linkOpen.value) ? h("div", {
3366
+ instance && (props.editable || props.selectionActions.length && !instance.state.selection.empty) && mobile.value && (editorFocused.value || linkOpen.value || !props.editable) ? h("div", {
3306
3367
  ref: mobileToolbarShell,
3307
3368
  class: "looma-editor__mobile-toolbar-shell",
3308
3369
  "data-mode": mobileToolbarMode.value,
@@ -3407,6 +3468,18 @@ var LoomaEditor = defineComponent({
3407
3468
  if (current && !current.empty && linkSelection && current.from === linkSelection.from && current.to === linkSelection.to) editor.value?.commands.setTextSelection(current.to);
3408
3469
  }
3409
3470
  }, renderLinkForm),
3471
+ props.editable && props.compactToolbar && !mobile.value ? h(Popover, {
3472
+ class: "looma-editor__compact-formatting",
3473
+ for: formatAnchorId,
3474
+ open: formatOpen.value,
3475
+ placement: "bottom-start",
3476
+ onOpen: () => {
3477
+ formatOpen.value = true;
3478
+ },
3479
+ onClose: () => {
3480
+ formatOpen.value = false;
3481
+ }
3482
+ }, () => instance ? renderToolbar(instance, true, false, true) : null) : null,
3410
3483
  h(Menu, {
3411
3484
  for: blockActionAnchorId,
3412
3485
  placement: mobile.value ? "top-start" : "bottom-start",