@designfever/web-review-kit 0.7.2 → 0.8.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.
Files changed (40) hide show
  1. package/README.md +7 -4
  2. package/dist/{chunk-RPVLRULC.js → chunk-4ZP7B7R6.js} +2345 -1786
  3. package/dist/chunk-4ZP7B7R6.js.map +1 -0
  4. package/dist/{chunk-K26WLRRP.js → chunk-UNDQZ4Y2.js} +33 -9
  5. package/dist/chunk-UNDQZ4Y2.js.map +1 -0
  6. package/dist/index.cjs +2381 -1801
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +17 -5
  9. package/dist/index.d.ts +17 -5
  10. package/dist/index.js +11 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/react-shell.cjs +14508 -11099
  13. package/dist/react-shell.cjs.map +1 -1
  14. package/dist/react-shell.d.cts +5 -4
  15. package/dist/react-shell.d.ts +5 -4
  16. package/dist/react-shell.js +10634 -7710
  17. package/dist/react-shell.js.map +1 -1
  18. package/dist/{types-DT9Z66mV.d.cts → types-BM8E7BFV.d.cts} +63 -3
  19. package/dist/{types-DT9Z66mV.d.ts → types-BM8E7BFV.d.ts} +63 -3
  20. package/dist/vite.cjs +344 -48
  21. package/dist/vite.cjs.map +1 -1
  22. package/dist/vite.d.cts +2 -0
  23. package/dist/vite.d.ts +2 -0
  24. package/dist/vite.js +346 -50
  25. package/dist/vite.js.map +1 -1
  26. package/docs/README.md +19 -11
  27. package/docs/adapters.md +56 -4
  28. package/docs/adaptor.sample.ts +53 -3
  29. package/docs/architecture.md +117 -3
  30. package/docs/figma-overlay.md +51 -0
  31. package/docs/grid-overlay.md +5 -0
  32. package/docs/installation.md +23 -8
  33. package/docs/release-notes-0.7.3.md +30 -0
  34. package/docs/release-notes-0.8.0.md +175 -0
  35. package/docs/testing.md +68 -0
  36. package/package.json +14 -5
  37. package/dist/chunk-K26WLRRP.js.map +0 -1
  38. package/dist/chunk-RPVLRULC.js.map +0 -1
  39. package/docs/code-review-0.6.0.md +0 -232
  40. package/docs/figma-image-mvp-0.7.0.md +0 -330
@@ -99,8 +99,8 @@ function localAdapter(options = {}) {
99
99
  function normalizeStoredReviewItem(value) {
100
100
  if (!value || typeof value !== "object") return void 0;
101
101
  const raw = value;
102
- const kind = raw.kind === "text" ? "note" : raw.kind === "capture" ? "area" : raw.kind;
103
- if (kind !== "note" && kind !== "area") return void 0;
102
+ const kind = raw.kind === "capture" ? "area" : raw.kind;
103
+ if (kind !== "dom" && kind !== "area") return void 0;
104
104
  const { screenshot: _screenshot, reviewNumber: _reviewNumber, ...item } = raw;
105
105
  if (kind === raw.kind && _screenshot === void 0 && _reviewNumber === void 0) {
106
106
  return raw;
@@ -125,8 +125,7 @@ var REVIEW_SCOPE_LABELS = {
125
125
  mobile: "Mobile",
126
126
  tablet: "Tablet",
127
127
  desktop: "Desktop",
128
- wide: "Wide",
129
- dom: "Element"
128
+ wide: "Wide"
130
129
  };
131
130
  var normalizeReviewItemScope = (value) => {
132
131
  if (value === "element") return "dom";
@@ -172,7 +171,6 @@ function getReviewItemScope(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
172
171
  }
173
172
  function getReviewItemScopeLabel(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
174
173
  const scope = getReviewItemScope(item, presets);
175
- if (scope === "dom") return REVIEW_SCOPE_LABELS.dom;
176
174
  const preset = findReviewViewportPreset(item.viewport, presets);
177
175
  return preset.label || REVIEW_SCOPE_LABELS[scope];
178
176
  }
@@ -315,26 +313,34 @@ function getPopoverBounds(environment) {
315
313
  }
316
314
  return environment.overlayRect;
317
315
  }
316
+ function getEnvironmentScale(environment) {
317
+ const scaleX = typeof environment.scaleX === "number" && Number.isFinite(environment.scaleX) && environment.scaleX > 0 ? environment.scaleX : 1;
318
+ const scaleY = typeof environment.scaleY === "number" && Number.isFinite(environment.scaleY) && environment.scaleY > 0 ? environment.scaleY : 1;
319
+ return { scaleX, scaleY };
320
+ }
318
321
  function toHostPoint(point, environment) {
319
322
  if (!environment) return point;
323
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
320
324
  return {
321
- x: point.x + environment.viewportRect.left,
322
- y: point.y + environment.viewportRect.top
325
+ x: point.x * scaleX + environment.viewportRect.left,
326
+ y: point.y * scaleY + environment.viewportRect.top
323
327
  };
324
328
  }
325
329
  function toHostSelection(selection, environment) {
330
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
326
331
  return {
327
- left: selection.left + environment.viewportRect.left,
328
- top: selection.top + environment.viewportRect.top,
329
- width: selection.width,
330
- height: selection.height
332
+ left: selection.left * scaleX + environment.viewportRect.left,
333
+ top: selection.top * scaleY + environment.viewportRect.top,
334
+ width: selection.width * scaleX,
335
+ height: selection.height * scaleY
331
336
  };
332
337
  }
333
338
  function toTargetPoint(point, environment) {
334
339
  if (!environment) return point;
340
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
335
341
  return {
336
- x: point.x - environment.viewportRect.left,
337
- y: point.y - environment.viewportRect.top
342
+ x: (point.x - environment.viewportRect.left) / scaleX,
343
+ y: (point.y - environment.viewportRect.top) / scaleY
338
344
  };
339
345
  }
340
346
  function toTargetPointFromHostEvent(event, environment) {
@@ -383,11 +389,6 @@ var SEMANTIC_ANCHOR_ATTRIBUTES = [
383
389
  "name",
384
390
  "href"
385
391
  ];
386
- function getDomAnchor(selection, configuredAttribute = "data-qa-id", environment) {
387
- const x = selection.left + selection.width / 2;
388
- const y = selection.top + selection.height / 2;
389
- return getDomAnchorFromPoint({ x, y }, configuredAttribute, environment);
390
- }
391
392
  function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", environment) {
392
393
  const target = environment.document.elementFromPoint(point.x, point.y);
393
394
  if (!target) return void 0;
@@ -937,6 +938,34 @@ function createId() {
937
938
  }
938
939
 
939
940
  // src/core/hotkey.ts
941
+ var HOTKEY_KEY_ALIASES = {
942
+ q: ["\u3142", "\u3143"],
943
+ w: ["\u3148", "\u3149"],
944
+ e: ["\u3137", "\u3138"],
945
+ r: ["\u3131", "\u3132"],
946
+ t: ["\u3145", "\u3146"],
947
+ y: ["\u315B"],
948
+ u: ["\u3155"],
949
+ i: ["\u3151"],
950
+ o: ["\u3150", "\u3152"],
951
+ p: ["\u3154", "\u3156"],
952
+ a: ["\u3141"],
953
+ s: ["\u3134"],
954
+ d: ["\u3147"],
955
+ f: ["\u3139"],
956
+ g: ["\u314E"],
957
+ h: ["\u3157"],
958
+ j: ["\u3153"],
959
+ k: ["\u314F"],
960
+ l: ["\u3163"],
961
+ z: ["\u314B"],
962
+ x: ["\u314C"],
963
+ c: ["\u314A"],
964
+ v: ["\u314D"],
965
+ b: ["\u3160"],
966
+ n: ["\u315C"],
967
+ m: ["\u3161"]
968
+ };
940
969
  function isHotkey(event, hotkey) {
941
970
  const parts = hotkey.split("+").map((part) => part.trim().toLowerCase()).filter(Boolean);
942
971
  const key = parts.find(
@@ -953,14 +982,14 @@ function isHotkey(event, hotkey) {
953
982
  }
954
983
  return isHotkeyKey(event, key);
955
984
  }
985
+ function getHotkeyActionKey(event, keys) {
986
+ return keys.find((key) => isHotkeyKey(event, key));
987
+ }
956
988
  function isHotkeyKey(event, key) {
957
989
  const normalizedKey = key.toLowerCase();
958
990
  if (event.key.toLowerCase() === normalizedKey) return true;
959
991
  if (getHotkeyCode(normalizedKey) === event.code) return true;
960
- const aliases = {
961
- q: ["\u3142", "\u3143"]
962
- };
963
- return aliases[normalizedKey]?.includes(event.key) ?? false;
992
+ return HOTKEY_KEY_ALIASES[normalizedKey]?.includes(event.key) ?? false;
964
993
  }
965
994
  function getHotkeyCode(key) {
966
995
  if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
@@ -995,7 +1024,7 @@ function getPublicSearch(location) {
995
1024
  function getBoundMarkerPoint(item, environment) {
996
1025
  const marker = getItemMarker(item);
997
1026
  if (!marker) return void 0;
998
- if (item.kind !== "area" && item.anchor && marker.relative) {
1027
+ if (item.anchor && marker.relative) {
999
1028
  const resolved = resolveAnchorElement(item.anchor, environment);
1000
1029
  const element = resolved?.element;
1001
1030
  if (element) {
@@ -1043,27 +1072,25 @@ function getItemHighlightSelection(item, environment) {
1043
1072
  environment
1044
1073
  );
1045
1074
  }
1046
- return getVisibleHighlightSelection(
1047
- [
1048
- getAnchorHighlightSelection(item, environment),
1049
- getBoundSelection(item, environment),
1050
- getPointHighlightSelection(item, environment)
1051
- ],
1052
- environment
1053
- );
1075
+ return void 0;
1054
1076
  }
1055
1077
  function getReviewItemHighlightMode(item) {
1056
1078
  if (isDomReviewItem(item)) return "dom";
1057
- if (item.kind === "area") return "area";
1058
- return "note";
1079
+ return "area";
1059
1080
  }
1060
1081
  function getItemMarker(item) {
1061
1082
  if (item.marker) return item.marker;
1062
1083
  const selection = getItemSelection(item);
1063
1084
  if (!selection?.viewport) return void 0;
1064
1085
  return {
1065
- viewport: roundPoint(getSelectionCenter(selection.viewport)),
1066
- relative: selection.relative ? roundPoint(getSelectionCenter(selection.relative)) : void 0
1086
+ viewport: roundPoint({
1087
+ x: selection.viewport.x,
1088
+ y: selection.viewport.y
1089
+ }),
1090
+ relative: selection.relative ? roundPoint({
1091
+ x: selection.relative.x,
1092
+ y: selection.relative.y
1093
+ }) : void 0
1067
1094
  };
1068
1095
  }
1069
1096
  function getItemSelection(item) {
@@ -1082,17 +1109,20 @@ function getItemSelection(item) {
1082
1109
  function shouldShowMarkerForScope(scope, currentScope) {
1083
1110
  return scope === currentScope;
1084
1111
  }
1085
- function createSelectionCenterMarker(selection, anchor, environment) {
1086
- const centerPoint = getSelectionCenter(selection);
1112
+ function createSelectionStartMarker(selection, anchor, environment) {
1113
+ const startPoint = {
1114
+ x: selection.left,
1115
+ y: selection.top
1116
+ };
1087
1117
  return {
1088
- viewport: roundPoint(centerPoint),
1089
- relative: anchor ? getRelativePoint(centerPoint, anchor, environment) : void 0
1118
+ viewport: roundPoint(startPoint),
1119
+ relative: anchor ? getRelativePoint(startPoint, anchor, environment) : void 0
1090
1120
  };
1091
1121
  }
1092
1122
  function getBoundSelection(item, environment) {
1093
1123
  const selection = getItemSelection(item);
1094
1124
  if (!selection?.viewport) return void 0;
1095
- if (item.kind !== "area" && item.anchor && selection.relative) {
1125
+ if (item.anchor && selection.relative) {
1096
1126
  const resolved = resolveAnchorElement(item.anchor, environment);
1097
1127
  const element = resolved?.element;
1098
1128
  if (element) {
@@ -1154,7 +1184,7 @@ function getVisibleHighlightSelection(candidates, environment) {
1154
1184
  );
1155
1185
  }
1156
1186
  function isDomReviewItem(item) {
1157
- return item.scope === "dom" || item.kind === "note" && Boolean(item.anchor && getItemSelection(item));
1187
+ return item.kind === "dom" || item.scope === "dom";
1158
1188
  }
1159
1189
 
1160
1190
  // src/core/scroll.ts
@@ -1251,6 +1281,117 @@ function getAdjustedDraftSelection(selection, draft, presets) {
1251
1281
  };
1252
1282
  }
1253
1283
 
1284
+ // src/core/draft.preview.ts
1285
+ var DraftPreviewController = class {
1286
+ constructor(config) {
1287
+ this.config = config;
1288
+ }
1289
+ clear() {
1290
+ if (!this.snapshot) return;
1291
+ const { element, clone, visibility } = this.snapshot;
1292
+ clone.remove();
1293
+ element.style.visibility = visibility;
1294
+ this.snapshot = void 0;
1295
+ }
1296
+ sync(draft) {
1297
+ const environment = this.config.getEnvironment();
1298
+ if (!draft || !environment || !this.config.hasAdjustment(draft)) {
1299
+ this.clear();
1300
+ return;
1301
+ }
1302
+ const element = this.getStyleableDraftElement(draft, environment);
1303
+ if (!element) {
1304
+ this.clear();
1305
+ return;
1306
+ }
1307
+ if (this.snapshot?.element !== element) {
1308
+ this.clear();
1309
+ }
1310
+ if (!this.snapshot) {
1311
+ const computedStyle = environment.window.getComputedStyle(element);
1312
+ const clone = element.cloneNode(true);
1313
+ removeDuplicateIds(clone);
1314
+ copyComputedStyle(element, clone, environment);
1315
+ positionDraftPreviewClone(clone, element, computedStyle);
1316
+ environment.document.body?.appendChild(clone);
1317
+ this.snapshot = {
1318
+ element,
1319
+ clone,
1320
+ visibility: element.style.visibility
1321
+ };
1322
+ element.style.visibility = "hidden";
1323
+ }
1324
+ const metrics = this.config.getMetrics(draft);
1325
+ const translate = `translate(${toCssNumber(metrics.cssX)}px, ${toCssNumber(
1326
+ metrics.cssY
1327
+ )}px)`;
1328
+ const scale = metrics.scaleFactor === 1 ? "" : `scale(${toCssNumber(metrics.scaleFactor)})`;
1329
+ this.snapshot.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
1330
+ }
1331
+ getStyleableDraftElement(draft, environment) {
1332
+ if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
1333
+ return draft.previewElement;
1334
+ }
1335
+ if (!draft.anchor) return void 0;
1336
+ const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
1337
+ const element = resolveAnchorElement(
1338
+ draft.anchor,
1339
+ environment,
1340
+ preferredSelection
1341
+ )?.element;
1342
+ if (!element) return void 0;
1343
+ if ("style" in element) return element;
1344
+ return void 0;
1345
+ }
1346
+ };
1347
+ function positionDraftPreviewClone(clone, element, computedStyle) {
1348
+ const rect = element.getBoundingClientRect();
1349
+ clone.setAttribute("data-dfwr-adjust-preview", "true");
1350
+ clone.setAttribute("aria-hidden", "true");
1351
+ clone.style.position = "fixed";
1352
+ clone.style.left = `${toCssNumber(rect.left)}px`;
1353
+ clone.style.top = `${toCssNumber(rect.top)}px`;
1354
+ clone.style.right = "auto";
1355
+ clone.style.bottom = "auto";
1356
+ clone.style.width = `${toCssNumber(rect.width)}px`;
1357
+ clone.style.height = `${toCssNumber(rect.height)}px`;
1358
+ clone.style.maxWidth = "none";
1359
+ clone.style.maxHeight = "none";
1360
+ clone.style.margin = "0";
1361
+ clone.style.boxSizing = "border-box";
1362
+ clone.style.display = getDraftPreviewDisplay(computedStyle.display);
1363
+ clone.style.zIndex = "2147483646";
1364
+ clone.style.pointerEvents = "none";
1365
+ clone.style.transition = "none";
1366
+ clone.style.willChange = "transform";
1367
+ clone.style.transformOrigin = "top left";
1368
+ clone.style.transform = "none";
1369
+ }
1370
+ function getDraftPreviewDisplay(display) {
1371
+ if (display === "inline" || display === "contents") return "inline-block";
1372
+ return display || "block";
1373
+ }
1374
+ function copyComputedStyle(element, clone, environment) {
1375
+ const computedStyle = environment.window.getComputedStyle(element);
1376
+ for (let index = 0; index < computedStyle.length; index += 1) {
1377
+ const property = computedStyle.item(index);
1378
+ clone.style.setProperty(
1379
+ property,
1380
+ computedStyle.getPropertyValue(property),
1381
+ computedStyle.getPropertyPriority(property)
1382
+ );
1383
+ }
1384
+ }
1385
+ function removeDuplicateIds(element) {
1386
+ element.removeAttribute("id");
1387
+ element.querySelectorAll("[id]").forEach((child) => {
1388
+ child.removeAttribute("id");
1389
+ });
1390
+ }
1391
+ function toCssNumber(value) {
1392
+ return Math.round(value * 1e3) / 1e3;
1393
+ }
1394
+
1254
1395
  // src/core/typography.tokens.ts
1255
1396
  var reviewTypographyTokens = `
1256
1397
  --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -1574,6 +1715,36 @@ function createStyleElement() {
1574
1715
  --dfwr-item-color-rgb: 255, 143, 97;
1575
1716
  }
1576
1717
 
1718
+ .dfwr-item-target-highlight.is-scope-mobile,
1719
+ .dfwr-item-target-label.is-scope-mobile {
1720
+ --dfwr-item-color: #7cc7ff;
1721
+ --dfwr-item-color-rgb: 124, 199, 255;
1722
+ }
1723
+
1724
+ .dfwr-item-target-highlight.is-scope-tablet,
1725
+ .dfwr-item-target-label.is-scope-tablet {
1726
+ --dfwr-item-color: #63d7c7;
1727
+ --dfwr-item-color-rgb: 99, 215, 199;
1728
+ }
1729
+
1730
+ .dfwr-item-target-highlight.is-scope-desktop,
1731
+ .dfwr-item-target-label.is-scope-desktop {
1732
+ --dfwr-item-color: #f3b75f;
1733
+ --dfwr-item-color-rgb: 243, 183, 95;
1734
+ }
1735
+
1736
+ .dfwr-item-target-highlight.is-scope-wide,
1737
+ .dfwr-item-target-label.is-scope-wide {
1738
+ --dfwr-item-color: #c99cff;
1739
+ --dfwr-item-color-rgb: 201, 156, 255;
1740
+ }
1741
+
1742
+ .dfwr-item-target-highlight.is-scope-dom,
1743
+ .dfwr-item-target-label.is-scope-dom {
1744
+ --dfwr-item-color: #ff8f61;
1745
+ --dfwr-item-color-rgb: 255, 143, 97;
1746
+ }
1747
+
1577
1748
  .dfwr-item-target-highlight {
1578
1749
  position: fixed;
1579
1750
  z-index: 2;
@@ -1582,7 +1753,6 @@ function createStyleElement() {
1582
1753
  background: rgba(var(--dfwr-item-color-rgb), 0.08);
1583
1754
  box-shadow:
1584
1755
  0 0 0 1px rgba(31, 36, 40, 0.78),
1585
- 0 0 0 9999px rgba(0, 0, 0, 0.08),
1586
1756
  0 12px 30px rgba(0, 0, 0, 0.24);
1587
1757
  pointer-events: none;
1588
1758
  }
@@ -1666,89 +1836,6 @@ function createStyleElement() {
1666
1836
  border-style: dashed;
1667
1837
  }
1668
1838
 
1669
- .dfwr-bound-marker.is-note-callout,
1670
- .dfwr-bound-marker.is-note-callout.is-highlighted {
1671
- --dfwr-scope: #7cc7ff;
1672
- --dfwr-scope-rgb: 124, 199, 255;
1673
- min-width: 0;
1674
- width: 0;
1675
- height: 0;
1676
- padding: 0;
1677
- transform: none;
1678
- border: 0;
1679
- border-radius: 0;
1680
- background: transparent;
1681
- box-shadow: none;
1682
- color: var(--dfwr-scope);
1683
- animation: none;
1684
- overflow: visible;
1685
- }
1686
-
1687
- .dfwr-bound-marker.is-note-callout::before {
1688
- content: "";
1689
- position: absolute;
1690
- left: 0;
1691
- top: 0;
1692
- width: 8px;
1693
- height: 8px;
1694
- transform: translate(-50%, -50%);
1695
- border: 2px solid #111820;
1696
- border-radius: var(--df-review-radius-pill);
1697
- background: var(--dfwr-scope);
1698
- box-shadow:
1699
- 0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.22),
1700
- 0 6px 16px rgba(0, 0, 0, 0.28);
1701
- }
1702
-
1703
- .dfwr-bound-marker.is-note-callout.is-highlighted::before {
1704
- animation: dfwr-note-dot-pulse 1000ms ease-in-out infinite;
1705
- }
1706
-
1707
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon {
1708
- position: absolute;
1709
- left: 0;
1710
- top: 0;
1711
- width: 31px;
1712
- height: 2px;
1713
- transform: rotate(-42deg);
1714
- transform-origin: left center;
1715
- border-radius: var(--df-review-radius-pill);
1716
- background: currentColor;
1717
- opacity: 1;
1718
- }
1719
-
1720
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::before,
1721
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::after {
1722
- display: none;
1723
- }
1724
-
1725
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-number {
1726
- position: absolute;
1727
- left: 24px;
1728
- top: -41px;
1729
- display: inline-flex;
1730
- align-items: center;
1731
- justify-content: center;
1732
- min-width: 28px;
1733
- height: 20px;
1734
- padding: 0 7px;
1735
- border: 1px solid var(--dfwr-scope);
1736
- border-radius: 4px;
1737
- background: var(--dfwr-scope);
1738
- box-shadow:
1739
- 0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.18),
1740
- 0 8px 18px rgba(0, 0, 0, 0.28);
1741
- color: #111820;
1742
- text-align: center;
1743
- line-height: 1;
1744
- white-space: nowrap;
1745
- }
1746
-
1747
- .dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-icon,
1748
- .dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-number {
1749
- animation: dfwr-selected-blink 1000ms ease-in-out infinite;
1750
- }
1751
-
1752
1839
  .dfwr-area-preview-layer .dfwr-bound-marker {
1753
1840
  border-color: #63d7c7;
1754
1841
  background: var(--df-review-color-panel);
@@ -1819,14 +1906,14 @@ function createStyleElement() {
1819
1906
  line-height: 1;
1820
1907
  }
1821
1908
 
1822
- .dfwr-note-draft {
1909
+ .dfwr-dom-draft {
1823
1910
  position: fixed;
1824
1911
  inset: 0;
1825
1912
  z-index: 4;
1826
1913
  pointer-events: none;
1827
1914
  }
1828
1915
 
1829
- .dfwr-note-pin {
1916
+ .dfwr-dom-pin {
1830
1917
  appearance: none;
1831
1918
  position: fixed;
1832
1919
  z-index: 5;
@@ -1844,11 +1931,11 @@ function createStyleElement() {
1844
1931
  pointer-events: auto;
1845
1932
  }
1846
1933
 
1847
- .dfwr-note-pin:active {
1934
+ .dfwr-dom-pin:active {
1848
1935
  cursor: grabbing;
1849
1936
  }
1850
1937
 
1851
- .dfwr-note-popover {
1938
+ .dfwr-dom-popover {
1852
1939
  position: fixed;
1853
1940
  z-index: 4;
1854
1941
  width: min(320px, calc(100vw - 24px));
@@ -1861,14 +1948,14 @@ function createStyleElement() {
1861
1948
  box-shadow: var(--df-review-shadow-popover);
1862
1949
  }
1863
1950
 
1864
- .dfwr-note-popover.is-composer,
1951
+ .dfwr-dom-popover.is-composer,
1865
1952
  .dfwr-area-draft.is-composer {
1866
1953
  max-height: min(360px, calc(100vh - 32px));
1867
1954
  overflow: auto;
1868
1955
  border-color: rgba(99, 215, 199, 0.56);
1869
1956
  }
1870
1957
 
1871
- .dfwr-shell.is-docked-composer .dfwr-note-popover.is-docked-composer,
1958
+ .dfwr-shell.is-docked-composer .dfwr-dom-popover.is-docked-composer,
1872
1959
  .dfwr-shell.is-docked-composer .dfwr-area-draft.is-docked-composer {
1873
1960
  position: relative;
1874
1961
  left: auto;
@@ -1882,7 +1969,7 @@ function createStyleElement() {
1882
1969
  min-height: 184px;
1883
1970
  }
1884
1971
 
1885
- .dfwr-note-popover.is-dragging,
1972
+ .dfwr-dom-popover.is-dragging,
1886
1973
  .dfwr-area-draft.is-dragging {
1887
1974
  user-select: none;
1888
1975
  }
@@ -1926,7 +2013,7 @@ function createStyleElement() {
1926
2013
  box-shadow: var(--df-review-shadow-popover);
1927
2014
  }
1928
2015
 
1929
- .dfwr-note-popover .dfwr-actions {
2016
+ .dfwr-dom-popover .dfwr-actions {
1930
2017
  padding: 0;
1931
2018
  }
1932
2019
 
@@ -1974,14 +2061,6 @@ function createStyleElement() {
1974
2061
  height: 18px;
1975
2062
  }
1976
2063
 
1977
- .dfwr-note-actions {
1978
- justify-content: flex-end;
1979
- }
1980
-
1981
- .dfwr-note-actions .dfwr-button:first-child {
1982
- margin-right: auto;
1983
- }
1984
-
1985
2064
  .dfwr-area-draft .dfwr-actions {
1986
2065
  padding: 0;
1987
2066
  }
@@ -1999,6 +2078,82 @@ function createStyleElement() {
1999
2078
  overflow-wrap: anywhere;
2000
2079
  }
2001
2080
 
2081
+ .dfwr-attachment-queue {
2082
+ display: grid;
2083
+ gap: 8px;
2084
+ min-width: 0;
2085
+ }
2086
+
2087
+ .dfwr-attachment-label {
2088
+ color: var(--df-review-color-text-muted);
2089
+ font-size: var(--df-review-font-size-xs);
2090
+ line-height: 1.35;
2091
+ }
2092
+
2093
+ .dfwr-attachment-list {
2094
+ display: grid;
2095
+ gap: 8px;
2096
+ }
2097
+
2098
+ .dfwr-attachment-item {
2099
+ display: grid;
2100
+ grid-template-columns: 42px minmax(0, 1fr) auto;
2101
+ align-items: center;
2102
+ gap: 8px;
2103
+ min-width: 0;
2104
+ padding: 6px;
2105
+ border: 1px solid rgba(255, 255, 255, 0.12);
2106
+ border-radius: var(--df-review-radius-sm);
2107
+ background: rgba(255, 255, 255, 0.04);
2108
+ }
2109
+
2110
+ .dfwr-attachment-thumb {
2111
+ display: block;
2112
+ width: 42px;
2113
+ height: 42px;
2114
+ object-fit: cover;
2115
+ border-radius: var(--df-review-radius-xs);
2116
+ background: var(--df-review-color-panel-strong);
2117
+ }
2118
+
2119
+ .dfwr-attachment-thumb.is-file {
2120
+ display: inline-flex;
2121
+ align-items: center;
2122
+ justify-content: center;
2123
+ color: var(--df-review-color-text-muted);
2124
+ font-size: var(--df-review-font-size-xs);
2125
+ font-weight: var(--df-review-font-weight-emphasis);
2126
+ }
2127
+
2128
+ .dfwr-attachment-name {
2129
+ min-width: 0;
2130
+ overflow: hidden;
2131
+ color: var(--df-review-color-text);
2132
+ font-size: var(--df-review-font-size-sm);
2133
+ line-height: 1.35;
2134
+ text-overflow: ellipsis;
2135
+ white-space: nowrap;
2136
+ }
2137
+
2138
+ .dfwr-attachment-remove {
2139
+ appearance: none;
2140
+ min-height: 28px;
2141
+ padding: 0 8px;
2142
+ border: 1px solid var(--df-review-color-border-strong);
2143
+ border-radius: var(--df-review-radius-sm);
2144
+ color: var(--df-review-color-text-muted);
2145
+ background: var(--df-review-color-control);
2146
+ cursor: pointer;
2147
+ font: inherit;
2148
+ font-size: var(--df-review-font-size-xs);
2149
+ line-height: 1;
2150
+ }
2151
+
2152
+ .dfwr-attachment-remove:hover {
2153
+ color: var(--df-review-color-text);
2154
+ background: var(--df-review-color-control-hover);
2155
+ }
2156
+
2002
2157
  .dfwr-input,
2003
2158
  .dfwr-select,
2004
2159
  .dfwr-textarea {
@@ -2301,18 +2456,6 @@ function createStyleElement() {
2301
2456
  }
2302
2457
  }
2303
2458
 
2304
- @keyframes dfwr-note-dot-pulse {
2305
- 0% {
2306
- transform: translate(-50%, -50%) scale(0.88);
2307
- }
2308
- 45% {
2309
- transform: translate(-50%, -50%) scale(1.3);
2310
- }
2311
- 100% {
2312
- transform: translate(-50%, -50%) scale(1);
2313
- }
2314
- }
2315
-
2316
2459
  @keyframes dfwr-selected-blink {
2317
2460
  0%,
2318
2461
  100% {
@@ -2361,1071 +2504,848 @@ function createStyleElement() {
2361
2504
  return style;
2362
2505
  }
2363
2506
 
2364
- // src/core/review/format.ts
2365
- function formatNoteDraftMeta(draft) {
2366
- const parts = [
2367
- `viewport ${formatSize(draft.viewport)}`,
2368
- `point ${formatPoint(draft.marker.viewport)}`
2369
- ];
2370
- if (draft.anchor) {
2371
- parts.push(formatAnchorMeta(draft.anchor));
2372
- }
2373
- return parts.join(" / ");
2374
- }
2375
- function formatItemMeta(item) {
2376
- const parts = [formatDate(item.createdAt)];
2377
- const marker = getItemMarker(item);
2378
- const selection = getItemSelection(item);
2379
- if (item.viewport) {
2380
- parts.push(`viewport ${formatSize(item.viewport)}`);
2381
- }
2382
- if (marker) {
2383
- parts.push(`point ${formatPoint(marker.viewport)}`);
2384
- }
2385
- if (selection) {
2386
- parts.push(`rect ${formatSelection(selection.viewport)}`);
2387
- }
2388
- if (item.anchor) {
2389
- parts.push(formatAnchorMeta(item.anchor));
2390
- }
2391
- return parts.join(" / ");
2507
+ // src/core/draft.attachments.ts
2508
+ function attachDraftImagePasteQueue(textarea, options) {
2509
+ textarea.addEventListener("paste", (event) => {
2510
+ const imageFiles = getClipboardImageFiles(event.clipboardData);
2511
+ if (imageFiles.length === 0) return;
2512
+ event.preventDefault();
2513
+ const text = event.clipboardData?.getData("text/plain");
2514
+ if (text) {
2515
+ insertTextAtTextareaSelection(textarea, text);
2516
+ options.onCommentChange(textarea.value);
2517
+ }
2518
+ const attachments = imageFiles.map(
2519
+ (file, index) => createDraftImageAttachment(file, index)
2520
+ );
2521
+ options.onAttachmentsChange([
2522
+ ...options.getAttachments() ?? [],
2523
+ ...attachments
2524
+ ]);
2525
+ options.onPasteComplete();
2526
+ });
2392
2527
  }
2393
- function formatDate(value) {
2394
- const date = new Date(value);
2395
- if (Number.isNaN(date.getTime())) return value;
2396
- return date.toLocaleString(void 0, {
2397
- month: "2-digit",
2398
- day: "2-digit",
2399
- hour: "2-digit",
2400
- minute: "2-digit"
2528
+ function createDraftAttachmentQueue(ownerDocument, attachments, onRemove) {
2529
+ if (!attachments?.length) return void 0;
2530
+ const queue = ownerDocument.createElement("div");
2531
+ queue.className = "dfwr-attachment-queue";
2532
+ const label = ownerDocument.createElement("div");
2533
+ label.className = "dfwr-attachment-label";
2534
+ label.textContent = `Attachments (${attachments.length})`;
2535
+ const list = ownerDocument.createElement("div");
2536
+ list.className = "dfwr-attachment-list";
2537
+ attachments.forEach((attachment) => {
2538
+ const item = ownerDocument.createElement("div");
2539
+ item.className = "dfwr-attachment-item";
2540
+ const preview = createDraftAttachmentPreview(ownerDocument, attachment);
2541
+ const name = ownerDocument.createElement("div");
2542
+ name.className = "dfwr-attachment-name";
2543
+ name.textContent = attachment.name;
2544
+ name.title = attachment.name;
2545
+ const remove = ownerDocument.createElement("button");
2546
+ remove.className = "dfwr-attachment-remove";
2547
+ remove.type = "button";
2548
+ remove.textContent = "Remove";
2549
+ remove.setAttribute("aria-label", `Remove ${attachment.name}`);
2550
+ remove.addEventListener("click", (event) => {
2551
+ event.preventDefault();
2552
+ event.stopPropagation();
2553
+ onRemove(attachment.id);
2554
+ });
2555
+ item.append(preview, name, remove);
2556
+ list.append(item);
2401
2557
  });
2558
+ queue.append(label, list);
2559
+ return queue;
2402
2560
  }
2403
- function formatSize(size) {
2404
- return `${Math.round(size.width)}x${Math.round(size.height)}`;
2561
+ function removeDraftAttachment(attachments, attachmentId) {
2562
+ if (!attachments?.length) return [];
2563
+ const removed = attachments.find((attachment) => attachment.id === attachmentId);
2564
+ if (removed?.previewUrl) {
2565
+ URL.revokeObjectURL(removed.previewUrl);
2566
+ }
2567
+ return attachments.filter((attachment) => attachment.id !== attachmentId);
2405
2568
  }
2406
- function formatPoint(point) {
2407
- return `${Math.round(point.x)},${Math.round(point.y)}`;
2569
+ function getClipboardImageFiles(data) {
2570
+ if (!data) return [];
2571
+ const itemFiles = Array.from(data.items).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => Boolean(file));
2572
+ if (itemFiles.length > 0) return itemFiles;
2573
+ return Array.from(data.files).filter((file) => file.type.startsWith("image/"));
2408
2574
  }
2409
- function formatSelection(selection) {
2410
- return [
2411
- Math.round(selection.x),
2412
- Math.round(selection.y),
2413
- Math.round(selection.width),
2414
- Math.round(selection.height)
2415
- ].join(",");
2575
+ function createDraftImageAttachment(file, index) {
2576
+ const mime = file.type || "image/png";
2577
+ const name = file.name || `pasted-image-${Date.now()}-${index + 1}${getImageExtension(mime)}`;
2578
+ return {
2579
+ id: createDraftAttachmentId(),
2580
+ file,
2581
+ name,
2582
+ mime,
2583
+ size: file.size,
2584
+ kind: "image",
2585
+ previewUrl: URL.createObjectURL(file),
2586
+ metadata: { source: "paste" }
2587
+ };
2416
2588
  }
2417
- function formatAnchorMeta(anchor) {
2418
- const parts = [`dom ${anchor.strategy}`];
2419
- if (typeof anchor.confidence === "number") {
2420
- parts.push(`${Math.round(anchor.confidence * 100)}%`);
2421
- }
2422
- const candidates = getAnchorCandidates(anchor);
2423
- if (candidates.length > 1) {
2424
- parts.push(`${candidates.length} candidates`);
2425
- }
2426
- return parts.join(" ");
2589
+ function createDraftAttachmentId() {
2590
+ return window.crypto?.randomUUID?.() ?? `draft-attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2591
+ }
2592
+ function getImageExtension(mime) {
2593
+ if (mime === "image/jpeg") return ".jpg";
2594
+ if (mime === "image/gif") return ".gif";
2595
+ if (mime === "image/webp") return ".webp";
2596
+ if (mime === "image/svg+xml") return ".svg";
2597
+ return ".png";
2598
+ }
2599
+ function insertTextAtTextareaSelection(textarea, text) {
2600
+ const start = textarea.selectionStart ?? textarea.value.length;
2601
+ const end = textarea.selectionEnd ?? start;
2602
+ textarea.value = [
2603
+ textarea.value.slice(0, start),
2604
+ text,
2605
+ textarea.value.slice(end)
2606
+ ].join("");
2607
+ const nextSelection = start + text.length;
2608
+ textarea.setSelectionRange(nextSelection, nextSelection);
2609
+ }
2610
+ function createDraftAttachmentPreview(ownerDocument, attachment) {
2611
+ if (attachment.previewUrl && attachment.mime.startsWith("image/")) {
2612
+ const image = ownerDocument.createElement("img");
2613
+ image.className = "dfwr-attachment-thumb";
2614
+ image.src = attachment.previewUrl;
2615
+ image.alt = "";
2616
+ image.decoding = "async";
2617
+ return image;
2618
+ }
2619
+ const fallback = ownerDocument.createElement("div");
2620
+ fallback.className = "dfwr-attachment-thumb is-file";
2621
+ fallback.textContent = "IMG";
2622
+ return fallback;
2427
2623
  }
2428
2624
 
2429
- // src/core/web.review.kit.view.ts
2430
- var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
2431
- var WebReviewKitView = class {
2432
- constructor(config) {
2433
- this.config = config;
2434
- }
2435
- clearDraftPreview() {
2436
- this.restoreDraftPreview();
2437
- this.clearShellComposer();
2438
- }
2439
- render(shadow, hiddenItemsStyle) {
2440
- const state = this.state;
2441
- this.syncDraftPreview(
2442
- state.isOpen && state.mode === "element" ? state.noteDraft : void 0
2443
- );
2444
- shadow.replaceChildren();
2445
- shadow.append(createStyleElement());
2446
- shadow.append(hiddenItemsStyle);
2447
- const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
2448
- const shouldDockComposer = this.config.options.ui?.panel === false && hasDismissableDraft && Boolean(this.getShellComposerHost());
2449
- let dockedComposer;
2450
- const shell = document.createElement("div");
2451
- shell.className = [
2452
- "dfwr-shell",
2453
- state.isOpen ? "is-open" : "",
2454
- hasDismissableDraft && !shouldDockComposer ? "has-dismissible-draft" : ""
2455
- ].filter(Boolean).join(" ");
2456
- shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
2457
- if (this.config.options.ui?.panel !== false) {
2458
- const panel = document.createElement("div");
2459
- panel.className = "dfwr-panel";
2460
- panel.setAttribute("role", "dialog");
2461
- panel.setAttribute("aria-label", "Web review kit");
2462
- panel.append(
2463
- this.createHeader(),
2464
- this.createToolbar(),
2465
- this.createBody(),
2466
- this.createList()
2467
- );
2468
- shell.append(panel);
2469
- }
2470
- shell.append(this.createMarkerLayer());
2471
- if (state.isOpen && hasDismissableDraft && !shouldDockComposer) {
2472
- shell.append(this.createDraftCancelLayer());
2473
- }
2474
- if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
2475
- if (state.noteDraft) {
2476
- const noteDraft = this.createNotePopover(state.noteDraft, {
2477
- dockComposer: shouldDockComposer
2478
- });
2479
- shell.append(noteDraft.layer);
2480
- dockedComposer = noteDraft.composer;
2481
- } else {
2482
- shell.append(
2483
- state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
2484
- );
2485
- }
2486
- }
2487
- if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
2488
- shell.append(this.createAreaLayer());
2489
- }
2490
- if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
2491
- if (state.areaDraft.selection) {
2492
- shell.append(this.createAreaDraftOverlay(state.areaDraft));
2493
- }
2494
- const areaComposer = this.createAreaDraftPopover(state.areaDraft, {
2495
- dockComposer: shouldDockComposer
2496
- });
2497
- if (shouldDockComposer) {
2498
- dockedComposer = areaComposer;
2499
- } else {
2500
- shell.append(areaComposer);
2501
- }
2502
- }
2503
- shadow.append(shell);
2504
- this.renderShellComposer(dockedComposer);
2505
- }
2506
- get state() {
2507
- return this.config.getState();
2508
- }
2509
- getShellComposerHost() {
2510
- const environment = this.config.getEnvironment();
2511
- if (this.config.options.ui?.panel !== false) return void 0;
2512
- return environment?.composerHost ?? void 0;
2513
- }
2514
- renderShellComposer(composer) {
2515
- const host = composer ? this.getShellComposerHost() : void 0;
2516
- if (!host || !composer) {
2517
- this.clearShellComposer();
2518
- return;
2519
- }
2520
- if (this.shellComposerHost && this.shellComposerHost !== host) {
2521
- this.clearShellComposer();
2522
- }
2523
- this.shellComposerHost = host;
2524
- host.dataset.hasDraftComposer = "true";
2525
- if (host.parentElement) {
2526
- host.parentElement.dataset.hasDraftComposer = "true";
2527
- }
2528
- const shell = document.createElement("div");
2529
- shell.className = "dfwr-shell is-open is-shell-draft is-docked-composer";
2530
- shell.append(composer);
2531
- host.replaceChildren(createStyleElement(), shell);
2532
- }
2533
- clearShellComposer() {
2534
- const host = this.shellComposerHost;
2535
- host?.replaceChildren();
2536
- if (host) {
2537
- delete host.dataset.hasDraftComposer;
2538
- delete host.parentElement?.dataset.hasDraftComposer;
2539
- }
2540
- this.shellComposerHost = void 0;
2541
- }
2542
- createDraftCancelLayer() {
2543
- const layer = document.createElement("div");
2544
- layer.className = "dfwr-draft-cancel-layer";
2545
- layer.setAttribute("aria-hidden", "true");
2546
- layer.addEventListener("pointerdown", (event) => {
2547
- if (event.button !== 0) return;
2548
- this.cancelDraft(event);
2549
- });
2550
- return layer;
2551
- }
2552
- cancelDraft(event) {
2553
- event?.preventDefault();
2554
- event?.stopPropagation();
2555
- event?.stopImmediatePropagation();
2556
- this.config.actions.setModeState("idle");
2557
- this.config.actions.clearDrafts();
2558
- this.config.actions.setSelectingArea(false);
2559
- this.config.actions.render();
2560
- }
2561
- // Draft adjustment geometry lives in draft.metrics.ts; these thin wrappers
2562
- // supply the configured viewport presets so call sites stay unchanged.
2563
- get viewportPresets() {
2564
- return this.config.options.viewports?.presets;
2565
- }
2566
- getDraftAdjustmentMetrics(draft) {
2567
- return getDraftAdjustmentMetrics(draft, this.viewportPresets);
2568
- }
2569
- hasDraftAdjustment(draft) {
2570
- return hasDraftAdjustment(draft, this.viewportPresets);
2571
- }
2572
- getAdjustedDraftPoint(point, draft) {
2573
- return getAdjustedDraftPoint(point, draft, this.viewportPresets);
2574
- }
2575
- getAdjustedDraftSelection(selection, draft) {
2576
- return getAdjustedDraftSelection(
2577
- selection,
2578
- draft,
2579
- this.viewportPresets
2580
- );
2581
- }
2582
- getDraftViewportScale(viewport) {
2583
- return getDraftViewportScale(viewport, this.viewportPresets);
2584
- }
2585
- getDraftComposerWidth(environment) {
2586
- const bounds = environment.overlayRect;
2587
- const margin = 12;
2588
- return Math.min(360, Math.max(240, bounds.width - margin * 2));
2589
- }
2590
- getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
2591
- const margin = 12;
2592
- const width = size?.width ?? this.getDraftComposerWidth(environment);
2593
- const height = size?.height ?? 236;
2594
- return {
2595
- x: clamp(
2596
- position.x,
2597
- bounds.left + margin,
2598
- bounds.left + bounds.width - width - margin
2599
- ),
2600
- y: clamp(
2601
- position.y,
2602
- bounds.top + margin,
2603
- bounds.top + bounds.height - height - margin
2604
- )
2605
- };
2606
- }
2607
- getHostComposerBounds() {
2608
- const root = document.documentElement;
2609
- return {
2610
- left: 0,
2611
- top: 0,
2612
- width: root.clientWidth || window.innerWidth,
2613
- height: root.clientHeight || window.innerHeight
2614
- };
2615
- }
2616
- getInitialDraftComposerPosition(selection, environment, size) {
2617
- const bounds = this.getHostComposerBounds();
2618
- const margin = 12;
2619
- const gap = 20;
2620
- if (!selection) {
2621
- return this.getClampedComposerPosition(
2622
- {
2623
- x: environment.overlayRect.left + margin,
2624
- y: environment.overlayRect.top + margin
2625
- },
2626
- environment,
2627
- size,
2628
- bounds
2629
- );
2630
- }
2631
- const preferredX = selection.left + selection.width + gap;
2632
- const maxX = bounds.left + bounds.width - size.width - margin;
2633
- const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
2634
- return this.getClampedComposerPosition(
2625
+ // src/core/view/composer.position.ts
2626
+ var COMPOSER_MARGIN = 12;
2627
+ function getDraftComposerWidth(environment) {
2628
+ const bounds = environment.overlayRect;
2629
+ return Math.min(360, Math.max(240, bounds.width - COMPOSER_MARGIN * 2));
2630
+ }
2631
+ function getHostComposerBounds() {
2632
+ const root = document.documentElement;
2633
+ return {
2634
+ left: 0,
2635
+ top: 0,
2636
+ width: root.clientWidth || window.innerWidth,
2637
+ height: root.clientHeight || window.innerHeight
2638
+ };
2639
+ }
2640
+ function getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
2641
+ const width = size?.width ?? getDraftComposerWidth(environment);
2642
+ const height = size?.height ?? 236;
2643
+ return {
2644
+ x: clamp(
2645
+ position.x,
2646
+ bounds.left + COMPOSER_MARGIN,
2647
+ bounds.left + bounds.width - width - COMPOSER_MARGIN
2648
+ ),
2649
+ y: clamp(
2650
+ position.y,
2651
+ bounds.top + COMPOSER_MARGIN,
2652
+ bounds.top + bounds.height - height - COMPOSER_MARGIN
2653
+ )
2654
+ };
2655
+ }
2656
+ function getInitialDraftComposerPosition(selection, environment, size) {
2657
+ const bounds = getHostComposerBounds();
2658
+ const gap = 20;
2659
+ if (!selection) {
2660
+ return getClampedComposerPosition(
2635
2661
  {
2636
- x,
2637
- y: selection.top
2662
+ x: environment.overlayRect.left + COMPOSER_MARGIN,
2663
+ y: environment.overlayRect.top + COMPOSER_MARGIN
2638
2664
  },
2639
2665
  environment,
2640
2666
  size,
2641
2667
  bounds
2642
2668
  );
2643
2669
  }
2644
- getDraftComposerPosition({
2645
- selection,
2670
+ const preferredX = selection.left + selection.width + gap;
2671
+ const maxX = bounds.left + bounds.width - size.width - COMPOSER_MARGIN;
2672
+ const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
2673
+ return getClampedComposerPosition(
2674
+ {
2675
+ x,
2676
+ y: selection.top
2677
+ },
2646
2678
  environment,
2647
- composerPosition,
2648
- estimatedHeight
2649
- }) {
2650
- const width = this.getDraftComposerWidth(environment);
2651
- if (composerPosition) {
2652
- const clamped = this.getClampedComposerPosition(
2653
- composerPosition,
2654
- environment,
2655
- { width, height: estimatedHeight },
2656
- this.getHostComposerBounds()
2657
- );
2658
- return { width, left: clamped.x, top: clamped.y };
2659
- }
2660
- const position = this.getInitialDraftComposerPosition(selection, environment, {
2661
- width,
2662
- height: estimatedHeight
2663
- });
2664
- return { width, left: position.x, top: position.y };
2665
- }
2666
- getSelectionMqMetrics(selection, viewport) {
2667
- const { scale } = this.getDraftViewportScale(viewport);
2668
- const ratio = scale > 0 ? 1 / scale : 1;
2669
- return {
2670
- x: selection.left * ratio,
2671
- y: selection.top * ratio,
2672
- width: selection.width * ratio,
2673
- height: selection.height * ratio
2674
- };
2675
- }
2676
- formatSignedPx(value) {
2677
- if (value === 0) return "+0px";
2678
- return `${value > 0 ? "+" : ""}${value}px`;
2679
- }
2680
- formatRoundedPx(value) {
2681
- return `${Math.round(value)}px`;
2682
- }
2683
- getAdjustmentLabel() {
2684
- return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
2685
- }
2686
- getSelectionMetricLines(selection, viewport) {
2687
- if (!selection) return ["area", "x none / y none", "w none / h none"];
2688
- const metrics = this.getSelectionMqMetrics(selection, viewport);
2689
- return [
2690
- "area",
2691
- `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
2692
- metrics.y
2693
- )}`,
2694
- `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
2695
- metrics.height
2696
- )}`
2697
- ];
2698
- }
2699
- getAreaDraftMetricSelection(draft) {
2700
- if (!draft.selection) return void 0;
2701
- return toViewportSelection(draft.selection.viewport);
2702
- }
2703
- getDraftAdjustmentMetricLines(draft) {
2704
- const metrics = this.getDraftAdjustmentMetrics(draft);
2705
- return [
2706
- `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
2707
- metrics.y
2708
- )}`,
2709
- `scale ${this.formatSignedPx(metrics.scale)}`
2710
- ];
2711
- }
2712
- withDraftAdjustmentComment(comment, draft) {
2713
- if (!this.hasDraftAdjustment(draft)) return comment;
2714
- const trimmedComment = comment.trim();
2715
- const metrics = this.getDraftAdjustmentMetrics(draft);
2716
- const adjustment = [
2717
- `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
2718
- metrics.x
2719
- )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
2720
- metrics.scale
2721
- )}`,
2722
- `(${metrics.presetLabel} viewport, ${Math.round(
2723
- metrics.viewportWidth
2724
- )}/design ${Math.round(metrics.designWidth)})`
2725
- ].join(" ");
2726
- return trimmedComment ? `${trimmedComment}
2727
- ${adjustment}` : adjustment;
2728
- }
2729
- getAssigneeOption(assigneeId) {
2730
- if (!assigneeId) return void 0;
2731
- return this.config.options.assigneeOptions?.find(
2732
- (option) => option.value === assigneeId
2679
+ size,
2680
+ bounds
2681
+ );
2682
+ }
2683
+ function getDraftComposerPosition({
2684
+ selection,
2685
+ environment,
2686
+ composerPosition,
2687
+ estimatedHeight
2688
+ }) {
2689
+ const width = getDraftComposerWidth(environment);
2690
+ if (composerPosition) {
2691
+ const clamped = getClampedComposerPosition(
2692
+ composerPosition,
2693
+ environment,
2694
+ { width, height: estimatedHeight },
2695
+ getHostComposerBounds()
2733
2696
  );
2697
+ return { width, left: clamped.x, top: clamped.y };
2734
2698
  }
2735
- getAssigneeName(assigneeId) {
2736
- return this.getAssigneeOption(assigneeId)?.label;
2737
- }
2738
- createDraftTitleInput(value, onInput) {
2739
- const input = document.createElement("input");
2740
- input.className = "dfwr-input";
2741
- input.placeholder = "Title";
2742
- input.type = "text";
2743
- input.value = value ?? "";
2744
- input.addEventListener("input", () => onInput(input.value));
2745
- return input;
2746
- }
2747
- isTitleFieldEnabled() {
2748
- return this.config.options.fields?.title === true;
2749
- }
2750
- createDraftAssigneeSelect(value, fallbackLabel, onChange) {
2751
- const assigneeOptions = this.config.options.assigneeOptions ?? [];
2752
- if (assigneeOptions.length === 0) return void 0;
2753
- const assigneeTitle = this.config.options.assigneeTitle?.trim() || "Assignee";
2754
- const select = document.createElement("select");
2755
- select.className = "dfwr-select";
2756
- const emptyOption = document.createElement("option");
2757
- emptyOption.value = "";
2758
- emptyOption.textContent = assigneeTitle;
2759
- select.append(emptyOption);
2760
- const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
2761
- if (hasUnknownAssignee && value) {
2762
- const option = document.createElement("option");
2763
- option.value = value;
2764
- option.textContent = fallbackLabel ?? value;
2765
- select.append(option);
2766
- }
2767
- assigneeOptions.forEach((assigneeOption) => {
2768
- const option = document.createElement("option");
2769
- option.value = assigneeOption.value;
2770
- option.textContent = assigneeOption.label;
2771
- select.append(option);
2772
- });
2773
- select.value = value ?? "";
2774
- select.addEventListener("change", () => {
2775
- onChange(select.value || null, this.getAssigneeName(select.value));
2776
- });
2777
- return select;
2778
- }
2779
- getDraftFields(titleInput, textarea, assigneeSelect) {
2780
- const title = titleInput?.value.trim();
2781
- const comment = textarea.value.trim();
2782
- const assigneeId = assigneeSelect?.value.trim() || void 0;
2699
+ const position = getInitialDraftComposerPosition(selection, environment, {
2700
+ width,
2701
+ height: estimatedHeight
2702
+ });
2703
+ return { width, left: position.x, top: position.y };
2704
+ }
2705
+ function attachDraftComposerDrag({
2706
+ getEnvironment,
2707
+ popover,
2708
+ handle,
2709
+ onMove
2710
+ }) {
2711
+ let isDragging = false;
2712
+ let offsetX = 0;
2713
+ let offsetY = 0;
2714
+ const movePopover = (event) => {
2715
+ const environment = getEnvironment();
2716
+ if (!environment) return;
2717
+ const position = getClampedComposerPosition(
2718
+ {
2719
+ x: event.clientX - offsetX,
2720
+ y: event.clientY - offsetY
2721
+ },
2722
+ environment,
2723
+ {
2724
+ width: popover.offsetWidth,
2725
+ height: popover.offsetHeight
2726
+ },
2727
+ getHostComposerBounds()
2728
+ );
2729
+ popover.style.left = `${position.x}px`;
2730
+ popover.style.top = `${position.y}px`;
2731
+ onMove(position);
2732
+ };
2733
+ handle.addEventListener("pointerdown", (event) => {
2734
+ if (event.button !== 0) return;
2735
+ const rect = popover.getBoundingClientRect();
2736
+ offsetX = event.clientX - rect.left;
2737
+ offsetY = event.clientY - rect.top;
2738
+ isDragging = true;
2739
+ event.preventDefault();
2740
+ event.stopPropagation();
2741
+ handle.setPointerCapture(event.pointerId);
2742
+ popover.classList.add("is-dragging");
2743
+ });
2744
+ handle.addEventListener("pointermove", (event) => {
2745
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
2746
+ event.preventDefault();
2747
+ movePopover(event);
2748
+ });
2749
+ const stopDrag = (event) => {
2750
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
2751
+ event.preventDefault();
2752
+ event.stopPropagation();
2753
+ isDragging = false;
2754
+ handle.releasePointerCapture(event.pointerId);
2755
+ popover.classList.remove("is-dragging");
2756
+ movePopover(event);
2757
+ };
2758
+ handle.addEventListener("pointerup", stopDrag);
2759
+ handle.addEventListener("pointercancel", stopDrag);
2760
+ }
2761
+
2762
+ // src/core/view/icons.ts
2763
+ function createIcon(paths) {
2764
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2765
+ svg.setAttribute("aria-hidden", "true");
2766
+ svg.setAttribute("viewBox", "0 0 24 24");
2767
+ svg.setAttribute("fill", "none");
2768
+ svg.setAttribute("stroke", "currentColor");
2769
+ svg.setAttribute("stroke-width", "2.4");
2770
+ svg.setAttribute("stroke-linecap", "round");
2771
+ svg.setAttribute("stroke-linejoin", "round");
2772
+ paths.forEach((d) => {
2773
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2774
+ path.setAttribute("d", d);
2775
+ svg.append(path);
2776
+ });
2777
+ return svg;
2778
+ }
2779
+ function setAdjustmentToggleIcon(button, isActive) {
2780
+ const paths = isActive ? ["M20 6 9 17l-5-5"] : [
2781
+ "M12 2v20",
2782
+ "M2 12h20",
2783
+ "m9 5 3-3 3 3",
2784
+ "m9 19 3 3 3-3",
2785
+ "m5 9-3 3 3 3",
2786
+ "m19 9 3 3-3 3"
2787
+ ];
2788
+ button.replaceChildren(createIcon(paths));
2789
+ }
2790
+ function createSpinner(className) {
2791
+ const spinner = document.createElement("span");
2792
+ spinner.className = className;
2793
+ spinner.setAttribute("aria-hidden", "true");
2794
+ return spinner;
2795
+ }
2796
+
2797
+ // src/core/view/draft.capture.ts
2798
+ function canCaptureViewport(config) {
2799
+ return Boolean(config.getEnvironment()?.captureViewport);
2800
+ }
2801
+ function getCaptureAreaDraft(draft) {
2802
+ return {
2803
+ viewport: draft.viewport,
2804
+ marker: draft.marker,
2805
+ selection: draft.selection
2806
+ };
2807
+ }
2808
+ function getCaptureDomDraft(config, draft, isElementDraft) {
2809
+ if (!isElementDraft) {
2783
2810
  return {
2784
- title: title || void 0,
2785
- comment,
2786
- assigneeId,
2787
- assigneeName: this.getAssigneeName(assigneeId)
2811
+ viewport: draft.viewport,
2812
+ marker: draft.marker,
2813
+ selection: draft.selection
2788
2814
  };
2789
2815
  }
2790
- getStyleableDraftElement(draft, environment) {
2791
- if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2792
- return draft.previewElement;
2793
- }
2794
- if (!draft.anchor) return void 0;
2795
- const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2796
- const element = resolveAnchorElement(
2797
- draft.anchor,
2798
- environment,
2799
- preferredSelection
2800
- )?.element;
2801
- if (!element) return void 0;
2802
- if ("style" in element) return element;
2803
- return void 0;
2804
- }
2805
- syncDraftPreview(draft) {
2806
- const environment = this.config.getEnvironment();
2807
- if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
2808
- this.restoreDraftPreview();
2809
- return;
2810
- }
2811
- const element = this.getStyleableDraftElement(draft, environment);
2812
- if (!element) {
2813
- this.restoreDraftPreview();
2816
+ const presets = config.options.viewports?.presets;
2817
+ const marker = {
2818
+ ...draft.marker,
2819
+ viewport: roundPoint(
2820
+ getAdjustedDraftPoint(draft.marker.viewport, draft, presets)
2821
+ )
2822
+ };
2823
+ const selection = draft.selection ? {
2824
+ ...draft.selection,
2825
+ viewport: toPublicSelection(
2826
+ getAdjustedDraftSelection(
2827
+ toViewportSelection(draft.selection.viewport),
2828
+ draft,
2829
+ presets
2830
+ )
2831
+ )
2832
+ } : void 0;
2833
+ return {
2834
+ viewport: draft.viewport,
2835
+ marker,
2836
+ selection
2837
+ };
2838
+ }
2839
+ function createDraftCaptureButton(config, draft, options) {
2840
+ const button = document.createElement("button");
2841
+ const state = config.getState();
2842
+ const isCapturing = state.isCapturingViewport;
2843
+ const canCapture = canCaptureViewport(config);
2844
+ button.className = "dfwr-button";
2845
+ button.type = "button";
2846
+ button.disabled = !canCapture || isCapturing || state.isCreatingItem;
2847
+ button.setAttribute("aria-busy", isCapturing ? "true" : "false");
2848
+ button.title = canCapture ? "Capture current viewport" : "Viewport capture helper is not available";
2849
+ if (isCapturing) {
2850
+ button.append(createSpinner("dfwr-spinner"), "Capturing...");
2851
+ } else {
2852
+ button.textContent = "Capture";
2853
+ }
2854
+ button.addEventListener("click", (event) => {
2855
+ event.preventDefault();
2856
+ event.stopPropagation();
2857
+ const currentState = config.getState();
2858
+ if (!canCaptureViewport(config) || currentState.isCapturingViewport) {
2814
2859
  return;
2815
2860
  }
2816
- if (this.draftPreview?.element !== element) {
2817
- this.restoreDraftPreview();
2818
- }
2819
- if (!this.draftPreview) {
2820
- const computedStyle = environment.window.getComputedStyle(element);
2821
- const clone = element.cloneNode(true);
2822
- this.removeDuplicateIds(clone);
2823
- this.copyComputedStyle(element, clone, environment);
2824
- this.positionDraftPreviewClone(clone, element, computedStyle);
2825
- environment.document.body?.appendChild(clone);
2826
- this.draftPreview = {
2827
- element,
2828
- clone,
2829
- visibility: element.style.visibility
2861
+ if (options.kind === "area") {
2862
+ const areaDraft = currentState.areaDraft ?? draft;
2863
+ const nextDraft2 = {
2864
+ ...areaDraft,
2865
+ comment: options.textarea.value
2830
2866
  };
2831
- element.style.visibility = "hidden";
2867
+ config.actions.setAreaDraft(nextDraft2);
2868
+ void config.actions.captureAreaDraft(getCaptureAreaDraft(nextDraft2));
2869
+ return;
2832
2870
  }
2833
- const metrics = this.getDraftAdjustmentMetrics(draft);
2834
- const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
2835
- metrics.cssY
2836
- )}px)`;
2837
- const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
2838
- this.draftPreview.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
2839
- }
2840
- restoreDraftPreview() {
2841
- if (!this.draftPreview) return;
2842
- const { element, clone, visibility } = this.draftPreview;
2843
- clone.remove();
2844
- element.style.visibility = visibility;
2845
- this.draftPreview = void 0;
2846
- }
2847
- positionDraftPreviewClone(clone, element, computedStyle) {
2848
- const rect = element.getBoundingClientRect();
2849
- clone.setAttribute("data-dfwr-adjust-preview", "true");
2850
- clone.setAttribute("aria-hidden", "true");
2851
- clone.style.position = "fixed";
2852
- clone.style.left = `${this.toCssNumber(rect.left)}px`;
2853
- clone.style.top = `${this.toCssNumber(rect.top)}px`;
2854
- clone.style.right = "auto";
2855
- clone.style.bottom = "auto";
2856
- clone.style.width = `${this.toCssNumber(rect.width)}px`;
2857
- clone.style.height = `${this.toCssNumber(rect.height)}px`;
2858
- clone.style.maxWidth = "none";
2859
- clone.style.maxHeight = "none";
2860
- clone.style.margin = "0";
2861
- clone.style.boxSizing = "border-box";
2862
- clone.style.display = this.getDraftPreviewDisplay(computedStyle.display);
2863
- clone.style.zIndex = "2147483646";
2864
- clone.style.pointerEvents = "none";
2865
- clone.style.transition = "none";
2866
- clone.style.willChange = "transform";
2867
- clone.style.transformOrigin = "top left";
2868
- clone.style.transform = "none";
2869
- }
2870
- getDraftPreviewDisplay(display) {
2871
- if (display === "inline" || display === "contents") return "inline-block";
2872
- return display || "block";
2871
+ const domDraft = currentState.domDraft ?? draft;
2872
+ const nextDraft = {
2873
+ ...domDraft,
2874
+ comment: options.textarea.value
2875
+ };
2876
+ config.actions.setDomDraft(nextDraft);
2877
+ void config.actions.captureDomDraft(
2878
+ getCaptureDomDraft(config, nextDraft, options.isElementDraft)
2879
+ );
2880
+ });
2881
+ return button;
2882
+ }
2883
+
2884
+ // src/core/view/draft.text.ts
2885
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
2886
+ function getAdjustmentLabel(options) {
2887
+ return options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
2888
+ }
2889
+ function formatSignedPx(value) {
2890
+ if (value === 0) return "+0px";
2891
+ return `${value > 0 ? "+" : ""}${value}px`;
2892
+ }
2893
+ function formatRoundedPx(value) {
2894
+ return `${Math.round(value)}px`;
2895
+ }
2896
+ function getSelectionMqMetrics(selection, viewport, presets) {
2897
+ const { scale } = getDraftViewportScale(viewport, presets);
2898
+ const ratio = scale > 0 ? 1 / scale : 1;
2899
+ return {
2900
+ x: selection.left * ratio,
2901
+ y: selection.top * ratio,
2902
+ width: selection.width * ratio,
2903
+ height: selection.height * ratio
2904
+ };
2905
+ }
2906
+ function getSelectionMetricLines(selection, viewport, presets) {
2907
+ if (!selection) return ["area", "x none / y none", "w none / h none"];
2908
+ const metrics = getSelectionMqMetrics(selection, viewport, presets);
2909
+ return [
2910
+ "area",
2911
+ `x ${formatRoundedPx(metrics.x)} / y ${formatRoundedPx(metrics.y)}`,
2912
+ `w ${formatRoundedPx(metrics.width)} / h ${formatRoundedPx(
2913
+ metrics.height
2914
+ )}`
2915
+ ];
2916
+ }
2917
+ function getAreaDraftMetricSelection(draft) {
2918
+ if (!draft.selection) return void 0;
2919
+ return toViewportSelection(draft.selection.viewport);
2920
+ }
2921
+ function getDraftAdjustmentMetricLines(draft, presets) {
2922
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
2923
+ return [
2924
+ `x ${formatSignedPx(metrics.x)} / y ${formatSignedPx(metrics.y)}`,
2925
+ `scale ${formatSignedPx(metrics.scale)}`
2926
+ ];
2927
+ }
2928
+ function withDraftAdjustmentComment(comment, draft, options) {
2929
+ const presets = options.viewports?.presets;
2930
+ if (!hasDraftAdjustment(draft, presets)) return comment;
2931
+ const trimmedComment = comment.trim();
2932
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
2933
+ const adjustment = [
2934
+ `${getAdjustmentLabel(options)}: x ${formatSignedPx(
2935
+ metrics.x
2936
+ )}, y ${formatSignedPx(metrics.y)}, scale ${formatSignedPx(
2937
+ metrics.scale
2938
+ )}`,
2939
+ `(${metrics.presetLabel} viewport, ${Math.round(
2940
+ metrics.viewportWidth
2941
+ )}/design ${Math.round(metrics.designWidth)})`
2942
+ ].join(" ");
2943
+ return trimmedComment ? `${trimmedComment}
2944
+ ${adjustment}` : adjustment;
2945
+ }
2946
+
2947
+ // src/core/view/form.widgets.ts
2948
+ function getAssigneeOption(options, assigneeId) {
2949
+ if (!assigneeId) return void 0;
2950
+ return options.assigneeOptions?.find(
2951
+ (option) => option.value === assigneeId
2952
+ );
2953
+ }
2954
+ function getAssigneeName(options, assigneeId) {
2955
+ return getAssigneeOption(options, assigneeId)?.label;
2956
+ }
2957
+ function isTitleFieldEnabled(options) {
2958
+ return options.fields?.title === true;
2959
+ }
2960
+ function createDraftTitleInput(value, onInput) {
2961
+ const input = document.createElement("input");
2962
+ input.className = "dfwr-input";
2963
+ input.placeholder = "Title";
2964
+ input.type = "text";
2965
+ input.value = value ?? "";
2966
+ input.addEventListener("input", () => onInput(input.value));
2967
+ return input;
2968
+ }
2969
+ function createDraftAssigneeSelect(options, value, fallbackLabel, onChange) {
2970
+ const assigneeOptions = options.assigneeOptions ?? [];
2971
+ if (assigneeOptions.length === 0) return void 0;
2972
+ const assigneeTitle = options.assigneeTitle?.trim() || "Assignee";
2973
+ const select = document.createElement("select");
2974
+ select.className = "dfwr-select";
2975
+ const emptyOption = document.createElement("option");
2976
+ emptyOption.value = "";
2977
+ emptyOption.textContent = assigneeTitle;
2978
+ select.append(emptyOption);
2979
+ const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
2980
+ if (hasUnknownAssignee && value) {
2981
+ const option = document.createElement("option");
2982
+ option.value = value;
2983
+ option.textContent = fallbackLabel ?? value;
2984
+ select.append(option);
2985
+ }
2986
+ assigneeOptions.forEach((assigneeOption) => {
2987
+ const option = document.createElement("option");
2988
+ option.value = assigneeOption.value;
2989
+ option.textContent = assigneeOption.label;
2990
+ select.append(option);
2991
+ });
2992
+ select.value = value ?? "";
2993
+ select.addEventListener("change", () => {
2994
+ onChange(select.value || null, getAssigneeName(options, select.value));
2995
+ });
2996
+ return select;
2997
+ }
2998
+ function getDraftFields(options, titleInput, textarea, assigneeSelect) {
2999
+ const title = titleInput?.value.trim();
3000
+ const comment = textarea.value.trim();
3001
+ const assigneeId = assigneeSelect?.value.trim() || void 0;
3002
+ return {
3003
+ title: title || void 0,
3004
+ comment,
3005
+ assigneeId,
3006
+ assigneeName: getAssigneeName(options, assigneeId)
3007
+ };
3008
+ }
3009
+ function createFormActions({
3010
+ saveLabel,
3011
+ onSave,
3012
+ onCancel,
3013
+ isSaving,
3014
+ beforeSave,
3015
+ className,
3016
+ leading
3017
+ }) {
3018
+ const actions = document.createElement("div");
3019
+ actions.className = ["dfwr-actions", className].filter(Boolean).join(" ");
3020
+ const save = document.createElement("button");
3021
+ save.className = "dfwr-button is-primary";
3022
+ save.type = "button";
3023
+ save.disabled = isSaving;
3024
+ save.setAttribute("aria-busy", isSaving ? "true" : "false");
3025
+ if (isSaving) {
3026
+ save.append(createSpinner("dfwr-spinner"), "Saving...");
3027
+ } else {
3028
+ save.textContent = saveLabel;
3029
+ }
3030
+ save.addEventListener("click", (event) => {
3031
+ event.preventDefault();
3032
+ event.stopPropagation();
3033
+ if (save.disabled) return;
3034
+ onSave();
3035
+ });
3036
+ const cancel = document.createElement("button");
3037
+ cancel.className = "dfwr-button";
3038
+ cancel.type = "button";
3039
+ cancel.disabled = isSaving;
3040
+ cancel.textContent = "Cancel";
3041
+ cancel.addEventListener("click", (event) => {
3042
+ onCancel(event);
3043
+ });
3044
+ if (leading?.length) {
3045
+ actions.classList.add("has-leading");
3046
+ const leadingGroup = document.createElement("div");
3047
+ leadingGroup.className = "dfwr-actions-leading";
3048
+ leadingGroup.append(...leading);
3049
+ const primary = document.createElement("div");
3050
+ primary.className = "dfwr-actions-primary";
3051
+ primary.append(save, cancel);
3052
+ actions.append(leadingGroup, primary);
3053
+ return actions;
2873
3054
  }
2874
- copyComputedStyle(element, clone, environment) {
2875
- const computedStyle = environment.window.getComputedStyle(element);
2876
- for (let index = 0; index < computedStyle.length; index += 1) {
2877
- const property = computedStyle.item(index);
2878
- clone.style.setProperty(
2879
- property,
2880
- computedStyle.getPropertyValue(property),
2881
- computedStyle.getPropertyPriority(property)
2882
- );
2883
- }
3055
+ if (beforeSave?.length || className) {
3056
+ actions.append(cancel, ...beforeSave ?? [], save);
3057
+ return actions;
2884
3058
  }
2885
- removeDuplicateIds(element) {
2886
- element.removeAttribute("id");
2887
- element.querySelectorAll("[id]").forEach((child) => {
2888
- child.removeAttribute("id");
2889
- });
3059
+ actions.append(save, cancel);
3060
+ return actions;
3061
+ }
3062
+ function createDraftError(message) {
3063
+ if (!message) return void 0;
3064
+ const error = document.createElement("p");
3065
+ error.className = "dfwr-form-error";
3066
+ error.setAttribute("role", "alert");
3067
+ error.textContent = message;
3068
+ return error;
3069
+ }
3070
+ function createDraftDragHandle(label) {
3071
+ const handle = document.createElement("button");
3072
+ handle.className = "dfwr-draft-drag-handle";
3073
+ handle.type = "button";
3074
+ handle.setAttribute("aria-label", label);
3075
+ return handle;
3076
+ }
3077
+
3078
+ // src/core/review/format.ts
3079
+ function formatDomDraftMeta(draft) {
3080
+ const parts = [
3081
+ `viewport ${formatSize(draft.viewport)}`,
3082
+ `point ${formatPoint(draft.marker.viewport)}`
3083
+ ];
3084
+ if (draft.anchor) {
3085
+ parts.push(formatAnchorMeta(draft.anchor));
2890
3086
  }
2891
- toCssNumber(value) {
2892
- return Math.round(value * 1e3) / 1e3;
3087
+ return parts.join(" / ");
3088
+ }
3089
+ function formatItemMeta(item) {
3090
+ const parts = [formatDate(item.createdAt)];
3091
+ const marker = getItemMarker(item);
3092
+ const selection = getItemSelection(item);
3093
+ if (item.viewport) {
3094
+ parts.push(`viewport ${formatSize(item.viewport)}`);
2893
3095
  }
2894
- createHeader() {
2895
- const header = document.createElement("div");
2896
- header.className = "dfwr-header";
2897
- const title = document.createElement("div");
2898
- title.className = "dfwr-title";
2899
- title.textContent = "Review Kit";
2900
- const meta = document.createElement("div");
2901
- meta.className = "dfwr-meta";
2902
- meta.textContent = getRouteKey(this.config.getEnvironment());
2903
- const titleGroup = document.createElement("div");
2904
- titleGroup.append(title, meta);
2905
- const close = document.createElement("button");
2906
- close.className = "dfwr-icon-button";
2907
- close.type = "button";
2908
- close.textContent = "x";
2909
- close.setAttribute("aria-label", "Close");
2910
- close.addEventListener("click", () => this.config.actions.close());
2911
- header.append(titleGroup, close);
2912
- return header;
3096
+ if (marker) {
3097
+ parts.push(`point ${formatPoint(marker.viewport)}`);
2913
3098
  }
2914
- createToolbar() {
2915
- const toolbar = document.createElement("div");
2916
- toolbar.className = "dfwr-toolbar";
2917
- toolbar.append(
2918
- this.createToolbarButton("Note", this.state.mode === "note", () => {
2919
- const mode = this.state.mode;
2920
- this.config.actions.setModeState(mode === "note" ? "idle" : "note");
2921
- this.config.actions.clearDrafts();
2922
- this.config.actions.render();
2923
- }),
2924
- this.createToolbarButton("Element", this.state.mode === "element", () => {
2925
- const mode = this.state.mode;
2926
- this.config.actions.setModeState(
2927
- mode === "element" ? "idle" : "element"
2928
- );
2929
- this.config.actions.clearDrafts();
2930
- this.config.actions.render();
2931
- }),
2932
- this.createToolbarButton(
2933
- this.state.isSelectingArea ? "Selecting" : "Area",
2934
- this.state.mode === "area",
2935
- () => {
2936
- const mode = this.state.mode;
2937
- this.config.actions.setModeState(mode === "area" ? "idle" : "area");
2938
- this.config.actions.clearDrafts();
2939
- this.config.actions.render();
2940
- }
2941
- ),
2942
- this.createToolbarButton("Refresh", false, () => {
2943
- void this.config.actions.reload();
2944
- })
2945
- );
2946
- return toolbar;
3099
+ if (selection) {
3100
+ parts.push(`rect ${formatSelection(selection.viewport)}`);
2947
3101
  }
2948
- createToolbarButton(label, active, onClick) {
2949
- const button = document.createElement("button");
2950
- button.className = `dfwr-button${active ? " is-active" : ""}`;
2951
- button.type = "button";
2952
- button.textContent = label;
2953
- button.addEventListener("click", onClick);
2954
- return button;
3102
+ if (item.anchor) {
3103
+ parts.push(formatAnchorMeta(item.anchor));
2955
3104
  }
2956
- createBody() {
2957
- const body = document.createElement("div");
2958
- body.className = "dfwr-body";
2959
- const state = this.state;
2960
- if (state.mode === "idle") {
2961
- const empty = document.createElement("p");
2962
- empty.className = "dfwr-empty";
2963
- empty.textContent = "Add a note or mark an area.";
2964
- body.append(empty);
2965
- return body;
2966
- }
2967
- if (state.mode === "note" || state.mode === "element") {
2968
- body.append(this.createNoteBody());
2969
- return body;
2970
- }
2971
- body.append(this.createAreaForm());
2972
- return body;
3105
+ return parts.join(" / ");
3106
+ }
3107
+ function formatDate(value) {
3108
+ const date = new Date(value);
3109
+ if (Number.isNaN(date.getTime())) return value;
3110
+ return date.toLocaleString(void 0, {
3111
+ month: "2-digit",
3112
+ day: "2-digit",
3113
+ hour: "2-digit",
3114
+ minute: "2-digit"
3115
+ });
3116
+ }
3117
+ function formatSize(size) {
3118
+ return `${Math.round(size.width)}x${Math.round(size.height)}`;
3119
+ }
3120
+ function formatPoint(point) {
3121
+ return `${Math.round(point.x)},${Math.round(point.y)}`;
3122
+ }
3123
+ function formatSelection(selection) {
3124
+ return [
3125
+ Math.round(selection.x),
3126
+ Math.round(selection.y),
3127
+ Math.round(selection.width),
3128
+ Math.round(selection.height)
3129
+ ].join(",");
3130
+ }
3131
+ function formatAnchorMeta(anchor) {
3132
+ const parts = [`dom ${anchor.strategy}`];
3133
+ if (typeof anchor.confidence === "number") {
3134
+ parts.push(`${Math.round(anchor.confidence * 100)}%`);
2973
3135
  }
2974
- createNoteBody() {
2975
- const empty = document.createElement("p");
2976
- empty.className = "dfwr-empty";
2977
- empty.textContent = this.state.noteDraft ? "Write the note in the page box." : this.state.mode === "element" ? "Click an element to add QA." : "Click on the page to place a note.";
2978
- return empty;
3136
+ const candidates = getAnchorCandidates(anchor);
3137
+ if (candidates.length > 1) {
3138
+ parts.push(`${candidates.length} candidates`);
2979
3139
  }
2980
- // Builds the note draft layer: the on-page marker/highlight plus its composer
2981
- // popover. When dockComposer is set the composer renders into the side panel
2982
- // instead of floating next to the marker (used for the docked review mode).
2983
- createNotePopover(draft, options = {}) {
2984
- const environment = this.config.getEnvironment();
2985
- const group = document.createElement("div");
2986
- group.className = "dfwr-note-draft";
2987
- if (!environment) return { layer: group, composer: void 0 };
2988
- const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
2989
- const hostPoint = toHostPoint(
2990
- isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
2991
- environment
2992
- );
2993
- let selectionHighlight;
2994
- if (draft.selection) {
2995
- const selection = toViewportSelection(draft.selection.viewport);
2996
- selectionHighlight = this.createSelectionHighlight(
2997
- isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
2998
- environment,
2999
- true
3000
- );
3001
- group.append(selectionHighlight);
3002
- }
3003
- const pin = document.createElement("button");
3004
- pin.className = "dfwr-note-pin";
3005
- pin.type = "button";
3006
- pin.setAttribute("aria-label", "Move note point");
3007
- pin.style.left = `${hostPoint.x}px`;
3008
- pin.style.top = `${hostPoint.y}px`;
3009
- const popover = document.createElement("div");
3010
- const position = getPopoverPosition(hostPoint, environment);
3011
- popover.className = [
3012
- "dfwr-note-popover",
3013
- isElementDraft ? "is-composer" : "",
3014
- options.dockComposer ? "is-docked-composer" : ""
3015
- ].filter(Boolean).join(" ");
3016
- if (options.dockComposer) {
3017
- popover.style.width = "100%";
3018
- } else if (isElementDraft) {
3019
- const selection = draft.selection ? toHostSelection(
3020
- this.getAdjustedDraftSelection(
3021
- toViewportSelection(draft.selection.viewport),
3022
- draft
3023
- ),
3024
- environment
3025
- ) : void 0;
3026
- const composer = this.getDraftComposerPosition({
3027
- selection,
3028
- environment,
3029
- composerPosition: draft.composerPosition,
3030
- estimatedHeight: 252
3031
- });
3032
- popover.style.left = `${composer.left}px`;
3033
- popover.style.top = `${composer.top}px`;
3034
- popover.style.width = `${composer.width}px`;
3035
- } else {
3036
- popover.style.left = `${position.left}px`;
3037
- popover.style.top = `${position.top}px`;
3140
+ return parts.join(" ");
3141
+ }
3142
+
3143
+ // src/core/view/markers.ts
3144
+ function createMarkerElement(itemId, hostPoint, label, scope, isBound, isHighlighted) {
3145
+ const marker = document.createElement("div");
3146
+ marker.className = [
3147
+ "dfwr-bound-marker",
3148
+ `is-scope-${scope}`,
3149
+ isBound ? "is-bound" : "is-fallback",
3150
+ isHighlighted ? "is-highlighted" : ""
3151
+ ].filter(Boolean).join(" ");
3152
+ marker.style.left = `${hostPoint.x}px`;
3153
+ marker.style.top = `${hostPoint.y}px`;
3154
+ marker.dataset.scope = scope;
3155
+ if (itemId) {
3156
+ marker.dataset.reviewItemId = itemId;
3157
+ }
3158
+ const iconElement = document.createElement("span");
3159
+ iconElement.className = "dfwr-bound-marker-icon";
3160
+ iconElement.setAttribute("aria-hidden", "true");
3161
+ const labelElement = document.createElement("span");
3162
+ labelElement.className = "dfwr-bound-marker-number";
3163
+ labelElement.textContent = label;
3164
+ marker.append(iconElement, labelElement);
3165
+ return marker;
3166
+ }
3167
+ function createSelectionHighlight(selection, environment, isDraft) {
3168
+ const rect = toHostSelection(selection, environment);
3169
+ const highlight = document.createElement("div");
3170
+ highlight.className = `dfwr-selection-highlight${isDraft ? " is-draft" : ""}`;
3171
+ highlight.style.left = `${rect.left}px`;
3172
+ highlight.style.top = `${rect.top}px`;
3173
+ highlight.style.width = `${rect.width}px`;
3174
+ highlight.style.height = `${rect.height}px`;
3175
+ return highlight;
3176
+ }
3177
+ function createItemHighlightElements(selection, environment, item, label, scope, isBound, isHighlighted) {
3178
+ const rect = toHostSelection(selection, environment);
3179
+ const mode = getReviewItemHighlightMode(item);
3180
+ const highlight = document.createElement("div");
3181
+ highlight.className = [
3182
+ "dfwr-item-target-highlight",
3183
+ `is-mode-${mode}`,
3184
+ `is-scope-${scope}`,
3185
+ isBound ? "is-bound" : "is-fallback",
3186
+ isHighlighted ? "is-highlighted" : ""
3187
+ ].filter(Boolean).join(" ");
3188
+ highlight.style.left = `${rect.left}px`;
3189
+ highlight.style.top = `${rect.top}px`;
3190
+ highlight.style.width = `${rect.width}px`;
3191
+ highlight.style.height = `${rect.height}px`;
3192
+ highlight.dataset.reviewItemId = item.id;
3193
+ const labelElement = document.createElement("div");
3194
+ labelElement.className = [
3195
+ "dfwr-item-target-label",
3196
+ `is-mode-${mode}`,
3197
+ `is-scope-${scope}`,
3198
+ isHighlighted ? "is-highlighted" : ""
3199
+ ].filter(Boolean).join(" ");
3200
+ labelElement.textContent = label;
3201
+ labelElement.style.left = `${Math.max(4, rect.left)}px`;
3202
+ labelElement.style.top = `${Math.max(4, rect.top - 24)}px`;
3203
+ labelElement.dataset.reviewItemId = item.id;
3204
+ return [highlight, labelElement];
3205
+ }
3206
+ function createMarkerLayer({
3207
+ items,
3208
+ highlightedItemId,
3209
+ environment,
3210
+ presets,
3211
+ showCompactMarkers = true
3212
+ }) {
3213
+ const layer = document.createElement("div");
3214
+ layer.className = "dfwr-marker-layer";
3215
+ if (!environment) return layer;
3216
+ const currentScope = getReviewViewportScope(
3217
+ getViewportSize(environment),
3218
+ presets
3219
+ );
3220
+ getNumberedReviewItems(items, presets).forEach((numberedItem) => {
3221
+ const { item, scope, displayLabel } = numberedItem;
3222
+ if (!shouldShowMarkerForScope(scope, currentScope)) {
3223
+ return;
3038
3224
  }
3039
- const form = document.createElement("form");
3040
- form.className = "dfwr-form";
3041
- const meta = isElementDraft ? void 0 : document.createElement("div");
3042
- if (meta) {
3043
- meta.className = "dfwr-item-date";
3044
- meta.textContent = formatNoteDraftMeta(draft);
3045
- }
3046
- const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(draft.title, (title) => {
3047
- const noteDraft = this.state.noteDraft;
3048
- if (!noteDraft) return;
3049
- this.config.actions.setNoteDraft({
3050
- ...noteDraft,
3051
- title
3052
- });
3053
- }) : void 0;
3054
- const textarea = document.createElement("textarea");
3055
- textarea.className = "dfwr-textarea";
3056
- textarea.placeholder = "Review comment";
3057
- textarea.rows = 4;
3058
- textarea.value = draft.comment ?? "";
3059
- textarea.addEventListener("input", () => {
3060
- const noteDraft = this.state.noteDraft;
3061
- if (!noteDraft) return;
3062
- this.config.actions.setNoteDraft({
3063
- ...noteDraft,
3064
- comment: textarea.value
3065
- });
3066
- });
3067
- const assigneeSelect = this.createDraftAssigneeSelect(
3068
- draft.assigneeId,
3069
- draft.assigneeName,
3070
- (assigneeId, assigneeName) => {
3071
- const noteDraft = this.state.noteDraft;
3072
- if (!noteDraft) return;
3073
- this.config.actions.setNoteDraft({
3074
- ...noteDraft,
3075
- assigneeId,
3076
- assigneeName
3077
- });
3225
+ const isHighlighted = item.id === highlightedItemId;
3226
+ if (isHighlighted) {
3227
+ const selection = getItemHighlightSelection(item, environment);
3228
+ if (selection) {
3229
+ layer.append(
3230
+ ...createItemHighlightElements(
3231
+ selection.viewport,
3232
+ environment,
3233
+ item,
3234
+ displayLabel,
3235
+ scope,
3236
+ selection.isBound,
3237
+ isHighlighted
3238
+ )
3239
+ );
3240
+ return;
3078
3241
  }
3079
- );
3080
- const saveDraft = () => {
3081
- const currentDraft = this.state.noteDraft ?? draft;
3082
- const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3083
- const comment = fields.comment;
3084
- if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
3085
- void this.config.actions.createItem({
3086
- kind: "note",
3087
- title: fields.title,
3088
- comment: this.withDraftAdjustmentComment(comment, currentDraft),
3089
- assigneeId: fields.assigneeId,
3090
- assigneeName: fields.assigneeName,
3091
- viewport: currentDraft.viewport,
3092
- anchor: currentDraft.anchor,
3093
- marker: currentDraft.marker,
3094
- selection: currentDraft.selection
3095
- });
3096
- };
3097
- const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
3098
- draft,
3099
- pin,
3100
- popover,
3101
- selectionHighlight,
3102
- textarea,
3103
- dockToggle: options.dockComposer
3104
- }) : void 0;
3105
- const actions = this.createFormActions("Save note", saveDraft, {
3106
- leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
3107
- });
3108
- const error = this.createDraftError();
3109
- form.append(
3110
- ...meta ? [meta] : [],
3111
- ...adjustmentControls ? [adjustmentControls.panel] : [],
3112
- ...titleInput ? [titleInput] : [],
3113
- textarea,
3114
- ...assigneeSelect ? [assigneeSelect] : [],
3115
- ...error ? [error] : [],
3116
- actions
3117
- );
3118
- const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
3119
- popover.append(
3120
- ...dragHandle ? [dragHandle] : [],
3121
- form
3122
- );
3123
- group.append(pin);
3124
- if (!options.dockComposer) {
3125
- group.append(popover);
3126
- }
3127
- if (dragHandle) {
3128
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3129
- const noteDraft = this.state.noteDraft ?? draft;
3130
- this.config.actions.setNoteDraft({
3131
- ...noteDraft,
3132
- composerPosition,
3133
- comment: textarea.value
3134
- });
3135
- });
3136
3242
  }
3137
- this.attachDraftPinDrag(
3138
- pin,
3139
- isElementDraft || options.dockComposer ? void 0 : popover,
3140
- meta,
3141
- textarea
3142
- );
3143
- if (!options.dockComposer) {
3144
- window.setTimeout(() => {
3145
- if (draft.adjustment?.isActive) {
3146
- adjustmentControls?.focusTarget.focus();
3147
- return;
3148
- }
3149
- textarea.focus();
3150
- }, 0);
3151
- }
3152
- return {
3153
- layer: group,
3154
- composer: options.dockComposer ? popover : void 0
3155
- };
3156
- }
3157
- createDraftDragHandle(label) {
3158
- const handle = document.createElement("button");
3159
- handle.className = "dfwr-draft-drag-handle";
3160
- handle.type = "button";
3161
- handle.setAttribute("aria-label", label);
3162
- return handle;
3163
- }
3164
- createIcon(paths) {
3165
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3166
- svg.setAttribute("aria-hidden", "true");
3167
- svg.setAttribute("viewBox", "0 0 24 24");
3168
- svg.setAttribute("fill", "none");
3169
- svg.setAttribute("stroke", "currentColor");
3170
- svg.setAttribute("stroke-width", "2.4");
3171
- svg.setAttribute("stroke-linecap", "round");
3172
- svg.setAttribute("stroke-linejoin", "round");
3173
- paths.forEach((d) => {
3174
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3175
- path.setAttribute("d", d);
3176
- svg.append(path);
3177
- });
3178
- return svg;
3179
- }
3180
- setAdjustmentToggleIcon(button, isActive) {
3181
- const paths = isActive ? ["M20 6 9 17l-5-5"] : [
3182
- "M12 2v20",
3183
- "M2 12h20",
3184
- "m9 5 3-3 3 3",
3185
- "m9 19 3 3 3-3",
3186
- "m5 9-3 3 3 3",
3187
- "m19 9 3 3-3 3"
3188
- ];
3189
- button.replaceChildren(this.createIcon(paths));
3190
- }
3191
- attachDraftComposerDrag(popover, handle, onMove) {
3192
- let isDragging = false;
3193
- let offsetX = 0;
3194
- let offsetY = 0;
3195
- const movePopover = (event) => {
3196
- const environment = this.config.getEnvironment();
3197
- if (!environment) return;
3198
- const position = this.getClampedComposerPosition(
3199
- {
3200
- x: event.clientX - offsetX,
3201
- y: event.clientY - offsetY
3202
- },
3203
- environment,
3204
- {
3205
- width: popover.offsetWidth,
3206
- height: popover.offsetHeight
3207
- },
3208
- this.getHostComposerBounds()
3209
- );
3210
- popover.style.left = `${position.x}px`;
3211
- popover.style.top = `${position.y}px`;
3212
- onMove(position);
3213
- };
3214
- handle.addEventListener("pointerdown", (event) => {
3215
- if (event.button !== 0) return;
3216
- const rect = popover.getBoundingClientRect();
3217
- offsetX = event.clientX - rect.left;
3218
- offsetY = event.clientY - rect.top;
3219
- isDragging = true;
3220
- event.preventDefault();
3221
- event.stopPropagation();
3222
- handle.setPointerCapture(event.pointerId);
3223
- popover.classList.add("is-dragging");
3224
- });
3225
- handle.addEventListener("pointermove", (event) => {
3226
- if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3227
- event.preventDefault();
3228
- movePopover(event);
3229
- });
3230
- const stopDrag = (event) => {
3231
- if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3232
- event.preventDefault();
3233
- event.stopPropagation();
3234
- isDragging = false;
3235
- handle.releasePointerCapture(event.pointerId);
3236
- popover.classList.remove("is-dragging");
3237
- movePopover(event);
3238
- };
3239
- handle.addEventListener("pointerup", stopDrag);
3240
- handle.addEventListener("pointercancel", stopDrag);
3241
- }
3242
- // Builds the element-adjustment controls (nudge the previewed element via
3243
- // arrow keys / buttons). Wires keyboard deltas to the draft transform and
3244
- // keeps the pin, popover, highlight and textarea in sync as the value changes.
3245
- createAdjustmentControls({
3246
- draft,
3247
- pin,
3248
- popover,
3249
- selectionHighlight,
3250
- textarea,
3251
- dockToggle
3252
- }) {
3253
- const panel = document.createElement("div");
3254
- panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
3255
- const header = document.createElement("div");
3256
- header.className = "dfwr-adjust-panel-header";
3257
- const help = document.createElement("div");
3258
- help.className = "dfwr-adjust-help";
3259
- help.textContent = this.getAdjustmentLabel();
3260
- const adjust = document.createElement("button");
3261
- adjust.className = "dfwr-adjust-toggle";
3262
- adjust.type = "button";
3263
- adjust.title = "Adjust DOM element with keyboard arrows";
3264
- adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
3265
- const xyStatus = document.createElement("div");
3266
- xyStatus.className = "dfwr-adjust-status";
3267
- const scaleStatus = document.createElement("div");
3268
- scaleStatus.className = "dfwr-adjust-status";
3269
- const syncControls = (nextDraft) => {
3270
- const isActive = nextDraft.adjustment?.isActive === true;
3271
- panel.classList.toggle("is-active", isActive);
3272
- adjust.classList.toggle("is-active", isActive);
3273
- adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
3274
- this.setAdjustmentToggleIcon(adjust, isActive);
3275
- adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
3276
- adjust.setAttribute(
3277
- "aria-label",
3278
- isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
3279
- );
3280
- const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
3281
- xyStatus.textContent = xyLine;
3282
- scaleStatus.textContent = scaleLine;
3283
- this.syncDraftAdjustmentUi({
3284
- draft: nextDraft,
3285
- pin,
3286
- selectionHighlight
3287
- });
3288
- };
3289
- const updateDraft = (updater) => {
3290
- const currentDraft = this.state.noteDraft ?? draft;
3291
- const nextDraft = updater(currentDraft);
3292
- this.config.actions.setNoteDraft({
3293
- ...nextDraft,
3294
- comment: textarea.value
3295
- });
3296
- syncControls(nextDraft);
3297
- };
3298
- adjust.addEventListener("click", () => {
3299
- updateDraft((currentDraft) => ({
3300
- ...currentDraft,
3301
- adjustment: {
3302
- x: currentDraft.adjustment?.x ?? 0,
3303
- y: currentDraft.adjustment?.y ?? 0,
3304
- scale: currentDraft.adjustment?.scale ?? 0,
3305
- isActive: currentDraft.adjustment?.isActive !== true
3306
- }
3307
- }));
3308
- adjust.focus();
3309
- });
3310
- popover.addEventListener("keydown", (event) => {
3311
- const currentDraft = this.state.noteDraft ?? draft;
3312
- if (currentDraft.adjustment?.isActive !== true) return;
3313
- const keyDelta = this.getAdjustmentKeyDelta(event);
3314
- if (!keyDelta) return;
3315
- event.preventDefault();
3316
- event.stopPropagation();
3317
- updateDraft((activeDraft) => ({
3318
- ...activeDraft,
3319
- adjustment: {
3320
- x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
3321
- y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
3322
- scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
3323
- isActive: true
3324
- }
3325
- }));
3326
- });
3327
- header.append(help);
3328
- if (!dockToggle) {
3329
- header.append(adjust);
3243
+ if (!showCompactMarkers && !isHighlighted) {
3244
+ return;
3330
3245
  }
3331
- panel.append(header, xyStatus, scaleStatus);
3332
- syncControls(draft);
3333
- return {
3334
- panel,
3335
- focusTarget: adjust,
3336
- actionButton: dockToggle ? adjust : void 0
3337
- };
3338
- }
3339
- getAdjustmentKeyDelta(event) {
3340
- const step = event.shiftKey ? 10 : 1;
3341
- if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
3342
- if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
3343
- if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
3344
- if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
3345
- if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
3346
- if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
3347
- return void 0;
3348
- }
3349
- syncDraftAdjustmentUi({
3350
- draft,
3351
- pin,
3352
- selectionHighlight
3353
- }) {
3354
- const environment = this.config.getEnvironment();
3355
- if (!environment) return;
3356
- const hostPoint = toHostPoint(
3357
- this.getAdjustedDraftPoint(draft.marker.viewport, draft),
3358
- environment
3359
- );
3360
- pin.style.left = `${hostPoint.x}px`;
3361
- pin.style.top = `${hostPoint.y}px`;
3362
- if (draft.selection && selectionHighlight) {
3363
- const rect = toHostSelection(
3364
- this.getAdjustedDraftSelection(
3365
- toViewportSelection(draft.selection.viewport),
3366
- draft
3367
- ),
3368
- environment
3369
- );
3370
- selectionHighlight.style.left = `${rect.left}px`;
3371
- selectionHighlight.style.top = `${rect.top}px`;
3372
- selectionHighlight.style.width = `${rect.width}px`;
3373
- selectionHighlight.style.height = `${rect.height}px`;
3246
+ const point = getBoundMarkerPoint(item, environment);
3247
+ if (!point || !isPointInViewport(point.viewport, environment)) {
3248
+ return;
3374
3249
  }
3375
- this.syncDraftPreview(draft);
3250
+ const hostPoint = toHostPoint(point.viewport, environment);
3251
+ const marker = createMarkerElement(
3252
+ item.id,
3253
+ hostPoint,
3254
+ displayLabel,
3255
+ scope,
3256
+ point.isBound,
3257
+ isHighlighted
3258
+ );
3259
+ marker.title = `${displayLabel} / ${item.comment}
3260
+ ${formatItemMeta(item)}`;
3261
+ layer.append(marker);
3262
+ });
3263
+ return layer;
3264
+ }
3265
+
3266
+ // src/core/view/area.draft.ts
3267
+ function createAreaForm(context) {
3268
+ const { config } = context;
3269
+ const form = document.createElement("form");
3270
+ form.className = "dfwr-form";
3271
+ const areaDraft = config.getState().areaDraft;
3272
+ if (!areaDraft) {
3273
+ const empty = document.createElement("p");
3274
+ empty.className = "dfwr-empty";
3275
+ empty.textContent = "Drag on the page to select an area.";
3276
+ form.append(empty);
3277
+ return form;
3376
3278
  }
3377
- createAreaForm() {
3378
- const form = document.createElement("form");
3379
- form.className = "dfwr-form";
3380
- const areaDraft = this.state.areaDraft;
3381
- if (!areaDraft) {
3382
- const empty = document.createElement("p");
3383
- empty.className = "dfwr-empty";
3384
- empty.textContent = "Drag on the page to select an area.";
3385
- form.append(empty);
3386
- return form;
3387
- }
3388
- form.append(this.createAreaMetricsPanel(areaDraft));
3389
- const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(areaDraft.title, (title) => {
3390
- const draft = this.state.areaDraft;
3391
- if (!draft) return;
3392
- this.config.actions.setAreaDraft({
3279
+ form.append(createAreaMetricsPanel(context, areaDraft));
3280
+ const titleInput = isTitleFieldEnabled(config.options) ? createDraftTitleInput(areaDraft.title, (title) => {
3281
+ const draft = config.getState().areaDraft;
3282
+ if (!draft) return;
3283
+ config.actions.setAreaDraft({
3284
+ ...draft,
3285
+ title
3286
+ });
3287
+ }) : void 0;
3288
+ const textarea = document.createElement("textarea");
3289
+ textarea.className = "dfwr-textarea";
3290
+ textarea.placeholder = "Area comment";
3291
+ textarea.rows = 4;
3292
+ textarea.value = areaDraft.comment ?? "";
3293
+ textarea.addEventListener("input", () => {
3294
+ const draft = config.getState().areaDraft;
3295
+ if (!draft) return;
3296
+ config.actions.setAreaDraft({
3297
+ ...draft,
3298
+ comment: textarea.value
3299
+ });
3300
+ });
3301
+ attachDraftImagePasteQueue(textarea, {
3302
+ getAttachments: () => config.getState().areaDraft?.attachments ?? areaDraft.attachments,
3303
+ onAttachmentsChange: (attachments) => {
3304
+ const draft = config.getState().areaDraft ?? areaDraft;
3305
+ config.actions.setAreaDraft({
3306
+ ...draft,
3307
+ comment: textarea.value,
3308
+ attachments
3309
+ });
3310
+ },
3311
+ onCommentChange: (comment) => {
3312
+ const draft = config.getState().areaDraft ?? areaDraft;
3313
+ config.actions.setAreaDraft({
3393
3314
  ...draft,
3394
- title
3315
+ comment
3395
3316
  });
3396
- }) : void 0;
3397
- const textarea = document.createElement("textarea");
3398
- textarea.className = "dfwr-textarea";
3399
- textarea.placeholder = "Area comment";
3400
- textarea.rows = 4;
3401
- textarea.value = areaDraft.comment ?? "";
3402
- textarea.addEventListener("input", () => {
3403
- const draft = this.state.areaDraft;
3317
+ },
3318
+ onPasteComplete: () => config.actions.render()
3319
+ });
3320
+ const assigneeSelect = createDraftAssigneeSelect(
3321
+ config.options,
3322
+ areaDraft.assigneeId,
3323
+ areaDraft.assigneeName,
3324
+ (assigneeId, assigneeName) => {
3325
+ const draft = config.getState().areaDraft;
3404
3326
  if (!draft) return;
3405
- this.config.actions.setAreaDraft({
3327
+ config.actions.setAreaDraft({
3406
3328
  ...draft,
3407
- comment: textarea.value
3329
+ assigneeId,
3330
+ assigneeName
3408
3331
  });
3409
- });
3410
- const assigneeSelect = this.createDraftAssigneeSelect(
3411
- areaDraft.assigneeId,
3412
- areaDraft.assigneeName,
3413
- (assigneeId, assigneeName) => {
3414
- const draft = this.state.areaDraft;
3415
- if (!draft) return;
3416
- this.config.actions.setAreaDraft({
3417
- ...draft,
3418
- assigneeId,
3419
- assigneeName
3420
- });
3421
- }
3422
- );
3423
- const actions = this.createFormActions("Save area", () => {
3424
- const draft = this.state.areaDraft;
3425
- const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
3332
+ }
3333
+ );
3334
+ const actions = createFormActions({
3335
+ saveLabel: "Save area",
3336
+ isSaving: config.getState().isCreatingItem,
3337
+ onCancel: context.cancelDraft,
3338
+ onSave: () => {
3339
+ const draft = config.getState().areaDraft;
3340
+ const fields = getDraftFields(
3341
+ config.options,
3342
+ titleInput,
3343
+ textarea,
3344
+ assigneeSelect
3345
+ );
3426
3346
  const comment = fields.comment;
3427
- if (!comment || !draft) return;
3428
- void this.config.actions.createItem({
3347
+ if (!comment && !draft?.attachments?.length || !draft) return;
3348
+ void config.actions.createItem({
3429
3349
  kind: "area",
3430
3350
  title: fields.title,
3431
3351
  comment,
@@ -3434,603 +3354,1077 @@ ${adjustment}` : adjustment;
3434
3354
  viewport: draft.viewport,
3435
3355
  anchor: draft.anchor,
3436
3356
  marker: draft.marker,
3437
- selection: draft.selection
3357
+ selection: draft.selection,
3358
+ attachments: draft.attachments
3438
3359
  });
3439
- });
3440
- const error = this.createDraftError();
3441
- form.append(
3442
- ...titleInput ? [titleInput] : [],
3443
- textarea,
3444
- ...assigneeSelect ? [assigneeSelect] : [],
3445
- ...error ? [error] : [],
3446
- actions
3447
- );
3448
- return form;
3449
- }
3450
- createAreaMetricsPanel(draft) {
3451
- const panel = document.createElement("div");
3452
- panel.className = "dfwr-adjust-panel is-area-metrics-panel";
3453
- const help = document.createElement("div");
3454
- help.className = "dfwr-adjust-help";
3455
- const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
3456
- this.getAreaDraftMetricSelection(draft),
3457
- draft.viewport
3458
- );
3459
- help.textContent = labelLine;
3460
- const xyStatus = document.createElement("div");
3461
- xyStatus.className = "dfwr-adjust-status";
3462
- xyStatus.textContent = xyLine;
3463
- const sizeStatus = document.createElement("div");
3464
- sizeStatus.className = "dfwr-adjust-status";
3465
- sizeStatus.textContent = sizeLine;
3466
- panel.append(help, xyStatus, sizeStatus);
3467
- return panel;
3468
- }
3469
- createAreaDraftOverlay(draft) {
3470
- const layer = document.createElement("div");
3471
- layer.className = "dfwr-area-preview-layer";
3472
- const environment = this.config.getEnvironment();
3473
- if (!environment || !draft.selection) return layer;
3474
- const selection = toViewportSelection(draft.selection.viewport);
3475
- layer.append(this.createSelectionHighlight(selection, environment, true));
3476
- if (draft.marker) {
3477
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
3478
- layer.append(
3479
- this.createMarkerElement(
3480
- void 0,
3481
- hostPoint,
3482
- "\u2022",
3483
- getReviewViewportScope(
3484
- draft.viewport,
3485
- this.config.options.viewports?.presets
3486
- ),
3487
- true,
3488
- true
3489
- )
3360
+ },
3361
+ leading: [
3362
+ createDraftCaptureButton(config, areaDraft, {
3363
+ kind: "area",
3364
+ textarea
3365
+ })
3366
+ ]
3367
+ });
3368
+ const error = createDraftError(config.getState().draftError);
3369
+ const attachmentQueue = createDraftAttachmentQueue(
3370
+ document,
3371
+ areaDraft.attachments,
3372
+ (attachmentId) => {
3373
+ const draft = config.getState().areaDraft ?? areaDraft;
3374
+ const attachments = removeDraftAttachment(
3375
+ draft.attachments,
3376
+ attachmentId
3490
3377
  );
3378
+ config.actions.setAreaDraft({
3379
+ ...draft,
3380
+ comment: textarea.value,
3381
+ attachments: attachments.length > 0 ? attachments : void 0
3382
+ });
3383
+ config.actions.render();
3491
3384
  }
3492
- return layer;
3385
+ );
3386
+ form.append(
3387
+ ...titleInput ? [titleInput] : [],
3388
+ textarea,
3389
+ ...attachmentQueue ? [attachmentQueue] : [],
3390
+ ...assigneeSelect ? [assigneeSelect] : [],
3391
+ ...error ? [error] : [],
3392
+ actions
3393
+ );
3394
+ return form;
3395
+ }
3396
+ function createAreaMetricsPanel(context, draft) {
3397
+ const panel = document.createElement("div");
3398
+ panel.className = "dfwr-adjust-panel is-area-metrics-panel";
3399
+ const [labelLine, xyLine, sizeLine] = getSelectionMetricLines(
3400
+ getAreaDraftMetricSelection(draft),
3401
+ draft.viewport,
3402
+ context.config.options.viewports?.presets
3403
+ );
3404
+ const help = document.createElement("div");
3405
+ help.className = "dfwr-adjust-help";
3406
+ help.textContent = labelLine;
3407
+ const xyStatus = document.createElement("div");
3408
+ xyStatus.className = "dfwr-adjust-status";
3409
+ xyStatus.textContent = xyLine;
3410
+ const sizeStatus = document.createElement("div");
3411
+ sizeStatus.className = "dfwr-adjust-status";
3412
+ sizeStatus.textContent = sizeLine;
3413
+ panel.append(help, xyStatus, sizeStatus);
3414
+ return panel;
3415
+ }
3416
+ function createAreaDraftOverlay(context, draft) {
3417
+ const { config } = context;
3418
+ const layer = document.createElement("div");
3419
+ layer.className = "dfwr-area-preview-layer";
3420
+ const environment = config.getEnvironment();
3421
+ if (!environment || !draft.selection) return layer;
3422
+ const selection = toViewportSelection(draft.selection.viewport);
3423
+ layer.append(createSelectionHighlight(selection, environment, true));
3424
+ if (draft.marker) {
3425
+ const hostPoint = toHostPoint(draft.marker.viewport, environment);
3426
+ layer.append(
3427
+ createMarkerElement(
3428
+ void 0,
3429
+ hostPoint,
3430
+ "\u2022",
3431
+ getReviewViewportScope(
3432
+ draft.viewport,
3433
+ config.options.viewports?.presets
3434
+ ),
3435
+ true,
3436
+ true
3437
+ )
3438
+ );
3493
3439
  }
3494
- createAreaDraftPopover(draft, options = {}) {
3495
- const environment = this.config.getEnvironment();
3496
- const popover = document.createElement("div");
3497
- popover.className = [
3498
- "dfwr-area-draft",
3499
- "is-composer",
3500
- options.dockComposer ? "is-docked-composer" : ""
3501
- ].filter(Boolean).join(" ");
3502
- if (options.dockComposer) {
3503
- popover.style.width = "100%";
3504
- } else if (environment && draft.selection) {
3505
- const selection = toHostSelection(
3506
- toViewportSelection(draft.selection.viewport),
3507
- environment
3508
- );
3509
- const composer = this.getDraftComposerPosition({
3510
- selection,
3511
- environment,
3512
- composerPosition: draft.composerPosition,
3513
- estimatedHeight: 220
3514
- });
3515
- popover.style.left = `${composer.left}px`;
3516
- popover.style.top = `${composer.top}px`;
3517
- popover.style.width = `${composer.width}px`;
3518
- popover.style.right = "auto";
3519
- }
3520
- const dragHandle = options.dockComposer ? void 0 : this.createDraftDragHandle("Move area composer");
3521
- popover.append(
3522
- ...dragHandle ? [dragHandle] : [],
3523
- this.createAreaForm()
3440
+ return layer;
3441
+ }
3442
+ function createAreaDraftPopover(context, draft, options = {}) {
3443
+ const { config } = context;
3444
+ const environment = config.getEnvironment();
3445
+ const popover = document.createElement("div");
3446
+ popover.className = [
3447
+ "dfwr-area-draft",
3448
+ "is-composer",
3449
+ options.dockComposer ? "is-docked-composer" : ""
3450
+ ].filter(Boolean).join(" ");
3451
+ if (options.dockComposer) {
3452
+ popover.style.width = "100%";
3453
+ } else if (environment && draft.selection) {
3454
+ const selection = toHostSelection(
3455
+ toViewportSelection(draft.selection.viewport),
3456
+ environment
3524
3457
  );
3525
- if (dragHandle) {
3526
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
3527
- const areaDraft = this.state.areaDraft ?? draft;
3528
- this.config.actions.setAreaDraft({
3458
+ const composer = getDraftComposerPosition({
3459
+ selection,
3460
+ environment,
3461
+ composerPosition: draft.composerPosition,
3462
+ estimatedHeight: 220
3463
+ });
3464
+ popover.style.left = `${composer.left}px`;
3465
+ popover.style.top = `${composer.top}px`;
3466
+ popover.style.width = `${composer.width}px`;
3467
+ popover.style.right = "auto";
3468
+ }
3469
+ const dragHandle = options.dockComposer ? void 0 : createDraftDragHandle("Move area composer");
3470
+ popover.append(
3471
+ ...dragHandle ? [dragHandle] : [],
3472
+ createAreaForm(context)
3473
+ );
3474
+ if (dragHandle) {
3475
+ attachDraftComposerDrag({
3476
+ getEnvironment: () => config.getEnvironment(),
3477
+ popover,
3478
+ handle: dragHandle,
3479
+ onMove: (composerPosition) => {
3480
+ const areaDraft = config.getState().areaDraft ?? draft;
3481
+ config.actions.setAreaDraft({
3529
3482
  ...areaDraft,
3530
3483
  composerPosition
3531
3484
  });
3485
+ }
3486
+ });
3487
+ }
3488
+ return popover;
3489
+ }
3490
+
3491
+ // src/core/view/dom.draft.ts
3492
+ function createDomDraftLayer(context, draft, options = {}) {
3493
+ const { config } = context;
3494
+ const presets = config.options.viewports?.presets;
3495
+ const environment = config.getEnvironment();
3496
+ const group = document.createElement("div");
3497
+ group.className = "dfwr-dom-draft";
3498
+ if (!environment) return { layer: group, composer: void 0 };
3499
+ const isElementDraft = config.getState().mode === "element" && Boolean(draft.selection);
3500
+ const hostPoint = toHostPoint(
3501
+ isElementDraft ? getAdjustedDraftPoint(draft.marker.viewport, draft, presets) : draft.marker.viewport,
3502
+ environment
3503
+ );
3504
+ let selectionHighlight;
3505
+ if (draft.selection) {
3506
+ const selection = toViewportSelection(draft.selection.viewport);
3507
+ selectionHighlight = createSelectionHighlight(
3508
+ isElementDraft ? getAdjustedDraftSelection(selection, draft, presets) : selection,
3509
+ environment,
3510
+ true
3511
+ );
3512
+ group.append(selectionHighlight);
3513
+ }
3514
+ const pin = document.createElement("button");
3515
+ pin.className = "dfwr-dom-pin";
3516
+ pin.type = "button";
3517
+ pin.setAttribute("aria-label", "Move DOM point");
3518
+ pin.style.left = `${hostPoint.x}px`;
3519
+ pin.style.top = `${hostPoint.y}px`;
3520
+ const popover = document.createElement("div");
3521
+ const position = getPopoverPosition(hostPoint, environment);
3522
+ popover.className = [
3523
+ "dfwr-dom-popover",
3524
+ isElementDraft ? "is-composer" : "",
3525
+ options.dockComposer ? "is-docked-composer" : ""
3526
+ ].filter(Boolean).join(" ");
3527
+ if (options.dockComposer) {
3528
+ popover.style.width = "100%";
3529
+ } else if (isElementDraft) {
3530
+ const selection = draft.selection ? toHostSelection(
3531
+ getAdjustedDraftSelection(
3532
+ toViewportSelection(draft.selection.viewport),
3533
+ draft,
3534
+ presets
3535
+ ),
3536
+ environment
3537
+ ) : void 0;
3538
+ const composer = getDraftComposerPosition({
3539
+ selection,
3540
+ environment,
3541
+ composerPosition: draft.composerPosition,
3542
+ estimatedHeight: 252
3543
+ });
3544
+ popover.style.left = `${composer.left}px`;
3545
+ popover.style.top = `${composer.top}px`;
3546
+ popover.style.width = `${composer.width}px`;
3547
+ } else {
3548
+ popover.style.left = `${position.left}px`;
3549
+ popover.style.top = `${position.top}px`;
3550
+ }
3551
+ const form = document.createElement("form");
3552
+ form.className = "dfwr-form";
3553
+ const meta = isElementDraft ? void 0 : document.createElement("div");
3554
+ if (meta) {
3555
+ meta.className = "dfwr-item-date";
3556
+ meta.textContent = formatDomDraftMeta(draft);
3557
+ }
3558
+ const titleInput = isTitleFieldEnabled(config.options) ? createDraftTitleInput(draft.title, (title) => {
3559
+ const domDraft = config.getState().domDraft;
3560
+ if (!domDraft) return;
3561
+ config.actions.setDomDraft({
3562
+ ...domDraft,
3563
+ title
3564
+ });
3565
+ }) : void 0;
3566
+ const textarea = document.createElement("textarea");
3567
+ textarea.className = "dfwr-textarea";
3568
+ textarea.placeholder = "Review comment";
3569
+ textarea.rows = 4;
3570
+ textarea.value = draft.comment ?? "";
3571
+ textarea.addEventListener("input", () => {
3572
+ const domDraft = config.getState().domDraft;
3573
+ if (!domDraft) return;
3574
+ config.actions.setDomDraft({
3575
+ ...domDraft,
3576
+ comment: textarea.value
3577
+ });
3578
+ });
3579
+ attachDraftImagePasteQueue(textarea, {
3580
+ getAttachments: () => config.getState().domDraft?.attachments ?? draft.attachments,
3581
+ onAttachmentsChange: (attachments) => {
3582
+ const domDraft = config.getState().domDraft ?? draft;
3583
+ config.actions.setDomDraft({
3584
+ ...domDraft,
3585
+ comment: textarea.value,
3586
+ attachments
3587
+ });
3588
+ },
3589
+ onCommentChange: (comment) => {
3590
+ const domDraft = config.getState().domDraft ?? draft;
3591
+ config.actions.setDomDraft({
3592
+ ...domDraft,
3593
+ comment
3594
+ });
3595
+ },
3596
+ onPasteComplete: () => config.actions.render()
3597
+ });
3598
+ const assigneeSelect = createDraftAssigneeSelect(
3599
+ config.options,
3600
+ draft.assigneeId,
3601
+ draft.assigneeName,
3602
+ (assigneeId, assigneeName) => {
3603
+ const domDraft = config.getState().domDraft;
3604
+ if (!domDraft) return;
3605
+ config.actions.setDomDraft({
3606
+ ...domDraft,
3607
+ assigneeId,
3608
+ assigneeName
3532
3609
  });
3533
3610
  }
3534
- return popover;
3535
- }
3536
- createFormActions(saveLabel, onSave, options) {
3537
- const actions = document.createElement("div");
3538
- actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
3539
- const isSaving = this.state.isCreatingItem;
3540
- const save = document.createElement("button");
3541
- save.className = "dfwr-button is-primary";
3542
- save.type = "button";
3543
- save.disabled = isSaving;
3544
- save.setAttribute("aria-busy", isSaving ? "true" : "false");
3545
- if (isSaving) {
3546
- save.append(this.createSpinner("dfwr-spinner"), "Saving...");
3547
- } else {
3548
- save.textContent = saveLabel;
3549
- }
3550
- save.addEventListener("click", (event) => {
3551
- event.preventDefault();
3552
- event.stopPropagation();
3553
- if (this.state.isCreatingItem) return;
3554
- onSave();
3611
+ );
3612
+ const saveDraft = () => {
3613
+ const currentDraft = config.getState().domDraft ?? draft;
3614
+ const fields = getDraftFields(
3615
+ config.options,
3616
+ titleInput,
3617
+ textarea,
3618
+ assigneeSelect
3619
+ );
3620
+ const comment = fields.comment;
3621
+ const hasAttachments = Boolean(currentDraft.attachments?.length);
3622
+ if (!comment && !hasDraftAdjustment(currentDraft, presets) && !hasAttachments) {
3623
+ return;
3624
+ }
3625
+ void config.actions.createItem({
3626
+ kind: "dom",
3627
+ title: fields.title,
3628
+ comment: withDraftAdjustmentComment(
3629
+ comment,
3630
+ currentDraft,
3631
+ config.options
3632
+ ),
3633
+ assigneeId: fields.assigneeId,
3634
+ assigneeName: fields.assigneeName,
3635
+ viewport: currentDraft.viewport,
3636
+ anchor: currentDraft.anchor,
3637
+ marker: currentDraft.marker,
3638
+ selection: currentDraft.selection,
3639
+ attachments: currentDraft.attachments
3555
3640
  });
3556
- const cancel = document.createElement("button");
3557
- cancel.className = "dfwr-button";
3558
- cancel.type = "button";
3559
- cancel.disabled = isSaving;
3560
- cancel.textContent = "Cancel";
3561
- cancel.addEventListener("click", (event) => {
3562
- this.cancelDraft(event);
3641
+ };
3642
+ const adjustmentControls = isElementDraft ? createAdjustmentControls(context, {
3643
+ draft,
3644
+ pin,
3645
+ popover,
3646
+ selectionHighlight,
3647
+ textarea,
3648
+ dockToggle: options.dockComposer
3649
+ }) : void 0;
3650
+ const leadingActions = [
3651
+ adjustmentControls?.actionButton,
3652
+ createDraftCaptureButton(config, draft, {
3653
+ kind: "dom",
3654
+ isElementDraft,
3655
+ textarea
3656
+ })
3657
+ ].filter((element) => Boolean(element));
3658
+ const actions = createFormActions({
3659
+ saveLabel: "Save DOM QA",
3660
+ isSaving: config.getState().isCreatingItem,
3661
+ onSave: saveDraft,
3662
+ onCancel: context.cancelDraft,
3663
+ leading: leadingActions.length > 0 ? leadingActions : void 0
3664
+ });
3665
+ const error = createDraftError(config.getState().draftError);
3666
+ const attachmentQueue = createDraftAttachmentQueue(
3667
+ document,
3668
+ draft.attachments,
3669
+ (attachmentId) => {
3670
+ const domDraft = config.getState().domDraft ?? draft;
3671
+ const attachments = removeDraftAttachment(
3672
+ domDraft.attachments,
3673
+ attachmentId
3674
+ );
3675
+ config.actions.setDomDraft({
3676
+ ...domDraft,
3677
+ comment: textarea.value,
3678
+ attachments: attachments.length > 0 ? attachments : void 0
3679
+ });
3680
+ config.actions.render();
3681
+ }
3682
+ );
3683
+ form.append(
3684
+ ...meta ? [meta] : [],
3685
+ ...adjustmentControls ? [adjustmentControls.panel] : [],
3686
+ ...titleInput ? [titleInput] : [],
3687
+ textarea,
3688
+ ...attachmentQueue ? [attachmentQueue] : [],
3689
+ ...assigneeSelect ? [assigneeSelect] : [],
3690
+ ...error ? [error] : [],
3691
+ actions
3692
+ );
3693
+ const dragHandle = isElementDraft && !options.dockComposer ? createDraftDragHandle("Move DOM composer") : void 0;
3694
+ popover.append(...dragHandle ? [dragHandle] : [], form);
3695
+ group.append(pin);
3696
+ if (!options.dockComposer) {
3697
+ group.append(popover);
3698
+ }
3699
+ if (dragHandle) {
3700
+ attachDraftComposerDrag({
3701
+ getEnvironment: () => config.getEnvironment(),
3702
+ popover,
3703
+ handle: dragHandle,
3704
+ onMove: (composerPosition) => {
3705
+ const domDraft = config.getState().domDraft ?? draft;
3706
+ config.actions.setDomDraft({
3707
+ ...domDraft,
3708
+ composerPosition,
3709
+ comment: textarea.value
3710
+ });
3711
+ }
3563
3712
  });
3564
- if (options?.leading?.length) {
3565
- actions.classList.add("has-leading");
3566
- const leading = document.createElement("div");
3567
- leading.className = "dfwr-actions-leading";
3568
- leading.append(...options.leading);
3569
- const primary = document.createElement("div");
3570
- primary.className = "dfwr-actions-primary";
3571
- primary.append(save, cancel);
3572
- actions.append(leading, primary);
3573
- return actions;
3574
- }
3575
- if (options?.beforeSave?.length || options?.className) {
3576
- actions.append(cancel, ...options.beforeSave ?? [], save);
3577
- return actions;
3578
- }
3579
- actions.append(save, cancel);
3580
- return actions;
3581
3713
  }
3582
- createSpinner(className) {
3583
- const spinner = document.createElement("span");
3584
- spinner.className = className;
3585
- spinner.setAttribute("aria-hidden", "true");
3586
- return spinner;
3587
- }
3588
- createDraftError() {
3589
- if (!this.state.draftError) return void 0;
3590
- const error = document.createElement("p");
3591
- error.className = "dfwr-form-error";
3592
- error.setAttribute("role", "alert");
3593
- error.textContent = this.state.draftError;
3594
- return error;
3595
- }
3596
- createList() {
3597
- const section = document.createElement("div");
3598
- section.className = "dfwr-list";
3599
- const state = this.state;
3600
- const heading = document.createElement("div");
3601
- heading.className = "dfwr-list-heading";
3602
- heading.textContent = `Review items (${state.items.length})`;
3603
- section.append(heading);
3604
- if (state.items.length === 0) {
3605
- const empty = document.createElement("p");
3606
- empty.className = "dfwr-empty";
3607
- empty.textContent = "No review items on this page.";
3608
- section.append(empty);
3609
- return section;
3610
- }
3611
- for (const numberedItem of getNumberedReviewItems(
3612
- state.items,
3613
- this.config.options.viewports?.presets
3614
- )) {
3615
- section.append(this.createListItem(numberedItem));
3616
- }
3617
- return section;
3714
+ attachDraftPinDrag(context, {
3715
+ pin,
3716
+ popover: isElementDraft || options.dockComposer ? void 0 : popover,
3717
+ meta,
3718
+ textarea
3719
+ });
3720
+ if (!options.dockComposer) {
3721
+ window.setTimeout(() => {
3722
+ if (draft.adjustment?.isActive) {
3723
+ adjustmentControls?.focusTarget.focus();
3724
+ return;
3725
+ }
3726
+ textarea.focus();
3727
+ }, 0);
3618
3728
  }
3619
- createListItem(numberedItem) {
3620
- const { item } = numberedItem;
3621
- const row = document.createElement("article");
3622
- row.className = "dfwr-item";
3623
- row.tabIndex = 0;
3624
- row.setAttribute("role", "button");
3625
- row.setAttribute(
3729
+ return {
3730
+ layer: group,
3731
+ composer: options.dockComposer ? popover : void 0
3732
+ };
3733
+ }
3734
+ function createAdjustmentControls(context, {
3735
+ draft,
3736
+ pin,
3737
+ popover,
3738
+ selectionHighlight,
3739
+ textarea,
3740
+ dockToggle
3741
+ }) {
3742
+ const { config } = context;
3743
+ const presets = config.options.viewports?.presets;
3744
+ const panel = document.createElement("div");
3745
+ panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
3746
+ const header = document.createElement("div");
3747
+ header.className = "dfwr-adjust-panel-header";
3748
+ const help = document.createElement("div");
3749
+ help.className = "dfwr-adjust-help";
3750
+ help.textContent = getAdjustmentLabel(config.options);
3751
+ const adjust = document.createElement("button");
3752
+ adjust.className = "dfwr-adjust-toggle";
3753
+ adjust.type = "button";
3754
+ adjust.title = "Adjust DOM element with keyboard arrows";
3755
+ adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
3756
+ const xyStatus = document.createElement("div");
3757
+ xyStatus.className = "dfwr-adjust-status";
3758
+ const scaleStatus = document.createElement("div");
3759
+ scaleStatus.className = "dfwr-adjust-status";
3760
+ const syncControls = (nextDraft) => {
3761
+ const isActive = nextDraft.adjustment?.isActive === true;
3762
+ panel.classList.toggle("is-active", isActive);
3763
+ adjust.classList.toggle("is-active", isActive);
3764
+ adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
3765
+ setAdjustmentToggleIcon(adjust, isActive);
3766
+ adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
3767
+ adjust.setAttribute(
3626
3768
  "aria-label",
3627
- `Restore review item: ${item.title ?? item.comment}`
3769
+ isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
3628
3770
  );
3629
- row.addEventListener("click", () => {
3630
- void this.config.actions.restoreItem(item);
3771
+ const [xyLine, scaleLine] = getDraftAdjustmentMetricLines(
3772
+ nextDraft,
3773
+ presets
3774
+ );
3775
+ xyStatus.textContent = xyLine;
3776
+ scaleStatus.textContent = scaleLine;
3777
+ syncDraftAdjustmentUi(context, {
3778
+ draft: nextDraft,
3779
+ pin,
3780
+ selectionHighlight
3631
3781
  });
3632
- row.addEventListener("keydown", (event) => {
3633
- if (event.key !== "Enter" && event.key !== " ") return;
3634
- event.preventDefault();
3635
- void this.config.actions.restoreItem(item);
3782
+ };
3783
+ const updateDraft = (updater) => {
3784
+ const currentDraft = config.getState().domDraft ?? draft;
3785
+ const nextDraft = updater(currentDraft);
3786
+ config.actions.setDomDraft({
3787
+ ...nextDraft,
3788
+ comment: textarea.value
3636
3789
  });
3637
- const body = document.createElement("div");
3638
- body.className = "dfwr-item-body";
3639
- const badges = document.createElement("div");
3640
- badges.className = "dfwr-item-badges";
3641
- const scope = document.createElement("div");
3642
- scope.className = `dfwr-item-scope is-scope-${numberedItem.scope}`;
3643
- scope.textContent = numberedItem.displayLabel;
3644
- const kind = document.createElement("div");
3645
- kind.className = "dfwr-item-kind";
3646
- kind.textContent = item.kind;
3647
- badges.append(scope, kind);
3648
- const title = this.isTitleFieldEnabled() ? item.title?.trim() : "";
3649
- const titleElement = title ? document.createElement("strong") : void 0;
3650
- if (title && titleElement) {
3651
- titleElement.className = "dfwr-item-title";
3652
- titleElement.textContent = title;
3653
- }
3654
- const comment = document.createElement("p");
3655
- comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
3656
- comment.textContent = item.comment;
3657
- const date = document.createElement("time");
3658
- date.className = "dfwr-item-date";
3659
- date.dateTime = item.createdAt;
3660
- date.textContent = formatItemMeta(item);
3661
- body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
3662
- const actions = document.createElement("div");
3663
- actions.className = "dfwr-item-actions";
3664
- actions.addEventListener("click", (event) => event.stopPropagation());
3665
- actions.addEventListener("keydown", (event) => event.stopPropagation());
3666
- const remove = document.createElement("button");
3667
- remove.className = "dfwr-icon-button";
3668
- remove.type = "button";
3669
- remove.textContent = "x";
3670
- remove.setAttribute("aria-label", "Delete");
3671
- remove.addEventListener("click", (event) => {
3672
- event.stopPropagation();
3673
- void this.config.actions.removeItem(item.id).then(() => this.config.actions.reload());
3790
+ syncControls(nextDraft);
3791
+ };
3792
+ adjust.addEventListener("click", () => {
3793
+ updateDraft((currentDraft) => ({
3794
+ ...currentDraft,
3795
+ adjustment: {
3796
+ x: currentDraft.adjustment?.x ?? 0,
3797
+ y: currentDraft.adjustment?.y ?? 0,
3798
+ scale: currentDraft.adjustment?.scale ?? 0,
3799
+ isActive: currentDraft.adjustment?.isActive !== true
3800
+ }
3801
+ }));
3802
+ adjust.focus();
3803
+ });
3804
+ popover.addEventListener("keydown", (event) => {
3805
+ const currentDraft = config.getState().domDraft ?? draft;
3806
+ if (currentDraft.adjustment?.isActive !== true) return;
3807
+ const keyDelta = getAdjustmentKeyDelta(event);
3808
+ if (!keyDelta) return;
3809
+ event.preventDefault();
3810
+ event.stopPropagation();
3811
+ updateDraft((activeDraft) => ({
3812
+ ...activeDraft,
3813
+ adjustment: {
3814
+ x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
3815
+ y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
3816
+ scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
3817
+ isActive: true
3818
+ }
3819
+ }));
3820
+ });
3821
+ header.append(help);
3822
+ if (!dockToggle) {
3823
+ header.append(adjust);
3824
+ }
3825
+ panel.append(header, xyStatus, scaleStatus);
3826
+ syncControls(draft);
3827
+ return {
3828
+ panel,
3829
+ focusTarget: adjust,
3830
+ // 도킹 모드에서는 토글 버튼을 폼 액션 줄(leading)로 옮긴다.
3831
+ actionButton: dockToggle ? adjust : void 0
3832
+ };
3833
+ }
3834
+ function getAdjustmentKeyDelta(event) {
3835
+ const step = event.shiftKey ? 10 : 1;
3836
+ if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
3837
+ if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
3838
+ if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
3839
+ if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
3840
+ if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
3841
+ if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
3842
+ return void 0;
3843
+ }
3844
+ function syncDraftAdjustmentUi(context, {
3845
+ draft,
3846
+ pin,
3847
+ selectionHighlight
3848
+ }) {
3849
+ const { config } = context;
3850
+ const presets = config.options.viewports?.presets;
3851
+ const environment = config.getEnvironment();
3852
+ if (!environment) return;
3853
+ const hostPoint = toHostPoint(
3854
+ getAdjustedDraftPoint(draft.marker.viewport, draft, presets),
3855
+ environment
3856
+ );
3857
+ pin.style.left = `${hostPoint.x}px`;
3858
+ pin.style.top = `${hostPoint.y}px`;
3859
+ if (draft.selection && selectionHighlight) {
3860
+ const rect = toHostSelection(
3861
+ getAdjustedDraftSelection(
3862
+ toViewportSelection(draft.selection.viewport),
3863
+ draft,
3864
+ presets
3865
+ ),
3866
+ environment
3867
+ );
3868
+ selectionHighlight.style.left = `${rect.left}px`;
3869
+ selectionHighlight.style.top = `${rect.top}px`;
3870
+ selectionHighlight.style.width = `${rect.width}px`;
3871
+ selectionHighlight.style.height = `${rect.height}px`;
3872
+ }
3873
+ context.syncDraftPreview(draft);
3874
+ }
3875
+ function attachDraftPinDrag(context, {
3876
+ pin,
3877
+ popover,
3878
+ meta,
3879
+ textarea
3880
+ }) {
3881
+ const { config } = context;
3882
+ let isDragging = false;
3883
+ const moveDraftUi = (hostPoint) => {
3884
+ const environment = config.getEnvironment();
3885
+ if (!environment) return;
3886
+ const nextPoint = clampPoint(
3887
+ toTargetPoint(hostPoint, environment),
3888
+ environment
3889
+ );
3890
+ const nextHostPoint = toHostPoint(nextPoint, environment);
3891
+ pin.style.left = `${nextHostPoint.x}px`;
3892
+ pin.style.top = `${nextHostPoint.y}px`;
3893
+ if (popover) {
3894
+ const position = getPopoverPosition(nextHostPoint, environment);
3895
+ popover.style.left = `${position.left}px`;
3896
+ popover.style.top = `${position.top}px`;
3897
+ }
3898
+ const domDraft = config.getState().domDraft;
3899
+ if (!domDraft) return;
3900
+ const nextDraft = {
3901
+ ...domDraft,
3902
+ marker: {
3903
+ ...domDraft.marker,
3904
+ viewport: roundPoint(nextPoint)
3905
+ },
3906
+ comment: textarea.value
3907
+ };
3908
+ config.actions.setDomDraft(nextDraft);
3909
+ if (meta) {
3910
+ meta.textContent = formatDomDraftMeta(nextDraft);
3911
+ }
3912
+ };
3913
+ pin.addEventListener("pointerdown", (event) => {
3914
+ if (event.button !== 0) return;
3915
+ event.preventDefault();
3916
+ event.stopPropagation();
3917
+ isDragging = true;
3918
+ pin.setPointerCapture(event.pointerId);
3919
+ });
3920
+ pin.addEventListener("pointermove", (event) => {
3921
+ if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
3922
+ event.preventDefault();
3923
+ moveDraftUi({
3924
+ x: event.clientX,
3925
+ y: event.clientY
3674
3926
  });
3675
- actions.append(remove);
3676
- row.append(body, actions);
3677
- return row;
3927
+ });
3928
+ pin.addEventListener("pointerup", (event) => {
3929
+ if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
3930
+ event.preventDefault();
3931
+ event.stopPropagation();
3932
+ isDragging = false;
3933
+ pin.releasePointerCapture(event.pointerId);
3934
+ const nextPoint = toTargetPointFromHostEvent(
3935
+ event,
3936
+ config.getEnvironment()
3937
+ );
3938
+ const currentDraft = config.getState().domDraft;
3939
+ const fields = {
3940
+ title: currentDraft?.title,
3941
+ comment: textarea.value,
3942
+ assigneeId: currentDraft?.assigneeId,
3943
+ assigneeName: currentDraft?.assigneeName
3944
+ };
3945
+ void config.actions.bindElementDraftToPoint(nextPoint, fields);
3946
+ });
3947
+ }
3948
+
3949
+ // src/core/view/panel.ts
3950
+ function createReviewPanel(config, renderAreaForm) {
3951
+ const panel = document.createElement("div");
3952
+ panel.className = "dfwr-panel";
3953
+ panel.setAttribute("role", "dialog");
3954
+ panel.setAttribute("aria-label", "Web review kit");
3955
+ panel.append(
3956
+ createHeader(config),
3957
+ createToolbar(config),
3958
+ createBody(config, renderAreaForm),
3959
+ createList(config)
3960
+ );
3961
+ return panel;
3962
+ }
3963
+ function createHeader(config) {
3964
+ const header = document.createElement("div");
3965
+ header.className = "dfwr-header";
3966
+ const title = document.createElement("div");
3967
+ title.className = "dfwr-title";
3968
+ title.textContent = "Review Kit";
3969
+ const meta = document.createElement("div");
3970
+ meta.className = "dfwr-meta";
3971
+ meta.textContent = getRouteKey(config.getEnvironment());
3972
+ const titleGroup = document.createElement("div");
3973
+ titleGroup.append(title, meta);
3974
+ const close = document.createElement("button");
3975
+ close.className = "dfwr-icon-button";
3976
+ close.type = "button";
3977
+ close.textContent = "x";
3978
+ close.setAttribute("aria-label", "Close");
3979
+ close.addEventListener("click", () => config.actions.close());
3980
+ header.append(titleGroup, close);
3981
+ return header;
3982
+ }
3983
+ function createToolbar(config) {
3984
+ const state = config.getState();
3985
+ const toolbar = document.createElement("div");
3986
+ toolbar.className = "dfwr-toolbar";
3987
+ const toggleMode = (target) => {
3988
+ const mode = config.getState().mode;
3989
+ config.actions.setModeState(mode === target ? "idle" : target);
3990
+ config.actions.clearDrafts();
3991
+ config.actions.render();
3992
+ };
3993
+ toolbar.append(
3994
+ createToolbarButton(
3995
+ "Element",
3996
+ state.mode === "element",
3997
+ () => toggleMode("element")
3998
+ ),
3999
+ createToolbarButton(
4000
+ state.isSelectingArea ? "Selecting" : "Area",
4001
+ state.mode === "area",
4002
+ () => toggleMode("area")
4003
+ ),
4004
+ createToolbarButton("Refresh", false, () => {
4005
+ void config.actions.reload();
4006
+ })
4007
+ );
4008
+ return toolbar;
4009
+ }
4010
+ function createToolbarButton(label, active, onClick) {
4011
+ const button = document.createElement("button");
4012
+ button.className = `dfwr-button${active ? " is-active" : ""}`;
4013
+ button.type = "button";
4014
+ button.textContent = label;
4015
+ button.addEventListener("click", onClick);
4016
+ return button;
4017
+ }
4018
+ function createBody(config, renderAreaForm) {
4019
+ const body = document.createElement("div");
4020
+ body.className = "dfwr-body";
4021
+ const state = config.getState();
4022
+ if (state.mode === "idle") {
4023
+ body.append(
4024
+ createEmptyText("Select an element or mark an area.")
4025
+ );
4026
+ return body;
3678
4027
  }
3679
- createMarkerLayer() {
3680
- const layer = document.createElement("div");
3681
- layer.className = "dfwr-marker-layer";
3682
- const environment = this.config.getEnvironment();
3683
- if (!environment) return layer;
3684
- const currentScope = getReviewViewportScope(
3685
- getViewportSize(environment),
3686
- this.config.options.viewports?.presets
4028
+ if (state.mode === "element") {
4029
+ body.append(
4030
+ createEmptyText(
4031
+ state.domDraft ? "Write the QA in the page box." : "Click an element to add QA."
4032
+ )
3687
4033
  );
3688
- getNumberedReviewItems(
3689
- this.state.items,
3690
- this.config.options.viewports?.presets
3691
- ).forEach((numberedItem) => {
3692
- const { item, scope, displayLabel } = numberedItem;
3693
- if (!shouldShowMarkerForScope(scope, currentScope)) {
3694
- return;
3695
- }
3696
- const isHighlighted = item.id === this.state.highlightedItemId;
3697
- const highlightMode = getReviewItemHighlightMode(item);
3698
- if (highlightMode !== "note" && (!this.state.highlightedItemId || isHighlighted)) {
3699
- const selection = getItemHighlightSelection(item, environment);
3700
- if (selection) {
3701
- layer.append(
3702
- ...this.createItemHighlightElements(
3703
- selection.viewport,
3704
- environment,
3705
- item,
3706
- displayLabel,
3707
- selection.isBound,
3708
- isHighlighted
3709
- )
3710
- );
3711
- return;
3712
- }
3713
- }
3714
- const point = getBoundMarkerPoint(item, environment);
3715
- if (!point || !isPointInViewport(point.viewport, environment)) {
3716
- return;
4034
+ return body;
4035
+ }
4036
+ body.append(renderAreaForm());
4037
+ return body;
4038
+ }
4039
+ function createEmptyText(text) {
4040
+ const empty = document.createElement("p");
4041
+ empty.className = "dfwr-empty";
4042
+ empty.textContent = text;
4043
+ return empty;
4044
+ }
4045
+ function createList(config) {
4046
+ const section = document.createElement("div");
4047
+ section.className = "dfwr-list";
4048
+ const state = config.getState();
4049
+ const heading = document.createElement("div");
4050
+ heading.className = "dfwr-list-heading";
4051
+ heading.textContent = `Review items (${state.items.length})`;
4052
+ section.append(heading);
4053
+ if (state.items.length === 0) {
4054
+ section.append(createEmptyText("No review items on this page."));
4055
+ return section;
4056
+ }
4057
+ for (const numberedItem of getNumberedReviewItems(
4058
+ state.items,
4059
+ config.options.viewports?.presets
4060
+ )) {
4061
+ section.append(createListItem(config, numberedItem));
4062
+ }
4063
+ return section;
4064
+ }
4065
+ function createListItem(config, numberedItem) {
4066
+ const { item } = numberedItem;
4067
+ const row = document.createElement("article");
4068
+ row.className = "dfwr-item";
4069
+ row.tabIndex = 0;
4070
+ row.setAttribute("role", "button");
4071
+ row.setAttribute(
4072
+ "aria-label",
4073
+ `Restore review item: ${item.title ?? item.comment}`
4074
+ );
4075
+ row.addEventListener("click", () => {
4076
+ void config.actions.restoreItem(item);
4077
+ });
4078
+ row.addEventListener("keydown", (event) => {
4079
+ if (event.key !== "Enter" && event.key !== " ") return;
4080
+ event.preventDefault();
4081
+ void config.actions.restoreItem(item);
4082
+ });
4083
+ const body = document.createElement("div");
4084
+ body.className = "dfwr-item-body";
4085
+ const badges = document.createElement("div");
4086
+ badges.className = "dfwr-item-badges";
4087
+ const scope = document.createElement("div");
4088
+ scope.className = `dfwr-item-scope is-scope-${numberedItem.scope}`;
4089
+ scope.textContent = numberedItem.displayLabel;
4090
+ const kind = document.createElement("div");
4091
+ kind.className = "dfwr-item-kind";
4092
+ kind.textContent = item.kind;
4093
+ badges.append(scope, kind);
4094
+ const title = isTitleFieldEnabled(config.options) ? item.title?.trim() : "";
4095
+ const titleElement = title ? document.createElement("strong") : void 0;
4096
+ if (title && titleElement) {
4097
+ titleElement.className = "dfwr-item-title";
4098
+ titleElement.textContent = title;
4099
+ }
4100
+ const comment = document.createElement("p");
4101
+ comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
4102
+ comment.textContent = item.comment;
4103
+ const date = document.createElement("time");
4104
+ date.className = "dfwr-item-date";
4105
+ date.dateTime = item.createdAt;
4106
+ date.textContent = formatItemMeta(item);
4107
+ body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
4108
+ const actions = document.createElement("div");
4109
+ actions.className = "dfwr-item-actions";
4110
+ actions.addEventListener("click", (event) => event.stopPropagation());
4111
+ actions.addEventListener("keydown", (event) => event.stopPropagation());
4112
+ const remove = document.createElement("button");
4113
+ remove.className = "dfwr-icon-button";
4114
+ remove.type = "button";
4115
+ remove.textContent = "x";
4116
+ remove.setAttribute("aria-label", "Delete");
4117
+ remove.addEventListener("click", (event) => {
4118
+ event.stopPropagation();
4119
+ void config.actions.removeItem(item.id).then(() => config.actions.reload());
4120
+ });
4121
+ actions.append(remove);
4122
+ row.append(body, actions);
4123
+ return row;
4124
+ }
4125
+
4126
+ // src/core/view/selection.layers.ts
4127
+ function createElementLayer(config) {
4128
+ const layer = document.createElement("div");
4129
+ layer.className = "dfwr-element-layer";
4130
+ const environment = config.getEnvironment();
4131
+ const hover = document.createElement("div");
4132
+ hover.className = "dfwr-dom-hover";
4133
+ hover.hidden = true;
4134
+ layer.append(hover);
4135
+ if (environment) {
4136
+ placeLayerOverTarget(layer, environment);
4137
+ }
4138
+ const updateHover = (point) => {
4139
+ const nextEnvironment = config.getEnvironment();
4140
+ if (!nextEnvironment) return;
4141
+ const anchor = getDomAnchorFromPoint(
4142
+ clampPoint(point, nextEnvironment),
4143
+ config.options.anchors?.attribute,
4144
+ nextEnvironment
4145
+ );
4146
+ const selection = anchor ? getElementViewportSelection(anchor, nextEnvironment) : void 0;
4147
+ if (!selection) {
4148
+ hover.hidden = true;
4149
+ return;
4150
+ }
4151
+ const rect = toHostSelection(selection, nextEnvironment);
4152
+ hover.hidden = false;
4153
+ hover.style.left = `${rect.left}px`;
4154
+ hover.style.top = `${rect.top}px`;
4155
+ hover.style.width = `${rect.width}px`;
4156
+ hover.style.height = `${rect.height}px`;
4157
+ };
4158
+ layer.addEventListener("pointermove", (event) => {
4159
+ updateHover(toTargetPointFromHostEvent(event, config.getEnvironment()));
4160
+ });
4161
+ layer.addEventListener("pointerleave", () => {
4162
+ hover.hidden = true;
4163
+ });
4164
+ layer.addEventListener("pointerdown", (event) => {
4165
+ if (event.button !== 0) return;
4166
+ event.preventDefault();
4167
+ void config.actions.bindElementDraftToPoint(
4168
+ toTargetPointFromHostEvent(event, config.getEnvironment())
4169
+ );
4170
+ });
4171
+ return layer;
4172
+ }
4173
+ function createAreaLayer(config) {
4174
+ const layer = document.createElement("div");
4175
+ layer.className = "dfwr-area-layer";
4176
+ const environment = config.getEnvironment();
4177
+ if (environment) {
4178
+ placeLayerOverTarget(layer, environment);
4179
+ }
4180
+ const box = document.createElement("div");
4181
+ box.className = "dfwr-area-box";
4182
+ layer.append(box);
4183
+ let startX = 0;
4184
+ let startY = 0;
4185
+ let selection;
4186
+ let activePointerId;
4187
+ let isDragging = false;
4188
+ const ownerWindow = layer.ownerDocument.defaultView ?? window;
4189
+ const updateBox = (event) => {
4190
+ const nextEnvironment = config.getEnvironment();
4191
+ const nextPoint = toTargetPointFromHostEvent(event, nextEnvironment);
4192
+ const left = Math.min(startX, nextPoint.x);
4193
+ const top = Math.min(startY, nextPoint.y);
4194
+ const width = Math.abs(nextPoint.x - startX);
4195
+ const height = Math.abs(nextPoint.y - startY);
4196
+ selection = { left, top, width, height };
4197
+ const rect = nextEnvironment ? toHostSelection(selection, nextEnvironment) : selection;
4198
+ box.style.left = `${rect.left}px`;
4199
+ box.style.top = `${rect.top}px`;
4200
+ box.style.width = `${rect.width}px`;
4201
+ box.style.height = `${rect.height}px`;
4202
+ };
4203
+ const addDragListeners = () => {
4204
+ ownerWindow.addEventListener("pointermove", handlePointerMove, true);
4205
+ ownerWindow.addEventListener("pointerup", handlePointerUp, true);
4206
+ ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
4207
+ };
4208
+ const removeDragListeners = () => {
4209
+ ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
4210
+ ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
4211
+ ownerWindow.removeEventListener(
4212
+ "pointercancel",
4213
+ handlePointerCancel,
4214
+ true
4215
+ );
4216
+ };
4217
+ const releasePointerCapture = (event) => {
4218
+ try {
4219
+ if (layer.hasPointerCapture(event.pointerId)) {
4220
+ layer.releasePointerCapture(event.pointerId);
3717
4221
  }
3718
- const hostPoint = toHostPoint(point.viewport, environment);
3719
- const marker = this.createMarkerElement(
3720
- item.id,
3721
- hostPoint,
3722
- displayLabel,
3723
- scope,
3724
- point.isBound,
3725
- isHighlighted,
3726
- highlightMode === "note" ? "note" : "default"
3727
- );
3728
- marker.title = `${displayLabel} / ${item.comment}
3729
- ${formatItemMeta(item)}`;
3730
- layer.append(marker);
4222
+ } catch {
4223
+ }
4224
+ };
4225
+ function isActivePointer(event) {
4226
+ return isDragging && event.pointerId === activePointerId;
4227
+ }
4228
+ const finishAreaSelection = (event) => {
4229
+ if (!isActivePointer(event)) return;
4230
+ event.preventDefault();
4231
+ updateBox(event);
4232
+ releasePointerCapture(event);
4233
+ removeDragListeners();
4234
+ isDragging = false;
4235
+ activePointerId = void 0;
4236
+ if (!selection || selection.width < 8 || selection.height < 8) return;
4237
+ config.actions.setSelectingArea(true);
4238
+ config.actions.render();
4239
+ void config.actions.createAreaDraft(selection);
4240
+ };
4241
+ function handlePointerMove(event) {
4242
+ if (!isActivePointer(event)) return;
4243
+ event.preventDefault();
4244
+ updateBox(event);
4245
+ }
4246
+ const handlePointerUp = (event) => {
4247
+ finishAreaSelection(event);
4248
+ };
4249
+ const handlePointerCancel = (event) => {
4250
+ if (!isActivePointer(event)) return;
4251
+ releasePointerCapture(event);
4252
+ removeDragListeners();
4253
+ isDragging = false;
4254
+ activePointerId = void 0;
4255
+ };
4256
+ layer.addEventListener("pointerdown", (event) => {
4257
+ if (event.button !== 0) return;
4258
+ event.preventDefault();
4259
+ activePointerId = event.pointerId;
4260
+ isDragging = true;
4261
+ try {
4262
+ layer.setPointerCapture(event.pointerId);
4263
+ } catch {
4264
+ }
4265
+ const startPoint = toTargetPointFromHostEvent(
4266
+ event,
4267
+ config.getEnvironment()
4268
+ );
4269
+ startX = startPoint.x;
4270
+ startY = startPoint.y;
4271
+ updateBox(event);
4272
+ addDragListeners();
4273
+ });
4274
+ layer.addEventListener("pointermove", handlePointerMove);
4275
+ layer.addEventListener("pointerup", handlePointerUp);
4276
+ layer.addEventListener("pointercancel", handlePointerCancel);
4277
+ return layer;
4278
+ }
4279
+
4280
+ // src/core/web.review.kit.view.ts
4281
+ var WebReviewKitView = class {
4282
+ constructor(config) {
4283
+ this.config = config;
4284
+ const presets = () => this.config.options.viewports?.presets;
4285
+ this.draftPreview = new DraftPreviewController({
4286
+ getEnvironment: () => this.config.getEnvironment(),
4287
+ getMetrics: (draft) => getDraftAdjustmentMetrics(draft, presets()),
4288
+ hasAdjustment: (draft) => hasDraftAdjustment(draft, presets())
3731
4289
  });
3732
- return layer;
3733
- }
3734
- createItemHighlightElements(selection, environment, item, label, isBound, isHighlighted) {
3735
- const rect = toHostSelection(selection, environment);
3736
- const mode = getReviewItemHighlightMode(item);
3737
- const highlight = document.createElement("div");
3738
- highlight.className = [
3739
- "dfwr-item-target-highlight",
3740
- `is-mode-${mode}`,
3741
- isBound ? "is-bound" : "is-fallback",
3742
- isHighlighted ? "is-highlighted" : ""
3743
- ].filter(Boolean).join(" ");
3744
- highlight.style.left = `${rect.left}px`;
3745
- highlight.style.top = `${rect.top}px`;
3746
- highlight.style.width = `${rect.width}px`;
3747
- highlight.style.height = `${rect.height}px`;
3748
- highlight.dataset.reviewItemId = item.id;
3749
- const labelElement = document.createElement("div");
3750
- labelElement.className = [
3751
- "dfwr-item-target-label",
3752
- `is-mode-${mode}`,
3753
- isHighlighted ? "is-highlighted" : ""
3754
- ].filter(Boolean).join(" ");
3755
- labelElement.textContent = label;
3756
- labelElement.style.left = `${Math.max(4, rect.left)}px`;
3757
- labelElement.style.top = `${Math.max(4, rect.top - 24)}px`;
3758
- labelElement.dataset.reviewItemId = item.id;
3759
- return [highlight, labelElement];
4290
+ this.draftContext = {
4291
+ config: this.config,
4292
+ cancelDraft: (event) => this.cancelDraft(event),
4293
+ syncDraftPreview: (draft) => this.draftPreview.sync(draft)
4294
+ };
3760
4295
  }
3761
- createSelectionHighlight(selection, environment, isDraft) {
3762
- const rect = toHostSelection(selection, environment);
3763
- const highlight = document.createElement("div");
3764
- highlight.className = `dfwr-selection-highlight${isDraft ? " is-draft" : ""}`;
3765
- highlight.style.left = `${rect.left}px`;
3766
- highlight.style.top = `${rect.top}px`;
3767
- highlight.style.width = `${rect.width}px`;
3768
- highlight.style.height = `${rect.height}px`;
3769
- return highlight;
4296
+ clearDraftPreview() {
4297
+ this.draftPreview.clear();
4298
+ this.clearShellComposer();
3770
4299
  }
3771
- createMarkerElement(itemId, hostPoint, label, scope, isBound, isHighlighted, variant = "default") {
3772
- const isNoteCallout = variant === "note";
3773
- const marker = document.createElement("div");
3774
- marker.className = [
3775
- "dfwr-bound-marker",
3776
- isNoteCallout ? "is-note-callout" : "",
3777
- `is-scope-${scope}`,
3778
- isBound ? "is-bound" : "is-fallback",
3779
- isHighlighted ? "is-highlighted" : ""
4300
+ /** Rebuilds the entire shadow-root UI from the current state snapshot. */
4301
+ render(shadow, hiddenItemsStyle) {
4302
+ const state = this.state;
4303
+ this.draftPreview.sync(
4304
+ state.isOpen && state.mode === "element" ? state.domDraft : void 0
4305
+ );
4306
+ shadow.replaceChildren();
4307
+ shadow.append(createStyleElement());
4308
+ shadow.append(hiddenItemsStyle);
4309
+ const hasDismissableDraft = Boolean(state.domDraft || state.areaDraft);
4310
+ const shouldDockComposer = this.config.options.ui?.panel === false && hasDismissableDraft && Boolean(this.getShellComposerHost());
4311
+ let dockedComposer;
4312
+ const shell = document.createElement("div");
4313
+ shell.className = [
4314
+ "dfwr-shell",
4315
+ state.isOpen ? "is-open" : "",
4316
+ hasDismissableDraft && !shouldDockComposer ? "has-dismissible-draft" : ""
3780
4317
  ].filter(Boolean).join(" ");
3781
- marker.style.left = `${hostPoint.x}px`;
3782
- marker.style.top = `${hostPoint.y}px`;
3783
- marker.dataset.scope = scope;
3784
- if (itemId) {
3785
- marker.dataset.reviewItemId = itemId;
3786
- }
3787
- const iconElement = document.createElement("span");
3788
- iconElement.className = "dfwr-bound-marker-icon";
3789
- iconElement.setAttribute("aria-hidden", "true");
3790
- const labelElement = document.createElement("span");
3791
- labelElement.className = "dfwr-bound-marker-number";
3792
- labelElement.textContent = label;
3793
- marker.append(iconElement, labelElement);
3794
- return marker;
3795
- }
3796
- attachDraftPinDrag(pin, popover, meta, textarea) {
3797
- let isDragging = false;
3798
- const moveDraftUi = (hostPoint) => {
3799
- const environment = this.config.getEnvironment();
3800
- if (!environment) return;
3801
- const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
3802
- const nextHostPoint = toHostPoint(nextPoint, environment);
3803
- pin.style.left = `${nextHostPoint.x}px`;
3804
- pin.style.top = `${nextHostPoint.y}px`;
3805
- if (popover) {
3806
- const position = getPopoverPosition(nextHostPoint, environment);
3807
- popover.style.left = `${position.left}px`;
3808
- popover.style.top = `${position.top}px`;
4318
+ shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
4319
+ if (this.config.options.ui?.panel !== false) {
4320
+ shell.append(
4321
+ createReviewPanel(
4322
+ this.config,
4323
+ () => createAreaForm(this.draftContext)
4324
+ )
4325
+ );
4326
+ }
4327
+ shell.append(
4328
+ createMarkerLayer({
4329
+ items: state.items,
4330
+ highlightedItemId: state.highlightedItemId,
4331
+ environment: this.config.getEnvironment(),
4332
+ presets: this.config.options.viewports?.presets,
4333
+ showCompactMarkers: this.config.options.ui?.markers !== "external"
4334
+ })
4335
+ );
4336
+ if (state.isOpen && hasDismissableDraft && !shouldDockComposer) {
4337
+ shell.append(this.createDraftCancelLayer());
4338
+ }
4339
+ if (state.isOpen && state.mode === "element") {
4340
+ if (state.domDraft) {
4341
+ const domDraft = createDomDraftLayer(this.draftContext, state.domDraft, {
4342
+ dockComposer: shouldDockComposer
4343
+ });
4344
+ shell.append(domDraft.layer);
4345
+ dockedComposer = domDraft.composer;
4346
+ } else {
4347
+ shell.append(createElementLayer(this.config));
3809
4348
  }
3810
- const noteDraft = this.state.noteDraft;
3811
- if (!noteDraft) return;
3812
- const nextDraft = {
3813
- ...noteDraft,
3814
- marker: {
3815
- ...noteDraft.marker,
3816
- viewport: roundPoint(nextPoint)
3817
- },
3818
- comment: textarea.value
3819
- };
3820
- this.config.actions.setNoteDraft(nextDraft);
3821
- if (meta) {
3822
- meta.textContent = formatNoteDraftMeta(nextDraft);
4349
+ }
4350
+ if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
4351
+ shell.append(createAreaLayer(this.config));
4352
+ }
4353
+ if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
4354
+ if (state.areaDraft.selection) {
4355
+ shell.append(createAreaDraftOverlay(this.draftContext, state.areaDraft));
3823
4356
  }
3824
- };
3825
- pin.addEventListener("pointerdown", (event) => {
3826
- if (event.button !== 0) return;
3827
- event.preventDefault();
3828
- event.stopPropagation();
3829
- isDragging = true;
3830
- pin.setPointerCapture(event.pointerId);
3831
- });
3832
- pin.addEventListener("pointermove", (event) => {
3833
- if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
3834
- event.preventDefault();
3835
- moveDraftUi({
3836
- x: event.clientX,
3837
- y: event.clientY
3838
- });
3839
- });
3840
- pin.addEventListener("pointerup", (event) => {
3841
- if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
3842
- event.preventDefault();
3843
- event.stopPropagation();
3844
- isDragging = false;
3845
- pin.releasePointerCapture(event.pointerId);
3846
- const nextPoint = toTargetPointFromHostEvent(
3847
- event,
3848
- this.config.getEnvironment()
4357
+ const areaComposer = createAreaDraftPopover(
4358
+ this.draftContext,
4359
+ state.areaDraft,
4360
+ { dockComposer: shouldDockComposer }
3849
4361
  );
3850
- const currentDraft = this.state.noteDraft;
3851
- const fields = {
3852
- title: currentDraft?.title,
3853
- comment: textarea.value,
3854
- assigneeId: currentDraft?.assigneeId,
3855
- assigneeName: currentDraft?.assigneeName
3856
- };
3857
- void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, fields) : this.config.actions.bindNoteDraftToPoint(nextPoint, fields));
3858
- });
4362
+ if (shouldDockComposer) {
4363
+ dockedComposer = areaComposer;
4364
+ } else {
4365
+ shell.append(areaComposer);
4366
+ }
4367
+ }
4368
+ shadow.append(shell);
4369
+ this.renderShellComposer(dockedComposer);
3859
4370
  }
3860
- createNoteLayer() {
3861
- const layer = document.createElement("div");
3862
- layer.className = "dfwr-text-layer";
4371
+ get state() {
4372
+ return this.config.getState();
4373
+ }
4374
+ getShellComposerHost() {
3863
4375
  const environment = this.config.getEnvironment();
3864
- if (environment) {
3865
- placeLayerOverTarget(layer, environment);
4376
+ if (this.config.options.ui?.panel !== false) return void 0;
4377
+ return environment?.composerHost ?? void 0;
4378
+ }
4379
+ /** Mounts the docked composer into the shell-provided host element. */
4380
+ renderShellComposer(composer) {
4381
+ const host = composer ? this.getShellComposerHost() : void 0;
4382
+ if (!host || !composer) {
4383
+ this.clearShellComposer();
4384
+ return;
3866
4385
  }
3867
- layer.addEventListener("pointerdown", (event) => {
3868
- if (event.button !== 0) return;
3869
- event.preventDefault();
3870
- void this.config.actions.bindNoteDraftToPoint(
3871
- toTargetPointFromHostEvent(event, this.config.getEnvironment())
3872
- );
3873
- });
3874
- return layer;
4386
+ if (this.shellComposerHost && this.shellComposerHost !== host) {
4387
+ this.clearShellComposer();
4388
+ }
4389
+ this.shellComposerHost = host;
4390
+ host.dataset.hasDraftComposer = "true";
4391
+ if (host.parentElement) {
4392
+ host.parentElement.dataset.hasDraftComposer = "true";
4393
+ }
4394
+ const shell = document.createElement("div");
4395
+ shell.className = "dfwr-shell is-open is-shell-draft is-docked-composer";
4396
+ shell.append(composer);
4397
+ host.replaceChildren(createStyleElement(), shell);
3875
4398
  }
3876
- createElementLayer() {
3877
- const layer = document.createElement("div");
3878
- layer.className = "dfwr-element-layer";
3879
- const environment = this.config.getEnvironment();
3880
- const hover = document.createElement("div");
3881
- hover.className = "dfwr-dom-hover";
3882
- hover.hidden = true;
3883
- layer.append(hover);
3884
- if (environment) {
3885
- placeLayerOverTarget(layer, environment);
4399
+ clearShellComposer() {
4400
+ const host = this.shellComposerHost;
4401
+ host?.replaceChildren();
4402
+ if (host) {
4403
+ delete host.dataset.hasDraftComposer;
4404
+ delete host.parentElement?.dataset.hasDraftComposer;
3886
4405
  }
3887
- const updateHover = (point) => {
3888
- const nextEnvironment = this.config.getEnvironment();
3889
- if (!nextEnvironment) return;
3890
- const anchor = getDomAnchorFromPoint(
3891
- clampPoint(point, nextEnvironment),
3892
- this.config.options.anchors?.attribute,
3893
- nextEnvironment
3894
- );
3895
- const selection = anchor ? getElementViewportSelection(anchor, nextEnvironment) : void 0;
3896
- if (!selection) {
3897
- hover.hidden = true;
3898
- return;
3899
- }
3900
- const rect = toHostSelection(selection, nextEnvironment);
3901
- hover.hidden = false;
3902
- hover.style.left = `${rect.left}px`;
3903
- hover.style.top = `${rect.top}px`;
3904
- hover.style.width = `${rect.width}px`;
3905
- hover.style.height = `${rect.height}px`;
3906
- };
3907
- layer.addEventListener("pointermove", (event) => {
3908
- updateHover(toTargetPointFromHostEvent(event, this.config.getEnvironment()));
3909
- });
3910
- layer.addEventListener("pointerleave", () => {
3911
- hover.hidden = true;
3912
- });
3913
- layer.addEventListener("pointerdown", (event) => {
3914
- if (event.button !== 0) return;
3915
- event.preventDefault();
3916
- void this.config.actions.bindElementDraftToPoint(
3917
- toTargetPointFromHostEvent(event, this.config.getEnvironment())
3918
- );
3919
- });
3920
- return layer;
4406
+ this.shellComposerHost = void 0;
3921
4407
  }
3922
- createAreaLayer() {
4408
+ /** Full-screen click-away layer that cancels the active draft. */
4409
+ createDraftCancelLayer() {
3923
4410
  const layer = document.createElement("div");
3924
- layer.className = "dfwr-area-layer";
3925
- const environment = this.config.getEnvironment();
3926
- if (environment) {
3927
- placeLayerOverTarget(layer, environment);
3928
- }
3929
- const box = document.createElement("div");
3930
- box.className = "dfwr-area-box";
3931
- layer.append(box);
3932
- let startX = 0;
3933
- let startY = 0;
3934
- let selection;
3935
- let activePointerId;
3936
- let isDragging = false;
3937
- const ownerWindow = layer.ownerDocument.defaultView ?? window;
3938
- const updateBox = (event) => {
3939
- const nextEnvironment = this.config.getEnvironment();
3940
- const nextPoint = toTargetPointFromHostEvent(
3941
- event,
3942
- nextEnvironment
3943
- );
3944
- const left = Math.min(startX, nextPoint.x);
3945
- const top = Math.min(startY, nextPoint.y);
3946
- const width = Math.abs(nextPoint.x - startX);
3947
- const height = Math.abs(nextPoint.y - startY);
3948
- const hostPoint = toHostPoint(
3949
- { x: left, y: top },
3950
- nextEnvironment
3951
- );
3952
- selection = { left, top, width, height };
3953
- box.style.left = `${hostPoint.x}px`;
3954
- box.style.top = `${hostPoint.y}px`;
3955
- box.style.width = `${width}px`;
3956
- box.style.height = `${height}px`;
3957
- };
3958
- const addDragListeners = () => {
3959
- ownerWindow.addEventListener("pointermove", handlePointerMove, true);
3960
- ownerWindow.addEventListener("pointerup", handlePointerUp, true);
3961
- ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
3962
- };
3963
- const removeDragListeners = () => {
3964
- ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
3965
- ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
3966
- ownerWindow.removeEventListener(
3967
- "pointercancel",
3968
- handlePointerCancel,
3969
- true
3970
- );
3971
- };
3972
- const releasePointerCapture = (event) => {
3973
- try {
3974
- if (layer.hasPointerCapture(event.pointerId)) {
3975
- layer.releasePointerCapture(event.pointerId);
3976
- }
3977
- } catch {
3978
- }
3979
- };
3980
- function isActivePointer(event) {
3981
- return isDragging && event.pointerId === activePointerId;
3982
- }
3983
- const finishAreaSelection = (event) => {
3984
- if (!isActivePointer(event)) return;
3985
- event.preventDefault();
3986
- updateBox(event);
3987
- releasePointerCapture(event);
3988
- removeDragListeners();
3989
- isDragging = false;
3990
- activePointerId = void 0;
3991
- if (!selection || selection.width < 8 || selection.height < 8) return;
3992
- this.config.actions.setSelectingArea(true);
3993
- this.config.actions.render();
3994
- void this.config.actions.createAreaDraft(selection);
3995
- };
3996
- function handlePointerMove(event) {
3997
- if (!isActivePointer(event)) return;
3998
- event.preventDefault();
3999
- updateBox(event);
4000
- }
4001
- const handlePointerUp = (event) => {
4002
- finishAreaSelection(event);
4003
- };
4004
- const handlePointerCancel = (event) => {
4005
- if (!isActivePointer(event)) return;
4006
- releasePointerCapture(event);
4007
- removeDragListeners();
4008
- isDragging = false;
4009
- activePointerId = void 0;
4010
- };
4411
+ layer.className = "dfwr-draft-cancel-layer";
4412
+ layer.setAttribute("aria-hidden", "true");
4011
4413
  layer.addEventListener("pointerdown", (event) => {
4012
4414
  if (event.button !== 0) return;
4013
- event.preventDefault();
4014
- activePointerId = event.pointerId;
4015
- isDragging = true;
4016
- try {
4017
- layer.setPointerCapture(event.pointerId);
4018
- } catch {
4019
- }
4020
- const startPoint = toTargetPointFromHostEvent(
4021
- event,
4022
- this.config.getEnvironment()
4023
- );
4024
- startX = startPoint.x;
4025
- startY = startPoint.y;
4026
- updateBox(event);
4027
- addDragListeners();
4415
+ this.cancelDraft(event);
4028
4416
  });
4029
- layer.addEventListener("pointermove", handlePointerMove);
4030
- layer.addEventListener("pointerup", handlePointerUp);
4031
- layer.addEventListener("pointercancel", handlePointerCancel);
4032
4417
  return layer;
4033
4418
  }
4419
+ cancelDraft(event) {
4420
+ event?.preventDefault();
4421
+ event?.stopPropagation();
4422
+ event?.stopImmediatePropagation();
4423
+ this.config.actions.setModeState("idle");
4424
+ this.config.actions.clearDrafts();
4425
+ this.config.actions.setSelectingArea(false);
4426
+ this.config.actions.render();
4427
+ }
4034
4428
  };
4035
4429
 
4036
4430
  // src/core/web.review.kit.app.ts
@@ -4070,6 +4464,7 @@ var WebReviewKitApp = class {
4070
4464
  this.items = [];
4071
4465
  this.draftError = "";
4072
4466
  this.isCreatingItem = false;
4467
+ this.isCapturingViewport = false;
4073
4468
  this.isSelectingArea = false;
4074
4469
  this.handleKeyDown = (event) => {
4075
4470
  if (event.key === "Escape" && this.cancelMode()) {
@@ -4098,10 +4493,11 @@ var WebReviewKitApp = class {
4098
4493
  isOpen: this.isOpen,
4099
4494
  mode: this.mode,
4100
4495
  items: this.items,
4101
- noteDraft: this.noteDraft,
4496
+ domDraft: this.domDraft,
4102
4497
  areaDraft: this.areaDraft,
4103
4498
  draftError: this.draftError,
4104
4499
  isCreatingItem: this.isCreatingItem,
4500
+ isCapturingViewport: this.isCapturingViewport,
4105
4501
  isSelectingArea: this.isSelectingArea,
4106
4502
  highlightedItemId: this.highlightedItemId
4107
4503
  }),
@@ -4113,13 +4509,9 @@ var WebReviewKitApp = class {
4113
4509
  restoreItem: (item) => this.restoreItem(item),
4114
4510
  removeItem: (itemId) => this.adapter.remove(itemId),
4115
4511
  setModeState: (mode) => this.setModeState(mode),
4116
- clearDrafts: () => {
4117
- this.noteDraft = void 0;
4118
- this.areaDraft = void 0;
4119
- this.draftError = "";
4120
- },
4121
- setNoteDraft: (draft) => {
4122
- this.noteDraft = draft;
4512
+ clearDrafts: () => this.clearDrafts(),
4513
+ setDomDraft: (draft) => {
4514
+ this.domDraft = draft;
4123
4515
  this.draftError = "";
4124
4516
  },
4125
4517
  setAreaDraft: (draft) => {
@@ -4130,7 +4522,8 @@ var WebReviewKitApp = class {
4130
4522
  this.isSelectingArea = isSelectingArea;
4131
4523
  },
4132
4524
  createItem: (input) => this.createItem(input),
4133
- bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
4525
+ captureDomDraft: (input) => this.captureDomDraft(input),
4526
+ captureAreaDraft: (input) => this.captureAreaDraft(input),
4134
4527
  bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
4135
4528
  createAreaDraft: (selection) => this.createAreaDraft(selection)
4136
4529
  }
@@ -4152,6 +4545,7 @@ var WebReviewKitApp = class {
4152
4545
  }
4153
4546
  destroy() {
4154
4547
  this.view.clearDraftPreview();
4548
+ this.clearDrafts();
4155
4549
  document.removeEventListener("keydown", this.handleKeyDown, true);
4156
4550
  window.removeEventListener("scroll", this.handleViewportChange, true);
4157
4551
  window.removeEventListener("resize", this.handleViewportChange);
@@ -4171,8 +4565,7 @@ var WebReviewKitApp = class {
4171
4565
  close() {
4172
4566
  this.isOpen = false;
4173
4567
  this.setModeState("idle");
4174
- this.noteDraft = void 0;
4175
- this.areaDraft = void 0;
4568
+ this.clearDrafts();
4176
4569
  this.isSelectingArea = false;
4177
4570
  this.render();
4178
4571
  }
@@ -4188,8 +4581,7 @@ var WebReviewKitApp = class {
4188
4581
  this.isOpen = true;
4189
4582
  }
4190
4583
  this.setModeState(this.mode === mode ? "idle" : mode);
4191
- this.noteDraft = void 0;
4192
- this.areaDraft = void 0;
4584
+ this.clearDrafts();
4193
4585
  this.render();
4194
4586
  }
4195
4587
  async startElementReview(element, comment) {
@@ -4197,8 +4589,7 @@ var WebReviewKitApp = class {
4197
4589
  this.isOpen = true;
4198
4590
  }
4199
4591
  this.setModeState("element");
4200
- this.noteDraft = void 0;
4201
- this.areaDraft = void 0;
4592
+ this.clearDrafts();
4202
4593
  this.isSelectingArea = false;
4203
4594
  await this.bindElementDraftToElement(element, { comment });
4204
4595
  }
@@ -4223,6 +4614,20 @@ var WebReviewKitApp = class {
4223
4614
  this.hiddenItemIds = itemIds ? new Set(itemIds) : void 0;
4224
4615
  this.updateHiddenItemsStyle();
4225
4616
  }
4617
+ clearDrafts() {
4618
+ this.revokeDraftAttachmentPreviews(this.domDraft);
4619
+ this.revokeDraftAttachmentPreviews(this.areaDraft);
4620
+ this.domDraft = void 0;
4621
+ this.areaDraft = void 0;
4622
+ this.draftError = "";
4623
+ }
4624
+ revokeDraftAttachmentPreviews(draft) {
4625
+ draft?.attachments?.forEach((attachment) => {
4626
+ if (attachment.previewUrl) {
4627
+ URL.revokeObjectURL(attachment.previewUrl);
4628
+ }
4629
+ });
4630
+ }
4226
4631
  clearHighlightedItem() {
4227
4632
  if (!this.highlightedItemId) return;
4228
4633
  this.highlightedItemId = void 0;
@@ -4258,18 +4663,17 @@ var WebReviewKitApp = class {
4258
4663
  this.options.onModeChange?.(mode);
4259
4664
  }
4260
4665
  cancelMode() {
4261
- if (this.mode === "idle" && !this.noteDraft && !this.areaDraft && !this.isSelectingArea) {
4666
+ if (this.mode === "idle" && !this.domDraft && !this.areaDraft && !this.isSelectingArea) {
4262
4667
  return false;
4263
4668
  }
4264
4669
  this.setModeState("idle");
4265
- this.noteDraft = void 0;
4266
- this.areaDraft = void 0;
4670
+ this.clearDrafts();
4267
4671
  this.isSelectingArea = false;
4268
4672
  this.render();
4269
4673
  return true;
4270
4674
  }
4271
4675
  isDraftComposerFocused() {
4272
- if (!this.noteDraft && !this.areaDraft) return false;
4676
+ if (!this.domDraft && !this.areaDraft) return false;
4273
4677
  const composerHost = this.getEnvironment()?.composerHost;
4274
4678
  const activeElement = composerHost?.ownerDocument.activeElement;
4275
4679
  return Boolean(
@@ -4305,6 +4709,8 @@ var WebReviewKitApp = class {
4305
4709
  };
4306
4710
  const overlayRect = target.getOverlayRect?.() ?? rect;
4307
4711
  const composerHost = target.getComposerHost?.();
4712
+ const scaleX = target.window.innerWidth > 0 ? rect.width / target.window.innerWidth : 1;
4713
+ const scaleY = target.window.innerHeight > 0 ? rect.height / target.window.innerHeight : 1;
4308
4714
  return {
4309
4715
  window: target.window,
4310
4716
  document: target.document,
@@ -4314,13 +4720,16 @@ var WebReviewKitApp = class {
4314
4720
  width: rect.width,
4315
4721
  height: rect.height
4316
4722
  },
4723
+ scaleX,
4724
+ scaleY,
4317
4725
  overlayRect: {
4318
4726
  left: overlayRect.left,
4319
4727
  top: overlayRect.top,
4320
4728
  width: overlayRect.width,
4321
4729
  height: overlayRect.height
4322
4730
  },
4323
- composerHost
4731
+ composerHost,
4732
+ captureViewport: target.captureViewport
4324
4733
  };
4325
4734
  } catch {
4326
4735
  return void 0;
@@ -4343,32 +4752,6 @@ var WebReviewKitApp = class {
4343
4752
  if (!this.shadow) return;
4344
4753
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
4345
4754
  }
4346
- async bindNoteDraftToPoint(point, fields = {}) {
4347
- const environment = this.getEnvironment();
4348
- if (!environment) return;
4349
- const viewport = getViewportSize(environment);
4350
- const nextPoint = clampPoint(point, environment);
4351
- const draft = await this.withOverlayHidden(() => {
4352
- const selection = getPointSelection(nextPoint);
4353
- const anchor = getDomAnchor(
4354
- selection,
4355
- this.options.anchors?.attribute,
4356
- environment
4357
- );
4358
- const marker = {
4359
- viewport: roundPoint(nextPoint),
4360
- relative: anchor ? getRelativePoint(nextPoint, anchor, environment) : void 0
4361
- };
4362
- return {
4363
- viewport,
4364
- anchor,
4365
- marker,
4366
- ...fields
4367
- };
4368
- });
4369
- this.noteDraft = draft;
4370
- this.render();
4371
- }
4372
4755
  async bindElementDraftToPoint(point, fields = {}) {
4373
4756
  const environment = this.getEnvironment();
4374
4757
  if (!environment) return;
@@ -4417,7 +4800,7 @@ var WebReviewKitApp = class {
4417
4800
  previewElement
4418
4801
  };
4419
4802
  });
4420
- this.noteDraft = draft;
4803
+ this.domDraft = draft;
4421
4804
  this.render();
4422
4805
  }
4423
4806
  async bindElementDraftToElement(element, fields = {}) {
@@ -4458,7 +4841,7 @@ var WebReviewKitApp = class {
4458
4841
  };
4459
4842
  });
4460
4843
  if (!draft) return;
4461
- this.noteDraft = draft;
4844
+ this.domDraft = draft;
4462
4845
  this.render();
4463
4846
  }
4464
4847
  async createAreaDraft(selection) {
@@ -4471,16 +4854,27 @@ var WebReviewKitApp = class {
4471
4854
  try {
4472
4855
  const viewport = getViewportSize(environment);
4473
4856
  this.areaDraft = await this.withOverlayHidden(() => {
4474
- const marker = createSelectionCenterMarker(
4857
+ const anchorPoint = clampPoint(
4858
+ getSelectionCenter(selection),
4859
+ environment
4860
+ );
4861
+ const anchor = getDomAnchorFromPoint(
4862
+ anchorPoint,
4863
+ this.options.anchors?.attribute,
4864
+ environment
4865
+ );
4866
+ const marker = createSelectionStartMarker(
4475
4867
  selection,
4476
- void 0,
4868
+ anchor,
4477
4869
  environment
4478
4870
  );
4479
4871
  const reviewSelection = {
4480
- viewport: toPublicSelection(selection)
4872
+ viewport: toPublicSelection(selection),
4873
+ relative: anchor ? getRelativeSelection(selection, anchor, environment) : void 0
4481
4874
  };
4482
4875
  return {
4483
4876
  viewport,
4877
+ anchor,
4484
4878
  marker,
4485
4879
  selection: reviewSelection
4486
4880
  };
@@ -4501,6 +4895,127 @@ var WebReviewKitApp = class {
4501
4895
  this.root.style.display = previousDisplay;
4502
4896
  }
4503
4897
  }
4898
+ async captureDomDraft(input) {
4899
+ if (this.isCapturingViewport) return;
4900
+ const environment = this.getEnvironment();
4901
+ const draft = this.domDraft;
4902
+ if (!draft) return;
4903
+ if (!environment?.captureViewport) {
4904
+ this.draftError = "Viewport capture helper is not available.";
4905
+ this.render();
4906
+ return;
4907
+ }
4908
+ const captureInput = this.createViewportCaptureInput(
4909
+ environment,
4910
+ input,
4911
+ input.selection?.viewport
4912
+ );
4913
+ this.draftError = "";
4914
+ this.isCapturingViewport = true;
4915
+ this.render();
4916
+ try {
4917
+ const result = await environment.captureViewport(captureInput);
4918
+ const attachment = this.createCaptureDraftAttachment(result, captureInput);
4919
+ const currentDraft = this.domDraft ?? draft;
4920
+ this.domDraft = {
4921
+ ...currentDraft,
4922
+ attachments: [...currentDraft.attachments ?? [], attachment]
4923
+ };
4924
+ } catch (error) {
4925
+ this.draftError = this.getErrorMessage(
4926
+ error,
4927
+ "Failed to capture viewport."
4928
+ );
4929
+ } finally {
4930
+ this.isCapturingViewport = false;
4931
+ this.render();
4932
+ }
4933
+ }
4934
+ async captureAreaDraft(input) {
4935
+ if (this.isCapturingViewport) return;
4936
+ const environment = this.getEnvironment();
4937
+ const draft = this.areaDraft;
4938
+ if (!draft) return;
4939
+ if (!environment?.captureViewport) {
4940
+ this.draftError = "Viewport capture helper is not available.";
4941
+ this.render();
4942
+ return;
4943
+ }
4944
+ const captureInput = this.createViewportCaptureInput(
4945
+ environment,
4946
+ input,
4947
+ input.selection?.viewport
4948
+ );
4949
+ this.draftError = "";
4950
+ this.isCapturingViewport = true;
4951
+ this.render();
4952
+ try {
4953
+ const result = await environment.captureViewport(captureInput);
4954
+ const attachment = this.createCaptureDraftAttachment(result, captureInput);
4955
+ const currentDraft = this.areaDraft ?? draft;
4956
+ this.areaDraft = {
4957
+ ...currentDraft,
4958
+ attachments: [...currentDraft.attachments ?? [], attachment]
4959
+ };
4960
+ } catch (error) {
4961
+ this.draftError = this.getErrorMessage(
4962
+ error,
4963
+ "Failed to capture viewport."
4964
+ );
4965
+ } finally {
4966
+ this.isCapturingViewport = false;
4967
+ this.render();
4968
+ }
4969
+ }
4970
+ createViewportCaptureInput(environment, draft, captureRegion) {
4971
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4972
+ const viewport = draft.viewport ?? getViewportSize(environment);
4973
+ const routeKey = getRouteKey(environment);
4974
+ return {
4975
+ routeKey,
4976
+ pageUrl: getPageUrl(environment),
4977
+ originalUrl: getOriginalUrl(environment),
4978
+ viewport,
4979
+ captureRegion,
4980
+ devicePixelRatio: environment.window.devicePixelRatio || 1,
4981
+ scroll: {
4982
+ x: environment.window.scrollX,
4983
+ y: environment.window.scrollY
4984
+ },
4985
+ marker: draft.marker,
4986
+ selection: draft.selection,
4987
+ timestamp
4988
+ };
4989
+ }
4990
+ createCaptureDraftAttachment(result, input) {
4991
+ const mime = result.mime || result.file.type || "image/png";
4992
+ const name = result.name || `review-capture-${Date.now()}.png`;
4993
+ return {
4994
+ id: createId(),
4995
+ file: result.file,
4996
+ name,
4997
+ mime,
4998
+ size: result.file.size,
4999
+ kind: "capture",
5000
+ previewUrl: mime.startsWith("image/") ? URL.createObjectURL(result.file) : void 0,
5001
+ metadata: {
5002
+ ...result.metadata,
5003
+ source: "viewport-capture",
5004
+ target: "iframe",
5005
+ routeKey: input.routeKey,
5006
+ pageUrl: input.pageUrl,
5007
+ originalUrl: input.originalUrl,
5008
+ viewport: input.viewport,
5009
+ scroll: input.scroll,
5010
+ marker: input.marker,
5011
+ selection: input.selection,
5012
+ timestamp: input.timestamp,
5013
+ devicePixelRatio: input.devicePixelRatio,
5014
+ width: result.width,
5015
+ height: result.height
5016
+ }
5017
+ };
5018
+ }
4504
5019
  async createItem(input) {
4505
5020
  const environment = this.getEnvironment();
4506
5021
  if (!environment || this.isCreatingItem) return;
@@ -4544,24 +5059,61 @@ var WebReviewKitApp = class {
4544
5059
  this.isCreatingItem = true;
4545
5060
  this.render();
4546
5061
  try {
4547
- const createdItem = await this.adapter.create(item);
5062
+ const attachments = await this.uploadDraftAttachments(
5063
+ input.attachments,
5064
+ item
5065
+ );
5066
+ const itemWithAttachments = attachments.length > 0 ? { ...item, attachments } : item;
5067
+ const createdItem = await this.adapter.create(itemWithAttachments);
4548
5068
  this.setModeState("idle");
4549
- this.noteDraft = void 0;
4550
- this.areaDraft = void 0;
5069
+ this.clearDrafts();
4551
5070
  this.highlightItem(createdItem.id);
4552
5071
  await this.reload();
4553
5072
  await this.options.onCreateItem?.(createdItem);
4554
5073
  } catch (error) {
4555
- this.draftError = error instanceof Error ? error.message : "Failed to save QA.";
5074
+ this.draftError = this.getCreateItemErrorMessage(
5075
+ error,
5076
+ Boolean(input.attachments?.length)
5077
+ );
4556
5078
  } finally {
4557
5079
  this.isCreatingItem = false;
4558
5080
  this.render();
4559
5081
  }
4560
5082
  }
5083
+ async uploadDraftAttachments(attachments, item) {
5084
+ if (!attachments?.length) return [];
5085
+ const uploadAttachment = this.adapter.uploadAttachment;
5086
+ if (!uploadAttachment) {
5087
+ throw new Error("Attachment upload adapter is not configured.");
5088
+ }
5089
+ return Promise.all(
5090
+ attachments.map(
5091
+ (attachment) => uploadAttachment({
5092
+ file: attachment.file,
5093
+ name: attachment.name,
5094
+ mime: attachment.mime,
5095
+ kind: attachment.kind,
5096
+ item,
5097
+ metadata: attachment.metadata
5098
+ })
5099
+ )
5100
+ );
5101
+ }
5102
+ getCreateItemErrorMessage(error, wasUploadingAttachments) {
5103
+ const message = this.getErrorMessage(error, "Failed to save QA.");
5104
+ const reason = error && typeof error === "object" && "reason" in error && typeof error.reason === "string" ? ` (${error.reason})` : "";
5105
+ return wasUploadingAttachments && reason ? `Attachment upload failed${reason}: ${message}` : message;
5106
+ }
5107
+ getErrorMessage(error, fallback) {
5108
+ if (error instanceof Error) return error.message;
5109
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
5110
+ return error.message;
5111
+ }
5112
+ return fallback;
5113
+ }
4561
5114
  async restoreItem(item) {
4562
5115
  this.setModeState("idle");
4563
- this.noteDraft = void 0;
4564
- this.areaDraft = void 0;
5116
+ this.clearDrafts();
4565
5117
  if (this.options.onRestoreItem) {
4566
5118
  await this.options.onRestoreItem(item);
4567
5119
  return;
@@ -4614,15 +5166,22 @@ export {
4614
5166
  normalizeReviewItemStatus,
4615
5167
  localAdapter,
4616
5168
  DEFAULT_REVIEW_FIGMA_IMAGE_FORMAT,
5169
+ isPointInViewport,
5170
+ toHostPoint,
4617
5171
  clamp,
5172
+ isHotkey,
5173
+ getHotkeyActionKey,
4618
5174
  DEFAULT_REVIEW_VIEWPORTS,
4619
5175
  findReviewViewportPreset,
4620
5176
  getReviewViewportScope,
4621
5177
  getReviewItemScope,
4622
5178
  getReviewItemScopeLabel,
4623
5179
  getNumberedReviewItems,
5180
+ getBoundMarkerPoint,
5181
+ getItemHighlightSelection,
5182
+ shouldShowMarkerForScope,
4624
5183
  runWithAutoScrollBehavior,
4625
5184
  reviewTypographyTokens,
4626
5185
  createWebReviewKit
4627
5186
  };
4628
- //# sourceMappingURL=chunk-RPVLRULC.js.map
5187
+ //# sourceMappingURL=chunk-4ZP7B7R6.js.map