@designfever/web-review-kit 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -398,8 +400,8 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
398
400
  source: getDomSourceHint(target)
399
401
  };
400
402
  }
401
- function getElementViewportSelection(anchor, environment) {
402
- const element = getAnchorElement(anchor, environment);
403
+ function getElementViewportSelection(anchor, environment, preferredSelection) {
404
+ const element = getAnchorElement(anchor, environment, preferredSelection);
403
405
  if (!element) return void 0;
404
406
  const rect = element.getBoundingClientRect();
405
407
  if (rect.width <= 0 || rect.height <= 0) return void 0;
@@ -438,22 +440,35 @@ function getAnchorCandidates(anchor) {
438
440
  ...anchor.candidates ?? []
439
441
  ]);
440
442
  }
441
- function resolveAnchorElement(anchor, environment) {
443
+ function resolveAnchorElement(anchor, environment, preferredSelection) {
442
444
  const matches = getAnchorCandidates(anchor).flatMap((candidate) => {
443
- const match = queryBestAnchorCandidate(
444
- candidate,
445
- candidate.textFingerprint ?? anchor.textFingerprint,
446
- environment
447
- );
448
- if (!match) return [];
449
- const confidence = roundRatio(
450
- (candidate.confidence ?? 0.5) * match.score
451
- );
452
- return [{
453
- element: match.element,
454
- candidate,
455
- confidence
456
- }];
445
+ const textFingerprint = candidate.textFingerprint ?? anchor.textFingerprint;
446
+ if (!preferredSelection) {
447
+ const match = queryBestAnchorCandidate(
448
+ candidate,
449
+ textFingerprint,
450
+ environment
451
+ );
452
+ if (!match) return [];
453
+ const confidence = roundRatio(
454
+ (candidate.confidence ?? 0.5) * match.score
455
+ );
456
+ return [{
457
+ element: match.element,
458
+ candidate,
459
+ confidence
460
+ }];
461
+ }
462
+ return queryAnchorElements(candidate.selector, environment).map((element) => {
463
+ const confidence = roundRatio(
464
+ (candidate.confidence ?? 0.5) * getTextFingerprintScore(textFingerprint, getTextFingerprint(element)) * getSelectionMatchScore(element, preferredSelection)
465
+ );
466
+ return {
467
+ element,
468
+ candidate,
469
+ confidence
470
+ };
471
+ });
457
472
  });
458
473
  return matches.sort((a, b) => b.confidence - a.confidence)[0];
459
474
  }
@@ -463,50 +478,70 @@ function cssEscape(value) {
463
478
  }
464
479
  return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
465
480
  }
466
- function getAnchorElement(anchor, environment) {
467
- return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment)?.element;
481
+ function getAnchorElement(anchor, environment, preferredSelection) {
482
+ return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment, preferredSelection)?.element;
468
483
  }
469
484
  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
- }
485
+ const targetCandidates = [];
486
+ const configuredAnchor = getExactAttributeAnchorCandidate(
487
+ target,
488
+ configuredAttribute,
489
+ 0.98,
490
+ "configured-attribute"
491
+ );
492
+ if (configuredAnchor) targetCandidates.push(configuredAnchor);
493
+ const targetAttributeAnchor = getAttributeAnchorCandidate(
494
+ target,
495
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
496
+ 0.9
497
+ );
498
+ if (targetAttributeAnchor) targetCandidates.push(targetAttributeAnchor);
483
499
  if (isMeaningfulId(target.id)) {
484
- candidates.push({
500
+ targetCandidates.push({
485
501
  selector: `#${cssEscape(target.id)}`,
486
502
  strategy: "id",
487
503
  confidence: 0.94,
488
504
  textFingerprint: getTextFingerprint(target)
489
505
  });
490
506
  }
507
+ const semanticAnchor = getAttributeAnchorCandidate(
508
+ target,
509
+ SEMANTIC_ANCHOR_ATTRIBUTES,
510
+ 0.84
511
+ );
512
+ if (semanticAnchor) targetCandidates.push(semanticAnchor);
491
513
  const targetClassName = getMeaningfulClassName(target);
492
514
  if (targetClassName) {
493
- candidates.push({
515
+ targetCandidates.push({
494
516
  selector: `${target.tagName.toLowerCase()}.${cssEscape(targetClassName)}`,
495
517
  strategy: "class",
496
518
  confidence: 0.82,
497
519
  textFingerprint: getTextFingerprint(target)
498
520
  });
499
521
  }
500
- candidates.push({
522
+ const scopedPath = getScopedDomPathCandidate(target, configuredAttribute);
523
+ if (scopedPath) targetCandidates.push(scopedPath);
524
+ const targetDomPath = {
501
525
  selector: getDomPath(target),
502
526
  strategy: "dom-path",
503
- confidence: 0.9,
527
+ confidence: targetCandidates.length > 0 ? 0.8 : 0.5,
504
528
  textFingerprint: getTextFingerprint(target)
505
- });
529
+ };
530
+ const parentCandidates = [];
506
531
  const parent = target.parentElement;
532
+ const parentConfiguredAnchor = parent ? findClosestAttributeAnchor(parent, [configuredAttribute], 0.72, {
533
+ strategy: "configured-attribute"
534
+ }) : void 0;
535
+ if (parentConfiguredAnchor) parentCandidates.push(parentConfiguredAnchor);
536
+ const anchoredByAttribute = parent ? findClosestAttributeAnchor(
537
+ parent,
538
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
539
+ 0.7
540
+ ) : void 0;
541
+ if (anchoredByAttribute) parentCandidates.push(anchoredByAttribute);
507
542
  const anchoredById = parent ? findClosest(parent, (element) => isMeaningfulId(element.id)) : void 0;
508
543
  if (anchoredById?.id) {
509
- candidates.push({
544
+ parentCandidates.push({
510
545
  selector: `#${cssEscape(anchoredById.id)}`,
511
546
  strategy: "id",
512
547
  confidence: 0.72,
@@ -516,7 +551,7 @@ function createAnchorCandidates(target, configuredAttribute) {
516
551
  const anchoredByClass = parent ? findClosest(parent, (element) => Boolean(getMeaningfulClassName(element))) : void 0;
517
552
  const className = anchoredByClass ? getMeaningfulClassName(anchoredByClass) : void 0;
518
553
  if (anchoredByClass && className) {
519
- candidates.push({
554
+ parentCandidates.push({
520
555
  selector: `${anchoredByClass.tagName.toLowerCase()}.${cssEscape(
521
556
  className
522
557
  )}`,
@@ -525,8 +560,107 @@ function createAnchorCandidates(target, configuredAttribute) {
525
560
  textFingerprint: getTextFingerprint(anchoredByClass)
526
561
  });
527
562
  }
563
+ const candidates = targetCandidates.length > 0 ? [...targetCandidates, targetDomPath, ...parentCandidates] : [...parentCandidates, targetDomPath];
528
564
  return dedupeAnchorCandidates(candidates);
529
565
  }
566
+ function findClosestAttributeAnchor(target, attributeNames, confidence, options) {
567
+ for (const attributeName of attributeNames) {
568
+ const selector = `[${attributeName}]`;
569
+ const element = safeClosest(target, selector);
570
+ if (!element) continue;
571
+ const value = getStableAttributeValue(element, attributeName);
572
+ if (!value) continue;
573
+ return {
574
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
575
+ strategy: options?.strategy ?? "attribute",
576
+ confidence,
577
+ textFingerprint: getTextFingerprint(element)
578
+ };
579
+ }
580
+ return void 0;
581
+ }
582
+ function getExactAttributeAnchorCandidate(element, attributeName, confidence, strategy) {
583
+ const value = getStableAttributeValue(element, attributeName);
584
+ if (!value) return void 0;
585
+ return {
586
+ selector: `[${attributeName}="${cssEscape(value)}"]`,
587
+ strategy,
588
+ confidence,
589
+ textFingerprint: getTextFingerprint(element)
590
+ };
591
+ }
592
+ function getAttributeAnchorCandidate(element, attributeNames, confidence) {
593
+ for (const attributeName of attributeNames) {
594
+ const value = getStableAttributeValue(element, attributeName);
595
+ if (!value) continue;
596
+ return {
597
+ selector: `${element.tagName.toLowerCase()}[${attributeName}="${cssEscape(
598
+ value
599
+ )}"]`,
600
+ strategy: "attribute",
601
+ confidence,
602
+ textFingerprint: getTextFingerprint(element)
603
+ };
604
+ }
605
+ return void 0;
606
+ }
607
+ function getScopedDomPathCandidate(target, configuredAttribute) {
608
+ const parent = target.parentElement;
609
+ if (!parent) return void 0;
610
+ const anchor = findStableAncestorSelector(parent, configuredAttribute);
611
+ if (!anchor) return void 0;
612
+ const selector = getDomPathBetween(anchor.element, target, anchor.selector);
613
+ if (!selector) return void 0;
614
+ return {
615
+ selector,
616
+ strategy: "dom-path",
617
+ confidence: anchor.confidence,
618
+ textFingerprint: getTextFingerprint(target)
619
+ };
620
+ }
621
+ function findStableAncestorSelector(start, configuredAttribute) {
622
+ let element = start;
623
+ const root = start.ownerDocument.documentElement;
624
+ while (element && element !== root) {
625
+ const configuredValue = getStableAttributeValue(element, configuredAttribute);
626
+ if (configuredValue) {
627
+ return {
628
+ element,
629
+ selector: `[${configuredAttribute}="${cssEscape(configuredValue)}"]`,
630
+ confidence: 0.88
631
+ };
632
+ }
633
+ const attributeAnchor = getAttributeAnchorCandidate(
634
+ element,
635
+ COMMON_ANCHOR_ATTRIBUTES.filter((name) => name !== configuredAttribute),
636
+ 0.84
637
+ );
638
+ if (attributeAnchor) {
639
+ return {
640
+ element,
641
+ selector: attributeAnchor.selector,
642
+ confidence: 0.84
643
+ };
644
+ }
645
+ if (isMeaningfulId(element.id)) {
646
+ return {
647
+ element,
648
+ selector: `#${cssEscape(element.id)}`,
649
+ confidence: 0.82
650
+ };
651
+ }
652
+ const className = getMeaningfulClassName(element);
653
+ if (className) {
654
+ return {
655
+ element,
656
+ selector: `${element.tagName.toLowerCase()}.${cssEscape(className)}`,
657
+ confidence: 0.76
658
+ };
659
+ }
660
+ element = element.parentElement;
661
+ }
662
+ return void 0;
663
+ }
530
664
  function getAnchorSourceElement(target, candidate, configuredAttribute) {
531
665
  if (candidate.strategy === "configured-attribute") {
532
666
  return target.closest(`[${configuredAttribute}]`);
@@ -538,6 +672,13 @@ function getAnchorSourceElement(target, candidate, configuredAttribute) {
538
672
  return target;
539
673
  }
540
674
  }
675
+ function safeClosest(element, selector) {
676
+ try {
677
+ return element.closest(selector);
678
+ } catch {
679
+ return null;
680
+ }
681
+ }
541
682
  function getElementHtmlSnippet(element, maxLength = 1e3) {
542
683
  const html = decodeHtmlEntities(element.outerHTML.replace(/\s+/g, " ").trim());
543
684
  if (html.length <= maxLength) return html;
@@ -659,10 +800,38 @@ function getDomPath(element) {
659
800
  }
660
801
  return `body > ${parts.join(" > ")}`;
661
802
  }
803
+ function getDomPathBetween(ancestor, target, ancestorSelector) {
804
+ const parts = [];
805
+ let current = target;
806
+ while (current && current !== ancestor) {
807
+ parts.unshift(getDomPathPart(current));
808
+ current = current.parentElement;
809
+ }
810
+ if (current !== ancestor || parts.length === 0) return void 0;
811
+ return `${ancestorSelector} > ${parts.join(" > ")}`;
812
+ }
813
+ function getDomPathPart(element) {
814
+ const parent = element.parentElement;
815
+ const tag = element.tagName.toLowerCase();
816
+ if (!parent) return tag;
817
+ const currentTagName = element.tagName;
818
+ const siblings = Array.from(parent.children).filter(
819
+ (child) => child.tagName === currentTagName
820
+ );
821
+ const index = siblings.indexOf(element) + 1;
822
+ return `${tag}:nth-of-type(${index})`;
823
+ }
662
824
  function getTextFingerprint(element) {
663
825
  const text = element.textContent?.replace(/\s+/g, " ").trim();
664
826
  return text ? text.slice(0, 120) : void 0;
665
827
  }
828
+ function getStableAttributeValue(element, attributeName) {
829
+ const value = element.getAttribute(attributeName)?.trim();
830
+ if (!value || value.length > 160) return void 0;
831
+ if (/^(true|false)$/i.test(value)) return void 0;
832
+ if (/^\d+$/.test(value) && value.length < 3) return void 0;
833
+ return value;
834
+ }
666
835
  function getTextFingerprintScore(expected, actual) {
667
836
  if (!expected) return 1;
668
837
  if (!actual) return 0.5;
@@ -674,6 +843,38 @@ function getTextFingerprintScore(expected, actual) {
674
843
  const matches = expectedTokens.filter((token) => actualTokens.has(token));
675
844
  return clamp(matches.length / expectedTokens.length, 0.25, 0.76);
676
845
  }
846
+ function getSelectionMatchScore(element, selection) {
847
+ const rect = element.getBoundingClientRect();
848
+ if (rect.width <= 0 || rect.height <= 0) return 0.05;
849
+ const overlapLeft = Math.max(rect.left, selection.left);
850
+ const overlapTop = Math.max(rect.top, selection.top);
851
+ const overlapRight = Math.min(rect.right, selection.left + selection.width);
852
+ const overlapBottom = Math.min(rect.bottom, selection.top + selection.height);
853
+ const overlapWidth = Math.max(0, overlapRight - overlapLeft);
854
+ const overlapHeight = Math.max(0, overlapBottom - overlapTop);
855
+ const overlapArea = overlapWidth * overlapHeight;
856
+ if (overlapArea > 0) {
857
+ const selectionArea = Math.max(1, selection.width * selection.height);
858
+ const rectArea = Math.max(1, rect.width * rect.height);
859
+ return 1 + overlapArea / Math.min(selectionArea, rectArea);
860
+ }
861
+ const rectCenterX = rect.left + rect.width / 2;
862
+ const rectCenterY = rect.top + rect.height / 2;
863
+ const selectionCenterX = selection.left + selection.width / 2;
864
+ const selectionCenterY = selection.top + selection.height / 2;
865
+ const distance = Math.hypot(
866
+ rectCenterX - selectionCenterX,
867
+ rectCenterY - selectionCenterY
868
+ );
869
+ const basis = Math.max(
870
+ 1,
871
+ rect.width,
872
+ rect.height,
873
+ selection.width,
874
+ selection.height
875
+ );
876
+ return clamp(1 / (1 + distance / basis), 0.05, 0.95);
877
+ }
677
878
  function getFingerprintTokens(value) {
678
879
  return value.toLowerCase().split(/[\s/|,.:;()[\]{}"'`~!?<>]+/).map((token) => token.trim()).filter((token) => token.length > 1);
679
880
  }
@@ -1038,6 +1239,19 @@ function createStyleElement() {
1038
1239
  display: block;
1039
1240
  }
1040
1241
 
1242
+ .dfwr-shell.has-dismissible-draft {
1243
+ z-index: 900;
1244
+ }
1245
+
1246
+ .dfwr-draft-cancel-layer {
1247
+ position: fixed;
1248
+ inset: 0;
1249
+ z-index: 2;
1250
+ pointer-events: auto;
1251
+ background: transparent;
1252
+ cursor: default;
1253
+ }
1254
+
1041
1255
  .dfwr-panel {
1042
1256
  position: fixed;
1043
1257
  right: 16px;
@@ -1531,6 +1745,40 @@ function createStyleElement() {
1531
1745
  box-shadow: var(--df-review-shadow-popover);
1532
1746
  }
1533
1747
 
1748
+ .dfwr-note-popover.is-composer,
1749
+ .dfwr-area-draft.is-composer {
1750
+ max-height: min(360px, calc(100vh - 32px));
1751
+ overflow: auto;
1752
+ border-color: rgba(99, 215, 199, 0.56);
1753
+ }
1754
+
1755
+ .dfwr-note-popover.is-dragging,
1756
+ .dfwr-area-draft.is-dragging {
1757
+ user-select: none;
1758
+ }
1759
+
1760
+ .dfwr-draft-drag-handle {
1761
+ display: block;
1762
+ width: 42px;
1763
+ height: 6px;
1764
+ margin: 0 auto 10px;
1765
+ padding: 0;
1766
+ cursor: grab;
1767
+ pointer-events: auto;
1768
+ background: rgba(247, 247, 242, 0.28);
1769
+ border: 0;
1770
+ border-radius: 999px;
1771
+ }
1772
+
1773
+ .dfwr-draft-drag-handle:hover,
1774
+ .dfwr-draft-drag-handle:focus-visible {
1775
+ background: rgba(215, 255, 95, 0.62);
1776
+ }
1777
+
1778
+ .dfwr-draft-drag-handle:active {
1779
+ cursor: grabbing;
1780
+ }
1781
+
1534
1782
  .dfwr-area-draft {
1535
1783
  position: fixed;
1536
1784
  right: 16px;
@@ -1552,6 +1800,14 @@ function createStyleElement() {
1552
1800
  padding: 0;
1553
1801
  }
1554
1802
 
1803
+ .dfwr-note-actions {
1804
+ justify-content: flex-end;
1805
+ }
1806
+
1807
+ .dfwr-note-actions .dfwr-button:first-child {
1808
+ margin-right: auto;
1809
+ }
1810
+
1555
1811
  .dfwr-area-draft .dfwr-actions {
1556
1812
  padding: 0;
1557
1813
  }
@@ -1580,6 +1836,79 @@ function createStyleElement() {
1580
1836
  outline-offset: 1px;
1581
1837
  }
1582
1838
 
1839
+ .dfwr-adjust-panel {
1840
+ display: grid;
1841
+ gap: 4px;
1842
+ padding: 8px 10px;
1843
+ border: 1px solid rgba(255, 255, 255, 0.12);
1844
+ border-radius: var(--df-review-radius-sm);
1845
+ background: rgba(255, 255, 255, 0.04);
1846
+ }
1847
+
1848
+ .dfwr-adjust-panel-header {
1849
+ display: flex;
1850
+ align-items: center;
1851
+ justify-content: space-between;
1852
+ gap: 10px;
1853
+ min-width: 0;
1854
+ }
1855
+
1856
+ .dfwr-adjust-panel-header .dfwr-adjust-help {
1857
+ flex: 1 1 auto;
1858
+ min-width: 0;
1859
+ }
1860
+
1861
+ .dfwr-adjust-panel.is-active {
1862
+ border-color: rgba(215, 255, 95, 0.5);
1863
+ background: var(--df-review-color-accent-soft);
1864
+ }
1865
+
1866
+ .dfwr-adjust-help,
1867
+ .dfwr-adjust-status {
1868
+ margin: 0;
1869
+ color: var(--df-review-color-text-muted);
1870
+ font-size: var(--df-review-font-size-xs);
1871
+ line-height: 1.35;
1872
+ }
1873
+
1874
+ .dfwr-adjust-status {
1875
+ color: var(--df-review-color-text);
1876
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1877
+ }
1878
+
1879
+ .dfwr-adjust-toggle {
1880
+ flex: 0 0 auto;
1881
+ display: inline-flex;
1882
+ align-items: center;
1883
+ justify-content: center;
1884
+ width: 34px;
1885
+ height: 30px;
1886
+ padding: 0;
1887
+ border: 1px solid rgba(255, 255, 255, 0.2);
1888
+ border-radius: var(--df-review-radius-sm);
1889
+ background: rgba(255, 255, 255, 0.04);
1890
+ color: var(--df-review-color-text);
1891
+ cursor: pointer;
1892
+ font: inherit;
1893
+ font-size: 14px;
1894
+ font-weight: 800;
1895
+ line-height: 1;
1896
+ }
1897
+
1898
+ .dfwr-adjust-toggle:hover,
1899
+ .dfwr-adjust-toggle:focus-visible,
1900
+ .dfwr-adjust-toggle.is-active {
1901
+ border-color: rgba(215, 255, 95, 0.68);
1902
+ background: var(--df-review-color-accent-soft);
1903
+ outline: none;
1904
+ }
1905
+
1906
+ .dfwr-adjust-toggle svg {
1907
+ width: 18px;
1908
+ height: 18px;
1909
+ pointer-events: none;
1910
+ }
1911
+
1583
1912
  .dfwr-empty,
1584
1913
  .dfwr-error {
1585
1914
  margin: 0;
@@ -1800,16 +2129,6 @@ function createStyleElement() {
1800
2129
  }
1801
2130
 
1802
2131
  // src/core/review/format.ts
1803
- function formatAreaDraftMeta(draft) {
1804
- const parts = [`viewport ${formatSize(draft.viewport)}`];
1805
- if (draft.selection) {
1806
- parts.push(`rect ${formatSelection(draft.selection.viewport)}`);
1807
- }
1808
- if (draft.marker) {
1809
- parts.push(`point ${formatPoint(draft.marker.viewport)}`);
1810
- }
1811
- return parts.join(" / ");
1812
- }
1813
2132
  function formatNoteDraftMeta(draft) {
1814
2133
  const parts = [
1815
2134
  `viewport ${formatSize(draft.viewport)}`,
@@ -1875,17 +2194,29 @@ function formatAnchorMeta(anchor) {
1875
2194
  }
1876
2195
 
1877
2196
  // src/core/web.review.kit.view.ts
2197
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
1878
2198
  var WebReviewKitView = class {
1879
2199
  constructor(config) {
1880
2200
  this.config = config;
1881
2201
  }
2202
+ clearDraftPreview() {
2203
+ this.restoreDraftPreview();
2204
+ }
1882
2205
  render(shadow, hiddenItemsStyle) {
1883
2206
  const state = this.state;
2207
+ this.syncDraftPreview(
2208
+ state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2209
+ );
1884
2210
  shadow.replaceChildren();
1885
2211
  shadow.append(createStyleElement());
1886
2212
  shadow.append(hiddenItemsStyle);
2213
+ const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
1887
2214
  const shell = document.createElement("div");
1888
- shell.className = `dfwr-shell${state.isOpen ? " is-open" : ""}`;
2215
+ shell.className = [
2216
+ "dfwr-shell",
2217
+ state.isOpen ? "is-open" : "",
2218
+ hasDismissableDraft ? "has-dismissible-draft" : ""
2219
+ ].filter(Boolean).join(" ");
1889
2220
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
1890
2221
  if (this.config.options.ui?.panel !== false) {
1891
2222
  const panel = document.createElement("div");
@@ -1901,6 +2232,9 @@ var WebReviewKitView = class {
1901
2232
  shell.append(panel);
1902
2233
  }
1903
2234
  shell.append(this.createMarkerLayer());
2235
+ if (state.isOpen && hasDismissableDraft) {
2236
+ shell.append(this.createDraftCancelLayer());
2237
+ }
1904
2238
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
1905
2239
  shell.append(
1906
2240
  state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
@@ -1920,6 +2254,332 @@ var WebReviewKitView = class {
1920
2254
  get state() {
1921
2255
  return this.config.getState();
1922
2256
  }
2257
+ createDraftCancelLayer() {
2258
+ const layer = document.createElement("div");
2259
+ layer.className = "dfwr-draft-cancel-layer";
2260
+ layer.setAttribute("aria-hidden", "true");
2261
+ const cancel = (event) => {
2262
+ event.preventDefault();
2263
+ event.stopPropagation();
2264
+ event.stopImmediatePropagation();
2265
+ this.config.actions.setModeState("idle");
2266
+ this.config.actions.clearDrafts();
2267
+ this.config.actions.setSelectingArea(false);
2268
+ this.config.actions.render();
2269
+ };
2270
+ layer.addEventListener("pointerdown", (event) => {
2271
+ if (event.button !== 0) return;
2272
+ cancel(event);
2273
+ });
2274
+ return layer;
2275
+ }
2276
+ getDraftAdjustmentMetrics(draft) {
2277
+ const adjustment = draft.adjustment;
2278
+ const x = adjustment?.x ?? 0;
2279
+ const y = adjustment?.y ?? 0;
2280
+ const scale = adjustment?.scale ?? 0;
2281
+ const {
2282
+ scale: viewportScale,
2283
+ designWidth,
2284
+ presetLabel
2285
+ } = this.getDraftViewportScale(draft.viewport);
2286
+ const selection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2287
+ const scaleCssDelta = scale * viewportScale;
2288
+ const scaleFactor = selection && selection.width > 0 ? Math.max(
2289
+ 1 / selection.width,
2290
+ (selection.width + scaleCssDelta) / selection.width
2291
+ ) : 1;
2292
+ return {
2293
+ x,
2294
+ y,
2295
+ scale,
2296
+ cssX: x * viewportScale,
2297
+ cssY: y * viewportScale,
2298
+ scaleFactor,
2299
+ viewportScale,
2300
+ designWidth,
2301
+ presetLabel,
2302
+ viewportWidth: draft.viewport.width
2303
+ };
2304
+ }
2305
+ hasDraftAdjustment(draft) {
2306
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2307
+ return metrics.x !== 0 || metrics.y !== 0 || metrics.scale !== 0;
2308
+ }
2309
+ getAdjustedDraftPoint(point, draft) {
2310
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2311
+ return {
2312
+ x: point.x + metrics.cssX,
2313
+ y: point.y + metrics.cssY
2314
+ };
2315
+ }
2316
+ getAdjustedDraftSelection(selection, draft) {
2317
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2318
+ return {
2319
+ ...selection,
2320
+ left: selection.left + metrics.cssX,
2321
+ top: selection.top + metrics.cssY,
2322
+ width: selection.width * metrics.scaleFactor,
2323
+ height: selection.height * metrics.scaleFactor
2324
+ };
2325
+ }
2326
+ getDraftViewportScale(viewport) {
2327
+ const preset = findReviewViewportPreset(
2328
+ viewport,
2329
+ this.config.options.viewports?.presets
2330
+ );
2331
+ const designWidth = typeof preset.designWidth === "number" && preset.designWidth > 0 ? preset.designWidth : viewport.width;
2332
+ const scale = designWidth > 0 ? viewport.width / designWidth : 1;
2333
+ return { scale, designWidth, presetLabel: preset.label };
2334
+ }
2335
+ getDraftComposerWidth(environment) {
2336
+ const bounds = environment.overlayRect;
2337
+ const margin = 12;
2338
+ return Math.min(360, Math.max(240, bounds.width - margin * 2));
2339
+ }
2340
+ getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
2341
+ const margin = 12;
2342
+ const width = size?.width ?? this.getDraftComposerWidth(environment);
2343
+ const height = size?.height ?? 236;
2344
+ return {
2345
+ x: clamp(
2346
+ position.x,
2347
+ bounds.left + margin,
2348
+ bounds.left + bounds.width - width - margin
2349
+ ),
2350
+ y: clamp(
2351
+ position.y,
2352
+ bounds.top + margin,
2353
+ bounds.top + bounds.height - height - margin
2354
+ )
2355
+ };
2356
+ }
2357
+ getHostComposerBounds() {
2358
+ const root = document.documentElement;
2359
+ return {
2360
+ left: 0,
2361
+ top: 0,
2362
+ width: root.clientWidth || window.innerWidth,
2363
+ height: root.clientHeight || window.innerHeight
2364
+ };
2365
+ }
2366
+ getInitialDraftComposerPosition(selection, environment, size) {
2367
+ const bounds = this.getHostComposerBounds();
2368
+ const margin = 12;
2369
+ const gap = 20;
2370
+ if (!selection) {
2371
+ return this.getClampedComposerPosition(
2372
+ {
2373
+ x: environment.overlayRect.left + margin,
2374
+ y: environment.overlayRect.top + margin
2375
+ },
2376
+ environment,
2377
+ size,
2378
+ bounds
2379
+ );
2380
+ }
2381
+ const preferredX = selection.left + selection.width + gap;
2382
+ const maxX = bounds.left + bounds.width - size.width - margin;
2383
+ const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
2384
+ return this.getClampedComposerPosition(
2385
+ {
2386
+ x,
2387
+ y: selection.top
2388
+ },
2389
+ environment,
2390
+ size,
2391
+ bounds
2392
+ );
2393
+ }
2394
+ getDraftComposerPosition({
2395
+ selection,
2396
+ environment,
2397
+ composerPosition,
2398
+ estimatedHeight
2399
+ }) {
2400
+ const width = this.getDraftComposerWidth(environment);
2401
+ if (composerPosition) {
2402
+ const clamped = this.getClampedComposerPosition(
2403
+ composerPosition,
2404
+ environment,
2405
+ { width, height: estimatedHeight },
2406
+ this.getHostComposerBounds()
2407
+ );
2408
+ return { width, left: clamped.x, top: clamped.y };
2409
+ }
2410
+ const position = this.getInitialDraftComposerPosition(selection, environment, {
2411
+ width,
2412
+ height: estimatedHeight
2413
+ });
2414
+ return { width, left: position.x, top: position.y };
2415
+ }
2416
+ getSelectionMqMetrics(selection, viewport) {
2417
+ const { scale } = this.getDraftViewportScale(viewport);
2418
+ const ratio = scale > 0 ? 1 / scale : 1;
2419
+ return {
2420
+ x: selection.left * ratio,
2421
+ y: selection.top * ratio,
2422
+ width: selection.width * ratio,
2423
+ height: selection.height * ratio
2424
+ };
2425
+ }
2426
+ formatSignedPx(value) {
2427
+ if (value === 0) return "+0px";
2428
+ return `${value > 0 ? "+" : ""}${value}px`;
2429
+ }
2430
+ formatRoundedPx(value) {
2431
+ return `${Math.round(value)}px`;
2432
+ }
2433
+ getAdjustmentLabel() {
2434
+ return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
2435
+ }
2436
+ getSelectionMetricLines(selection, viewport) {
2437
+ if (!selection) return ["area", "x none / y none", "w none / h none"];
2438
+ const metrics = this.getSelectionMqMetrics(selection, viewport);
2439
+ return [
2440
+ "area",
2441
+ `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
2442
+ metrics.y
2443
+ )}`,
2444
+ `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
2445
+ metrics.height
2446
+ )}`
2447
+ ];
2448
+ }
2449
+ getAreaDraftMetricSelection(draft) {
2450
+ if (!draft.selection) return void 0;
2451
+ return toViewportSelection(draft.selection.viewport);
2452
+ }
2453
+ getDraftAdjustmentMetricLines(draft) {
2454
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2455
+ return [
2456
+ `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
2457
+ metrics.y
2458
+ )}`,
2459
+ `scale ${this.formatSignedPx(metrics.scale)}`
2460
+ ];
2461
+ }
2462
+ withDraftAdjustmentComment(comment, draft) {
2463
+ if (!this.hasDraftAdjustment(draft)) return comment;
2464
+ const trimmedComment = comment.trim();
2465
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2466
+ const adjustment = [
2467
+ `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
2468
+ metrics.x
2469
+ )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
2470
+ metrics.scale
2471
+ )}`,
2472
+ `(${metrics.presetLabel} viewport, ${Math.round(
2473
+ metrics.viewportWidth
2474
+ )}/design ${Math.round(metrics.designWidth)})`
2475
+ ].join(" ");
2476
+ return trimmedComment ? `${trimmedComment}
2477
+ ${adjustment}` : adjustment;
2478
+ }
2479
+ getStyleableDraftElement(draft, environment) {
2480
+ if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2481
+ return draft.previewElement;
2482
+ }
2483
+ if (!draft.anchor) return void 0;
2484
+ const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2485
+ const element = resolveAnchorElement(
2486
+ draft.anchor,
2487
+ environment,
2488
+ preferredSelection
2489
+ )?.element;
2490
+ if (!element) return void 0;
2491
+ if ("style" in element) return element;
2492
+ return void 0;
2493
+ }
2494
+ syncDraftPreview(draft) {
2495
+ const environment = this.config.getEnvironment();
2496
+ if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
2497
+ this.restoreDraftPreview();
2498
+ return;
2499
+ }
2500
+ const element = this.getStyleableDraftElement(draft, environment);
2501
+ if (!element) {
2502
+ this.restoreDraftPreview();
2503
+ return;
2504
+ }
2505
+ if (this.draftPreview?.element !== element) {
2506
+ this.restoreDraftPreview();
2507
+ }
2508
+ if (!this.draftPreview) {
2509
+ const computedStyle = environment.window.getComputedStyle(element);
2510
+ const clone = element.cloneNode(true);
2511
+ this.removeDuplicateIds(clone);
2512
+ this.copyComputedStyle(element, clone, environment);
2513
+ this.positionDraftPreviewClone(clone, element, computedStyle);
2514
+ environment.document.body?.appendChild(clone);
2515
+ this.draftPreview = {
2516
+ element,
2517
+ clone,
2518
+ visibility: element.style.visibility
2519
+ };
2520
+ element.style.visibility = "hidden";
2521
+ }
2522
+ const metrics = this.getDraftAdjustmentMetrics(draft);
2523
+ const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
2524
+ metrics.cssY
2525
+ )}px)`;
2526
+ const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
2527
+ this.draftPreview.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
2528
+ }
2529
+ restoreDraftPreview() {
2530
+ if (!this.draftPreview) return;
2531
+ const { element, clone, visibility } = this.draftPreview;
2532
+ clone.remove();
2533
+ element.style.visibility = visibility;
2534
+ this.draftPreview = void 0;
2535
+ }
2536
+ positionDraftPreviewClone(clone, element, computedStyle) {
2537
+ const rect = element.getBoundingClientRect();
2538
+ clone.setAttribute("data-dfwr-adjust-preview", "true");
2539
+ clone.setAttribute("aria-hidden", "true");
2540
+ clone.style.position = "fixed";
2541
+ clone.style.left = `${this.toCssNumber(rect.left)}px`;
2542
+ clone.style.top = `${this.toCssNumber(rect.top)}px`;
2543
+ clone.style.right = "auto";
2544
+ clone.style.bottom = "auto";
2545
+ clone.style.width = `${this.toCssNumber(rect.width)}px`;
2546
+ clone.style.height = `${this.toCssNumber(rect.height)}px`;
2547
+ clone.style.maxWidth = "none";
2548
+ clone.style.maxHeight = "none";
2549
+ clone.style.margin = "0";
2550
+ clone.style.boxSizing = "border-box";
2551
+ clone.style.display = this.getDraftPreviewDisplay(computedStyle.display);
2552
+ clone.style.zIndex = "2147483646";
2553
+ clone.style.pointerEvents = "none";
2554
+ clone.style.transition = "none";
2555
+ clone.style.willChange = "transform";
2556
+ clone.style.transformOrigin = "top left";
2557
+ clone.style.transform = "none";
2558
+ }
2559
+ getDraftPreviewDisplay(display) {
2560
+ if (display === "inline" || display === "contents") return "inline-block";
2561
+ return display || "block";
2562
+ }
2563
+ copyComputedStyle(element, clone, environment) {
2564
+ const computedStyle = environment.window.getComputedStyle(element);
2565
+ for (let index = 0; index < computedStyle.length; index += 1) {
2566
+ const property = computedStyle.item(index);
2567
+ clone.style.setProperty(
2568
+ property,
2569
+ computedStyle.getPropertyValue(property),
2570
+ computedStyle.getPropertyPriority(property)
2571
+ );
2572
+ }
2573
+ }
2574
+ removeDuplicateIds(element) {
2575
+ element.removeAttribute("id");
2576
+ element.querySelectorAll("[id]").forEach((child) => {
2577
+ child.removeAttribute("id");
2578
+ });
2579
+ }
2580
+ toCssNumber(value) {
2581
+ return Math.round(value * 1e3) / 1e3;
2582
+ }
1923
2583
  createHeader() {
1924
2584
  const header = document.createElement("div");
1925
2585
  header.className = "dfwr-header";
@@ -2011,15 +2671,20 @@ var WebReviewKitView = class {
2011
2671
  const group = document.createElement("div");
2012
2672
  group.className = "dfwr-note-draft";
2013
2673
  if (!environment) return group;
2014
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
2674
+ const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
2675
+ const hostPoint = toHostPoint(
2676
+ isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
2677
+ environment
2678
+ );
2679
+ let selectionHighlight;
2015
2680
  if (draft.selection) {
2016
- group.append(
2017
- this.createSelectionHighlight(
2018
- toViewportSelection(draft.selection.viewport),
2019
- environment,
2020
- true
2021
- )
2681
+ const selection = toViewportSelection(draft.selection.viewport);
2682
+ selectionHighlight = this.createSelectionHighlight(
2683
+ isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2684
+ environment,
2685
+ true
2022
2686
  );
2687
+ group.append(selectionHighlight);
2023
2688
  }
2024
2689
  const pin = document.createElement("button");
2025
2690
  pin.className = "dfwr-note-pin";
@@ -2029,14 +2694,35 @@ var WebReviewKitView = class {
2029
2694
  pin.style.top = `${hostPoint.y}px`;
2030
2695
  const popover = document.createElement("div");
2031
2696
  const position = getPopoverPosition(hostPoint, environment);
2032
- popover.className = "dfwr-note-popover";
2033
- popover.style.left = `${position.left}px`;
2034
- popover.style.top = `${position.top}px`;
2697
+ popover.className = `dfwr-note-popover${isElementDraft ? " is-composer" : ""}`;
2698
+ if (isElementDraft) {
2699
+ const selection = draft.selection ? toHostSelection(
2700
+ this.getAdjustedDraftSelection(
2701
+ toViewportSelection(draft.selection.viewport),
2702
+ draft
2703
+ ),
2704
+ environment
2705
+ ) : void 0;
2706
+ const composer = this.getDraftComposerPosition({
2707
+ selection,
2708
+ environment,
2709
+ composerPosition: draft.composerPosition,
2710
+ estimatedHeight: 252
2711
+ });
2712
+ popover.style.left = `${composer.left}px`;
2713
+ popover.style.top = `${composer.top}px`;
2714
+ popover.style.width = `${composer.width}px`;
2715
+ } else {
2716
+ popover.style.left = `${position.left}px`;
2717
+ popover.style.top = `${position.top}px`;
2718
+ }
2035
2719
  const form = document.createElement("form");
2036
2720
  form.className = "dfwr-form";
2037
- const meta = document.createElement("div");
2038
- meta.className = "dfwr-item-date";
2039
- meta.textContent = formatNoteDraftMeta(draft);
2721
+ const meta = isElementDraft ? void 0 : document.createElement("div");
2722
+ if (meta) {
2723
+ meta.className = "dfwr-item-date";
2724
+ meta.textContent = formatNoteDraftMeta(draft);
2725
+ }
2040
2726
  const textarea = document.createElement("textarea");
2041
2727
  textarea.className = "dfwr-textarea";
2042
2728
  textarea.placeholder = "Review comment";
@@ -2050,25 +2736,273 @@ var WebReviewKitView = class {
2050
2736
  comment: textarea.value
2051
2737
  });
2052
2738
  });
2053
- const actions = this.createFormActions("Save note", () => {
2739
+ const saveDraft = () => {
2054
2740
  const comment = textarea.value.trim();
2055
- if (!comment) return;
2741
+ const currentDraft = this.state.noteDraft ?? draft;
2742
+ if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
2056
2743
  void this.config.actions.createItem({
2057
2744
  kind: "note",
2058
- comment,
2059
- viewport: draft.viewport,
2060
- anchor: draft.anchor,
2061
- marker: draft.marker,
2062
- selection: draft.selection
2745
+ comment: this.withDraftAdjustmentComment(comment, currentDraft),
2746
+ viewport: currentDraft.viewport,
2747
+ anchor: currentDraft.anchor,
2748
+ marker: currentDraft.marker,
2749
+ selection: currentDraft.selection
2063
2750
  });
2064
- });
2065
- form.append(meta, textarea, actions);
2066
- popover.append(form);
2751
+ };
2752
+ const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
2753
+ draft,
2754
+ pin,
2755
+ popover,
2756
+ selectionHighlight,
2757
+ textarea
2758
+ }) : void 0;
2759
+ const actions = this.createFormActions("Save note", saveDraft);
2760
+ form.append(
2761
+ ...meta ? [meta] : [],
2762
+ ...adjustmentControls ? [adjustmentControls.panel] : [],
2763
+ textarea,
2764
+ actions
2765
+ );
2766
+ const dragHandle = isElementDraft ? this.createDraftDragHandle("Move DOM composer") : void 0;
2767
+ popover.append(...dragHandle ? [dragHandle] : [], form);
2067
2768
  group.append(pin, popover);
2068
- this.attachDraftPinDrag(pin, popover, meta, textarea);
2069
- window.setTimeout(() => textarea.focus(), 0);
2769
+ if (dragHandle) {
2770
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
2771
+ const noteDraft = this.state.noteDraft ?? draft;
2772
+ this.config.actions.setNoteDraft({
2773
+ ...noteDraft,
2774
+ composerPosition,
2775
+ comment: textarea.value
2776
+ });
2777
+ });
2778
+ }
2779
+ this.attachDraftPinDrag(
2780
+ pin,
2781
+ isElementDraft ? void 0 : popover,
2782
+ meta,
2783
+ textarea
2784
+ );
2785
+ window.setTimeout(() => {
2786
+ if (draft.adjustment?.isActive) {
2787
+ adjustmentControls?.focusTarget.focus();
2788
+ return;
2789
+ }
2790
+ textarea.focus();
2791
+ }, 0);
2070
2792
  return group;
2071
2793
  }
2794
+ createDraftDragHandle(label) {
2795
+ const handle = document.createElement("button");
2796
+ handle.className = "dfwr-draft-drag-handle";
2797
+ handle.type = "button";
2798
+ handle.setAttribute("aria-label", label);
2799
+ return handle;
2800
+ }
2801
+ createIcon(paths) {
2802
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2803
+ svg.setAttribute("aria-hidden", "true");
2804
+ svg.setAttribute("viewBox", "0 0 24 24");
2805
+ svg.setAttribute("fill", "none");
2806
+ svg.setAttribute("stroke", "currentColor");
2807
+ svg.setAttribute("stroke-width", "2.4");
2808
+ svg.setAttribute("stroke-linecap", "round");
2809
+ svg.setAttribute("stroke-linejoin", "round");
2810
+ paths.forEach((d) => {
2811
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2812
+ path.setAttribute("d", d);
2813
+ svg.append(path);
2814
+ });
2815
+ return svg;
2816
+ }
2817
+ setAdjustmentToggleIcon(button, isActive) {
2818
+ const paths = isActive ? ["M20 6 9 17l-5-5"] : [
2819
+ "M12 2v20",
2820
+ "M2 12h20",
2821
+ "m9 5 3-3 3 3",
2822
+ "m9 19 3 3 3-3",
2823
+ "m5 9-3 3 3 3",
2824
+ "m19 9 3 3-3 3"
2825
+ ];
2826
+ button.replaceChildren(this.createIcon(paths));
2827
+ }
2828
+ attachDraftComposerDrag(popover, handle, onMove) {
2829
+ let isDragging = false;
2830
+ let offsetX = 0;
2831
+ let offsetY = 0;
2832
+ const movePopover = (event) => {
2833
+ const environment = this.config.getEnvironment();
2834
+ if (!environment) return;
2835
+ const position = this.getClampedComposerPosition(
2836
+ {
2837
+ x: event.clientX - offsetX,
2838
+ y: event.clientY - offsetY
2839
+ },
2840
+ environment,
2841
+ {
2842
+ width: popover.offsetWidth,
2843
+ height: popover.offsetHeight
2844
+ },
2845
+ this.getHostComposerBounds()
2846
+ );
2847
+ popover.style.left = `${position.x}px`;
2848
+ popover.style.top = `${position.y}px`;
2849
+ onMove(position);
2850
+ };
2851
+ handle.addEventListener("pointerdown", (event) => {
2852
+ if (event.button !== 0) return;
2853
+ const rect = popover.getBoundingClientRect();
2854
+ offsetX = event.clientX - rect.left;
2855
+ offsetY = event.clientY - rect.top;
2856
+ isDragging = true;
2857
+ event.preventDefault();
2858
+ event.stopPropagation();
2859
+ handle.setPointerCapture(event.pointerId);
2860
+ popover.classList.add("is-dragging");
2861
+ });
2862
+ handle.addEventListener("pointermove", (event) => {
2863
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
2864
+ event.preventDefault();
2865
+ movePopover(event);
2866
+ });
2867
+ const stopDrag = (event) => {
2868
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
2869
+ event.preventDefault();
2870
+ event.stopPropagation();
2871
+ isDragging = false;
2872
+ handle.releasePointerCapture(event.pointerId);
2873
+ popover.classList.remove("is-dragging");
2874
+ movePopover(event);
2875
+ };
2876
+ handle.addEventListener("pointerup", stopDrag);
2877
+ handle.addEventListener("pointercancel", stopDrag);
2878
+ }
2879
+ createAdjustmentControls({
2880
+ draft,
2881
+ pin,
2882
+ popover,
2883
+ selectionHighlight,
2884
+ textarea
2885
+ }) {
2886
+ const panel = document.createElement("div");
2887
+ panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
2888
+ const header = document.createElement("div");
2889
+ header.className = "dfwr-adjust-panel-header";
2890
+ const help = document.createElement("div");
2891
+ help.className = "dfwr-adjust-help";
2892
+ help.textContent = this.getAdjustmentLabel();
2893
+ const adjust = document.createElement("button");
2894
+ adjust.className = "dfwr-adjust-toggle";
2895
+ adjust.type = "button";
2896
+ adjust.title = "Adjust DOM element with keyboard arrows";
2897
+ adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
2898
+ const xyStatus = document.createElement("div");
2899
+ xyStatus.className = "dfwr-adjust-status";
2900
+ const scaleStatus = document.createElement("div");
2901
+ scaleStatus.className = "dfwr-adjust-status";
2902
+ const syncControls = (nextDraft) => {
2903
+ const isActive = nextDraft.adjustment?.isActive === true;
2904
+ panel.classList.toggle("is-active", isActive);
2905
+ adjust.classList.toggle("is-active", isActive);
2906
+ adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
2907
+ this.setAdjustmentToggleIcon(adjust, isActive);
2908
+ adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
2909
+ adjust.setAttribute(
2910
+ "aria-label",
2911
+ isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
2912
+ );
2913
+ const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
2914
+ xyStatus.textContent = xyLine;
2915
+ scaleStatus.textContent = scaleLine;
2916
+ this.syncDraftAdjustmentUi({
2917
+ draft: nextDraft,
2918
+ pin,
2919
+ selectionHighlight
2920
+ });
2921
+ };
2922
+ const updateDraft = (updater) => {
2923
+ const currentDraft = this.state.noteDraft ?? draft;
2924
+ const nextDraft = updater(currentDraft);
2925
+ this.config.actions.setNoteDraft({
2926
+ ...nextDraft,
2927
+ comment: textarea.value
2928
+ });
2929
+ syncControls(nextDraft);
2930
+ };
2931
+ adjust.addEventListener("click", () => {
2932
+ updateDraft((currentDraft) => ({
2933
+ ...currentDraft,
2934
+ adjustment: {
2935
+ x: currentDraft.adjustment?.x ?? 0,
2936
+ y: currentDraft.adjustment?.y ?? 0,
2937
+ scale: currentDraft.adjustment?.scale ?? 0,
2938
+ isActive: currentDraft.adjustment?.isActive !== true
2939
+ }
2940
+ }));
2941
+ adjust.focus();
2942
+ });
2943
+ popover.addEventListener("keydown", (event) => {
2944
+ const currentDraft = this.state.noteDraft ?? draft;
2945
+ if (currentDraft.adjustment?.isActive !== true) return;
2946
+ const keyDelta = this.getAdjustmentKeyDelta(event);
2947
+ if (!keyDelta) return;
2948
+ event.preventDefault();
2949
+ event.stopPropagation();
2950
+ updateDraft((activeDraft) => ({
2951
+ ...activeDraft,
2952
+ adjustment: {
2953
+ x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
2954
+ y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
2955
+ scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
2956
+ isActive: true
2957
+ }
2958
+ }));
2959
+ });
2960
+ header.append(help, adjust);
2961
+ panel.append(header, xyStatus, scaleStatus);
2962
+ syncControls(draft);
2963
+ return {
2964
+ panel,
2965
+ focusTarget: adjust
2966
+ };
2967
+ }
2968
+ getAdjustmentKeyDelta(event) {
2969
+ const step = event.shiftKey ? 10 : 1;
2970
+ if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
2971
+ if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
2972
+ if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
2973
+ if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
2974
+ if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
2975
+ if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
2976
+ return void 0;
2977
+ }
2978
+ syncDraftAdjustmentUi({
2979
+ draft,
2980
+ pin,
2981
+ selectionHighlight
2982
+ }) {
2983
+ const environment = this.config.getEnvironment();
2984
+ if (!environment) return;
2985
+ const hostPoint = toHostPoint(
2986
+ this.getAdjustedDraftPoint(draft.marker.viewport, draft),
2987
+ environment
2988
+ );
2989
+ pin.style.left = `${hostPoint.x}px`;
2990
+ pin.style.top = `${hostPoint.y}px`;
2991
+ if (draft.selection && selectionHighlight) {
2992
+ const rect = toHostSelection(
2993
+ this.getAdjustedDraftSelection(
2994
+ toViewportSelection(draft.selection.viewport),
2995
+ draft
2996
+ ),
2997
+ environment
2998
+ );
2999
+ selectionHighlight.style.left = `${rect.left}px`;
3000
+ selectionHighlight.style.top = `${rect.top}px`;
3001
+ selectionHighlight.style.width = `${rect.width}px`;
3002
+ selectionHighlight.style.height = `${rect.height}px`;
3003
+ }
3004
+ this.syncDraftPreview(draft);
3005
+ }
2072
3006
  createAreaForm() {
2073
3007
  const form = document.createElement("form");
2074
3008
  form.className = "dfwr-form";
@@ -2080,14 +3014,20 @@ var WebReviewKitView = class {
2080
3014
  form.append(empty);
2081
3015
  return form;
2082
3016
  }
2083
- const meta = document.createElement("div");
2084
- meta.className = "dfwr-item-date";
2085
- meta.textContent = formatAreaDraftMeta(areaDraft);
2086
- form.append(meta);
3017
+ form.append(this.createAreaMetricsPanel(areaDraft));
2087
3018
  const textarea = document.createElement("textarea");
2088
3019
  textarea.className = "dfwr-textarea";
2089
3020
  textarea.placeholder = "Area comment";
2090
3021
  textarea.rows = 4;
3022
+ textarea.value = areaDraft.comment ?? "";
3023
+ textarea.addEventListener("input", () => {
3024
+ const draft = this.state.areaDraft;
3025
+ if (!draft) return;
3026
+ this.config.actions.setAreaDraft({
3027
+ ...draft,
3028
+ comment: textarea.value
3029
+ });
3030
+ });
2091
3031
  const actions = this.createFormActions("Save area", () => {
2092
3032
  const comment = textarea.value.trim();
2093
3033
  const draft = this.state.areaDraft;
@@ -2104,6 +3044,25 @@ var WebReviewKitView = class {
2104
3044
  form.append(textarea, actions);
2105
3045
  return form;
2106
3046
  }
3047
+ createAreaMetricsPanel(draft) {
3048
+ const panel = document.createElement("div");
3049
+ panel.className = "dfwr-adjust-panel is-area-metrics-panel";
3050
+ const help = document.createElement("div");
3051
+ help.className = "dfwr-adjust-help";
3052
+ const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
3053
+ this.getAreaDraftMetricSelection(draft),
3054
+ draft.viewport
3055
+ );
3056
+ help.textContent = labelLine;
3057
+ const xyStatus = document.createElement("div");
3058
+ xyStatus.className = "dfwr-adjust-status";
3059
+ xyStatus.textContent = xyLine;
3060
+ const sizeStatus = document.createElement("div");
3061
+ sizeStatus.className = "dfwr-adjust-status";
3062
+ sizeStatus.textContent = sizeLine;
3063
+ panel.append(help, xyStatus, sizeStatus);
3064
+ return panel;
3065
+ }
2107
3066
  createAreaDraftOverlay(draft) {
2108
3067
  const layer = document.createElement("div");
2109
3068
  layer.className = "dfwr-area-preview-layer";
@@ -2132,37 +3091,61 @@ var WebReviewKitView = class {
2132
3091
  createAreaDraftPopover(draft) {
2133
3092
  const environment = this.config.getEnvironment();
2134
3093
  const popover = document.createElement("div");
2135
- popover.className = "dfwr-area-draft";
3094
+ popover.className = "dfwr-area-draft is-composer";
2136
3095
  if (environment && draft.selection) {
2137
3096
  const selection = toHostSelection(
2138
3097
  toViewportSelection(draft.selection.viewport),
2139
3098
  environment
2140
3099
  );
2141
- const position = getAreaPopoverPosition(selection, environment);
2142
- popover.style.left = `${position.left}px`;
2143
- popover.style.top = `${position.top}px`;
3100
+ const composer = this.getDraftComposerPosition({
3101
+ selection,
3102
+ environment,
3103
+ composerPosition: draft.composerPosition,
3104
+ estimatedHeight: 220
3105
+ });
3106
+ popover.style.left = `${composer.left}px`;
3107
+ popover.style.top = `${composer.top}px`;
3108
+ popover.style.width = `${composer.width}px`;
2144
3109
  popover.style.right = "auto";
2145
3110
  }
2146
- popover.append(this.createAreaForm());
3111
+ const dragHandle = this.createDraftDragHandle("Move area composer");
3112
+ popover.append(dragHandle, this.createAreaForm());
3113
+ this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3114
+ const areaDraft = this.state.areaDraft ?? draft;
3115
+ this.config.actions.setAreaDraft({
3116
+ ...areaDraft,
3117
+ composerPosition
3118
+ });
3119
+ });
2147
3120
  return popover;
2148
3121
  }
2149
- createFormActions(saveLabel, onSave) {
3122
+ createFormActions(saveLabel, onSave, options) {
2150
3123
  const actions = document.createElement("div");
2151
- actions.className = "dfwr-actions";
3124
+ actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
2152
3125
  const save = document.createElement("button");
2153
3126
  save.className = "dfwr-button is-primary";
2154
3127
  save.type = "button";
2155
3128
  save.textContent = saveLabel;
2156
- save.addEventListener("click", onSave);
3129
+ save.addEventListener("click", (event) => {
3130
+ event.preventDefault();
3131
+ event.stopPropagation();
3132
+ onSave();
3133
+ });
2157
3134
  const cancel = document.createElement("button");
2158
3135
  cancel.className = "dfwr-button";
2159
3136
  cancel.type = "button";
2160
3137
  cancel.textContent = "Cancel";
2161
- cancel.addEventListener("click", () => {
3138
+ cancel.addEventListener("click", (event) => {
3139
+ event.preventDefault();
3140
+ event.stopPropagation();
2162
3141
  this.config.actions.setModeState("idle");
2163
3142
  this.config.actions.clearDrafts();
2164
3143
  this.config.actions.render();
2165
3144
  });
3145
+ if (options?.beforeSave?.length || options?.className) {
3146
+ actions.append(cancel, ...options.beforeSave ?? [], save);
3147
+ return actions;
3148
+ }
2166
3149
  actions.append(save, cancel);
2167
3150
  return actions;
2168
3151
  }
@@ -2367,11 +3350,13 @@ ${formatItemMeta(item)}`;
2367
3350
  if (!environment) return;
2368
3351
  const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
2369
3352
  const nextHostPoint = toHostPoint(nextPoint, environment);
2370
- const position = getPopoverPosition(nextHostPoint, environment);
2371
3353
  pin.style.left = `${nextHostPoint.x}px`;
2372
3354
  pin.style.top = `${nextHostPoint.y}px`;
2373
- popover.style.left = `${position.left}px`;
2374
- popover.style.top = `${position.top}px`;
3355
+ if (popover) {
3356
+ const position = getPopoverPosition(nextHostPoint, environment);
3357
+ popover.style.left = `${position.left}px`;
3358
+ popover.style.top = `${position.top}px`;
3359
+ }
2375
3360
  const noteDraft = this.state.noteDraft;
2376
3361
  if (!noteDraft) return;
2377
3362
  const nextDraft = {
@@ -2383,7 +3368,9 @@ ${formatItemMeta(item)}`;
2383
3368
  comment: textarea.value
2384
3369
  };
2385
3370
  this.config.actions.setNoteDraft(nextDraft);
2386
- meta.textContent = formatNoteDraftMeta(nextDraft);
3371
+ if (meta) {
3372
+ meta.textContent = formatNoteDraftMeta(nextDraft);
3373
+ }
2387
3374
  };
2388
3375
  pin.addEventListener("pointerdown", (event) => {
2389
3376
  if (event.button !== 0) return;
@@ -2616,6 +3603,9 @@ var WebReviewKitApp = class {
2616
3603
  setNoteDraft: (draft) => {
2617
3604
  this.noteDraft = draft;
2618
3605
  },
3606
+ setAreaDraft: (draft) => {
3607
+ this.areaDraft = draft;
3608
+ },
2619
3609
  setSelectingArea: (isSelectingArea) => {
2620
3610
  this.isSelectingArea = isSelectingArea;
2621
3611
  },
@@ -2641,6 +3631,7 @@ var WebReviewKitApp = class {
2641
3631
  this.render();
2642
3632
  }
2643
3633
  destroy() {
3634
+ this.view.clearDraftPreview();
2644
3635
  document.removeEventListener("keydown", this.handleKeyDown, true);
2645
3636
  window.removeEventListener("scroll", this.handleViewportChange, true);
2646
3637
  window.removeEventListener("resize", this.handleViewportChange);
@@ -2844,14 +3835,27 @@ var WebReviewKitApp = class {
2844
3835
  const viewport = getViewportSize(environment);
2845
3836
  const nextPoint = clampPoint(point, environment);
2846
3837
  const draft = await this.withOverlayHidden(() => {
3838
+ const pointSelection = getPointSelection(nextPoint);
3839
+ const targetElement = environment.document.elementFromPoint(
3840
+ nextPoint.x,
3841
+ nextPoint.y
3842
+ );
3843
+ const previewElement = targetElement && "style" in targetElement ? targetElement : void 0;
3844
+ const targetRect = targetElement?.getBoundingClientRect();
3845
+ const clickedSelection = targetRect && targetRect.width > 0 && targetRect.height > 0 ? {
3846
+ left: targetRect.left,
3847
+ top: targetRect.top,
3848
+ width: targetRect.width,
3849
+ height: targetRect.height
3850
+ } : void 0;
2847
3851
  const anchor = getDomAnchorFromPoint(
2848
3852
  nextPoint,
2849
3853
  this.options.anchors?.attribute,
2850
3854
  environment
2851
3855
  );
2852
- const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
2853
- const selection = elementSelection ?? getPointSelection(nextPoint);
2854
- const markerPoint = getSelectionCenter(selection);
3856
+ const elementSelection = anchor ? clickedSelection ?? getElementViewportSelection(anchor, environment, pointSelection) : void 0;
3857
+ const selection = elementSelection ?? pointSelection;
3858
+ const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
2855
3859
  const reviewSelection = elementSelection ? {
2856
3860
  viewport: toPublicSelection(elementSelection),
2857
3861
  relative: getRelativeSelection(
@@ -2869,7 +3873,8 @@ var WebReviewKitApp = class {
2869
3873
  anchor,
2870
3874
  marker,
2871
3875
  selection: reviewSelection,
2872
- comment
3876
+ comment,
3877
+ previewElement
2873
3878
  };
2874
3879
  });
2875
3880
  this.noteDraft = draft;
@@ -3009,4 +4014,4 @@ export {
3009
4014
  getNumberedReviewItems,
3010
4015
  createWebReviewKit
3011
4016
  };
3012
- //# sourceMappingURL=chunk-I76WEDLA.js.map
4017
+ //# sourceMappingURL=chunk-6L2KJ7XL.js.map