@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.
@@ -301,19 +301,6 @@ function getPopoverPosition(point, environment, options) {
301
301
  )
302
302
  };
303
303
  }
304
- function getAreaPopoverPosition(selection, environment) {
305
- return getPopoverPosition(
306
- {
307
- x: selection.left + selection.width,
308
- y: selection.top
309
- },
310
- environment,
311
- {
312
- width: 360,
313
- estimatedHeight: 206
314
- }
315
- );
316
- }
317
304
  function getPopoverBounds(environment) {
318
305
  if (!environment) {
319
306
  return {
@@ -378,6 +365,21 @@ function roundPoint(point) {
378
365
  }
379
366
 
380
367
  // src/core/dom.anchor.ts
368
+ var COMMON_ANCHOR_ATTRIBUTES = [
369
+ "data-testid",
370
+ "data-test-id",
371
+ "data-cy",
372
+ "data-test",
373
+ "data-qa",
374
+ "data-section-id",
375
+ "data-component"
376
+ ];
377
+ var SEMANTIC_ANCHOR_ATTRIBUTES = [
378
+ "aria-label",
379
+ "title",
380
+ "name",
381
+ "href"
382
+ ];
381
383
  function getDomAnchor(selection, configuredAttribute = "data-qa-id", environment) {
382
384
  const x = selection.left + selection.width / 2;
383
385
  const y = selection.top + selection.height / 2;
@@ -467,46 +469,66 @@ function getAnchorElement(anchor, environment) {
467
469
  return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment)?.element;
468
470
  }
469
471
  function createAnchorCandidates(target, configuredAttribute) {
470
- const candidates = [];
471
- const anchoredByAttribute = target.closest(`[${configuredAttribute}]`);
472
- if (anchoredByAttribute) {
473
- const value = anchoredByAttribute.getAttribute(configuredAttribute);
474
- if (value) {
475
- candidates.push({
476
- selector: `[${configuredAttribute}="${cssEscape(value)}"]`,
477
- strategy: "configured-attribute",
478
- confidence: 0.98,
479
- textFingerprint: getTextFingerprint(anchoredByAttribute)
480
- });
481
- }
482
- }
472
+ const targetCandidates = [];
473
+ const configuredAnchor = getExactAttributeAnchorCandidate(
474
+ target,
475
+ configuredAttribute,
476
+ 0.98,
477
+ "configured-attribute"
478
+ );
479
+ if (configuredAnchor) targetCandidates.push(configuredAnchor);
480
+ const targetAttributeAnchor = getAttributeAnchorCandidate(
481
+ target,
482
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
483
+ 0.9
484
+ );
485
+ if (targetAttributeAnchor) targetCandidates.push(targetAttributeAnchor);
483
486
  if (isMeaningfulId(target.id)) {
484
- candidates.push({
487
+ targetCandidates.push({
485
488
  selector: `#${cssEscape(target.id)}`,
486
489
  strategy: "id",
487
490
  confidence: 0.94,
488
491
  textFingerprint: getTextFingerprint(target)
489
492
  });
490
493
  }
494
+ const semanticAnchor = getAttributeAnchorCandidate(
495
+ target,
496
+ SEMANTIC_ANCHOR_ATTRIBUTES,
497
+ 0.84
498
+ );
499
+ if (semanticAnchor) targetCandidates.push(semanticAnchor);
491
500
  const targetClassName = getMeaningfulClassName(target);
492
501
  if (targetClassName) {
493
- candidates.push({
502
+ targetCandidates.push({
494
503
  selector: `${target.tagName.toLowerCase()}.${cssEscape(targetClassName)}`,
495
504
  strategy: "class",
496
505
  confidence: 0.82,
497
506
  textFingerprint: getTextFingerprint(target)
498
507
  });
499
508
  }
500
- candidates.push({
509
+ const scopedPath = getScopedDomPathCandidate(target, configuredAttribute);
510
+ if (scopedPath) targetCandidates.push(scopedPath);
511
+ const targetDomPath = {
501
512
  selector: getDomPath(target),
502
513
  strategy: "dom-path",
503
- confidence: 0.9,
514
+ confidence: targetCandidates.length > 0 ? 0.8 : 0.5,
504
515
  textFingerprint: getTextFingerprint(target)
505
- });
516
+ };
517
+ const parentCandidates = [];
506
518
  const parent = target.parentElement;
519
+ const parentConfiguredAnchor = parent ? findClosestAttributeAnchor(parent, [configuredAttribute], 0.72, {
520
+ strategy: "configured-attribute"
521
+ }) : void 0;
522
+ if (parentConfiguredAnchor) parentCandidates.push(parentConfiguredAnchor);
523
+ const anchoredByAttribute = parent ? findClosestAttributeAnchor(
524
+ parent,
525
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
526
+ 0.7
527
+ ) : void 0;
528
+ if (anchoredByAttribute) parentCandidates.push(anchoredByAttribute);
507
529
  const anchoredById = parent ? findClosest(parent, (element) => isMeaningfulId(element.id)) : void 0;
508
530
  if (anchoredById?.id) {
509
- candidates.push({
531
+ parentCandidates.push({
510
532
  selector: `#${cssEscape(anchoredById.id)}`,
511
533
  strategy: "id",
512
534
  confidence: 0.72,
@@ -516,7 +538,7 @@ function createAnchorCandidates(target, configuredAttribute) {
516
538
  const anchoredByClass = parent ? findClosest(parent, (element) => Boolean(getMeaningfulClassName(element))) : void 0;
517
539
  const className = anchoredByClass ? getMeaningfulClassName(anchoredByClass) : void 0;
518
540
  if (anchoredByClass && className) {
519
- candidates.push({
541
+ parentCandidates.push({
520
542
  selector: `${anchoredByClass.tagName.toLowerCase()}.${cssEscape(
521
543
  className
522
544
  )}`,
@@ -525,8 +547,107 @@ function createAnchorCandidates(target, configuredAttribute) {
525
547
  textFingerprint: getTextFingerprint(anchoredByClass)
526
548
  });
527
549
  }
550
+ const candidates = targetCandidates.length > 0 ? [...targetCandidates, targetDomPath, ...parentCandidates] : [...parentCandidates, targetDomPath];
528
551
  return dedupeAnchorCandidates(candidates);
529
552
  }
553
+ function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
554
+ for (const attributeName of attributeNames) {
555
+ const selector = `[${attributeName}]`;
556
+ const element = safeClosest(target, selector);
557
+ if (!element) continue;
558
+ const value = getStableAttributeValue(element, attributeName);
559
+ if (!value) continue;
560
+ return {
561
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
562
+ strategy: options?.strategy ?? "attribute",
563
+ confidence,
564
+ textFingerprint: getTextFingerprint(element)
565
+ };
566
+ }
567
+ return void 0;
568
+ }
569
+ function getExactAttributeAnchorCandidate(element, attributeName, confidence, strategy) {
570
+ const value = getStableAttributeValue(element, attributeName);
571
+ if (!value) return void 0;
572
+ return {
573
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
574
+ strategy,
575
+ confidence,
576
+ textFingerprint: getTextFingerprint(element)
577
+ };
578
+ }
579
+ function getAttributeAnchorCandidate(element, attributeNames, confidence) {
580
+ for (const attributeName of attributeNames) {
581
+ const value = getStableAttributeValue(element, attributeName);
582
+ if (!value) continue;
583
+ return {
584
+ selector: `${element.tagName.toLowerCase()}[${attributeName}="${cssEscape(
585
+ value
586
+ )}"]`,
587
+ strategy: "attribute",
588
+ confidence,
589
+ textFingerprint: getTextFingerprint(element)
590
+ };
591
+ }
592
+ return void 0;
593
+ }
594
+ function getScopedDomPathCandidate(target, configuredAttribute) {
595
+ const parent = target.parentElement;
596
+ if (!parent) return void 0;
597
+ const anchor = findStableAncestorSelector(parent, configuredAttribute);
598
+ if (!anchor) return void 0;
599
+ const selector = getDomPathBetween(anchor.element, target, anchor.selector);
600
+ if (!selector) return void 0;
601
+ return {
602
+ selector,
603
+ strategy: "dom-path",
604
+ confidence: anchor.confidence,
605
+ textFingerprint: getTextFingerprint(target)
606
+ };
607
+ }
608
+ function findStableAncestorSelector(start, configuredAttribute) {
609
+ let element = start;
610
+ const root = start.ownerDocument.documentElement;
611
+ while (element && element !== root) {
612
+ const configuredValue = getStableAttributeValue(element, configuredAttribute);
613
+ if (configuredValue) {
614
+ return {
615
+ element,
616
+ selector: `[${configuredAttribute}="${cssEscape(configuredValue)}"]`,
617
+ confidence: 0.88
618
+ };
619
+ }
620
+ const attributeAnchor = getAttributeAnchorCandidate(
621
+ element,
622
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
623
+ 0.84
624
+ );
625
+ if (attributeAnchor) {
626
+ return {
627
+ element,
628
+ selector: attributeAnchor.selector,
629
+ confidence: 0.84
630
+ };
631
+ }
632
+ if (isMeaningfulId(element.id)) {
633
+ return {
634
+ element,
635
+ selector: `#${cssEscape(element.id)}`,
636
+ confidence: 0.82
637
+ };
638
+ }
639
+ const className = getMeaningfulClassName(element);
640
+ if (className) {
641
+ return {
642
+ element,
643
+ selector: `${element.tagName.toLowerCase()}.${cssEscape(className)}`,
644
+ confidence: 0.76
645
+ };
646
+ }
647
+ element = element.parentElement;
648
+ }
649
+ return void 0;
650
+ }
530
651
  function getAnchorSourceElement(target, candidate, configuredAttribute) {
531
652
  if (candidate.strategy === "configured-attribute") {
532
653
  return target.closest(`[${configuredAttribute}]`);
@@ -538,6 +659,13 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
538
659
  return target;
539
660
  }
540
661
  }
662
+ function safeClosest(element, selector) {
663
+ try {
664
+ return element.closest(selector);
665
+ } catch {
666
+ return null;
667
+ }
668
+ }
541
669
  function getElementHtmlSnippet(element, maxLength = 1e3) {
542
670
  const html = decodeHtmlEntities(element.outerHTML.replace(/\s+/g, " ").trim());
543
671
  if (html.length <= maxLength) return html;
@@ -560,18 +688,39 @@ function decodeHtmlEntities(value) {
560
688
  }
561
689
  function getDomSourceHint(target) {
562
690
  const sourceElement = target.closest(
563
- "[data-file], [data-component], [data-section-index], [data-section-id]"
691
+ [
692
+ "[data-wrk-source-file]",
693
+ "[data-wrk-source-component]",
694
+ "[data-wrk-source-line]",
695
+ "[data-wrk-source-column]",
696
+ "[data-file]",
697
+ "[data-component]",
698
+ "[data-section-index]",
699
+ "[data-section-id]"
700
+ ].join(", ")
564
701
  );
565
702
  if (!sourceElement) return void 0;
566
- const dataset = sourceElement.dataset;
567
703
  const source = {
568
- component: dataset.component,
569
- file: dataset.file,
570
- sectionId: dataset.sectionId,
571
- sectionIndex: dataset.sectionIndex
704
+ component: getSourceAttribute(
705
+ sourceElement,
706
+ "data-wrk-source-component",
707
+ "data-component"
708
+ ),
709
+ file: getSourceAttribute(sourceElement, "data-wrk-source-file", "data-file"),
710
+ line: getSourceAttribute(sourceElement, "data-wrk-source-line"),
711
+ column: getSourceAttribute(sourceElement, "data-wrk-source-column"),
712
+ sectionId: getSourceAttribute(sourceElement, "data-section-id"),
713
+ sectionIndex: getSourceAttribute(sourceElement, "data-section-index")
572
714
  };
573
715
  return Object.values(source).some(Boolean) ? source : void 0;
574
716
  }
717
+ function getSourceAttribute(element, ...names) {
718
+ for (const name of names) {
719
+ const value = element.getAttribute(name)?.trim();
720
+ if (value) return value;
721
+ }
722
+ return void 0;
723
+ }
575
724
  function dedupeAnchorCandidates(candidates) {
576
725
  const seen = /* @__PURE__ */ new Set();
577
726
  return candidates.filter((candidate) => {
@@ -638,10 +787,38 @@ function getDomPath(element) {
638
787
  }
639
788
  return `body > ${parts.join(" > ")}`;
640
789
  }
790
+ function getDomPathBetween(ancestor, target, ancestorSelector) {
791
+ const parts = [];
792
+ let current = target;
793
+ while (current && current !== ancestor) {
794
+ parts.unshift(getDomPathPart(current));
795
+ current = current.parentElement;
796
+ }
797
+ if (current !== ancestor || parts.length === 0) return void 0;
798
+ return `${ancestorSelector} > ${parts.join(" > ")}`;
799
+ }
800
+ function getDomPathPart(element) {
801
+ const parent = element.parentElement;
802
+ const tag = element.tagName.toLowerCase();
803
+ if (!parent) return tag;
804
+ const currentTagName = element.tagName;
805
+ const siblings = Array.from(parent.children).filter(
806
+ (child) => child.tagName === currentTagName
807
+ );
808
+ const index = siblings.indexOf(element) + 1;
809
+ return `${tag}:nth-of-type(${index})`;
810
+ }
641
811
  function getTextFingerprint(element) {
642
812
  const text = element.textContent?.replace(/\s+/g, " ").trim();
643
813
  return text ? text.slice(0, 120) : void 0;
644
814
  }
815
+ function getStableAttributeValue(element, attributeName) {
816
+ const value = element.getAttribute(attributeName)?.trim();
817
+ if (!value || value.length > 160) return void 0;
818
+ if (/^(true|false)$/i.test(value)) return void 0;
819
+ if (/^\d+$/.test(value) && value.length < 3) return void 0;
820
+ return value;
821
+ }
645
822
  function getTextFingerprintScore(expected, actual) {
646
823
  if (!expected) return 1;
647
824
  if (!actual) return 0.5;
@@ -1017,6 +1194,19 @@ function createStyleElement() {
1017
1194
  display: block;
1018
1195
  }
1019
1196
 
1197
+ .dfwr-shell.has-dismissible-draft {
1198
+ z-index: 900;
1199
+ }
1200
+
1201
+ .dfwr-draft-cancel-layer {
1202
+ position: fixed;
1203
+ inset: 0;
1204
+ z-index: 2;
1205
+ pointer-events: auto;
1206
+ background: transparent;
1207
+ cursor: default;
1208
+ }
1209
+
1020
1210
  .dfwr-panel {
1021
1211
  position: fixed;
1022
1212
  right: 16px;
@@ -1510,6 +1700,40 @@ function createStyleElement() {
1510
1700
  box-shadow: var(--df-review-shadow-popover);
1511
1701
  }
1512
1702
 
1703
+ .dfwr-note-popover.is-composer,
1704
+ .dfwr-area-draft.is-composer {
1705
+ max-height: min(360px, calc(100vh - 32px));
1706
+ overflow: auto;
1707
+ border-color: rgba(99, 215, 199, 0.56);
1708
+ }
1709
+
1710
+ .dfwr-note-popover.is-dragging,
1711
+ .dfwr-area-draft.is-dragging {
1712
+ user-select: none;
1713
+ }
1714
+
1715
+ .dfwr-draft-drag-handle {
1716
+ display: block;
1717
+ width: 42px;
1718
+ height: 6px;
1719
+ margin: 0 auto 10px;
1720
+ padding: 0;
1721
+ cursor: grab;
1722
+ pointer-events: auto;
1723
+ background: rgba(247, 247, 242, 0.28);
1724
+ border: 0;
1725
+ border-radius: 999px;
1726
+ }
1727
+
1728
+ .dfwr-draft-drag-handle:hover,
1729
+ .dfwr-draft-drag-handle:focus-visible {
1730
+ background: rgba(215, 255, 95, 0.62);
1731
+ }
1732
+
1733
+ .dfwr-draft-drag-handle:active {
1734
+ cursor: grabbing;
1735
+ }
1736
+
1513
1737
  .dfwr-area-draft {
1514
1738
  position: fixed;
1515
1739
  right: 16px;
@@ -1531,6 +1755,14 @@ function createStyleElement() {
1531
1755
  padding: 0;
1532
1756
  }
1533
1757
 
1758
+ .dfwr-note-actions {
1759
+ justify-content: flex-end;
1760
+ }
1761
+
1762
+ .dfwr-note-actions .dfwr-button:first-child {
1763
+ margin-right: auto;
1764
+ }
1765
+
1534
1766
  .dfwr-area-draft .dfwr-actions {
1535
1767
  padding: 0;
1536
1768
  }
@@ -1559,6 +1791,105 @@ function createStyleElement() {
1559
1791
  outline-offset: 1px;
1560
1792
  }
1561
1793
 
1794
+ .dfwr-adjust-panel {
1795
+ display: grid;
1796
+ gap: 4px;
1797
+ padding: 8px 10px;
1798
+ border: 1px solid rgba(255, 255, 255, 0.12);
1799
+ border-radius: var(--df-review-radius-sm);
1800
+ background: rgba(255, 255, 255, 0.04);
1801
+ }
1802
+
1803
+ .dfwr-adjust-panel-header {
1804
+ display: flex;
1805
+ align-items: center;
1806
+ justify-content: space-between;
1807
+ gap: 10px;
1808
+ min-width: 0;
1809
+ }
1810
+
1811
+ .dfwr-adjust-panel-header .dfwr-adjust-help {
1812
+ flex: 1 1 auto;
1813
+ min-width: 0;
1814
+ }
1815
+
1816
+ .dfwr-adjust-panel.is-active {
1817
+ border-color: rgba(215, 255, 95, 0.5);
1818
+ background: var(--df-review-color-accent-soft);
1819
+ }
1820
+
1821
+ .dfwr-adjust-help,
1822
+ .dfwr-adjust-status {
1823
+ margin: 0;
1824
+ color: var(--df-review-color-text-muted);
1825
+ font-size: var(--df-review-font-size-xs);
1826
+ line-height: 1.35;
1827
+ }
1828
+
1829
+ .dfwr-adjust-status {
1830
+ color: var(--df-review-color-text);
1831
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1832
+ }
1833
+
1834
+ .dfwr-adjust-toggle {
1835
+ flex: 0 0 auto;
1836
+ display: inline-flex;
1837
+ align-items: center;
1838
+ justify-content: center;
1839
+ width: 34px;
1840
+ height: 30px;
1841
+ padding: 0;
1842
+ border: 1px solid rgba(255, 255, 255, 0.2);
1843
+ border-radius: var(--df-review-radius-sm);
1844
+ background: rgba(255, 255, 255, 0.04);
1845
+ color: var(--df-review-color-text);
1846
+ cursor: pointer;
1847
+ font: inherit;
1848
+ font-size: 14px;
1849
+ font-weight: 800;
1850
+ line-height: 1;
1851
+ }
1852
+
1853
+ .dfwr-adjust-toggle:hover,
1854
+ .dfwr-adjust-toggle:focus-visible,
1855
+ .dfwr-adjust-toggle.is-active {
1856
+ border-color: rgba(215, 255, 95, 0.68);
1857
+ background: var(--df-review-color-accent-soft);
1858
+ outline: none;
1859
+ }
1860
+
1861
+ .dfwr-adjust-toggle svg {
1862
+ width: 18px;
1863
+ height: 18px;
1864
+ pointer-events: none;
1865
+ }
1866
+
1867
+ .dfwr-adjust-hud {
1868
+ position: fixed;
1869
+ z-index: 5;
1870
+ display: inline-flex;
1871
+ align-items: center;
1872
+ min-height: 22px;
1873
+ padding: 0 8px;
1874
+ border: 1px solid rgba(99, 215, 199, 0.72);
1875
+ border-radius: var(--df-review-radius-sm);
1876
+ background: rgba(21, 25, 29, 0.92);
1877
+ box-shadow:
1878
+ 0 0 0 3px rgba(99, 215, 199, 0.14),
1879
+ 0 8px 18px rgba(0, 0, 0, 0.26);
1880
+ color: #63d7c7;
1881
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1882
+ font-size: var(--df-review-font-size-2xs);
1883
+ font-weight: 800;
1884
+ line-height: 1;
1885
+ pointer-events: none;
1886
+ white-space: nowrap;
1887
+ }
1888
+
1889
+ .dfwr-adjust-hud[hidden] {
1890
+ display: none;
1891
+ }
1892
+
1562
1893
  .dfwr-empty,
1563
1894
  .dfwr-error {
1564
1895
  margin: 0;
@@ -1779,16 +2110,6 @@ function createStyleElement() {
1779
2110
  }
1780
2111
 
1781
2112
  // src/core/review/format.ts
1782
- function formatAreaDraftMeta(draft) {
1783
- const parts = [`viewport ${formatSize(draft.viewport)}`];
1784
- if (draft.selection) {
1785
- parts.push(`rect ${formatSelection(draft.selection.viewport)}`);
1786
- }
1787
- if (draft.marker) {
1788
- parts.push(`point ${formatPoint(draft.marker.viewport)}`);
1789
- }
1790
- return parts.join(" / ");
1791
- }
1792
2113
  function formatNoteDraftMeta(draft) {
1793
2114
  const parts = [
1794
2115
  `viewport ${formatSize(draft.viewport)}`,
@@ -1854,17 +2175,29 @@ function formatAnchorMeta(anchor) {
1854
2175
  }
1855
2176
 
1856
2177
  // src/core/web.review.kit.view.ts
2178
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
1857
2179
  var WebReviewKitView = class {
1858
2180
  constructor(config) {
1859
2181
  this.config = config;
1860
2182
  }
2183
+ clearDraftPreview() {
2184
+ this.restoreDraftPreview();
2185
+ }
1861
2186
  render(shadow, hiddenItemsStyle) {
1862
2187
  const state = this.state;
2188
+ this.syncDraftPreview(
2189
+ state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2190
+ );
1863
2191
  shadow.replaceChildren();
1864
2192
  shadow.append(createStyleElement());
1865
2193
  shadow.append(hiddenItemsStyle);
2194
+ const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
1866
2195
  const shell = document.createElement("div");
1867
- shell.className = `dfwr-shell${state.isOpen ? " is-open" : ""}`;
2196
+ shell.className = [
2197
+ "dfwr-shell",
2198
+ state.isOpen ? "is-open" : "",
2199
+ hasDismissableDraft ? "has-dismissible-draft" : ""
2200
+ ].filter(Boolean).join(" ");
1868
2201
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
1869
2202
  if (this.config.options.ui?.panel !== false) {
1870
2203
  const panel = document.createElement("div");
@@ -1880,6 +2213,9 @@ var WebReviewKitView = class {
1880
2213
  shell.append(panel);
1881
2214
  }
1882
2215
  shell.append(this.createMarkerLayer());
2216
+ if (state.isOpen && hasDismissableDraft) {
2217
+ shell.append(this.createDraftCancelLayer());
2218
+ }
1883
2219
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
1884
2220
  shell.append(
1885
2221
  state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
@@ -1899,6 +2235,294 @@ var WebReviewKitView = class {
1899
2235
  get state() {
1900
2236
  return this.config.getState();
1901
2237
  }
2238
+ createDraftCancelLayer() {
2239
+ const layer = document.createElement("div");
2240
+ layer.className = "dfwr-draft-cancel-layer";
2241
+ layer.setAttribute("aria-hidden", "true");
2242
+ const cancel = (event) => {
2243
+ event.preventDefault();
2244
+ event.stopPropagation();
2245
+ event.stopImmediatePropagation();
2246
+ this.config.actions.setModeState("idle");
2247
+ this.config.actions.clearDrafts();
2248
+ this.config.actions.setSelectingArea(false);
2249
+ this.config.actions.render();
2250
+ };
2251
+ layer.addEventListener("pointerdown", (event) => {
2252
+ if (event.button !== 0) return;
2253
+ cancel(event);
2254
+ });
2255
+ layer.addEventListener("click", cancel);
2256
+ return layer;
2257
+ }
2258
+ getDraftAdjustmentMetrics(draft) {
2259
+ const adjustment = draft.adjustment;
2260
+ const x = adjustment?.x ?? 0;
2261
+ const y = adjustment?.y ?? 0;
2262
+ const scale = adjustment?.scale ?? 0;
2263
+ const {
2264
+ scale: viewportScale,
2265
+ designWidth,
2266
+ presetLabel
2267
+ } = this.getDraftViewportScale(draft.viewport);
2268
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2269
+ const scaleCssDelta = scale * viewportScale;
2270
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
2271
+ 1 / selection.width,
2272
+ (selection.width + scaleCssDelta) / selection.width
2273
+ ) : 1;
2274
+ return {
2275
+ x,
2276
+ y,
2277
+ scale,
2278
+ cssX: x * viewportScale,
2279
+ cssY: y * viewportScale,
2280
+ scaleFactor,
2281
+ viewportScale,
2282
+ designWidth,
2283
+ presetLabel,
2284
+ viewportWidth: draft.viewport.width
2285
+ };
2286
+ }
2287
+ hasDraftAdjustment(draft) {
2288
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2289
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
2290
+ }
2291
+ getAdjustedDraftPoint(point, draft) {
2292
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2293
+ return {
2294
+ x: point.x + metrics.cssX,
2295
+ y: point.y + metrics.cssY
2296
+ };
2297
+ }
2298
+ getAdjustedDraftSelection(selection, draft) {
2299
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2300
+ return {
2301
+ ...selection,
2302
+ left: selection.left + metrics.cssX,
2303
+ top: selection.top + metrics.cssY,
2304
+ width: selection.width * metrics.scaleFactor,
2305
+ height: selection.height * metrics.scaleFactor
2306
+ };
2307
+ }
2308
+ getDraftViewportScale(viewport) {
2309
+ const preset = findReviewViewportPreset(
2310
+ viewport,
2311
+ this.config.options.viewports?.presets
2312
+ );
2313
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2314
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2315
+ return { scale, designWidth, presetLabel: preset.label };
2316
+ }
2317
+ getDraftComposerWidth(environment) {
2318
+ const bounds = environment.overlayRect;
2319
+ const margin = 12;
2320
+ return Math.min(360, Math.max(240, bounds.width - margin * 2));
2321
+ }
2322
+ getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
2323
+ const margin = 12;
2324
+ const width = size?.width ?? this.getDraftComposerWidth(environment);
2325
+ const height = size?.height ?? 236;
2326
+ return {
2327
+ x: clamp(
2328
+ position.x,
2329
+ bounds.left + margin,
2330
+ bounds.left + bounds.width - width - margin
2331
+ ),
2332
+ y: clamp(
2333
+ position.y,
2334
+ bounds.top + margin,
2335
+ bounds.top + bounds.height - height - margin
2336
+ )
2337
+ };
2338
+ }
2339
+ getHostComposerBounds() {
2340
+ const root = document.documentElement;
2341
+ return {
2342
+ left: 0,
2343
+ top: 0,
2344
+ width: root.clientWidth || window.innerWidth,
2345
+ height: root.clientHeight || window.innerHeight
2346
+ };
2347
+ }
2348
+ getInitialDraftComposerPosition(selection, environment, size) {
2349
+ const bounds = this.getHostComposerBounds();
2350
+ const margin = 12;
2351
+ const gap = 20;
2352
+ if (!selection) {
2353
+ return this.getClampedComposerPosition(
2354
+ {
2355
+ x: environment.overlayRect.left + margin,
2356
+ y: environment.overlayRect.top + margin
2357
+ },
2358
+ environment,
2359
+ size,
2360
+ bounds
2361
+ );
2362
+ }
2363
+ const preferredX = selection.left + selection.width + gap;
2364
+ const maxX = bounds.left + bounds.width - size.width - margin;
2365
+ const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
2366
+ return this.getClampedComposerPosition(
2367
+ {
2368
+ x,
2369
+ y: selection.top
2370
+ },
2371
+ environment,
2372
+ size,
2373
+ bounds
2374
+ );
2375
+ }
2376
+ getDraftComposerPosition({
2377
+ selection,
2378
+ environment,
2379
+ composerPosition,
2380
+ estimatedHeight
2381
+ }) {
2382
+ const width = this.getDraftComposerWidth(environment);
2383
+ if (composerPosition) {
2384
+ const clamped = this.getClampedComposerPosition(
2385
+ composerPosition,
2386
+ environment,
2387
+ { width, height: estimatedHeight },
2388
+ this.getHostComposerBounds()
2389
+ );
2390
+ return { width, left: clamped.x, top: clamped.y };
2391
+ }
2392
+ const position = this.getInitialDraftComposerPosition(selection, environment, {
2393
+ width,
2394
+ height: estimatedHeight
2395
+ });
2396
+ return { width, left: position.x, top: position.y };
2397
+ }
2398
+ getSelectionMqMetrics(selection, viewport) {
2399
+ const { scale } = this.getDraftViewportScale(viewport);
2400
+ const ratio = scale > 0 ? 1 / scale : 1;
2401
+ return {
2402
+ x: selection.left * ratio,
2403
+ y: selection.top * ratio,
2404
+ width: selection.width * ratio,
2405
+ height: selection.height * ratio
2406
+ };
2407
+ }
2408
+ formatSignedPx(value) {
2409
+ if (value === 0) return "+0px";
2410
+ return `${value > 0 ? "+" : ""}${value}px`;
2411
+ }
2412
+ formatRoundedPx(value) {
2413
+ return `${Math.round(value)}px`;
2414
+ }
2415
+ getAdjustmentLabel() {
2416
+ return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
2417
+ }
2418
+ getSelectionMetricLines(selection, viewport) {
2419
+ if (!selection) return ["area", "x none / y none", "w none / h none"];
2420
+ const metrics = this.getSelectionMqMetrics(selection, viewport);
2421
+ return [
2422
+ "area",
2423
+ `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
2424
+ metrics.y
2425
+ )}`,
2426
+ `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
2427
+ metrics.height
2428
+ )}`
2429
+ ];
2430
+ }
2431
+ getAreaDraftMetricSelection(draft) {
2432
+ if (!draft.selection) return void 0;
2433
+ return toViewportSelection(draft.selection.viewport);
2434
+ }
2435
+ formatDraftAdjustmentStatus(draft) {
2436
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2437
+ return [
2438
+ `x ${this.formatSignedPx(metrics.x)}`,
2439
+ `y ${this.formatSignedPx(metrics.y)}`,
2440
+ `scale ${this.formatSignedPx(metrics.scale)}`
2441
+ ].join(" / ");
2442
+ }
2443
+ getDraftAdjustmentMetricLines(draft) {
2444
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2445
+ return [
2446
+ `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
2447
+ metrics.y
2448
+ )}`,
2449
+ `scale ${this.formatSignedPx(metrics.scale)}`
2450
+ ];
2451
+ }
2452
+ withDraftAdjustmentComment(comment, draft) {
2453
+ if (!this.hasDraftAdjustment(draft)) return comment;
2454
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2455
+ const adjustment = [
2456
+ `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
2457
+ metrics.x
2458
+ )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
2459
+ metrics.scale
2460
+ )}`,
2461
+ `(${metrics.presetLabel} viewport, ${Math.round(
2462
+ metrics.viewportWidth
2463
+ )}/design ${Math.round(metrics.designWidth)})`
2464
+ ].join(" ");
2465
+ return `${comment.trim()}
2466
+ ${adjustment}`;
2467
+ }
2468
+ getStyleableDraftElement(draft, environment) {
2469
+ if (!draft.anchor) return void 0;
2470
+ const element = resolveAnchorElement(draft.anchor, environment)?.element;
2471
+ if (!element) return void 0;
2472
+ if ("style" in element) return element;
2473
+ return void 0;
2474
+ }
2475
+ syncDraftPreview(draft) {
2476
+ const environment = this.config.getEnvironment();
2477
+ if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
2478
+ this.restoreDraftPreview();
2479
+ return;
2480
+ }
2481
+ const element = this.getStyleableDraftElement(draft, environment);
2482
+ if (!element) {
2483
+ this.restoreDraftPreview();
2484
+ return;
2485
+ }
2486
+ if (this.draftPreview?.element !== element) {
2487
+ this.restoreDraftPreview();
2488
+ }
2489
+ if (!this.draftPreview) {
2490
+ const computedTransform = environment.window.getComputedStyle(element).transform;
2491
+ this.draftPreview = {
2492
+ element,
2493
+ transform: element.style.transform,
2494
+ transformOrigin: element.style.transformOrigin,
2495
+ transition: element.style.transition,
2496
+ willChange: element.style.willChange,
2497
+ baseTransform: element.style.transform || (computedTransform && computedTransform !== "none" ? computedTransform : "")
2498
+ };
2499
+ }
2500
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2501
+ const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
2502
+ metrics.cssY
2503
+ )}px)`;
2504
+ const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
2505
+ element.style.transition = "none";
2506
+ element.style.willChange = "transform";
2507
+ element.style.transformOrigin = "top left";
2508
+ element.style.transform = [
2509
+ this.draftPreview.baseTransform,
2510
+ translate,
2511
+ scale
2512
+ ].filter(Boolean).join(" ");
2513
+ }
2514
+ restoreDraftPreview() {
2515
+ if (!this.draftPreview) return;
2516
+ const { element, transform, transformOrigin, transition, willChange } = this.draftPreview;
2517
+ element.style.transform = transform;
2518
+ element.style.transformOrigin = transformOrigin;
2519
+ element.style.transition = transition;
2520
+ element.style.willChange = willChange;
2521
+ this.draftPreview = void 0;
2522
+ }
2523
+ toCssNumber(value) {
2524
+ return Math.round(value * 1e3) / 1e3;
2525
+ }
1902
2526
  createHeader() {
1903
2527
  const header = document.createElement("div");
1904
2528
  header.className = "dfwr-header";
@@ -1990,15 +2614,20 @@ var WebReviewKitView = class {
1990
2614
  const group = document.createElement("div");
1991
2615
  group.className = "dfwr-note-draft";
1992
2616
  if (!environment) return group;
1993
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
2617
+ const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
2618
+ const hostPoint = toHostPoint(
2619
+ isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
2620
+ environment
2621
+ );
2622
+ let selectionHighlight;
1994
2623
  if (draft.selection) {
1995
- group.append(
1996
- this.createSelectionHighlight(
1997
- toViewportSelection(draft.selection.viewport),
1998
- environment,
1999
- true
2000
- )
2624
+ const selection = toViewportSelection(draft.selection.viewport);
2625
+ selectionHighlight = this.createSelectionHighlight(
2626
+ isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2627
+ environment,
2628
+ true
2001
2629
  );
2630
+ group.append(selectionHighlight);
2002
2631
  }
2003
2632
  const pin = document.createElement("button");
2004
2633
  pin.className = "dfwr-note-pin";
@@ -2008,14 +2637,35 @@ var WebReviewKitView = class {
2008
2637
  pin.style.top = `${hostPoint.y}px`;
2009
2638
  const popover = document.createElement("div");
2010
2639
  const position = getPopoverPosition(hostPoint, environment);
2011
- popover.className = "dfwr-note-popover";
2012
- popover.style.left = `${position.left}px`;
2013
- popover.style.top = `${position.top}px`;
2640
+ popover.className = `dfwr-note-popover${isElementDraft ? " is-composer" : ""}`;
2641
+ if (isElementDraft) {
2642
+ const selection = draft.selection ? toHostSelection(
2643
+ this.getAdjustedDraftSelection(
2644
+ toViewportSelection(draft.selection.viewport),
2645
+ draft
2646
+ ),
2647
+ environment
2648
+ ) : void 0;
2649
+ const composer = this.getDraftComposerPosition({
2650
+ selection,
2651
+ environment,
2652
+ composerPosition: draft.composerPosition,
2653
+ estimatedHeight: 252
2654
+ });
2655
+ popover.style.left = `${composer.left}px`;
2656
+ popover.style.top = `${composer.top}px`;
2657
+ popover.style.width = `${composer.width}px`;
2658
+ } else {
2659
+ popover.style.left = `${position.left}px`;
2660
+ popover.style.top = `${position.top}px`;
2661
+ }
2014
2662
  const form = document.createElement("form");
2015
2663
  form.className = "dfwr-form";
2016
- const meta = document.createElement("div");
2017
- meta.className = "dfwr-item-date";
2018
- meta.textContent = formatNoteDraftMeta(draft);
2664
+ const meta = isElementDraft ? void 0 : document.createElement("div");
2665
+ if (meta) {
2666
+ meta.className = "dfwr-item-date";
2667
+ meta.textContent = formatNoteDraftMeta(draft);
2668
+ }
2019
2669
  const textarea = document.createElement("textarea");
2020
2670
  textarea.className = "dfwr-textarea";
2021
2671
  textarea.placeholder = "Review comment";
@@ -2029,25 +2679,306 @@ var WebReviewKitView = class {
2029
2679
  comment: textarea.value
2030
2680
  });
2031
2681
  });
2032
- const actions = this.createFormActions("Save note", () => {
2682
+ const saveDraft = () => {
2033
2683
  const comment = textarea.value.trim();
2034
2684
  if (!comment) return;
2685
+ const currentDraft = this.state.noteDraft ?? draft;
2035
2686
  void this.config.actions.createItem({
2036
2687
  kind: "note",
2037
- comment,
2038
- viewport: draft.viewport,
2039
- anchor: draft.anchor,
2040
- marker: draft.marker,
2041
- selection: draft.selection
2688
+ comment: this.withDraftAdjustmentComment(comment, currentDraft),
2689
+ viewport: currentDraft.viewport,
2690
+ anchor: currentDraft.anchor,
2691
+ marker: currentDraft.marker,
2692
+ selection: currentDraft.selection
2042
2693
  });
2043
- });
2044
- form.append(meta, textarea, actions);
2045
- popover.append(form);
2694
+ };
2695
+ const adjustmentHud = isElementDraft ? this.createAdjustmentHud(draft, environment) : void 0;
2696
+ if (adjustmentHud) {
2697
+ group.append(adjustmentHud);
2698
+ }
2699
+ const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
2700
+ draft,
2701
+ hud: adjustmentHud,
2702
+ pin,
2703
+ popover,
2704
+ selectionHighlight,
2705
+ textarea
2706
+ }) : void 0;
2707
+ const actions = this.createFormActions("Save note", saveDraft);
2708
+ form.append(
2709
+ ...meta ? [meta] : [],
2710
+ ...adjustmentControls ? [adjustmentControls.panel] : [],
2711
+ textarea,
2712
+ actions
2713
+ );
2714
+ const dragHandle = isElementDraft ? this.createDraftDragHandle("Move DOM composer") : void 0;
2715
+ popover.append(...dragHandle ? [dragHandle] : [], form);
2046
2716
  group.append(pin, popover);
2047
- this.attachDraftPinDrag(pin, popover, meta, textarea);
2048
- window.setTimeout(() => textarea.focus(), 0);
2717
+ if (dragHandle) {
2718
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
2719
+ const noteDraft = this.state.noteDraft ?? draft;
2720
+ this.config.actions.setNoteDraft({
2721
+ ...noteDraft,
2722
+ composerPosition,
2723
+ comment: textarea.value
2724
+ });
2725
+ });
2726
+ }
2727
+ this.attachDraftPinDrag(
2728
+ pin,
2729
+ isElementDraft ? void 0 : popover,
2730
+ meta,
2731
+ textarea
2732
+ );
2733
+ window.setTimeout(() => {
2734
+ if (draft.adjustment?.isActive) {
2735
+ adjustmentControls?.focusTarget.focus();
2736
+ return;
2737
+ }
2738
+ textarea.focus();
2739
+ }, 0);
2049
2740
  return group;
2050
2741
  }
2742
+ createDraftDragHandle(label) {
2743
+ const handle = document.createElement("button");
2744
+ handle.className = "dfwr-draft-drag-handle";
2745
+ handle.type = "button";
2746
+ handle.setAttribute("aria-label", label);
2747
+ return handle;
2748
+ }
2749
+ createIcon(paths) {
2750
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2751
+ svg.setAttribute("aria-hidden", "true");
2752
+ svg.setAttribute("viewBox", "0 0 24 24");
2753
+ svg.setAttribute("fill", "none");
2754
+ svg.setAttribute("stroke", "currentColor");
2755
+ svg.setAttribute("stroke-width", "2.4");
2756
+ svg.setAttribute("stroke-linecap", "round");
2757
+ svg.setAttribute("stroke-linejoin", "round");
2758
+ paths.forEach((d) => {
2759
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2760
+ path.setAttribute("d", d);
2761
+ svg.append(path);
2762
+ });
2763
+ return svg;
2764
+ }
2765
+ setAdjustmentToggleIcon(button, isActive) {
2766
+ const paths = isActive ? ["M20 6 9 17l-5-5"] : [
2767
+ "M12 2v20",
2768
+ "M2 12h20",
2769
+ "m9 5 3-3 3 3",
2770
+ "m9 19 3 3 3-3",
2771
+ "m5 9-3 3 3 3",
2772
+ "m19 9 3 3-3 3"
2773
+ ];
2774
+ button.replaceChildren(this.createIcon(paths));
2775
+ }
2776
+ attachDraftComposerDrag(popover, handle, onMove) {
2777
+ let isDragging = false;
2778
+ let offsetX = 0;
2779
+ let offsetY = 0;
2780
+ const movePopover = (event) => {
2781
+ const environment = this.config.getEnvironment();
2782
+ if (!environment) return;
2783
+ const position = this.getClampedComposerPosition(
2784
+ {
2785
+ x: event.clientX - offsetX,
2786
+ y: event.clientY - offsetY
2787
+ },
2788
+ environment,
2789
+ {
2790
+ width: popover.offsetWidth,
2791
+ height: popover.offsetHeight
2792
+ },
2793
+ this.getHostComposerBounds()
2794
+ );
2795
+ popover.style.left = `${position.x}px`;
2796
+ popover.style.top = `${position.y}px`;
2797
+ onMove(position);
2798
+ };
2799
+ handle.addEventListener("pointerdown", (event) => {
2800
+ if (event.button !== 0) return;
2801
+ const rect = popover.getBoundingClientRect();
2802
+ offsetX = event.clientX - rect.left;
2803
+ offsetY = event.clientY - rect.top;
2804
+ isDragging = true;
2805
+ event.preventDefault();
2806
+ event.stopPropagation();
2807
+ handle.setPointerCapture(event.pointerId);
2808
+ popover.classList.add("is-dragging");
2809
+ });
2810
+ handle.addEventListener("pointermove", (event) => {
2811
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
2812
+ event.preventDefault();
2813
+ movePopover(event);
2814
+ });
2815
+ const stopDrag = (event) => {
2816
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
2817
+ event.preventDefault();
2818
+ event.stopPropagation();
2819
+ isDragging = false;
2820
+ handle.releasePointerCapture(event.pointerId);
2821
+ popover.classList.remove("is-dragging");
2822
+ movePopover(event);
2823
+ };
2824
+ handle.addEventListener("pointerup", stopDrag);
2825
+ handle.addEventListener("pointercancel", stopDrag);
2826
+ }
2827
+ createAdjustmentControls({
2828
+ draft,
2829
+ hud,
2830
+ pin,
2831
+ popover,
2832
+ selectionHighlight,
2833
+ textarea
2834
+ }) {
2835
+ const panel = document.createElement("div");
2836
+ panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
2837
+ const header = document.createElement("div");
2838
+ header.className = "dfwr-adjust-panel-header";
2839
+ const help = document.createElement("div");
2840
+ help.className = "dfwr-adjust-help";
2841
+ help.textContent = this.getAdjustmentLabel();
2842
+ const adjust = document.createElement("button");
2843
+ adjust.className = "dfwr-adjust-toggle";
2844
+ adjust.type = "button";
2845
+ adjust.title = "Adjust DOM element with keyboard arrows";
2846
+ adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
2847
+ const xyStatus = document.createElement("div");
2848
+ xyStatus.className = "dfwr-adjust-status";
2849
+ const scaleStatus = document.createElement("div");
2850
+ scaleStatus.className = "dfwr-adjust-status";
2851
+ const syncControls = (nextDraft) => {
2852
+ const isActive = nextDraft.adjustment?.isActive === true;
2853
+ panel.classList.toggle("is-active", isActive);
2854
+ adjust.classList.toggle("is-active", isActive);
2855
+ adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
2856
+ this.setAdjustmentToggleIcon(adjust, isActive);
2857
+ adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
2858
+ adjust.setAttribute(
2859
+ "aria-label",
2860
+ isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
2861
+ );
2862
+ const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
2863
+ xyStatus.textContent = xyLine;
2864
+ scaleStatus.textContent = scaleLine;
2865
+ this.syncDraftAdjustmentUi({
2866
+ draft: nextDraft,
2867
+ hud,
2868
+ pin,
2869
+ selectionHighlight
2870
+ });
2871
+ };
2872
+ const updateDraft = (updater) => {
2873
+ const currentDraft = this.state.noteDraft ?? draft;
2874
+ const nextDraft = updater(currentDraft);
2875
+ this.config.actions.setNoteDraft({
2876
+ ...nextDraft,
2877
+ comment: textarea.value
2878
+ });
2879
+ syncControls(nextDraft);
2880
+ };
2881
+ adjust.addEventListener("click", () => {
2882
+ updateDraft((currentDraft) => ({
2883
+ ...currentDraft,
2884
+ adjustment: {
2885
+ x: currentDraft.adjustment?.x ?? 0,
2886
+ y: currentDraft.adjustment?.y ?? 0,
2887
+ scale: currentDraft.adjustment?.scale ?? 0,
2888
+ isActive: currentDraft.adjustment?.isActive !== true
2889
+ }
2890
+ }));
2891
+ adjust.focus();
2892
+ });
2893
+ popover.addEventListener("keydown", (event) => {
2894
+ const currentDraft = this.state.noteDraft ?? draft;
2895
+ if (currentDraft.adjustment?.isActive !== true) return;
2896
+ const keyDelta = this.getAdjustmentKeyDelta(event);
2897
+ if (!keyDelta) return;
2898
+ event.preventDefault();
2899
+ event.stopPropagation();
2900
+ updateDraft((activeDraft) => ({
2901
+ ...activeDraft,
2902
+ adjustment: {
2903
+ x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
2904
+ y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
2905
+ scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
2906
+ isActive: true
2907
+ }
2908
+ }));
2909
+ });
2910
+ header.append(help, adjust);
2911
+ panel.append(header, xyStatus, scaleStatus);
2912
+ syncControls(draft);
2913
+ return {
2914
+ panel,
2915
+ focusTarget: adjust
2916
+ };
2917
+ }
2918
+ getAdjustmentKeyDelta(event) {
2919
+ const step = event.shiftKey ? 10 : 1;
2920
+ if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
2921
+ if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
2922
+ if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
2923
+ if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
2924
+ if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
2925
+ if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
2926
+ return void 0;
2927
+ }
2928
+ createAdjustmentHud(draft, environment) {
2929
+ const hud = document.createElement("div");
2930
+ hud.className = "dfwr-adjust-hud";
2931
+ hud.setAttribute("aria-hidden", "true");
2932
+ this.syncAdjustmentHud(hud, draft, environment);
2933
+ return hud;
2934
+ }
2935
+ syncDraftAdjustmentUi({
2936
+ draft,
2937
+ hud,
2938
+ pin,
2939
+ selectionHighlight
2940
+ }) {
2941
+ const environment = this.config.getEnvironment();
2942
+ if (!environment) return;
2943
+ const hostPoint = toHostPoint(
2944
+ this.getAdjustedDraftPoint(draft.marker.viewport, draft),
2945
+ environment
2946
+ );
2947
+ pin.style.left = `${hostPoint.x}px`;
2948
+ pin.style.top = `${hostPoint.y}px`;
2949
+ if (draft.selection && selectionHighlight) {
2950
+ const rect = toHostSelection(
2951
+ this.getAdjustedDraftSelection(
2952
+ toViewportSelection(draft.selection.viewport),
2953
+ draft
2954
+ ),
2955
+ environment
2956
+ );
2957
+ selectionHighlight.style.left = `${rect.left}px`;
2958
+ selectionHighlight.style.top = `${rect.top}px`;
2959
+ selectionHighlight.style.width = `${rect.width}px`;
2960
+ selectionHighlight.style.height = `${rect.height}px`;
2961
+ }
2962
+ if (hud) {
2963
+ this.syncAdjustmentHud(hud, draft, environment);
2964
+ }
2965
+ this.syncDraftPreview(draft);
2966
+ }
2967
+ syncAdjustmentHud(hud, draft, environment) {
2968
+ if (!draft.selection) return;
2969
+ const rect = toHostSelection(
2970
+ this.getAdjustedDraftSelection(
2971
+ toViewportSelection(draft.selection.viewport),
2972
+ draft
2973
+ ),
2974
+ environment
2975
+ );
2976
+ const isVisible = draft.adjustment?.isActive === true || this.hasDraftAdjustment(draft);
2977
+ hud.hidden = !isVisible;
2978
+ hud.textContent = this.formatDraftAdjustmentStatus(draft);
2979
+ hud.style.left = `${Math.max(4, rect.left)}px`;
2980
+ hud.style.top = `${Math.max(4, rect.top - 28)}px`;
2981
+ }
2051
2982
  createAreaForm() {
2052
2983
  const form = document.createElement("form");
2053
2984
  form.className = "dfwr-form";
@@ -2059,14 +2990,20 @@ var WebReviewKitView = class {
2059
2990
  form.append(empty);
2060
2991
  return form;
2061
2992
  }
2062
- const meta = document.createElement("div");
2063
- meta.className = "dfwr-item-date";
2064
- meta.textContent = formatAreaDraftMeta(areaDraft);
2065
- form.append(meta);
2993
+ form.append(this.createAreaMetricsPanel(areaDraft));
2066
2994
  const textarea = document.createElement("textarea");
2067
2995
  textarea.className = "dfwr-textarea";
2068
2996
  textarea.placeholder = "Area comment";
2069
2997
  textarea.rows = 4;
2998
+ textarea.value = areaDraft.comment ?? "";
2999
+ textarea.addEventListener("input", () => {
3000
+ const draft = this.state.areaDraft;
3001
+ if (!draft) return;
3002
+ this.config.actions.setAreaDraft({
3003
+ ...draft,
3004
+ comment: textarea.value
3005
+ });
3006
+ });
2070
3007
  const actions = this.createFormActions("Save area", () => {
2071
3008
  const comment = textarea.value.trim();
2072
3009
  const draft = this.state.areaDraft;
@@ -2083,6 +3020,25 @@ var WebReviewKitView = class {
2083
3020
  form.append(textarea, actions);
2084
3021
  return form;
2085
3022
  }
3023
+ createAreaMetricsPanel(draft) {
3024
+ const panel = document.createElement("div");
3025
+ panel.className = "dfwr-adjust-panel is-area-metrics-panel";
3026
+ const help = document.createElement("div");
3027
+ help.className = "dfwr-adjust-help";
3028
+ const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
3029
+ this.getAreaDraftMetricSelection(draft),
3030
+ draft.viewport
3031
+ );
3032
+ help.textContent = labelLine;
3033
+ const xyStatus = document.createElement("div");
3034
+ xyStatus.className = "dfwr-adjust-status";
3035
+ xyStatus.textContent = xyLine;
3036
+ const sizeStatus = document.createElement("div");
3037
+ sizeStatus.className = "dfwr-adjust-status";
3038
+ sizeStatus.textContent = sizeLine;
3039
+ panel.append(help, xyStatus, sizeStatus);
3040
+ return panel;
3041
+ }
2086
3042
  createAreaDraftOverlay(draft) {
2087
3043
  const layer = document.createElement("div");
2088
3044
  layer.className = "dfwr-area-preview-layer";
@@ -2111,23 +3067,37 @@ var WebReviewKitView = class {
2111
3067
  createAreaDraftPopover(draft) {
2112
3068
  const environment = this.config.getEnvironment();
2113
3069
  const popover = document.createElement("div");
2114
- popover.className = "dfwr-area-draft";
3070
+ popover.className = "dfwr-area-draft is-composer";
2115
3071
  if (environment && draft.selection) {
2116
3072
  const selection = toHostSelection(
2117
3073
  toViewportSelection(draft.selection.viewport),
2118
3074
  environment
2119
3075
  );
2120
- const position = getAreaPopoverPosition(selection, environment);
2121
- popover.style.left = `${position.left}px`;
2122
- popover.style.top = `${position.top}px`;
3076
+ const composer = this.getDraftComposerPosition({
3077
+ selection,
3078
+ environment,
3079
+ composerPosition: draft.composerPosition,
3080
+ estimatedHeight: 220
3081
+ });
3082
+ popover.style.left = `${composer.left}px`;
3083
+ popover.style.top = `${composer.top}px`;
3084
+ popover.style.width = `${composer.width}px`;
2123
3085
  popover.style.right = "auto";
2124
3086
  }
2125
- popover.append(this.createAreaForm());
3087
+ const dragHandle = this.createDraftDragHandle("Move area composer");
3088
+ popover.append(dragHandle, this.createAreaForm());
3089
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3090
+ const areaDraft = this.state.areaDraft ?? draft;
3091
+ this.config.actions.setAreaDraft({
3092
+ ...areaDraft,
3093
+ composerPosition
3094
+ });
3095
+ });
2126
3096
  return popover;
2127
3097
  }
2128
- createFormActions(saveLabel, onSave) {
3098
+ createFormActions(saveLabel, onSave, options) {
2129
3099
  const actions = document.createElement("div");
2130
- actions.className = "dfwr-actions";
3100
+ actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
2131
3101
  const save = document.createElement("button");
2132
3102
  save.className = "dfwr-button is-primary";
2133
3103
  save.type = "button";
@@ -2142,6 +3112,10 @@ var WebReviewKitView = class {
2142
3112
  this.config.actions.clearDrafts();
2143
3113
  this.config.actions.render();
2144
3114
  });
3115
+ if (options?.beforeSave?.length || options?.className) {
3116
+ actions.append(cancel, ...options.beforeSave ?? [], save);
3117
+ return actions;
3118
+ }
2145
3119
  actions.append(save, cancel);
2146
3120
  return actions;
2147
3121
  }
@@ -2346,11 +3320,13 @@ ${formatItemMeta(item)}`;
2346
3320
  if (!environment) return;
2347
3321
  const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
2348
3322
  const nextHostPoint = toHostPoint(nextPoint, environment);
2349
- const position = getPopoverPosition(nextHostPoint, environment);
2350
3323
  pin.style.left = `${nextHostPoint.x}px`;
2351
3324
  pin.style.top = `${nextHostPoint.y}px`;
2352
- popover.style.left = `${position.left}px`;
2353
- popover.style.top = `${position.top}px`;
3325
+ if (popover) {
3326
+ const position = getPopoverPosition(nextHostPoint, environment);
3327
+ popover.style.left = `${position.left}px`;
3328
+ popover.style.top = `${position.top}px`;
3329
+ }
2354
3330
  const noteDraft = this.state.noteDraft;
2355
3331
  if (!noteDraft) return;
2356
3332
  const nextDraft = {
@@ -2362,7 +3338,9 @@ ${formatItemMeta(item)}`;
2362
3338
  comment: textarea.value
2363
3339
  };
2364
3340
  this.config.actions.setNoteDraft(nextDraft);
2365
- meta.textContent = formatNoteDraftMeta(nextDraft);
3341
+ if (meta) {
3342
+ meta.textContent = formatNoteDraftMeta(nextDraft);
3343
+ }
2366
3344
  };
2367
3345
  pin.addEventListener("pointerdown", (event) => {
2368
3346
  if (event.button !== 0) return;
@@ -2516,6 +3494,13 @@ ${formatItemMeta(item)}`;
2516
3494
 
2517
3495
  // src/core/web.review.kit.app.ts
2518
3496
  var ROOT_ID = "df-web-review-kit-root";
3497
+ function isEditableEventTarget(event) {
3498
+ const path = event.composedPath?.() ?? [];
3499
+ const element = path[0] ?? event.target;
3500
+ if (!element || typeof element.tagName !== "string") return false;
3501
+ const tag = element.tagName;
3502
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || element.isContentEditable === true;
3503
+ }
2519
3504
  function createWebReviewKit(options) {
2520
3505
  if (typeof window === "undefined" || typeof document === "undefined") {
2521
3506
  return createNoopController();
@@ -2548,7 +3533,7 @@ var WebReviewKitApp = class {
2548
3533
  event.stopPropagation();
2549
3534
  return;
2550
3535
  }
2551
- if (!isHotkey(event, this.hotkey)) return;
3536
+ if (isEditableEventTarget(event) || !isHotkey(event, this.hotkey)) return;
2552
3537
  event.preventDefault();
2553
3538
  event.stopPropagation();
2554
3539
  this.toggle();
@@ -2588,6 +3573,9 @@ var WebReviewKitApp = class {
2588
3573
  setNoteDraft: (draft) => {
2589
3574
  this.noteDraft = draft;
2590
3575
  },
3576
+ setAreaDraft: (draft) => {
3577
+ this.areaDraft = draft;
3578
+ },
2591
3579
  setSelectingArea: (isSelectingArea) => {
2592
3580
  this.isSelectingArea = isSelectingArea;
2593
3581
  },
@@ -2613,6 +3601,7 @@ var WebReviewKitApp = class {
2613
3601
  this.render();
2614
3602
  }
2615
3603
  destroy() {
3604
+ this.view.clearDraftPreview();
2616
3605
  document.removeEventListener("keydown", this.handleKeyDown, true);
2617
3606
  window.removeEventListener("scroll", this.handleViewportChange, true);
2618
3607
  window.removeEventListener("resize", this.handleViewportChange);
@@ -2823,7 +3812,7 @@ var WebReviewKitApp = class {
2823
3812
  );
2824
3813
  const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
2825
3814
  const selection = elementSelection ?? getPointSelection(nextPoint);
2826
- const markerPoint = getSelectionCenter(selection);
3815
+ const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
2827
3816
  const reviewSelection = elementSelection ? {
2828
3817
  viewport: toPublicSelection(elementSelection),
2829
3818
  relative: getRelativeSelection(
@@ -2981,4 +3970,4 @@ export {
2981
3970
  getNumberedReviewItems,
2982
3971
  createWebReviewKit
2983
3972
  };
2984
- //# sourceMappingURL=chunk-EJDROXJM.js.map
3973
+ //# sourceMappingURL=chunk-QFNYQCTA.js.map