@threadlabs/looma 0.18.4 → 0.18.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -23,6 +23,10 @@
23
23
  syntax: "*";
24
24
  inherits: false;
25
25
  }
26
+ @property --ui-popover-padding {
27
+ syntax: "*";
28
+ inherits: false;
29
+ }
26
30
  @scope ([data-component~="ui-popover"]) to ([data-component], [data-slotted]) {
27
31
  /* This component's hooks style the element they are set on, and never a nested one. */
28
32
 
@@ -31,6 +35,7 @@
31
35
  --_ui-popover-border: var(--ui-popover-border);
32
36
  --_ui-popover-radius: var(--ui-popover-radius);
33
37
  --_ui-popover-shadow: var(--ui-popover-shadow);
38
+ --_ui-popover-padding: var(--ui-popover-padding);
34
39
  box-sizing: border-box;
35
40
  max-inline-size: min(
36
41
  20rem,
@@ -51,7 +56,7 @@
51
56
  max-inline-size: inherit;
52
57
  max-block-size: inherit;
53
58
  overflow: auto;
54
- padding: var(--ui-space-3);
59
+ padding: var(--_ui-popover-padding, var(--ui-space-3));
55
60
  background: var(--_ui-popover-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
56
61
  border: 1px solid var(--_ui-popover-border, var(--ui-overlay-border, var(--ui-border-strong)));
57
62
  border-radius: var(--_ui-popover-radius, var(--ui-overlay-radius, var(--ui-radius-lg)));
package/vue/UiPopover.vue CHANGED
@@ -120,12 +120,17 @@ useComponentHost(controllerModule.default, {
120
120
  syntax: "*";
121
121
  inherits: false;
122
122
  }
123
+ @property --ui-popover-padding {
124
+ syntax: "*";
125
+ inherits: false;
126
+ }
123
127
 
124
128
  [data-component~="ui-popover"] {
125
129
  --_ui-popover-surface: var(--ui-popover-surface);
126
130
  --_ui-popover-border: var(--ui-popover-border);
127
131
  --_ui-popover-radius: var(--ui-popover-radius);
128
132
  --_ui-popover-shadow: var(--ui-popover-shadow);
133
+ --_ui-popover-padding: var(--ui-popover-padding);
129
134
  box-sizing: border-box;
130
135
  max-inline-size: min(
131
136
  20rem,
@@ -146,7 +151,7 @@ useComponentHost(controllerModule.default, {
146
151
  max-inline-size: inherit;
147
152
  max-block-size: inherit;
148
153
  overflow: auto;
149
- padding: var(--ui-space-3);
154
+ padding: var(--_ui-popover-padding, var(--ui-space-3));
150
155
  background: var(--_ui-popover-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
151
156
  border: 1px solid var(--_ui-popover-border, var(--ui-overlay-border, var(--ui-border-strong)));
152
157
  border-radius: var(--_ui-popover-radius, var(--ui-overlay-radius, var(--ui-radius-lg)));
@@ -56,6 +56,6 @@ var UiPopover_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PU
56
56
  }), [createElementVNode("div", _hoisted_2, [renderSlot(_ctx.$slots, "default", {}, void 0, true)])], 16, _hoisted_1);
57
57
  };
58
58
  }
59
- }), [["__scopeId", "data-v-dc71213c"]]);
59
+ }), [["__scopeId", "data-v-fd69c5a5"]]);
60
60
  //#endregion
61
61
  export { UiPopover_default as t };
@@ -5732,11 +5732,16 @@ svg[data-v-9828be81-s] {
5732
5732
  syntax: "*";
5733
5733
  inherits: false;
5734
5734
  }
5735
- [data-component~="ui-popover"][data-v-dc71213c] {
5735
+ @property --ui-popover-padding {
5736
+ syntax: "*";
5737
+ inherits: false;
5738
+ }
5739
+ [data-component~="ui-popover"][data-v-fd69c5a5] {
5736
5740
  --_ui-popover-surface: var(--ui-popover-surface);
5737
5741
  --_ui-popover-border: var(--ui-popover-border);
5738
5742
  --_ui-popover-radius: var(--ui-popover-radius);
5739
5743
  --_ui-popover-shadow: var(--ui-popover-shadow);
5744
+ --_ui-popover-padding: var(--ui-popover-padding);
5740
5745
  box-sizing: border-box;
5741
5746
  max-inline-size: min(
5742
5747
  20rem,
@@ -5751,12 +5756,12 @@ svg[data-v-9828be81-s] {
5751
5756
  background: transparent;
5752
5757
  box-shadow: none;
5753
5758
  }
5754
- .surface[data-v-dc71213c] {
5759
+ .surface[data-v-fd69c5a5] {
5755
5760
  box-sizing: border-box;
5756
5761
  max-inline-size: inherit;
5757
5762
  max-block-size: inherit;
5758
5763
  overflow: auto;
5759
- padding: var(--ui-space-3);
5764
+ padding: var(--_ui-popover-padding, var(--ui-space-3));
5760
5765
  background: var(--_ui-popover-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
5761
5766
  border: 1px solid var(--_ui-popover-border, var(--ui-overlay-border, var(--ui-border-strong)));
5762
5767
  border-radius: var(--_ui-popover-radius, var(--ui-overlay-radius, var(--ui-radius-lg)));
@@ -5765,10 +5770,10 @@ svg[data-v-9828be81-s] {
5765
5770
  var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg)))
5766
5771
  );
5767
5772
  }
5768
- [data-component~="ui-popover"][data-v-dc71213c]:not([data-component~="ui-popover"][data-ui-popover-state~="internalOpen"]) {
5773
+ [data-component~="ui-popover"][data-v-fd69c5a5]:not([data-component~="ui-popover"][data-ui-popover-state~="internalOpen"]) {
5769
5774
  display: none;
5770
5775
  }
5771
- [data-component~="ui-popover"][popover][data-v-dc71213c] {
5776
+ [data-component~="ui-popover"][popover][data-v-fd69c5a5] {
5772
5777
  position: fixed;
5773
5778
  inset: auto;
5774
5779
  margin: 0;
@@ -5780,21 +5785,21 @@ svg[data-v-9828be81-s] {
5780
5785
  display var(--ui-motion-fast) allow-discrete,
5781
5786
  overlay var(--ui-motion-fast) allow-discrete;
5782
5787
  }
5783
- [data-component~="ui-popover"][popover][data-v-dc71213c]:popover-open {
5788
+ [data-component~="ui-popover"][popover][data-v-fd69c5a5]:popover-open {
5784
5789
  opacity: 1;
5785
5790
  transform: translateY(0) scale(1);
5786
5791
  }
5787
5792
  @starting-style {
5788
- [data-component~="ui-popover"][popover][data-v-dc71213c]:popover-open {
5793
+ [data-component~="ui-popover"][popover][data-v-fd69c5a5]:popover-open {
5789
5794
  opacity: 0;
5790
5795
  transform: translateY(-4px) scale(0.99);
5791
5796
  }
5792
5797
  }
5793
- [data-component~="ui-popover"][hidden][data-v-dc71213c] {
5798
+ [data-component~="ui-popover"][hidden][data-v-fd69c5a5] {
5794
5799
  display: none;
5795
5800
  }
5796
5801
  @media (prefers-reduced-motion: reduce) {
5797
- [data-component~="ui-popover"][popover][data-v-dc71213c] {
5802
+ [data-component~="ui-popover"][popover][data-v-fd69c5a5] {
5798
5803
  transition: none;
5799
5804
  }
5800
5805
  }
@@ -8884,6 +8889,73 @@ html[data-ui-input-modality="touch"]
8884
8889
  inline-size: min(22rem, calc(100vw - 2rem));
8885
8890
  }
8886
8891
 
8892
+ /* The floating toolbar supplies the surface; the containing popover only
8893
+ positions it beside the app-owned trigger. */
8894
+ .looma-editor__formatting-popover {
8895
+ --ui-popover-padding: 0;
8896
+ --ui-popover-surface: transparent;
8897
+ --ui-popover-border: transparent;
8898
+ --ui-popover-shadow: none;
8899
+ }
8900
+
8901
+ .looma-editor__link-context {
8902
+ box-sizing: border-box;
8903
+ max-width: min(24rem, calc(100vw - 1.5rem));
8904
+ padding: var(--ui-editor-toolbar-floating-padding, 4px);
8905
+ border: 1px solid var(--ui-editor-toolbar-border, var(--ui-border-strong));
8906
+ border-radius: var(--ui-editor-toolbar-floating-radius, var(--ui-radius-lg));
8907
+ background: var(--ui-editor-toolbar-surface, var(--ui-surface));
8908
+ box-shadow: var(--ui-editor-toolbar-floating-shadow, var(--ui-shadow-lg, 0 10px 24px rgb(0 0 0 / 0.12)));
8909
+ color: var(--ui-text-primary);
8910
+ font-family: var(--ui-font-family-sans, sans-serif);
8911
+ }
8912
+
8913
+ .looma-editor__link-context-actions {
8914
+ display: flex;
8915
+ align-items: center;
8916
+ gap: var(--ui-space-1);
8917
+ }
8918
+
8919
+ .looma-editor__link-context-url {
8920
+ min-width: 0;
8921
+ max-width: 10rem;
8922
+ overflow: hidden;
8923
+ padding-inline: var(--ui-space-2);
8924
+ color: var(--ui-text-secondary);
8925
+ text-overflow: ellipsis;
8926
+ white-space: nowrap;
8927
+ }
8928
+
8929
+ .looma-editor__link-context-actions :is(button, a) {
8930
+ display: inline-flex;
8931
+ flex: 0 0 auto;
8932
+ align-items: center;
8933
+ min-height: var(--ui-control-min-block-size);
8934
+ padding: 0 var(--ui-space-2);
8935
+ border: 0;
8936
+ border-radius: var(--ui-radius-sm);
8937
+ background: transparent;
8938
+ color: var(--ui-text-primary);
8939
+ font: inherit;
8940
+ font-size: var(--ui-font-size-sm);
8941
+ text-decoration: none;
8942
+ cursor: pointer;
8943
+ }
8944
+
8945
+ .looma-editor__link-context-actions :is(button, a):is(:hover, :focus-visible) {
8946
+ background: var(--ui-surface-subtle);
8947
+ }
8948
+
8949
+ .looma-editor__link-context-actions :is(button, a):focus-visible {
8950
+ outline: 2px solid var(--ui-focus-ring);
8951
+ outline-offset: 2px;
8952
+ }
8953
+
8954
+ .looma-editor__link-context--editing {
8955
+ width: min(22rem, calc(100vw - 1.5rem));
8956
+ padding: var(--ui-space-3);
8957
+ }
8958
+
8887
8959
  .looma-editor__chip-popover {
8888
8960
  inline-size: min(15rem, calc(100vw - 2rem));
8889
8961
  }
@@ -370,6 +370,7 @@ var LoomaEditor = defineComponent({
370
370
  const linkAnchorId = `looma-editor-link-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
371
371
  const linkUrlInput = ref(null);
372
372
  const linkOpen = ref(false);
373
+ const linkContextEditing = ref(false);
373
374
  const linkHref = ref("");
374
375
  const linkText = ref("");
375
376
  const linkNewTab = ref(true);
@@ -550,7 +551,10 @@ var LoomaEditor = defineComponent({
550
551
  } },
551
552
  onCreate: ({ editor: instance }) => emit("ready", instance),
552
553
  onFocus: ({ editor: instance }) => rememberSelection(instance),
553
- onSelectionUpdate: ({ editor: instance }) => rememberSelection(instance),
554
+ onSelectionUpdate: ({ editor: instance }) => {
555
+ rememberSelection(instance);
556
+ if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
557
+ },
554
558
  onTransaction: () => {
555
559
  editorStateVersion.value += 1;
556
560
  },
@@ -560,7 +564,7 @@ var LoomaEditor = defineComponent({
560
564
  emit("update", value);
561
565
  }
562
566
  });
563
- const openLinkEditor = () => {
567
+ const openLinkEditor = (context = false) => {
564
568
  const instance = editor.value;
565
569
  if (!instance || !props.editable) return;
566
570
  const current = instance.state.selection;
@@ -581,7 +585,8 @@ var LoomaEditor = defineComponent({
581
585
  linkNewTab.value = attrs.target !== "_self";
582
586
  linkText.value = empty && !existing ? "" : instance.state.doc.textBetween(from, to);
583
587
  linkError.value = "";
584
- linkOpen.value = true;
588
+ linkContextEditing.value = context;
589
+ linkOpen.value = !context;
585
590
  nextTick(() => linkUrlInput.value?.focus());
586
591
  };
587
592
  const saveLink = () => {
@@ -622,6 +627,7 @@ var LoomaEditor = defineComponent({
622
627
  chain.setLink(attrs).run();
623
628
  }
624
629
  linkOpen.value = false;
630
+ linkContextEditing.value = false;
625
631
  };
626
632
  const removeLink = () => {
627
633
  const instance = editor.value;
@@ -632,6 +638,92 @@ var LoomaEditor = defineComponent({
632
638
  to: selection.to
633
639
  }).extendMarkRange("link").unsetLink().run();
634
640
  linkOpen.value = false;
641
+ linkContextEditing.value = false;
642
+ };
643
+ const removeCurrentLink = () => {
644
+ const instance = editor.value;
645
+ if (!instance?.isActive("link")) return;
646
+ instance.chain().focus().extendMarkRange("link").unsetLink().run();
647
+ linkContextEditing.value = false;
648
+ };
649
+ const renderLinkForm = () => h("form", {
650
+ class: "looma-editor__link-form",
651
+ "aria-label": "Edit link",
652
+ onSubmit: (event) => {
653
+ event.preventDefault();
654
+ saveLink();
655
+ }
656
+ }, [
657
+ linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h("label", [h("span", "Text"), h("input", {
658
+ value: linkText.value,
659
+ onInput: (event) => {
660
+ linkText.value = event.target.value;
661
+ linkError.value = "";
662
+ }
663
+ })]) : null,
664
+ h("label", [h("span", "URL"), h("input", {
665
+ ref: linkUrlInput,
666
+ type: "text",
667
+ inputmode: "url",
668
+ value: linkHref.value,
669
+ "aria-invalid": linkError.value ? "true" : void 0,
670
+ onInput: (event) => {
671
+ linkHref.value = event.target.value;
672
+ linkError.value = "";
673
+ }
674
+ })]),
675
+ h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
676
+ type: "checkbox",
677
+ checked: linkNewTab.value,
678
+ onChange: (event) => {
679
+ linkNewTab.value = event.target.checked;
680
+ }
681
+ }), h("span", "Open in new tab")]),
682
+ linkError.value ? h("p", {
683
+ class: "looma-editor__link-error",
684
+ role: "alert"
685
+ }, linkError.value) : null,
686
+ validLinkHref(linkHref.value) ? h("a", {
687
+ class: "looma-editor__link-preview",
688
+ href: validLinkHref(linkHref.value),
689
+ target: "_blank",
690
+ rel: "noopener noreferrer"
691
+ }, "Preview link") : null,
692
+ h("div", { class: "looma-editor__link-actions" }, [linkSelection?.existing ? h("button", {
693
+ type: "button",
694
+ onClick: removeLink
695
+ }, "Remove link") : null, h("button", { type: "submit" }, "Save link")])
696
+ ]);
697
+ const renderLinkContext = (instance) => {
698
+ if (linkContextEditing.value) return renderLinkForm();
699
+ const rawHref = instance.getAttributes("link").href;
700
+ const href = typeof rawHref === "string" ? rawHref : "";
701
+ const safeHref = validLinkHref(href);
702
+ return h("div", {
703
+ class: "looma-editor__link-context-actions",
704
+ role: "group",
705
+ "aria-label": "Link actions"
706
+ }, [
707
+ h("span", {
708
+ class: "looma-editor__link-context-url",
709
+ title: href
710
+ }, href),
711
+ h("button", {
712
+ type: "button",
713
+ onPointerdown: (event) => event.preventDefault(),
714
+ onClick: () => openLinkEditor(true)
715
+ }, "Edit link"),
716
+ h("button", {
717
+ type: "button",
718
+ onPointerdown: (event) => event.preventDefault(),
719
+ onClick: removeCurrentLink
720
+ }, "Remove link"),
721
+ safeHref ? h("a", {
722
+ href: safeHref,
723
+ target: "_blank",
724
+ rel: "noopener noreferrer"
725
+ }, "Open link") : null
726
+ ]);
635
727
  };
636
728
  const captureBlockAction = () => {
637
729
  const instance = editor.value;
@@ -1330,10 +1422,10 @@ var LoomaEditor = defineComponent({
1330
1422
  onDblclick: onImageDoubleClick,
1331
1423
  onKeydown: onImageKeyDown
1332
1424
  }, [
1333
- instance && props.editable && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" && !props.toolbarOpen) ? h(BubbleMenu, {
1425
+ instance && props.editable && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover") ? h(BubbleMenu, {
1334
1426
  editor: instance,
1335
1427
  pluginKey: "looma-text-formatting-menu",
1336
- shouldShow: ({ editor: menuEditor, from, to }) => shouldShowTextFormattingToolbar(menuEditor, from, to),
1428
+ shouldShow: ({ editor: menuEditor, from, to }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && shouldShowTextFormattingToolbar(menuEditor, from, to),
1337
1429
  tippyOptions: {
1338
1430
  appendTo: () => root.value ?? document.body,
1339
1431
  duration: 100,
@@ -1341,6 +1433,17 @@ var LoomaEditor = defineComponent({
1341
1433
  placement: "top"
1342
1434
  }
1343
1435
  }, { default: () => renderToolbar(instance, true, props.toolbarMode === "popover") }) : null,
1436
+ instance && props.editable && !mobile.value ? h(BubbleMenu, {
1437
+ editor: instance,
1438
+ pluginKey: "looma-link-context-menu",
1439
+ shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && !linkOpen.value,
1440
+ tippyOptions: {
1441
+ appendTo: () => root.value ?? document.body,
1442
+ duration: 100,
1443
+ maxWidth: "none",
1444
+ placement: "bottom"
1445
+ }
1446
+ }, { default: () => h("div", { class: ["looma-editor__link-context", { "looma-editor__link-context--editing": linkContextEditing.value }] }, [renderLinkContext(instance)]) }) : null,
1344
1447
  instance && props.editable && !mobile.value && props.toolbarMode === "popover" && props.toolbarTriggerId ? h(Popover, {
1345
1448
  class: "looma-editor__formatting-popover",
1346
1449
  role: "region",
@@ -1350,7 +1453,7 @@ var LoomaEditor = defineComponent({
1350
1453
  placement: "bottom-end",
1351
1454
  onOpen: () => emit("update:toolbarOpen", true),
1352
1455
  onClose: () => emit("update:toolbarOpen", false)
1353
- }, () => [renderToolbar(instance, false)]) : null,
1456
+ }, () => [renderToolbar(instance, true)]) : null,
1354
1457
  instance && props.editable && !mobile.value && props.toolbarMode === "sticky" ? h("div", {
1355
1458
  class: "looma-editor__sticky-toolbar-shell",
1356
1459
  role: "toolbar",
@@ -1467,59 +1570,12 @@ var LoomaEditor = defineComponent({
1467
1570
  open: linkOpen.value,
1468
1571
  for: linkAnchorId,
1469
1572
  placement: mobile.value ? "top-start" : "bottom-start",
1470
- onOpen: openLinkEditor,
1573
+ onOpen: () => openLinkEditor(),
1471
1574
  onClose: () => {
1472
1575
  linkOpen.value = false;
1473
1576
  linkPressedSelection = null;
1474
1577
  }
1475
- }, () => h("form", {
1476
- class: "looma-editor__link-form",
1477
- "aria-label": "Edit link",
1478
- onSubmit: (event) => {
1479
- event.preventDefault();
1480
- saveLink();
1481
- }
1482
- }, [
1483
- linkSelection?.from === linkSelection?.to && !linkSelection?.existing ? h("label", [h("span", "Text"), h("input", {
1484
- value: linkText.value,
1485
- onInput: (event) => {
1486
- linkText.value = event.target.value;
1487
- linkError.value = "";
1488
- }
1489
- })]) : null,
1490
- h("label", [h("span", "URL"), h("input", {
1491
- ref: linkUrlInput,
1492
- type: "text",
1493
- inputmode: "url",
1494
- value: linkHref.value,
1495
- "aria-invalid": linkError.value ? "true" : void 0,
1496
- onInput: (event) => {
1497
- linkHref.value = event.target.value;
1498
- linkError.value = "";
1499
- }
1500
- })]),
1501
- h("label", { class: "looma-editor__link-new-tab" }, [h("input", {
1502
- type: "checkbox",
1503
- checked: linkNewTab.value,
1504
- onChange: (event) => {
1505
- linkNewTab.value = event.target.checked;
1506
- }
1507
- }), h("span", "Open in new tab")]),
1508
- linkError.value ? h("p", {
1509
- class: "looma-editor__link-error",
1510
- role: "alert"
1511
- }, linkError.value) : null,
1512
- validLinkHref(linkHref.value) ? h("a", {
1513
- class: "looma-editor__link-preview",
1514
- href: validLinkHref(linkHref.value),
1515
- target: "_blank",
1516
- rel: "noopener noreferrer"
1517
- }, "Preview link") : null,
1518
- h("div", { class: "looma-editor__link-actions" }, [linkSelection?.existing ? h("button", {
1519
- type: "button",
1520
- onClick: removeLink
1521
- }, "Remove link") : null, h("button", { type: "submit" }, "Save link")])
1522
- ])),
1578
+ }, renderLinkForm),
1523
1579
  h(Menu, {
1524
1580
  for: blockActionAnchorId,
1525
1581
  placement: mobile.value ? "top-start" : "bottom-start",