@threadlabs/looma 0.26.25 → 0.27.0

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 (80) hide show
  1. package/components/shared/editor.js +4 -5
  2. package/components/shared/overlay.js +231 -55
  3. package/components/ui-affordance-scope/ui-affordance-scope.js +4 -8
  4. package/components/ui-card/ui-card.html +3 -0
  5. package/components/ui-dialog/ui-dialog.html +7 -5
  6. package/components/ui-dialog/ui-dialog.js +67 -7
  7. package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +6 -0
  8. package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +5 -9
  9. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +6 -0
  10. package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +5 -9
  11. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +21 -4
  12. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.js +50 -22
  13. package/components/ui-editor-table-overlay/ui-editor-table-overlay.js +4 -6
  14. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +3 -11
  15. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.js +33 -44
  16. package/components/ui-icon-button/ui-icon-button.html +12 -2
  17. package/components/ui-listbox/ui-listbox.html +22 -17
  18. package/components/ui-menu/ui-menu.js +5 -3
  19. package/components/ui-popover/ui-popover.js +3 -2
  20. package/components/ui-scroll-area/ui-scroll-area.html +16 -0
  21. package/components/ui-search-shell/ui-search-shell.html +1 -1
  22. package/components/ui-search-shell/ui-search-shell.js +3 -5
  23. package/components/ui-section/ui-section.html +3 -0
  24. package/components/ui-toast/ui-toast.html +2 -1
  25. package/components/ui-toast/ui-toast.js +18 -2
  26. package/components/ui-toast-region/ui-toast-region.js +11 -3
  27. package/components/ui-tooltip/ui-tooltip.html +2 -1
  28. package/components/ui-tooltip/ui-tooltip.js +20 -0
  29. package/dist/index.js +1 -1
  30. package/editor/chunks/extensions.js +2 -0
  31. package/editor/extensions/slash-command.d.ts +2 -0
  32. package/editor/mention-contract.d.ts +2 -0
  33. package/package.json +1 -1
  34. package/styles/ui-card.css +3 -0
  35. package/styles/ui-dialog.css +4 -2
  36. package/styles/ui-editor-mention-menu.css +7 -1
  37. package/styles/ui-editor-slash-menu.css +7 -1
  38. package/styles/ui-editor-table-context-menu.css +29 -5
  39. package/styles/ui-editor-table-toolbar.css +3 -11
  40. package/styles/ui-icon-button.css +9 -2
  41. package/styles/ui-listbox.css +6 -0
  42. package/styles/ui-scroll-area.css +13 -0
  43. package/styles/ui-search-shell.css +1 -1
  44. package/styles/ui-section.css +3 -0
  45. package/vanilla/UiDialog.js +10 -6
  46. package/vanilla/UiIconButton.d.ts +1 -0
  47. package/vanilla/UiIconButton.js +14 -14
  48. package/vanilla/UiListbox.js +10 -9
  49. package/vanilla/UiScrollArea.d.ts +1 -0
  50. package/vanilla/UiScrollArea.js +4 -4
  51. package/vue/UiCard.vue +3 -0
  52. package/vue/UiDialog.vue +12 -6
  53. package/vue/UiEditorMentionMenu.vue +6 -0
  54. package/vue/UiEditorSlashMenu.vue +6 -0
  55. package/vue/UiEditorTableContextMenu.vue +21 -4
  56. package/vue/UiEditorTableToolbar.vue +3 -11
  57. package/vue/UiIconButton.d.ts +9 -0
  58. package/vue/UiIconButton.vue +31 -2
  59. package/vue/UiListbox.vue +20 -15
  60. package/vue/UiScrollArea.d.ts +9 -0
  61. package/vue/UiScrollArea.vue +39 -1
  62. package/vue/UiSearchShell.vue +1 -1
  63. package/vue/UiSection.vue +3 -0
  64. package/vue/UiToast.vue +1 -1
  65. package/vue/UiTooltip.vue +1 -1
  66. package/vue/chunks/UiCard.js +1 -1
  67. package/vue/chunks/UiDialog.js +25 -8
  68. package/vue/chunks/UiEditorMentionMenu.js +1 -1
  69. package/vue/chunks/UiEditorSlashMenu.js +1 -1
  70. package/vue/chunks/UiEditorTableContextMenu.js +1 -1
  71. package/vue/chunks/UiEditorTableToolbar.js +1 -1
  72. package/vue/chunks/UiIconButton.js +26 -1
  73. package/vue/chunks/UiListbox.js +12 -12
  74. package/vue/chunks/UiScrollArea.js +33 -2
  75. package/vue/chunks/UiSearchShell.js +1 -1
  76. package/vue/chunks/UiSection.js +1 -1
  77. package/vue/chunks/UiToast.js +2 -2
  78. package/vue/chunks/UiTooltip.js +2 -2
  79. package/vue/components.css +340 -289
  80. package/vue/editor/index.js +225 -40
@@ -28,6 +28,152 @@ var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
28
28
  function announceOverlayOpen(document, id) {
29
29
  document.dispatchEvent(new CustomEvent("ui-overlay-open", { detail: { id } }));
30
30
  }
31
+ function topLayerOpen(surface) {
32
+ try {
33
+ return surface.matches(":popover-open");
34
+ } catch {
35
+ return !surface.hidden;
36
+ }
37
+ }
38
+ function show(surface) {
39
+ surface.setAttribute("popover", "manual");
40
+ surface.hidden = false;
41
+ if (typeof surface.showPopover === "function" && !topLayerOpen(surface)) try {
42
+ surface.showPopover();
43
+ } catch {}
44
+ }
45
+ function hide(surface) {
46
+ if (typeof surface.hidePopover === "function" && topLayerOpen(surface)) try {
47
+ surface.hidePopover();
48
+ } catch {}
49
+ surface.hidden = true;
50
+ }
51
+ /**
52
+ * One document-scoped scroll/resize listener and one frame for all active floating surfaces.
53
+ * The registry lives on the document so HTML and Vue bundles share the same coordinator.
54
+ * Each subscriber rereads its own live anchor and applies its viewport containment policy.
55
+ */
56
+ function observeOverlayViewport(document, update) {
57
+ const key = Symbol.for("@threadlabs/looma/overlay-viewport/v1");
58
+ const owner = document.defaultView;
59
+ let state = document[key];
60
+ if (!state) {
61
+ state = {
62
+ clients: /* @__PURE__ */ new Set(),
63
+ pending: /* @__PURE__ */ new Map(),
64
+ frame: null,
65
+ abort: null
66
+ };
67
+ Object.defineProperty(document, key, { value: state });
68
+ }
69
+ const schedule = (event) => {
70
+ for (const client of state.clients) state.pending.set(client, event);
71
+ if (state.frame !== null) return;
72
+ state.frame = owner.requestAnimationFrame(() => {
73
+ state.frame = null;
74
+ const pending = Array.from(state.pending);
75
+ state.pending.clear();
76
+ for (const [client, event] of pending) if (state.clients.has(client)) client(event);
77
+ });
78
+ };
79
+ state.clients.add(update);
80
+ if (!state.abort) {
81
+ state.abort = new owner.AbortController();
82
+ const signal = state.abort.signal;
83
+ owner.addEventListener("resize", schedule, {
84
+ passive: true,
85
+ signal
86
+ });
87
+ owner.addEventListener("scroll", schedule, {
88
+ passive: true,
89
+ capture: true,
90
+ signal
91
+ });
92
+ owner.visualViewport?.addEventListener("resize", schedule, {
93
+ passive: true,
94
+ signal
95
+ });
96
+ owner.visualViewport?.addEventListener("scroll", schedule, {
97
+ passive: true,
98
+ signal
99
+ });
100
+ }
101
+ return () => {
102
+ state.clients.delete(update);
103
+ state.pending.delete(update);
104
+ if (state.clients.size) return;
105
+ state.abort?.abort();
106
+ state.abort = null;
107
+ if (state.frame !== null) owner.cancelAnimationFrame(state.frame);
108
+ state.frame = null;
109
+ };
110
+ }
111
+ /** Presents an unanchored or virtual-anchor surface in the top layer, using the shared viewport pass. */
112
+ function createViewportSurface(surface, options = {}) {
113
+ const owner = surface.ownerDocument.defaultView;
114
+ let destroyed = false;
115
+ let stop = null;
116
+ let sizeObserver = null;
117
+ let frame = null;
118
+ const position = () => {
119
+ frame = null;
120
+ if (stop) options.position?.();
121
+ };
122
+ const refresh = () => {
123
+ if (stop && frame === null) frame = owner.requestAnimationFrame(position);
124
+ };
125
+ const chrome = options.preserveChrome ? owner.getComputedStyle(surface) : null;
126
+ const absentBorders = chrome ? [
127
+ "Top",
128
+ "Right",
129
+ "Bottom",
130
+ "Left"
131
+ ].filter((side) => chrome[`border${side}Width`] === "0px") : [];
132
+ surface.setAttribute("popover", "manual");
133
+ if (options.bare) {
134
+ Object.assign(surface.style, {
135
+ position: "fixed",
136
+ margin: "0",
137
+ top: surface.style.top || "auto",
138
+ right: surface.style.right || "auto",
139
+ bottom: surface.style.bottom || "auto",
140
+ left: surface.style.left || "auto"
141
+ });
142
+ if (chrome) for (const side of absentBorders) surface.style[`border${side}Width`] = "0";
143
+ else Object.assign(surface.style, {
144
+ padding: "0",
145
+ border: "0",
146
+ background: "transparent"
147
+ });
148
+ }
149
+ const hideSurface = () => {
150
+ stop?.();
151
+ stop = null;
152
+ sizeObserver?.disconnect();
153
+ sizeObserver = null;
154
+ if (frame !== null) owner.cancelAnimationFrame(frame);
155
+ frame = null;
156
+ hide(surface);
157
+ };
158
+ return {
159
+ show() {
160
+ if (destroyed) return;
161
+ show(surface);
162
+ stop ??= observeOverlayViewport(surface.ownerDocument, position);
163
+ options.position?.();
164
+ if (!sizeObserver && owner.ResizeObserver) {
165
+ sizeObserver = new owner.ResizeObserver(refresh);
166
+ sizeObserver.observe(surface);
167
+ }
168
+ },
169
+ hide: hideSurface,
170
+ refresh,
171
+ destroy() {
172
+ destroyed = true;
173
+ hideSurface();
174
+ }
175
+ };
176
+ }
31
177
  //#endregion
32
178
  //#region ../../node_modules/.pnpm/@tiptap+extension-image@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-image/dist/index.js
33
179
  /**
@@ -2761,8 +2907,10 @@ var LoomaEditor = defineComponent({
2761
2907
  }, { immediate: true });
2762
2908
  const onViewportChange = (event) => {
2763
2909
  if (event?.type === "scroll" && event.target instanceof Node && (tableToolbarShell.value?.contains(event.target) || tableMenuShell.value?.contains(event.target))) return;
2764
- cancelHeldPress();
2910
+ if (!event || !heldPress || event.timeStamp >= heldPress.time) cancelHeldPress();
2765
2911
  const wasMobile = mobile.value;
2912
+ if (slash.active && slash.getRect) slash.rect = slash.getRect();
2913
+ if (mention.active && mention.getRect) mention.rect = mention.getRect();
2766
2914
  updateMobileViewport();
2767
2915
  if (wasMobile !== mobile.value) heldFormattingSelection = null;
2768
2916
  updateTableUi();
@@ -2770,6 +2918,62 @@ var LoomaEditor = defineComponent({
2770
2918
  if (floatingFormattingMode() && editor.value && !formattingSelectionActive(editor.value)) formattingPopup?.hide();
2771
2919
  tableUi.menuOpen = false;
2772
2920
  };
2921
+ let stopViewport;
2922
+ const popupRef = (target, preserveChrome = false) => {
2923
+ let presentation;
2924
+ return (node) => {
2925
+ const element = node instanceof HTMLElement ? node : null;
2926
+ if (target.value === element) return;
2927
+ presentation?.destroy();
2928
+ target.value = element;
2929
+ presentation = element ? createViewportSurface(element, {
2930
+ bare: true,
2931
+ preserveChrome
2932
+ }) : void 0;
2933
+ presentation?.show();
2934
+ };
2935
+ };
2936
+ const tableToolbarRef = popupRef(tableToolbarShell);
2937
+ const codeLanguageRef = popupRef(codeLanguageShell, true);
2938
+ const mobileToolbarRef = popupRef(mobileToolbarShell, true);
2939
+ const tippyPresentations = /* @__PURE__ */ new WeakMap();
2940
+ const floatingTippyOptions = (placement, boundary = null) => ({
2941
+ appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
2942
+ popperOptions: {
2943
+ strategy: "fixed",
2944
+ modifiers: [{
2945
+ name: "eventListeners",
2946
+ enabled: false
2947
+ }, ...boundary ? [{
2948
+ name: "flip",
2949
+ options: { boundary }
2950
+ }, {
2951
+ name: "preventOverflow",
2952
+ options: {
2953
+ boundary,
2954
+ altAxis: true
2955
+ }
2956
+ }] : []]
2957
+ },
2958
+ onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
2959
+ onMount: (instance) => {
2960
+ let presentation = tippyPresentations.get(instance.popper);
2961
+ if (!presentation) {
2962
+ presentation = createViewportSurface(instance.popper, {
2963
+ bare: true,
2964
+ position: () => {
2965
+ instance.popperInstance?.update();
2966
+ }
2967
+ });
2968
+ tippyPresentations.set(instance.popper, presentation);
2969
+ }
2970
+ presentation.show();
2971
+ },
2972
+ onHidden: (instance) => tippyPresentations.get(instance.popper)?.hide(),
2973
+ onDestroy: (instance) => tippyPresentations.get(instance.popper)?.destroy(),
2974
+ duration: 100,
2975
+ placement
2976
+ });
2773
2977
  const onDocumentPointerDown = (event) => {
2774
2978
  const path = typeof event.composedPath === "function" ? event.composedPath() : [];
2775
2979
  const inElement = (element) => Boolean(element && (path.includes(element) || event.target instanceof Node && element.contains(event.target)));
@@ -2788,7 +2992,8 @@ var LoomaEditor = defineComponent({
2788
2992
  heldPress = {
2789
2993
  id: event.pointerId,
2790
2994
  x: event.clientX,
2791
- y: event.clientY
2995
+ y: event.clientY,
2996
+ time: event.timeStamp
2792
2997
  };
2793
2998
  heldPressTimer = setTimeout(() => {
2794
2999
  heldPressTimer = void 0;
@@ -2911,10 +3116,7 @@ var LoomaEditor = defineComponent({
2911
3116
  window.addEventListener("pointermove", onLinkNavigationModifier);
2912
3117
  window.addEventListener("blur", clearLinkNavigationModifier);
2913
3118
  updateMobileViewport();
2914
- window.addEventListener("resize", onViewportChange);
2915
- window.addEventListener("scroll", onViewportChange, true);
2916
- window.visualViewport?.addEventListener("resize", onViewportChange);
2917
- window.visualViewport?.addEventListener("scroll", onViewportChange);
3119
+ stopViewport = observeOverlayViewport(document, onViewportChange);
2918
3120
  document.addEventListener("pointerdown", onDocumentPointerDown, true);
2919
3121
  document.addEventListener("pointerdown", onFormattingPointerDown);
2920
3122
  document.addEventListener("pointermove", onFormattingPointerMove);
@@ -2926,14 +3128,11 @@ var LoomaEditor = defineComponent({
2926
3128
  root.value?.addEventListener("error", onImageError, true);
2927
3129
  });
2928
3130
  onBeforeUnmount(() => {
3131
+ stopViewport?.();
2929
3132
  window.removeEventListener("keydown", onLinkNavigationModifier);
2930
3133
  window.removeEventListener("keyup", onLinkNavigationModifier);
2931
3134
  window.removeEventListener("pointermove", onLinkNavigationModifier);
2932
3135
  window.removeEventListener("blur", clearLinkNavigationModifier);
2933
- window.removeEventListener("resize", onViewportChange);
2934
- window.removeEventListener("scroll", onViewportChange, true);
2935
- window.visualViewport?.removeEventListener("resize", onViewportChange);
2936
- window.visualViewport?.removeEventListener("scroll", onViewportChange);
2937
3136
  cancelHeldPress();
2938
3137
  formattingPopup = null;
2939
3138
  document.removeEventListener("pointerdown", onDocumentPointerDown, true);
@@ -3374,28 +3573,16 @@ var LoomaEditor = defineComponent({
3374
3573
  updateDelay: 0,
3375
3574
  shouldShow: ({ editor: menuEditor }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && !slash.active && !mention.active && (!props.editable && (props.selectionActions.length > 0 || props.rangeActions.length > 0) || menuEditor.isFocused || formattingPopup?.popper.contains(document.activeElement) || linkContextEditing.value || formattingPickerOpen()) && formattingSelectionActive(menuEditor),
3376
3575
  tippyOptions: {
3576
+ ...floatingTippyOptions("top", props.toolbarMode === "contextual" ? root.value : null),
3577
+ maxWidth: "none",
3377
3578
  onCreate: (popup) => {
3378
3579
  formattingPopup = popup;
3379
3580
  },
3380
- onDestroy: () => {
3581
+ onDestroy: (popup) => {
3381
3582
  formattingPopup = null;
3583
+ tippyPresentations.get(popup.popper)?.destroy();
3382
3584
  },
3383
- appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
3384
- onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
3385
- onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0,
3386
- duration: 100,
3387
- maxWidth: "none",
3388
- placement: "top",
3389
- ...props.toolbarMode === "contextual" && root.value ? { popperOptions: { modifiers: [{
3390
- name: "flip",
3391
- options: { boundary: root.value }
3392
- }, {
3393
- name: "preventOverflow",
3394
- options: {
3395
- boundary: root.value,
3396
- altAxis: true
3397
- }
3398
- }] } } : {}
3585
+ onHide: () => instance.isEditable && floatingFormattingMode() && formattingPickerOpen() && formattingSelectionActive(instance) ? false : void 0
3399
3586
  }
3400
3587
  }, { default: () => props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkContextEditing.value || instance.state.selection.empty && instance.isActive("link")) && !linkOpen.value && formattingSelectionActive(instance) ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover") }) : null]),
3401
3588
  h(Stack, {
@@ -3407,17 +3594,15 @@ var LoomaEditor = defineComponent({
3407
3594
  updateDelay: 0,
3408
3595
  shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && !linkOpen.value && !dismissedFormattingSelection?.eq(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkContextEditing.value || linkPopup?.popper.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(menuEditor)),
3409
3596
  tippyOptions: {
3597
+ ...floatingTippyOptions("top"),
3598
+ maxWidth: Math.min(480, window.innerWidth - 24),
3410
3599
  onCreate: (popup) => {
3411
3600
  linkPopup = popup;
3412
3601
  },
3413
- onDestroy: () => {
3602
+ onDestroy: (popup) => {
3414
3603
  linkPopup = null;
3415
- },
3416
- appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
3417
- onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
3418
- duration: 100,
3419
- maxWidth: Math.min(480, window.innerWidth - 24),
3420
- placement: "top"
3604
+ tippyPresentations.get(popup.popper)?.destroy();
3605
+ }
3421
3606
  }
3422
3607
  }, { default: () => props.toolbarMode === "contextual" && floatingFormattingMode() && formattingSelectionActive(instance) ? null : renderLinkContextCard(instance) }) : null]),
3423
3608
  instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
@@ -3452,7 +3637,7 @@ var LoomaEditor = defineComponent({
3452
3637
  }, { default: () => h(ImageControls, { editor: instance }) }) : null,
3453
3638
  instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h(EditorContent, { editor: instance })) : null,
3454
3639
  codeUi.open ? h("div", {
3455
- ref: codeLanguageShell,
3640
+ ref: codeLanguageRef,
3456
3641
  class: "looma-editor__code-language",
3457
3642
  style: codeUi.style,
3458
3643
  onFocusout: () => updateCodeUi()
@@ -3466,7 +3651,7 @@ var LoomaEditor = defineComponent({
3466
3651
  onValueChange: (event) => chooseCodeLanguage(event.detail)
3467
3652
  }, () => codeLanguageOptions.value.map((option) => h("option", { value: option.value }, option.label)))]) : null,
3468
3653
  instance && (props.editable || props.selectionActions.length && !instance.state.selection.empty || activeRangeActions(instance).length > 0) && mobile.value && (editorFocused.value || linkOpen.value || !props.editable) ? h("div", {
3469
- ref: mobileToolbarShell,
3654
+ ref: mobileToolbarRef,
3470
3655
  class: "looma-editor__mobile-toolbar-shell",
3471
3656
  "data-mode": mobileToolbarMode.value,
3472
3657
  style: mobileToolbarStyle.value,
@@ -3646,7 +3831,7 @@ var LoomaEditor = defineComponent({
3646
3831
  }
3647
3832
  }) : null,
3648
3833
  tableUi.toolbarOpen ? h("div", {
3649
- ref: tableToolbarShell,
3834
+ ref: tableToolbarRef,
3650
3835
  class: "looma-editor__table-toolbar-shell",
3651
3836
  style: tableUi.toolbarStyle
3652
3837
  }, [h(EditorTableToolbar, tableProps)]) : null,
@@ -3706,11 +3891,11 @@ var LoomaEditor = defineComponent({
3706
3891
  })]) : null,
3707
3892
  tableUi.menuOpen ? h("div", {
3708
3893
  ref: tableMenuShell,
3709
- class: "looma-editor__table-menu-shell",
3710
- style: tableUi.menuStyle
3894
+ class: "looma-editor__table-menu-shell"
3711
3895
  }, [h(EditorTableContextMenu, {
3712
3896
  ...tableProps,
3713
- scope: tableUi.menuScope
3897
+ scope: tableUi.menuScope,
3898
+ style: tableUi.menuStyle
3714
3899
  })]) : null
3715
3900
  ]);
3716
3901
  };