@designfever/web-review-kit 0.3.0 → 0.4.1

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/dist/index.cjs CHANGED
@@ -459,19 +459,6 @@ function getPopoverPosition(point, environment, options) {
459
459
  )
460
460
  };
461
461
  }
462
- function getAreaPopoverPosition(selection, environment) {
463
- return getPopoverPosition(
464
- {
465
- x: selection.left + selection.width,
466
- y: selection.top
467
- },
468
- environment,
469
- {
470
- width: 360,
471
- estimatedHeight: 206
472
- }
473
- );
474
- }
475
462
  function getPopoverBounds(environment) {
476
463
  if (!environment) {
477
464
  return {
@@ -536,6 +523,21 @@ function roundPoint(point) {
536
523
  }
537
524
 
538
525
  // src/core/dom.anchor.ts
526
+ var COMMON_ANCHOR_ATTRIBUTES = [
527
+ "data-testid",
528
+ "data-test-id",
529
+ "data-cy",
530
+ "data-test",
531
+ "data-qa",
532
+ "data-section-id",
533
+ "data-component"
534
+ ];
535
+ var SEMANTIC_ANCHOR_ATTRIBUTES = [
536
+ "aria-label",
537
+ "title",
538
+ "name",
539
+ "href"
540
+ ];
539
541
  function getDomAnchor(selection, configuredAttribute = "data-qa-id", environment) {
540
542
  const x = selection.left + selection.width / 2;
541
543
  const y = selection.top + selection.height / 2;
@@ -556,8 +558,8 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
556
558
  source: getDomSourceHint(target)
557
559
  };
558
560
  }
559
- function getElementViewportSelection(anchor, environment) {
560
- const element = getAnchorElement(anchor, environment);
561
+ function getElementViewportSelection(anchor, environment, preferredSelection) {
562
+ const element = getAnchorElement(anchor, environment, preferredSelection);
561
563
  if (!element) return void 0;
562
564
  const rect = element.getBoundingClientRect();
563
565
  if (rect.width <= 0 || rect.height <= 0) return void 0;
@@ -596,22 +598,35 @@ function getAnchorCandidates(anchor) {
596
598
  ...anchor.candidates ?? []
597
599
  ]);
598
600
  }
599
- function resolveAnchorElement(anchor, environment) {
601
+ function resolveAnchorElement(anchor, environment, preferredSelection) {
600
602
  const matches = getAnchorCandidates(anchor).flatMap((candidate) => {
601
- const match = queryBestAnchorCandidate(
602
- candidate,
603
- candidate.textFingerprint ?? anchor.textFingerprint,
604
- environment
605
- );
606
- if (!match) return [];
607
- const confidence = roundRatio(
608
- (candidate.confidence ?? 0.5) * match.score
609
- );
610
- return [{
611
- element: match.element,
612
- candidate,
613
- confidence
614
- }];
603
+ const textFingerprint = candidate.textFingerprint ?? anchor.textFingerprint;
604
+ if (!preferredSelection) {
605
+ const match = queryBestAnchorCandidate(
606
+ candidate,
607
+ textFingerprint,
608
+ environment
609
+ );
610
+ if (!match) return [];
611
+ const confidence = roundRatio(
612
+ (candidate.confidence ?? 0.5) * match.score
613
+ );
614
+ return [{
615
+ element: match.element,
616
+ candidate,
617
+ confidence
618
+ }];
619
+ }
620
+ return queryAnchorElements(candidate.selector, environment).map((element) => {
621
+ const confidence = roundRatio(
622
+ (candidate.confidence ?? 0.5) * getTextFingerprintScore(textFingerprint, getTextFingerprint(element)) * getSelectionMatchScore(element, preferredSelection)
623
+ );
624
+ return {
625
+ element,
626
+ candidate,
627
+ confidence
628
+ };
629
+ });
615
630
  });
616
631
  return matches.sort((a, b) => b.confidence - a.confidence)[0];
617
632
  }
@@ -621,50 +636,70 @@ function cssEscape(value) {
621
636
  }
622
637
  return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
623
638
  }
624
- function getAnchorElement(anchor, environment) {
625
- return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment)?.element;
639
+ function getAnchorElement(anchor, environment, preferredSelection) {
640
+ return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment, preferredSelection)?.element;
626
641
  }
627
642
  function createAnchorCandidates(target, configuredAttribute) {
628
- const candidates = [];
629
- const anchoredByAttribute = target.closest(`[${configuredAttribute}]`);
630
- if (anchoredByAttribute) {
631
- const value = anchoredByAttribute.getAttribute(configuredAttribute);
632
- if (value) {
633
- candidates.push({
634
- selector: `[${configuredAttribute}="${cssEscape(value)}"]`,
635
- strategy: "configured-attribute",
636
- confidence: 0.98,
637
- textFingerprint: getTextFingerprint(anchoredByAttribute)
638
- });
639
- }
640
- }
643
+ const targetCandidates = [];
644
+ const configuredAnchor = getExactAttributeAnchorCandidate(
645
+ target,
646
+ configuredAttribute,
647
+ 0.98,
648
+ "configured-attribute"
649
+ );
650
+ if (configuredAnchor) targetCandidates.push(configuredAnchor);
651
+ const targetAttributeAnchor = getAttributeAnchorCandidate(
652
+ target,
653
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
654
+ 0.9
655
+ );
656
+ if (targetAttributeAnchor) targetCandidates.push(targetAttributeAnchor);
641
657
  if (isMeaningfulId(target.id)) {
642
- candidates.push({
658
+ targetCandidates.push({
643
659
  selector: `#${cssEscape(target.id)}`,
644
660
  strategy: "id",
645
661
  confidence: 0.94,
646
662
  textFingerprint: getTextFingerprint(target)
647
663
  });
648
664
  }
665
+ const semanticAnchor = getAttributeAnchorCandidate(
666
+ target,
667
+ SEMANTIC_ANCHOR_ATTRIBUTES,
668
+ 0.84
669
+ );
670
+ if (semanticAnchor) targetCandidates.push(semanticAnchor);
649
671
  const targetClassName = getMeaningfulClassName(target);
650
672
  if (targetClassName) {
651
- candidates.push({
673
+ targetCandidates.push({
652
674
  selector: `${target.tagName.toLowerCase()}.${cssEscape(targetClassName)}`,
653
675
  strategy: "class",
654
676
  confidence: 0.82,
655
677
  textFingerprint: getTextFingerprint(target)
656
678
  });
657
679
  }
658
- candidates.push({
680
+ const scopedPath = getScopedDomPathCandidate(target, configuredAttribute);
681
+ if (scopedPath) targetCandidates.push(scopedPath);
682
+ const targetDomPath = {
659
683
  selector: getDomPath(target),
660
684
  strategy: "dom-path",
661
- confidence: 0.9,
685
+ confidence: targetCandidates.length > 0 ? 0.8 : 0.5,
662
686
  textFingerprint: getTextFingerprint(target)
663
- });
687
+ };
688
+ const parentCandidates = [];
664
689
  const parent = target.parentElement;
690
+ const parentConfiguredAnchor = parent ? findClosestAttributeAnchor(parent, [configuredAttribute], 0.72, {
691
+ strategy: "configured-attribute"
692
+ }) : void 0;
693
+ if (parentConfiguredAnchor) parentCandidates.push(parentConfiguredAnchor);
694
+ const anchoredByAttribute = parent ? findClosestAttributeAnchor(
695
+ parent,
696
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
697
+ 0.7
698
+ ) : void 0;
699
+ if (anchoredByAttribute) parentCandidates.push(anchoredByAttribute);
665
700
  const anchoredById = parent ? findClosest(parent, (element) => isMeaningfulId(element.id)) : void 0;
666
701
  if (anchoredById?.id) {
667
- candidates.push({
702
+ parentCandidates.push({
668
703
  selector: `#${cssEscape(anchoredById.id)}`,
669
704
  strategy: "id",
670
705
  confidence: 0.72,
@@ -674,7 +709,7 @@ function createAnchorCandidates(target, configuredAttribute) {
674
709
  const anchoredByClass = parent ? findClosest(parent, (element) => Boolean(getMeaningfulClassName(element))) : void 0;
675
710
  const className = anchoredByClass ? getMeaningfulClassName(anchoredByClass) : void 0;
676
711
  if (anchoredByClass && className) {
677
- candidates.push({
712
+ parentCandidates.push({
678
713
  selector: `${anchoredByClass.tagName.toLowerCase()}.${cssEscape(
679
714
  className
680
715
  )}`,
@@ -683,8 +718,107 @@ function createAnchorCandidates(target, configuredAttribute) {
683
718
  textFingerprint: getTextFingerprint(anchoredByClass)
684
719
  });
685
720
  }
721
+ const candidates = targetCandidates.length > 0 ? [...targetCandidates, targetDomPath, ...parentCandidates] : [...parentCandidates, targetDomPath];
686
722
  return dedupeAnchorCandidates(candidates);
687
723
  }
724
+ function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
725
+ for (const attributeName of attributeNames) {
726
+ const selector = `[${attributeName}]`;
727
+ const element = safeClosest(target, selector);
728
+ if (!element) continue;
729
+ const value = getStableAttributeValue(element, attributeName);
730
+ if (!value) continue;
731
+ return {
732
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
733
+ strategy: options?.strategy ?? "attribute",
734
+ confidence,
735
+ textFingerprint: getTextFingerprint(element)
736
+ };
737
+ }
738
+ return void 0;
739
+ }
740
+ function getExactAttributeAnchorCandidate(element, attributeName, confidence, strategy) {
741
+ const value = getStableAttributeValue(element, attributeName);
742
+ if (!value) return void 0;
743
+ return {
744
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
745
+ strategy,
746
+ confidence,
747
+ textFingerprint: getTextFingerprint(element)
748
+ };
749
+ }
750
+ function getAttributeAnchorCandidate(element, attributeNames, confidence) {
751
+ for (const attributeName of attributeNames) {
752
+ const value = getStableAttributeValue(element, attributeName);
753
+ if (!value) continue;
754
+ return {
755
+ selector: `${element.tagName.toLowerCase()}[${attributeName}="${cssEscape(
756
+ value
757
+ )}"]`,
758
+ strategy: "attribute",
759
+ confidence,
760
+ textFingerprint: getTextFingerprint(element)
761
+ };
762
+ }
763
+ return void 0;
764
+ }
765
+ function getScopedDomPathCandidate(target, configuredAttribute) {
766
+ const parent = target.parentElement;
767
+ if (!parent) return void 0;
768
+ const anchor = findStableAncestorSelector(parent, configuredAttribute);
769
+ if (!anchor) return void 0;
770
+ const selector = getDomPathBetween(anchor.element, target, anchor.selector);
771
+ if (!selector) return void 0;
772
+ return {
773
+ selector,
774
+ strategy: "dom-path",
775
+ confidence: anchor.confidence,
776
+ textFingerprint: getTextFingerprint(target)
777
+ };
778
+ }
779
+ function findStableAncestorSelector(start, configuredAttribute) {
780
+ let element = start;
781
+ const root = start.ownerDocument.documentElement;
782
+ while (element && element !== root) {
783
+ const configuredValue = getStableAttributeValue(element, configuredAttribute);
784
+ if (configuredValue) {
785
+ return {
786
+ element,
787
+ selector: `[${configuredAttribute}="${cssEscape(configuredValue)}"]`,
788
+ confidence: 0.88
789
+ };
790
+ }
791
+ const attributeAnchor = getAttributeAnchorCandidate(
792
+ element,
793
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
794
+ 0.84
795
+ );
796
+ if (attributeAnchor) {
797
+ return {
798
+ element,
799
+ selector: attributeAnchor.selector,
800
+ confidence: 0.84
801
+ };
802
+ }
803
+ if (isMeaningfulId(element.id)) {
804
+ return {
805
+ element,
806
+ selector: `#${cssEscape(element.id)}`,
807
+ confidence: 0.82
808
+ };
809
+ }
810
+ const className = getMeaningfulClassName(element);
811
+ if (className) {
812
+ return {
813
+ element,
814
+ selector: `${element.tagName.toLowerCase()}.${cssEscape(className)}`,
815
+ confidence: 0.76
816
+ };
817
+ }
818
+ element = element.parentElement;
819
+ }
820
+ return void 0;
821
+ }
688
822
  function getAnchorSourceElement(target, candidate, configuredAttribute) {
689
823
  if (candidate.strategy === "configured-attribute") {
690
824
  return target.closest(`[${configuredAttribute}]`);
@@ -696,6 +830,13 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
696
830
  return target;
697
831
  }
698
832
  }
833
+ function safeClosest(element, selector) {
834
+ try {
835
+ return element.closest(selector);
836
+ } catch {
837
+ return null;
838
+ }
839
+ }
699
840
  function getElementHtmlSnippet(element, maxLength = 1e3) {
700
841
  const html = decodeHtmlEntities(element.outerHTML.replace(/\s+/g, " ").trim());
701
842
  if (html.length <= maxLength) return html;
@@ -817,10 +958,38 @@ function getDomPath(element) {
817
958
  }
818
959
  return `body > ${parts.join(" > ")}`;
819
960
  }
961
+ function getDomPathBetween(ancestor, target, ancestorSelector) {
962
+ const parts = [];
963
+ let current = target;
964
+ while (current && current !== ancestor) {
965
+ parts.unshift(getDomPathPart(current));
966
+ current = current.parentElement;
967
+ }
968
+ if (current !== ancestor || parts.length === 0) return void 0;
969
+ return `${ancestorSelector} > ${parts.join(" > ")}`;
970
+ }
971
+ function getDomPathPart(element) {
972
+ const parent = element.parentElement;
973
+ const tag = element.tagName.toLowerCase();
974
+ if (!parent) return tag;
975
+ const currentTagName = element.tagName;
976
+ const siblings = Array.from(parent.children).filter(
977
+ (child) => child.tagName === currentTagName
978
+ );
979
+ const index = siblings.indexOf(element) + 1;
980
+ return `${tag}:nth-of-type(${index})`;
981
+ }
820
982
  function getTextFingerprint(element) {
821
983
  const text = element.textContent?.replace(/\s+/g, " ").trim();
822
984
  return text ? text.slice(0, 120) : void 0;
823
985
  }
986
+ function getStableAttributeValue(element, attributeName) {
987
+ const value = element.getAttribute(attributeName)?.trim();
988
+ if (!value || value.length > 160) return void 0;
989
+ if (/^(true|false)$/i.test(value)) return void 0;
990
+ if (/^\d+$/.test(value) && value.length < 3) return void 0;
991
+ return value;
992
+ }
824
993
  function getTextFingerprintScore(expected, actual) {
825
994
  if (!expected) return 1;
826
995
  if (!actual) return 0.5;
@@ -832,6 +1001,38 @@ function getTextFingerprintScore(expected, actual) {
832
1001
  const matches = expectedTokens.filter((token) => actualTokens.has(token));
833
1002
  return clamp(matches.length / expectedTokens.length, 0.25, 0.76);
834
1003
  }
1004
+ function getSelectionMatchScore(element, selection) {
1005
+ const rect = element.getBoundingClientRect();
1006
+ if (rect.width <= 0 || rect.height <= 0) return 0.05;
1007
+ const overlapLeft = Math.max(rect.left, selection.left);
1008
+ const overlapTop = Math.max(rect.top, selection.top);
1009
+ const overlapRight = Math.min(rect.right, selection.left + selection.width);
1010
+ const overlapBottom = Math.min(rect.bottom, selection.top + selection.height);
1011
+ const overlapWidth = Math.max(0, overlapRight - overlapLeft);
1012
+ const overlapHeight = Math.max(0, overlapBottom - overlapTop);
1013
+ const overlapArea = overlapWidth * overlapHeight;
1014
+ if (overlapArea > 0) {
1015
+ const selectionArea = Math.max(1, selection.width * selection.height);
1016
+ const rectArea = Math.max(1, rect.width * rect.height);
1017
+ return 1 + overlapArea / Math.min(selectionArea, rectArea);
1018
+ }
1019
+ const rectCenterX = rect.left + rect.width / 2;
1020
+ const rectCenterY = rect.top + rect.height / 2;
1021
+ const selectionCenterX = selection.left + selection.width / 2;
1022
+ const selectionCenterY = selection.top + selection.height / 2;
1023
+ const distance = Math.hypot(
1024
+ rectCenterX - selectionCenterX,
1025
+ rectCenterY - selectionCenterY
1026
+ );
1027
+ const basis = Math.max(
1028
+ 1,
1029
+ rect.width,
1030
+ rect.height,
1031
+ selection.width,
1032
+ selection.height
1033
+ );
1034
+ return clamp(1 / (1 + distance / basis), 0.05, 0.95);
1035
+ }
835
1036
  function getFingerprintTokens(value) {
836
1037
  return value.toLowerCase().split(/[\s/|,.:;()[\]{}"'`~!?<>]+/).map((token) => token.trim()).filter((token) => token.length > 1);
837
1038
  }
@@ -1292,6 +1493,19 @@ function createStyleElement() {
1292
1493
  display: block;
1293
1494
  }
1294
1495
 
1496
+ .dfwr-shell.has-dismissible-draft {
1497
+ z-index: 900;
1498
+ }
1499
+
1500
+ .dfwr-draft-cancel-layer {
1501
+ position: fixed;
1502
+ inset: 0;
1503
+ z-index: 2;
1504
+ pointer-events: auto;
1505
+ background: transparent;
1506
+ cursor: default;
1507
+ }
1508
+
1295
1509
  .dfwr-panel {
1296
1510
  position: fixed;
1297
1511
  right: 16px;
@@ -1785,6 +1999,40 @@ function createStyleElement() {
1785
1999
  box-shadow: var(--df-review-shadow-popover);
1786
2000
  }
1787
2001
 
2002
+ .dfwr-note-popover.is-composer,
2003
+ .dfwr-area-draft.is-composer {
2004
+ max-height: min(360px, calc(100vh - 32px));
2005
+ overflow: auto;
2006
+ border-color: rgba(99, 215, 199, 0.56);
2007
+ }
2008
+
2009
+ .dfwr-note-popover.is-dragging,
2010
+ .dfwr-area-draft.is-dragging {
2011
+ user-select: none;
2012
+ }
2013
+
2014
+ .dfwr-draft-drag-handle {
2015
+ display: block;
2016
+ width: 42px;
2017
+ height: 6px;
2018
+ margin: 0 auto 10px;
2019
+ padding: 0;
2020
+ cursor: grab;
2021
+ pointer-events: auto;
2022
+ background: rgba(247, 247, 242, 0.28);
2023
+ border: 0;
2024
+ border-radius: 999px;
2025
+ }
2026
+
2027
+ .dfwr-draft-drag-handle:hover,
2028
+ .dfwr-draft-drag-handle:focus-visible {
2029
+ background: rgba(215, 255, 95, 0.62);
2030
+ }
2031
+
2032
+ .dfwr-draft-drag-handle:active {
2033
+ cursor: grabbing;
2034
+ }
2035
+
1788
2036
  .dfwr-area-draft {
1789
2037
  position: fixed;
1790
2038
  right: 16px;
@@ -1806,6 +2054,14 @@ function createStyleElement() {
1806
2054
  padding: 0;
1807
2055
  }
1808
2056
 
2057
+ .dfwr-note-actions {
2058
+ justify-content: flex-end;
2059
+ }
2060
+
2061
+ .dfwr-note-actions .dfwr-button:first-child {
2062
+ margin-right: auto;
2063
+ }
2064
+
1809
2065
  .dfwr-area-draft .dfwr-actions {
1810
2066
  padding: 0;
1811
2067
  }
@@ -1834,6 +2090,79 @@ function createStyleElement() {
1834
2090
  outline-offset: 1px;
1835
2091
  }
1836
2092
 
2093
+ .dfwr-adjust-panel {
2094
+ display: grid;
2095
+ gap: 4px;
2096
+ padding: 8px 10px;
2097
+ border: 1px solid rgba(255, 255, 255, 0.12);
2098
+ border-radius: var(--df-review-radius-sm);
2099
+ background: rgba(255, 255, 255, 0.04);
2100
+ }
2101
+
2102
+ .dfwr-adjust-panel-header {
2103
+ display: flex;
2104
+ align-items: center;
2105
+ justify-content: space-between;
2106
+ gap: 10px;
2107
+ min-width: 0;
2108
+ }
2109
+
2110
+ .dfwr-adjust-panel-header .dfwr-adjust-help {
2111
+ flex: 1 1 auto;
2112
+ min-width: 0;
2113
+ }
2114
+
2115
+ .dfwr-adjust-panel.is-active {
2116
+ border-color: rgba(215, 255, 95, 0.5);
2117
+ background: var(--df-review-color-accent-soft);
2118
+ }
2119
+
2120
+ .dfwr-adjust-help,
2121
+ .dfwr-adjust-status {
2122
+ margin: 0;
2123
+ color: var(--df-review-color-text-muted);
2124
+ font-size: var(--df-review-font-size-xs);
2125
+ line-height: 1.35;
2126
+ }
2127
+
2128
+ .dfwr-adjust-status {
2129
+ color: var(--df-review-color-text);
2130
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2131
+ }
2132
+
2133
+ .dfwr-adjust-toggle {
2134
+ flex: 0 0 auto;
2135
+ display: inline-flex;
2136
+ align-items: center;
2137
+ justify-content: center;
2138
+ width: 34px;
2139
+ height: 30px;
2140
+ padding: 0;
2141
+ border: 1px solid rgba(255, 255, 255, 0.2);
2142
+ border-radius: var(--df-review-radius-sm);
2143
+ background: rgba(255, 255, 255, 0.04);
2144
+ color: var(--df-review-color-text);
2145
+ cursor: pointer;
2146
+ font: inherit;
2147
+ font-size: 14px;
2148
+ font-weight: 800;
2149
+ line-height: 1;
2150
+ }
2151
+
2152
+ .dfwr-adjust-toggle:hover,
2153
+ .dfwr-adjust-toggle:focus-visible,
2154
+ .dfwr-adjust-toggle.is-active {
2155
+ border-color: rgba(215, 255, 95, 0.68);
2156
+ background: var(--df-review-color-accent-soft);
2157
+ outline: none;
2158
+ }
2159
+
2160
+ .dfwr-adjust-toggle svg {
2161
+ width: 18px;
2162
+ height: 18px;
2163
+ pointer-events: none;
2164
+ }
2165
+
1837
2166
  .dfwr-empty,
1838
2167
  .dfwr-error {
1839
2168
  margin: 0;
@@ -2054,16 +2383,6 @@ function createStyleElement() {
2054
2383
  }
2055
2384
 
2056
2385
  // src/core/review/format.ts
2057
- function formatAreaDraftMeta(draft) {
2058
- const parts = [`viewport ${formatSize(draft.viewport)}`];
2059
- if (draft.selection) {
2060
- parts.push(`rect ${formatSelection(draft.selection.viewport)}`);
2061
- }
2062
- if (draft.marker) {
2063
- parts.push(`point ${formatPoint(draft.marker.viewport)}`);
2064
- }
2065
- return parts.join(" / ");
2066
- }
2067
2386
  function formatNoteDraftMeta(draft) {
2068
2387
  const parts = [
2069
2388
  `viewport ${formatSize(draft.viewport)}`,
@@ -2129,17 +2448,29 @@ function formatAnchorMeta(anchor) {
2129
2448
  }
2130
2449
 
2131
2450
  // src/core/web.review.kit.view.ts
2451
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
2132
2452
  var WebReviewKitView = class {
2133
2453
  constructor(config) {
2134
2454
  this.config = config;
2135
2455
  }
2456
+ clearDraftPreview() {
2457
+ this.restoreDraftPreview();
2458
+ }
2136
2459
  render(shadow, hiddenItemsStyle) {
2137
2460
  const state = this.state;
2461
+ this.syncDraftPreview(
2462
+ state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2463
+ );
2138
2464
  shadow.replaceChildren();
2139
2465
  shadow.append(createStyleElement());
2140
2466
  shadow.append(hiddenItemsStyle);
2467
+ const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
2141
2468
  const shell = document.createElement("div");
2142
- shell.className = `dfwr-shell${state.isOpen ? " is-open" : ""}`;
2469
+ shell.className = [
2470
+ "dfwr-shell",
2471
+ state.isOpen ? "is-open" : "",
2472
+ hasDismissableDraft ? "has-dismissible-draft" : ""
2473
+ ].filter(Boolean).join(" ");
2143
2474
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
2144
2475
  if (this.config.options.ui?.panel !== false) {
2145
2476
  const panel = document.createElement("div");
@@ -2155,6 +2486,9 @@ var WebReviewKitView = class {
2155
2486
  shell.append(panel);
2156
2487
  }
2157
2488
  shell.append(this.createMarkerLayer());
2489
+ if (state.isOpen && hasDismissableDraft) {
2490
+ shell.append(this.createDraftCancelLayer());
2491
+ }
2158
2492
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
2159
2493
  shell.append(
2160
2494
  state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
@@ -2174,6 +2508,332 @@ var WebReviewKitView = class {
2174
2508
  get state() {
2175
2509
  return this.config.getState();
2176
2510
  }
2511
+ createDraftCancelLayer() {
2512
+ const layer = document.createElement("div");
2513
+ layer.className = "dfwr-draft-cancel-layer";
2514
+ layer.setAttribute("aria-hidden", "true");
2515
+ const cancel = (event) => {
2516
+ event.preventDefault();
2517
+ event.stopPropagation();
2518
+ event.stopImmediatePropagation();
2519
+ this.config.actions.setModeState("idle");
2520
+ this.config.actions.clearDrafts();
2521
+ this.config.actions.setSelectingArea(false);
2522
+ this.config.actions.render();
2523
+ };
2524
+ layer.addEventListener("pointerdown", (event) => {
2525
+ if (event.button !== 0) return;
2526
+ cancel(event);
2527
+ });
2528
+ return layer;
2529
+ }
2530
+ getDraftAdjustmentMetrics(draft) {
2531
+ const adjustment = draft.adjustment;
2532
+ const x = adjustment?.x ?? 0;
2533
+ const y = adjustment?.y ?? 0;
2534
+ const scale = adjustment?.scale ?? 0;
2535
+ const {
2536
+ scale: viewportScale,
2537
+ designWidth,
2538
+ presetLabel
2539
+ } = this.getDraftViewportScale(draft.viewport);
2540
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2541
+ const scaleCssDelta = scale * viewportScale;
2542
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
2543
+ 1 / selection.width,
2544
+ (selection.width + scaleCssDelta) / selection.width
2545
+ ) : 1;
2546
+ return {
2547
+ x,
2548
+ y,
2549
+ scale,
2550
+ cssX: x * viewportScale,
2551
+ cssY: y * viewportScale,
2552
+ scaleFactor,
2553
+ viewportScale,
2554
+ designWidth,
2555
+ presetLabel,
2556
+ viewportWidth: draft.viewport.width
2557
+ };
2558
+ }
2559
+ hasDraftAdjustment(draft) {
2560
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2561
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
2562
+ }
2563
+ getAdjustedDraftPoint(point, draft) {
2564
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2565
+ return {
2566
+ x: point.x + metrics.cssX,
2567
+ y: point.y + metrics.cssY
2568
+ };
2569
+ }
2570
+ getAdjustedDraftSelection(selection, draft) {
2571
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2572
+ return {
2573
+ ...selection,
2574
+ left: selection.left + metrics.cssX,
2575
+ top: selection.top + metrics.cssY,
2576
+ width: selection.width * metrics.scaleFactor,
2577
+ height: selection.height * metrics.scaleFactor
2578
+ };
2579
+ }
2580
+ getDraftViewportScale(viewport) {
2581
+ const preset = findReviewViewportPreset(
2582
+ viewport,
2583
+ this.config.options.viewports?.presets
2584
+ );
2585
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2586
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2587
+ return { scale, designWidth, presetLabel: preset.label };
2588
+ }
2589
+ getDraftComposerWidth(environment) {
2590
+ const bounds = environment.overlayRect;
2591
+ const margin = 12;
2592
+ return Math.min(360, Math.max(240, bounds.width - margin * 2));
2593
+ }
2594
+ getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
2595
+ const margin = 12;
2596
+ const width = size?.width ?? this.getDraftComposerWidth(environment);
2597
+ const height = size?.height ?? 236;
2598
+ return {
2599
+ x: clamp(
2600
+ position.x,
2601
+ bounds.left + margin,
2602
+ bounds.left + bounds.width - width - margin
2603
+ ),
2604
+ y: clamp(
2605
+ position.y,
2606
+ bounds.top + margin,
2607
+ bounds.top + bounds.height - height - margin
2608
+ )
2609
+ };
2610
+ }
2611
+ getHostComposerBounds() {
2612
+ const root = document.documentElement;
2613
+ return {
2614
+ left: 0,
2615
+ top: 0,
2616
+ width: root.clientWidth || window.innerWidth,
2617
+ height: root.clientHeight || window.innerHeight
2618
+ };
2619
+ }
2620
+ getInitialDraftComposerPosition(selection, environment, size) {
2621
+ const bounds = this.getHostComposerBounds();
2622
+ const margin = 12;
2623
+ const gap = 20;
2624
+ if (!selection) {
2625
+ return this.getClampedComposerPosition(
2626
+ {
2627
+ x: environment.overlayRect.left + margin,
2628
+ y: environment.overlayRect.top + margin
2629
+ },
2630
+ environment,
2631
+ size,
2632
+ bounds
2633
+ );
2634
+ }
2635
+ const preferredX = selection.left + selection.width + gap;
2636
+ const maxX = bounds.left + bounds.width - size.width - margin;
2637
+ const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
2638
+ return this.getClampedComposerPosition(
2639
+ {
2640
+ x,
2641
+ y: selection.top
2642
+ },
2643
+ environment,
2644
+ size,
2645
+ bounds
2646
+ );
2647
+ }
2648
+ getDraftComposerPosition({
2649
+ selection,
2650
+ environment,
2651
+ composerPosition,
2652
+ estimatedHeight
2653
+ }) {
2654
+ const width = this.getDraftComposerWidth(environment);
2655
+ if (composerPosition) {
2656
+ const clamped = this.getClampedComposerPosition(
2657
+ composerPosition,
2658
+ environment,
2659
+ { width, height: estimatedHeight },
2660
+ this.getHostComposerBounds()
2661
+ );
2662
+ return { width, left: clamped.x, top: clamped.y };
2663
+ }
2664
+ const position = this.getInitialDraftComposerPosition(selection, environment, {
2665
+ width,
2666
+ height: estimatedHeight
2667
+ });
2668
+ return { width, left: position.x, top: position.y };
2669
+ }
2670
+ getSelectionMqMetrics(selection, viewport) {
2671
+ const { scale } = this.getDraftViewportScale(viewport);
2672
+ const ratio = scale > 0 ? 1 / scale : 1;
2673
+ return {
2674
+ x: selection.left * ratio,
2675
+ y: selection.top * ratio,
2676
+ width: selection.width * ratio,
2677
+ height: selection.height * ratio
2678
+ };
2679
+ }
2680
+ formatSignedPx(value) {
2681
+ if (value === 0) return "+0px";
2682
+ return `${value > 0 ? "+" : ""}${value}px`;
2683
+ }
2684
+ formatRoundedPx(value) {
2685
+ return `${Math.round(value)}px`;
2686
+ }
2687
+ getAdjustmentLabel() {
2688
+ return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
2689
+ }
2690
+ getSelectionMetricLines(selection, viewport) {
2691
+ if (!selection) return ["area", "x none / y none", "w none / h none"];
2692
+ const metrics = this.getSelectionMqMetrics(selection, viewport);
2693
+ return [
2694
+ "area",
2695
+ `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
2696
+ metrics.y
2697
+ )}`,
2698
+ `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
2699
+ metrics.height
2700
+ )}`
2701
+ ];
2702
+ }
2703
+ getAreaDraftMetricSelection(draft) {
2704
+ if (!draft.selection) return void 0;
2705
+ return toViewportSelection(draft.selection.viewport);
2706
+ }
2707
+ getDraftAdjustmentMetricLines(draft) {
2708
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2709
+ return [
2710
+ `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
2711
+ metrics.y
2712
+ )}`,
2713
+ `scale ${this.formatSignedPx(metrics.scale)}`
2714
+ ];
2715
+ }
2716
+ withDraftAdjustmentComment(comment, draft) {
2717
+ if (!this.hasDraftAdjustment(draft)) return comment;
2718
+ const trimmedComment = comment.trim();
2719
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2720
+ const adjustment = [
2721
+ `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
2722
+ metrics.x
2723
+ )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
2724
+ metrics.scale
2725
+ )}`,
2726
+ `(${metrics.presetLabel} viewport, ${Math.round(
2727
+ metrics.viewportWidth
2728
+ )}/design ${Math.round(metrics.designWidth)})`
2729
+ ].join(" ");
2730
+ return trimmedComment ? `${trimmedComment}
2731
+ ${adjustment}` : adjustment;
2732
+ }
2733
+ getStyleableDraftElement(draft, environment) {
2734
+ if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2735
+ return draft.previewElement;
2736
+ }
2737
+ if (!draft.anchor) return void 0;
2738
+ const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2739
+ const element = resolveAnchorElement(
2740
+ draft.anchor,
2741
+ environment,
2742
+ preferredSelection
2743
+ )?.element;
2744
+ if (!element) return void 0;
2745
+ if ("style" in element) return element;
2746
+ return void 0;
2747
+ }
2748
+ syncDraftPreview(draft) {
2749
+ const environment = this.config.getEnvironment();
2750
+ if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
2751
+ this.restoreDraftPreview();
2752
+ return;
2753
+ }
2754
+ const element = this.getStyleableDraftElement(draft, environment);
2755
+ if (!element) {
2756
+ this.restoreDraftPreview();
2757
+ return;
2758
+ }
2759
+ if (this.draftPreview?.element !== element) {
2760
+ this.restoreDraftPreview();
2761
+ }
2762
+ if (!this.draftPreview) {
2763
+ const computedStyle = environment.window.getComputedStyle(element);
2764
+ const clone = element.cloneNode(true);
2765
+ this.removeDuplicateIds(clone);
2766
+ this.copyComputedStyle(element, clone, environment);
2767
+ this.positionDraftPreviewClone(clone, element, computedStyle);
2768
+ environment.document.body?.appendChild(clone);
2769
+ this.draftPreview = {
2770
+ element,
2771
+ clone,
2772
+ visibility: element.style.visibility
2773
+ };
2774
+ element.style.visibility = "hidden";
2775
+ }
2776
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2777
+ const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
2778
+ metrics.cssY
2779
+ )}px)`;
2780
+ const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
2781
+ this.draftPreview.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
2782
+ }
2783
+ restoreDraftPreview() {
2784
+ if (!this.draftPreview) return;
2785
+ const { element, clone, visibility } = this.draftPreview;
2786
+ clone.remove();
2787
+ element.style.visibility = visibility;
2788
+ this.draftPreview = void 0;
2789
+ }
2790
+ positionDraftPreviewClone(clone, element, computedStyle) {
2791
+ const rect = element.getBoundingClientRect();
2792
+ clone.setAttribute("data-dfwr-adjust-preview", "true");
2793
+ clone.setAttribute("aria-hidden", "true");
2794
+ clone.style.position = "fixed";
2795
+ clone.style.left = `${this.toCssNumber(rect.left)}px`;
2796
+ clone.style.top = `${this.toCssNumber(rect.top)}px`;
2797
+ clone.style.right = "auto";
2798
+ clone.style.bottom = "auto";
2799
+ clone.style.width = `${this.toCssNumber(rect.width)}px`;
2800
+ clone.style.height = `${this.toCssNumber(rect.height)}px`;
2801
+ clone.style.maxWidth = "none";
2802
+ clone.style.maxHeight = "none";
2803
+ clone.style.margin = "0";
2804
+ clone.style.boxSizing = "border-box";
2805
+ clone.style.display = this.getDraftPreviewDisplay(computedStyle.display);
2806
+ clone.style.zIndex = "2147483646";
2807
+ clone.style.pointerEvents = "none";
2808
+ clone.style.transition = "none";
2809
+ clone.style.willChange = "transform";
2810
+ clone.style.transformOrigin = "top left";
2811
+ clone.style.transform = "none";
2812
+ }
2813
+ getDraftPreviewDisplay(display) {
2814
+ if (display === "inline" || display === "contents") return "inline-block";
2815
+ return display || "block";
2816
+ }
2817
+ copyComputedStyle(element, clone, environment) {
2818
+ const computedStyle = environment.window.getComputedStyle(element);
2819
+ for (let index = 0; index < computedStyle.length; index += 1) {
2820
+ const property = computedStyle.item(index);
2821
+ clone.style.setProperty(
2822
+ property,
2823
+ computedStyle.getPropertyValue(property),
2824
+ computedStyle.getPropertyPriority(property)
2825
+ );
2826
+ }
2827
+ }
2828
+ removeDuplicateIds(element) {
2829
+ element.removeAttribute("id");
2830
+ element.querySelectorAll("[id]").forEach((child) => {
2831
+ child.removeAttribute("id");
2832
+ });
2833
+ }
2834
+ toCssNumber(value) {
2835
+ return Math.round(value * 1e3) / 1e3;
2836
+ }
2177
2837
  createHeader() {
2178
2838
  const header = document.createElement("div");
2179
2839
  header.className = "dfwr-header";
@@ -2265,15 +2925,20 @@ var WebReviewKitView = class {
2265
2925
  const group = document.createElement("div");
2266
2926
  group.className = "dfwr-note-draft";
2267
2927
  if (!environment) return group;
2268
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
2928
+ const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
2929
+ const hostPoint = toHostPoint(
2930
+ isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
2931
+ environment
2932
+ );
2933
+ let selectionHighlight;
2269
2934
  if (draft.selection) {
2270
- group.append(
2271
- this.createSelectionHighlight(
2272
- toViewportSelection(draft.selection.viewport),
2273
- environment,
2274
- true
2275
- )
2935
+ const selection = toViewportSelection(draft.selection.viewport);
2936
+ selectionHighlight = this.createSelectionHighlight(
2937
+ isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2938
+ environment,
2939
+ true
2276
2940
  );
2941
+ group.append(selectionHighlight);
2277
2942
  }
2278
2943
  const pin = document.createElement("button");
2279
2944
  pin.className = "dfwr-note-pin";
@@ -2283,14 +2948,35 @@ var WebReviewKitView = class {
2283
2948
  pin.style.top = `${hostPoint.y}px`;
2284
2949
  const popover = document.createElement("div");
2285
2950
  const position = getPopoverPosition(hostPoint, environment);
2286
- popover.className = "dfwr-note-popover";
2287
- popover.style.left = `${position.left}px`;
2288
- popover.style.top = `${position.top}px`;
2951
+ popover.className = `dfwr-note-popover${isElementDraft ? " is-composer" : ""}`;
2952
+ if (isElementDraft) {
2953
+ const selection = draft.selection ? toHostSelection(
2954
+ this.getAdjustedDraftSelection(
2955
+ toViewportSelection(draft.selection.viewport),
2956
+ draft
2957
+ ),
2958
+ environment
2959
+ ) : void 0;
2960
+ const composer = this.getDraftComposerPosition({
2961
+ selection,
2962
+ environment,
2963
+ composerPosition: draft.composerPosition,
2964
+ estimatedHeight: 252
2965
+ });
2966
+ popover.style.left = `${composer.left}px`;
2967
+ popover.style.top = `${composer.top}px`;
2968
+ popover.style.width = `${composer.width}px`;
2969
+ } else {
2970
+ popover.style.left = `${position.left}px`;
2971
+ popover.style.top = `${position.top}px`;
2972
+ }
2289
2973
  const form = document.createElement("form");
2290
2974
  form.className = "dfwr-form";
2291
- const meta = document.createElement("div");
2292
- meta.className = "dfwr-item-date";
2293
- meta.textContent = formatNoteDraftMeta(draft);
2975
+ const meta = isElementDraft ? void 0 : document.createElement("div");
2976
+ if (meta) {
2977
+ meta.className = "dfwr-item-date";
2978
+ meta.textContent = formatNoteDraftMeta(draft);
2979
+ }
2294
2980
  const textarea = document.createElement("textarea");
2295
2981
  textarea.className = "dfwr-textarea";
2296
2982
  textarea.placeholder = "Review comment";
@@ -2304,25 +2990,273 @@ var WebReviewKitView = class {
2304
2990
  comment: textarea.value
2305
2991
  });
2306
2992
  });
2307
- const actions = this.createFormActions("Save note", () => {
2993
+ const saveDraft = () => {
2308
2994
  const comment = textarea.value.trim();
2309
- if (!comment) return;
2995
+ const currentDraft = this.state.noteDraft ?? draft;
2996
+ if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
2310
2997
  void this.config.actions.createItem({
2311
2998
  kind: "note",
2312
- comment,
2313
- viewport: draft.viewport,
2314
- anchor: draft.anchor,
2315
- marker: draft.marker,
2316
- selection: draft.selection
2999
+ comment: this.withDraftAdjustmentComment(comment, currentDraft),
3000
+ viewport: currentDraft.viewport,
3001
+ anchor: currentDraft.anchor,
3002
+ marker: currentDraft.marker,
3003
+ selection: currentDraft.selection
2317
3004
  });
2318
- });
2319
- form.append(meta, textarea, actions);
2320
- popover.append(form);
3005
+ };
3006
+ const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
3007
+ draft,
3008
+ pin,
3009
+ popover,
3010
+ selectionHighlight,
3011
+ textarea
3012
+ }) : void 0;
3013
+ const actions = this.createFormActions("Save note", saveDraft);
3014
+ form.append(
3015
+ ...meta ? [meta] : [],
3016
+ ...adjustmentControls ? [adjustmentControls.panel] : [],
3017
+ textarea,
3018
+ actions
3019
+ );
3020
+ const dragHandle = isElementDraft ? this.createDraftDragHandle("Move DOM composer") : void 0;
3021
+ popover.append(...dragHandle ? [dragHandle] : [], form);
2321
3022
  group.append(pin, popover);
2322
- this.attachDraftPinDrag(pin, popover, meta, textarea);
2323
- window.setTimeout(() => textarea.focus(), 0);
3023
+ if (dragHandle) {
3024
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3025
+ const noteDraft = this.state.noteDraft ?? draft;
3026
+ this.config.actions.setNoteDraft({
3027
+ ...noteDraft,
3028
+ composerPosition,
3029
+ comment: textarea.value
3030
+ });
3031
+ });
3032
+ }
3033
+ this.attachDraftPinDrag(
3034
+ pin,
3035
+ isElementDraft ? void 0 : popover,
3036
+ meta,
3037
+ textarea
3038
+ );
3039
+ window.setTimeout(() => {
3040
+ if (draft.adjustment?.isActive) {
3041
+ adjustmentControls?.focusTarget.focus();
3042
+ return;
3043
+ }
3044
+ textarea.focus();
3045
+ }, 0);
2324
3046
  return group;
2325
3047
  }
3048
+ createDraftDragHandle(label) {
3049
+ const handle = document.createElement("button");
3050
+ handle.className = "dfwr-draft-drag-handle";
3051
+ handle.type = "button";
3052
+ handle.setAttribute("aria-label", label);
3053
+ return handle;
3054
+ }
3055
+ createIcon(paths) {
3056
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3057
+ svg.setAttribute("aria-hidden", "true");
3058
+ svg.setAttribute("viewBox", "0 0 24 24");
3059
+ svg.setAttribute("fill", "none");
3060
+ svg.setAttribute("stroke", "currentColor");
3061
+ svg.setAttribute("stroke-width", "2.4");
3062
+ svg.setAttribute("stroke-linecap", "round");
3063
+ svg.setAttribute("stroke-linejoin", "round");
3064
+ paths.forEach((d) => {
3065
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3066
+ path.setAttribute("d", d);
3067
+ svg.append(path);
3068
+ });
3069
+ return svg;
3070
+ }
3071
+ setAdjustmentToggleIcon(button, isActive) {
3072
+ const paths = isActive ? ["M20 6 9 17l-5-5"] : [
3073
+ "M12 2v20",
3074
+ "M2 12h20",
3075
+ "m9 5 3-3 3 3",
3076
+ "m9 19 3 3 3-3",
3077
+ "m5 9-3 3 3 3",
3078
+ "m19 9 3 3-3 3"
3079
+ ];
3080
+ button.replaceChildren(this.createIcon(paths));
3081
+ }
3082
+ attachDraftComposerDrag(popover, handle, onMove) {
3083
+ let isDragging = false;
3084
+ let offsetX = 0;
3085
+ let offsetY = 0;
3086
+ const movePopover = (event) => {
3087
+ const environment = this.config.getEnvironment();
3088
+ if (!environment) return;
3089
+ const position = this.getClampedComposerPosition(
3090
+ {
3091
+ x: event.clientX - offsetX,
3092
+ y: event.clientY - offsetY
3093
+ },
3094
+ environment,
3095
+ {
3096
+ width: popover.offsetWidth,
3097
+ height: popover.offsetHeight
3098
+ },
3099
+ this.getHostComposerBounds()
3100
+ );
3101
+ popover.style.left = `${position.x}px`;
3102
+ popover.style.top = `${position.y}px`;
3103
+ onMove(position);
3104
+ };
3105
+ handle.addEventListener("pointerdown", (event) => {
3106
+ if (event.button !== 0) return;
3107
+ const rect = popover.getBoundingClientRect();
3108
+ offsetX = event.clientX - rect.left;
3109
+ offsetY = event.clientY - rect.top;
3110
+ isDragging = true;
3111
+ event.preventDefault();
3112
+ event.stopPropagation();
3113
+ handle.setPointerCapture(event.pointerId);
3114
+ popover.classList.add("is-dragging");
3115
+ });
3116
+ handle.addEventListener("pointermove", (event) => {
3117
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3118
+ event.preventDefault();
3119
+ movePopover(event);
3120
+ });
3121
+ const stopDrag = (event) => {
3122
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3123
+ event.preventDefault();
3124
+ event.stopPropagation();
3125
+ isDragging = false;
3126
+ handle.releasePointerCapture(event.pointerId);
3127
+ popover.classList.remove("is-dragging");
3128
+ movePopover(event);
3129
+ };
3130
+ handle.addEventListener("pointerup", stopDrag);
3131
+ handle.addEventListener("pointercancel", stopDrag);
3132
+ }
3133
+ createAdjustmentControls({
3134
+ draft,
3135
+ pin,
3136
+ popover,
3137
+ selectionHighlight,
3138
+ textarea
3139
+ }) {
3140
+ const panel = document.createElement("div");
3141
+ panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
3142
+ const header = document.createElement("div");
3143
+ header.className = "dfwr-adjust-panel-header";
3144
+ const help = document.createElement("div");
3145
+ help.className = "dfwr-adjust-help";
3146
+ help.textContent = this.getAdjustmentLabel();
3147
+ const adjust = document.createElement("button");
3148
+ adjust.className = "dfwr-adjust-toggle";
3149
+ adjust.type = "button";
3150
+ adjust.title = "Adjust DOM element with keyboard arrows";
3151
+ adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
3152
+ const xyStatus = document.createElement("div");
3153
+ xyStatus.className = "dfwr-adjust-status";
3154
+ const scaleStatus = document.createElement("div");
3155
+ scaleStatus.className = "dfwr-adjust-status";
3156
+ const syncControls = (nextDraft) => {
3157
+ const isActive = nextDraft.adjustment?.isActive === true;
3158
+ panel.classList.toggle("is-active", isActive);
3159
+ adjust.classList.toggle("is-active", isActive);
3160
+ adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
3161
+ this.setAdjustmentToggleIcon(adjust, isActive);
3162
+ adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
3163
+ adjust.setAttribute(
3164
+ "aria-label",
3165
+ isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
3166
+ );
3167
+ const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
3168
+ xyStatus.textContent = xyLine;
3169
+ scaleStatus.textContent = scaleLine;
3170
+ this.syncDraftAdjustmentUi({
3171
+ draft: nextDraft,
3172
+ pin,
3173
+ selectionHighlight
3174
+ });
3175
+ };
3176
+ const updateDraft = (updater) => {
3177
+ const currentDraft = this.state.noteDraft ?? draft;
3178
+ const nextDraft = updater(currentDraft);
3179
+ this.config.actions.setNoteDraft({
3180
+ ...nextDraft,
3181
+ comment: textarea.value
3182
+ });
3183
+ syncControls(nextDraft);
3184
+ };
3185
+ adjust.addEventListener("click", () => {
3186
+ updateDraft((currentDraft) => ({
3187
+ ...currentDraft,
3188
+ adjustment: {
3189
+ x: currentDraft.adjustment?.x ?? 0,
3190
+ y: currentDraft.adjustment?.y ?? 0,
3191
+ scale: currentDraft.adjustment?.scale ?? 0,
3192
+ isActive: currentDraft.adjustment?.isActive !== true
3193
+ }
3194
+ }));
3195
+ adjust.focus();
3196
+ });
3197
+ popover.addEventListener("keydown", (event) => {
3198
+ const currentDraft = this.state.noteDraft ?? draft;
3199
+ if (currentDraft.adjustment?.isActive !== true) return;
3200
+ const keyDelta = this.getAdjustmentKeyDelta(event);
3201
+ if (!keyDelta) return;
3202
+ event.preventDefault();
3203
+ event.stopPropagation();
3204
+ updateDraft((activeDraft) => ({
3205
+ ...activeDraft,
3206
+ adjustment: {
3207
+ x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
3208
+ y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
3209
+ scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
3210
+ isActive: true
3211
+ }
3212
+ }));
3213
+ });
3214
+ header.append(help, adjust);
3215
+ panel.append(header, xyStatus, scaleStatus);
3216
+ syncControls(draft);
3217
+ return {
3218
+ panel,
3219
+ focusTarget: adjust
3220
+ };
3221
+ }
3222
+ getAdjustmentKeyDelta(event) {
3223
+ const step = event.shiftKey ? 10 : 1;
3224
+ if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
3225
+ if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
3226
+ if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
3227
+ if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
3228
+ if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
3229
+ if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
3230
+ return void 0;
3231
+ }
3232
+ syncDraftAdjustmentUi({
3233
+ draft,
3234
+ pin,
3235
+ selectionHighlight
3236
+ }) {
3237
+ const environment = this.config.getEnvironment();
3238
+ if (!environment) return;
3239
+ const hostPoint = toHostPoint(
3240
+ this.getAdjustedDraftPoint(draft.marker.viewport, draft),
3241
+ environment
3242
+ );
3243
+ pin.style.left = `${hostPoint.x}px`;
3244
+ pin.style.top = `${hostPoint.y}px`;
3245
+ if (draft.selection && selectionHighlight) {
3246
+ const rect = toHostSelection(
3247
+ this.getAdjustedDraftSelection(
3248
+ toViewportSelection(draft.selection.viewport),
3249
+ draft
3250
+ ),
3251
+ environment
3252
+ );
3253
+ selectionHighlight.style.left = `${rect.left}px`;
3254
+ selectionHighlight.style.top = `${rect.top}px`;
3255
+ selectionHighlight.style.width = `${rect.width}px`;
3256
+ selectionHighlight.style.height = `${rect.height}px`;
3257
+ }
3258
+ this.syncDraftPreview(draft);
3259
+ }
2326
3260
  createAreaForm() {
2327
3261
  const form = document.createElement("form");
2328
3262
  form.className = "dfwr-form";
@@ -2334,14 +3268,20 @@ var WebReviewKitView = class {
2334
3268
  form.append(empty);
2335
3269
  return form;
2336
3270
  }
2337
- const meta = document.createElement("div");
2338
- meta.className = "dfwr-item-date";
2339
- meta.textContent = formatAreaDraftMeta(areaDraft);
2340
- form.append(meta);
3271
+ form.append(this.createAreaMetricsPanel(areaDraft));
2341
3272
  const textarea = document.createElement("textarea");
2342
3273
  textarea.className = "dfwr-textarea";
2343
3274
  textarea.placeholder = "Area comment";
2344
3275
  textarea.rows = 4;
3276
+ textarea.value = areaDraft.comment ?? "";
3277
+ textarea.addEventListener("input", () => {
3278
+ const draft = this.state.areaDraft;
3279
+ if (!draft) return;
3280
+ this.config.actions.setAreaDraft({
3281
+ ...draft,
3282
+ comment: textarea.value
3283
+ });
3284
+ });
2345
3285
  const actions = this.createFormActions("Save area", () => {
2346
3286
  const comment = textarea.value.trim();
2347
3287
  const draft = this.state.areaDraft;
@@ -2358,6 +3298,25 @@ var WebReviewKitView = class {
2358
3298
  form.append(textarea, actions);
2359
3299
  return form;
2360
3300
  }
3301
+ createAreaMetricsPanel(draft) {
3302
+ const panel = document.createElement("div");
3303
+ panel.className = "dfwr-adjust-panel is-area-metrics-panel";
3304
+ const help = document.createElement("div");
3305
+ help.className = "dfwr-adjust-help";
3306
+ const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
3307
+ this.getAreaDraftMetricSelection(draft),
3308
+ draft.viewport
3309
+ );
3310
+ help.textContent = labelLine;
3311
+ const xyStatus = document.createElement("div");
3312
+ xyStatus.className = "dfwr-adjust-status";
3313
+ xyStatus.textContent = xyLine;
3314
+ const sizeStatus = document.createElement("div");
3315
+ sizeStatus.className = "dfwr-adjust-status";
3316
+ sizeStatus.textContent = sizeLine;
3317
+ panel.append(help, xyStatus, sizeStatus);
3318
+ return panel;
3319
+ }
2361
3320
  createAreaDraftOverlay(draft) {
2362
3321
  const layer = document.createElement("div");
2363
3322
  layer.className = "dfwr-area-preview-layer";
@@ -2386,37 +3345,61 @@ var WebReviewKitView = class {
2386
3345
  createAreaDraftPopover(draft) {
2387
3346
  const environment = this.config.getEnvironment();
2388
3347
  const popover = document.createElement("div");
2389
- popover.className = "dfwr-area-draft";
3348
+ popover.className = "dfwr-area-draft is-composer";
2390
3349
  if (environment && draft.selection) {
2391
3350
  const selection = toHostSelection(
2392
3351
  toViewportSelection(draft.selection.viewport),
2393
3352
  environment
2394
3353
  );
2395
- const position = getAreaPopoverPosition(selection, environment);
2396
- popover.style.left = `${position.left}px`;
2397
- popover.style.top = `${position.top}px`;
3354
+ const composer = this.getDraftComposerPosition({
3355
+ selection,
3356
+ environment,
3357
+ composerPosition: draft.composerPosition,
3358
+ estimatedHeight: 220
3359
+ });
3360
+ popover.style.left = `${composer.left}px`;
3361
+ popover.style.top = `${composer.top}px`;
3362
+ popover.style.width = `${composer.width}px`;
2398
3363
  popover.style.right = "auto";
2399
3364
  }
2400
- popover.append(this.createAreaForm());
3365
+ const dragHandle = this.createDraftDragHandle("Move area composer");
3366
+ popover.append(dragHandle, this.createAreaForm());
3367
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3368
+ const areaDraft = this.state.areaDraft ?? draft;
3369
+ this.config.actions.setAreaDraft({
3370
+ ...areaDraft,
3371
+ composerPosition
3372
+ });
3373
+ });
2401
3374
  return popover;
2402
3375
  }
2403
- createFormActions(saveLabel, onSave) {
3376
+ createFormActions(saveLabel, onSave, options) {
2404
3377
  const actions = document.createElement("div");
2405
- actions.className = "dfwr-actions";
3378
+ actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
2406
3379
  const save = document.createElement("button");
2407
3380
  save.className = "dfwr-button is-primary";
2408
3381
  save.type = "button";
2409
3382
  save.textContent = saveLabel;
2410
- save.addEventListener("click", onSave);
3383
+ save.addEventListener("click", (event) => {
3384
+ event.preventDefault();
3385
+ event.stopPropagation();
3386
+ onSave();
3387
+ });
2411
3388
  const cancel = document.createElement("button");
2412
3389
  cancel.className = "dfwr-button";
2413
3390
  cancel.type = "button";
2414
3391
  cancel.textContent = "Cancel";
2415
- cancel.addEventListener("click", () => {
3392
+ cancel.addEventListener("click", (event) => {
3393
+ event.preventDefault();
3394
+ event.stopPropagation();
2416
3395
  this.config.actions.setModeState("idle");
2417
3396
  this.config.actions.clearDrafts();
2418
3397
  this.config.actions.render();
2419
3398
  });
3399
+ if (options?.beforeSave?.length || options?.className) {
3400
+ actions.append(cancel, ...options.beforeSave ?? [], save);
3401
+ return actions;
3402
+ }
2420
3403
  actions.append(save, cancel);
2421
3404
  return actions;
2422
3405
  }
@@ -2621,11 +3604,13 @@ ${formatItemMeta(item)}`;
2621
3604
  if (!environment) return;
2622
3605
  const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
2623
3606
  const nextHostPoint = toHostPoint(nextPoint, environment);
2624
- const position = getPopoverPosition(nextHostPoint, environment);
2625
3607
  pin.style.left = `${nextHostPoint.x}px`;
2626
3608
  pin.style.top = `${nextHostPoint.y}px`;
2627
- popover.style.left = `${position.left}px`;
2628
- popover.style.top = `${position.top}px`;
3609
+ if (popover) {
3610
+ const position = getPopoverPosition(nextHostPoint, environment);
3611
+ popover.style.left = `${position.left}px`;
3612
+ popover.style.top = `${position.top}px`;
3613
+ }
2629
3614
  const noteDraft = this.state.noteDraft;
2630
3615
  if (!noteDraft) return;
2631
3616
  const nextDraft = {
@@ -2637,7 +3622,9 @@ ${formatItemMeta(item)}`;
2637
3622
  comment: textarea.value
2638
3623
  };
2639
3624
  this.config.actions.setNoteDraft(nextDraft);
2640
- meta.textContent = formatNoteDraftMeta(nextDraft);
3625
+ if (meta) {
3626
+ meta.textContent = formatNoteDraftMeta(nextDraft);
3627
+ }
2641
3628
  };
2642
3629
  pin.addEventListener("pointerdown", (event) => {
2643
3630
  if (event.button !== 0) return;
@@ -2870,6 +3857,9 @@ var WebReviewKitApp = class {
2870
3857
  setNoteDraft: (draft) => {
2871
3858
  this.noteDraft = draft;
2872
3859
  },
3860
+ setAreaDraft: (draft) => {
3861
+ this.areaDraft = draft;
3862
+ },
2873
3863
  setSelectingArea: (isSelectingArea) => {
2874
3864
  this.isSelectingArea = isSelectingArea;
2875
3865
  },
@@ -2895,6 +3885,7 @@ var WebReviewKitApp = class {
2895
3885
  this.render();
2896
3886
  }
2897
3887
  destroy() {
3888
+ this.view.clearDraftPreview();
2898
3889
  document.removeEventListener("keydown", this.handleKeyDown, true);
2899
3890
  window.removeEventListener("scroll", this.handleViewportChange, true);
2900
3891
  window.removeEventListener("resize", this.handleViewportChange);
@@ -3098,14 +4089,27 @@ var WebReviewKitApp = class {
3098
4089
  const viewport = getViewportSize(environment);
3099
4090
  const nextPoint = clampPoint(point, environment);
3100
4091
  const draft = await this.withOverlayHidden(() => {
4092
+ const pointSelection = getPointSelection(nextPoint);
4093
+ const targetElement = environment.document.elementFromPoint(
4094
+ nextPoint.x,
4095
+ nextPoint.y
4096
+ );
4097
+ const previewElement = targetElement && "style" in targetElement ? targetElement : void 0;
4098
+ const targetRect = targetElement?.getBoundingClientRect();
4099
+ const clickedSelection = targetRect && targetRect.width > 0 && targetRect.height > 0 ? {
4100
+ left: targetRect.left,
4101
+ top: targetRect.top,
4102
+ width: targetRect.width,
4103
+ height: targetRect.height
4104
+ } : void 0;
3101
4105
  const anchor = getDomAnchorFromPoint(
3102
4106
  nextPoint,
3103
4107
  this.options.anchors?.attribute,
3104
4108
  environment
3105
4109
  );
3106
- const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
3107
- const selection = elementSelection ?? getPointSelection(nextPoint);
3108
- const markerPoint = getSelectionCenter(selection);
4110
+ const elementSelection = anchor ? clickedSelection ?? getElementViewportSelection(anchor, environment, pointSelection) : void 0;
4111
+ const selection = elementSelection ?? pointSelection;
4112
+ const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
3109
4113
  const reviewSelection = elementSelection ? {
3110
4114
  viewport: toPublicSelection(elementSelection),
3111
4115
  relative: getRelativeSelection(
@@ -3123,7 +4127,8 @@ var WebReviewKitApp = class {
3123
4127
  anchor,
3124
4128
  marker,
3125
4129
  selection: reviewSelection,
3126
- comment
4130
+ comment,
4131
+ previewElement
3127
4132
  };
3128
4133
  });
3129
4134
  this.noteDraft = draft;