@designfever/web-review-kit 0.2.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
@@ -176,7 +176,7 @@ function supabaseAdapter(options) {
176
176
  );
177
177
  }
178
178
  const rows = await unwrapResponse(
179
- request.order("updated_at", { ascending: false }),
179
+ request.order("created_at", { ascending: false }),
180
180
  "supabase list review items"
181
181
  );
182
182
  return (rows ?? []).flatMap((row) => {
@@ -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;
@@ -718,18 +846,39 @@ function decodeHtmlEntities(value) {
718
846
  }
719
847
  function getDomSourceHint(target) {
720
848
  const sourceElement = target.closest(
721
- "[data-file], [data-component], [data-section-index], [data-section-id]"
849
+ [
850
+ "[data-wrk-source-file]",
851
+ "[data-wrk-source-component]",
852
+ "[data-wrk-source-line]",
853
+ "[data-wrk-source-column]",
854
+ "[data-file]",
855
+ "[data-component]",
856
+ "[data-section-index]",
857
+ "[data-section-id]"
858
+ ].join(", ")
722
859
  );
723
860
  if (!sourceElement) return void 0;
724
- const dataset = sourceElement.dataset;
725
861
  const source = {
726
- component: dataset.component,
727
- file: dataset.file,
728
- sectionId: dataset.sectionId,
729
- sectionIndex: dataset.sectionIndex
862
+ component: getSourceAttribute(
863
+ sourceElement,
864
+ "data-wrk-source-component",
865
+ "data-component"
866
+ ),
867
+ file: getSourceAttribute(sourceElement, "data-wrk-source-file", "data-file"),
868
+ line: getSourceAttribute(sourceElement, "data-wrk-source-line"),
869
+ column: getSourceAttribute(sourceElement, "data-wrk-source-column"),
870
+ sectionId: getSourceAttribute(sourceElement, "data-section-id"),
871
+ sectionIndex: getSourceAttribute(sourceElement, "data-section-index")
730
872
  };
731
873
  return Object.values(source).some(Boolean) ? source : void 0;
732
874
  }
875
+ function getSourceAttribute(element, ...names) {
876
+ for (const name of names) {
877
+ const value = element.getAttribute(name)?.trim();
878
+ if (value) return value;
879
+ }
880
+ return void 0;
881
+ }
733
882
  function dedupeAnchorCandidates(candidates) {
734
883
  const seen = /* @__PURE__ */ new Set();
735
884
  return candidates.filter((candidate) => {
@@ -796,10 +945,38 @@ function getDomPath(element) {
796
945
  }
797
946
  return `body > ${parts.join(" > ")}`;
798
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
+ }
799
969
  function getTextFingerprint(element) {
800
970
  const text = element.textContent?.replace(/\s+/g, " ").trim();
801
971
  return text ? text.slice(0, 120) : void 0;
802
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
+ }
803
980
  function getTextFingerprintScore(expected, actual) {
804
981
  if (!expected) return 1;
805
982
  if (!actual) return 0.5;
@@ -1271,6 +1448,19 @@ function createStyleElement() {
1271
1448
  display: block;
1272
1449
  }
1273
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
+
1274
1464
  .dfwr-panel {
1275
1465
  position: fixed;
1276
1466
  right: 16px;
@@ -1764,6 +1954,40 @@ function createStyleElement() {
1764
1954
  box-shadow: var(--df-review-shadow-popover);
1765
1955
  }
1766
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
+
1767
1991
  .dfwr-area-draft {
1768
1992
  position: fixed;
1769
1993
  right: 16px;
@@ -1785,6 +2009,14 @@ function createStyleElement() {
1785
2009
  padding: 0;
1786
2010
  }
1787
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
+
1788
2020
  .dfwr-area-draft .dfwr-actions {
1789
2021
  padding: 0;
1790
2022
  }
@@ -1813,6 +2045,105 @@ function createStyleElement() {
1813
2045
  outline-offset: 1px;
1814
2046
  }
1815
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
+
1816
2147
  .dfwr-empty,
1817
2148
  .dfwr-error {
1818
2149
  margin: 0;
@@ -2033,16 +2364,6 @@ function createStyleElement() {
2033
2364
  }
2034
2365
 
2035
2366
  // src/core/review/format.ts
2036
- function formatAreaDraftMeta(draft) {
2037
- const parts = [`viewport ${formatSize(draft.viewport)}`];
2038
- if (draft.selection) {
2039
- parts.push(`rect ${formatSelection(draft.selection.viewport)}`);
2040
- }
2041
- if (draft.marker) {
2042
- parts.push(`point ${formatPoint(draft.marker.viewport)}`);
2043
- }
2044
- return parts.join(" / ");
2045
- }
2046
2367
  function formatNoteDraftMeta(draft) {
2047
2368
  const parts = [
2048
2369
  `viewport ${formatSize(draft.viewport)}`,
@@ -2108,17 +2429,29 @@ function formatAnchorMeta(anchor) {
2108
2429
  }
2109
2430
 
2110
2431
  // src/core/web.review.kit.view.ts
2432
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
2111
2433
  var WebReviewKitView = class {
2112
2434
  constructor(config) {
2113
2435
  this.config = config;
2114
2436
  }
2437
+ clearDraftPreview() {
2438
+ this.restoreDraftPreview();
2439
+ }
2115
2440
  render(shadow, hiddenItemsStyle) {
2116
2441
  const state = this.state;
2442
+ this.syncDraftPreview(
2443
+ state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2444
+ );
2117
2445
  shadow.replaceChildren();
2118
2446
  shadow.append(createStyleElement());
2119
2447
  shadow.append(hiddenItemsStyle);
2448
+ const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
2120
2449
  const shell = document.createElement("div");
2121
- 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(" ");
2122
2455
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
2123
2456
  if (this.config.options.ui?.panel !== false) {
2124
2457
  const panel = document.createElement("div");
@@ -2134,6 +2467,9 @@ var WebReviewKitView = class {
2134
2467
  shell.append(panel);
2135
2468
  }
2136
2469
  shell.append(this.createMarkerLayer());
2470
+ if (state.isOpen && hasDismissableDraft) {
2471
+ shell.append(this.createDraftCancelLayer());
2472
+ }
2137
2473
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
2138
2474
  shell.append(
2139
2475
  state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
@@ -2153,6 +2489,294 @@ var WebReviewKitView = class {
2153
2489
  get state() {
2154
2490
  return this.config.getState();
2155
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
+ }
2156
2780
  createHeader() {
2157
2781
  const header = document.createElement("div");
2158
2782
  header.className = "dfwr-header";
@@ -2244,15 +2868,20 @@ var WebReviewKitView = class {
2244
2868
  const group = document.createElement("div");
2245
2869
  group.className = "dfwr-note-draft";
2246
2870
  if (!environment) return group;
2247
- 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;
2248
2877
  if (draft.selection) {
2249
- group.append(
2250
- this.createSelectionHighlight(
2251
- toViewportSelection(draft.selection.viewport),
2252
- environment,
2253
- true
2254
- )
2878
+ const selection = toViewportSelection(draft.selection.viewport);
2879
+ selectionHighlight = this.createSelectionHighlight(
2880
+ isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2881
+ environment,
2882
+ true
2255
2883
  );
2884
+ group.append(selectionHighlight);
2256
2885
  }
2257
2886
  const pin = document.createElement("button");
2258
2887
  pin.className = "dfwr-note-pin";
@@ -2262,14 +2891,35 @@ var WebReviewKitView = class {
2262
2891
  pin.style.top = `${hostPoint.y}px`;
2263
2892
  const popover = document.createElement("div");
2264
2893
  const position = getPopoverPosition(hostPoint, environment);
2265
- popover.className = "dfwr-note-popover";
2266
- popover.style.left = `${position.left}px`;
2267
- 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
+ }
2268
2916
  const form = document.createElement("form");
2269
2917
  form.className = "dfwr-form";
2270
- const meta = document.createElement("div");
2271
- meta.className = "dfwr-item-date";
2272
- 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
+ }
2273
2923
  const textarea = document.createElement("textarea");
2274
2924
  textarea.className = "dfwr-textarea";
2275
2925
  textarea.placeholder = "Review comment";
@@ -2283,25 +2933,306 @@ var WebReviewKitView = class {
2283
2933
  comment: textarea.value
2284
2934
  });
2285
2935
  });
2286
- const actions = this.createFormActions("Save note", () => {
2936
+ const saveDraft = () => {
2287
2937
  const comment = textarea.value.trim();
2288
2938
  if (!comment) return;
2939
+ const currentDraft = this.state.noteDraft ?? draft;
2289
2940
  void this.config.actions.createItem({
2290
2941
  kind: "note",
2291
- comment,
2292
- viewport: draft.viewport,
2293
- anchor: draft.anchor,
2294
- marker: draft.marker,
2295
- 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
2296
2947
  });
2297
- });
2298
- form.append(meta, textarea, actions);
2299
- 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);
2300
2970
  group.append(pin, popover);
2301
- this.attachDraftPinDrag(pin, popover, meta, textarea);
2302
- 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);
2303
2994
  return group;
2304
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
+ }
2305
3236
  createAreaForm() {
2306
3237
  const form = document.createElement("form");
2307
3238
  form.className = "dfwr-form";
@@ -2313,14 +3244,20 @@ var WebReviewKitView = class {
2313
3244
  form.append(empty);
2314
3245
  return form;
2315
3246
  }
2316
- const meta = document.createElement("div");
2317
- meta.className = "dfwr-item-date";
2318
- meta.textContent = formatAreaDraftMeta(areaDraft);
2319
- form.append(meta);
3247
+ form.append(this.createAreaMetricsPanel(areaDraft));
2320
3248
  const textarea = document.createElement("textarea");
2321
3249
  textarea.className = "dfwr-textarea";
2322
3250
  textarea.placeholder = "Area comment";
2323
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
+ });
2324
3261
  const actions = this.createFormActions("Save area", () => {
2325
3262
  const comment = textarea.value.trim();
2326
3263
  const draft = this.state.areaDraft;
@@ -2337,6 +3274,25 @@ var WebReviewKitView = class {
2337
3274
  form.append(textarea, actions);
2338
3275
  return form;
2339
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
+ }
2340
3296
  createAreaDraftOverlay(draft) {
2341
3297
  const layer = document.createElement("div");
2342
3298
  layer.className = "dfwr-area-preview-layer";
@@ -2365,23 +3321,37 @@ var WebReviewKitView = class {
2365
3321
  createAreaDraftPopover(draft) {
2366
3322
  const environment = this.config.getEnvironment();
2367
3323
  const popover = document.createElement("div");
2368
- popover.className = "dfwr-area-draft";
3324
+ popover.className = "dfwr-area-draft is-composer";
2369
3325
  if (environment && draft.selection) {
2370
3326
  const selection = toHostSelection(
2371
3327
  toViewportSelection(draft.selection.viewport),
2372
3328
  environment
2373
3329
  );
2374
- const position = getAreaPopoverPosition(selection, environment);
2375
- popover.style.left = `${position.left}px`;
2376
- 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`;
2377
3339
  popover.style.right = "auto";
2378
3340
  }
2379
- 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
+ });
2380
3350
  return popover;
2381
3351
  }
2382
- createFormActions(saveLabel, onSave) {
3352
+ createFormActions(saveLabel, onSave, options) {
2383
3353
  const actions = document.createElement("div");
2384
- actions.className = "dfwr-actions";
3354
+ actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
2385
3355
  const save = document.createElement("button");
2386
3356
  save.className = "dfwr-button is-primary";
2387
3357
  save.type = "button";
@@ -2396,6 +3366,10 @@ var WebReviewKitView = class {
2396
3366
  this.config.actions.clearDrafts();
2397
3367
  this.config.actions.render();
2398
3368
  });
3369
+ if (options?.beforeSave?.length || options?.className) {
3370
+ actions.append(cancel, ...options.beforeSave ?? [], save);
3371
+ return actions;
3372
+ }
2399
3373
  actions.append(save, cancel);
2400
3374
  return actions;
2401
3375
  }
@@ -2600,11 +3574,13 @@ ${formatItemMeta(item)}`;
2600
3574
  if (!environment) return;
2601
3575
  const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
2602
3576
  const nextHostPoint = toHostPoint(nextPoint, environment);
2603
- const position = getPopoverPosition(nextHostPoint, environment);
2604
3577
  pin.style.left = `${nextHostPoint.x}px`;
2605
3578
  pin.style.top = `${nextHostPoint.y}px`;
2606
- popover.style.left = `${position.left}px`;
2607
- 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
+ }
2608
3584
  const noteDraft = this.state.noteDraft;
2609
3585
  if (!noteDraft) return;
2610
3586
  const nextDraft = {
@@ -2616,7 +3592,9 @@ ${formatItemMeta(item)}`;
2616
3592
  comment: textarea.value
2617
3593
  };
2618
3594
  this.config.actions.setNoteDraft(nextDraft);
2619
- meta.textContent = formatNoteDraftMeta(nextDraft);
3595
+ if (meta) {
3596
+ meta.textContent = formatNoteDraftMeta(nextDraft);
3597
+ }
2620
3598
  };
2621
3599
  pin.addEventListener("pointerdown", (event) => {
2622
3600
  if (event.button !== 0) return;
@@ -2770,6 +3748,13 @@ ${formatItemMeta(item)}`;
2770
3748
 
2771
3749
  // src/core/web.review.kit.app.ts
2772
3750
  var ROOT_ID = "df-web-review-kit-root";
3751
+ function isEditableEventTarget(event) {
3752
+ const path = event.composedPath?.() ?? [];
3753
+ const element = path[0] ?? event.target;
3754
+ if (!element || typeof element.tagName !== "string") return false;
3755
+ const tag = element.tagName;
3756
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element.isContentEditable === true;
3757
+ }
2773
3758
  function createWebReviewKit(options) {
2774
3759
  if (typeof window === "undefined" || typeof document === "undefined") {
2775
3760
  return createNoopController();
@@ -2802,7 +3787,7 @@ var WebReviewKitApp = class {
2802
3787
  event.stopPropagation();
2803
3788
  return;
2804
3789
  }
2805
- if (!isHotkey(event, this.hotkey)) return;
3790
+ if (isEditableEventTarget(event) || !isHotkey(event, this.hotkey)) return;
2806
3791
  event.preventDefault();
2807
3792
  event.stopPropagation();
2808
3793
  this.toggle();
@@ -2842,6 +3827,9 @@ var WebReviewKitApp = class {
2842
3827
  setNoteDraft: (draft) => {
2843
3828
  this.noteDraft = draft;
2844
3829
  },
3830
+ setAreaDraft: (draft) => {
3831
+ this.areaDraft = draft;
3832
+ },
2845
3833
  setSelectingArea: (isSelectingArea) => {
2846
3834
  this.isSelectingArea = isSelectingArea;
2847
3835
  },
@@ -2867,6 +3855,7 @@ var WebReviewKitApp = class {
2867
3855
  this.render();
2868
3856
  }
2869
3857
  destroy() {
3858
+ this.view.clearDraftPreview();
2870
3859
  document.removeEventListener("keydown", this.handleKeyDown, true);
2871
3860
  window.removeEventListener("scroll", this.handleViewportChange, true);
2872
3861
  window.removeEventListener("resize", this.handleViewportChange);
@@ -3077,7 +4066,7 @@ var WebReviewKitApp = class {
3077
4066
  );
3078
4067
  const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
3079
4068
  const selection = elementSelection ?? getPointSelection(nextPoint);
3080
- const markerPoint = getSelectionCenter(selection);
4069
+ const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
3081
4070
  const reviewSelection = elementSelection ? {
3082
4071
  viewport: toPublicSelection(elementSelection),
3083
4072
  relative: getRelativeSelection(