@threadlabs/looma 0.22.12 → 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 (71) 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 +75 -6
  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/extensions/slash-command.d.ts +5 -3
  18. package/editor/icons.d.ts +4 -0
  19. package/editor/index.js +7 -3
  20. package/package.json +1 -1
  21. package/styles/ui-card.css +17 -0
  22. package/styles/ui-input-group.css +9 -0
  23. package/styles/ui-input.css +1 -0
  24. package/styles/ui-popover.css +13 -0
  25. package/styles/ui-scroll-area.css +10 -0
  26. package/styles/ui-search-result-row.css +9 -0
  27. package/styles/ui-stack.css +3 -0
  28. package/styles/ui-text.css +8 -0
  29. package/vanilla/UiCard.d.ts +2 -1
  30. package/vanilla/UiCard.js +1 -1
  31. package/vanilla/UiIcon.js +1 -1
  32. package/vanilla/UiInputGroup.d.ts +1 -0
  33. package/vanilla/UiInputGroup.js +1 -1
  34. package/vanilla/UiPopover.d.ts +1 -0
  35. package/vanilla/UiPopover.js +1 -1
  36. package/vanilla/UiScrollArea.d.ts +1 -0
  37. package/vanilla/UiScrollArea.js +1 -1
  38. package/vanilla/UiSearchResultRow.d.ts +1 -0
  39. package/vanilla/UiSearchResultRow.js +1 -1
  40. package/vanilla/UiStack.d.ts +1 -1
  41. package/vanilla/UiStack.js +1 -1
  42. package/vanilla/UiText.d.ts +2 -0
  43. package/vanilla/UiText.js +1 -1
  44. package/vue/UiCard.d.ts +12 -3
  45. package/vue/UiCard.vue +49 -5
  46. package/vue/UiIcon.vue +23 -0
  47. package/vue/UiInput.vue +1 -0
  48. package/vue/UiInputGroup.d.ts +9 -0
  49. package/vue/UiInputGroup.vue +33 -2
  50. package/vue/UiPopover.d.ts +9 -0
  51. package/vue/UiPopover.vue +43 -2
  52. package/vue/UiScrollArea.d.ts +9 -0
  53. package/vue/UiScrollArea.vue +35 -2
  54. package/vue/UiSearchResultRow.d.ts +9 -0
  55. package/vue/UiSearchResultRow.vue +38 -1
  56. package/vue/UiStack.d.ts +3 -3
  57. package/vue/UiStack.vue +10 -4
  58. package/vue/UiText.d.ts +18 -0
  59. package/vue/UiText.vue +70 -0
  60. package/vue/chunks/UiCard.js +39 -3
  61. package/vue/chunks/UiIcon.js +69 -1
  62. package/vue/chunks/UiInput.js +1 -1
  63. package/vue/chunks/UiInputGroup.js +59 -22
  64. package/vue/chunks/UiPopover.js +37 -5
  65. package/vue/chunks/UiScrollArea.js +66 -23
  66. package/vue/chunks/UiSearchResultRow.js +28 -2
  67. package/vue/chunks/UiStack.js +3 -2
  68. package/vue/chunks/UiText.js +92 -4
  69. package/vue/components.css +174 -297
  70. package/vue/editor/LoomaEditor.d.ts +11 -0
  71. package/vue/editor/index.js +262 -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);
@@ -769,6 +777,7 @@ var LoomaEditor = defineComponent({
769
777
  const slashExtension = createLoomaSlashCommandExtension({
770
778
  onOpenImagePicker: () => fileInput.value?.click(),
771
779
  onOpenChipEditor: openChipEditor,
780
+ onOpenLinkEditor: () => openLinkEditor(),
772
781
  onStateChange: (state) => {
773
782
  Object.assign(slash, state);
774
783
  }
@@ -849,6 +858,7 @@ var LoomaEditor = defineComponent({
849
858
  extensions: [
850
859
  ...getDefaultEditorExtensions({
851
860
  placeholder: props.placeholder,
861
+ linkBaseUrl: props.linkBaseUrl,
852
862
  mention: mentionExtension ?? false,
853
863
  disableHighlight: props.disableHighlight,
854
864
  codeLanguages: props.codeLanguages ?? void 0,
@@ -965,7 +975,7 @@ var LoomaEditor = defineComponent({
965
975
  linkError.value = "";
966
976
  linkSelectedTarget.value = linkTargetCache.get(linkHref.value) ?? null;
967
977
  linkMode.value = props.linkSearch && (!existing || linkSelectedTarget.value) ? "target" : "url";
968
- linkQuery.value = linkSelectedTarget.value?.label ?? "";
978
+ linkQuery.value = linkSelectedTarget.value?.label ?? linkHref.value;
969
979
  linkContextEditing.value = context;
970
980
  linkOpen.value = !context;
971
981
  if (existing && props.linkResolve && !linkTargetCache.has(linkHref.value)) {
@@ -979,7 +989,7 @@ var LoomaEditor = defineComponent({
979
989
  }
980
990
  }).catch(() => {});
981
991
  }
982
- 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());
983
993
  };
984
994
  const saveLink = () => {
985
995
  const instance = editor.value;
@@ -988,16 +998,16 @@ var LoomaEditor = defineComponent({
988
998
  const href = linkMode.value === "target" ? validLinkHref(linkSelectedTarget.value?.href ?? "") : validLinkHref(linkHref.value);
989
999
  if (!href) {
990
1000
  linkError.value = linkMode.value === "target" ? "Choose a destination from the results." : "Enter an http, https, mailto, tel, or relative URL.";
991
- (linkMode.value === "url" ? linkUrlInput.value : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
1001
+ (linkMode.value === "url" ? linkDestinationInput() : linkFormRoot.value?.querySelector("[data-link-search]"))?.focus();
992
1002
  return;
993
1003
  }
994
1004
  if (linkMode.value === "url" && props.linkSearch && !selection.existing && !/^(https?:|mailto:|tel:)/i.test(href)) {
995
1005
  linkError.value = "Enter a full website, email, or phone URL, or choose a destination.";
996
- linkUrlInput.value?.focus();
1006
+ linkDestinationInput()?.focus();
997
1007
  return;
998
1008
  }
999
1009
  const attrs = {
1000
- href,
1010
+ href: props.linkBaseUrl ? siteRelativeHref(href, props.linkBaseUrl) : href,
1001
1011
  target: linkNewTab.value ? "_blank" : "_self",
1002
1012
  rel: linkNewTab.value ? "noopener noreferrer" : null
1003
1013
  };
@@ -1021,7 +1031,7 @@ var LoomaEditor = defineComponent({
1021
1031
  }).run();
1022
1032
  } else {
1023
1033
  if (selection.existing && selection.from === selection.to) chain.extendMarkRange("link");
1024
- chain.setLink(attrs).run();
1034
+ chain.setLink(attrs).setTextSelection(selection.to).run();
1025
1035
  }
1026
1036
  linkOpen.value = false;
1027
1037
  linkContextEditing.value = false;
@@ -1033,7 +1043,7 @@ var LoomaEditor = defineComponent({
1033
1043
  instance.chain().focus().setTextSelection({
1034
1044
  from: selection.from,
1035
1045
  to: selection.to
1036
- }).extendMarkRange("link").unsetLink().run();
1046
+ }).extendMarkRange("link").unsetLink().setTextSelection(selection.to).run();
1037
1047
  linkOpen.value = false;
1038
1048
  linkContextEditing.value = false;
1039
1049
  };
@@ -1043,6 +1053,114 @@ var LoomaEditor = defineComponent({
1043
1053
  instance.chain().focus().extendMarkRange("link").unsetLink().run();
1044
1054
  linkContextEditing.value = false;
1045
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")];
1046
1164
  const renderLinkForm = () => h("form", {
1047
1165
  ref: linkFormRoot,
1048
1166
  class: "looma-editor__link-form",
@@ -1051,134 +1169,120 @@ var LoomaEditor = defineComponent({
1051
1169
  event.preventDefault();
1052
1170
  saveLink();
1053
1171
  }
1054
- }, [
1055
- linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h("label", [h("span", "Text"), h("input", {
1056
- value: linkText.value,
1057
- onInput: (event) => {
1058
- linkText.value = event.target.value;
1059
- linkError.value = "";
1060
- }
1061
- })]) : null,
1062
- props.linkSearch ? h("div", {
1063
- class: "looma-editor__link-modes",
1064
- role: "group",
1065
- "aria-label": "Link destination"
1066
- }, [h("button", {
1067
- type: "button",
1068
- "aria-pressed": linkMode.value === "target",
1069
- onClick: () => {
1070
- linkMode.value = "target";
1071
- linkHref.value = "";
1072
- linkSelectedTarget.value = null;
1073
- linkQuery.value = "";
1074
- linkError.value = "";
1075
- nextTick(() => linkFormRoot.value?.querySelector("[data-link-search]")?.focus());
1076
- }
1077
- }, props.linkTargetLabel), h("button", {
1078
- type: "button",
1079
- "aria-pressed": linkMode.value === "url",
1080
- onClick: () => {
1081
- linkMode.value = "url";
1082
- linkHref.value = "";
1083
- linkSelectedTarget.value = null;
1084
- linkError.value = "";
1085
- nextTick(() => linkUrlInput.value?.focus());
1086
- }
1087
- }, "URL")]) : null,
1088
- linkMode.value === "target" && props.linkSearch ? h("label", [h("span", `Find a ${props.linkTargetLabel.toLowerCase()}`), h("input", {
1089
- "data-link-search": "",
1090
- type: "search",
1091
- value: linkQuery.value,
1092
- autocomplete: "off",
1093
- "aria-invalid": linkError.value ? "true" : void 0,
1094
- onInput: (event) => {
1095
- linkQuery.value = event.target.value;
1096
- linkSelectedTarget.value = null;
1097
- linkError.value = "";
1098
- },
1099
- onKeydown: (event) => {
1100
- if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
1101
- }
1102
- })]) : h("label", [h("span", "URL"), h("input", {
1103
- ref: linkUrlInput,
1104
- type: "text",
1105
- inputmode: "url",
1106
- value: linkHref.value,
1107
- "aria-invalid": linkError.value ? "true" : void 0,
1108
- onInput: (event) => {
1109
- linkHref.value = event.target.value;
1110
- linkError.value = "";
1111
- }
1112
- })]),
1113
- linkMode.value === "target" && props.linkSearch ? h("div", {
1114
- class: "looma-editor__link-results",
1115
- "aria-label": "Destination search results"
1116
- }, [
1117
- linkSearching.value ? h("p", { role: "status" }, "Searching…") : null,
1118
- linkSelectedTarget.value ? h("p", { class: "looma-editor__link-selected" }, `Selected: ${linkSelectedTarget.value.label}${linkSelectedTarget.value.detail ? ` · ${linkSelectedTarget.value.detail}` : ""}`) : null,
1119
- !linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length < 2 ? h("p", "Type at least two letters to search.") : null,
1120
- !linkSearching.value && !linkSelectedTarget.value && linkQuery.value.trim().length >= 2 && !linkResults.value.length && !linkError.value ? h("p", "No results found.") : null,
1121
- ...linkResults.value.map((target) => h("button", {
1122
- key: target.id,
1123
- type: "button",
1124
- "data-link-result": "",
1125
- onClick: () => selectLinkTarget(target),
1126
- onKeydown: (event) => {
1127
- if (event.key === "ArrowDown") moveLinkResultFocus(event, 1);
1128
- 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 = "";
1129
1187
  }
1130
- }, [h("strong", target.label), target.detail ? h("small", target.detail) : null]))
1131
- ]) : h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
1132
- type: "checkbox",
1133
- checked: linkNewTab.value,
1134
- onChange: (event) => {
1135
- linkNewTab.value = event.target.checked;
1136
- }
1137
- }), h("span", "Open in new tab")]),
1138
- linkError.value ? h("p", {
1139
- class: "looma-editor__link-error",
1188
+ })
1189
+ }) : null,
1190
+ linkError.value ? h(Text, {
1191
+ size: "sm",
1192
+ tone: "danger",
1140
1193
  role: "alert"
1141
- }, linkError.value) : null,
1142
- linkMode.value === "url" && validLinkHref(linkHref.value) ? h("a", {
1143
- 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",
1144
1209
  href: validLinkHref(linkHref.value),
1145
1210
  target: "_blank",
1146
1211
  rel: "noopener noreferrer"
1147
- }, "Preview link") : null,
1148
- h("div", { class: "looma-editor__link-actions" }, [linkSelection?.existing ? h("button", {
1212
+ }, () => "Preview link") : null]), h(Cluster, { gap: "xs" }, () => [h(Button, {
1149
1213
  type: "button",
1150
- onClick: removeLink
1151
- }, "Remove link") : null, h("button", { type: "submit" }, "Save link")])
1152
- ]);
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
+ ])]);
1153
1224
  const renderLinkContext = (instance) => {
1154
1225
  if (linkContextEditing.value) return renderLinkForm();
1155
1226
  const rawHref = instance.getAttributes("link").href;
1156
1227
  const href = typeof rawHref === "string" ? rawHref : "";
1157
1228
  const safeHref = validLinkHref(href);
1158
- return h("div", {
1159
- 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",
1160
1236
  role: "group",
1161
1237
  "aria-label": "Link actions"
1162
- }, [
1163
- h("span", {
1164
- class: "looma-editor__link-context-url",
1165
- title: href
1166
- }, linkContextTarget.value ? [h("strong", linkContextTarget.value.label), linkContextTarget.value.detail ? h("small", linkContextTarget.value.detail) : null] : href),
1167
- h("button", {
1168
- 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",
1169
1273
  onPointerdown: (event) => event.preventDefault(),
1170
1274
  onClick: () => openLinkEditor(true)
1171
- }, "Edit link"),
1172
- h("button", {
1173
- 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",
1174
1282
  onPointerdown: (event) => event.preventDefault(),
1175
1283
  onClick: removeCurrentLink
1176
- }, "Remove link"),
1177
- safeHref ? h("a", {
1178
- href: safeHref,
1179
- target: "_blank",
1180
- rel: "noopener noreferrer"
1181
- }, "Open link") : null
1284
+ }, () => loomaIcon("unlink")),
1285
+ h(Tooltip, { for: removeId }, () => "Remove link")
1182
1286
  ]);
1183
1287
  };
1184
1288
  const captureBlockAction = () => {
@@ -1723,29 +1827,34 @@ var LoomaEditor = defineComponent({
1723
1827
  ...authorHighlight ? [commandButton("Highlight", "highlighter", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run(), "Mod-Shift-h")] : [],
1724
1828
  commandButton("Inline code", "code-xml", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run(), "Mod-e")
1725
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")));
1726
1856
  const buttons = textOnly ? textButtons : [
1727
1857
  ...textButtons,
1728
- h(IconButton, {
1729
- id: linkAnchorId,
1730
- class: "looma-editor__toolbar-button",
1731
- label: "Link",
1732
- size: "sm",
1733
- variant: "ghost",
1734
- "aria-expanded": linkOpen.value ? "true" : "false",
1735
- "aria-pressed": instance.isActive("link") ? "true" : "false",
1736
- "data-active": instance.isActive("link") ? "true" : "false",
1737
- onPointerenter: () => showTool(linkAnchorId, "Link", false),
1738
- onPointerleave: hideTool,
1739
- onPointerdown: () => {
1740
- const selection = instance.state.selection;
1741
- linkPressedSelection = selection.empty ? null : {
1742
- from: selection.from,
1743
- to: selection.to
1744
- };
1745
- },
1746
- onFocusin: () => showTool(linkAnchorId, "Link", true),
1747
- onFocusout: hideTool
1748
- }, () => loomaIcon("link")),
1749
1858
  h("span", {
1750
1859
  class: "divider",
1751
1860
  "aria-hidden": "true"
@@ -1900,10 +2009,15 @@ var LoomaEditor = defineComponent({
1900
2009
  appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
1901
2010
  onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),
1902
2011
  duration: 100,
1903
- maxWidth: "none",
2012
+ maxWidth: Math.min(480, window.innerWidth - 24),
1904
2013
  placement: "bottom"
1905
2014
  }
1906
- }, { 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,
1907
2021
  instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
1908
2022
  class: "looma-editor__formatting-popover",
1909
2023
  role: "region",
@@ -2027,13 +2141,16 @@ var LoomaEditor = defineComponent({
2027
2141
  }, chipColor.value === color ? "✓" : "")))])),
2028
2142
  h(Popover, {
2029
2143
  class: "looma-editor__link-popover",
2144
+ size: "sm",
2030
2145
  open: linkOpen.value,
2031
- for: linkAnchorId,
2146
+ for: linkPopoverAnchorId.value,
2032
2147
  placement: mobile.value ? "top-start" : "bottom-start",
2033
2148
  onOpen: () => openLinkEditor(),
2034
2149
  onClose: () => {
2035
2150
  linkOpen.value = false;
2036
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);
2037
2154
  }
2038
2155
  }, renderLinkForm),
2039
2156
  h(Menu, {