@threadlabs/looma 0.22.13 → 0.22.14

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 (70) hide show
  1. package/components/shared/icons.js +4 -0
  2. package/components/ui-card/ui-card.html +21 -2
  3. package/components/ui-icon/ui-icon.html +4 -0
  4. package/components/ui-input/ui-input.html +1 -0
  5. package/components/ui-input-group/ui-input-group.html +11 -0
  6. package/components/ui-popover/ui-popover.html +15 -0
  7. package/components/ui-scroll-area/ui-scroll-area.html +12 -0
  8. package/components/ui-search-result-row/ui-search-result-row.html +11 -0
  9. package/components/ui-stack/ui-stack.html +6 -3
  10. package/components/ui-text/ui-text.html +12 -0
  11. package/dist/index.js +1 -1
  12. package/editor/chunks/extensions.js +55 -2
  13. package/editor/extensions/index.d.ts +1 -0
  14. package/editor/extensions/index.js +2 -2
  15. package/editor/extensions/preset.d.ts +2 -0
  16. package/editor/extensions/relative-links.d.ts +8 -0
  17. package/editor/icons.d.ts +4 -0
  18. package/editor/index.js +7 -3
  19. package/package.json +1 -1
  20. package/styles/ui-card.css +17 -0
  21. package/styles/ui-input-group.css +9 -0
  22. package/styles/ui-input.css +1 -0
  23. package/styles/ui-popover.css +13 -0
  24. package/styles/ui-scroll-area.css +10 -0
  25. package/styles/ui-search-result-row.css +9 -0
  26. package/styles/ui-stack.css +3 -0
  27. package/styles/ui-text.css +8 -0
  28. package/vanilla/UiCard.d.ts +2 -1
  29. package/vanilla/UiCard.js +1 -1
  30. package/vanilla/UiIcon.js +1 -1
  31. package/vanilla/UiInputGroup.d.ts +1 -0
  32. package/vanilla/UiInputGroup.js +1 -1
  33. package/vanilla/UiPopover.d.ts +1 -0
  34. package/vanilla/UiPopover.js +1 -1
  35. package/vanilla/UiScrollArea.d.ts +1 -0
  36. package/vanilla/UiScrollArea.js +1 -1
  37. package/vanilla/UiSearchResultRow.d.ts +1 -0
  38. package/vanilla/UiSearchResultRow.js +1 -1
  39. package/vanilla/UiStack.d.ts +1 -1
  40. package/vanilla/UiStack.js +1 -1
  41. package/vanilla/UiText.d.ts +2 -0
  42. package/vanilla/UiText.js +1 -1
  43. package/vue/UiCard.d.ts +12 -3
  44. package/vue/UiCard.vue +49 -5
  45. package/vue/UiIcon.vue +23 -0
  46. package/vue/UiInput.vue +1 -0
  47. package/vue/UiInputGroup.d.ts +9 -0
  48. package/vue/UiInputGroup.vue +33 -2
  49. package/vue/UiPopover.d.ts +9 -0
  50. package/vue/UiPopover.vue +43 -2
  51. package/vue/UiScrollArea.d.ts +9 -0
  52. package/vue/UiScrollArea.vue +35 -2
  53. package/vue/UiSearchResultRow.d.ts +9 -0
  54. package/vue/UiSearchResultRow.vue +38 -1
  55. package/vue/UiStack.d.ts +3 -3
  56. package/vue/UiStack.vue +10 -4
  57. package/vue/UiText.d.ts +18 -0
  58. package/vue/UiText.vue +70 -0
  59. package/vue/chunks/UiCard.js +39 -3
  60. package/vue/chunks/UiIcon.js +69 -1
  61. package/vue/chunks/UiInput.js +1 -1
  62. package/vue/chunks/UiInputGroup.js +59 -22
  63. package/vue/chunks/UiPopover.js +37 -5
  64. package/vue/chunks/UiScrollArea.js +66 -23
  65. package/vue/chunks/UiSearchResultRow.js +28 -2
  66. package/vue/chunks/UiStack.js +3 -2
  67. package/vue/chunks/UiText.js +92 -4
  68. package/vue/components.css +174 -301
  69. package/vue/editor/LoomaEditor.d.ts +11 -0
  70. package/vue/editor/index.js +261 -145
@@ -107,6 +107,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
107
107
  type: NumberConstructor;
108
108
  default: number;
109
109
  };
110
+ /** Site origin for relative links. Applied to newly inserted links, including paste over selected text. */
111
+ linkBaseUrl: {
112
+ type: StringConstructor;
113
+ default: string;
114
+ };
110
115
  /** Host-owned search and identity lookup for links to application records. */
111
116
  linkSearch: {
112
117
  type: PropType<LoomaLinkSearch | undefined>;
@@ -213,6 +218,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
213
218
  type: NumberConstructor;
214
219
  default: number;
215
220
  };
221
+ /** Site origin for relative links. Applied to newly inserted links, including paste over selected text. */
222
+ linkBaseUrl: {
223
+ type: StringConstructor;
224
+ default: string;
225
+ };
216
226
  /** Host-owned search and identity lookup for links to application records. */
217
227
  linkSearch: {
218
228
  type: PropType<LoomaLinkSearch | undefined>;
@@ -270,6 +280,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
270
280
  }>, {
271
281
  label: string;
272
282
  placeholder: string;
283
+ linkBaseUrl: string;
273
284
  disableHighlight: boolean;
274
285
  codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
275
286
  modelValue: JSONContent;
@@ -1,6 +1,6 @@
1
1
  import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
2
- import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
3
- import { Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Menu, MenuItem, Popover, Tooltip } from "@threadlabs/looma/vue";
2
+ import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar, siteRelativeHref } from "@threadlabs/looma/editor";
3
+ import { Button, Card, Checkbox, Cluster, Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, IconButton, Input, InputGroup, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
4
4
  import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
5
5
  import { closeHistory } from "@tiptap/pm/history";
6
6
  import { createLowlight } from "lowlight";
@@ -501,6 +501,8 @@ function loomaIcon(name) {
501
501
  "aria-hidden": "true",
502
502
  focusable: "false",
503
503
  viewBox: "0 0 24 24",
504
+ width: 16,
505
+ height: 16,
504
506
  fill: "none",
505
507
  stroke: "currentColor",
506
508
  "stroke-width": 2,
@@ -562,6 +564,11 @@ var LoomaEditor = defineComponent({
562
564
  type: Number,
563
565
  default: DEFAULT_MENTION_RESULT_LIMIT
564
566
  },
567
+ /** Site origin for relative links. Applied to newly inserted links, including paste over selected text. */
568
+ linkBaseUrl: {
569
+ type: String,
570
+ default: ""
571
+ },
565
572
  /** Host-owned search and identity lookup for links to application records. */
566
573
  linkSearch: {
567
574
  type: Function,
@@ -625,7 +632,8 @@ var LoomaEditor = defineComponent({
625
632
  const blockActionAnchorId = `ui-block-tools-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
626
633
  let blockActionTarget = null;
627
634
  const linkAnchorId = `looma-editor-link-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
628
- const linkUrlInput = ref(null);
635
+ const linkPopoverAnchorId = ref(linkAnchorId);
636
+ const linkDestinationInput = () => linkFormRoot.value?.querySelector("[data-link-destination]");
629
637
  const linkFormRoot = ref(null);
630
638
  const linkOpen = ref(false);
631
639
  const linkContextEditing = ref(false);
@@ -850,6 +858,7 @@ var LoomaEditor = defineComponent({
850
858
  extensions: [
851
859
  ...getDefaultEditorExtensions({
852
860
  placeholder: props.placeholder,
861
+ linkBaseUrl: props.linkBaseUrl,
853
862
  mention: mentionExtension ?? false,
854
863
  disableHighlight: props.disableHighlight,
855
864
  codeLanguages: props.codeLanguages ?? void 0,
@@ -966,7 +975,7 @@ var LoomaEditor = defineComponent({
966
975
  linkError.value = "";
967
976
  linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
968
977
  linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
969
- linkQuery.value = linkSelectedTarget.value?.label ?? "";
978
+ linkQuery.value = linkSelectedTarget.value?.label ?? linkHref.value;
970
979
  linkContextEditing.value = context;
971
980
  linkOpen.value = !context;
972
981
  if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
@@ -980,7 +989,7 @@ var LoomaEditor = defineComponent({
980
989
  }
981
990
  }).catch(() => {});
982
991
  }
983
- nextTick(() => (linkMode.value === "url" ? linkUrlInput.value : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
992
+ nextTick(() => (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus());
984
993
  };
985
994
  const saveLink = () => {
986
995
  const instance = editor.value;
@@ -989,16 +998,16 @@ var LoomaEditor = defineComponent({
989
998
  const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
990
999
  if (!href) {
991
1000
  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();
1001
+ (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
993
1002
  return;
994
1003
  }
995
1004
  if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
996
1005
  linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
997
- linkUrlInput.value?.focus();
1006
+ linkDestinationInput()?.focus();
998
1007
  return;
999
1008
  }
1000
1009
  const attrs = {
1001
- href,
1010
+ href: props.linkBaseUrl ? siteRelativeHref(href, props.linkBaseUrl) : href,
1002
1011
  target: linkNewTab.value ? "_blank" : "_self",
1003
1012
  rel: linkNewTab.value ? "noopener noreferrer" : null
1004
1013
  };
@@ -1022,7 +1031,7 @@ var LoomaEditor = defineComponent({
1022
1031
  }).run();
1023
1032
  } else {
1024
1033
  if (selection.existing && selection.from === selection.to) chain.extendMarkRange("link");
1025
- chain.setLink(attrs).run();
1034
+ chain.setLink(attrs).setTextSelection(selection.to).run();
1026
1035
  }
1027
1036
  linkOpen.value = false;
1028
1037
  linkContextEditing.value = false;
@@ -1034,7 +1043,7 @@ var LoomaEditor = defineComponent({
1034
1043
  instance.chain().focus().setTextSelection({
1035
1044
  from: selection.from,
1036
1045
  to: selection.to
1037
- }).extendMarkRange("link").unsetLink().run();
1046
+ }).extendMarkRange("link").unsetLink().setTextSelection(selection.to).run();
1038
1047
  linkOpen.value = false;
1039
1048
  linkContextEditing.value = false;
1040
1049
  };
@@ -1044,6 +1053,114 @@ var LoomaEditor = defineComponent({
1044
1053
  instance.chain().focus().extendMarkRange("link").unsetLink().run();
1045
1054
  linkContextEditing.value = false;
1046
1055
  };
1056
+ const cancelLink = () => {
1057
+ linkOpen.value = false;
1058
+ linkContextEditing.value = false;
1059
+ const selection = linkSelection;
1060
+ if (selection) editor.value?.chain().focus().setTextSelection(selection.to).run();
1061
+ };
1062
+ const renderLinkDestination = (mode) => [h(FormField, { invalid: Boolean(linkError.value) }, {
1063
+ label: () => h("label", props.linkSearch ? "Link destination" : "URL"),
1064
+ default: () => {
1065
+ const input = h(Input, {
1066
+ "data-link-destination": "",
1067
+ ...props.linkSearch ? {
1068
+ "data-link-search": "",
1069
+ type: "text",
1070
+ role: "searchbox",
1071
+ autocomplete: "off",
1072
+ placeholder: `Search ${props.linkTargetLabel.toLowerCase()}s or paste a URL…`
1073
+ } : {
1074
+ type: "text",
1075
+ inputmode: "url",
1076
+ placeholder: "https://example.com"
1077
+ },
1078
+ size: props.linkSearch ? "md" : "sm",
1079
+ value: props.linkSearch ? linkQuery.value : linkHref.value,
1080
+ onInput: (event) => {
1081
+ const value = event.target.value;
1082
+ if (props.linkSearch) {
1083
+ linkQuery.value = value;
1084
+ linkMode.value = /^(https?:\/\/|mailto:|tel:)/i.test(value.trim()) || linkSelection?.existing && /^(\/|\.\.?\/|#|\?)/.test(value.trim()) ? "url" : "target";
1085
+ linkSelectedTarget.value = null;
1086
+ }
1087
+ linkHref.value = value;
1088
+ linkError.value = "";
1089
+ },
1090
+ onKeydown: (event) => {
1091
+ if (mode === "target" && event.key === "ArrowDown") moveLinkResultFocus(event, 1);
1092
+ }
1093
+ });
1094
+ return props.linkSearch ? h(InputGroup, { prefixKind: "icon" }, {
1095
+ prefix: () => loomaIcon(mode === "target" ? "search" : "link"),
1096
+ default: () => input,
1097
+ action: () => linkQuery.value ? h(IconButton, {
1098
+ label: "Clear search",
1099
+ variant: "ghost",
1100
+ size: "sm",
1101
+ onClick: () => {
1102
+ linkQuery.value = "";
1103
+ linkHref.value = "";
1104
+ linkMode.value = "target";
1105
+ linkSelectedTarget.value = null;
1106
+ linkError.value = "";
1107
+ nextTick(() => linkFormRoot.value?.querySelector("[data-link-search]")?.focus());
1108
+ }
1109
+ }, () => loomaIcon("x")) : null
1110
+ }) : input;
1111
+ }
1112
+ }), mode === "target" ? h(ScrollArea, {
1113
+ size: "sm",
1114
+ "aria-label": "Destination search results"
1115
+ }, () => [
1116
+ linkSearching.value ? h(Text, {
1117
+ size: "sm",
1118
+ tone: "secondary",
1119
+ role: "status"
1120
+ }, () => "Searching…") : null,
1121
+ !linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length < 2 ? h(Text, {
1122
+ size: "sm",
1123
+ tone: "secondary"
1124
+ }, () => "Type at least two letters to search.") : null,
1125
+ !linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length >= 2 && !linkResults.value.length && !linkError.value ? h(Text, {
1126
+ size: "sm",
1127
+ tone: "secondary"
1128
+ }, () => "No results found.") : null,
1129
+ ...(linkSelectedTarget.value ? [linkSelectedTarget.value] : linkResults.value).map((target) => h(SearchResultRow, {
1130
+ key: target.id,
1131
+ density: "compact",
1132
+ selected: target.id === linkSelectedTarget.value?.id,
1133
+ "data-link-result": "",
1134
+ onClick: () => selectLinkTarget(target),
1135
+ onKeydown: (event) => {
1136
+ if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
1137
+ if (event.key === "ArrowUp") moveLinkResultFocus(event, -1);
1138
+ }
1139
+ }, {
1140
+ leading: () => loomaIcon("file-text"),
1141
+ title: () => h(Text, {
1142
+ as: "strong",
1143
+ size: "sm",
1144
+ weight: "medium",
1145
+ truncate: true,
1146
+ title: target.label
1147
+ }, () => target.label),
1148
+ meta: () => target.detail ? h(Text, {
1149
+ as: "span",
1150
+ size: "xs",
1151
+ tone: "secondary",
1152
+ truncate: true,
1153
+ title: target.detail
1154
+ }, () => target.detail) : null,
1155
+ trailing: () => target.id === linkSelectedTarget.value?.id ? loomaIcon("check") : null
1156
+ }))
1157
+ ]) : h(Checkbox, {
1158
+ size: "sm",
1159
+ checked: linkNewTab.value,
1160
+ onChange: (event) => {
1161
+ linkNewTab.value = event.detail.checked;
1162
+ }
1163
+ }, () => "Open in new tab")];
1047
1164
  const renderLinkForm = () => h("form", {
1048
1165
  ref: linkFormRoot,
1049
1166
  class: "looma-editor__link-form",
@@ -1052,134 +1169,120 @@ var LoomaEditor = defineComponent({
1052
1169
  event.preventDefault();
1053
1170
  saveLink();
1054
1171
  }
1055
- }, [
1056
- linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h("label", [h("span", "Text"), h("input", {
1057
- value: linkText.value,
1058
- onInput: (event) => {
1059
- linkText.value = event.target.value;
1060
- linkError.value = "";
1061
- }
1062
- })]) : null,
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", {
1104
- ref: linkUrlInput,
1105
- type: "text",
1106
- inputmode: "url",
1107
- value: linkHref.value,
1108
- "aria-invalid": linkError.value ? "true" : void 0,
1109
- onInput: (event) => {
1110
- linkHref.value = event.target.value;
1111
- linkError.value = "";
1112
- }
1113
- })]),
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);
1172
+ }, [h(Stack, { gap: "s" }, () => [
1173
+ h(Text, {
1174
+ as: "h3",
1175
+ size: "sm",
1176
+ weight: "semibold"
1177
+ }, () => linkSelection?.existing ? "Edit link" : "Add link"),
1178
+ h(Stack, { gap: "s" }, () => renderLinkDestination(linkMode.value)),
1179
+ linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h(FormField, {}, {
1180
+ label: () => h("label", "Text"),
1181
+ default: () => h(Input, {
1182
+ size: "sm",
1183
+ value: linkText.value,
1184
+ onInput: (event) => {
1185
+ linkText.value = event.target.value;
1186
+ linkError.value = "";
1130
1187
  }
1131
- }, [h("strong", target.label), target.detail ? h("small", target.detail) : null]))
1132
- ]) : h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
1133
- type: "checkbox",
1134
- checked: linkNewTab.value,
1135
- onChange: (event) => {
1136
- linkNewTab.value = event.target.checked;
1137
- }
1138
- }), h("span", "Open in new tab")]),
1139
- linkError.value ? h("p", {
1140
- class: "looma-editor__link-error",
1188
+ })
1189
+ }) : null,
1190
+ linkError.value ? h(Text, {
1191
+ size: "sm",
1192
+ tone: "danger",
1141
1193
  role: "alert"
1142
- }, linkError.value) : null,
1143
- linkMode.value === "url" && validLinkHref(linkHref.value) ? h("a", {
1144
- class: "looma-editor__link-preview",
1194
+ }, () => linkError.value) : null,
1195
+ h(Separator),
1196
+ h(Cluster, {
1197
+ gap: "xs",
1198
+ justify: "between"
1199
+ }, () => [h(Cluster, { gap: "xs" }, () => [linkSelection?.existing ? h(Button, {
1200
+ type: "button",
1201
+ variant: "ghost",
1202
+ tone: "danger",
1203
+ size: "sm",
1204
+ onClick: removeLink
1205
+ }, () => "Remove link") : null, linkMode.value === "url" && validLinkHref(linkHref.value) ? h(Button, {
1206
+ as: "a",
1207
+ variant: "link",
1208
+ size: "sm",
1145
1209
  href: validLinkHref(linkHref.value),
1146
1210
  target: "_blank",
1147
1211
  rel: "noopener noreferrer"
1148
- }, "Preview link") : null,
1149
- h("div", { class: "looma-editor__link-actions" }, [linkSelection?.existing ? h("button", {
1212
+ }, () => "Preview link") : null]), h(Cluster, { gap: "xs" }, () => [h(Button, {
1150
1213
  type: "button",
1151
- onClick: removeLink
1152
- }, "Remove link") : null, h("button", { type: "submit" }, "Save link")])
1153
- ]);
1214
+ variant: "ghost",
1215
+ tone: "neutral",
1216
+ size: "sm",
1217
+ onClick: cancelLink
1218
+ }, () => "Cancel"), h(Button, {
1219
+ type: "submit",
1220
+ variant: "solid",
1221
+ size: "sm"
1222
+ }, () => "Save link")])])
1223
+ ])]);
1154
1224
  const renderLinkContext = (instance) => {
1155
1225
  if (linkContextEditing.value) return renderLinkForm();
1156
1226
  const rawHref = instance.getAttributes("link").href;
1157
1227
  const href = typeof rawHref === "string" ? rawHref : "";
1158
1228
  const safeHref = validLinkHref(href);
1159
- return h("div", {
1160
- class: "looma-editor__link-context-actions",
1229
+ const editId = `${toolbarScope}-link-edit`;
1230
+ const removeId = `${toolbarScope}-link-remove`;
1231
+ const openId = `${toolbarScope}-link-open`;
1232
+ const target = linkContextTarget.value;
1233
+ return h(Cluster, {
1234
+ gap: "xs",
1235
+ wrap: "nowrap",
1161
1236
  role: "group",
1162
1237
  "aria-label": "Link actions"
1163
- }, [
1164
- h("span", {
1165
- class: "looma-editor__link-context-url",
1166
- title: 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]]),
1168
- h("button", {
1169
- type: "button",
1238
+ }, () => [
1239
+ loomaIcon(target ? "file-text" : "link"),
1240
+ h(Stack, {
1241
+ gap: "none",
1242
+ title: href,
1243
+ "aria-label": `${target ? props.linkTargetLabel : "URL"}: ${target?.label ?? href}`
1244
+ }, () => [h(Text, {
1245
+ as: "strong",
1246
+ size: "sm",
1247
+ weight: "medium",
1248
+ font: "sans",
1249
+ wrap: "anywhere"
1250
+ }, () => target?.label ?? href), target?.detail ? h(Text, {
1251
+ as: "small",
1252
+ size: "xs",
1253
+ tone: "secondary",
1254
+ font: "sans"
1255
+ }, () => target.detail) : null]),
1256
+ safeHref ? h(Button, {
1257
+ id: openId,
1258
+ as: "a",
1259
+ variant: "ghost",
1260
+ tone: "neutral",
1261
+ size: "sm",
1262
+ href: safeHref,
1263
+ target: "_blank",
1264
+ rel: "noopener noreferrer",
1265
+ "aria-label": "Open link"
1266
+ }, () => loomaIcon("external-link")) : null,
1267
+ safeHref ? h(Tooltip, { for: openId }, () => "Open link") : null,
1268
+ h(IconButton, {
1269
+ id: editId,
1270
+ label: "Edit link",
1271
+ variant: "ghost",
1272
+ size: "sm",
1170
1273
  onPointerdown: (event) => event.preventDefault(),
1171
1274
  onClick: () => openLinkEditor(true)
1172
- }, "Edit link"),
1173
- h("button", {
1174
- type: "button",
1275
+ }, () => loomaIcon("pencil")),
1276
+ h(Tooltip, { for: editId }, () => "Edit link"),
1277
+ h(IconButton, {
1278
+ id: removeId,
1279
+ label: "Remove link",
1280
+ variant: "ghost",
1281
+ size: "sm",
1175
1282
  onPointerdown: (event) => event.preventDefault(),
1176
1283
  onClick: removeCurrentLink
1177
- }, "Remove link"),
1178
- safeHref ? h("a", {
1179
- href: safeHref,
1180
- target: "_blank",
1181
- rel: "noopener noreferrer"
1182
- }, "Open link") : null
1284
+ }, () => loomaIcon("unlink")),
1285
+ h(Tooltip, { for: removeId }, () => "Remove link")
1183
1286
  ]);
1184
1287
  };
1185
1288
  const captureBlockAction = () => {
@@ -1724,29 +1827,34 @@ var LoomaEditor = defineComponent({
1724
1827
  ...authorHighlight ? [commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run(), "Mod-Shift-h")] : [],
1725
1828
  commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run(), "Mod-e")
1726
1829
  ];
1830
+ const linkTriggerId = textOnly ? `${linkAnchorId}-selection` : linkAnchorId;
1831
+ textButtons.push(h(IconButton, {
1832
+ id: linkTriggerId,
1833
+ class: "looma-editor__toolbar-button",
1834
+ label: "Link",
1835
+ size: "sm",
1836
+ variant: "ghost",
1837
+ "aria-expanded": linkOpen.value ? "true" : "false",
1838
+ "aria-pressed": instance.isActive("link") ? "true" : "false",
1839
+ "data-active": instance.isActive("link") ? "true" : "false",
1840
+ onPointerenter: () => showTool(linkTriggerId, "Link", false),
1841
+ onPointerleave: hideTool,
1842
+ onPointerdown: () => {
1843
+ linkPopoverAnchorId.value = linkTriggerId;
1844
+ const selection = instance.state.selection;
1845
+ linkPressedSelection = selection.empty ? null : {
1846
+ from: selection.from,
1847
+ to: selection.to
1848
+ };
1849
+ },
1850
+ onFocusin: () => {
1851
+ linkPopoverAnchorId.value = linkTriggerId;
1852
+ showTool(linkTriggerId, "Link", true);
1853
+ },
1854
+ onFocusout: hideTool
1855
+ }, () => loomaIcon("link")));
1727
1856
  const buttons = textOnly ? textButtons : [
1728
1857
  ...textButtons,
1729
- h(IconButton, {
1730
- id: linkAnchorId,
1731
- class: "looma-editor__toolbar-button",
1732
- label: "Link",
1733
- size: "sm",
1734
- variant: "ghost",
1735
- "aria-expanded": linkOpen.value ? "true" : "false",
1736
- "aria-pressed": instance.isActive("link") ? "true" : "false",
1737
- "data-active": instance.isActive("link") ? "true" : "false",
1738
- onPointerenter: () => showTool(linkAnchorId, "Link", false),
1739
- onPointerleave: hideTool,
1740
- onPointerdown: () => {
1741
- const selection = instance.state.selection;
1742
- linkPressedSelection = selection.empty ? null : {
1743
- from: selection.from,
1744
- to: selection.to
1745
- };
1746
- },
1747
- onFocusin: () => showTool(linkAnchorId, "Link", true),
1748
- onFocusout: hideTool
1749
- }, () => loomaIcon("link")),
1750
1858
  h("span", {
1751
1859
  class: "divider",
1752
1860
  "aria-hidden": "true"
@@ -1901,10 +2009,15 @@ var LoomaEditor = defineComponent({
1901
2009
  appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
1902
2010
  onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
1903
2011
  duration: 100,
1904
- maxWidth: "none",
2012
+ maxWidth: Math.min(480, window.innerWidth - 24),
1905
2013
  placement: "bottom"
1906
2014
  }
1907
- }, { default: () => h("div", { class: ["looma-editor__link-context", { "looma-editor__link-context--editing": linkContextEditing.value }] }, [renderLinkContext(instance)]) }) : null,
2015
+ }, { default: () => h(Card, {
2016
+ variant: "elevated",
2017
+ padding: linkContextEditing.value ? "sm" : "xs",
2018
+ size: linkContextEditing.value ? "sm" : "content",
2019
+ class: ["looma-editor__link-context", { "looma-editor__link-context--editing": linkContextEditing.value }]
2020
+ }, () => [renderLinkContext(instance)]) }) : null,
1908
2021
  instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
1909
2022
  class: "looma-editor__formatting-popover",
1910
2023
  role: "region",
@@ -2028,13 +2141,16 @@ var LoomaEditor = defineComponent({
2028
2141
  }, chipColor.value === color ? "✓" : "")))])),
2029
2142
  h(Popover, {
2030
2143
  class: "looma-editor__link-popover",
2144
+ size: "sm",
2031
2145
  open: linkOpen.value,
2032
- for: linkAnchorId,
2146
+ for: linkPopoverAnchorId.value,
2033
2147
  placement: mobile.value ? "top-start" : "bottom-start",
2034
2148
  onOpen: () => openLinkEditor(),
2035
2149
  onClose: () => {
2036
2150
  linkOpen.value = false;
2037
2151
  linkPressedSelection = null;
2152
+ const current = editor.value?.state.selection;
2153
+ if (current && !current.empty && linkSelection && current.from === linkSelection.from && current.to === linkSelection.to) editor.value?.commands.setTextSelection(current.to);
2038
2154
  }
2039
2155
  }, renderLinkForm),
2040
2156
  h(Menu, {