@designfever/web-review-kit 0.3.0 → 0.4.0

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;
@@ -625,46 +627,66 @@ function getAnchorElement(anchor, environment) {
625
627
  return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment)?.element;
626
628
  }
627
629
  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
- }
630
+ const targetCandidates = [];
631
+ const configuredAnchor = getExactAttributeAnchorCandidate(
632
+ target,
633
+ configuredAttribute,
634
+ 0.98,
635
+ "configured-attribute"
636
+ );
637
+ if (configuredAnchor) targetCandidates.push(configuredAnchor);
638
+ const targetAttributeAnchor = getAttributeAnchorCandidate(
639
+ target,
640
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
641
+ 0.9
642
+ );
643
+ if (targetAttributeAnchor) targetCandidates.push(targetAttributeAnchor);
641
644
  if (isMeaningfulId(target.id)) {
642
- candidates.push({
645
+ targetCandidates.push({
643
646
  selector: `#${cssEscape(target.id)}`,
644
647
  strategy: "id",
645
648
  confidence: 0.94,
646
649
  textFingerprint: getTextFingerprint(target)
647
650
  });
648
651
  }
652
+ const semanticAnchor = getAttributeAnchorCandidate(
653
+ target,
654
+ SEMANTIC_ANCHOR_ATTRIBUTES,
655
+ 0.84
656
+ );
657
+ if (semanticAnchor) targetCandidates.push(semanticAnchor);
649
658
  const targetClassName = getMeaningfulClassName(target);
650
659
  if (targetClassName) {
651
- candidates.push({
660
+ targetCandidates.push({
652
661
  selector: `${target.tagName.toLowerCase()}.${cssEscape(targetClassName)}`,
653
662
  strategy: "class",
654
663
  confidence: 0.82,
655
664
  textFingerprint: getTextFingerprint(target)
656
665
  });
657
666
  }
658
- candidates.push({
667
+ const scopedPath = getScopedDomPathCandidate(target, configuredAttribute);
668
+ if (scopedPath) targetCandidates.push(scopedPath);
669
+ const targetDomPath = {
659
670
  selector: getDomPath(target),
660
671
  strategy: "dom-path",
661
- confidence: 0.9,
672
+ confidence: targetCandidates.length > 0 ? 0.8 : 0.5,
662
673
  textFingerprint: getTextFingerprint(target)
663
- });
674
+ };
675
+ const parentCandidates = [];
664
676
  const parent = target.parentElement;
677
+ const parentConfiguredAnchor = parent ? findClosestAttributeAnchor(parent, [configuredAttribute], 0.72, {
678
+ strategy: "configured-attribute"
679
+ }) : void 0;
680
+ if (parentConfiguredAnchor) parentCandidates.push(parentConfiguredAnchor);
681
+ const anchoredByAttribute = parent ? findClosestAttributeAnchor(
682
+ parent,
683
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
684
+ 0.7
685
+ ) : void 0;
686
+ if (anchoredByAttribute) parentCandidates.push(anchoredByAttribute);
665
687
  const anchoredById = parent ? findClosest(parent, (element) => isMeaningfulId(element.id)) : void 0;
666
688
  if (anchoredById?.id) {
667
- candidates.push({
689
+ parentCandidates.push({
668
690
  selector: `#${cssEscape(anchoredById.id)}`,
669
691
  strategy: "id",
670
692
  confidence: 0.72,
@@ -674,7 +696,7 @@ function createAnchorCandidates(target, configuredAttribute) {
674
696
  const anchoredByClass = parent ? findClosest(parent, (element) => Boolean(getMeaningfulClassName(element))) : void 0;
675
697
  const className = anchoredByClass ? getMeaningfulClassName(anchoredByClass) : void 0;
676
698
  if (anchoredByClass && className) {
677
- candidates.push({
699
+ parentCandidates.push({
678
700
  selector: `${anchoredByClass.tagName.toLowerCase()}.${cssEscape(
679
701
  className
680
702
  )}`,
@@ -683,8 +705,107 @@ function createAnchorCandidates(target, configuredAttribute) {
683
705
  textFingerprint: getTextFingerprint(anchoredByClass)
684
706
  });
685
707
  }
708
+ const candidates = targetCandidates.length > 0 ? [...targetCandidates, targetDomPath, ...parentCandidates] : [...parentCandidates, targetDomPath];
686
709
  return dedupeAnchorCandidates(candidates);
687
710
  }
711
+ function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
712
+ for (const attributeName of attributeNames) {
713
+ const selector = `[${attributeName}]`;
714
+ const element = safeClosest(target, selector);
715
+ if (!element) continue;
716
+ const value = getStableAttributeValue(element, attributeName);
717
+ if (!value) continue;
718
+ return {
719
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
720
+ strategy: options?.strategy ?? "attribute",
721
+ confidence,
722
+ textFingerprint: getTextFingerprint(element)
723
+ };
724
+ }
725
+ return void 0;
726
+ }
727
+ function getExactAttributeAnchorCandidate(element, attributeName, confidence, strategy) {
728
+ const value = getStableAttributeValue(element, attributeName);
729
+ if (!value) return void 0;
730
+ return {
731
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
732
+ strategy,
733
+ confidence,
734
+ textFingerprint: getTextFingerprint(element)
735
+ };
736
+ }
737
+ function getAttributeAnchorCandidate(element, attributeNames, confidence) {
738
+ for (const attributeName of attributeNames) {
739
+ const value = getStableAttributeValue(element, attributeName);
740
+ if (!value) continue;
741
+ return {
742
+ selector: `${element.tagName.toLowerCase()}[${attributeName}="${cssEscape(
743
+ value
744
+ )}"]`,
745
+ strategy: "attribute",
746
+ confidence,
747
+ textFingerprint: getTextFingerprint(element)
748
+ };
749
+ }
750
+ return void 0;
751
+ }
752
+ function getScopedDomPathCandidate(target, configuredAttribute) {
753
+ const parent = target.parentElement;
754
+ if (!parent) return void 0;
755
+ const anchor = findStableAncestorSelector(parent, configuredAttribute);
756
+ if (!anchor) return void 0;
757
+ const selector = getDomPathBetween(anchor.element, target, anchor.selector);
758
+ if (!selector) return void 0;
759
+ return {
760
+ selector,
761
+ strategy: "dom-path",
762
+ confidence: anchor.confidence,
763
+ textFingerprint: getTextFingerprint(target)
764
+ };
765
+ }
766
+ function findStableAncestorSelector(start, configuredAttribute) {
767
+ let element = start;
768
+ const root = start.ownerDocument.documentElement;
769
+ while (element && element !== root) {
770
+ const configuredValue = getStableAttributeValue(element, configuredAttribute);
771
+ if (configuredValue) {
772
+ return {
773
+ element,
774
+ selector: `[${configuredAttribute}="${cssEscape(configuredValue)}"]`,
775
+ confidence: 0.88
776
+ };
777
+ }
778
+ const attributeAnchor = getAttributeAnchorCandidate(
779
+ element,
780
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
781
+ 0.84
782
+ );
783
+ if (attributeAnchor) {
784
+ return {
785
+ element,
786
+ selector: attributeAnchor.selector,
787
+ confidence: 0.84
788
+ };
789
+ }
790
+ if (isMeaningfulId(element.id)) {
791
+ return {
792
+ element,
793
+ selector: `#${cssEscape(element.id)}`,
794
+ confidence: 0.82
795
+ };
796
+ }
797
+ const className = getMeaningfulClassName(element);
798
+ if (className) {
799
+ return {
800
+ element,
801
+ selector: `${element.tagName.toLowerCase()}.${cssEscape(className)}`,
802
+ confidence: 0.76
803
+ };
804
+ }
805
+ element = element.parentElement;
806
+ }
807
+ return void 0;
808
+ }
688
809
  function getAnchorSourceElement(target, candidate, configuredAttribute) {
689
810
  if (candidate.strategy === "configured-attribute") {
690
811
  return target.closest(`[${configuredAttribute}]`);
@@ -696,6 +817,13 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
696
817
  return target;
697
818
  }
698
819
  }
820
+ function safeClosest(element, selector) {
821
+ try {
822
+ return element.closest(selector);
823
+ } catch {
824
+ return null;
825
+ }
826
+ }
699
827
  function getElementHtmlSnippet(element, maxLength = 1e3) {
700
828
  const html = decodeHtmlEntities(element.outerHTML.replace(/\s+/g, " ").trim());
701
829
  if (html.length <= maxLength) return html;
@@ -817,10 +945,38 @@ function getDomPath(element) {
817
945
  }
818
946
  return `body > ${parts.join(" > ")}`;
819
947
  }
948
+ function getDomPathBetween(ancestor, target, ancestorSelector) {
949
+ const parts = [];
950
+ let current = target;
951
+ while (current && current !== ancestor) {
952
+ parts.unshift(getDomPathPart(current));
953
+ current = current.parentElement;
954
+ }
955
+ if (current !== ancestor || parts.length === 0) return void 0;
956
+ return `${ancestorSelector} > ${parts.join(" > ")}`;
957
+ }
958
+ function getDomPathPart(element) {
959
+ const parent = element.parentElement;
960
+ const tag = element.tagName.toLowerCase();
961
+ if (!parent) return tag;
962
+ const currentTagName = element.tagName;
963
+ const siblings = Array.from(parent.children).filter(
964
+ (child) => child.tagName === currentTagName
965
+ );
966
+ const index = siblings.indexOf(element) + 1;
967
+ return `${tag}:nth-of-type(${index})`;
968
+ }
820
969
  function getTextFingerprint(element) {
821
970
  const text = element.textContent?.replace(/\s+/g, " ").trim();
822
971
  return text ? text.slice(0, 120) : void 0;
823
972
  }
973
+ function getStableAttributeValue(element, attributeName) {
974
+ const value = element.getAttribute(attributeName)?.trim();
975
+ if (!value || value.length > 160) return void 0;
976
+ if (/^(true|false)$/i.test(value)) return void 0;
977
+ if (/^\d+$/.test(value) && value.length < 3) return void 0;
978
+ return value;
979
+ }
824
980
  function getTextFingerprintScore(expected, actual) {
825
981
  if (!expected) return 1;
826
982
  if (!actual) return 0.5;
@@ -1292,6 +1448,19 @@ function createStyleElement() {
1292
1448
  display: block;
1293
1449
  }
1294
1450
 
1451
+ .dfwr-shell.has-dismissible-draft {
1452
+ z-index: 900;
1453
+ }
1454
+
1455
+ .dfwr-draft-cancel-layer {
1456
+ position: fixed;
1457
+ inset: 0;
1458
+ z-index: 2;
1459
+ pointer-events: auto;
1460
+ background: transparent;
1461
+ cursor: default;
1462
+ }
1463
+
1295
1464
  .dfwr-panel {
1296
1465
  position: fixed;
1297
1466
  right: 16px;
@@ -1785,6 +1954,40 @@ function createStyleElement() {
1785
1954
  box-shadow: var(--df-review-shadow-popover);
1786
1955
  }
1787
1956
 
1957
+ .dfwr-note-popover.is-composer,
1958
+ .dfwr-area-draft.is-composer {
1959
+ max-height: min(360px, calc(100vh - 32px));
1960
+ overflow: auto;
1961
+ border-color: rgba(99, 215, 199, 0.56);
1962
+ }
1963
+
1964
+ .dfwr-note-popover.is-dragging,
1965
+ .dfwr-area-draft.is-dragging {
1966
+ user-select: none;
1967
+ }
1968
+
1969
+ .dfwr-draft-drag-handle {
1970
+ display: block;
1971
+ width: 42px;
1972
+ height: 6px;
1973
+ margin: 0 auto 10px;
1974
+ padding: 0;
1975
+ cursor: grab;
1976
+ pointer-events: auto;
1977
+ background: rgba(247, 247, 242, 0.28);
1978
+ border: 0;
1979
+ border-radius: 999px;
1980
+ }
1981
+
1982
+ .dfwr-draft-drag-handle:hover,
1983
+ .dfwr-draft-drag-handle:focus-visible {
1984
+ background: rgba(215, 255, 95, 0.62);
1985
+ }
1986
+
1987
+ .dfwr-draft-drag-handle:active {
1988
+ cursor: grabbing;
1989
+ }
1990
+
1788
1991
  .dfwr-area-draft {
1789
1992
  position: fixed;
1790
1993
  right: 16px;
@@ -1806,6 +2009,14 @@ function createStyleElement() {
1806
2009
  padding: 0;
1807
2010
  }
1808
2011
 
2012
+ .dfwr-note-actions {
2013
+ justify-content: flex-end;
2014
+ }
2015
+
2016
+ .dfwr-note-actions .dfwr-button:first-child {
2017
+ margin-right: auto;
2018
+ }
2019
+
1809
2020
  .dfwr-area-draft .dfwr-actions {
1810
2021
  padding: 0;
1811
2022
  }
@@ -1834,6 +2045,105 @@ function createStyleElement() {
1834
2045
  outline-offset: 1px;
1835
2046
  }
1836
2047
 
2048
+ .dfwr-adjust-panel {
2049
+ display: grid;
2050
+ gap: 4px;
2051
+ padding: 8px 10px;
2052
+ border: 1px solid rgba(255, 255, 255, 0.12);
2053
+ border-radius: var(--df-review-radius-sm);
2054
+ background: rgba(255, 255, 255, 0.04);
2055
+ }
2056
+
2057
+ .dfwr-adjust-panel-header {
2058
+ display: flex;
2059
+ align-items: center;
2060
+ justify-content: space-between;
2061
+ gap: 10px;
2062
+ min-width: 0;
2063
+ }
2064
+
2065
+ .dfwr-adjust-panel-header .dfwr-adjust-help {
2066
+ flex: 1 1 auto;
2067
+ min-width: 0;
2068
+ }
2069
+
2070
+ .dfwr-adjust-panel.is-active {
2071
+ border-color: rgba(215, 255, 95, 0.5);
2072
+ background: var(--df-review-color-accent-soft);
2073
+ }
2074
+
2075
+ .dfwr-adjust-help,
2076
+ .dfwr-adjust-status {
2077
+ margin: 0;
2078
+ color: var(--df-review-color-text-muted);
2079
+ font-size: var(--df-review-font-size-xs);
2080
+ line-height: 1.35;
2081
+ }
2082
+
2083
+ .dfwr-adjust-status {
2084
+ color: var(--df-review-color-text);
2085
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2086
+ }
2087
+
2088
+ .dfwr-adjust-toggle {
2089
+ flex: 0 0 auto;
2090
+ display: inline-flex;
2091
+ align-items: center;
2092
+ justify-content: center;
2093
+ width: 34px;
2094
+ height: 30px;
2095
+ padding: 0;
2096
+ border: 1px solid rgba(255, 255, 255, 0.2);
2097
+ border-radius: var(--df-review-radius-sm);
2098
+ background: rgba(255, 255, 255, 0.04);
2099
+ color: var(--df-review-color-text);
2100
+ cursor: pointer;
2101
+ font: inherit;
2102
+ font-size: 14px;
2103
+ font-weight: 800;
2104
+ line-height: 1;
2105
+ }
2106
+
2107
+ .dfwr-adjust-toggle:hover,
2108
+ .dfwr-adjust-toggle:focus-visible,
2109
+ .dfwr-adjust-toggle.is-active {
2110
+ border-color: rgba(215, 255, 95, 0.68);
2111
+ background: var(--df-review-color-accent-soft);
2112
+ outline: none;
2113
+ }
2114
+
2115
+ .dfwr-adjust-toggle svg {
2116
+ width: 18px;
2117
+ height: 18px;
2118
+ pointer-events: none;
2119
+ }
2120
+
2121
+ .dfwr-adjust-hud {
2122
+ position: fixed;
2123
+ z-index: 5;
2124
+ display: inline-flex;
2125
+ align-items: center;
2126
+ min-height: 22px;
2127
+ padding: 0 8px;
2128
+ border: 1px solid rgba(99, 215, 199, 0.72);
2129
+ border-radius: var(--df-review-radius-sm);
2130
+ background: rgba(21, 25, 29, 0.92);
2131
+ box-shadow:
2132
+ 0 0 0 3px rgba(99, 215, 199, 0.14),
2133
+ 0 8px 18px rgba(0, 0, 0, 0.26);
2134
+ color: #63d7c7;
2135
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
2136
+ font-size: var(--df-review-font-size-2xs);
2137
+ font-weight: 800;
2138
+ line-height: 1;
2139
+ pointer-events: none;
2140
+ white-space: nowrap;
2141
+ }
2142
+
2143
+ .dfwr-adjust-hud[hidden] {
2144
+ display: none;
2145
+ }
2146
+
1837
2147
  .dfwr-empty,
1838
2148
  .dfwr-error {
1839
2149
  margin: 0;
@@ -2054,16 +2364,6 @@ function createStyleElement() {
2054
2364
  }
2055
2365
 
2056
2366
  // 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
2367
  function formatNoteDraftMeta(draft) {
2068
2368
  const parts = [
2069
2369
  `viewport ${formatSize(draft.viewport)}`,
@@ -2129,17 +2429,29 @@ function formatAnchorMeta(anchor) {
2129
2429
  }
2130
2430
 
2131
2431
  // src/core/web.review.kit.view.ts
2432
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
2132
2433
  var WebReviewKitView = class {
2133
2434
  constructor(config) {
2134
2435
  this.config = config;
2135
2436
  }
2437
+ clearDraftPreview() {
2438
+ this.restoreDraftPreview();
2439
+ }
2136
2440
  render(shadow, hiddenItemsStyle) {
2137
2441
  const state = this.state;
2442
+ this.syncDraftPreview(
2443
+ state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2444
+ );
2138
2445
  shadow.replaceChildren();
2139
2446
  shadow.append(createStyleElement());
2140
2447
  shadow.append(hiddenItemsStyle);
2448
+ const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
2141
2449
  const shell = document.createElement("div");
2142
- shell.className = `dfwr-shell${state.isOpen ? " is-open" : ""}`;
2450
+ shell.className = [
2451
+ "dfwr-shell",
2452
+ state.isOpen ? "is-open" : "",
2453
+ hasDismissableDraft ? "has-dismissible-draft" : ""
2454
+ ].filter(Boolean).join(" ");
2143
2455
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
2144
2456
  if (this.config.options.ui?.panel !== false) {
2145
2457
  const panel = document.createElement("div");
@@ -2155,6 +2467,9 @@ var WebReviewKitView = class {
2155
2467
  shell.append(panel);
2156
2468
  }
2157
2469
  shell.append(this.createMarkerLayer());
2470
+ if (state.isOpen && hasDismissableDraft) {
2471
+ shell.append(this.createDraftCancelLayer());
2472
+ }
2158
2473
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
2159
2474
  shell.append(
2160
2475
  state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
@@ -2174,6 +2489,294 @@ var WebReviewKitView = class {
2174
2489
  get state() {
2175
2490
  return this.config.getState();
2176
2491
  }
2492
+ createDraftCancelLayer() {
2493
+ const layer = document.createElement("div");
2494
+ layer.className = "dfwr-draft-cancel-layer";
2495
+ layer.setAttribute("aria-hidden", "true");
2496
+ const cancel = (event) => {
2497
+ event.preventDefault();
2498
+ event.stopPropagation();
2499
+ event.stopImmediatePropagation();
2500
+ this.config.actions.setModeState("idle");
2501
+ this.config.actions.clearDrafts();
2502
+ this.config.actions.setSelectingArea(false);
2503
+ this.config.actions.render();
2504
+ };
2505
+ layer.addEventListener("pointerdown", (event) => {
2506
+ if (event.button !== 0) return;
2507
+ cancel(event);
2508
+ });
2509
+ layer.addEventListener("click", cancel);
2510
+ return layer;
2511
+ }
2512
+ getDraftAdjustmentMetrics(draft) {
2513
+ const adjustment = draft.adjustment;
2514
+ const x = adjustment?.x ?? 0;
2515
+ const y = adjustment?.y ?? 0;
2516
+ const scale = adjustment?.scale ?? 0;
2517
+ const {
2518
+ scale: viewportScale,
2519
+ designWidth,
2520
+ presetLabel
2521
+ } = this.getDraftViewportScale(draft.viewport);
2522
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2523
+ const scaleCssDelta = scale * viewportScale;
2524
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
2525
+ 1 / selection.width,
2526
+ (selection.width + scaleCssDelta) / selection.width
2527
+ ) : 1;
2528
+ return {
2529
+ x,
2530
+ y,
2531
+ scale,
2532
+ cssX: x * viewportScale,
2533
+ cssY: y * viewportScale,
2534
+ scaleFactor,
2535
+ viewportScale,
2536
+ designWidth,
2537
+ presetLabel,
2538
+ viewportWidth: draft.viewport.width
2539
+ };
2540
+ }
2541
+ hasDraftAdjustment(draft) {
2542
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2543
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
2544
+ }
2545
+ getAdjustedDraftPoint(point, draft) {
2546
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2547
+ return {
2548
+ x: point.x + metrics.cssX,
2549
+ y: point.y + metrics.cssY
2550
+ };
2551
+ }
2552
+ getAdjustedDraftSelection(selection, draft) {
2553
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2554
+ return {
2555
+ ...selection,
2556
+ left: selection.left + metrics.cssX,
2557
+ top: selection.top + metrics.cssY,
2558
+ width: selection.width * metrics.scaleFactor,
2559
+ height: selection.height * metrics.scaleFactor
2560
+ };
2561
+ }
2562
+ getDraftViewportScale(viewport) {
2563
+ const preset = findReviewViewportPreset(
2564
+ viewport,
2565
+ this.config.options.viewports?.presets
2566
+ );
2567
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2568
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2569
+ return { scale, designWidth, presetLabel: preset.label };
2570
+ }
2571
+ getDraftComposerWidth(environment) {
2572
+ const bounds = environment.overlayRect;
2573
+ const margin = 12;
2574
+ return Math.min(360, Math.max(240, bounds.width - margin * 2));
2575
+ }
2576
+ getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
2577
+ const margin = 12;
2578
+ const width = size?.width ?? this.getDraftComposerWidth(environment);
2579
+ const height = size?.height ?? 236;
2580
+ return {
2581
+ x: clamp(
2582
+ position.x,
2583
+ bounds.left + margin,
2584
+ bounds.left + bounds.width - width - margin
2585
+ ),
2586
+ y: clamp(
2587
+ position.y,
2588
+ bounds.top + margin,
2589
+ bounds.top + bounds.height - height - margin
2590
+ )
2591
+ };
2592
+ }
2593
+ getHostComposerBounds() {
2594
+ const root = document.documentElement;
2595
+ return {
2596
+ left: 0,
2597
+ top: 0,
2598
+ width: root.clientWidth || window.innerWidth,
2599
+ height: root.clientHeight || window.innerHeight
2600
+ };
2601
+ }
2602
+ getInitialDraftComposerPosition(selection, environment, size) {
2603
+ const bounds = this.getHostComposerBounds();
2604
+ const margin = 12;
2605
+ const gap = 20;
2606
+ if (!selection) {
2607
+ return this.getClampedComposerPosition(
2608
+ {
2609
+ x: environment.overlayRect.left + margin,
2610
+ y: environment.overlayRect.top + margin
2611
+ },
2612
+ environment,
2613
+ size,
2614
+ bounds
2615
+ );
2616
+ }
2617
+ const preferredX = selection.left + selection.width + gap;
2618
+ const maxX = bounds.left + bounds.width - size.width - margin;
2619
+ const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
2620
+ return this.getClampedComposerPosition(
2621
+ {
2622
+ x,
2623
+ y: selection.top
2624
+ },
2625
+ environment,
2626
+ size,
2627
+ bounds
2628
+ );
2629
+ }
2630
+ getDraftComposerPosition({
2631
+ selection,
2632
+ environment,
2633
+ composerPosition,
2634
+ estimatedHeight
2635
+ }) {
2636
+ const width = this.getDraftComposerWidth(environment);
2637
+ if (composerPosition) {
2638
+ const clamped = this.getClampedComposerPosition(
2639
+ composerPosition,
2640
+ environment,
2641
+ { width, height: estimatedHeight },
2642
+ this.getHostComposerBounds()
2643
+ );
2644
+ return { width, left: clamped.x, top: clamped.y };
2645
+ }
2646
+ const position = this.getInitialDraftComposerPosition(selection, environment, {
2647
+ width,
2648
+ height: estimatedHeight
2649
+ });
2650
+ return { width, left: position.x, top: position.y };
2651
+ }
2652
+ getSelectionMqMetrics(selection, viewport) {
2653
+ const { scale } = this.getDraftViewportScale(viewport);
2654
+ const ratio = scale > 0 ? 1 / scale : 1;
2655
+ return {
2656
+ x: selection.left * ratio,
2657
+ y: selection.top * ratio,
2658
+ width: selection.width * ratio,
2659
+ height: selection.height * ratio
2660
+ };
2661
+ }
2662
+ formatSignedPx(value) {
2663
+ if (value === 0) return "+0px";
2664
+ return `${value > 0 ? "+" : ""}${value}px`;
2665
+ }
2666
+ formatRoundedPx(value) {
2667
+ return `${Math.round(value)}px`;
2668
+ }
2669
+ getAdjustmentLabel() {
2670
+ return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
2671
+ }
2672
+ getSelectionMetricLines(selection, viewport) {
2673
+ if (!selection) return ["area", "x none / y none", "w none / h none"];
2674
+ const metrics = this.getSelectionMqMetrics(selection, viewport);
2675
+ return [
2676
+ "area",
2677
+ `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
2678
+ metrics.y
2679
+ )}`,
2680
+ `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
2681
+ metrics.height
2682
+ )}`
2683
+ ];
2684
+ }
2685
+ getAreaDraftMetricSelection(draft) {
2686
+ if (!draft.selection) return void 0;
2687
+ return toViewportSelection(draft.selection.viewport);
2688
+ }
2689
+ formatDraftAdjustmentStatus(draft) {
2690
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2691
+ return [
2692
+ `x ${this.formatSignedPx(metrics.x)}`,
2693
+ `y ${this.formatSignedPx(metrics.y)}`,
2694
+ `scale ${this.formatSignedPx(metrics.scale)}`
2695
+ ].join(" / ");
2696
+ }
2697
+ getDraftAdjustmentMetricLines(draft) {
2698
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2699
+ return [
2700
+ `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
2701
+ metrics.y
2702
+ )}`,
2703
+ `scale ${this.formatSignedPx(metrics.scale)}`
2704
+ ];
2705
+ }
2706
+ withDraftAdjustmentComment(comment, draft) {
2707
+ if (!this.hasDraftAdjustment(draft)) return comment;
2708
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2709
+ const adjustment = [
2710
+ `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
2711
+ metrics.x
2712
+ )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
2713
+ metrics.scale
2714
+ )}`,
2715
+ `(${metrics.presetLabel} viewport, ${Math.round(
2716
+ metrics.viewportWidth
2717
+ )}/design ${Math.round(metrics.designWidth)})`
2718
+ ].join(" ");
2719
+ return `${comment.trim()}
2720
+ ${adjustment}`;
2721
+ }
2722
+ getStyleableDraftElement(draft, environment) {
2723
+ if (!draft.anchor) return void 0;
2724
+ const element = resolveAnchorElement(draft.anchor, environment)?.element;
2725
+ if (!element) return void 0;
2726
+ if ("style" in element) return element;
2727
+ return void 0;
2728
+ }
2729
+ syncDraftPreview(draft) {
2730
+ const environment = this.config.getEnvironment();
2731
+ if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
2732
+ this.restoreDraftPreview();
2733
+ return;
2734
+ }
2735
+ const element = this.getStyleableDraftElement(draft, environment);
2736
+ if (!element) {
2737
+ this.restoreDraftPreview();
2738
+ return;
2739
+ }
2740
+ if (this.draftPreview?.element !== element) {
2741
+ this.restoreDraftPreview();
2742
+ }
2743
+ if (!this.draftPreview) {
2744
+ const computedTransform = environment.window.getComputedStyle(element).transform;
2745
+ this.draftPreview = {
2746
+ element,
2747
+ transform: element.style.transform,
2748
+ transformOrigin: element.style.transformOrigin,
2749
+ transition: element.style.transition,
2750
+ willChange: element.style.willChange,
2751
+ baseTransform: element.style.transform || (computedTransform && computedTransform !== "none" ? computedTransform : "")
2752
+ };
2753
+ }
2754
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2755
+ const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
2756
+ metrics.cssY
2757
+ )}px)`;
2758
+ const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
2759
+ element.style.transition = "none";
2760
+ element.style.willChange = "transform";
2761
+ element.style.transformOrigin = "top left";
2762
+ element.style.transform = [
2763
+ this.draftPreview.baseTransform,
2764
+ translate,
2765
+ scale
2766
+ ].filter(Boolean).join(" ");
2767
+ }
2768
+ restoreDraftPreview() {
2769
+ if (!this.draftPreview) return;
2770
+ const { element, transform, transformOrigin, transition, willChange } = this.draftPreview;
2771
+ element.style.transform = transform;
2772
+ element.style.transformOrigin = transformOrigin;
2773
+ element.style.transition = transition;
2774
+ element.style.willChange = willChange;
2775
+ this.draftPreview = void 0;
2776
+ }
2777
+ toCssNumber(value) {
2778
+ return Math.round(value * 1e3) / 1e3;
2779
+ }
2177
2780
  createHeader() {
2178
2781
  const header = document.createElement("div");
2179
2782
  header.className = "dfwr-header";
@@ -2265,15 +2868,20 @@ var WebReviewKitView = class {
2265
2868
  const group = document.createElement("div");
2266
2869
  group.className = "dfwr-note-draft";
2267
2870
  if (!environment) return group;
2268
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
2871
+ const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
2872
+ const hostPoint = toHostPoint(
2873
+ isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
2874
+ environment
2875
+ );
2876
+ let selectionHighlight;
2269
2877
  if (draft.selection) {
2270
- group.append(
2271
- this.createSelectionHighlight(
2272
- toViewportSelection(draft.selection.viewport),
2273
- environment,
2274
- true
2275
- )
2878
+ const selection = toViewportSelection(draft.selection.viewport);
2879
+ selectionHighlight = this.createSelectionHighlight(
2880
+ isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2881
+ environment,
2882
+ true
2276
2883
  );
2884
+ group.append(selectionHighlight);
2277
2885
  }
2278
2886
  const pin = document.createElement("button");
2279
2887
  pin.className = "dfwr-note-pin";
@@ -2283,14 +2891,35 @@ var WebReviewKitView = class {
2283
2891
  pin.style.top = `${hostPoint.y}px`;
2284
2892
  const popover = document.createElement("div");
2285
2893
  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`;
2894
+ popover.className = `dfwr-note-popover${isElementDraft ? " is-composer" : ""}`;
2895
+ if (isElementDraft) {
2896
+ const selection = draft.selection ? toHostSelection(
2897
+ this.getAdjustedDraftSelection(
2898
+ toViewportSelection(draft.selection.viewport),
2899
+ draft
2900
+ ),
2901
+ environment
2902
+ ) : void 0;
2903
+ const composer = this.getDraftComposerPosition({
2904
+ selection,
2905
+ environment,
2906
+ composerPosition: draft.composerPosition,
2907
+ estimatedHeight: 252
2908
+ });
2909
+ popover.style.left = `${composer.left}px`;
2910
+ popover.style.top = `${composer.top}px`;
2911
+ popover.style.width = `${composer.width}px`;
2912
+ } else {
2913
+ popover.style.left = `${position.left}px`;
2914
+ popover.style.top = `${position.top}px`;
2915
+ }
2289
2916
  const form = document.createElement("form");
2290
2917
  form.className = "dfwr-form";
2291
- const meta = document.createElement("div");
2292
- meta.className = "dfwr-item-date";
2293
- meta.textContent = formatNoteDraftMeta(draft);
2918
+ const meta = isElementDraft ? void 0 : document.createElement("div");
2919
+ if (meta) {
2920
+ meta.className = "dfwr-item-date";
2921
+ meta.textContent = formatNoteDraftMeta(draft);
2922
+ }
2294
2923
  const textarea = document.createElement("textarea");
2295
2924
  textarea.className = "dfwr-textarea";
2296
2925
  textarea.placeholder = "Review comment";
@@ -2304,25 +2933,306 @@ var WebReviewKitView = class {
2304
2933
  comment: textarea.value
2305
2934
  });
2306
2935
  });
2307
- const actions = this.createFormActions("Save note", () => {
2936
+ const saveDraft = () => {
2308
2937
  const comment = textarea.value.trim();
2309
2938
  if (!comment) return;
2939
+ const currentDraft = this.state.noteDraft ?? draft;
2310
2940
  void this.config.actions.createItem({
2311
2941
  kind: "note",
2312
- comment,
2313
- viewport: draft.viewport,
2314
- anchor: draft.anchor,
2315
- marker: draft.marker,
2316
- selection: draft.selection
2942
+ comment: this.withDraftAdjustmentComment(comment, currentDraft),
2943
+ viewport: currentDraft.viewport,
2944
+ anchor: currentDraft.anchor,
2945
+ marker: currentDraft.marker,
2946
+ selection: currentDraft.selection
2317
2947
  });
2318
- });
2319
- form.append(meta, textarea, actions);
2320
- popover.append(form);
2948
+ };
2949
+ const adjustmentHud = isElementDraft ? this.createAdjustmentHud(draft, environment) : void 0;
2950
+ if (adjustmentHud) {
2951
+ group.append(adjustmentHud);
2952
+ }
2953
+ const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
2954
+ draft,
2955
+ hud: adjustmentHud,
2956
+ pin,
2957
+ popover,
2958
+ selectionHighlight,
2959
+ textarea
2960
+ }) : void 0;
2961
+ const actions = this.createFormActions("Save note", saveDraft);
2962
+ form.append(
2963
+ ...meta ? [meta] : [],
2964
+ ...adjustmentControls ? [adjustmentControls.panel] : [],
2965
+ textarea,
2966
+ actions
2967
+ );
2968
+ const dragHandle = isElementDraft ? this.createDraftDragHandle("Move DOM composer") : void 0;
2969
+ popover.append(...dragHandle ? [dragHandle] : [], form);
2321
2970
  group.append(pin, popover);
2322
- this.attachDraftPinDrag(pin, popover, meta, textarea);
2323
- window.setTimeout(() => textarea.focus(), 0);
2971
+ if (dragHandle) {
2972
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
2973
+ const noteDraft = this.state.noteDraft ?? draft;
2974
+ this.config.actions.setNoteDraft({
2975
+ ...noteDraft,
2976
+ composerPosition,
2977
+ comment: textarea.value
2978
+ });
2979
+ });
2980
+ }
2981
+ this.attachDraftPinDrag(
2982
+ pin,
2983
+ isElementDraft ? void 0 : popover,
2984
+ meta,
2985
+ textarea
2986
+ );
2987
+ window.setTimeout(() => {
2988
+ if (draft.adjustment?.isActive) {
2989
+ adjustmentControls?.focusTarget.focus();
2990
+ return;
2991
+ }
2992
+ textarea.focus();
2993
+ }, 0);
2324
2994
  return group;
2325
2995
  }
2996
+ createDraftDragHandle(label) {
2997
+ const handle = document.createElement("button");
2998
+ handle.className = "dfwr-draft-drag-handle";
2999
+ handle.type = "button";
3000
+ handle.setAttribute("aria-label", label);
3001
+ return handle;
3002
+ }
3003
+ createIcon(paths) {
3004
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3005
+ svg.setAttribute("aria-hidden", "true");
3006
+ svg.setAttribute("viewBox", "0 0 24 24");
3007
+ svg.setAttribute("fill", "none");
3008
+ svg.setAttribute("stroke", "currentColor");
3009
+ svg.setAttribute("stroke-width", "2.4");
3010
+ svg.setAttribute("stroke-linecap", "round");
3011
+ svg.setAttribute("stroke-linejoin", "round");
3012
+ paths.forEach((d) => {
3013
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3014
+ path.setAttribute("d", d);
3015
+ svg.append(path);
3016
+ });
3017
+ return svg;
3018
+ }
3019
+ setAdjustmentToggleIcon(button, isActive) {
3020
+ const paths = isActive ? ["M20 6 9 17l-5-5"] : [
3021
+ "M12 2v20",
3022
+ "M2 12h20",
3023
+ "m9 5 3-3 3 3",
3024
+ "m9 19 3 3 3-3",
3025
+ "m5 9-3 3 3 3",
3026
+ "m19 9 3 3-3 3"
3027
+ ];
3028
+ button.replaceChildren(this.createIcon(paths));
3029
+ }
3030
+ attachDraftComposerDrag(popover, handle, onMove) {
3031
+ let isDragging = false;
3032
+ let offsetX = 0;
3033
+ let offsetY = 0;
3034
+ const movePopover = (event) => {
3035
+ const environment = this.config.getEnvironment();
3036
+ if (!environment) return;
3037
+ const position = this.getClampedComposerPosition(
3038
+ {
3039
+ x: event.clientX - offsetX,
3040
+ y: event.clientY - offsetY
3041
+ },
3042
+ environment,
3043
+ {
3044
+ width: popover.offsetWidth,
3045
+ height: popover.offsetHeight
3046
+ },
3047
+ this.getHostComposerBounds()
3048
+ );
3049
+ popover.style.left = `${position.x}px`;
3050
+ popover.style.top = `${position.y}px`;
3051
+ onMove(position);
3052
+ };
3053
+ handle.addEventListener("pointerdown", (event) => {
3054
+ if (event.button !== 0) return;
3055
+ const rect = popover.getBoundingClientRect();
3056
+ offsetX = event.clientX - rect.left;
3057
+ offsetY = event.clientY - rect.top;
3058
+ isDragging = true;
3059
+ event.preventDefault();
3060
+ event.stopPropagation();
3061
+ handle.setPointerCapture(event.pointerId);
3062
+ popover.classList.add("is-dragging");
3063
+ });
3064
+ handle.addEventListener("pointermove", (event) => {
3065
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3066
+ event.preventDefault();
3067
+ movePopover(event);
3068
+ });
3069
+ const stopDrag = (event) => {
3070
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3071
+ event.preventDefault();
3072
+ event.stopPropagation();
3073
+ isDragging = false;
3074
+ handle.releasePointerCapture(event.pointerId);
3075
+ popover.classList.remove("is-dragging");
3076
+ movePopover(event);
3077
+ };
3078
+ handle.addEventListener("pointerup", stopDrag);
3079
+ handle.addEventListener("pointercancel", stopDrag);
3080
+ }
3081
+ createAdjustmentControls({
3082
+ draft,
3083
+ hud,
3084
+ pin,
3085
+ popover,
3086
+ selectionHighlight,
3087
+ textarea
3088
+ }) {
3089
+ const panel = document.createElement("div");
3090
+ panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
3091
+ const header = document.createElement("div");
3092
+ header.className = "dfwr-adjust-panel-header";
3093
+ const help = document.createElement("div");
3094
+ help.className = "dfwr-adjust-help";
3095
+ help.textContent = this.getAdjustmentLabel();
3096
+ const adjust = document.createElement("button");
3097
+ adjust.className = "dfwr-adjust-toggle";
3098
+ adjust.type = "button";
3099
+ adjust.title = "Adjust DOM element with keyboard arrows";
3100
+ adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
3101
+ const xyStatus = document.createElement("div");
3102
+ xyStatus.className = "dfwr-adjust-status";
3103
+ const scaleStatus = document.createElement("div");
3104
+ scaleStatus.className = "dfwr-adjust-status";
3105
+ const syncControls = (nextDraft) => {
3106
+ const isActive = nextDraft.adjustment?.isActive === true;
3107
+ panel.classList.toggle("is-active", isActive);
3108
+ adjust.classList.toggle("is-active", isActive);
3109
+ adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
3110
+ this.setAdjustmentToggleIcon(adjust, isActive);
3111
+ adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
3112
+ adjust.setAttribute(
3113
+ "aria-label",
3114
+ isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
3115
+ );
3116
+ const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
3117
+ xyStatus.textContent = xyLine;
3118
+ scaleStatus.textContent = scaleLine;
3119
+ this.syncDraftAdjustmentUi({
3120
+ draft: nextDraft,
3121
+ hud,
3122
+ pin,
3123
+ selectionHighlight
3124
+ });
3125
+ };
3126
+ const updateDraft = (updater) => {
3127
+ const currentDraft = this.state.noteDraft ?? draft;
3128
+ const nextDraft = updater(currentDraft);
3129
+ this.config.actions.setNoteDraft({
3130
+ ...nextDraft,
3131
+ comment: textarea.value
3132
+ });
3133
+ syncControls(nextDraft);
3134
+ };
3135
+ adjust.addEventListener("click", () => {
3136
+ updateDraft((currentDraft) => ({
3137
+ ...currentDraft,
3138
+ adjustment: {
3139
+ x: currentDraft.adjustment?.x ?? 0,
3140
+ y: currentDraft.adjustment?.y ?? 0,
3141
+ scale: currentDraft.adjustment?.scale ?? 0,
3142
+ isActive: currentDraft.adjustment?.isActive !== true
3143
+ }
3144
+ }));
3145
+ adjust.focus();
3146
+ });
3147
+ popover.addEventListener("keydown", (event) => {
3148
+ const currentDraft = this.state.noteDraft ?? draft;
3149
+ if (currentDraft.adjustment?.isActive !== true) return;
3150
+ const keyDelta = this.getAdjustmentKeyDelta(event);
3151
+ if (!keyDelta) return;
3152
+ event.preventDefault();
3153
+ event.stopPropagation();
3154
+ updateDraft((activeDraft) => ({
3155
+ ...activeDraft,
3156
+ adjustment: {
3157
+ x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
3158
+ y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
3159
+ scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
3160
+ isActive: true
3161
+ }
3162
+ }));
3163
+ });
3164
+ header.append(help, adjust);
3165
+ panel.append(header, xyStatus, scaleStatus);
3166
+ syncControls(draft);
3167
+ return {
3168
+ panel,
3169
+ focusTarget: adjust
3170
+ };
3171
+ }
3172
+ getAdjustmentKeyDelta(event) {
3173
+ const step = event.shiftKey ? 10 : 1;
3174
+ if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
3175
+ if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
3176
+ if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
3177
+ if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
3178
+ if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
3179
+ if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
3180
+ return void 0;
3181
+ }
3182
+ createAdjustmentHud(draft, environment) {
3183
+ const hud = document.createElement("div");
3184
+ hud.className = "dfwr-adjust-hud";
3185
+ hud.setAttribute("aria-hidden", "true");
3186
+ this.syncAdjustmentHud(hud, draft, environment);
3187
+ return hud;
3188
+ }
3189
+ syncDraftAdjustmentUi({
3190
+ draft,
3191
+ hud,
3192
+ pin,
3193
+ selectionHighlight
3194
+ }) {
3195
+ const environment = this.config.getEnvironment();
3196
+ if (!environment) return;
3197
+ const hostPoint = toHostPoint(
3198
+ this.getAdjustedDraftPoint(draft.marker.viewport, draft),
3199
+ environment
3200
+ );
3201
+ pin.style.left = `${hostPoint.x}px`;
3202
+ pin.style.top = `${hostPoint.y}px`;
3203
+ if (draft.selection && selectionHighlight) {
3204
+ const rect = toHostSelection(
3205
+ this.getAdjustedDraftSelection(
3206
+ toViewportSelection(draft.selection.viewport),
3207
+ draft
3208
+ ),
3209
+ environment
3210
+ );
3211
+ selectionHighlight.style.left = `${rect.left}px`;
3212
+ selectionHighlight.style.top = `${rect.top}px`;
3213
+ selectionHighlight.style.width = `${rect.width}px`;
3214
+ selectionHighlight.style.height = `${rect.height}px`;
3215
+ }
3216
+ if (hud) {
3217
+ this.syncAdjustmentHud(hud, draft, environment);
3218
+ }
3219
+ this.syncDraftPreview(draft);
3220
+ }
3221
+ syncAdjustmentHud(hud, draft, environment) {
3222
+ if (!draft.selection) return;
3223
+ const rect = toHostSelection(
3224
+ this.getAdjustedDraftSelection(
3225
+ toViewportSelection(draft.selection.viewport),
3226
+ draft
3227
+ ),
3228
+ environment
3229
+ );
3230
+ const isVisible = draft.adjustment?.isActive === true || this.hasDraftAdjustment(draft);
3231
+ hud.hidden = !isVisible;
3232
+ hud.textContent = this.formatDraftAdjustmentStatus(draft);
3233
+ hud.style.left = `${Math.max(4, rect.left)}px`;
3234
+ hud.style.top = `${Math.max(4, rect.top - 28)}px`;
3235
+ }
2326
3236
  createAreaForm() {
2327
3237
  const form = document.createElement("form");
2328
3238
  form.className = "dfwr-form";
@@ -2334,14 +3244,20 @@ var WebReviewKitView = class {
2334
3244
  form.append(empty);
2335
3245
  return form;
2336
3246
  }
2337
- const meta = document.createElement("div");
2338
- meta.className = "dfwr-item-date";
2339
- meta.textContent = formatAreaDraftMeta(areaDraft);
2340
- form.append(meta);
3247
+ form.append(this.createAreaMetricsPanel(areaDraft));
2341
3248
  const textarea = document.createElement("textarea");
2342
3249
  textarea.className = "dfwr-textarea";
2343
3250
  textarea.placeholder = "Area comment";
2344
3251
  textarea.rows = 4;
3252
+ textarea.value = areaDraft.comment ?? "";
3253
+ textarea.addEventListener("input", () => {
3254
+ const draft = this.state.areaDraft;
3255
+ if (!draft) return;
3256
+ this.config.actions.setAreaDraft({
3257
+ ...draft,
3258
+ comment: textarea.value
3259
+ });
3260
+ });
2345
3261
  const actions = this.createFormActions("Save area", () => {
2346
3262
  const comment = textarea.value.trim();
2347
3263
  const draft = this.state.areaDraft;
@@ -2358,6 +3274,25 @@ var WebReviewKitView = class {
2358
3274
  form.append(textarea, actions);
2359
3275
  return form;
2360
3276
  }
3277
+ createAreaMetricsPanel(draft) {
3278
+ const panel = document.createElement("div");
3279
+ panel.className = "dfwr-adjust-panel is-area-metrics-panel";
3280
+ const help = document.createElement("div");
3281
+ help.className = "dfwr-adjust-help";
3282
+ const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
3283
+ this.getAreaDraftMetricSelection(draft),
3284
+ draft.viewport
3285
+ );
3286
+ help.textContent = labelLine;
3287
+ const xyStatus = document.createElement("div");
3288
+ xyStatus.className = "dfwr-adjust-status";
3289
+ xyStatus.textContent = xyLine;
3290
+ const sizeStatus = document.createElement("div");
3291
+ sizeStatus.className = "dfwr-adjust-status";
3292
+ sizeStatus.textContent = sizeLine;
3293
+ panel.append(help, xyStatus, sizeStatus);
3294
+ return panel;
3295
+ }
2361
3296
  createAreaDraftOverlay(draft) {
2362
3297
  const layer = document.createElement("div");
2363
3298
  layer.className = "dfwr-area-preview-layer";
@@ -2386,23 +3321,37 @@ var WebReviewKitView = class {
2386
3321
  createAreaDraftPopover(draft) {
2387
3322
  const environment = this.config.getEnvironment();
2388
3323
  const popover = document.createElement("div");
2389
- popover.className = "dfwr-area-draft";
3324
+ popover.className = "dfwr-area-draft is-composer";
2390
3325
  if (environment && draft.selection) {
2391
3326
  const selection = toHostSelection(
2392
3327
  toViewportSelection(draft.selection.viewport),
2393
3328
  environment
2394
3329
  );
2395
- const position = getAreaPopoverPosition(selection, environment);
2396
- popover.style.left = `${position.left}px`;
2397
- popover.style.top = `${position.top}px`;
3330
+ const composer = this.getDraftComposerPosition({
3331
+ selection,
3332
+ environment,
3333
+ composerPosition: draft.composerPosition,
3334
+ estimatedHeight: 220
3335
+ });
3336
+ popover.style.left = `${composer.left}px`;
3337
+ popover.style.top = `${composer.top}px`;
3338
+ popover.style.width = `${composer.width}px`;
2398
3339
  popover.style.right = "auto";
2399
3340
  }
2400
- popover.append(this.createAreaForm());
3341
+ const dragHandle = this.createDraftDragHandle("Move area composer");
3342
+ popover.append(dragHandle, this.createAreaForm());
3343
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3344
+ const areaDraft = this.state.areaDraft ?? draft;
3345
+ this.config.actions.setAreaDraft({
3346
+ ...areaDraft,
3347
+ composerPosition
3348
+ });
3349
+ });
2401
3350
  return popover;
2402
3351
  }
2403
- createFormActions(saveLabel, onSave) {
3352
+ createFormActions(saveLabel, onSave, options) {
2404
3353
  const actions = document.createElement("div");
2405
- actions.className = "dfwr-actions";
3354
+ actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
2406
3355
  const save = document.createElement("button");
2407
3356
  save.className = "dfwr-button is-primary";
2408
3357
  save.type = "button";
@@ -2417,6 +3366,10 @@ var WebReviewKitView = class {
2417
3366
  this.config.actions.clearDrafts();
2418
3367
  this.config.actions.render();
2419
3368
  });
3369
+ if (options?.beforeSave?.length || options?.className) {
3370
+ actions.append(cancel, ...options.beforeSave ?? [], save);
3371
+ return actions;
3372
+ }
2420
3373
  actions.append(save, cancel);
2421
3374
  return actions;
2422
3375
  }
@@ -2621,11 +3574,13 @@ ${formatItemMeta(item)}`;
2621
3574
  if (!environment) return;
2622
3575
  const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
2623
3576
  const nextHostPoint = toHostPoint(nextPoint, environment);
2624
- const position = getPopoverPosition(nextHostPoint, environment);
2625
3577
  pin.style.left = `${nextHostPoint.x}px`;
2626
3578
  pin.style.top = `${nextHostPoint.y}px`;
2627
- popover.style.left = `${position.left}px`;
2628
- popover.style.top = `${position.top}px`;
3579
+ if (popover) {
3580
+ const position = getPopoverPosition(nextHostPoint, environment);
3581
+ popover.style.left = `${position.left}px`;
3582
+ popover.style.top = `${position.top}px`;
3583
+ }
2629
3584
  const noteDraft = this.state.noteDraft;
2630
3585
  if (!noteDraft) return;
2631
3586
  const nextDraft = {
@@ -2637,7 +3592,9 @@ ${formatItemMeta(item)}`;
2637
3592
  comment: textarea.value
2638
3593
  };
2639
3594
  this.config.actions.setNoteDraft(nextDraft);
2640
- meta.textContent = formatNoteDraftMeta(nextDraft);
3595
+ if (meta) {
3596
+ meta.textContent = formatNoteDraftMeta(nextDraft);
3597
+ }
2641
3598
  };
2642
3599
  pin.addEventListener("pointerdown", (event) => {
2643
3600
  if (event.button !== 0) return;
@@ -2870,6 +3827,9 @@ var WebReviewKitApp = class {
2870
3827
  setNoteDraft: (draft) => {
2871
3828
  this.noteDraft = draft;
2872
3829
  },
3830
+ setAreaDraft: (draft) => {
3831
+ this.areaDraft = draft;
3832
+ },
2873
3833
  setSelectingArea: (isSelectingArea) => {
2874
3834
  this.isSelectingArea = isSelectingArea;
2875
3835
  },
@@ -2895,6 +3855,7 @@ var WebReviewKitApp = class {
2895
3855
  this.render();
2896
3856
  }
2897
3857
  destroy() {
3858
+ this.view.clearDraftPreview();
2898
3859
  document.removeEventListener("keydown", this.handleKeyDown, true);
2899
3860
  window.removeEventListener("scroll", this.handleViewportChange, true);
2900
3861
  window.removeEventListener("resize", this.handleViewportChange);
@@ -3105,7 +4066,7 @@ var WebReviewKitApp = class {
3105
4066
  );
3106
4067
  const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
3107
4068
  const selection = elementSelection ?? getPointSelection(nextPoint);
3108
- const markerPoint = getSelectionCenter(selection);
4069
+ const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
3109
4070
  const reviewSelection = elementSelection ? {
3110
4071
  viewport: toPublicSelection(elementSelection),
3111
4072
  relative: getRelativeSelection(