@threadlabs/looma 0.22.11 → 0.22.13

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.
@@ -9778,13 +9778,14 @@ var CALLOUT_COMMANDS = [
9778
9778
  ];
9779
9779
  /**
9780
9780
  * Builds Looma's default command policy as fresh objects for one editor.
9781
- * The image command delegates asset selection because Looma does not own upload,
9782
- * persistence, or media-library concerns.
9781
+ * Image and link commands delegate their picker because Looma does not own
9782
+ * upload, persistence, or destination search. Link appears only when a picker
9783
+ * callback is supplied, so a headless default inventory has no dead action.
9783
9784
  *
9784
9785
  * @ownership Returned command objects belong to the caller and are recreated
9785
9786
  * per call so one editor cannot mutate another editor's command inventory.
9786
9787
  */
9787
- function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor) {
9788
+ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor) {
9788
9789
  return [
9789
9790
  {
9790
9791
  title: "Text",
@@ -9950,6 +9951,20 @@ function getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor) {
9950
9951
  editor.chain().focus().deleteRange(range).setHorizontalRule().run();
9951
9952
  }
9952
9953
  },
9954
+ ...onOpenLinkEditor ? [{
9955
+ title: "Link",
9956
+ description: "Link to a destination or URL",
9957
+ icon: "link",
9958
+ keywords: [
9959
+ "link",
9960
+ "url",
9961
+ "website"
9962
+ ],
9963
+ command: ({ editor, range }) => {
9964
+ editor.chain().focus().deleteRange(range).run();
9965
+ onOpenLinkEditor();
9966
+ }
9967
+ }] : [],
9953
9968
  {
9954
9969
  title: "Image",
9955
9970
  description: "Upload an image",
@@ -10081,9 +10096,10 @@ var LoomaSlashCommand = Extension.create({
10081
10096
  function createLoomaSlashCommandExtension(options = {}) {
10082
10097
  const onOpenImagePicker = options.onOpenImagePicker;
10083
10098
  const onOpenChipEditor = options.onOpenChipEditor;
10099
+ const onOpenLinkEditor = options.onOpenLinkEditor;
10084
10100
  return LoomaSlashCommand.configure({
10085
10101
  ...options,
10086
- commands: options.commands ?? getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor)
10102
+ commands: options.commands ?? getDefaultSlashCommands(onOpenImagePicker, onOpenChipEditor, onOpenLinkEditor)
10087
10103
  });
10088
10104
  }
10089
10105
  //#endregion
@@ -49,16 +49,18 @@ export interface LoomaSlashCommandOptions {
49
49
  onStateChange?: (state: LoomaSlashMenuSnapshot) => void;
50
50
  onOpenImagePicker?: () => void;
51
51
  onOpenChipEditor?: (editor: Editor, position: number) => void;
52
+ onOpenLinkEditor?: () => void;
52
53
  }
53
54
  /**
54
55
  * Builds Looma's default command policy as fresh objects for one editor.
55
- * The image command delegates asset selection because Looma does not own upload,
56
- * persistence, or media-library concerns.
56
+ * Image and link commands delegate their picker because Looma does not own
57
+ * upload, persistence, or destination search. Link appears only when a picker
58
+ * callback is supplied, so a headless default inventory has no dead action.
57
59
  *
58
60
  * @ownership Returned command objects belong to the caller and are recreated
59
61
  * per call so one editor cannot mutate another editor's command inventory.
60
62
  */
61
- export declare function getDefaultSlashCommands(onOpenImagePicker?: () => void, onOpenChipEditor?: (editor: Editor, position: number) => void): LoomaSlashCommand[];
63
+ export declare function getDefaultSlashCommands(onOpenImagePicker?: () => void, onOpenChipEditor?: (editor: Editor, position: number) => void, onOpenLinkEditor?: () => void): LoomaSlashCommand[];
62
64
  /**
63
65
  * Framework-neutral slash-command extension used by the turnkey editor.
64
66
  * Consumers embedding Looma into their own Tiptap instance can configure the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.22.11",
3
+ "version": "0.22.13",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -9070,18 +9070,30 @@ html[data-ui-input-modality="touch"]
9070
9070
 
9071
9071
  .looma-editor__link-context-actions {
9072
9072
  display: flex;
9073
+ flex-wrap: wrap;
9073
9074
  align-items: center;
9074
9075
  gap: var(--ui-space-1);
9075
9076
  }
9076
9077
 
9077
9078
  .looma-editor__link-context-url {
9078
9079
  min-width: 0;
9079
- max-width: 10rem;
9080
- overflow: hidden;
9080
+ flex: 1 1 100%;
9081
9081
  padding-inline: var(--ui-space-2);
9082
9082
  color: var(--ui-text-secondary);
9083
- text-overflow: ellipsis;
9084
- white-space: nowrap;
9083
+ overflow-wrap: anywhere;
9084
+ font-size: var(--ui-font-size-sm);
9085
+ }
9086
+
9087
+ .looma-editor__link-context-url :is(strong, small) {
9088
+ display: block;
9089
+ }
9090
+
9091
+ .looma-editor__link-context-url strong {
9092
+ color: var(--ui-text-primary);
9093
+ }
9094
+
9095
+ .looma-editor__link-context-kind {
9096
+ font-size: var(--ui-font-size-xs);
9085
9097
  }
9086
9098
 
9087
9099
  .looma-editor__link-context-actions :is(button, a) {
@@ -9198,6 +9210,57 @@ html[data-ui-input-modality="touch"]
9198
9210
  align-items: center;
9199
9211
  }
9200
9212
 
9213
+ .looma-editor__link-modes {
9214
+ display: flex;
9215
+ gap: var(--ui-space-1);
9216
+ }
9217
+
9218
+ .looma-editor__link-modes button,
9219
+ .looma-editor__link-results button {
9220
+ min-height: var(--ui-control-min-block-size);
9221
+ padding: var(--ui-space-2);
9222
+ border: 1px solid var(--ui-control-border);
9223
+ border-radius: var(--ui-field-radius, var(--ui-radius-md));
9224
+ background: var(--ui-control-surface);
9225
+ color: var(--ui-text-primary);
9226
+ font: inherit;
9227
+ text-align: left;
9228
+ cursor: pointer;
9229
+ }
9230
+
9231
+ .looma-editor__link-modes button[aria-pressed="true"] {
9232
+ border-color: var(--ui-accent);
9233
+ color: var(--ui-accent);
9234
+ }
9235
+
9236
+ .looma-editor__link-results {
9237
+ display: flex;
9238
+ flex-direction: column;
9239
+ gap: var(--ui-space-1);
9240
+ max-height: 15rem;
9241
+ overflow-y: auto;
9242
+ }
9243
+
9244
+ .looma-editor__link-results p {
9245
+ margin: 0;
9246
+ color: var(--ui-text-secondary);
9247
+ font-size: var(--ui-font-size-sm);
9248
+ }
9249
+
9250
+ .looma-editor__link-results button {
9251
+ display: flex;
9252
+ flex-direction: column;
9253
+ width: 100%;
9254
+ }
9255
+
9256
+ .looma-editor__link-results button:is(:hover, :focus-visible) {
9257
+ background: var(--ui-surface-subtle);
9258
+ }
9259
+
9260
+ .looma-editor__link-results button small {
9261
+ color: var(--ui-text-secondary);
9262
+ }
9263
+
9201
9264
  .looma-editor__link-error {
9202
9265
  margin: 0;
9203
9266
  color: var(--ui-field-danger, var(--ui-danger));
@@ -26,6 +26,25 @@ export type LoomaImageUploader = (file: File) => Promise<string | LoomaImageUplo
26
26
  * bubble for selections); it does not alter document commands or stored content.
27
27
  */
28
28
  export type LoomaEditorToolbarMode = "bubble" | "sticky" | "popover";
29
+ /** A destination supplied by the host application, such as a page or record. */
30
+ export interface LoomaLinkTarget {
31
+ id: string;
32
+ label: string;
33
+ detail?: string;
34
+ href: string;
35
+ }
36
+ /**
37
+ * Finds host records for a link without teaching the editor how they are stored.
38
+ * @ownership The host checks access and returns durable, openable href values.
39
+ * @failure A rejection leaves the existing document untouched and shows a retryable search error.
40
+ */
41
+ export type LoomaLinkSearch = (query: string) => Promise<LoomaLinkTarget[]>;
42
+ /**
43
+ * Describes a saved host link so a renamed record can show its current label.
44
+ * @ownership The host resolves identity and access; Looma keeps the saved href as is.
45
+ * @failure A rejection leaves the original href visible and editable.
46
+ */
47
+ export type LoomaLinkResolve = (href: string) => Promise<LoomaLinkTarget | null>;
29
48
  /**
30
49
  * Turnkey Vue editor that composes Looma's framework-neutral Tiptap extensions
31
50
  * with declarative menus, image delivery, and table interaction chrome.
@@ -88,6 +107,19 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
88
107
  type: NumberConstructor;
89
108
  default: number;
90
109
  };
110
+ /** Host-owned search and identity lookup for links to application records. */
111
+ linkSearch: {
112
+ type: PropType<LoomaLinkSearch | undefined>;
113
+ default: undefined;
114
+ };
115
+ linkResolve: {
116
+ type: PropType<LoomaLinkResolve | undefined>;
117
+ default: undefined;
118
+ };
119
+ linkTargetLabel: {
120
+ type: StringConstructor;
121
+ default: string;
122
+ };
91
123
  uploadImage: {
92
124
  type: PropType<LoomaImageUploader | undefined>;
93
125
  default: undefined;
@@ -181,6 +213,19 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
181
213
  type: NumberConstructor;
182
214
  default: number;
183
215
  };
216
+ /** Host-owned search and identity lookup for links to application records. */
217
+ linkSearch: {
218
+ type: PropType<LoomaLinkSearch | undefined>;
219
+ default: undefined;
220
+ };
221
+ linkResolve: {
222
+ type: PropType<LoomaLinkResolve | undefined>;
223
+ default: undefined;
224
+ };
225
+ linkTargetLabel: {
226
+ type: StringConstructor;
227
+ default: string;
228
+ };
184
229
  uploadImage: {
185
230
  type: PropType<LoomaImageUploader | undefined>;
186
231
  default: undefined;
@@ -233,6 +278,9 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
233
278
  mentionItems: LoomaMentionItem[];
234
279
  mentionProvider: LoomaMentionProvider | undefined;
235
280
  mentionLimit: number;
281
+ linkSearch: LoomaLinkSearch | undefined;
282
+ linkResolve: LoomaLinkResolve | undefined;
283
+ linkTargetLabel: string;
236
284
  uploadImage: LoomaImageUploader | undefined;
237
285
  resolveImageAttributes: LoomaImageAttributeResolver | undefined;
238
286
  toolbarMode: LoomaEditorToolbarMode;
@@ -1,4 +1,4 @@
1
1
  export * from "@threadlabs/looma/editor";
2
2
  export * from "./primitives.js";
3
- export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
3
+ export { LoomaEditor, type LoomaEditorToolbarMode, 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";
@@ -562,6 +562,19 @@ var LoomaEditor = defineComponent({
562
562
  type: Number,
563
563
  default: DEFAULT_MENTION_RESULT_LIMIT
564
564
  },
565
+ /** Host-owned search and identity lookup for links to application records. */
566
+ linkSearch: {
567
+ type: Function,
568
+ default: void 0
569
+ },
570
+ linkResolve: {
571
+ type: Function,
572
+ default: void 0
573
+ },
574
+ linkTargetLabel: {
575
+ type: String,
576
+ default: "Item"
577
+ },
565
578
  uploadImage: {
566
579
  type: Function,
567
580
  default: void 0
@@ -613,12 +626,22 @@ var LoomaEditor = defineComponent({
613
626
  let blockActionTarget = null;
614
627
  const linkAnchorId = `looma-editor-link-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
615
628
  const linkUrlInput = ref(null);
629
+ const linkFormRoot = ref(null);
616
630
  const linkOpen = ref(false);
617
631
  const linkContextEditing = ref(false);
618
632
  const linkHref = ref("");
619
633
  const linkText = ref("");
620
634
  const linkNewTab = ref(true);
621
635
  const linkError = ref("");
636
+ const linkMode = ref("url");
637
+ const linkQuery = ref("");
638
+ const linkResults = ref([]);
639
+ const linkSearching = ref(false);
640
+ const linkSelectedTarget = ref(null);
641
+ const linkContextTarget = ref(null);
642
+ const linkTargetCache = /* @__PURE__ */ new Map();
643
+ let linkSearchSequence = 0;
644
+ let linkResolveSequence = 0;
622
645
  const chipAnchorId = `looma-editor-chip-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
623
646
  const chipInput = ref(null);
624
647
  const chipOpen = ref(false);
@@ -746,6 +769,7 @@ var LoomaEditor = defineComponent({
746
769
  const slashExtension = createLoomaSlashCommandExtension({
747
770
  onOpenImagePicker: () => fileInput.value?.click(),
748
771
  onOpenChipEditor: openChipEditor,
772
+ onOpenLinkEditor: () => openLinkEditor(),
749
773
  onStateChange: (state) => {
750
774
  Object.assign(slash, state);
751
775
  }
@@ -862,6 +886,63 @@ var LoomaEditor = defineComponent({
862
886
  emit("update", value);
863
887
  }
864
888
  });
889
+ watch(() => {
890
+ editorStateVersion.value;
891
+ return editor.value?.isActive("link") ? editor.value.getAttributes("link").href : null;
892
+ }, async (href) => {
893
+ const sequence = ++linkResolveSequence;
894
+ linkContextTarget.value = null;
895
+ if (!href || !props.linkResolve) return;
896
+ if (linkTargetCache.has(href)) {
897
+ linkContextTarget.value = linkTargetCache.get(href) ?? null;
898
+ return;
899
+ }
900
+ try {
901
+ const target = await props.linkResolve(href);
902
+ linkTargetCache.set(href, target);
903
+ if (sequence === linkResolveSequence) linkContextTarget.value = target;
904
+ } catch {}
905
+ });
906
+ watch([
907
+ linkQuery,
908
+ linkMode,
909
+ linkOpen,
910
+ linkContextEditing
911
+ ], async ([query, mode, open, contextEditing]) => {
912
+ const sequence = ++linkSearchSequence;
913
+ linkResults.value = [];
914
+ linkSearching.value = false;
915
+ if (!props.linkSearch || mode !== "target" || !open && !contextEditing || query.trim().length < 2 || linkSelectedTarget.value && query === linkSelectedTarget.value.label) return;
916
+ linkSearching.value = true;
917
+ try {
918
+ const results = await props.linkSearch(query.trim());
919
+ if (sequence === linkSearchSequence) linkResults.value = results.filter((target) => validLinkHref(target.href));
920
+ } catch {
921
+ if (sequence === linkSearchSequence) linkError.value = "Search is unavailable. Try again.";
922
+ } finally {
923
+ if (sequence === linkSearchSequence) linkSearching.value = false;
924
+ }
925
+ });
926
+ const selectLinkTarget = (target) => {
927
+ linkSelectedTarget.value = target;
928
+ linkTargetCache.set(target.href, target);
929
+ linkHref.value = target.href;
930
+ linkQuery.value = target.label;
931
+ linkResults.value = [];
932
+ linkNewTab.value = false;
933
+ linkError.value = "";
934
+ };
935
+ const moveLinkResultFocus = (event, offset) => {
936
+ const buttons = [...linkFormRoot.value?.querySelectorAll("[data-link-result]") ?? []];
937
+ const next = buttons[buttons.indexOf(event.target) + offset];
938
+ if (next) {
939
+ event.preventDefault();
940
+ next.focus();
941
+ } else if (offset < 0) {
942
+ event.preventDefault();
943
+ linkFormRoot.value?.querySelector("[data-link-search]")?.focus();
944
+ }
945
+ };
865
946
  const openLinkEditor = (context = false) => {
866
947
  const instance = editor.value;
867
948
  if (!instance || !props.editable) return;
@@ -883,17 +964,36 @@ var LoomaEditor = defineComponent({
883
964
  linkNewTab.value = attrs.target !== "_self";
884
965
  linkText.value = empty && !existing ? "" : instance.state.doc.textBetween(from, to);
885
966
  linkError.value = "";
967
+ linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
968
+ linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
969
+ linkQuery.value = linkSelectedTarget.value?.label ?? "";
886
970
  linkContextEditing.value = context;
887
971
  linkOpen.value = !context;
888
- nextTick(() => linkUrlInput.value?.focus());
972
+ if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
973
+ const href = linkHref.value;
974
+ props.linkResolve(href).then((target) => {
975
+ linkTargetCache.set(href, target);
976
+ if (linkHref.value === href && target) {
977
+ linkSelectedTarget.value = target;
978
+ linkMode.value = "target";
979
+ linkQuery.value = target.label;
980
+ }
981
+ }).catch(() => {});
982
+ }
983
+ nextTick(() => (linkMode.value === "url" ? linkUrlInput.value : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
889
984
  };
890
985
  const saveLink = () => {
891
986
  const instance = editor.value;
892
987
  const selection = linkSelection;
893
988
  if (!instance || !selection) return;
894
- const href = validLinkHref(linkHref.value);
989
+ const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
895
990
  if (!href) {
896
- linkError.value = "Enter an http, https, mailto, tel, or relative URL.";
991
+ linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
992
+ (linkMode.value === "url" ? linkUrlInput.value : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
993
+ return;
994
+ }
995
+ if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
996
+ linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
897
997
  linkUrlInput.value?.focus();
898
998
  return;
899
999
  }
@@ -945,6 +1045,7 @@ var LoomaEditor = defineComponent({
945
1045
  linkContextEditing.value = false;
946
1046
  };
947
1047
  const renderLinkForm = () => h("form", {
1048
+ ref: linkFormRoot,
948
1049
  class: "looma-editor__link-form",
949
1050
  "aria-label": "Edit link",
950
1051
  onSubmit: (event) => {
@@ -959,7 +1060,47 @@ var LoomaEditor = defineComponent({
959
1060
  linkError.value = "";
960
1061
  }
961
1062
  })]) : null,
962
- h("label", [h("span", "URL"), h("input", {
1063
+ props.linkSearch ? h("div", {
1064
+ class: "looma-editor__link-modes",
1065
+ role: "group",
1066
+ "aria-label": "Link destination"
1067
+ }, [h("button", {
1068
+ type: "button",
1069
+ "aria-pressed": linkMode.value === "target",
1070
+ onClick: () => {
1071
+ linkMode.value = "target";
1072
+ linkHref.value = "";
1073
+ linkSelectedTarget.value = null;
1074
+ linkQuery.value = "";
1075
+ linkError.value = "";
1076
+ nextTick(() => linkFormRoot.value?.querySelector("[data-link-search]")?.focus());
1077
+ }
1078
+ }, props.linkTargetLabel), h("button", {
1079
+ type: "button",
1080
+ "aria-pressed": linkMode.value === "url",
1081
+ onClick: () => {
1082
+ linkMode.value = "url";
1083
+ linkHref.value = "";
1084
+ linkSelectedTarget.value = null;
1085
+ linkError.value = "";
1086
+ nextTick(() => linkUrlInput.value?.focus());
1087
+ }
1088
+ }, "URL")]) : null,
1089
+ linkMode.value === "target" && props.linkSearch ? h("label", [h("span", `Find a ${props.linkTargetLabel.toLowerCase()}`), h("input", {
1090
+ "data-link-search": "",
1091
+ type: "search",
1092
+ value: linkQuery.value,
1093
+ autocomplete: "off",
1094
+ "aria-invalid": linkError.value ? "true" : void 0,
1095
+ onInput: (event) => {
1096
+ linkQuery.value = event.target.value;
1097
+ linkSelectedTarget.value = null;
1098
+ linkError.value = "";
1099
+ },
1100
+ onKeydown: (event) => {
1101
+ if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
1102
+ }
1103
+ })]) : h("label", [h("span", "URL"), h("input", {
963
1104
  ref: linkUrlInput,
964
1105
  type: "text",
965
1106
  inputmode: "url",
@@ -970,7 +1111,25 @@ var LoomaEditor = defineComponent({
970
1111
  linkError.value = "";
971
1112
  }
972
1113
  })]),
973
- h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
1114
+ linkMode.value === "target" && props.linkSearch ? h("div", {
1115
+ class: "looma-editor__link-results",
1116
+ "aria-label": "Destination search results"
1117
+ }, [
1118
+ linkSearching.value ? h("p", { role: "status" }, "Searching…") : null,
1119
+ linkSelectedTarget.value ? h("p", { class: "looma-editor__link-selected" }, `Selected: ${linkSelectedTarget.value.label}${linkSelectedTarget.value.detail ? ` · ${linkSelectedTarget.value.detail}` : ""}`) : null,
1120
+ !linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length < 2 ? h("p", "Type at least two letters to search.") : null,
1121
+ !linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length >= 2 && !linkResults.value.length && !linkError.value ? h("p", "No results found.") : null,
1122
+ ...linkResults.value.map((target) => h("button", {
1123
+ key: target.id,
1124
+ type: "button",
1125
+ "data-link-result": "",
1126
+ onClick: () => selectLinkTarget(target),
1127
+ onKeydown: (event) => {
1128
+ if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
1129
+ if (event.key === "ArrowUp") moveLinkResultFocus(event, -1);
1130
+ }
1131
+ }, [h("strong", target.label), target.detail ? h("small", target.detail) : null]))
1132
+ ]) : h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
974
1133
  type: "checkbox",
975
1134
  checked: linkNewTab.value,
976
1135
  onChange: (event) => {
@@ -981,7 +1140,7 @@ var LoomaEditor = defineComponent({
981
1140
  class: "looma-editor__link-error",
982
1141
  role: "alert"
983
1142
  }, linkError.value) : null,
984
- validLinkHref(linkHref.value) ? h("a", {
1143
+ linkMode.value === "url" && validLinkHref(linkHref.value) ? h("a", {
985
1144
  class: "looma-editor__link-preview",
986
1145
  href: validLinkHref(linkHref.value),
987
1146
  target: "_blank",
@@ -1005,7 +1164,7 @@ var LoomaEditor = defineComponent({
1005
1164
  h("span", {
1006
1165
  class: "looma-editor__link-context-url",
1007
1166
  title: href
1008
- }, href),
1167
+ }, [h("small", { class: "looma-editor__link-context-kind" }, linkContextTarget.value ? props.linkTargetLabel : "External URL"), ...linkContextTarget.value ? [h("strong", linkContextTarget.value.label), linkContextTarget.value.detail ? h("small", linkContextTarget.value.detail) : null] : [href]]),
1009
1168
  h("button", {
1010
1169
  type: "button",
1011
1170
  onPointerdown: (event) => event.preventDefault(),