@designfever/web-review-kit 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -659,10 +787,38 @@ function getDomPath(element) {
659
787
  }
660
788
  return `body > ${parts.join(" > ")}`;
661
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
+ }
662
811
  function getTextFingerprint(element) {
663
812
  const text = element.textContent?.replace(/\s+/g, " ").trim();
664
813
  return text ? text.slice(0, 120) : void 0;
665
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
+ }
666
822
  function getTextFingerprintScore(expected, actual) {
667
823
  if (!expected) return 1;
668
824
  if (!actual) return 0.5;
@@ -1038,6 +1194,19 @@ function createStyleElement() {
1038
1194
  display: block;
1039
1195
  }
1040
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
+
1041
1210
  .dfwr-panel {
1042
1211
  position: fixed;
1043
1212
  right: 16px;
@@ -1531,6 +1700,40 @@ function createStyleElement() {
1531
1700
  box-shadow: var(--df-review-shadow-popover);
1532
1701
  }
1533
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
+
1534
1737
  .dfwr-area-draft {
1535
1738
  position: fixed;
1536
1739
  right: 16px;
@@ -1552,6 +1755,14 @@ function createStyleElement() {
1552
1755
  padding: 0;
1553
1756
  }
1554
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
+
1555
1766
  .dfwr-area-draft .dfwr-actions {
1556
1767
  padding: 0;
1557
1768
  }
@@ -1580,6 +1791,105 @@ function createStyleElement() {
1580
1791
  outline-offset: 1px;
1581
1792
  }
1582
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
+
1583
1893
  .dfwr-empty,
1584
1894
  .dfwr-error {
1585
1895
  margin: 0;
@@ -1800,16 +2110,6 @@ function createStyleElement() {
1800
2110
  }
1801
2111
 
1802
2112
  // 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
2113
  function formatNoteDraftMeta(draft) {
1814
2114
  const parts = [
1815
2115
  `viewport ${formatSize(draft.viewport)}`,
@@ -1875,17 +2175,29 @@ function formatAnchorMeta(anchor) {
1875
2175
  }
1876
2176
 
1877
2177
  // src/core/web.review.kit.view.ts
2178
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
1878
2179
  var WebReviewKitView = class {
1879
2180
  constructor(config) {
1880
2181
  this.config = config;
1881
2182
  }
2183
+ clearDraftPreview() {
2184
+ this.restoreDraftPreview();
2185
+ }
1882
2186
  render(shadow, hiddenItemsStyle) {
1883
2187
  const state = this.state;
2188
+ this.syncDraftPreview(
2189
+ state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2190
+ );
1884
2191
  shadow.replaceChildren();
1885
2192
  shadow.append(createStyleElement());
1886
2193
  shadow.append(hiddenItemsStyle);
2194
+ const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
1887
2195
  const shell = document.createElement("div");
1888
- 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(" ");
1889
2201
  shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
1890
2202
  if (this.config.options.ui?.panel !== false) {
1891
2203
  const panel = document.createElement("div");
@@ -1901,6 +2213,9 @@ var WebReviewKitView = class {
1901
2213
  shell.append(panel);
1902
2214
  }
1903
2215
  shell.append(this.createMarkerLayer());
2216
+ if (state.isOpen && hasDismissableDraft) {
2217
+ shell.append(this.createDraftCancelLayer());
2218
+ }
1904
2219
  if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
1905
2220
  shell.append(
1906
2221
  state.noteDraft ? this.createNotePopover(state.noteDraft) : state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
@@ -1920,6 +2235,294 @@ var WebReviewKitView = class {
1920
2235
  get state() {
1921
2236
  return this.config.getState();
1922
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
+ }
1923
2526
  createHeader() {
1924
2527
  const header = document.createElement("div");
1925
2528
  header.className = "dfwr-header";
@@ -2011,15 +2614,20 @@ var WebReviewKitView = class {
2011
2614
  const group = document.createElement("div");
2012
2615
  group.className = "dfwr-note-draft";
2013
2616
  if (!environment) return group;
2014
- 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;
2015
2623
  if (draft.selection) {
2016
- group.append(
2017
- this.createSelectionHighlight(
2018
- toViewportSelection(draft.selection.viewport),
2019
- environment,
2020
- true
2021
- )
2624
+ const selection = toViewportSelection(draft.selection.viewport);
2625
+ selectionHighlight = this.createSelectionHighlight(
2626
+ isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2627
+ environment,
2628
+ true
2022
2629
  );
2630
+ group.append(selectionHighlight);
2023
2631
  }
2024
2632
  const pin = document.createElement("button");
2025
2633
  pin.className = "dfwr-note-pin";
@@ -2029,14 +2637,35 @@ var WebReviewKitView = class {
2029
2637
  pin.style.top = `${hostPoint.y}px`;
2030
2638
  const popover = document.createElement("div");
2031
2639
  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`;
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
+ }
2035
2662
  const form = document.createElement("form");
2036
2663
  form.className = "dfwr-form";
2037
- const meta = document.createElement("div");
2038
- meta.className = "dfwr-item-date";
2039
- 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
+ }
2040
2669
  const textarea = document.createElement("textarea");
2041
2670
  textarea.className = "dfwr-textarea";
2042
2671
  textarea.placeholder = "Review comment";
@@ -2050,25 +2679,306 @@ var WebReviewKitView = class {
2050
2679
  comment: textarea.value
2051
2680
  });
2052
2681
  });
2053
- const actions = this.createFormActions("Save note", () => {
2682
+ const saveDraft = () => {
2054
2683
  const comment = textarea.value.trim();
2055
2684
  if (!comment) return;
2685
+ const currentDraft = this.state.noteDraft ?? draft;
2056
2686
  void this.config.actions.createItem({
2057
2687
  kind: "note",
2058
- comment,
2059
- viewport: draft.viewport,
2060
- anchor: draft.anchor,
2061
- marker: draft.marker,
2062
- 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
2063
2693
  });
2064
- });
2065
- form.append(meta, textarea, actions);
2066
- 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);
2067
2716
  group.append(pin, popover);
2068
- this.attachDraftPinDrag(pin, popover, meta, textarea);
2069
- 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);
2070
2740
  return group;
2071
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
+ }
2072
2982
  createAreaForm() {
2073
2983
  const form = document.createElement("form");
2074
2984
  form.className = "dfwr-form";
@@ -2080,14 +2990,20 @@ var WebReviewKitView = class {
2080
2990
  form.append(empty);
2081
2991
  return form;
2082
2992
  }
2083
- const meta = document.createElement("div");
2084
- meta.className = "dfwr-item-date";
2085
- meta.textContent = formatAreaDraftMeta(areaDraft);
2086
- form.append(meta);
2993
+ form.append(this.createAreaMetricsPanel(areaDraft));
2087
2994
  const textarea = document.createElement("textarea");
2088
2995
  textarea.className = "dfwr-textarea";
2089
2996
  textarea.placeholder = "Area comment";
2090
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
+ });
2091
3007
  const actions = this.createFormActions("Save area", () => {
2092
3008
  const comment = textarea.value.trim();
2093
3009
  const draft = this.state.areaDraft;
@@ -2104,6 +3020,25 @@ var WebReviewKitView = class {
2104
3020
  form.append(textarea, actions);
2105
3021
  return form;
2106
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
+ }
2107
3042
  createAreaDraftOverlay(draft) {
2108
3043
  const layer = document.createElement("div");
2109
3044
  layer.className = "dfwr-area-preview-layer";
@@ -2132,23 +3067,37 @@ var WebReviewKitView = class {
2132
3067
  createAreaDraftPopover(draft) {
2133
3068
  const environment = this.config.getEnvironment();
2134
3069
  const popover = document.createElement("div");
2135
- popover.className = "dfwr-area-draft";
3070
+ popover.className = "dfwr-area-draft is-composer";
2136
3071
  if (environment && draft.selection) {
2137
3072
  const selection = toHostSelection(
2138
3073
  toViewportSelection(draft.selection.viewport),
2139
3074
  environment
2140
3075
  );
2141
- const position = getAreaPopoverPosition(selection, environment);
2142
- popover.style.left = `${position.left}px`;
2143
- 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`;
2144
3085
  popover.style.right = "auto";
2145
3086
  }
2146
- 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
+ });
2147
3096
  return popover;
2148
3097
  }
2149
- createFormActions(saveLabel, onSave) {
3098
+ createFormActions(saveLabel, onSave, options) {
2150
3099
  const actions = document.createElement("div");
2151
- actions.className = "dfwr-actions";
3100
+ actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
2152
3101
  const save = document.createElement("button");
2153
3102
  save.className = "dfwr-button is-primary";
2154
3103
  save.type = "button";
@@ -2163,6 +3112,10 @@ var WebReviewKitView = class {
2163
3112
  this.config.actions.clearDrafts();
2164
3113
  this.config.actions.render();
2165
3114
  });
3115
+ if (options?.beforeSave?.length || options?.className) {
3116
+ actions.append(cancel, ...options.beforeSave ?? [], save);
3117
+ return actions;
3118
+ }
2166
3119
  actions.append(save, cancel);
2167
3120
  return actions;
2168
3121
  }
@@ -2367,11 +3320,13 @@ ${formatItemMeta(item)}`;
2367
3320
  if (!environment) return;
2368
3321
  const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
2369
3322
  const nextHostPoint = toHostPoint(nextPoint, environment);
2370
- const position = getPopoverPosition(nextHostPoint, environment);
2371
3323
  pin.style.left = `${nextHostPoint.x}px`;
2372
3324
  pin.style.top = `${nextHostPoint.y}px`;
2373
- popover.style.left = `${position.left}px`;
2374
- 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
+ }
2375
3330
  const noteDraft = this.state.noteDraft;
2376
3331
  if (!noteDraft) return;
2377
3332
  const nextDraft = {
@@ -2383,7 +3338,9 @@ ${formatItemMeta(item)}`;
2383
3338
  comment: textarea.value
2384
3339
  };
2385
3340
  this.config.actions.setNoteDraft(nextDraft);
2386
- meta.textContent = formatNoteDraftMeta(nextDraft);
3341
+ if (meta) {
3342
+ meta.textContent = formatNoteDraftMeta(nextDraft);
3343
+ }
2387
3344
  };
2388
3345
  pin.addEventListener("pointerdown", (event) => {
2389
3346
  if (event.button !== 0) return;
@@ -2616,6 +3573,9 @@ var WebReviewKitApp = class {
2616
3573
  setNoteDraft: (draft) => {
2617
3574
  this.noteDraft = draft;
2618
3575
  },
3576
+ setAreaDraft: (draft) => {
3577
+ this.areaDraft = draft;
3578
+ },
2619
3579
  setSelectingArea: (isSelectingArea) => {
2620
3580
  this.isSelectingArea = isSelectingArea;
2621
3581
  },
@@ -2641,6 +3601,7 @@ var WebReviewKitApp = class {
2641
3601
  this.render();
2642
3602
  }
2643
3603
  destroy() {
3604
+ this.view.clearDraftPreview();
2644
3605
  document.removeEventListener("keydown", this.handleKeyDown, true);
2645
3606
  window.removeEventListener("scroll", this.handleViewportChange, true);
2646
3607
  window.removeEventListener("resize", this.handleViewportChange);
@@ -2851,7 +3812,7 @@ var WebReviewKitApp = class {
2851
3812
  );
2852
3813
  const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
2853
3814
  const selection = elementSelection ?? getPointSelection(nextPoint);
2854
- const markerPoint = getSelectionCenter(selection);
3815
+ const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
2855
3816
  const reviewSelection = elementSelection ? {
2856
3817
  viewport: toPublicSelection(elementSelection),
2857
3818
  relative: getRelativeSelection(
@@ -3009,4 +3970,4 @@ export {
3009
3970
  getNumberedReviewItems,
3010
3971
  createWebReviewKit
3011
3972
  };
3012
- //# sourceMappingURL=chunk-I76WEDLA.js.map
3973
+ //# sourceMappingURL=chunk-QFNYQCTA.js.map