@designfever/web-review-kit 0.7.3 → 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.
package/dist/index.cjs CHANGED
@@ -157,8 +157,8 @@ function localAdapter(options = {}) {
157
157
  function normalizeStoredReviewItem(value) {
158
158
  if (!value || typeof value !== "object") return void 0;
159
159
  const raw = value;
160
- const kind = raw.kind === "text" ? "note" : raw.kind === "capture" ? "area" : raw.kind;
161
- if (kind !== "note" && kind !== "area") return void 0;
160
+ const kind = raw.kind === "capture" ? "area" : raw.kind;
161
+ if (kind !== "dom" && kind !== "area") return void 0;
162
162
  const { screenshot: _screenshot, reviewNumber: _reviewNumber, ...item } = raw;
163
163
  if (kind === raw.kind && _screenshot === void 0 && _reviewNumber === void 0) {
164
164
  return raw;
@@ -333,6 +333,8 @@ function rowToReviewItem(row, options) {
333
333
  const routeKey = row.route_key || item.routeKey || item.normalizedPath || "/";
334
334
  const viewport = item.viewport ?? { width: 390, height: 720 };
335
335
  const now = (/* @__PURE__ */ new Date()).toISOString();
336
+ const kind = normalizeReviewItemKind(item);
337
+ if (!kind) return null;
336
338
  return {
337
339
  ...item,
338
340
  id: row.id,
@@ -341,7 +343,7 @@ function rowToReviewItem(row, options) {
341
343
  routeKey,
342
344
  pageUrl: item.pageUrl || toAbsoluteReviewUrl(routeKey),
343
345
  normalizedPath: item.normalizedPath || routeKey,
344
- kind: item.kind === "area" ? "area" : "note",
346
+ kind,
345
347
  comment: item.comment || "",
346
348
  status,
347
349
  viewport,
@@ -358,6 +360,10 @@ function rowToReviewItem(row, options) {
358
360
  updatedAt: row.updated_at ?? item.updatedAt ?? now
359
361
  };
360
362
  }
363
+ function normalizeReviewItemKind(item) {
364
+ if (item.kind === "area" || item.kind === "dom") return item.kind;
365
+ return null;
366
+ }
361
367
  async function unwrapResponse(request, label) {
362
368
  const { data, error } = await request;
363
369
  if (error) {
@@ -1321,26 +1327,34 @@ function getPopoverBounds(environment) {
1321
1327
  }
1322
1328
  return environment.overlayRect;
1323
1329
  }
1330
+ function getEnvironmentScale(environment) {
1331
+ const scaleX = typeof environment.scaleX === "number" && Number.isFinite(environment.scaleX) && environment.scaleX > 0 ? environment.scaleX : 1;
1332
+ const scaleY = typeof environment.scaleY === "number" && Number.isFinite(environment.scaleY) && environment.scaleY > 0 ? environment.scaleY : 1;
1333
+ return { scaleX, scaleY };
1334
+ }
1324
1335
  function toHostPoint(point, environment) {
1325
1336
  if (!environment) return point;
1337
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
1326
1338
  return {
1327
- x: point.x + environment.viewportRect.left,
1328
- y: point.y + environment.viewportRect.top
1339
+ x: point.x * scaleX + environment.viewportRect.left,
1340
+ y: point.y * scaleY + environment.viewportRect.top
1329
1341
  };
1330
1342
  }
1331
1343
  function toHostSelection(selection, environment) {
1344
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
1332
1345
  return {
1333
- left: selection.left + environment.viewportRect.left,
1334
- top: selection.top + environment.viewportRect.top,
1335
- width: selection.width,
1336
- height: selection.height
1346
+ left: selection.left * scaleX + environment.viewportRect.left,
1347
+ top: selection.top * scaleY + environment.viewportRect.top,
1348
+ width: selection.width * scaleX,
1349
+ height: selection.height * scaleY
1337
1350
  };
1338
1351
  }
1339
1352
  function toTargetPoint(point, environment) {
1340
1353
  if (!environment) return point;
1354
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
1341
1355
  return {
1342
- x: point.x - environment.viewportRect.left,
1343
- y: point.y - environment.viewportRect.top
1356
+ x: (point.x - environment.viewportRect.left) / scaleX,
1357
+ y: (point.y - environment.viewportRect.top) / scaleY
1344
1358
  };
1345
1359
  }
1346
1360
  function toTargetPointFromHostEvent(event, environment) {
@@ -1389,11 +1403,6 @@ var SEMANTIC_ANCHOR_ATTRIBUTES = [
1389
1403
  "name",
1390
1404
  "href"
1391
1405
  ];
1392
- function getDomAnchor(selection, configuredAttribute = "data-qa-id", environment) {
1393
- const x = selection.left + selection.width / 2;
1394
- const y = selection.top + selection.height / 2;
1395
- return getDomAnchorFromPoint({ x, y }, configuredAttribute, environment);
1396
- }
1397
1406
  function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", environment) {
1398
1407
  const target = environment.document.elementFromPoint(point.x, point.y);
1399
1408
  if (!target) return void 0;
@@ -1943,6 +1952,34 @@ function createId() {
1943
1952
  }
1944
1953
 
1945
1954
  // src/core/hotkey.ts
1955
+ var HOTKEY_KEY_ALIASES = {
1956
+ q: ["\u3142", "\u3143"],
1957
+ w: ["\u3148", "\u3149"],
1958
+ e: ["\u3137", "\u3138"],
1959
+ r: ["\u3131", "\u3132"],
1960
+ t: ["\u3145", "\u3146"],
1961
+ y: ["\u315B"],
1962
+ u: ["\u3155"],
1963
+ i: ["\u3151"],
1964
+ o: ["\u3150", "\u3152"],
1965
+ p: ["\u3154", "\u3156"],
1966
+ a: ["\u3141"],
1967
+ s: ["\u3134"],
1968
+ d: ["\u3147"],
1969
+ f: ["\u3139"],
1970
+ g: ["\u314E"],
1971
+ h: ["\u3157"],
1972
+ j: ["\u3153"],
1973
+ k: ["\u314F"],
1974
+ l: ["\u3163"],
1975
+ z: ["\u314B"],
1976
+ x: ["\u314C"],
1977
+ c: ["\u314A"],
1978
+ v: ["\u314D"],
1979
+ b: ["\u3160"],
1980
+ n: ["\u315C"],
1981
+ m: ["\u3161"]
1982
+ };
1946
1983
  function isHotkey(event, hotkey) {
1947
1984
  const parts = hotkey.split("+").map((part) => part.trim().toLowerCase()).filter(Boolean);
1948
1985
  const key = parts.find(
@@ -1963,10 +2000,7 @@ function isHotkeyKey(event, key) {
1963
2000
  const normalizedKey = key.toLowerCase();
1964
2001
  if (event.key.toLowerCase() === normalizedKey) return true;
1965
2002
  if (getHotkeyCode(normalizedKey) === event.code) return true;
1966
- const aliases = {
1967
- q: ["\u3142", "\u3143"]
1968
- };
1969
- return aliases[normalizedKey]?.includes(event.key) ?? false;
2003
+ return HOTKEY_KEY_ALIASES[normalizedKey]?.includes(event.key) ?? false;
1970
2004
  }
1971
2005
  function getHotkeyCode(key) {
1972
2006
  if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
@@ -2008,8 +2042,7 @@ var REVIEW_SCOPE_LABELS = {
2008
2042
  mobile: "Mobile",
2009
2043
  tablet: "Tablet",
2010
2044
  desktop: "Desktop",
2011
- wide: "Wide",
2012
- dom: "Element"
2045
+ wide: "Wide"
2013
2046
  };
2014
2047
  var normalizeReviewItemScope = (value) => {
2015
2048
  if (value === "element") return "dom";
@@ -2055,7 +2088,6 @@ function getReviewItemScope(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
2055
2088
  }
2056
2089
  function getReviewItemScopeLabel(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
2057
2090
  const scope = getReviewItemScope(item, presets);
2058
- if (scope === "dom") return REVIEW_SCOPE_LABELS.dom;
2059
2091
  const preset = findReviewViewportPreset(item.viewport, presets);
2060
2092
  return preset.label || REVIEW_SCOPE_LABELS[scope];
2061
2093
  }
@@ -2097,7 +2129,7 @@ function normalizeReviewNumber(value) {
2097
2129
  function getBoundMarkerPoint(item, environment) {
2098
2130
  const marker = getItemMarker(item);
2099
2131
  if (!marker) return void 0;
2100
- if (item.kind !== "area" && item.anchor && marker.relative) {
2132
+ if (item.anchor && marker.relative) {
2101
2133
  const resolved = resolveAnchorElement(item.anchor, environment);
2102
2134
  const element = resolved?.element;
2103
2135
  if (element) {
@@ -2145,27 +2177,25 @@ function getItemHighlightSelection(item, environment) {
2145
2177
  environment
2146
2178
  );
2147
2179
  }
2148
- return getVisibleHighlightSelection(
2149
- [
2150
- getAnchorHighlightSelection(item, environment),
2151
- getBoundSelection(item, environment),
2152
- getPointHighlightSelection(item, environment)
2153
- ],
2154
- environment
2155
- );
2180
+ return void 0;
2156
2181
  }
2157
2182
  function getReviewItemHighlightMode(item) {
2158
2183
  if (isDomReviewItem(item)) return "dom";
2159
- if (item.kind === "area") return "area";
2160
- return "note";
2184
+ return "area";
2161
2185
  }
2162
2186
  function getItemMarker(item) {
2163
2187
  if (item.marker) return item.marker;
2164
2188
  const selection = getItemSelection(item);
2165
2189
  if (!selection?.viewport) return void 0;
2166
2190
  return {
2167
- viewport: roundPoint(getSelectionCenter(selection.viewport)),
2168
- relative: selection.relative ? roundPoint(getSelectionCenter(selection.relative)) : void 0
2191
+ viewport: roundPoint({
2192
+ x: selection.viewport.x,
2193
+ y: selection.viewport.y
2194
+ }),
2195
+ relative: selection.relative ? roundPoint({
2196
+ x: selection.relative.x,
2197
+ y: selection.relative.y
2198
+ }) : void 0
2169
2199
  };
2170
2200
  }
2171
2201
  function getItemSelection(item) {
@@ -2184,17 +2214,20 @@ function getItemSelection(item) {
2184
2214
  function shouldShowMarkerForScope(scope, currentScope) {
2185
2215
  return scope === currentScope;
2186
2216
  }
2187
- function createSelectionCenterMarker(selection, anchor, environment) {
2188
- const centerPoint = getSelectionCenter(selection);
2217
+ function createSelectionStartMarker(selection, anchor, environment) {
2218
+ const startPoint = {
2219
+ x: selection.left,
2220
+ y: selection.top
2221
+ };
2189
2222
  return {
2190
- viewport: roundPoint(centerPoint),
2191
- relative: anchor ? getRelativePoint(centerPoint, anchor, environment) : void 0
2223
+ viewport: roundPoint(startPoint),
2224
+ relative: anchor ? getRelativePoint(startPoint, anchor, environment) : void 0
2192
2225
  };
2193
2226
  }
2194
2227
  function getBoundSelection(item, environment) {
2195
2228
  const selection = getItemSelection(item);
2196
2229
  if (!selection?.viewport) return void 0;
2197
- if (item.kind !== "area" && item.anchor && selection.relative) {
2230
+ if (item.anchor && selection.relative) {
2198
2231
  const resolved = resolveAnchorElement(item.anchor, environment);
2199
2232
  const element = resolved?.element;
2200
2233
  if (element) {
@@ -2256,7 +2289,7 @@ function getVisibleHighlightSelection(candidates, environment) {
2256
2289
  );
2257
2290
  }
2258
2291
  function isDomReviewItem(item) {
2259
- return item.scope === "dom" || item.kind === "note" && Boolean(item.anchor && getItemSelection(item));
2292
+ return item.kind === "dom" || item.scope === "dom";
2260
2293
  }
2261
2294
 
2262
2295
  // src/core/scroll.ts
@@ -2353,6 +2386,117 @@ function getAdjustedDraftSelection(selection, draft, presets) {
2353
2386
  };
2354
2387
  }
2355
2388
 
2389
+ // src/core/draft.preview.ts
2390
+ var DraftPreviewController = class {
2391
+ constructor(config) {
2392
+ this.config = config;
2393
+ }
2394
+ clear() {
2395
+ if (!this.snapshot) return;
2396
+ const { element, clone, visibility } = this.snapshot;
2397
+ clone.remove();
2398
+ element.style.visibility = visibility;
2399
+ this.snapshot = void 0;
2400
+ }
2401
+ sync(draft) {
2402
+ const environment = this.config.getEnvironment();
2403
+ if (!draft || !environment || !this.config.hasAdjustment(draft)) {
2404
+ this.clear();
2405
+ return;
2406
+ }
2407
+ const element = this.getStyleableDraftElement(draft, environment);
2408
+ if (!element) {
2409
+ this.clear();
2410
+ return;
2411
+ }
2412
+ if (this.snapshot?.element !== element) {
2413
+ this.clear();
2414
+ }
2415
+ if (!this.snapshot) {
2416
+ const computedStyle = environment.window.getComputedStyle(element);
2417
+ const clone = element.cloneNode(true);
2418
+ removeDuplicateIds(clone);
2419
+ copyComputedStyle(element, clone, environment);
2420
+ positionDraftPreviewClone(clone, element, computedStyle);
2421
+ environment.document.body?.appendChild(clone);
2422
+ this.snapshot = {
2423
+ element,
2424
+ clone,
2425
+ visibility: element.style.visibility
2426
+ };
2427
+ element.style.visibility = "hidden";
2428
+ }
2429
+ const metrics = this.config.getMetrics(draft);
2430
+ const translate = `translate(${toCssNumber(metrics.cssX)}px, ${toCssNumber(
2431
+ metrics.cssY
2432
+ )}px)`;
2433
+ const scale = metrics.scaleFactor === 1 ? "" : `scale(${toCssNumber(metrics.scaleFactor)})`;
2434
+ this.snapshot.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
2435
+ }
2436
+ getStyleableDraftElement(draft, environment) {
2437
+ if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2438
+ return draft.previewElement;
2439
+ }
2440
+ if (!draft.anchor) return void 0;
2441
+ const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2442
+ const element = resolveAnchorElement(
2443
+ draft.anchor,
2444
+ environment,
2445
+ preferredSelection
2446
+ )?.element;
2447
+ if (!element) return void 0;
2448
+ if ("style" in element) return element;
2449
+ return void 0;
2450
+ }
2451
+ };
2452
+ function positionDraftPreviewClone(clone, element, computedStyle) {
2453
+ const rect = element.getBoundingClientRect();
2454
+ clone.setAttribute("data-dfwr-adjust-preview", "true");
2455
+ clone.setAttribute("aria-hidden", "true");
2456
+ clone.style.position = "fixed";
2457
+ clone.style.left = `${toCssNumber(rect.left)}px`;
2458
+ clone.style.top = `${toCssNumber(rect.top)}px`;
2459
+ clone.style.right = "auto";
2460
+ clone.style.bottom = "auto";
2461
+ clone.style.width = `${toCssNumber(rect.width)}px`;
2462
+ clone.style.height = `${toCssNumber(rect.height)}px`;
2463
+ clone.style.maxWidth = "none";
2464
+ clone.style.maxHeight = "none";
2465
+ clone.style.margin = "0";
2466
+ clone.style.boxSizing = "border-box";
2467
+ clone.style.display = getDraftPreviewDisplay(computedStyle.display);
2468
+ clone.style.zIndex = "2147483646";
2469
+ clone.style.pointerEvents = "none";
2470
+ clone.style.transition = "none";
2471
+ clone.style.willChange = "transform";
2472
+ clone.style.transformOrigin = "top left";
2473
+ clone.style.transform = "none";
2474
+ }
2475
+ function getDraftPreviewDisplay(display) {
2476
+ if (display === "inline" || display === "contents") return "inline-block";
2477
+ return display || "block";
2478
+ }
2479
+ function copyComputedStyle(element, clone, environment) {
2480
+ const computedStyle = environment.window.getComputedStyle(element);
2481
+ for (let index = 0; index < computedStyle.length; index += 1) {
2482
+ const property = computedStyle.item(index);
2483
+ clone.style.setProperty(
2484
+ property,
2485
+ computedStyle.getPropertyValue(property),
2486
+ computedStyle.getPropertyPriority(property)
2487
+ );
2488
+ }
2489
+ }
2490
+ function removeDuplicateIds(element) {
2491
+ element.removeAttribute("id");
2492
+ element.querySelectorAll("[id]").forEach((child) => {
2493
+ child.removeAttribute("id");
2494
+ });
2495
+ }
2496
+ function toCssNumber(value) {
2497
+ return Math.round(value * 1e3) / 1e3;
2498
+ }
2499
+
2356
2500
  // src/core/typography.tokens.ts
2357
2501
  var reviewTypographyTokens = `
2358
2502
  --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -2676,6 +2820,36 @@ function createStyleElement() {
2676
2820
  --dfwr-item-color-rgb: 255, 143, 97;
2677
2821
  }
2678
2822
 
2823
+ .dfwr-item-target-highlight.is-scope-mobile,
2824
+ .dfwr-item-target-label.is-scope-mobile {
2825
+ --dfwr-item-color: #7cc7ff;
2826
+ --dfwr-item-color-rgb: 124, 199, 255;
2827
+ }
2828
+
2829
+ .dfwr-item-target-highlight.is-scope-tablet,
2830
+ .dfwr-item-target-label.is-scope-tablet {
2831
+ --dfwr-item-color: #63d7c7;
2832
+ --dfwr-item-color-rgb: 99, 215, 199;
2833
+ }
2834
+
2835
+ .dfwr-item-target-highlight.is-scope-desktop,
2836
+ .dfwr-item-target-label.is-scope-desktop {
2837
+ --dfwr-item-color: #f3b75f;
2838
+ --dfwr-item-color-rgb: 243, 183, 95;
2839
+ }
2840
+
2841
+ .dfwr-item-target-highlight.is-scope-wide,
2842
+ .dfwr-item-target-label.is-scope-wide {
2843
+ --dfwr-item-color: #c99cff;
2844
+ --dfwr-item-color-rgb: 201, 156, 255;
2845
+ }
2846
+
2847
+ .dfwr-item-target-highlight.is-scope-dom,
2848
+ .dfwr-item-target-label.is-scope-dom {
2849
+ --dfwr-item-color: #ff8f61;
2850
+ --dfwr-item-color-rgb: 255, 143, 97;
2851
+ }
2852
+
2679
2853
  .dfwr-item-target-highlight {
2680
2854
  position: fixed;
2681
2855
  z-index: 2;
@@ -2684,7 +2858,6 @@ function createStyleElement() {
2684
2858
  background: rgba(var(--dfwr-item-color-rgb), 0.08);
2685
2859
  box-shadow:
2686
2860
  0 0 0 1px rgba(31, 36, 40, 0.78),
2687
- 0 0 0 9999px rgba(0, 0, 0, 0.08),
2688
2861
  0 12px 30px rgba(0, 0, 0, 0.24);
2689
2862
  pointer-events: none;
2690
2863
  }
@@ -2768,89 +2941,6 @@ function createStyleElement() {
2768
2941
  border-style: dashed;
2769
2942
  }
2770
2943
 
2771
- .dfwr-bound-marker.is-note-callout,
2772
- .dfwr-bound-marker.is-note-callout.is-highlighted {
2773
- --dfwr-scope: #7cc7ff;
2774
- --dfwr-scope-rgb: 124, 199, 255;
2775
- min-width: 0;
2776
- width: 0;
2777
- height: 0;
2778
- padding: 0;
2779
- transform: none;
2780
- border: 0;
2781
- border-radius: 0;
2782
- background: transparent;
2783
- box-shadow: none;
2784
- color: var(--dfwr-scope);
2785
- animation: none;
2786
- overflow: visible;
2787
- }
2788
-
2789
- .dfwr-bound-marker.is-note-callout::before {
2790
- content: "";
2791
- position: absolute;
2792
- left: 0;
2793
- top: 0;
2794
- width: 8px;
2795
- height: 8px;
2796
- transform: translate(-50%, -50%);
2797
- border: 2px solid #111820;
2798
- border-radius: var(--df-review-radius-pill);
2799
- background: var(--dfwr-scope);
2800
- box-shadow:
2801
- 0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.22),
2802
- 0 6px 16px rgba(0, 0, 0, 0.28);
2803
- }
2804
-
2805
- .dfwr-bound-marker.is-note-callout.is-highlighted::before {
2806
- animation: dfwr-note-dot-pulse 1000ms ease-in-out infinite;
2807
- }
2808
-
2809
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon {
2810
- position: absolute;
2811
- left: 0;
2812
- top: 0;
2813
- width: 31px;
2814
- height: 2px;
2815
- transform: rotate(-42deg);
2816
- transform-origin: left center;
2817
- border-radius: var(--df-review-radius-pill);
2818
- background: currentColor;
2819
- opacity: 1;
2820
- }
2821
-
2822
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::before,
2823
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::after {
2824
- display: none;
2825
- }
2826
-
2827
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-number {
2828
- position: absolute;
2829
- left: 24px;
2830
- top: -41px;
2831
- display: inline-flex;
2832
- align-items: center;
2833
- justify-content: center;
2834
- min-width: 28px;
2835
- height: 20px;
2836
- padding: 0 7px;
2837
- border: 1px solid var(--dfwr-scope);
2838
- border-radius: 4px;
2839
- background: var(--dfwr-scope);
2840
- box-shadow:
2841
- 0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.18),
2842
- 0 8px 18px rgba(0, 0, 0, 0.28);
2843
- color: #111820;
2844
- text-align: center;
2845
- line-height: 1;
2846
- white-space: nowrap;
2847
- }
2848
-
2849
- .dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-icon,
2850
- .dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-number {
2851
- animation: dfwr-selected-blink 1000ms ease-in-out infinite;
2852
- }
2853
-
2854
2944
  .dfwr-area-preview-layer .dfwr-bound-marker {
2855
2945
  border-color: #63d7c7;
2856
2946
  background: var(--df-review-color-panel);
@@ -2921,14 +3011,14 @@ function createStyleElement() {
2921
3011
  line-height: 1;
2922
3012
  }
2923
3013
 
2924
- .dfwr-note-draft {
3014
+ .dfwr-dom-draft {
2925
3015
  position: fixed;
2926
3016
  inset: 0;
2927
3017
  z-index: 4;
2928
3018
  pointer-events: none;
2929
3019
  }
2930
3020
 
2931
- .dfwr-note-pin {
3021
+ .dfwr-dom-pin {
2932
3022
  appearance: none;
2933
3023
  position: fixed;
2934
3024
  z-index: 5;
@@ -2946,11 +3036,11 @@ function createStyleElement() {
2946
3036
  pointer-events: auto;
2947
3037
  }
2948
3038
 
2949
- .dfwr-note-pin:active {
3039
+ .dfwr-dom-pin:active {
2950
3040
  cursor: grabbing;
2951
3041
  }
2952
3042
 
2953
- .dfwr-note-popover {
3043
+ .dfwr-dom-popover {
2954
3044
  position: fixed;
2955
3045
  z-index: 4;
2956
3046
  width: min(320px, calc(100vw - 24px));
@@ -2963,14 +3053,14 @@ function createStyleElement() {
2963
3053
  box-shadow: var(--df-review-shadow-popover);
2964
3054
  }
2965
3055
 
2966
- .dfwr-note-popover.is-composer,
3056
+ .dfwr-dom-popover.is-composer,
2967
3057
  .dfwr-area-draft.is-composer {
2968
3058
  max-height: min(360px, calc(100vh - 32px));
2969
3059
  overflow: auto;
2970
3060
  border-color: rgba(99, 215, 199, 0.56);
2971
3061
  }
2972
3062
 
2973
- .dfwr-shell.is-docked-composer .dfwr-note-popover.is-docked-composer,
3063
+ .dfwr-shell.is-docked-composer .dfwr-dom-popover.is-docked-composer,
2974
3064
  .dfwr-shell.is-docked-composer .dfwr-area-draft.is-docked-composer {
2975
3065
  position: relative;
2976
3066
  left: auto;
@@ -2984,7 +3074,7 @@ function createStyleElement() {
2984
3074
  min-height: 184px;
2985
3075
  }
2986
3076
 
2987
- .dfwr-note-popover.is-dragging,
3077
+ .dfwr-dom-popover.is-dragging,
2988
3078
  .dfwr-area-draft.is-dragging {
2989
3079
  user-select: none;
2990
3080
  }
@@ -3028,7 +3118,7 @@ function createStyleElement() {
3028
3118
  box-shadow: var(--df-review-shadow-popover);
3029
3119
  }
3030
3120
 
3031
- .dfwr-note-popover .dfwr-actions {
3121
+ .dfwr-dom-popover .dfwr-actions {
3032
3122
  padding: 0;
3033
3123
  }
3034
3124
 
@@ -3076,14 +3166,6 @@ function createStyleElement() {
3076
3166
  height: 18px;
3077
3167
  }
3078
3168
 
3079
- .dfwr-note-actions {
3080
- justify-content: flex-end;
3081
- }
3082
-
3083
- .dfwr-note-actions .dfwr-button:first-child {
3084
- margin-right: auto;
3085
- }
3086
-
3087
3169
  .dfwr-area-draft .dfwr-actions {
3088
3170
  padding: 0;
3089
3171
  }
@@ -3101,6 +3183,82 @@ function createStyleElement() {
3101
3183
  overflow-wrap: anywhere;
3102
3184
  }
3103
3185
 
3186
+ .dfwr-attachment-queue {
3187
+ display: grid;
3188
+ gap: 8px;
3189
+ min-width: 0;
3190
+ }
3191
+
3192
+ .dfwr-attachment-label {
3193
+ color: var(--df-review-color-text-muted);
3194
+ font-size: var(--df-review-font-size-xs);
3195
+ line-height: 1.35;
3196
+ }
3197
+
3198
+ .dfwr-attachment-list {
3199
+ display: grid;
3200
+ gap: 8px;
3201
+ }
3202
+
3203
+ .dfwr-attachment-item {
3204
+ display: grid;
3205
+ grid-template-columns: 42px minmax(0, 1fr) auto;
3206
+ align-items: center;
3207
+ gap: 8px;
3208
+ min-width: 0;
3209
+ padding: 6px;
3210
+ border: 1px solid rgba(255, 255, 255, 0.12);
3211
+ border-radius: var(--df-review-radius-sm);
3212
+ background: rgba(255, 255, 255, 0.04);
3213
+ }
3214
+
3215
+ .dfwr-attachment-thumb {
3216
+ display: block;
3217
+ width: 42px;
3218
+ height: 42px;
3219
+ object-fit: cover;
3220
+ border-radius: var(--df-review-radius-xs);
3221
+ background: var(--df-review-color-panel-strong);
3222
+ }
3223
+
3224
+ .dfwr-attachment-thumb.is-file {
3225
+ display: inline-flex;
3226
+ align-items: center;
3227
+ justify-content: center;
3228
+ color: var(--df-review-color-text-muted);
3229
+ font-size: var(--df-review-font-size-xs);
3230
+ font-weight: var(--df-review-font-weight-emphasis);
3231
+ }
3232
+
3233
+ .dfwr-attachment-name {
3234
+ min-width: 0;
3235
+ overflow: hidden;
3236
+ color: var(--df-review-color-text);
3237
+ font-size: var(--df-review-font-size-sm);
3238
+ line-height: 1.35;
3239
+ text-overflow: ellipsis;
3240
+ white-space: nowrap;
3241
+ }
3242
+
3243
+ .dfwr-attachment-remove {
3244
+ appearance: none;
3245
+ min-height: 28px;
3246
+ padding: 0 8px;
3247
+ border: 1px solid var(--df-review-color-border-strong);
3248
+ border-radius: var(--df-review-radius-sm);
3249
+ color: var(--df-review-color-text-muted);
3250
+ background: var(--df-review-color-control);
3251
+ cursor: pointer;
3252
+ font: inherit;
3253
+ font-size: var(--df-review-font-size-xs);
3254
+ line-height: 1;
3255
+ }
3256
+
3257
+ .dfwr-attachment-remove:hover {
3258
+ color: var(--df-review-color-text);
3259
+ background: var(--df-review-color-control-hover);
3260
+ }
3261
+
3104
3262
  .dfwr-input,
3105
3263
  .dfwr-select,
3106
3264
  .dfwr-textarea {
@@ -3403,18 +3561,6 @@ function createStyleElement() {
3403
3561
  }
3404
3562
  }
3405
3563
 
3406
- @keyframes dfwr-note-dot-pulse {
3407
- 0% {
3408
- transform: translate(-50%, -50%) scale(0.88);
3409
- }
3410
- 45% {
3411
- transform: translate(-50%, -50%) scale(1.3);
3412
- }
3413
- 100% {
3414
- transform: translate(-50%, -50%) scale(1);
3415
- }
3416
- }
3417
-
3418
3564
  @keyframes dfwr-selected-blink {
3419
3565
  0%,
3420
3566
  100% {
@@ -3463,1071 +3609,848 @@ function createStyleElement() {
3463
3609
  return style;
3464
3610
  }
3465
3611
 
3466
- // src/core/review/format.ts
3467
- function formatNoteDraftMeta(draft) {
3468
- const parts = [
3469
- `viewport ${formatSize(draft.viewport)}`,
3470
- `point ${formatPoint(draft.marker.viewport)}`
3471
- ];
3472
- if (draft.anchor) {
3473
- parts.push(formatAnchorMeta(draft.anchor));
3474
- }
3475
- return parts.join(" / ");
3476
- }
3477
- function formatItemMeta(item) {
3478
- const parts = [formatDate(item.createdAt)];
3479
- const marker = getItemMarker(item);
3480
- const selection = getItemSelection(item);
3481
- if (item.viewport) {
3482
- parts.push(`viewport ${formatSize(item.viewport)}`);
3483
- }
3484
- if (marker) {
3485
- parts.push(`point ${formatPoint(marker.viewport)}`);
3486
- }
3487
- if (selection) {
3488
- parts.push(`rect ${formatSelection(selection.viewport)}`);
3489
- }
3490
- if (item.anchor) {
3491
- parts.push(formatAnchorMeta(item.anchor));
3492
- }
3493
- return parts.join(" / ");
3494
- }
3495
- function formatDate(value) {
3496
- const date = new Date(value);
3497
- if (Number.isNaN(date.getTime())) return value;
3498
- return date.toLocaleString(void 0, {
3499
- month: "2-digit",
3500
- day: "2-digit",
3501
- hour: "2-digit",
3502
- minute: "2-digit"
3612
+ // src/core/draft.attachments.ts
3613
+ function attachDraftImagePasteQueue(textarea, options) {
3614
+ textarea.addEventListener("paste", (event) => {
3615
+ const imageFiles = getClipboardImageFiles(event.clipboardData);
3616
+ if (imageFiles.length === 0) return;
3617
+ event.preventDefault();
3618
+ const text = event.clipboardData?.getData("text/plain");
3619
+ if (text) {
3620
+ insertTextAtTextareaSelection(textarea, text);
3621
+ options.onCommentChange(textarea.value);
3622
+ }
3623
+ const attachments = imageFiles.map(
3624
+ (file, index) => createDraftImageAttachment(file, index)
3625
+ );
3626
+ options.onAttachmentsChange([
3627
+ ...options.getAttachments() ?? [],
3628
+ ...attachments
3629
+ ]);
3630
+ options.onPasteComplete();
3503
3631
  });
3504
3632
  }
3505
- function formatSize(size) {
3506
- return `${Math.round(size.width)}x${Math.round(size.height)}`;
3633
+ function createDraftAttachmentQueue(ownerDocument, attachments, onRemove) {
3634
+ if (!attachments?.length) return void 0;
3635
+ const queue = ownerDocument.createElement("div");
3636
+ queue.className = "dfwr-attachment-queue";
3637
+ const label = ownerDocument.createElement("div");
3638
+ label.className = "dfwr-attachment-label";
3639
+ label.textContent = `Attachments (${attachments.length})`;
3640
+ const list = ownerDocument.createElement("div");
3641
+ list.className = "dfwr-attachment-list";
3642
+ attachments.forEach((attachment) => {
3643
+ const item = ownerDocument.createElement("div");
3644
+ item.className = "dfwr-attachment-item";
3645
+ const preview = createDraftAttachmentPreview(ownerDocument, attachment);
3646
+ const name = ownerDocument.createElement("div");
3647
+ name.className = "dfwr-attachment-name";
3648
+ name.textContent = attachment.name;
3649
+ name.title = attachment.name;
3650
+ const remove = ownerDocument.createElement("button");
3651
+ remove.className = "dfwr-attachment-remove";
3652
+ remove.type = "button";
3653
+ remove.textContent = "Remove";
3654
+ remove.setAttribute("aria-label", `Remove ${attachment.name}`);
3655
+ remove.addEventListener("click", (event) => {
3656
+ event.preventDefault();
3657
+ event.stopPropagation();
3658
+ onRemove(attachment.id);
3659
+ });
3660
+ item.append(preview, name, remove);
3661
+ list.append(item);
3662
+ });
3663
+ queue.append(label, list);
3664
+ return queue;
3665
+ }
3666
+ function removeDraftAttachment(attachments, attachmentId) {
3667
+ if (!attachments?.length) return [];
3668
+ const removed = attachments.find((attachment) => attachment.id === attachmentId);
3669
+ if (removed?.previewUrl) {
3670
+ URL.revokeObjectURL(removed.previewUrl);
3671
+ }
3672
+ return attachments.filter((attachment) => attachment.id !== attachmentId);
3673
+ }
3674
+ function getClipboardImageFiles(data) {
3675
+ if (!data) return [];
3676
+ const itemFiles = Array.from(data.items).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => Boolean(file));
3677
+ if (itemFiles.length > 0) return itemFiles;
3678
+ return Array.from(data.files).filter((file) => file.type.startsWith("image/"));
3679
+ }
3680
+ function createDraftImageAttachment(file, index) {
3681
+ const mime = file.type || "image/png";
3682
+ const name = file.name || `pasted-image-${Date.now()}-${index + 1}${getImageExtension(mime)}`;
3683
+ return {
3684
+ id: createDraftAttachmentId(),
3685
+ file,
3686
+ name,
3687
+ mime,
3688
+ size: file.size,
3689
+ kind: "image",
3690
+ previewUrl: URL.createObjectURL(file),
3691
+ metadata: { source: "paste" }
3692
+ };
3507
3693
  }
3508
- function formatPoint(point) {
3509
- return `${Math.round(point.x)},${Math.round(point.y)}`;
3510
- }
3511
- function formatSelection(selection) {
3512
- return [
3513
- Math.round(selection.x),
3514
- Math.round(selection.y),
3515
- Math.round(selection.width),
3516
- Math.round(selection.height)
3517
- ].join(",");
3694
+ function createDraftAttachmentId() {
3695
+ return window.crypto?.randomUUID?.() ?? `draft-attachment-${Date.now()}-${Math.random().toString(36).slice(2)}`;
3696
+ }
3697
+ function getImageExtension(mime) {
3698
+ if (mime === "image/jpeg") return ".jpg";
3699
+ if (mime === "image/gif") return ".gif";
3700
+ if (mime === "image/webp") return ".webp";
3701
+ if (mime === "image/svg+xml") return ".svg";
3702
+ return ".png";
3703
+ }
3704
+ function insertTextAtTextareaSelection(textarea, text) {
3705
+ const start = textarea.selectionStart ?? textarea.value.length;
3706
+ const end = textarea.selectionEnd ?? start;
3707
+ textarea.value = [
3708
+ textarea.value.slice(0, start),
3709
+ text,
3710
+ textarea.value.slice(end)
3711
+ ].join("");
3712
+ const nextSelection = start + text.length;
3713
+ textarea.setSelectionRange(nextSelection, nextSelection);
3714
+ }
3715
+ function createDraftAttachmentPreview(ownerDocument, attachment) {
3716
+ if (attachment.previewUrl && attachment.mime.startsWith("image/")) {
3717
+ const image = ownerDocument.createElement("img");
3718
+ image.className = "dfwr-attachment-thumb";
3719
+ image.src = attachment.previewUrl;
3720
+ image.alt = "";
3721
+ image.decoding = "async";
3722
+ return image;
3723
+ }
3724
+ const fallback = ownerDocument.createElement("div");
3725
+ fallback.className = "dfwr-attachment-thumb is-file";
3726
+ fallback.textContent = "IMG";
3727
+ return fallback;
3728
+ }
3729
+
3730
+ // src/core/view/composer.position.ts
3731
+ var COMPOSER_MARGIN = 12;
3732
+ function getDraftComposerWidth(environment) {
3733
+ const bounds = environment.overlayRect;
3734
+ return Math.min(360, Math.max(240, bounds.width - COMPOSER_MARGIN * 2));
3735
+ }
3736
+ function getHostComposerBounds() {
3737
+ const root = document.documentElement;
3738
+ return {
3739
+ left: 0,
3740
+ top: 0,
3741
+ width: root.clientWidth || window.innerWidth,
3742
+ height: root.clientHeight || window.innerHeight
3743
+ };
3518
3744
  }
3519
- function formatAnchorMeta(anchor) {
3520
- const parts = [`dom ${anchor.strategy}`];
3521
- if (typeof anchor.confidence === "number") {
3522
- parts.push(`${Math.round(anchor.confidence * 100)}%`);
3523
- }
3524
- const candidates = getAnchorCandidates(anchor);
3525
- if (candidates.length > 1) {
3526
- parts.push(`${candidates.length} candidates`);
3527
- }
3528
- return parts.join(" ");
3745
+ function getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
3746
+ const width = size?.width ?? getDraftComposerWidth(environment);
3747
+ const height = size?.height ?? 236;
3748
+ return {
3749
+ x: clamp(
3750
+ position.x,
3751
+ bounds.left + COMPOSER_MARGIN,
3752
+ bounds.left + bounds.width - width - COMPOSER_MARGIN
3753
+ ),
3754
+ y: clamp(
3755
+ position.y,
3756
+ bounds.top + COMPOSER_MARGIN,
3757
+ bounds.top + bounds.height - height - COMPOSER_MARGIN
3758
+ )
3759
+ };
3529
3760
  }
3530
-
3531
- // src/core/web.review.kit.view.ts
3532
- var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
3533
- var WebReviewKitView = class {
3534
- constructor(config) {
3535
- this.config = config;
3536
- }
3537
- clearDraftPreview() {
3538
- this.restoreDraftPreview();
3539
- this.clearShellComposer();
3540
- }
3541
- render(shadow, hiddenItemsStyle) {
3542
- const state = this.state;
3543
- this.syncDraftPreview(
3544
- state.isOpen && state.mode === "element" ? state.noteDraft : void 0
3545
- );
3546
- shadow.replaceChildren();
3547
- shadow.append(createStyleElement());
3548
- shadow.append(hiddenItemsStyle);
3549
- const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
3550
- const shouldDockComposer = this.config.options.ui?.panel === false && hasDismissableDraft && Boolean(this.getShellComposerHost());
3551
- let dockedComposer;
3552
- const shell = document.createElement("div");
3553
- shell.className = [
3554
- "dfwr-shell",
3555
- state.isOpen ? "is-open" : "",
3556
- hasDismissableDraft && !shouldDockComposer ? "has-dismissible-draft" : ""
3557
- ].filter(Boolean).join(" ");
3558
- shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
3559
- if (this.config.options.ui?.panel !== false) {
3560
- const panel = document.createElement("div");
3561
- panel.className = "dfwr-panel";
3562
- panel.setAttribute("role", "dialog");
3563
- panel.setAttribute("aria-label", "Web review kit");
3564
- panel.append(
3565
- this.createHeader(),
3566
- this.createToolbar(),
3567
- this.createBody(),
3568
- this.createList()
3569
- );
3570
- shell.append(panel);
3571
- }
3572
- shell.append(this.createMarkerLayer());
3573
- if (state.isOpen && hasDismissableDraft && !shouldDockComposer) {
3574
- shell.append(this.createDraftCancelLayer());
3575
- }
3576
- if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
3577
- if (state.noteDraft) {
3578
- const noteDraft = this.createNotePopover(state.noteDraft, {
3579
- dockComposer: shouldDockComposer
3580
- });
3581
- shell.append(noteDraft.layer);
3582
- dockedComposer = noteDraft.composer;
3583
- } else {
3584
- shell.append(
3585
- state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
3586
- );
3587
- }
3588
- }
3589
- if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
3590
- shell.append(this.createAreaLayer());
3591
- }
3592
- if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
3593
- if (state.areaDraft.selection) {
3594
- shell.append(this.createAreaDraftOverlay(state.areaDraft));
3595
- }
3596
- const areaComposer = this.createAreaDraftPopover(state.areaDraft, {
3597
- dockComposer: shouldDockComposer
3598
- });
3599
- if (shouldDockComposer) {
3600
- dockedComposer = areaComposer;
3601
- } else {
3602
- shell.append(areaComposer);
3603
- }
3604
- }
3605
- shadow.append(shell);
3606
- this.renderShellComposer(dockedComposer);
3607
- }
3608
- get state() {
3609
- return this.config.getState();
3610
- }
3611
- getShellComposerHost() {
3612
- const environment = this.config.getEnvironment();
3613
- if (this.config.options.ui?.panel !== false) return void 0;
3614
- return environment?.composerHost ?? void 0;
3615
- }
3616
- renderShellComposer(composer) {
3617
- const host = composer ? this.getShellComposerHost() : void 0;
3618
- if (!host || !composer) {
3619
- this.clearShellComposer();
3620
- return;
3621
- }
3622
- if (this.shellComposerHost && this.shellComposerHost !== host) {
3623
- this.clearShellComposer();
3624
- }
3625
- this.shellComposerHost = host;
3626
- host.dataset.hasDraftComposer = "true";
3627
- if (host.parentElement) {
3628
- host.parentElement.dataset.hasDraftComposer = "true";
3629
- }
3630
- const shell = document.createElement("div");
3631
- shell.className = "dfwr-shell is-open is-shell-draft is-docked-composer";
3632
- shell.append(composer);
3633
- host.replaceChildren(createStyleElement(), shell);
3634
- }
3635
- clearShellComposer() {
3636
- const host = this.shellComposerHost;
3637
- host?.replaceChildren();
3638
- if (host) {
3639
- delete host.dataset.hasDraftComposer;
3640
- delete host.parentElement?.dataset.hasDraftComposer;
3641
- }
3642
- this.shellComposerHost = void 0;
3643
- }
3644
- createDraftCancelLayer() {
3645
- const layer = document.createElement("div");
3646
- layer.className = "dfwr-draft-cancel-layer";
3647
- layer.setAttribute("aria-hidden", "true");
3648
- layer.addEventListener("pointerdown", (event) => {
3649
- if (event.button !== 0) return;
3650
- this.cancelDraft(event);
3651
- });
3652
- return layer;
3653
- }
3654
- cancelDraft(event) {
3655
- event?.preventDefault();
3656
- event?.stopPropagation();
3657
- event?.stopImmediatePropagation();
3658
- this.config.actions.setModeState("idle");
3659
- this.config.actions.clearDrafts();
3660
- this.config.actions.setSelectingArea(false);
3661
- this.config.actions.render();
3662
- }
3663
- // Draft adjustment geometry lives in draft.metrics.ts; these thin wrappers
3664
- // supply the configured viewport presets so call sites stay unchanged.
3665
- get viewportPresets() {
3666
- return this.config.options.viewports?.presets;
3667
- }
3668
- getDraftAdjustmentMetrics(draft) {
3669
- return getDraftAdjustmentMetrics(draft, this.viewportPresets);
3670
- }
3671
- hasDraftAdjustment(draft) {
3672
- return hasDraftAdjustment(draft, this.viewportPresets);
3673
- }
3674
- getAdjustedDraftPoint(point, draft) {
3675
- return getAdjustedDraftPoint(point, draft, this.viewportPresets);
3676
- }
3677
- getAdjustedDraftSelection(selection, draft) {
3678
- return getAdjustedDraftSelection(
3679
- selection,
3680
- draft,
3681
- this.viewportPresets
3682
- );
3683
- }
3684
- getDraftViewportScale(viewport) {
3685
- return getDraftViewportScale(viewport, this.viewportPresets);
3686
- }
3687
- getDraftComposerWidth(environment) {
3688
- const bounds = environment.overlayRect;
3689
- const margin = 12;
3690
- return Math.min(360, Math.max(240, bounds.width - margin * 2));
3691
- }
3692
- getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
3693
- const margin = 12;
3694
- const width = size?.width ?? this.getDraftComposerWidth(environment);
3695
- const height = size?.height ?? 236;
3696
- return {
3697
- x: clamp(
3698
- position.x,
3699
- bounds.left + margin,
3700
- bounds.left + bounds.width - width - margin
3701
- ),
3702
- y: clamp(
3703
- position.y,
3704
- bounds.top + margin,
3705
- bounds.top + bounds.height - height - margin
3706
- )
3707
- };
3708
- }
3709
- getHostComposerBounds() {
3710
- const root = document.documentElement;
3711
- return {
3712
- left: 0,
3713
- top: 0,
3714
- width: root.clientWidth || window.innerWidth,
3715
- height: root.clientHeight || window.innerHeight
3716
- };
3717
- }
3718
- getInitialDraftComposerPosition(selection, environment, size) {
3719
- const bounds = this.getHostComposerBounds();
3720
- const margin = 12;
3721
- const gap = 20;
3722
- if (!selection) {
3723
- return this.getClampedComposerPosition(
3724
- {
3725
- x: environment.overlayRect.left + margin,
3726
- y: environment.overlayRect.top + margin
3727
- },
3728
- environment,
3729
- size,
3730
- bounds
3731
- );
3732
- }
3733
- const preferredX = selection.left + selection.width + gap;
3734
- const maxX = bounds.left + bounds.width - size.width - margin;
3735
- const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
3736
- return this.getClampedComposerPosition(
3761
+ function getInitialDraftComposerPosition(selection, environment, size) {
3762
+ const bounds = getHostComposerBounds();
3763
+ const gap = 20;
3764
+ if (!selection) {
3765
+ return getClampedComposerPosition(
3737
3766
  {
3738
- x,
3739
- y: selection.top
3767
+ x: environment.overlayRect.left + COMPOSER_MARGIN,
3768
+ y: environment.overlayRect.top + COMPOSER_MARGIN
3740
3769
  },
3741
3770
  environment,
3742
3771
  size,
3743
3772
  bounds
3744
3773
  );
3745
3774
  }
3746
- getDraftComposerPosition({
3747
- selection,
3775
+ const preferredX = selection.left + selection.width + gap;
3776
+ const maxX = bounds.left + bounds.width - size.width - COMPOSER_MARGIN;
3777
+ const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
3778
+ return getClampedComposerPosition(
3779
+ {
3780
+ x,
3781
+ y: selection.top
3782
+ },
3748
3783
  environment,
3749
- composerPosition,
3750
- estimatedHeight
3751
- }) {
3752
- const width = this.getDraftComposerWidth(environment);
3753
- if (composerPosition) {
3754
- const clamped = this.getClampedComposerPosition(
3755
- composerPosition,
3756
- environment,
3757
- { width, height: estimatedHeight },
3758
- this.getHostComposerBounds()
3759
- );
3760
- return { width, left: clamped.x, top: clamped.y };
3761
- }
3762
- const position = this.getInitialDraftComposerPosition(selection, environment, {
3763
- width,
3764
- height: estimatedHeight
3765
- });
3766
- return { width, left: position.x, top: position.y };
3784
+ size,
3785
+ bounds
3786
+ );
3787
+ }
3788
+ function getDraftComposerPosition({
3789
+ selection,
3790
+ environment,
3791
+ composerPosition,
3792
+ estimatedHeight
3793
+ }) {
3794
+ const width = getDraftComposerWidth(environment);
3795
+ if (composerPosition) {
3796
+ const clamped = getClampedComposerPosition(
3797
+ composerPosition,
3798
+ environment,
3799
+ { width, height: estimatedHeight },
3800
+ getHostComposerBounds()
3801
+ );
3802
+ return { width, left: clamped.x, top: clamped.y };
3767
3803
  }
3768
- getSelectionMqMetrics(selection, viewport) {
3769
- const { scale } = this.getDraftViewportScale(viewport);
3770
- const ratio = scale > 0 ? 1 / scale : 1;
3804
+ const position = getInitialDraftComposerPosition(selection, environment, {
3805
+ width,
3806
+ height: estimatedHeight
3807
+ });
3808
+ return { width, left: position.x, top: position.y };
3809
+ }
3810
+ function attachDraftComposerDrag({
3811
+ getEnvironment,
3812
+ popover,
3813
+ handle,
3814
+ onMove
3815
+ }) {
3816
+ let isDragging = false;
3817
+ let offsetX = 0;
3818
+ let offsetY = 0;
3819
+ const movePopover = (event) => {
3820
+ const environment = getEnvironment();
3821
+ if (!environment) return;
3822
+ const position = getClampedComposerPosition(
3823
+ {
3824
+ x: event.clientX - offsetX,
3825
+ y: event.clientY - offsetY
3826
+ },
3827
+ environment,
3828
+ {
3829
+ width: popover.offsetWidth,
3830
+ height: popover.offsetHeight
3831
+ },
3832
+ getHostComposerBounds()
3833
+ );
3834
+ popover.style.left = `${position.x}px`;
3835
+ popover.style.top = `${position.y}px`;
3836
+ onMove(position);
3837
+ };
3838
+ handle.addEventListener("pointerdown", (event) => {
3839
+ if (event.button !== 0) return;
3840
+ const rect = popover.getBoundingClientRect();
3841
+ offsetX = event.clientX - rect.left;
3842
+ offsetY = event.clientY - rect.top;
3843
+ isDragging = true;
3844
+ event.preventDefault();
3845
+ event.stopPropagation();
3846
+ handle.setPointerCapture(event.pointerId);
3847
+ popover.classList.add("is-dragging");
3848
+ });
3849
+ handle.addEventListener("pointermove", (event) => {
3850
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3851
+ event.preventDefault();
3852
+ movePopover(event);
3853
+ });
3854
+ const stopDrag = (event) => {
3855
+ if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
3856
+ event.preventDefault();
3857
+ event.stopPropagation();
3858
+ isDragging = false;
3859
+ handle.releasePointerCapture(event.pointerId);
3860
+ popover.classList.remove("is-dragging");
3861
+ movePopover(event);
3862
+ };
3863
+ handle.addEventListener("pointerup", stopDrag);
3864
+ handle.addEventListener("pointercancel", stopDrag);
3865
+ }
3866
+
3867
+ // src/core/view/icons.ts
3868
+ function createIcon(paths) {
3869
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3870
+ svg.setAttribute("aria-hidden", "true");
3871
+ svg.setAttribute("viewBox", "0 0 24 24");
3872
+ svg.setAttribute("fill", "none");
3873
+ svg.setAttribute("stroke", "currentColor");
3874
+ svg.setAttribute("stroke-width", "2.4");
3875
+ svg.setAttribute("stroke-linecap", "round");
3876
+ svg.setAttribute("stroke-linejoin", "round");
3877
+ paths.forEach((d) => {
3878
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3879
+ path.setAttribute("d", d);
3880
+ svg.append(path);
3881
+ });
3882
+ return svg;
3883
+ }
3884
+ function setAdjustmentToggleIcon(button, isActive) {
3885
+ const paths = isActive ? ["M20 6 9 17l-5-5"] : [
3886
+ "M12 2v20",
3887
+ "M2 12h20",
3888
+ "m9 5 3-3 3 3",
3889
+ "m9 19 3 3 3-3",
3890
+ "m5 9-3 3 3 3",
3891
+ "m19 9 3 3-3 3"
3892
+ ];
3893
+ button.replaceChildren(createIcon(paths));
3894
+ }
3895
+ function createSpinner(className) {
3896
+ const spinner = document.createElement("span");
3897
+ spinner.className = className;
3898
+ spinner.setAttribute("aria-hidden", "true");
3899
+ return spinner;
3900
+ }
3901
+
3902
+ // src/core/view/draft.capture.ts
3903
+ function canCaptureViewport(config) {
3904
+ return Boolean(config.getEnvironment()?.captureViewport);
3905
+ }
3906
+ function getCaptureAreaDraft(draft) {
3907
+ return {
3908
+ viewport: draft.viewport,
3909
+ marker: draft.marker,
3910
+ selection: draft.selection
3911
+ };
3912
+ }
3913
+ function getCaptureDomDraft(config, draft, isElementDraft) {
3914
+ if (!isElementDraft) {
3771
3915
  return {
3772
- x: selection.left * ratio,
3773
- y: selection.top * ratio,
3774
- width: selection.width * ratio,
3775
- height: selection.height * ratio
3916
+ viewport: draft.viewport,
3917
+ marker: draft.marker,
3918
+ selection: draft.selection
3776
3919
  };
3777
3920
  }
3778
- formatSignedPx(value) {
3779
- if (value === 0) return "+0px";
3780
- return `${value > 0 ? "+" : ""}${value}px`;
3781
- }
3782
- formatRoundedPx(value) {
3783
- return `${Math.round(value)}px`;
3921
+ const presets = config.options.viewports?.presets;
3922
+ const marker = {
3923
+ ...draft.marker,
3924
+ viewport: roundPoint(
3925
+ getAdjustedDraftPoint(draft.marker.viewport, draft, presets)
3926
+ )
3927
+ };
3928
+ const selection = draft.selection ? {
3929
+ ...draft.selection,
3930
+ viewport: toPublicSelection(
3931
+ getAdjustedDraftSelection(
3932
+ toViewportSelection(draft.selection.viewport),
3933
+ draft,
3934
+ presets
3935
+ )
3936
+ )
3937
+ } : void 0;
3938
+ return {
3939
+ viewport: draft.viewport,
3940
+ marker,
3941
+ selection
3942
+ };
3943
+ }
3944
+ function createDraftCaptureButton(config, draft, options) {
3945
+ const button = document.createElement("button");
3946
+ const state = config.getState();
3947
+ const isCapturing = state.isCapturingViewport;
3948
+ const canCapture = canCaptureViewport(config);
3949
+ button.className = "dfwr-button";
3950
+ button.type = "button";
3951
+ button.disabled = !canCapture || isCapturing || state.isCreatingItem;
3952
+ button.setAttribute("aria-busy", isCapturing ? "true" : "false");
3953
+ button.title = canCapture ? "Capture current viewport" : "Viewport capture helper is not available";
3954
+ if (isCapturing) {
3955
+ button.append(createSpinner("dfwr-spinner"), "Capturing...");
3956
+ } else {
3957
+ button.textContent = "Capture";
3958
+ }
3959
+ button.addEventListener("click", (event) => {
3960
+ event.preventDefault();
3961
+ event.stopPropagation();
3962
+ const currentState = config.getState();
3963
+ if (!canCaptureViewport(config) || currentState.isCapturingViewport) {
3964
+ return;
3965
+ }
3966
+ if (options.kind === "area") {
3967
+ const areaDraft = currentState.areaDraft ?? draft;
3968
+ const nextDraft2 = {
3969
+ ...areaDraft,
3970
+ comment: options.textarea.value
3971
+ };
3972
+ config.actions.setAreaDraft(nextDraft2);
3973
+ void config.actions.captureAreaDraft(getCaptureAreaDraft(nextDraft2));
3974
+ return;
3975
+ }
3976
+ const domDraft = currentState.domDraft ?? draft;
3977
+ const nextDraft = {
3978
+ ...domDraft,
3979
+ comment: options.textarea.value
3980
+ };
3981
+ config.actions.setDomDraft(nextDraft);
3982
+ void config.actions.captureDomDraft(
3983
+ getCaptureDomDraft(config, nextDraft, options.isElementDraft)
3984
+ );
3985
+ });
3986
+ return button;
3987
+ }
3988
+
3989
+ // src/core/view/draft.text.ts
3990
+ var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
3991
+ function getAdjustmentLabel(options) {
3992
+ return options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
3993
+ }
3994
+ function formatSignedPx(value) {
3995
+ if (value === 0) return "+0px";
3996
+ return `${value > 0 ? "+" : ""}${value}px`;
3997
+ }
3998
+ function formatRoundedPx(value) {
3999
+ return `${Math.round(value)}px`;
4000
+ }
4001
+ function getSelectionMqMetrics(selection, viewport, presets) {
4002
+ const { scale } = getDraftViewportScale(viewport, presets);
4003
+ const ratio = scale > 0 ? 1 / scale : 1;
4004
+ return {
4005
+ x: selection.left * ratio,
4006
+ y: selection.top * ratio,
4007
+ width: selection.width * ratio,
4008
+ height: selection.height * ratio
4009
+ };
4010
+ }
4011
+ function getSelectionMetricLines(selection, viewport, presets) {
4012
+ if (!selection) return ["area", "x none / y none", "w none / h none"];
4013
+ const metrics = getSelectionMqMetrics(selection, viewport, presets);
4014
+ return [
4015
+ "area",
4016
+ `x ${formatRoundedPx(metrics.x)} / y ${formatRoundedPx(metrics.y)}`,
4017
+ `w ${formatRoundedPx(metrics.width)} / h ${formatRoundedPx(
4018
+ metrics.height
4019
+ )}`
4020
+ ];
4021
+ }
4022
+ function getAreaDraftMetricSelection(draft) {
4023
+ if (!draft.selection) return void 0;
4024
+ return toViewportSelection(draft.selection.viewport);
4025
+ }
4026
+ function getDraftAdjustmentMetricLines(draft, presets) {
4027
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
4028
+ return [
4029
+ `x ${formatSignedPx(metrics.x)} / y ${formatSignedPx(metrics.y)}`,
4030
+ `scale ${formatSignedPx(metrics.scale)}`
4031
+ ];
4032
+ }
4033
+ function withDraftAdjustmentComment(comment, draft, options) {
4034
+ const presets = options.viewports?.presets;
4035
+ if (!hasDraftAdjustment(draft, presets)) return comment;
4036
+ const trimmedComment = comment.trim();
4037
+ const metrics = getDraftAdjustmentMetrics(draft, presets);
4038
+ const adjustment = [
4039
+ `${getAdjustmentLabel(options)}: x ${formatSignedPx(
4040
+ metrics.x
4041
+ )}, y ${formatSignedPx(metrics.y)}, scale ${formatSignedPx(
4042
+ metrics.scale
4043
+ )}`,
4044
+ `(${metrics.presetLabel} viewport, ${Math.round(
4045
+ metrics.viewportWidth
4046
+ )}/design ${Math.round(metrics.designWidth)})`
4047
+ ].join(" ");
4048
+ return trimmedComment ? `${trimmedComment}
4049
+ ${adjustment}` : adjustment;
4050
+ }
4051
+
4052
+ // src/core/view/form.widgets.ts
4053
+ function getAssigneeOption(options, assigneeId) {
4054
+ if (!assigneeId) return void 0;
4055
+ return options.assigneeOptions?.find(
4056
+ (option) => option.value === assigneeId
4057
+ );
4058
+ }
4059
+ function getAssigneeName(options, assigneeId) {
4060
+ return getAssigneeOption(options, assigneeId)?.label;
4061
+ }
4062
+ function isTitleFieldEnabled(options) {
4063
+ return options.fields?.title === true;
4064
+ }
4065
+ function createDraftTitleInput(value, onInput) {
4066
+ const input = document.createElement("input");
4067
+ input.className = "dfwr-input";
4068
+ input.placeholder = "Title";
4069
+ input.type = "text";
4070
+ input.value = value ?? "";
4071
+ input.addEventListener("input", () => onInput(input.value));
4072
+ return input;
4073
+ }
4074
+ function createDraftAssigneeSelect(options, value, fallbackLabel, onChange) {
4075
+ const assigneeOptions = options.assigneeOptions ?? [];
4076
+ if (assigneeOptions.length === 0) return void 0;
4077
+ const assigneeTitle = options.assigneeTitle?.trim() || "Assignee";
4078
+ const select = document.createElement("select");
4079
+ select.className = "dfwr-select";
4080
+ const emptyOption = document.createElement("option");
4081
+ emptyOption.value = "";
4082
+ emptyOption.textContent = assigneeTitle;
4083
+ select.append(emptyOption);
4084
+ const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
4085
+ if (hasUnknownAssignee && value) {
4086
+ const option = document.createElement("option");
4087
+ option.value = value;
4088
+ option.textContent = fallbackLabel ?? value;
4089
+ select.append(option);
4090
+ }
4091
+ assigneeOptions.forEach((assigneeOption) => {
4092
+ const option = document.createElement("option");
4093
+ option.value = assigneeOption.value;
4094
+ option.textContent = assigneeOption.label;
4095
+ select.append(option);
4096
+ });
4097
+ select.value = value ?? "";
4098
+ select.addEventListener("change", () => {
4099
+ onChange(select.value || null, getAssigneeName(options, select.value));
4100
+ });
4101
+ return select;
4102
+ }
4103
+ function getDraftFields(options, titleInput, textarea, assigneeSelect) {
4104
+ const title = titleInput?.value.trim();
4105
+ const comment = textarea.value.trim();
4106
+ const assigneeId = assigneeSelect?.value.trim() || void 0;
4107
+ return {
4108
+ title: title || void 0,
4109
+ comment,
4110
+ assigneeId,
4111
+ assigneeName: getAssigneeName(options, assigneeId)
4112
+ };
4113
+ }
4114
+ function createFormActions({
4115
+ saveLabel,
4116
+ onSave,
4117
+ onCancel,
4118
+ isSaving,
4119
+ beforeSave,
4120
+ className,
4121
+ leading
4122
+ }) {
4123
+ const actions = document.createElement("div");
4124
+ actions.className = ["dfwr-actions", className].filter(Boolean).join(" ");
4125
+ const save = document.createElement("button");
4126
+ save.className = "dfwr-button is-primary";
4127
+ save.type = "button";
4128
+ save.disabled = isSaving;
4129
+ save.setAttribute("aria-busy", isSaving ? "true" : "false");
4130
+ if (isSaving) {
4131
+ save.append(createSpinner("dfwr-spinner"), "Saving...");
4132
+ } else {
4133
+ save.textContent = saveLabel;
4134
+ }
4135
+ save.addEventListener("click", (event) => {
4136
+ event.preventDefault();
4137
+ event.stopPropagation();
4138
+ if (save.disabled) return;
4139
+ onSave();
4140
+ });
4141
+ const cancel = document.createElement("button");
4142
+ cancel.className = "dfwr-button";
4143
+ cancel.type = "button";
4144
+ cancel.disabled = isSaving;
4145
+ cancel.textContent = "Cancel";
4146
+ cancel.addEventListener("click", (event) => {
4147
+ onCancel(event);
4148
+ });
4149
+ if (leading?.length) {
4150
+ actions.classList.add("has-leading");
4151
+ const leadingGroup = document.createElement("div");
4152
+ leadingGroup.className = "dfwr-actions-leading";
4153
+ leadingGroup.append(...leading);
4154
+ const primary = document.createElement("div");
4155
+ primary.className = "dfwr-actions-primary";
4156
+ primary.append(save, cancel);
4157
+ actions.append(leadingGroup, primary);
4158
+ return actions;
3784
4159
  }
3785
- getAdjustmentLabel() {
3786
- return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
4160
+ if (beforeSave?.length || className) {
4161
+ actions.append(cancel, ...beforeSave ?? [], save);
4162
+ return actions;
3787
4163
  }
3788
- getSelectionMetricLines(selection, viewport) {
3789
- if (!selection) return ["area", "x none / y none", "w none / h none"];
3790
- const metrics = this.getSelectionMqMetrics(selection, viewport);
3791
- return [
3792
- "area",
3793
- `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
3794
- metrics.y
3795
- )}`,
3796
- `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
3797
- metrics.height
3798
- )}`
3799
- ];
3800
- }
3801
- getAreaDraftMetricSelection(draft) {
3802
- if (!draft.selection) return void 0;
3803
- return toViewportSelection(draft.selection.viewport);
3804
- }
3805
- getDraftAdjustmentMetricLines(draft) {
3806
- const metrics = this.getDraftAdjustmentMetrics(draft);
3807
- return [
3808
- `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
3809
- metrics.y
3810
- )}`,
3811
- `scale ${this.formatSignedPx(metrics.scale)}`
3812
- ];
3813
- }
3814
- withDraftAdjustmentComment(comment, draft) {
3815
- if (!this.hasDraftAdjustment(draft)) return comment;
3816
- const trimmedComment = comment.trim();
3817
- const metrics = this.getDraftAdjustmentMetrics(draft);
3818
- const adjustment = [
3819
- `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
3820
- metrics.x
3821
- )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
3822
- metrics.scale
3823
- )}`,
3824
- `(${metrics.presetLabel} viewport, ${Math.round(
3825
- metrics.viewportWidth
3826
- )}/design ${Math.round(metrics.designWidth)})`
3827
- ].join(" ");
3828
- return trimmedComment ? `${trimmedComment}
3829
- ${adjustment}` : adjustment;
4164
+ actions.append(save, cancel);
4165
+ return actions;
4166
+ }
4167
+ function createDraftError(message) {
4168
+ if (!message) return void 0;
4169
+ const error = document.createElement("p");
4170
+ error.className = "dfwr-form-error";
4171
+ error.setAttribute("role", "alert");
4172
+ error.textContent = message;
4173
+ return error;
4174
+ }
4175
+ function createDraftDragHandle(label) {
4176
+ const handle = document.createElement("button");
4177
+ handle.className = "dfwr-draft-drag-handle";
4178
+ handle.type = "button";
4179
+ handle.setAttribute("aria-label", label);
4180
+ return handle;
4181
+ }
4182
+
4183
+ // src/core/review/format.ts
4184
+ function formatDomDraftMeta(draft) {
4185
+ const parts = [
4186
+ `viewport ${formatSize(draft.viewport)}`,
4187
+ `point ${formatPoint(draft.marker.viewport)}`
4188
+ ];
4189
+ if (draft.anchor) {
4190
+ parts.push(formatAnchorMeta(draft.anchor));
3830
4191
  }
3831
- getAssigneeOption(assigneeId) {
3832
- if (!assigneeId) return void 0;
3833
- return this.config.options.assigneeOptions?.find(
3834
- (option) => option.value === assigneeId
3835
- );
4192
+ return parts.join(" / ");
4193
+ }
4194
+ function formatItemMeta(item) {
4195
+ const parts = [formatDate(item.createdAt)];
4196
+ const marker = getItemMarker(item);
4197
+ const selection = getItemSelection(item);
4198
+ if (item.viewport) {
4199
+ parts.push(`viewport ${formatSize(item.viewport)}`);
3836
4200
  }
3837
- getAssigneeName(assigneeId) {
3838
- return this.getAssigneeOption(assigneeId)?.label;
4201
+ if (marker) {
4202
+ parts.push(`point ${formatPoint(marker.viewport)}`);
3839
4203
  }
3840
- createDraftTitleInput(value, onInput) {
3841
- const input = document.createElement("input");
3842
- input.className = "dfwr-input";
3843
- input.placeholder = "Title";
3844
- input.type = "text";
3845
- input.value = value ?? "";
3846
- input.addEventListener("input", () => onInput(input.value));
3847
- return input;
4204
+ if (selection) {
4205
+ parts.push(`rect ${formatSelection(selection.viewport)}`);
3848
4206
  }
3849
- isTitleFieldEnabled() {
3850
- return this.config.options.fields?.title === true;
3851
- }
3852
- createDraftAssigneeSelect(value, fallbackLabel, onChange) {
3853
- const assigneeOptions = this.config.options.assigneeOptions ?? [];
3854
- if (assigneeOptions.length === 0) return void 0;
3855
- const assigneeTitle = this.config.options.assigneeTitle?.trim() || "Assignee";
3856
- const select = document.createElement("select");
3857
- select.className = "dfwr-select";
3858
- const emptyOption = document.createElement("option");
3859
- emptyOption.value = "";
3860
- emptyOption.textContent = assigneeTitle;
3861
- select.append(emptyOption);
3862
- const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
3863
- if (hasUnknownAssignee && value) {
3864
- const option = document.createElement("option");
3865
- option.value = value;
3866
- option.textContent = fallbackLabel ?? value;
3867
- select.append(option);
3868
- }
3869
- assigneeOptions.forEach((assigneeOption) => {
3870
- const option = document.createElement("option");
3871
- option.value = assigneeOption.value;
3872
- option.textContent = assigneeOption.label;
3873
- select.append(option);
3874
- });
3875
- select.value = value ?? "";
3876
- select.addEventListener("change", () => {
3877
- onChange(select.value || null, this.getAssigneeName(select.value));
3878
- });
3879
- return select;
4207
+ if (item.anchor) {
4208
+ parts.push(formatAnchorMeta(item.anchor));
3880
4209
  }
3881
- getDraftFields(titleInput, textarea, assigneeSelect) {
3882
- const title = titleInput?.value.trim();
3883
- const comment = textarea.value.trim();
3884
- const assigneeId = assigneeSelect?.value.trim() || void 0;
3885
- return {
3886
- title: title || void 0,
3887
- comment,
3888
- assigneeId,
3889
- assigneeName: this.getAssigneeName(assigneeId)
3890
- };
4210
+ return parts.join(" / ");
4211
+ }
4212
+ function formatDate(value) {
4213
+ const date = new Date(value);
4214
+ if (Number.isNaN(date.getTime())) return value;
4215
+ return date.toLocaleString(void 0, {
4216
+ month: "2-digit",
4217
+ day: "2-digit",
4218
+ hour: "2-digit",
4219
+ minute: "2-digit"
4220
+ });
4221
+ }
4222
+ function formatSize(size) {
4223
+ return `${Math.round(size.width)}x${Math.round(size.height)}`;
4224
+ }
4225
+ function formatPoint(point) {
4226
+ return `${Math.round(point.x)},${Math.round(point.y)}`;
4227
+ }
4228
+ function formatSelection(selection) {
4229
+ return [
4230
+ Math.round(selection.x),
4231
+ Math.round(selection.y),
4232
+ Math.round(selection.width),
4233
+ Math.round(selection.height)
4234
+ ].join(",");
4235
+ }
4236
+ function formatAnchorMeta(anchor) {
4237
+ const parts = [`dom ${anchor.strategy}`];
4238
+ if (typeof anchor.confidence === "number") {
4239
+ parts.push(`${Math.round(anchor.confidence * 100)}%`);
3891
4240
  }
3892
- getStyleableDraftElement(draft, environment) {
3893
- if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
3894
- return draft.previewElement;
3895
- }
3896
- if (!draft.anchor) return void 0;
3897
- const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
3898
- const element = resolveAnchorElement(
3899
- draft.anchor,
3900
- environment,
3901
- preferredSelection
3902
- )?.element;
3903
- if (!element) return void 0;
3904
- if ("style" in element) return element;
3905
- return void 0;
4241
+ const candidates = getAnchorCandidates(anchor);
4242
+ if (candidates.length > 1) {
4243
+ parts.push(`${candidates.length} candidates`);
3906
4244
  }
3907
- syncDraftPreview(draft) {
3908
- const environment = this.config.getEnvironment();
3909
- if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
3910
- this.restoreDraftPreview();
3911
- return;
3912
- }
3913
- const element = this.getStyleableDraftElement(draft, environment);
3914
- if (!element) {
3915
- this.restoreDraftPreview();
4245
+ return parts.join(" ");
4246
+ }
4247
+
4248
+ // src/core/view/markers.ts
4249
+ function createMarkerElement(itemId, hostPoint, label, scope, isBound, isHighlighted) {
4250
+ const marker = document.createElement("div");
4251
+ marker.className = [
4252
+ "dfwr-bound-marker",
4253
+ `is-scope-${scope}`,
4254
+ isBound ? "is-bound" : "is-fallback",
4255
+ isHighlighted ? "is-highlighted" : ""
4256
+ ].filter(Boolean).join(" ");
4257
+ marker.style.left = `${hostPoint.x}px`;
4258
+ marker.style.top = `${hostPoint.y}px`;
4259
+ marker.dataset.scope = scope;
4260
+ if (itemId) {
4261
+ marker.dataset.reviewItemId = itemId;
4262
+ }
4263
+ const iconElement = document.createElement("span");
4264
+ iconElement.className = "dfwr-bound-marker-icon";
4265
+ iconElement.setAttribute("aria-hidden", "true");
4266
+ const labelElement = document.createElement("span");
4267
+ labelElement.className = "dfwr-bound-marker-number";
4268
+ labelElement.textContent = label;
4269
+ marker.append(iconElement, labelElement);
4270
+ return marker;
4271
+ }
4272
+ function createSelectionHighlight(selection, environment, isDraft) {
4273
+ const rect = toHostSelection(selection, environment);
4274
+ const highlight = document.createElement("div");
4275
+ highlight.className = `dfwr-selection-highlight${isDraft ? " is-draft" : ""}`;
4276
+ highlight.style.left = `${rect.left}px`;
4277
+ highlight.style.top = `${rect.top}px`;
4278
+ highlight.style.width = `${rect.width}px`;
4279
+ highlight.style.height = `${rect.height}px`;
4280
+ return highlight;
4281
+ }
4282
+ function createItemHighlightElements(selection, environment, item, label, scope, isBound, isHighlighted) {
4283
+ const rect = toHostSelection(selection, environment);
4284
+ const mode = getReviewItemHighlightMode(item);
4285
+ const highlight = document.createElement("div");
4286
+ highlight.className = [
4287
+ "dfwr-item-target-highlight",
4288
+ `is-mode-${mode}`,
4289
+ `is-scope-${scope}`,
4290
+ isBound ? "is-bound" : "is-fallback",
4291
+ isHighlighted ? "is-highlighted" : ""
4292
+ ].filter(Boolean).join(" ");
4293
+ highlight.style.left = `${rect.left}px`;
4294
+ highlight.style.top = `${rect.top}px`;
4295
+ highlight.style.width = `${rect.width}px`;
4296
+ highlight.style.height = `${rect.height}px`;
4297
+ highlight.dataset.reviewItemId = item.id;
4298
+ const labelElement = document.createElement("div");
4299
+ labelElement.className = [
4300
+ "dfwr-item-target-label",
4301
+ `is-mode-${mode}`,
4302
+ `is-scope-${scope}`,
4303
+ isHighlighted ? "is-highlighted" : ""
4304
+ ].filter(Boolean).join(" ");
4305
+ labelElement.textContent = label;
4306
+ labelElement.style.left = `${Math.max(4, rect.left)}px`;
4307
+ labelElement.style.top = `${Math.max(4, rect.top - 24)}px`;
4308
+ labelElement.dataset.reviewItemId = item.id;
4309
+ return [highlight, labelElement];
4310
+ }
4311
+ function createMarkerLayer({
4312
+ items,
4313
+ highlightedItemId,
4314
+ environment,
4315
+ presets,
4316
+ showCompactMarkers = true
4317
+ }) {
4318
+ const layer = document.createElement("div");
4319
+ layer.className = "dfwr-marker-layer";
4320
+ if (!environment) return layer;
4321
+ const currentScope = getReviewViewportScope(
4322
+ getViewportSize(environment),
4323
+ presets
4324
+ );
4325
+ getNumberedReviewItems(items, presets).forEach((numberedItem) => {
4326
+ const { item, scope, displayLabel } = numberedItem;
4327
+ if (!shouldShowMarkerForScope(scope, currentScope)) {
3916
4328
  return;
3917
4329
  }
3918
- if (this.draftPreview?.element !== element) {
3919
- this.restoreDraftPreview();
3920
- }
3921
- if (!this.draftPreview) {
3922
- const computedStyle = environment.window.getComputedStyle(element);
3923
- const clone = element.cloneNode(true);
3924
- this.removeDuplicateIds(clone);
3925
- this.copyComputedStyle(element, clone, environment);
3926
- this.positionDraftPreviewClone(clone, element, computedStyle);
3927
- environment.document.body?.appendChild(clone);
3928
- this.draftPreview = {
3929
- element,
3930
- clone,
3931
- visibility: element.style.visibility
3932
- };
3933
- element.style.visibility = "hidden";
3934
- }
3935
- const metrics = this.getDraftAdjustmentMetrics(draft);
3936
- const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
3937
- metrics.cssY
3938
- )}px)`;
3939
- const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
3940
- this.draftPreview.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
3941
- }
3942
- restoreDraftPreview() {
3943
- if (!this.draftPreview) return;
3944
- const { element, clone, visibility } = this.draftPreview;
3945
- clone.remove();
3946
- element.style.visibility = visibility;
3947
- this.draftPreview = void 0;
3948
- }
3949
- positionDraftPreviewClone(clone, element, computedStyle) {
3950
- const rect = element.getBoundingClientRect();
3951
- clone.setAttribute("data-dfwr-adjust-preview", "true");
3952
- clone.setAttribute("aria-hidden", "true");
3953
- clone.style.position = "fixed";
3954
- clone.style.left = `${this.toCssNumber(rect.left)}px`;
3955
- clone.style.top = `${this.toCssNumber(rect.top)}px`;
3956
- clone.style.right = "auto";
3957
- clone.style.bottom = "auto";
3958
- clone.style.width = `${this.toCssNumber(rect.width)}px`;
3959
- clone.style.height = `${this.toCssNumber(rect.height)}px`;
3960
- clone.style.maxWidth = "none";
3961
- clone.style.maxHeight = "none";
3962
- clone.style.margin = "0";
3963
- clone.style.boxSizing = "border-box";
3964
- clone.style.display = this.getDraftPreviewDisplay(computedStyle.display);
3965
- clone.style.zIndex = "2147483646";
3966
- clone.style.pointerEvents = "none";
3967
- clone.style.transition = "none";
3968
- clone.style.willChange = "transform";
3969
- clone.style.transformOrigin = "top left";
3970
- clone.style.transform = "none";
3971
- }
3972
- getDraftPreviewDisplay(display) {
3973
- if (display === "inline" || display === "contents") return "inline-block";
3974
- return display || "block";
3975
- }
3976
- copyComputedStyle(element, clone, environment) {
3977
- const computedStyle = environment.window.getComputedStyle(element);
3978
- for (let index = 0; index < computedStyle.length; index += 1) {
3979
- const property = computedStyle.item(index);
3980
- clone.style.setProperty(
3981
- property,
3982
- computedStyle.getPropertyValue(property),
3983
- computedStyle.getPropertyPriority(property)
3984
- );
3985
- }
3986
- }
3987
- removeDuplicateIds(element) {
3988
- element.removeAttribute("id");
3989
- element.querySelectorAll("[id]").forEach((child) => {
3990
- child.removeAttribute("id");
3991
- });
3992
- }
3993
- toCssNumber(value) {
3994
- return Math.round(value * 1e3) / 1e3;
3995
- }
3996
- createHeader() {
3997
- const header = document.createElement("div");
3998
- header.className = "dfwr-header";
3999
- const title = document.createElement("div");
4000
- title.className = "dfwr-title";
4001
- title.textContent = "Review Kit";
4002
- const meta = document.createElement("div");
4003
- meta.className = "dfwr-meta";
4004
- meta.textContent = getRouteKey(this.config.getEnvironment());
4005
- const titleGroup = document.createElement("div");
4006
- titleGroup.append(title, meta);
4007
- const close = document.createElement("button");
4008
- close.className = "dfwr-icon-button";
4009
- close.type = "button";
4010
- close.textContent = "x";
4011
- close.setAttribute("aria-label", "Close");
4012
- close.addEventListener("click", () => this.config.actions.close());
4013
- header.append(titleGroup, close);
4014
- return header;
4015
- }
4016
- createToolbar() {
4017
- const toolbar = document.createElement("div");
4018
- toolbar.className = "dfwr-toolbar";
4019
- toolbar.append(
4020
- this.createToolbarButton("Note", this.state.mode === "note", () => {
4021
- const mode = this.state.mode;
4022
- this.config.actions.setModeState(mode === "note" ? "idle" : "note");
4023
- this.config.actions.clearDrafts();
4024
- this.config.actions.render();
4025
- }),
4026
- this.createToolbarButton("Element", this.state.mode === "element", () => {
4027
- const mode = this.state.mode;
4028
- this.config.actions.setModeState(
4029
- mode === "element" ? "idle" : "element"
4330
+ const isHighlighted = item.id === highlightedItemId;
4331
+ if (isHighlighted) {
4332
+ const selection = getItemHighlightSelection(item, environment);
4333
+ if (selection) {
4334
+ layer.append(
4335
+ ...createItemHighlightElements(
4336
+ selection.viewport,
4337
+ environment,
4338
+ item,
4339
+ displayLabel,
4340
+ scope,
4341
+ selection.isBound,
4342
+ isHighlighted
4343
+ )
4030
4344
  );
4031
- this.config.actions.clearDrafts();
4032
- this.config.actions.render();
4033
- }),
4034
- this.createToolbarButton(
4035
- this.state.isSelectingArea ? "Selecting" : "Area",
4036
- this.state.mode === "area",
4037
- () => {
4038
- const mode = this.state.mode;
4039
- this.config.actions.setModeState(mode === "area" ? "idle" : "area");
4040
- this.config.actions.clearDrafts();
4041
- this.config.actions.render();
4042
- }
4043
- ),
4044
- this.createToolbarButton("Refresh", false, () => {
4045
- void this.config.actions.reload();
4046
- })
4047
- );
4048
- return toolbar;
4049
- }
4050
- createToolbarButton(label, active, onClick) {
4051
- const button = document.createElement("button");
4052
- button.className = `dfwr-button${active ? " is-active" : ""}`;
4053
- button.type = "button";
4054
- button.textContent = label;
4055
- button.addEventListener("click", onClick);
4056
- return button;
4057
- }
4058
- createBody() {
4059
- const body = document.createElement("div");
4060
- body.className = "dfwr-body";
4061
- const state = this.state;
4062
- if (state.mode === "idle") {
4063
- const empty = document.createElement("p");
4064
- empty.className = "dfwr-empty";
4065
- empty.textContent = "Add a note or mark an area.";
4066
- body.append(empty);
4067
- return body;
4068
- }
4069
- if (state.mode === "note" || state.mode === "element") {
4070
- body.append(this.createNoteBody());
4071
- return body;
4072
- }
4073
- body.append(this.createAreaForm());
4074
- return body;
4075
- }
4076
- createNoteBody() {
4077
- const empty = document.createElement("p");
4078
- empty.className = "dfwr-empty";
4079
- 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.";
4080
- return empty;
4081
- }
4082
- // Builds the note draft layer: the on-page marker/highlight plus its composer
4083
- // popover. When dockComposer is set the composer renders into the side panel
4084
- // instead of floating next to the marker (used for the docked review mode).
4085
- createNotePopover(draft, options = {}) {
4086
- const environment = this.config.getEnvironment();
4087
- const group = document.createElement("div");
4088
- group.className = "dfwr-note-draft";
4089
- if (!environment) return { layer: group, composer: void 0 };
4090
- const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
4091
- const hostPoint = toHostPoint(
4092
- isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
4093
- environment
4094
- );
4095
- let selectionHighlight;
4096
- if (draft.selection) {
4097
- const selection = toViewportSelection(draft.selection.viewport);
4098
- selectionHighlight = this.createSelectionHighlight(
4099
- isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
4100
- environment,
4101
- true
4102
- );
4103
- group.append(selectionHighlight);
4104
- }
4105
- const pin = document.createElement("button");
4106
- pin.className = "dfwr-note-pin";
4107
- pin.type = "button";
4108
- pin.setAttribute("aria-label", "Move note point");
4109
- pin.style.left = `${hostPoint.x}px`;
4110
- pin.style.top = `${hostPoint.y}px`;
4111
- const popover = document.createElement("div");
4112
- const position = getPopoverPosition(hostPoint, environment);
4113
- popover.className = [
4114
- "dfwr-note-popover",
4115
- isElementDraft ? "is-composer" : "",
4116
- options.dockComposer ? "is-docked-composer" : ""
4117
- ].filter(Boolean).join(" ");
4118
- if (options.dockComposer) {
4119
- popover.style.width = "100%";
4120
- } else if (isElementDraft) {
4121
- const selection = draft.selection ? toHostSelection(
4122
- this.getAdjustedDraftSelection(
4123
- toViewportSelection(draft.selection.viewport),
4124
- draft
4125
- ),
4126
- environment
4127
- ) : void 0;
4128
- const composer = this.getDraftComposerPosition({
4129
- selection,
4130
- environment,
4131
- composerPosition: draft.composerPosition,
4132
- estimatedHeight: 252
4133
- });
4134
- popover.style.left = `${composer.left}px`;
4135
- popover.style.top = `${composer.top}px`;
4136
- popover.style.width = `${composer.width}px`;
4137
- } else {
4138
- popover.style.left = `${position.left}px`;
4139
- popover.style.top = `${position.top}px`;
4140
- }
4141
- const form = document.createElement("form");
4142
- form.className = "dfwr-form";
4143
- const meta = isElementDraft ? void 0 : document.createElement("div");
4144
- if (meta) {
4145
- meta.className = "dfwr-item-date";
4146
- meta.textContent = formatNoteDraftMeta(draft);
4147
- }
4148
- const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(draft.title, (title) => {
4149
- const noteDraft = this.state.noteDraft;
4150
- if (!noteDraft) return;
4151
- this.config.actions.setNoteDraft({
4152
- ...noteDraft,
4153
- title
4154
- });
4155
- }) : void 0;
4156
- const textarea = document.createElement("textarea");
4157
- textarea.className = "dfwr-textarea";
4158
- textarea.placeholder = "Review comment";
4159
- textarea.rows = 4;
4160
- textarea.value = draft.comment ?? "";
4161
- textarea.addEventListener("input", () => {
4162
- const noteDraft = this.state.noteDraft;
4163
- if (!noteDraft) return;
4164
- this.config.actions.setNoteDraft({
4165
- ...noteDraft,
4166
- comment: textarea.value
4167
- });
4168
- });
4169
- const assigneeSelect = this.createDraftAssigneeSelect(
4170
- draft.assigneeId,
4171
- draft.assigneeName,
4172
- (assigneeId, assigneeName) => {
4173
- const noteDraft = this.state.noteDraft;
4174
- if (!noteDraft) return;
4175
- this.config.actions.setNoteDraft({
4176
- ...noteDraft,
4177
- assigneeId,
4178
- assigneeName
4179
- });
4345
+ return;
4180
4346
  }
4181
- );
4182
- const saveDraft = () => {
4183
- const currentDraft = this.state.noteDraft ?? draft;
4184
- const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
4185
- const comment = fields.comment;
4186
- if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
4187
- void this.config.actions.createItem({
4188
- kind: "note",
4189
- title: fields.title,
4190
- comment: this.withDraftAdjustmentComment(comment, currentDraft),
4191
- assigneeId: fields.assigneeId,
4192
- assigneeName: fields.assigneeName,
4193
- viewport: currentDraft.viewport,
4194
- anchor: currentDraft.anchor,
4195
- marker: currentDraft.marker,
4196
- selection: currentDraft.selection
4197
- });
4198
- };
4199
- const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
4200
- draft,
4201
- pin,
4202
- popover,
4203
- selectionHighlight,
4204
- textarea,
4205
- dockToggle: options.dockComposer
4206
- }) : void 0;
4207
- const actions = this.createFormActions("Save note", saveDraft, {
4208
- leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
4209
- });
4210
- const error = this.createDraftError();
4211
- form.append(
4212
- ...meta ? [meta] : [],
4213
- ...adjustmentControls ? [adjustmentControls.panel] : [],
4214
- ...titleInput ? [titleInput] : [],
4215
- textarea,
4216
- ...assigneeSelect ? [assigneeSelect] : [],
4217
- ...error ? [error] : [],
4218
- actions
4219
- );
4220
- const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
4221
- popover.append(
4222
- ...dragHandle ? [dragHandle] : [],
4223
- form
4224
- );
4225
- group.append(pin);
4226
- if (!options.dockComposer) {
4227
- group.append(popover);
4228
- }
4229
- if (dragHandle) {
4230
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
4231
- const noteDraft = this.state.noteDraft ?? draft;
4232
- this.config.actions.setNoteDraft({
4233
- ...noteDraft,
4234
- composerPosition,
4235
- comment: textarea.value
4236
- });
4237
- });
4238
4347
  }
4239
- this.attachDraftPinDrag(
4240
- pin,
4241
- isElementDraft || options.dockComposer ? void 0 : popover,
4242
- meta,
4243
- textarea
4244
- );
4245
- if (!options.dockComposer) {
4246
- window.setTimeout(() => {
4247
- if (draft.adjustment?.isActive) {
4248
- adjustmentControls?.focusTarget.focus();
4249
- return;
4250
- }
4251
- textarea.focus();
4252
- }, 0);
4348
+ if (!showCompactMarkers && !isHighlighted) {
4349
+ return;
4253
4350
  }
4254
- return {
4255
- layer: group,
4256
- composer: options.dockComposer ? popover : void 0
4257
- };
4258
- }
4259
- createDraftDragHandle(label) {
4260
- const handle = document.createElement("button");
4261
- handle.className = "dfwr-draft-drag-handle";
4262
- handle.type = "button";
4263
- handle.setAttribute("aria-label", label);
4264
- return handle;
4265
- }
4266
- createIcon(paths) {
4267
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
4268
- svg.setAttribute("aria-hidden", "true");
4269
- svg.setAttribute("viewBox", "0 0 24 24");
4270
- svg.setAttribute("fill", "none");
4271
- svg.setAttribute("stroke", "currentColor");
4272
- svg.setAttribute("stroke-width", "2.4");
4273
- svg.setAttribute("stroke-linecap", "round");
4274
- svg.setAttribute("stroke-linejoin", "round");
4275
- paths.forEach((d) => {
4276
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4277
- path.setAttribute("d", d);
4278
- svg.append(path);
4279
- });
4280
- return svg;
4281
- }
4282
- setAdjustmentToggleIcon(button, isActive) {
4283
- const paths = isActive ? ["M20 6 9 17l-5-5"] : [
4284
- "M12 2v20",
4285
- "M2 12h20",
4286
- "m9 5 3-3 3 3",
4287
- "m9 19 3 3 3-3",
4288
- "m5 9-3 3 3 3",
4289
- "m19 9 3 3-3 3"
4290
- ];
4291
- button.replaceChildren(this.createIcon(paths));
4292
- }
4293
- attachDraftComposerDrag(popover, handle, onMove) {
4294
- let isDragging = false;
4295
- let offsetX = 0;
4296
- let offsetY = 0;
4297
- const movePopover = (event) => {
4298
- const environment = this.config.getEnvironment();
4299
- if (!environment) return;
4300
- const position = this.getClampedComposerPosition(
4301
- {
4302
- x: event.clientX - offsetX,
4303
- y: event.clientY - offsetY
4304
- },
4305
- environment,
4306
- {
4307
- width: popover.offsetWidth,
4308
- height: popover.offsetHeight
4309
- },
4310
- this.getHostComposerBounds()
4311
- );
4312
- popover.style.left = `${position.x}px`;
4313
- popover.style.top = `${position.y}px`;
4314
- onMove(position);
4315
- };
4316
- handle.addEventListener("pointerdown", (event) => {
4317
- if (event.button !== 0) return;
4318
- const rect = popover.getBoundingClientRect();
4319
- offsetX = event.clientX - rect.left;
4320
- offsetY = event.clientY - rect.top;
4321
- isDragging = true;
4322
- event.preventDefault();
4323
- event.stopPropagation();
4324
- handle.setPointerCapture(event.pointerId);
4325
- popover.classList.add("is-dragging");
4326
- });
4327
- handle.addEventListener("pointermove", (event) => {
4328
- if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
4329
- event.preventDefault();
4330
- movePopover(event);
4331
- });
4332
- const stopDrag = (event) => {
4333
- if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
4334
- event.preventDefault();
4335
- event.stopPropagation();
4336
- isDragging = false;
4337
- handle.releasePointerCapture(event.pointerId);
4338
- popover.classList.remove("is-dragging");
4339
- movePopover(event);
4340
- };
4341
- handle.addEventListener("pointerup", stopDrag);
4342
- handle.addEventListener("pointercancel", stopDrag);
4343
- }
4344
- // Builds the element-adjustment controls (nudge the previewed element via
4345
- // arrow keys / buttons). Wires keyboard deltas to the draft transform and
4346
- // keeps the pin, popover, highlight and textarea in sync as the value changes.
4347
- createAdjustmentControls({
4348
- draft,
4349
- pin,
4350
- popover,
4351
- selectionHighlight,
4352
- textarea,
4353
- dockToggle
4354
- }) {
4355
- const panel = document.createElement("div");
4356
- panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
4357
- const header = document.createElement("div");
4358
- header.className = "dfwr-adjust-panel-header";
4359
- const help = document.createElement("div");
4360
- help.className = "dfwr-adjust-help";
4361
- help.textContent = this.getAdjustmentLabel();
4362
- const adjust = document.createElement("button");
4363
- adjust.className = "dfwr-adjust-toggle";
4364
- adjust.type = "button";
4365
- adjust.title = "Adjust DOM element with keyboard arrows";
4366
- adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
4367
- const xyStatus = document.createElement("div");
4368
- xyStatus.className = "dfwr-adjust-status";
4369
- const scaleStatus = document.createElement("div");
4370
- scaleStatus.className = "dfwr-adjust-status";
4371
- const syncControls = (nextDraft) => {
4372
- const isActive = nextDraft.adjustment?.isActive === true;
4373
- panel.classList.toggle("is-active", isActive);
4374
- adjust.classList.toggle("is-active", isActive);
4375
- adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
4376
- this.setAdjustmentToggleIcon(adjust, isActive);
4377
- adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
4378
- adjust.setAttribute(
4379
- "aria-label",
4380
- isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
4381
- );
4382
- const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
4383
- xyStatus.textContent = xyLine;
4384
- scaleStatus.textContent = scaleLine;
4385
- this.syncDraftAdjustmentUi({
4386
- draft: nextDraft,
4387
- pin,
4388
- selectionHighlight
4389
- });
4390
- };
4391
- const updateDraft = (updater) => {
4392
- const currentDraft = this.state.noteDraft ?? draft;
4393
- const nextDraft = updater(currentDraft);
4394
- this.config.actions.setNoteDraft({
4395
- ...nextDraft,
4396
- comment: textarea.value
4397
- });
4398
- syncControls(nextDraft);
4399
- };
4400
- adjust.addEventListener("click", () => {
4401
- updateDraft((currentDraft) => ({
4402
- ...currentDraft,
4403
- adjustment: {
4404
- x: currentDraft.adjustment?.x ?? 0,
4405
- y: currentDraft.adjustment?.y ?? 0,
4406
- scale: currentDraft.adjustment?.scale ?? 0,
4407
- isActive: currentDraft.adjustment?.isActive !== true
4408
- }
4409
- }));
4410
- adjust.focus();
4411
- });
4412
- popover.addEventListener("keydown", (event) => {
4413
- const currentDraft = this.state.noteDraft ?? draft;
4414
- if (currentDraft.adjustment?.isActive !== true) return;
4415
- const keyDelta = this.getAdjustmentKeyDelta(event);
4416
- if (!keyDelta) return;
4417
- event.preventDefault();
4418
- event.stopPropagation();
4419
- updateDraft((activeDraft) => ({
4420
- ...activeDraft,
4421
- adjustment: {
4422
- x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
4423
- y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
4424
- scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
4425
- isActive: true
4426
- }
4427
- }));
4428
- });
4429
- header.append(help);
4430
- if (!dockToggle) {
4431
- header.append(adjust);
4351
+ const point = getBoundMarkerPoint(item, environment);
4352
+ if (!point || !isPointInViewport(point.viewport, environment)) {
4353
+ return;
4432
4354
  }
4433
- panel.append(header, xyStatus, scaleStatus);
4434
- syncControls(draft);
4435
- return {
4436
- panel,
4437
- focusTarget: adjust,
4438
- actionButton: dockToggle ? adjust : void 0
4439
- };
4440
- }
4441
- getAdjustmentKeyDelta(event) {
4442
- const step = event.shiftKey ? 10 : 1;
4443
- if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
4444
- if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
4445
- if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
4446
- if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
4447
- if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
4448
- if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
4449
- return void 0;
4450
- }
4451
- syncDraftAdjustmentUi({
4452
- draft,
4453
- pin,
4454
- selectionHighlight
4455
- }) {
4456
- const environment = this.config.getEnvironment();
4457
- if (!environment) return;
4458
- const hostPoint = toHostPoint(
4459
- this.getAdjustedDraftPoint(draft.marker.viewport, draft),
4460
- environment
4355
+ const hostPoint = toHostPoint(point.viewport, environment);
4356
+ const marker = createMarkerElement(
4357
+ item.id,
4358
+ hostPoint,
4359
+ displayLabel,
4360
+ scope,
4361
+ point.isBound,
4362
+ isHighlighted
4461
4363
  );
4462
- pin.style.left = `${hostPoint.x}px`;
4463
- pin.style.top = `${hostPoint.y}px`;
4464
- if (draft.selection && selectionHighlight) {
4465
- const rect = toHostSelection(
4466
- this.getAdjustedDraftSelection(
4467
- toViewportSelection(draft.selection.viewport),
4468
- draft
4469
- ),
4470
- environment
4471
- );
4472
- selectionHighlight.style.left = `${rect.left}px`;
4473
- selectionHighlight.style.top = `${rect.top}px`;
4474
- selectionHighlight.style.width = `${rect.width}px`;
4475
- selectionHighlight.style.height = `${rect.height}px`;
4476
- }
4477
- this.syncDraftPreview(draft);
4478
- }
4479
- createAreaForm() {
4480
- const form = document.createElement("form");
4481
- form.className = "dfwr-form";
4482
- const areaDraft = this.state.areaDraft;
4483
- if (!areaDraft) {
4484
- const empty = document.createElement("p");
4485
- empty.className = "dfwr-empty";
4486
- empty.textContent = "Drag on the page to select an area.";
4487
- form.append(empty);
4488
- return form;
4489
- }
4490
- form.append(this.createAreaMetricsPanel(areaDraft));
4491
- const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(areaDraft.title, (title) => {
4492
- const draft = this.state.areaDraft;
4493
- if (!draft) return;
4494
- this.config.actions.setAreaDraft({
4364
+ marker.title = `${displayLabel} / ${item.comment}
4365
+ ${formatItemMeta(item)}`;
4366
+ layer.append(marker);
4367
+ });
4368
+ return layer;
4369
+ }
4370
+
4371
+ // src/core/view/area.draft.ts
4372
+ function createAreaForm(context) {
4373
+ const { config } = context;
4374
+ const form = document.createElement("form");
4375
+ form.className = "dfwr-form";
4376
+ const areaDraft = config.getState().areaDraft;
4377
+ if (!areaDraft) {
4378
+ const empty = document.createElement("p");
4379
+ empty.className = "dfwr-empty";
4380
+ empty.textContent = "Drag on the page to select an area.";
4381
+ form.append(empty);
4382
+ return form;
4383
+ }
4384
+ form.append(createAreaMetricsPanel(context, areaDraft));
4385
+ const titleInput = isTitleFieldEnabled(config.options) ? createDraftTitleInput(areaDraft.title, (title) => {
4386
+ const draft = config.getState().areaDraft;
4387
+ if (!draft) return;
4388
+ config.actions.setAreaDraft({
4389
+ ...draft,
4390
+ title
4391
+ });
4392
+ }) : void 0;
4393
+ const textarea = document.createElement("textarea");
4394
+ textarea.className = "dfwr-textarea";
4395
+ textarea.placeholder = "Area comment";
4396
+ textarea.rows = 4;
4397
+ textarea.value = areaDraft.comment ?? "";
4398
+ textarea.addEventListener("input", () => {
4399
+ const draft = config.getState().areaDraft;
4400
+ if (!draft) return;
4401
+ config.actions.setAreaDraft({
4402
+ ...draft,
4403
+ comment: textarea.value
4404
+ });
4405
+ });
4406
+ attachDraftImagePasteQueue(textarea, {
4407
+ getAttachments: () => config.getState().areaDraft?.attachments ?? areaDraft.attachments,
4408
+ onAttachmentsChange: (attachments) => {
4409
+ const draft = config.getState().areaDraft ?? areaDraft;
4410
+ config.actions.setAreaDraft({
4411
+ ...draft,
4412
+ comment: textarea.value,
4413
+ attachments
4414
+ });
4415
+ },
4416
+ onCommentChange: (comment) => {
4417
+ const draft = config.getState().areaDraft ?? areaDraft;
4418
+ config.actions.setAreaDraft({
4495
4419
  ...draft,
4496
- title
4420
+ comment
4497
4421
  });
4498
- }) : void 0;
4499
- const textarea = document.createElement("textarea");
4500
- textarea.className = "dfwr-textarea";
4501
- textarea.placeholder = "Area comment";
4502
- textarea.rows = 4;
4503
- textarea.value = areaDraft.comment ?? "";
4504
- textarea.addEventListener("input", () => {
4505
- const draft = this.state.areaDraft;
4422
+ },
4423
+ onPasteComplete: () => config.actions.render()
4424
+ });
4425
+ const assigneeSelect = createDraftAssigneeSelect(
4426
+ config.options,
4427
+ areaDraft.assigneeId,
4428
+ areaDraft.assigneeName,
4429
+ (assigneeId, assigneeName) => {
4430
+ const draft = config.getState().areaDraft;
4506
4431
  if (!draft) return;
4507
- this.config.actions.setAreaDraft({
4432
+ config.actions.setAreaDraft({
4508
4433
  ...draft,
4509
- comment: textarea.value
4434
+ assigneeId,
4435
+ assigneeName
4510
4436
  });
4511
- });
4512
- const assigneeSelect = this.createDraftAssigneeSelect(
4513
- areaDraft.assigneeId,
4514
- areaDraft.assigneeName,
4515
- (assigneeId, assigneeName) => {
4516
- const draft = this.state.areaDraft;
4517
- if (!draft) return;
4518
- this.config.actions.setAreaDraft({
4519
- ...draft,
4520
- assigneeId,
4521
- assigneeName
4522
- });
4523
- }
4524
- );
4525
- const actions = this.createFormActions("Save area", () => {
4526
- const draft = this.state.areaDraft;
4527
- const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
4437
+ }
4438
+ );
4439
+ const actions = createFormActions({
4440
+ saveLabel: "Save area",
4441
+ isSaving: config.getState().isCreatingItem,
4442
+ onCancel: context.cancelDraft,
4443
+ onSave: () => {
4444
+ const draft = config.getState().areaDraft;
4445
+ const fields = getDraftFields(
4446
+ config.options,
4447
+ titleInput,
4448
+ textarea,
4449
+ assigneeSelect
4450
+ );
4528
4451
  const comment = fields.comment;
4529
- if (!comment || !draft) return;
4530
- void this.config.actions.createItem({
4452
+ if (!comment && !draft?.attachments?.length || !draft) return;
4453
+ void config.actions.createItem({
4531
4454
  kind: "area",
4532
4455
  title: fields.title,
4533
4456
  comment,
@@ -4536,603 +4459,1077 @@ ${adjustment}` : adjustment;
4536
4459
  viewport: draft.viewport,
4537
4460
  anchor: draft.anchor,
4538
4461
  marker: draft.marker,
4539
- selection: draft.selection
4462
+ selection: draft.selection,
4463
+ attachments: draft.attachments
4540
4464
  });
4541
- });
4542
- const error = this.createDraftError();
4543
- form.append(
4544
- ...titleInput ? [titleInput] : [],
4545
- textarea,
4546
- ...assigneeSelect ? [assigneeSelect] : [],
4547
- ...error ? [error] : [],
4548
- actions
4549
- );
4550
- return form;
4551
- }
4552
- createAreaMetricsPanel(draft) {
4553
- const panel = document.createElement("div");
4554
- panel.className = "dfwr-adjust-panel is-area-metrics-panel";
4555
- const help = document.createElement("div");
4556
- help.className = "dfwr-adjust-help";
4557
- const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
4558
- this.getAreaDraftMetricSelection(draft),
4559
- draft.viewport
4560
- );
4561
- help.textContent = labelLine;
4562
- const xyStatus = document.createElement("div");
4563
- xyStatus.className = "dfwr-adjust-status";
4564
- xyStatus.textContent = xyLine;
4565
- const sizeStatus = document.createElement("div");
4566
- sizeStatus.className = "dfwr-adjust-status";
4567
- sizeStatus.textContent = sizeLine;
4568
- panel.append(help, xyStatus, sizeStatus);
4569
- return panel;
4570
- }
4571
- createAreaDraftOverlay(draft) {
4572
- const layer = document.createElement("div");
4573
- layer.className = "dfwr-area-preview-layer";
4574
- const environment = this.config.getEnvironment();
4575
- if (!environment || !draft.selection) return layer;
4576
- const selection = toViewportSelection(draft.selection.viewport);
4577
- layer.append(this.createSelectionHighlight(selection, environment, true));
4578
- if (draft.marker) {
4579
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
4580
- layer.append(
4581
- this.createMarkerElement(
4582
- void 0,
4583
- hostPoint,
4584
- "\u2022",
4585
- getReviewViewportScope(
4586
- draft.viewport,
4587
- this.config.options.viewports?.presets
4588
- ),
4589
- true,
4590
- true
4591
- )
4465
+ },
4466
+ leading: [
4467
+ createDraftCaptureButton(config, areaDraft, {
4468
+ kind: "area",
4469
+ textarea
4470
+ })
4471
+ ]
4472
+ });
4473
+ const error = createDraftError(config.getState().draftError);
4474
+ const attachmentQueue = createDraftAttachmentQueue(
4475
+ document,
4476
+ areaDraft.attachments,
4477
+ (attachmentId) => {
4478
+ const draft = config.getState().areaDraft ?? areaDraft;
4479
+ const attachments = removeDraftAttachment(
4480
+ draft.attachments,
4481
+ attachmentId
4592
4482
  );
4483
+ config.actions.setAreaDraft({
4484
+ ...draft,
4485
+ comment: textarea.value,
4486
+ attachments: attachments.length > 0 ? attachments : void 0
4487
+ });
4488
+ config.actions.render();
4593
4489
  }
4594
- return layer;
4490
+ );
4491
+ form.append(
4492
+ ...titleInput ? [titleInput] : [],
4493
+ textarea,
4494
+ ...attachmentQueue ? [attachmentQueue] : [],
4495
+ ...assigneeSelect ? [assigneeSelect] : [],
4496
+ ...error ? [error] : [],
4497
+ actions
4498
+ );
4499
+ return form;
4500
+ }
4501
+ function createAreaMetricsPanel(context, draft) {
4502
+ const panel = document.createElement("div");
4503
+ panel.className = "dfwr-adjust-panel is-area-metrics-panel";
4504
+ const [labelLine, xyLine, sizeLine] = getSelectionMetricLines(
4505
+ getAreaDraftMetricSelection(draft),
4506
+ draft.viewport,
4507
+ context.config.options.viewports?.presets
4508
+ );
4509
+ const help = document.createElement("div");
4510
+ help.className = "dfwr-adjust-help";
4511
+ help.textContent = labelLine;
4512
+ const xyStatus = document.createElement("div");
4513
+ xyStatus.className = "dfwr-adjust-status";
4514
+ xyStatus.textContent = xyLine;
4515
+ const sizeStatus = document.createElement("div");
4516
+ sizeStatus.className = "dfwr-adjust-status";
4517
+ sizeStatus.textContent = sizeLine;
4518
+ panel.append(help, xyStatus, sizeStatus);
4519
+ return panel;
4520
+ }
4521
+ function createAreaDraftOverlay(context, draft) {
4522
+ const { config } = context;
4523
+ const layer = document.createElement("div");
4524
+ layer.className = "dfwr-area-preview-layer";
4525
+ const environment = config.getEnvironment();
4526
+ if (!environment || !draft.selection) return layer;
4527
+ const selection = toViewportSelection(draft.selection.viewport);
4528
+ layer.append(createSelectionHighlight(selection, environment, true));
4529
+ if (draft.marker) {
4530
+ const hostPoint = toHostPoint(draft.marker.viewport, environment);
4531
+ layer.append(
4532
+ createMarkerElement(
4533
+ void 0,
4534
+ hostPoint,
4535
+ "\u2022",
4536
+ getReviewViewportScope(
4537
+ draft.viewport,
4538
+ config.options.viewports?.presets
4539
+ ),
4540
+ true,
4541
+ true
4542
+ )
4543
+ );
4595
4544
  }
4596
- createAreaDraftPopover(draft, options = {}) {
4597
- const environment = this.config.getEnvironment();
4598
- const popover = document.createElement("div");
4599
- popover.className = [
4600
- "dfwr-area-draft",
4601
- "is-composer",
4602
- options.dockComposer ? "is-docked-composer" : ""
4603
- ].filter(Boolean).join(" ");
4604
- if (options.dockComposer) {
4605
- popover.style.width = "100%";
4606
- } else if (environment && draft.selection) {
4607
- const selection = toHostSelection(
4608
- toViewportSelection(draft.selection.viewport),
4609
- environment
4610
- );
4611
- const composer = this.getDraftComposerPosition({
4612
- selection,
4613
- environment,
4614
- composerPosition: draft.composerPosition,
4615
- estimatedHeight: 220
4616
- });
4617
- popover.style.left = `${composer.left}px`;
4618
- popover.style.top = `${composer.top}px`;
4619
- popover.style.width = `${composer.width}px`;
4620
- popover.style.right = "auto";
4621
- }
4622
- const dragHandle = options.dockComposer ? void 0 : this.createDraftDragHandle("Move area composer");
4623
- popover.append(
4624
- ...dragHandle ? [dragHandle] : [],
4625
- this.createAreaForm()
4545
+ return layer;
4546
+ }
4547
+ function createAreaDraftPopover(context, draft, options = {}) {
4548
+ const { config } = context;
4549
+ const environment = config.getEnvironment();
4550
+ const popover = document.createElement("div");
4551
+ popover.className = [
4552
+ "dfwr-area-draft",
4553
+ "is-composer",
4554
+ options.dockComposer ? "is-docked-composer" : ""
4555
+ ].filter(Boolean).join(" ");
4556
+ if (options.dockComposer) {
4557
+ popover.style.width = "100%";
4558
+ } else if (environment && draft.selection) {
4559
+ const selection = toHostSelection(
4560
+ toViewportSelection(draft.selection.viewport),
4561
+ environment
4626
4562
  );
4627
- if (dragHandle) {
4628
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
4629
- const areaDraft = this.state.areaDraft ?? draft;
4630
- this.config.actions.setAreaDraft({
4563
+ const composer = getDraftComposerPosition({
4564
+ selection,
4565
+ environment,
4566
+ composerPosition: draft.composerPosition,
4567
+ estimatedHeight: 220
4568
+ });
4569
+ popover.style.left = `${composer.left}px`;
4570
+ popover.style.top = `${composer.top}px`;
4571
+ popover.style.width = `${composer.width}px`;
4572
+ popover.style.right = "auto";
4573
+ }
4574
+ const dragHandle = options.dockComposer ? void 0 : createDraftDragHandle("Move area composer");
4575
+ popover.append(
4576
+ ...dragHandle ? [dragHandle] : [],
4577
+ createAreaForm(context)
4578
+ );
4579
+ if (dragHandle) {
4580
+ attachDraftComposerDrag({
4581
+ getEnvironment: () => config.getEnvironment(),
4582
+ popover,
4583
+ handle: dragHandle,
4584
+ onMove: (composerPosition) => {
4585
+ const areaDraft = config.getState().areaDraft ?? draft;
4586
+ config.actions.setAreaDraft({
4631
4587
  ...areaDraft,
4632
4588
  composerPosition
4633
4589
  });
4590
+ }
4591
+ });
4592
+ }
4593
+ return popover;
4594
+ }
4595
+
4596
+ // src/core/view/dom.draft.ts
4597
+ function createDomDraftLayer(context, draft, options = {}) {
4598
+ const { config } = context;
4599
+ const presets = config.options.viewports?.presets;
4600
+ const environment = config.getEnvironment();
4601
+ const group = document.createElement("div");
4602
+ group.className = "dfwr-dom-draft";
4603
+ if (!environment) return { layer: group, composer: void 0 };
4604
+ const isElementDraft = config.getState().mode === "element" && Boolean(draft.selection);
4605
+ const hostPoint = toHostPoint(
4606
+ isElementDraft ? getAdjustedDraftPoint(draft.marker.viewport, draft, presets) : draft.marker.viewport,
4607
+ environment
4608
+ );
4609
+ let selectionHighlight;
4610
+ if (draft.selection) {
4611
+ const selection = toViewportSelection(draft.selection.viewport);
4612
+ selectionHighlight = createSelectionHighlight(
4613
+ isElementDraft ? getAdjustedDraftSelection(selection, draft, presets) : selection,
4614
+ environment,
4615
+ true
4616
+ );
4617
+ group.append(selectionHighlight);
4618
+ }
4619
+ const pin = document.createElement("button");
4620
+ pin.className = "dfwr-dom-pin";
4621
+ pin.type = "button";
4622
+ pin.setAttribute("aria-label", "Move DOM point");
4623
+ pin.style.left = `${hostPoint.x}px`;
4624
+ pin.style.top = `${hostPoint.y}px`;
4625
+ const popover = document.createElement("div");
4626
+ const position = getPopoverPosition(hostPoint, environment);
4627
+ popover.className = [
4628
+ "dfwr-dom-popover",
4629
+ isElementDraft ? "is-composer" : "",
4630
+ options.dockComposer ? "is-docked-composer" : ""
4631
+ ].filter(Boolean).join(" ");
4632
+ if (options.dockComposer) {
4633
+ popover.style.width = "100%";
4634
+ } else if (isElementDraft) {
4635
+ const selection = draft.selection ? toHostSelection(
4636
+ getAdjustedDraftSelection(
4637
+ toViewportSelection(draft.selection.viewport),
4638
+ draft,
4639
+ presets
4640
+ ),
4641
+ environment
4642
+ ) : void 0;
4643
+ const composer = getDraftComposerPosition({
4644
+ selection,
4645
+ environment,
4646
+ composerPosition: draft.composerPosition,
4647
+ estimatedHeight: 252
4648
+ });
4649
+ popover.style.left = `${composer.left}px`;
4650
+ popover.style.top = `${composer.top}px`;
4651
+ popover.style.width = `${composer.width}px`;
4652
+ } else {
4653
+ popover.style.left = `${position.left}px`;
4654
+ popover.style.top = `${position.top}px`;
4655
+ }
4656
+ const form = document.createElement("form");
4657
+ form.className = "dfwr-form";
4658
+ const meta = isElementDraft ? void 0 : document.createElement("div");
4659
+ if (meta) {
4660
+ meta.className = "dfwr-item-date";
4661
+ meta.textContent = formatDomDraftMeta(draft);
4662
+ }
4663
+ const titleInput = isTitleFieldEnabled(config.options) ? createDraftTitleInput(draft.title, (title) => {
4664
+ const domDraft = config.getState().domDraft;
4665
+ if (!domDraft) return;
4666
+ config.actions.setDomDraft({
4667
+ ...domDraft,
4668
+ title
4669
+ });
4670
+ }) : void 0;
4671
+ const textarea = document.createElement("textarea");
4672
+ textarea.className = "dfwr-textarea";
4673
+ textarea.placeholder = "Review comment";
4674
+ textarea.rows = 4;
4675
+ textarea.value = draft.comment ?? "";
4676
+ textarea.addEventListener("input", () => {
4677
+ const domDraft = config.getState().domDraft;
4678
+ if (!domDraft) return;
4679
+ config.actions.setDomDraft({
4680
+ ...domDraft,
4681
+ comment: textarea.value
4682
+ });
4683
+ });
4684
+ attachDraftImagePasteQueue(textarea, {
4685
+ getAttachments: () => config.getState().domDraft?.attachments ?? draft.attachments,
4686
+ onAttachmentsChange: (attachments) => {
4687
+ const domDraft = config.getState().domDraft ?? draft;
4688
+ config.actions.setDomDraft({
4689
+ ...domDraft,
4690
+ comment: textarea.value,
4691
+ attachments
4692
+ });
4693
+ },
4694
+ onCommentChange: (comment) => {
4695
+ const domDraft = config.getState().domDraft ?? draft;
4696
+ config.actions.setDomDraft({
4697
+ ...domDraft,
4698
+ comment
4699
+ });
4700
+ },
4701
+ onPasteComplete: () => config.actions.render()
4702
+ });
4703
+ const assigneeSelect = createDraftAssigneeSelect(
4704
+ config.options,
4705
+ draft.assigneeId,
4706
+ draft.assigneeName,
4707
+ (assigneeId, assigneeName) => {
4708
+ const domDraft = config.getState().domDraft;
4709
+ if (!domDraft) return;
4710
+ config.actions.setDomDraft({
4711
+ ...domDraft,
4712
+ assigneeId,
4713
+ assigneeName
4634
4714
  });
4635
4715
  }
4636
- return popover;
4637
- }
4638
- createFormActions(saveLabel, onSave, options) {
4639
- const actions = document.createElement("div");
4640
- actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
4641
- const isSaving = this.state.isCreatingItem;
4642
- const save = document.createElement("button");
4643
- save.className = "dfwr-button is-primary";
4644
- save.type = "button";
4645
- save.disabled = isSaving;
4646
- save.setAttribute("aria-busy", isSaving ? "true" : "false");
4647
- if (isSaving) {
4648
- save.append(this.createSpinner("dfwr-spinner"), "Saving...");
4649
- } else {
4650
- save.textContent = saveLabel;
4651
- }
4652
- save.addEventListener("click", (event) => {
4653
- event.preventDefault();
4654
- event.stopPropagation();
4655
- if (this.state.isCreatingItem) return;
4656
- onSave();
4716
+ );
4717
+ const saveDraft = () => {
4718
+ const currentDraft = config.getState().domDraft ?? draft;
4719
+ const fields = getDraftFields(
4720
+ config.options,
4721
+ titleInput,
4722
+ textarea,
4723
+ assigneeSelect
4724
+ );
4725
+ const comment = fields.comment;
4726
+ const hasAttachments = Boolean(currentDraft.attachments?.length);
4727
+ if (!comment && !hasDraftAdjustment(currentDraft, presets) && !hasAttachments) {
4728
+ return;
4729
+ }
4730
+ void config.actions.createItem({
4731
+ kind: "dom",
4732
+ title: fields.title,
4733
+ comment: withDraftAdjustmentComment(
4734
+ comment,
4735
+ currentDraft,
4736
+ config.options
4737
+ ),
4738
+ assigneeId: fields.assigneeId,
4739
+ assigneeName: fields.assigneeName,
4740
+ viewport: currentDraft.viewport,
4741
+ anchor: currentDraft.anchor,
4742
+ marker: currentDraft.marker,
4743
+ selection: currentDraft.selection,
4744
+ attachments: currentDraft.attachments
4657
4745
  });
4658
- const cancel = document.createElement("button");
4659
- cancel.className = "dfwr-button";
4660
- cancel.type = "button";
4661
- cancel.disabled = isSaving;
4662
- cancel.textContent = "Cancel";
4663
- cancel.addEventListener("click", (event) => {
4664
- this.cancelDraft(event);
4746
+ };
4747
+ const adjustmentControls = isElementDraft ? createAdjustmentControls(context, {
4748
+ draft,
4749
+ pin,
4750
+ popover,
4751
+ selectionHighlight,
4752
+ textarea,
4753
+ dockToggle: options.dockComposer
4754
+ }) : void 0;
4755
+ const leadingActions = [
4756
+ adjustmentControls?.actionButton,
4757
+ createDraftCaptureButton(config, draft, {
4758
+ kind: "dom",
4759
+ isElementDraft,
4760
+ textarea
4761
+ })
4762
+ ].filter((element) => Boolean(element));
4763
+ const actions = createFormActions({
4764
+ saveLabel: "Save DOM QA",
4765
+ isSaving: config.getState().isCreatingItem,
4766
+ onSave: saveDraft,
4767
+ onCancel: context.cancelDraft,
4768
+ leading: leadingActions.length > 0 ? leadingActions : void 0
4769
+ });
4770
+ const error = createDraftError(config.getState().draftError);
4771
+ const attachmentQueue = createDraftAttachmentQueue(
4772
+ document,
4773
+ draft.attachments,
4774
+ (attachmentId) => {
4775
+ const domDraft = config.getState().domDraft ?? draft;
4776
+ const attachments = removeDraftAttachment(
4777
+ domDraft.attachments,
4778
+ attachmentId
4779
+ );
4780
+ config.actions.setDomDraft({
4781
+ ...domDraft,
4782
+ comment: textarea.value,
4783
+ attachments: attachments.length > 0 ? attachments : void 0
4784
+ });
4785
+ config.actions.render();
4786
+ }
4787
+ );
4788
+ form.append(
4789
+ ...meta ? [meta] : [],
4790
+ ...adjustmentControls ? [adjustmentControls.panel] : [],
4791
+ ...titleInput ? [titleInput] : [],
4792
+ textarea,
4793
+ ...attachmentQueue ? [attachmentQueue] : [],
4794
+ ...assigneeSelect ? [assigneeSelect] : [],
4795
+ ...error ? [error] : [],
4796
+ actions
4797
+ );
4798
+ const dragHandle = isElementDraft && !options.dockComposer ? createDraftDragHandle("Move DOM composer") : void 0;
4799
+ popover.append(...dragHandle ? [dragHandle] : [], form);
4800
+ group.append(pin);
4801
+ if (!options.dockComposer) {
4802
+ group.append(popover);
4803
+ }
4804
+ if (dragHandle) {
4805
+ attachDraftComposerDrag({
4806
+ getEnvironment: () => config.getEnvironment(),
4807
+ popover,
4808
+ handle: dragHandle,
4809
+ onMove: (composerPosition) => {
4810
+ const domDraft = config.getState().domDraft ?? draft;
4811
+ config.actions.setDomDraft({
4812
+ ...domDraft,
4813
+ composerPosition,
4814
+ comment: textarea.value
4815
+ });
4816
+ }
4665
4817
  });
4666
- if (options?.leading?.length) {
4667
- actions.classList.add("has-leading");
4668
- const leading = document.createElement("div");
4669
- leading.className = "dfwr-actions-leading";
4670
- leading.append(...options.leading);
4671
- const primary = document.createElement("div");
4672
- primary.className = "dfwr-actions-primary";
4673
- primary.append(save, cancel);
4674
- actions.append(leading, primary);
4675
- return actions;
4676
- }
4677
- if (options?.beforeSave?.length || options?.className) {
4678
- actions.append(cancel, ...options.beforeSave ?? [], save);
4679
- return actions;
4680
- }
4681
- actions.append(save, cancel);
4682
- return actions;
4683
4818
  }
4684
- createSpinner(className) {
4685
- const spinner = document.createElement("span");
4686
- spinner.className = className;
4687
- spinner.setAttribute("aria-hidden", "true");
4688
- return spinner;
4689
- }
4690
- createDraftError() {
4691
- if (!this.state.draftError) return void 0;
4692
- const error = document.createElement("p");
4693
- error.className = "dfwr-form-error";
4694
- error.setAttribute("role", "alert");
4695
- error.textContent = this.state.draftError;
4696
- return error;
4697
- }
4698
- createList() {
4699
- const section = document.createElement("div");
4700
- section.className = "dfwr-list";
4701
- const state = this.state;
4702
- const heading = document.createElement("div");
4703
- heading.className = "dfwr-list-heading";
4704
- heading.textContent = `Review items (${state.items.length})`;
4705
- section.append(heading);
4706
- if (state.items.length === 0) {
4707
- const empty = document.createElement("p");
4708
- empty.className = "dfwr-empty";
4709
- empty.textContent = "No review items on this page.";
4710
- section.append(empty);
4711
- return section;
4712
- }
4713
- for (const numberedItem of getNumberedReviewItems(
4714
- state.items,
4715
- this.config.options.viewports?.presets
4716
- )) {
4717
- section.append(this.createListItem(numberedItem));
4718
- }
4719
- return section;
4819
+ attachDraftPinDrag(context, {
4820
+ pin,
4821
+ popover: isElementDraft || options.dockComposer ? void 0 : popover,
4822
+ meta,
4823
+ textarea
4824
+ });
4825
+ if (!options.dockComposer) {
4826
+ window.setTimeout(() => {
4827
+ if (draft.adjustment?.isActive) {
4828
+ adjustmentControls?.focusTarget.focus();
4829
+ return;
4830
+ }
4831
+ textarea.focus();
4832
+ }, 0);
4720
4833
  }
4721
- createListItem(numberedItem) {
4722
- const { item } = numberedItem;
4723
- const row = document.createElement("article");
4724
- row.className = "dfwr-item";
4725
- row.tabIndex = 0;
4726
- row.setAttribute("role", "button");
4727
- row.setAttribute(
4834
+ return {
4835
+ layer: group,
4836
+ composer: options.dockComposer ? popover : void 0
4837
+ };
4838
+ }
4839
+ function createAdjustmentControls(context, {
4840
+ draft,
4841
+ pin,
4842
+ popover,
4843
+ selectionHighlight,
4844
+ textarea,
4845
+ dockToggle
4846
+ }) {
4847
+ const { config } = context;
4848
+ const presets = config.options.viewports?.presets;
4849
+ const panel = document.createElement("div");
4850
+ panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
4851
+ const header = document.createElement("div");
4852
+ header.className = "dfwr-adjust-panel-header";
4853
+ const help = document.createElement("div");
4854
+ help.className = "dfwr-adjust-help";
4855
+ help.textContent = getAdjustmentLabel(config.options);
4856
+ const adjust = document.createElement("button");
4857
+ adjust.className = "dfwr-adjust-toggle";
4858
+ adjust.type = "button";
4859
+ adjust.title = "Adjust DOM element with keyboard arrows";
4860
+ adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
4861
+ const xyStatus = document.createElement("div");
4862
+ xyStatus.className = "dfwr-adjust-status";
4863
+ const scaleStatus = document.createElement("div");
4864
+ scaleStatus.className = "dfwr-adjust-status";
4865
+ const syncControls = (nextDraft) => {
4866
+ const isActive = nextDraft.adjustment?.isActive === true;
4867
+ panel.classList.toggle("is-active", isActive);
4868
+ adjust.classList.toggle("is-active", isActive);
4869
+ adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
4870
+ setAdjustmentToggleIcon(adjust, isActive);
4871
+ adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
4872
+ adjust.setAttribute(
4728
4873
  "aria-label",
4729
- `Restore review item: ${item.title ?? item.comment}`
4874
+ isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
4875
+ );
4876
+ const [xyLine, scaleLine] = getDraftAdjustmentMetricLines(
4877
+ nextDraft,
4878
+ presets
4730
4879
  );
4731
- row.addEventListener("click", () => {
4732
- void this.config.actions.restoreItem(item);
4880
+ xyStatus.textContent = xyLine;
4881
+ scaleStatus.textContent = scaleLine;
4882
+ syncDraftAdjustmentUi(context, {
4883
+ draft: nextDraft,
4884
+ pin,
4885
+ selectionHighlight
4733
4886
  });
4734
- row.addEventListener("keydown", (event) => {
4735
- if (event.key !== "Enter" && event.key !== " ") return;
4736
- event.preventDefault();
4737
- void this.config.actions.restoreItem(item);
4887
+ };
4888
+ const updateDraft = (updater) => {
4889
+ const currentDraft = config.getState().domDraft ?? draft;
4890
+ const nextDraft = updater(currentDraft);
4891
+ config.actions.setDomDraft({
4892
+ ...nextDraft,
4893
+ comment: textarea.value
4738
4894
  });
4739
- const body = document.createElement("div");
4740
- body.className = "dfwr-item-body";
4741
- const badges = document.createElement("div");
4742
- badges.className = "dfwr-item-badges";
4743
- const scope = document.createElement("div");
4744
- scope.className = `dfwr-item-scope is-scope-${numberedItem.scope}`;
4745
- scope.textContent = numberedItem.displayLabel;
4746
- const kind = document.createElement("div");
4747
- kind.className = "dfwr-item-kind";
4748
- kind.textContent = item.kind;
4749
- badges.append(scope, kind);
4750
- const title = this.isTitleFieldEnabled() ? item.title?.trim() : "";
4751
- const titleElement = title ? document.createElement("strong") : void 0;
4752
- if (title && titleElement) {
4753
- titleElement.className = "dfwr-item-title";
4754
- titleElement.textContent = title;
4755
- }
4756
- const comment = document.createElement("p");
4757
- comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
4758
- comment.textContent = item.comment;
4759
- const date = document.createElement("time");
4760
- date.className = "dfwr-item-date";
4761
- date.dateTime = item.createdAt;
4762
- date.textContent = formatItemMeta(item);
4763
- body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
4764
- const actions = document.createElement("div");
4765
- actions.className = "dfwr-item-actions";
4766
- actions.addEventListener("click", (event) => event.stopPropagation());
4767
- actions.addEventListener("keydown", (event) => event.stopPropagation());
4768
- const remove = document.createElement("button");
4769
- remove.className = "dfwr-icon-button";
4770
- remove.type = "button";
4771
- remove.textContent = "x";
4772
- remove.setAttribute("aria-label", "Delete");
4773
- remove.addEventListener("click", (event) => {
4774
- event.stopPropagation();
4775
- void this.config.actions.removeItem(item.id).then(() => this.config.actions.reload());
4895
+ syncControls(nextDraft);
4896
+ };
4897
+ adjust.addEventListener("click", () => {
4898
+ updateDraft((currentDraft) => ({
4899
+ ...currentDraft,
4900
+ adjustment: {
4901
+ x: currentDraft.adjustment?.x ?? 0,
4902
+ y: currentDraft.adjustment?.y ?? 0,
4903
+ scale: currentDraft.adjustment?.scale ?? 0,
4904
+ isActive: currentDraft.adjustment?.isActive !== true
4905
+ }
4906
+ }));
4907
+ adjust.focus();
4908
+ });
4909
+ popover.addEventListener("keydown", (event) => {
4910
+ const currentDraft = config.getState().domDraft ?? draft;
4911
+ if (currentDraft.adjustment?.isActive !== true) return;
4912
+ const keyDelta = getAdjustmentKeyDelta(event);
4913
+ if (!keyDelta) return;
4914
+ event.preventDefault();
4915
+ event.stopPropagation();
4916
+ updateDraft((activeDraft) => ({
4917
+ ...activeDraft,
4918
+ adjustment: {
4919
+ x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
4920
+ y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
4921
+ scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
4922
+ isActive: true
4923
+ }
4924
+ }));
4925
+ });
4926
+ header.append(help);
4927
+ if (!dockToggle) {
4928
+ header.append(adjust);
4929
+ }
4930
+ panel.append(header, xyStatus, scaleStatus);
4931
+ syncControls(draft);
4932
+ return {
4933
+ panel,
4934
+ focusTarget: adjust,
4935
+ // 도킹 모드에서는 토글 버튼을 폼 액션 줄(leading)로 옮긴다.
4936
+ actionButton: dockToggle ? adjust : void 0
4937
+ };
4938
+ }
4939
+ function getAdjustmentKeyDelta(event) {
4940
+ const step = event.shiftKey ? 10 : 1;
4941
+ if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
4942
+ if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
4943
+ if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
4944
+ if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
4945
+ if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
4946
+ if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
4947
+ return void 0;
4948
+ }
4949
+ function syncDraftAdjustmentUi(context, {
4950
+ draft,
4951
+ pin,
4952
+ selectionHighlight
4953
+ }) {
4954
+ const { config } = context;
4955
+ const presets = config.options.viewports?.presets;
4956
+ const environment = config.getEnvironment();
4957
+ if (!environment) return;
4958
+ const hostPoint = toHostPoint(
4959
+ getAdjustedDraftPoint(draft.marker.viewport, draft, presets),
4960
+ environment
4961
+ );
4962
+ pin.style.left = `${hostPoint.x}px`;
4963
+ pin.style.top = `${hostPoint.y}px`;
4964
+ if (draft.selection && selectionHighlight) {
4965
+ const rect = toHostSelection(
4966
+ getAdjustedDraftSelection(
4967
+ toViewportSelection(draft.selection.viewport),
4968
+ draft,
4969
+ presets
4970
+ ),
4971
+ environment
4972
+ );
4973
+ selectionHighlight.style.left = `${rect.left}px`;
4974
+ selectionHighlight.style.top = `${rect.top}px`;
4975
+ selectionHighlight.style.width = `${rect.width}px`;
4976
+ selectionHighlight.style.height = `${rect.height}px`;
4977
+ }
4978
+ context.syncDraftPreview(draft);
4979
+ }
4980
+ function attachDraftPinDrag(context, {
4981
+ pin,
4982
+ popover,
4983
+ meta,
4984
+ textarea
4985
+ }) {
4986
+ const { config } = context;
4987
+ let isDragging = false;
4988
+ const moveDraftUi = (hostPoint) => {
4989
+ const environment = config.getEnvironment();
4990
+ if (!environment) return;
4991
+ const nextPoint = clampPoint(
4992
+ toTargetPoint(hostPoint, environment),
4993
+ environment
4994
+ );
4995
+ const nextHostPoint = toHostPoint(nextPoint, environment);
4996
+ pin.style.left = `${nextHostPoint.x}px`;
4997
+ pin.style.top = `${nextHostPoint.y}px`;
4998
+ if (popover) {
4999
+ const position = getPopoverPosition(nextHostPoint, environment);
5000
+ popover.style.left = `${position.left}px`;
5001
+ popover.style.top = `${position.top}px`;
5002
+ }
5003
+ const domDraft = config.getState().domDraft;
5004
+ if (!domDraft) return;
5005
+ const nextDraft = {
5006
+ ...domDraft,
5007
+ marker: {
5008
+ ...domDraft.marker,
5009
+ viewport: roundPoint(nextPoint)
5010
+ },
5011
+ comment: textarea.value
5012
+ };
5013
+ config.actions.setDomDraft(nextDraft);
5014
+ if (meta) {
5015
+ meta.textContent = formatDomDraftMeta(nextDraft);
5016
+ }
5017
+ };
5018
+ pin.addEventListener("pointerdown", (event) => {
5019
+ if (event.button !== 0) return;
5020
+ event.preventDefault();
5021
+ event.stopPropagation();
5022
+ isDragging = true;
5023
+ pin.setPointerCapture(event.pointerId);
5024
+ });
5025
+ pin.addEventListener("pointermove", (event) => {
5026
+ if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
5027
+ event.preventDefault();
5028
+ moveDraftUi({
5029
+ x: event.clientX,
5030
+ y: event.clientY
4776
5031
  });
4777
- actions.append(remove);
4778
- row.append(body, actions);
4779
- return row;
5032
+ });
5033
+ pin.addEventListener("pointerup", (event) => {
5034
+ if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
5035
+ event.preventDefault();
5036
+ event.stopPropagation();
5037
+ isDragging = false;
5038
+ pin.releasePointerCapture(event.pointerId);
5039
+ const nextPoint = toTargetPointFromHostEvent(
5040
+ event,
5041
+ config.getEnvironment()
5042
+ );
5043
+ const currentDraft = config.getState().domDraft;
5044
+ const fields = {
5045
+ title: currentDraft?.title,
5046
+ comment: textarea.value,
5047
+ assigneeId: currentDraft?.assigneeId,
5048
+ assigneeName: currentDraft?.assigneeName
5049
+ };
5050
+ void config.actions.bindElementDraftToPoint(nextPoint, fields);
5051
+ });
5052
+ }
5053
+
5054
+ // src/core/view/panel.ts
5055
+ function createReviewPanel(config, renderAreaForm) {
5056
+ const panel = document.createElement("div");
5057
+ panel.className = "dfwr-panel";
5058
+ panel.setAttribute("role", "dialog");
5059
+ panel.setAttribute("aria-label", "Web review kit");
5060
+ panel.append(
5061
+ createHeader(config),
5062
+ createToolbar(config),
5063
+ createBody(config, renderAreaForm),
5064
+ createList(config)
5065
+ );
5066
+ return panel;
5067
+ }
5068
+ function createHeader(config) {
5069
+ const header = document.createElement("div");
5070
+ header.className = "dfwr-header";
5071
+ const title = document.createElement("div");
5072
+ title.className = "dfwr-title";
5073
+ title.textContent = "Review Kit";
5074
+ const meta = document.createElement("div");
5075
+ meta.className = "dfwr-meta";
5076
+ meta.textContent = getRouteKey(config.getEnvironment());
5077
+ const titleGroup = document.createElement("div");
5078
+ titleGroup.append(title, meta);
5079
+ const close = document.createElement("button");
5080
+ close.className = "dfwr-icon-button";
5081
+ close.type = "button";
5082
+ close.textContent = "x";
5083
+ close.setAttribute("aria-label", "Close");
5084
+ close.addEventListener("click", () => config.actions.close());
5085
+ header.append(titleGroup, close);
5086
+ return header;
5087
+ }
5088
+ function createToolbar(config) {
5089
+ const state = config.getState();
5090
+ const toolbar = document.createElement("div");
5091
+ toolbar.className = "dfwr-toolbar";
5092
+ const toggleMode = (target) => {
5093
+ const mode = config.getState().mode;
5094
+ config.actions.setModeState(mode === target ? "idle" : target);
5095
+ config.actions.clearDrafts();
5096
+ config.actions.render();
5097
+ };
5098
+ toolbar.append(
5099
+ createToolbarButton(
5100
+ "Element",
5101
+ state.mode === "element",
5102
+ () => toggleMode("element")
5103
+ ),
5104
+ createToolbarButton(
5105
+ state.isSelectingArea ? "Selecting" : "Area",
5106
+ state.mode === "area",
5107
+ () => toggleMode("area")
5108
+ ),
5109
+ createToolbarButton("Refresh", false, () => {
5110
+ void config.actions.reload();
5111
+ })
5112
+ );
5113
+ return toolbar;
5114
+ }
5115
+ function createToolbarButton(label, active, onClick) {
5116
+ const button = document.createElement("button");
5117
+ button.className = `dfwr-button${active ? " is-active" : ""}`;
5118
+ button.type = "button";
5119
+ button.textContent = label;
5120
+ button.addEventListener("click", onClick);
5121
+ return button;
5122
+ }
5123
+ function createBody(config, renderAreaForm) {
5124
+ const body = document.createElement("div");
5125
+ body.className = "dfwr-body";
5126
+ const state = config.getState();
5127
+ if (state.mode === "idle") {
5128
+ body.append(
5129
+ createEmptyText("Select an element or mark an area.")
5130
+ );
5131
+ return body;
4780
5132
  }
4781
- createMarkerLayer() {
4782
- const layer = document.createElement("div");
4783
- layer.className = "dfwr-marker-layer";
4784
- const environment = this.config.getEnvironment();
4785
- if (!environment) return layer;
4786
- const currentScope = getReviewViewportScope(
4787
- getViewportSize(environment),
4788
- this.config.options.viewports?.presets
5133
+ if (state.mode === "element") {
5134
+ body.append(
5135
+ createEmptyText(
5136
+ state.domDraft ? "Write the QA in the page box." : "Click an element to add QA."
5137
+ )
4789
5138
  );
4790
- getNumberedReviewItems(
4791
- this.state.items,
4792
- this.config.options.viewports?.presets
4793
- ).forEach((numberedItem) => {
4794
- const { item, scope, displayLabel } = numberedItem;
4795
- if (!shouldShowMarkerForScope(scope, currentScope)) {
4796
- return;
4797
- }
4798
- const isHighlighted = item.id === this.state.highlightedItemId;
4799
- const highlightMode = getReviewItemHighlightMode(item);
4800
- if (highlightMode !== "note" && (!this.state.highlightedItemId || isHighlighted)) {
4801
- const selection = getItemHighlightSelection(item, environment);
4802
- if (selection) {
4803
- layer.append(
4804
- ...this.createItemHighlightElements(
4805
- selection.viewport,
4806
- environment,
4807
- item,
4808
- displayLabel,
4809
- selection.isBound,
4810
- isHighlighted
4811
- )
4812
- );
4813
- return;
4814
- }
4815
- }
4816
- const point = getBoundMarkerPoint(item, environment);
4817
- if (!point || !isPointInViewport(point.viewport, environment)) {
4818
- return;
5139
+ return body;
5140
+ }
5141
+ body.append(renderAreaForm());
5142
+ return body;
5143
+ }
5144
+ function createEmptyText(text) {
5145
+ const empty = document.createElement("p");
5146
+ empty.className = "dfwr-empty";
5147
+ empty.textContent = text;
5148
+ return empty;
5149
+ }
5150
+ function createList(config) {
5151
+ const section = document.createElement("div");
5152
+ section.className = "dfwr-list";
5153
+ const state = config.getState();
5154
+ const heading = document.createElement("div");
5155
+ heading.className = "dfwr-list-heading";
5156
+ heading.textContent = `Review items (${state.items.length})`;
5157
+ section.append(heading);
5158
+ if (state.items.length === 0) {
5159
+ section.append(createEmptyText("No review items on this page."));
5160
+ return section;
5161
+ }
5162
+ for (const numberedItem of getNumberedReviewItems(
5163
+ state.items,
5164
+ config.options.viewports?.presets
5165
+ )) {
5166
+ section.append(createListItem(config, numberedItem));
5167
+ }
5168
+ return section;
5169
+ }
5170
+ function createListItem(config, numberedItem) {
5171
+ const { item } = numberedItem;
5172
+ const row = document.createElement("article");
5173
+ row.className = "dfwr-item";
5174
+ row.tabIndex = 0;
5175
+ row.setAttribute("role", "button");
5176
+ row.setAttribute(
5177
+ "aria-label",
5178
+ `Restore review item: ${item.title ?? item.comment}`
5179
+ );
5180
+ row.addEventListener("click", () => {
5181
+ void config.actions.restoreItem(item);
5182
+ });
5183
+ row.addEventListener("keydown", (event) => {
5184
+ if (event.key !== "Enter" && event.key !== " ") return;
5185
+ event.preventDefault();
5186
+ void config.actions.restoreItem(item);
5187
+ });
5188
+ const body = document.createElement("div");
5189
+ body.className = "dfwr-item-body";
5190
+ const badges = document.createElement("div");
5191
+ badges.className = "dfwr-item-badges";
5192
+ const scope = document.createElement("div");
5193
+ scope.className = `dfwr-item-scope is-scope-${numberedItem.scope}`;
5194
+ scope.textContent = numberedItem.displayLabel;
5195
+ const kind = document.createElement("div");
5196
+ kind.className = "dfwr-item-kind";
5197
+ kind.textContent = item.kind;
5198
+ badges.append(scope, kind);
5199
+ const title = isTitleFieldEnabled(config.options) ? item.title?.trim() : "";
5200
+ const titleElement = title ? document.createElement("strong") : void 0;
5201
+ if (title && titleElement) {
5202
+ titleElement.className = "dfwr-item-title";
5203
+ titleElement.textContent = title;
5204
+ }
5205
+ const comment = document.createElement("p");
5206
+ comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
5207
+ comment.textContent = item.comment;
5208
+ const date = document.createElement("time");
5209
+ date.className = "dfwr-item-date";
5210
+ date.dateTime = item.createdAt;
5211
+ date.textContent = formatItemMeta(item);
5212
+ body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
5213
+ const actions = document.createElement("div");
5214
+ actions.className = "dfwr-item-actions";
5215
+ actions.addEventListener("click", (event) => event.stopPropagation());
5216
+ actions.addEventListener("keydown", (event) => event.stopPropagation());
5217
+ const remove = document.createElement("button");
5218
+ remove.className = "dfwr-icon-button";
5219
+ remove.type = "button";
5220
+ remove.textContent = "x";
5221
+ remove.setAttribute("aria-label", "Delete");
5222
+ remove.addEventListener("click", (event) => {
5223
+ event.stopPropagation();
5224
+ void config.actions.removeItem(item.id).then(() => config.actions.reload());
5225
+ });
5226
+ actions.append(remove);
5227
+ row.append(body, actions);
5228
+ return row;
5229
+ }
5230
+
5231
+ // src/core/view/selection.layers.ts
5232
+ function createElementLayer(config) {
5233
+ const layer = document.createElement("div");
5234
+ layer.className = "dfwr-element-layer";
5235
+ const environment = config.getEnvironment();
5236
+ const hover = document.createElement("div");
5237
+ hover.className = "dfwr-dom-hover";
5238
+ hover.hidden = true;
5239
+ layer.append(hover);
5240
+ if (environment) {
5241
+ placeLayerOverTarget(layer, environment);
5242
+ }
5243
+ const updateHover = (point) => {
5244
+ const nextEnvironment = config.getEnvironment();
5245
+ if (!nextEnvironment) return;
5246
+ const anchor = getDomAnchorFromPoint(
5247
+ clampPoint(point, nextEnvironment),
5248
+ config.options.anchors?.attribute,
5249
+ nextEnvironment
5250
+ );
5251
+ const selection = anchor ? getElementViewportSelection(anchor, nextEnvironment) : void 0;
5252
+ if (!selection) {
5253
+ hover.hidden = true;
5254
+ return;
5255
+ }
5256
+ const rect = toHostSelection(selection, nextEnvironment);
5257
+ hover.hidden = false;
5258
+ hover.style.left = `${rect.left}px`;
5259
+ hover.style.top = `${rect.top}px`;
5260
+ hover.style.width = `${rect.width}px`;
5261
+ hover.style.height = `${rect.height}px`;
5262
+ };
5263
+ layer.addEventListener("pointermove", (event) => {
5264
+ updateHover(toTargetPointFromHostEvent(event, config.getEnvironment()));
5265
+ });
5266
+ layer.addEventListener("pointerleave", () => {
5267
+ hover.hidden = true;
5268
+ });
5269
+ layer.addEventListener("pointerdown", (event) => {
5270
+ if (event.button !== 0) return;
5271
+ event.preventDefault();
5272
+ void config.actions.bindElementDraftToPoint(
5273
+ toTargetPointFromHostEvent(event, config.getEnvironment())
5274
+ );
5275
+ });
5276
+ return layer;
5277
+ }
5278
+ function createAreaLayer(config) {
5279
+ const layer = document.createElement("div");
5280
+ layer.className = "dfwr-area-layer";
5281
+ const environment = config.getEnvironment();
5282
+ if (environment) {
5283
+ placeLayerOverTarget(layer, environment);
5284
+ }
5285
+ const box = document.createElement("div");
5286
+ box.className = "dfwr-area-box";
5287
+ layer.append(box);
5288
+ let startX = 0;
5289
+ let startY = 0;
5290
+ let selection;
5291
+ let activePointerId;
5292
+ let isDragging = false;
5293
+ const ownerWindow = layer.ownerDocument.defaultView ?? window;
5294
+ const updateBox = (event) => {
5295
+ const nextEnvironment = config.getEnvironment();
5296
+ const nextPoint = toTargetPointFromHostEvent(event, nextEnvironment);
5297
+ const left = Math.min(startX, nextPoint.x);
5298
+ const top = Math.min(startY, nextPoint.y);
5299
+ const width = Math.abs(nextPoint.x - startX);
5300
+ const height = Math.abs(nextPoint.y - startY);
5301
+ selection = { left, top, width, height };
5302
+ const rect = nextEnvironment ? toHostSelection(selection, nextEnvironment) : selection;
5303
+ box.style.left = `${rect.left}px`;
5304
+ box.style.top = `${rect.top}px`;
5305
+ box.style.width = `${rect.width}px`;
5306
+ box.style.height = `${rect.height}px`;
5307
+ };
5308
+ const addDragListeners = () => {
5309
+ ownerWindow.addEventListener("pointermove", handlePointerMove, true);
5310
+ ownerWindow.addEventListener("pointerup", handlePointerUp, true);
5311
+ ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
5312
+ };
5313
+ const removeDragListeners = () => {
5314
+ ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
5315
+ ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
5316
+ ownerWindow.removeEventListener(
5317
+ "pointercancel",
5318
+ handlePointerCancel,
5319
+ true
5320
+ );
5321
+ };
5322
+ const releasePointerCapture = (event) => {
5323
+ try {
5324
+ if (layer.hasPointerCapture(event.pointerId)) {
5325
+ layer.releasePointerCapture(event.pointerId);
4819
5326
  }
4820
- const hostPoint = toHostPoint(point.viewport, environment);
4821
- const marker = this.createMarkerElement(
4822
- item.id,
4823
- hostPoint,
4824
- displayLabel,
4825
- scope,
4826
- point.isBound,
4827
- isHighlighted,
4828
- highlightMode === "note" ? "note" : "default"
4829
- );
4830
- marker.title = `${displayLabel} / ${item.comment}
4831
- ${formatItemMeta(item)}`;
4832
- layer.append(marker);
5327
+ } catch {
5328
+ }
5329
+ };
5330
+ function isActivePointer(event) {
5331
+ return isDragging && event.pointerId === activePointerId;
5332
+ }
5333
+ const finishAreaSelection = (event) => {
5334
+ if (!isActivePointer(event)) return;
5335
+ event.preventDefault();
5336
+ updateBox(event);
5337
+ releasePointerCapture(event);
5338
+ removeDragListeners();
5339
+ isDragging = false;
5340
+ activePointerId = void 0;
5341
+ if (!selection || selection.width < 8 || selection.height < 8) return;
5342
+ config.actions.setSelectingArea(true);
5343
+ config.actions.render();
5344
+ void config.actions.createAreaDraft(selection);
5345
+ };
5346
+ function handlePointerMove(event) {
5347
+ if (!isActivePointer(event)) return;
5348
+ event.preventDefault();
5349
+ updateBox(event);
5350
+ }
5351
+ const handlePointerUp = (event) => {
5352
+ finishAreaSelection(event);
5353
+ };
5354
+ const handlePointerCancel = (event) => {
5355
+ if (!isActivePointer(event)) return;
5356
+ releasePointerCapture(event);
5357
+ removeDragListeners();
5358
+ isDragging = false;
5359
+ activePointerId = void 0;
5360
+ };
5361
+ layer.addEventListener("pointerdown", (event) => {
5362
+ if (event.button !== 0) return;
5363
+ event.preventDefault();
5364
+ activePointerId = event.pointerId;
5365
+ isDragging = true;
5366
+ try {
5367
+ layer.setPointerCapture(event.pointerId);
5368
+ } catch {
5369
+ }
5370
+ const startPoint = toTargetPointFromHostEvent(
5371
+ event,
5372
+ config.getEnvironment()
5373
+ );
5374
+ startX = startPoint.x;
5375
+ startY = startPoint.y;
5376
+ updateBox(event);
5377
+ addDragListeners();
5378
+ });
5379
+ layer.addEventListener("pointermove", handlePointerMove);
5380
+ layer.addEventListener("pointerup", handlePointerUp);
5381
+ layer.addEventListener("pointercancel", handlePointerCancel);
5382
+ return layer;
5383
+ }
5384
+
5385
+ // src/core/web.review.kit.view.ts
5386
+ var WebReviewKitView = class {
5387
+ constructor(config) {
5388
+ this.config = config;
5389
+ const presets = () => this.config.options.viewports?.presets;
5390
+ this.draftPreview = new DraftPreviewController({
5391
+ getEnvironment: () => this.config.getEnvironment(),
5392
+ getMetrics: (draft) => getDraftAdjustmentMetrics(draft, presets()),
5393
+ hasAdjustment: (draft) => hasDraftAdjustment(draft, presets())
4833
5394
  });
4834
- return layer;
5395
+ this.draftContext = {
5396
+ config: this.config,
5397
+ cancelDraft: (event) => this.cancelDraft(event),
5398
+ syncDraftPreview: (draft) => this.draftPreview.sync(draft)
5399
+ };
4835
5400
  }
4836
- createItemHighlightElements(selection, environment, item, label, isBound, isHighlighted) {
4837
- const rect = toHostSelection(selection, environment);
4838
- const mode = getReviewItemHighlightMode(item);
4839
- const highlight = document.createElement("div");
4840
- highlight.className = [
4841
- "dfwr-item-target-highlight",
4842
- `is-mode-${mode}`,
4843
- isBound ? "is-bound" : "is-fallback",
4844
- isHighlighted ? "is-highlighted" : ""
4845
- ].filter(Boolean).join(" ");
4846
- highlight.style.left = `${rect.left}px`;
4847
- highlight.style.top = `${rect.top}px`;
4848
- highlight.style.width = `${rect.width}px`;
4849
- highlight.style.height = `${rect.height}px`;
4850
- highlight.dataset.reviewItemId = item.id;
4851
- const labelElement = document.createElement("div");
4852
- labelElement.className = [
4853
- "dfwr-item-target-label",
4854
- `is-mode-${mode}`,
4855
- isHighlighted ? "is-highlighted" : ""
4856
- ].filter(Boolean).join(" ");
4857
- labelElement.textContent = label;
4858
- labelElement.style.left = `${Math.max(4, rect.left)}px`;
4859
- labelElement.style.top = `${Math.max(4, rect.top - 24)}px`;
4860
- labelElement.dataset.reviewItemId = item.id;
4861
- return [highlight, labelElement];
4862
- }
4863
- createSelectionHighlight(selection, environment, isDraft) {
4864
- const rect = toHostSelection(selection, environment);
4865
- const highlight = document.createElement("div");
4866
- highlight.className = `dfwr-selection-highlight${isDraft ? " is-draft" : ""}`;
4867
- highlight.style.left = `${rect.left}px`;
4868
- highlight.style.top = `${rect.top}px`;
4869
- highlight.style.width = `${rect.width}px`;
4870
- highlight.style.height = `${rect.height}px`;
4871
- return highlight;
4872
- }
4873
- createMarkerElement(itemId, hostPoint, label, scope, isBound, isHighlighted, variant = "default") {
4874
- const isNoteCallout = variant === "note";
4875
- const marker = document.createElement("div");
4876
- marker.className = [
4877
- "dfwr-bound-marker",
4878
- isNoteCallout ? "is-note-callout" : "",
4879
- `is-scope-${scope}`,
4880
- isBound ? "is-bound" : "is-fallback",
4881
- isHighlighted ? "is-highlighted" : ""
5401
+ clearDraftPreview() {
5402
+ this.draftPreview.clear();
5403
+ this.clearShellComposer();
5404
+ }
5405
+ /** Rebuilds the entire shadow-root UI from the current state snapshot. */
5406
+ render(shadow, hiddenItemsStyle) {
5407
+ const state = this.state;
5408
+ this.draftPreview.sync(
5409
+ state.isOpen && state.mode === "element" ? state.domDraft : void 0
5410
+ );
5411
+ shadow.replaceChildren();
5412
+ shadow.append(createStyleElement());
5413
+ shadow.append(hiddenItemsStyle);
5414
+ const hasDismissableDraft = Boolean(state.domDraft || state.areaDraft);
5415
+ const shouldDockComposer = this.config.options.ui?.panel === false && hasDismissableDraft && Boolean(this.getShellComposerHost());
5416
+ let dockedComposer;
5417
+ const shell = document.createElement("div");
5418
+ shell.className = [
5419
+ "dfwr-shell",
5420
+ state.isOpen ? "is-open" : "",
5421
+ hasDismissableDraft && !shouldDockComposer ? "has-dismissible-draft" : ""
4882
5422
  ].filter(Boolean).join(" ");
4883
- marker.style.left = `${hostPoint.x}px`;
4884
- marker.style.top = `${hostPoint.y}px`;
4885
- marker.dataset.scope = scope;
4886
- if (itemId) {
4887
- marker.dataset.reviewItemId = itemId;
4888
- }
4889
- const iconElement = document.createElement("span");
4890
- iconElement.className = "dfwr-bound-marker-icon";
4891
- iconElement.setAttribute("aria-hidden", "true");
4892
- const labelElement = document.createElement("span");
4893
- labelElement.className = "dfwr-bound-marker-number";
4894
- labelElement.textContent = label;
4895
- marker.append(iconElement, labelElement);
4896
- return marker;
4897
- }
4898
- attachDraftPinDrag(pin, popover, meta, textarea) {
4899
- let isDragging = false;
4900
- const moveDraftUi = (hostPoint) => {
4901
- const environment = this.config.getEnvironment();
4902
- if (!environment) return;
4903
- const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
4904
- const nextHostPoint = toHostPoint(nextPoint, environment);
4905
- pin.style.left = `${nextHostPoint.x}px`;
4906
- pin.style.top = `${nextHostPoint.y}px`;
4907
- if (popover) {
4908
- const position = getPopoverPosition(nextHostPoint, environment);
4909
- popover.style.left = `${position.left}px`;
4910
- popover.style.top = `${position.top}px`;
5423
+ shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
5424
+ if (this.config.options.ui?.panel !== false) {
5425
+ shell.append(
5426
+ createReviewPanel(
5427
+ this.config,
5428
+ () => createAreaForm(this.draftContext)
5429
+ )
5430
+ );
5431
+ }
5432
+ shell.append(
5433
+ createMarkerLayer({
5434
+ items: state.items,
5435
+ highlightedItemId: state.highlightedItemId,
5436
+ environment: this.config.getEnvironment(),
5437
+ presets: this.config.options.viewports?.presets,
5438
+ showCompactMarkers: this.config.options.ui?.markers !== "external"
5439
+ })
5440
+ );
5441
+ if (state.isOpen && hasDismissableDraft && !shouldDockComposer) {
5442
+ shell.append(this.createDraftCancelLayer());
5443
+ }
5444
+ if (state.isOpen && state.mode === "element") {
5445
+ if (state.domDraft) {
5446
+ const domDraft = createDomDraftLayer(this.draftContext, state.domDraft, {
5447
+ dockComposer: shouldDockComposer
5448
+ });
5449
+ shell.append(domDraft.layer);
5450
+ dockedComposer = domDraft.composer;
5451
+ } else {
5452
+ shell.append(createElementLayer(this.config));
4911
5453
  }
4912
- const noteDraft = this.state.noteDraft;
4913
- if (!noteDraft) return;
4914
- const nextDraft = {
4915
- ...noteDraft,
4916
- marker: {
4917
- ...noteDraft.marker,
4918
- viewport: roundPoint(nextPoint)
4919
- },
4920
- comment: textarea.value
4921
- };
4922
- this.config.actions.setNoteDraft(nextDraft);
4923
- if (meta) {
4924
- meta.textContent = formatNoteDraftMeta(nextDraft);
5454
+ }
5455
+ if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
5456
+ shell.append(createAreaLayer(this.config));
5457
+ }
5458
+ if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
5459
+ if (state.areaDraft.selection) {
5460
+ shell.append(createAreaDraftOverlay(this.draftContext, state.areaDraft));
4925
5461
  }
4926
- };
4927
- pin.addEventListener("pointerdown", (event) => {
4928
- if (event.button !== 0) return;
4929
- event.preventDefault();
4930
- event.stopPropagation();
4931
- isDragging = true;
4932
- pin.setPointerCapture(event.pointerId);
4933
- });
4934
- pin.addEventListener("pointermove", (event) => {
4935
- if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
4936
- event.preventDefault();
4937
- moveDraftUi({
4938
- x: event.clientX,
4939
- y: event.clientY
4940
- });
4941
- });
4942
- pin.addEventListener("pointerup", (event) => {
4943
- if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
4944
- event.preventDefault();
4945
- event.stopPropagation();
4946
- isDragging = false;
4947
- pin.releasePointerCapture(event.pointerId);
4948
- const nextPoint = toTargetPointFromHostEvent(
4949
- event,
4950
- this.config.getEnvironment()
5462
+ const areaComposer = createAreaDraftPopover(
5463
+ this.draftContext,
5464
+ state.areaDraft,
5465
+ { dockComposer: shouldDockComposer }
4951
5466
  );
4952
- const currentDraft = this.state.noteDraft;
4953
- const fields = {
4954
- title: currentDraft?.title,
4955
- comment: textarea.value,
4956
- assigneeId: currentDraft?.assigneeId,
4957
- assigneeName: currentDraft?.assigneeName
4958
- };
4959
- void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, fields) : this.config.actions.bindNoteDraftToPoint(nextPoint, fields));
4960
- });
5467
+ if (shouldDockComposer) {
5468
+ dockedComposer = areaComposer;
5469
+ } else {
5470
+ shell.append(areaComposer);
5471
+ }
5472
+ }
5473
+ shadow.append(shell);
5474
+ this.renderShellComposer(dockedComposer);
4961
5475
  }
4962
- createNoteLayer() {
4963
- const layer = document.createElement("div");
4964
- layer.className = "dfwr-text-layer";
5476
+ get state() {
5477
+ return this.config.getState();
5478
+ }
5479
+ getShellComposerHost() {
4965
5480
  const environment = this.config.getEnvironment();
4966
- if (environment) {
4967
- placeLayerOverTarget(layer, environment);
5481
+ if (this.config.options.ui?.panel !== false) return void 0;
5482
+ return environment?.composerHost ?? void 0;
5483
+ }
5484
+ /** Mounts the docked composer into the shell-provided host element. */
5485
+ renderShellComposer(composer) {
5486
+ const host = composer ? this.getShellComposerHost() : void 0;
5487
+ if (!host || !composer) {
5488
+ this.clearShellComposer();
5489
+ return;
4968
5490
  }
4969
- layer.addEventListener("pointerdown", (event) => {
4970
- if (event.button !== 0) return;
4971
- event.preventDefault();
4972
- void this.config.actions.bindNoteDraftToPoint(
4973
- toTargetPointFromHostEvent(event, this.config.getEnvironment())
4974
- );
4975
- });
4976
- return layer;
5491
+ if (this.shellComposerHost && this.shellComposerHost !== host) {
5492
+ this.clearShellComposer();
5493
+ }
5494
+ this.shellComposerHost = host;
5495
+ host.dataset.hasDraftComposer = "true";
5496
+ if (host.parentElement) {
5497
+ host.parentElement.dataset.hasDraftComposer = "true";
5498
+ }
5499
+ const shell = document.createElement("div");
5500
+ shell.className = "dfwr-shell is-open is-shell-draft is-docked-composer";
5501
+ shell.append(composer);
5502
+ host.replaceChildren(createStyleElement(), shell);
4977
5503
  }
4978
- createElementLayer() {
4979
- const layer = document.createElement("div");
4980
- layer.className = "dfwr-element-layer";
4981
- const environment = this.config.getEnvironment();
4982
- const hover = document.createElement("div");
4983
- hover.className = "dfwr-dom-hover";
4984
- hover.hidden = true;
4985
- layer.append(hover);
4986
- if (environment) {
4987
- placeLayerOverTarget(layer, environment);
5504
+ clearShellComposer() {
5505
+ const host = this.shellComposerHost;
5506
+ host?.replaceChildren();
5507
+ if (host) {
5508
+ delete host.dataset.hasDraftComposer;
5509
+ delete host.parentElement?.dataset.hasDraftComposer;
4988
5510
  }
4989
- const updateHover = (point) => {
4990
- const nextEnvironment = this.config.getEnvironment();
4991
- if (!nextEnvironment) return;
4992
- const anchor = getDomAnchorFromPoint(
4993
- clampPoint(point, nextEnvironment),
4994
- this.config.options.anchors?.attribute,
4995
- nextEnvironment
4996
- );
4997
- const selection = anchor ? getElementViewportSelection(anchor, nextEnvironment) : void 0;
4998
- if (!selection) {
4999
- hover.hidden = true;
5000
- return;
5001
- }
5002
- const rect = toHostSelection(selection, nextEnvironment);
5003
- hover.hidden = false;
5004
- hover.style.left = `${rect.left}px`;
5005
- hover.style.top = `${rect.top}px`;
5006
- hover.style.width = `${rect.width}px`;
5007
- hover.style.height = `${rect.height}px`;
5008
- };
5009
- layer.addEventListener("pointermove", (event) => {
5010
- updateHover(toTargetPointFromHostEvent(event, this.config.getEnvironment()));
5011
- });
5012
- layer.addEventListener("pointerleave", () => {
5013
- hover.hidden = true;
5014
- });
5015
- layer.addEventListener("pointerdown", (event) => {
5016
- if (event.button !== 0) return;
5017
- event.preventDefault();
5018
- void this.config.actions.bindElementDraftToPoint(
5019
- toTargetPointFromHostEvent(event, this.config.getEnvironment())
5020
- );
5021
- });
5022
- return layer;
5511
+ this.shellComposerHost = void 0;
5023
5512
  }
5024
- createAreaLayer() {
5513
+ /** Full-screen click-away layer that cancels the active draft. */
5514
+ createDraftCancelLayer() {
5025
5515
  const layer = document.createElement("div");
5026
- layer.className = "dfwr-area-layer";
5027
- const environment = this.config.getEnvironment();
5028
- if (environment) {
5029
- placeLayerOverTarget(layer, environment);
5030
- }
5031
- const box = document.createElement("div");
5032
- box.className = "dfwr-area-box";
5033
- layer.append(box);
5034
- let startX = 0;
5035
- let startY = 0;
5036
- let selection;
5037
- let activePointerId;
5038
- let isDragging = false;
5039
- const ownerWindow = layer.ownerDocument.defaultView ?? window;
5040
- const updateBox = (event) => {
5041
- const nextEnvironment = this.config.getEnvironment();
5042
- const nextPoint = toTargetPointFromHostEvent(
5043
- event,
5044
- nextEnvironment
5045
- );
5046
- const left = Math.min(startX, nextPoint.x);
5047
- const top = Math.min(startY, nextPoint.y);
5048
- const width = Math.abs(nextPoint.x - startX);
5049
- const height = Math.abs(nextPoint.y - startY);
5050
- const hostPoint = toHostPoint(
5051
- { x: left, y: top },
5052
- nextEnvironment
5053
- );
5054
- selection = { left, top, width, height };
5055
- box.style.left = `${hostPoint.x}px`;
5056
- box.style.top = `${hostPoint.y}px`;
5057
- box.style.width = `${width}px`;
5058
- box.style.height = `${height}px`;
5059
- };
5060
- const addDragListeners = () => {
5061
- ownerWindow.addEventListener("pointermove", handlePointerMove, true);
5062
- ownerWindow.addEventListener("pointerup", handlePointerUp, true);
5063
- ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
5064
- };
5065
- const removeDragListeners = () => {
5066
- ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
5067
- ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
5068
- ownerWindow.removeEventListener(
5069
- "pointercancel",
5070
- handlePointerCancel,
5071
- true
5072
- );
5073
- };
5074
- const releasePointerCapture = (event) => {
5075
- try {
5076
- if (layer.hasPointerCapture(event.pointerId)) {
5077
- layer.releasePointerCapture(event.pointerId);
5078
- }
5079
- } catch {
5080
- }
5081
- };
5082
- function isActivePointer(event) {
5083
- return isDragging && event.pointerId === activePointerId;
5084
- }
5085
- const finishAreaSelection = (event) => {
5086
- if (!isActivePointer(event)) return;
5087
- event.preventDefault();
5088
- updateBox(event);
5089
- releasePointerCapture(event);
5090
- removeDragListeners();
5091
- isDragging = false;
5092
- activePointerId = void 0;
5093
- if (!selection || selection.width < 8 || selection.height < 8) return;
5094
- this.config.actions.setSelectingArea(true);
5095
- this.config.actions.render();
5096
- void this.config.actions.createAreaDraft(selection);
5097
- };
5098
- function handlePointerMove(event) {
5099
- if (!isActivePointer(event)) return;
5100
- event.preventDefault();
5101
- updateBox(event);
5102
- }
5103
- const handlePointerUp = (event) => {
5104
- finishAreaSelection(event);
5105
- };
5106
- const handlePointerCancel = (event) => {
5107
- if (!isActivePointer(event)) return;
5108
- releasePointerCapture(event);
5109
- removeDragListeners();
5110
- isDragging = false;
5111
- activePointerId = void 0;
5112
- };
5516
+ layer.className = "dfwr-draft-cancel-layer";
5517
+ layer.setAttribute("aria-hidden", "true");
5113
5518
  layer.addEventListener("pointerdown", (event) => {
5114
5519
  if (event.button !== 0) return;
5115
- event.preventDefault();
5116
- activePointerId = event.pointerId;
5117
- isDragging = true;
5118
- try {
5119
- layer.setPointerCapture(event.pointerId);
5120
- } catch {
5121
- }
5122
- const startPoint = toTargetPointFromHostEvent(
5123
- event,
5124
- this.config.getEnvironment()
5125
- );
5126
- startX = startPoint.x;
5127
- startY = startPoint.y;
5128
- updateBox(event);
5129
- addDragListeners();
5520
+ this.cancelDraft(event);
5130
5521
  });
5131
- layer.addEventListener("pointermove", handlePointerMove);
5132
- layer.addEventListener("pointerup", handlePointerUp);
5133
- layer.addEventListener("pointercancel", handlePointerCancel);
5134
5522
  return layer;
5135
5523
  }
5524
+ cancelDraft(event) {
5525
+ event?.preventDefault();
5526
+ event?.stopPropagation();
5527
+ event?.stopImmediatePropagation();
5528
+ this.config.actions.setModeState("idle");
5529
+ this.config.actions.clearDrafts();
5530
+ this.config.actions.setSelectingArea(false);
5531
+ this.config.actions.render();
5532
+ }
5136
5533
  };
5137
5534
 
5138
5535
  // src/core/web.review.kit.app.ts
@@ -5172,6 +5569,7 @@ var WebReviewKitApp = class {
5172
5569
  this.items = [];
5173
5570
  this.draftError = "";
5174
5571
  this.isCreatingItem = false;
5572
+ this.isCapturingViewport = false;
5175
5573
  this.isSelectingArea = false;
5176
5574
  this.handleKeyDown = (event) => {
5177
5575
  if (event.key === "Escape" && this.cancelMode()) {
@@ -5200,10 +5598,11 @@ var WebReviewKitApp = class {
5200
5598
  isOpen: this.isOpen,
5201
5599
  mode: this.mode,
5202
5600
  items: this.items,
5203
- noteDraft: this.noteDraft,
5601
+ domDraft: this.domDraft,
5204
5602
  areaDraft: this.areaDraft,
5205
5603
  draftError: this.draftError,
5206
5604
  isCreatingItem: this.isCreatingItem,
5605
+ isCapturingViewport: this.isCapturingViewport,
5207
5606
  isSelectingArea: this.isSelectingArea,
5208
5607
  highlightedItemId: this.highlightedItemId
5209
5608
  }),
@@ -5215,13 +5614,9 @@ var WebReviewKitApp = class {
5215
5614
  restoreItem: (item) => this.restoreItem(item),
5216
5615
  removeItem: (itemId) => this.adapter.remove(itemId),
5217
5616
  setModeState: (mode) => this.setModeState(mode),
5218
- clearDrafts: () => {
5219
- this.noteDraft = void 0;
5220
- this.areaDraft = void 0;
5221
- this.draftError = "";
5222
- },
5223
- setNoteDraft: (draft) => {
5224
- this.noteDraft = draft;
5617
+ clearDrafts: () => this.clearDrafts(),
5618
+ setDomDraft: (draft) => {
5619
+ this.domDraft = draft;
5225
5620
  this.draftError = "";
5226
5621
  },
5227
5622
  setAreaDraft: (draft) => {
@@ -5232,7 +5627,8 @@ var WebReviewKitApp = class {
5232
5627
  this.isSelectingArea = isSelectingArea;
5233
5628
  },
5234
5629
  createItem: (input) => this.createItem(input),
5235
- bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
5630
+ captureDomDraft: (input) => this.captureDomDraft(input),
5631
+ captureAreaDraft: (input) => this.captureAreaDraft(input),
5236
5632
  bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
5237
5633
  createAreaDraft: (selection) => this.createAreaDraft(selection)
5238
5634
  }
@@ -5254,6 +5650,7 @@ var WebReviewKitApp = class {
5254
5650
  }
5255
5651
  destroy() {
5256
5652
  this.view.clearDraftPreview();
5653
+ this.clearDrafts();
5257
5654
  document.removeEventListener("keydown", this.handleKeyDown, true);
5258
5655
  window.removeEventListener("scroll", this.handleViewportChange, true);
5259
5656
  window.removeEventListener("resize", this.handleViewportChange);
@@ -5273,8 +5670,7 @@ var WebReviewKitApp = class {
5273
5670
  close() {
5274
5671
  this.isOpen = false;
5275
5672
  this.setModeState("idle");
5276
- this.noteDraft = void 0;
5277
- this.areaDraft = void 0;
5673
+ this.clearDrafts();
5278
5674
  this.isSelectingArea = false;
5279
5675
  this.render();
5280
5676
  }
@@ -5290,8 +5686,7 @@ var WebReviewKitApp = class {
5290
5686
  this.isOpen = true;
5291
5687
  }
5292
5688
  this.setModeState(this.mode === mode ? "idle" : mode);
5293
- this.noteDraft = void 0;
5294
- this.areaDraft = void 0;
5689
+ this.clearDrafts();
5295
5690
  this.render();
5296
5691
  }
5297
5692
  async startElementReview(element, comment) {
@@ -5299,8 +5694,7 @@ var WebReviewKitApp = class {
5299
5694
  this.isOpen = true;
5300
5695
  }
5301
5696
  this.setModeState("element");
5302
- this.noteDraft = void 0;
5303
- this.areaDraft = void 0;
5697
+ this.clearDrafts();
5304
5698
  this.isSelectingArea = false;
5305
5699
  await this.bindElementDraftToElement(element, { comment });
5306
5700
  }
@@ -5325,6 +5719,20 @@ var WebReviewKitApp = class {
5325
5719
  this.hiddenItemIds = itemIds ? new Set(itemIds) : void 0;
5326
5720
  this.updateHiddenItemsStyle();
5327
5721
  }
5722
+ clearDrafts() {
5723
+ this.revokeDraftAttachmentPreviews(this.domDraft);
5724
+ this.revokeDraftAttachmentPreviews(this.areaDraft);
5725
+ this.domDraft = void 0;
5726
+ this.areaDraft = void 0;
5727
+ this.draftError = "";
5728
+ }
5729
+ revokeDraftAttachmentPreviews(draft) {
5730
+ draft?.attachments?.forEach((attachment) => {
5731
+ if (attachment.previewUrl) {
5732
+ URL.revokeObjectURL(attachment.previewUrl);
5733
+ }
5734
+ });
5735
+ }
5328
5736
  clearHighlightedItem() {
5329
5737
  if (!this.highlightedItemId) return;
5330
5738
  this.highlightedItemId = void 0;
@@ -5360,18 +5768,17 @@ var WebReviewKitApp = class {
5360
5768
  this.options.onModeChange?.(mode);
5361
5769
  }
5362
5770
  cancelMode() {
5363
- if (this.mode === "idle" && !this.noteDraft && !this.areaDraft && !this.isSelectingArea) {
5771
+ if (this.mode === "idle" && !this.domDraft && !this.areaDraft && !this.isSelectingArea) {
5364
5772
  return false;
5365
5773
  }
5366
5774
  this.setModeState("idle");
5367
- this.noteDraft = void 0;
5368
- this.areaDraft = void 0;
5775
+ this.clearDrafts();
5369
5776
  this.isSelectingArea = false;
5370
5777
  this.render();
5371
5778
  return true;
5372
5779
  }
5373
5780
  isDraftComposerFocused() {
5374
- if (!this.noteDraft && !this.areaDraft) return false;
5781
+ if (!this.domDraft && !this.areaDraft) return false;
5375
5782
  const composerHost = this.getEnvironment()?.composerHost;
5376
5783
  const activeElement = composerHost?.ownerDocument.activeElement;
5377
5784
  return Boolean(
@@ -5407,6 +5814,8 @@ var WebReviewKitApp = class {
5407
5814
  };
5408
5815
  const overlayRect = target.getOverlayRect?.() ?? rect;
5409
5816
  const composerHost = target.getComposerHost?.();
5817
+ const scaleX = target.window.innerWidth > 0 ? rect.width / target.window.innerWidth : 1;
5818
+ const scaleY = target.window.innerHeight > 0 ? rect.height / target.window.innerHeight : 1;
5410
5819
  return {
5411
5820
  window: target.window,
5412
5821
  document: target.document,
@@ -5416,13 +5825,16 @@ var WebReviewKitApp = class {
5416
5825
  width: rect.width,
5417
5826
  height: rect.height
5418
5827
  },
5828
+ scaleX,
5829
+ scaleY,
5419
5830
  overlayRect: {
5420
5831
  left: overlayRect.left,
5421
5832
  top: overlayRect.top,
5422
5833
  width: overlayRect.width,
5423
5834
  height: overlayRect.height
5424
5835
  },
5425
- composerHost
5836
+ composerHost,
5837
+ captureViewport: target.captureViewport
5426
5838
  };
5427
5839
  } catch {
5428
5840
  return void 0;
@@ -5445,32 +5857,6 @@ var WebReviewKitApp = class {
5445
5857
  if (!this.shadow) return;
5446
5858
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
5447
5859
  }
5448
- async bindNoteDraftToPoint(point, fields = {}) {
5449
- const environment = this.getEnvironment();
5450
- if (!environment) return;
5451
- const viewport = getViewportSize(environment);
5452
- const nextPoint = clampPoint(point, environment);
5453
- const draft = await this.withOverlayHidden(() => {
5454
- const selection = getPointSelection(nextPoint);
5455
- const anchor = getDomAnchor(
5456
- selection,
5457
- this.options.anchors?.attribute,
5458
- environment
5459
- );
5460
- const marker = {
5461
- viewport: roundPoint(nextPoint),
5462
- relative: anchor ? getRelativePoint(nextPoint, anchor, environment) : void 0
5463
- };
5464
- return {
5465
- viewport,
5466
- anchor,
5467
- marker,
5468
- ...fields
5469
- };
5470
- });
5471
- this.noteDraft = draft;
5472
- this.render();
5473
- }
5474
5860
  async bindElementDraftToPoint(point, fields = {}) {
5475
5861
  const environment = this.getEnvironment();
5476
5862
  if (!environment) return;
@@ -5519,7 +5905,7 @@ var WebReviewKitApp = class {
5519
5905
  previewElement
5520
5906
  };
5521
5907
  });
5522
- this.noteDraft = draft;
5908
+ this.domDraft = draft;
5523
5909
  this.render();
5524
5910
  }
5525
5911
  async bindElementDraftToElement(element, fields = {}) {
@@ -5560,7 +5946,7 @@ var WebReviewKitApp = class {
5560
5946
  };
5561
5947
  });
5562
5948
  if (!draft) return;
5563
- this.noteDraft = draft;
5949
+ this.domDraft = draft;
5564
5950
  this.render();
5565
5951
  }
5566
5952
  async createAreaDraft(selection) {
@@ -5573,16 +5959,27 @@ var WebReviewKitApp = class {
5573
5959
  try {
5574
5960
  const viewport = getViewportSize(environment);
5575
5961
  this.areaDraft = await this.withOverlayHidden(() => {
5576
- const marker = createSelectionCenterMarker(
5962
+ const anchorPoint = clampPoint(
5963
+ getSelectionCenter(selection),
5964
+ environment
5965
+ );
5966
+ const anchor = getDomAnchorFromPoint(
5967
+ anchorPoint,
5968
+ this.options.anchors?.attribute,
5969
+ environment
5970
+ );
5971
+ const marker = createSelectionStartMarker(
5577
5972
  selection,
5578
- void 0,
5973
+ anchor,
5579
5974
  environment
5580
5975
  );
5581
5976
  const reviewSelection = {
5582
- viewport: toPublicSelection(selection)
5977
+ viewport: toPublicSelection(selection),
5978
+ relative: anchor ? getRelativeSelection(selection, anchor, environment) : void 0
5583
5979
  };
5584
5980
  return {
5585
5981
  viewport,
5982
+ anchor,
5586
5983
  marker,
5587
5984
  selection: reviewSelection
5588
5985
  };
@@ -5603,6 +6000,127 @@ var WebReviewKitApp = class {
5603
6000
  this.root.style.display = previousDisplay;
5604
6001
  }
5605
6002
  }
6003
+ async captureDomDraft(input) {
6004
+ if (this.isCapturingViewport) return;
6005
+ const environment = this.getEnvironment();
6006
+ const draft = this.domDraft;
6007
+ if (!draft) return;
6008
+ if (!environment?.captureViewport) {
6009
+ this.draftError = "Viewport capture helper is not available.";
6010
+ this.render();
6011
+ return;
6012
+ }
6013
+ const captureInput = this.createViewportCaptureInput(
6014
+ environment,
6015
+ input,
6016
+ input.selection?.viewport
6017
+ );
6018
+ this.draftError = "";
6019
+ this.isCapturingViewport = true;
6020
+ this.render();
6021
+ try {
6022
+ const result = await environment.captureViewport(captureInput);
6023
+ const attachment = this.createCaptureDraftAttachment(result, captureInput);
6024
+ const currentDraft = this.domDraft ?? draft;
6025
+ this.domDraft = {
6026
+ ...currentDraft,
6027
+ attachments: [...currentDraft.attachments ?? [], attachment]
6028
+ };
6029
+ } catch (error) {
6030
+ this.draftError = this.getErrorMessage(
6031
+ error,
6032
+ "Failed to capture viewport."
6033
+ );
6034
+ } finally {
6035
+ this.isCapturingViewport = false;
6036
+ this.render();
6037
+ }
6038
+ }
6039
+ async captureAreaDraft(input) {
6040
+ if (this.isCapturingViewport) return;
6041
+ const environment = this.getEnvironment();
6042
+ const draft = this.areaDraft;
6043
+ if (!draft) return;
6044
+ if (!environment?.captureViewport) {
6045
+ this.draftError = "Viewport capture helper is not available.";
6046
+ this.render();
6047
+ return;
6048
+ }
6049
+ const captureInput = this.createViewportCaptureInput(
6050
+ environment,
6051
+ input,
6052
+ input.selection?.viewport
6053
+ );
6054
+ this.draftError = "";
6055
+ this.isCapturingViewport = true;
6056
+ this.render();
6057
+ try {
6058
+ const result = await environment.captureViewport(captureInput);
6059
+ const attachment = this.createCaptureDraftAttachment(result, captureInput);
6060
+ const currentDraft = this.areaDraft ?? draft;
6061
+ this.areaDraft = {
6062
+ ...currentDraft,
6063
+ attachments: [...currentDraft.attachments ?? [], attachment]
6064
+ };
6065
+ } catch (error) {
6066
+ this.draftError = this.getErrorMessage(
6067
+ error,
6068
+ "Failed to capture viewport."
6069
+ );
6070
+ } finally {
6071
+ this.isCapturingViewport = false;
6072
+ this.render();
6073
+ }
6074
+ }
6075
+ createViewportCaptureInput(environment, draft, captureRegion) {
6076
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
6077
+ const viewport = draft.viewport ?? getViewportSize(environment);
6078
+ const routeKey = getRouteKey(environment);
6079
+ return {
6080
+ routeKey,
6081
+ pageUrl: getPageUrl(environment),
6082
+ originalUrl: getOriginalUrl(environment),
6083
+ viewport,
6084
+ captureRegion,
6085
+ devicePixelRatio: environment.window.devicePixelRatio || 1,
6086
+ scroll: {
6087
+ x: environment.window.scrollX,
6088
+ y: environment.window.scrollY
6089
+ },
6090
+ marker: draft.marker,
6091
+ selection: draft.selection,
6092
+ timestamp
6093
+ };
6094
+ }
6095
+ createCaptureDraftAttachment(result, input) {
6096
+ const mime = result.mime || result.file.type || "image/png";
6097
+ const name = result.name || `review-capture-${Date.now()}.png`;
6098
+ return {
6099
+ id: createId(),
6100
+ file: result.file,
6101
+ name,
6102
+ mime,
6103
+ size: result.file.size,
6104
+ kind: "capture",
6105
+ previewUrl: mime.startsWith("image/") ? URL.createObjectURL(result.file) : void 0,
6106
+ metadata: {
6107
+ ...result.metadata,
6108
+ source: "viewport-capture",
6109
+ target: "iframe",
6110
+ routeKey: input.routeKey,
6111
+ pageUrl: input.pageUrl,
6112
+ originalUrl: input.originalUrl,
6113
+ viewport: input.viewport,
6114
+ scroll: input.scroll,
6115
+ marker: input.marker,
6116
+ selection: input.selection,
6117
+ timestamp: input.timestamp,
6118
+ devicePixelRatio: input.devicePixelRatio,
6119
+ width: result.width,
6120
+ height: result.height
6121
+ }
6122
+ };
6123
+ }
5606
6124
  async createItem(input) {
5607
6125
  const environment = this.getEnvironment();
5608
6126
  if (!environment || this.isCreatingItem) return;
@@ -5646,24 +6164,61 @@ var WebReviewKitApp = class {
5646
6164
  this.isCreatingItem = true;
5647
6165
  this.render();
5648
6166
  try {
5649
- const createdItem = await this.adapter.create(item);
6167
+ const attachments = await this.uploadDraftAttachments(
6168
+ input.attachments,
6169
+ item
6170
+ );
6171
+ const itemWithAttachments = attachments.length > 0 ? { ...item, attachments } : item;
6172
+ const createdItem = await this.adapter.create(itemWithAttachments);
5650
6173
  this.setModeState("idle");
5651
- this.noteDraft = void 0;
5652
- this.areaDraft = void 0;
6174
+ this.clearDrafts();
5653
6175
  this.highlightItem(createdItem.id);
5654
6176
  await this.reload();
5655
6177
  await this.options.onCreateItem?.(createdItem);
5656
6178
  } catch (error) {
5657
- this.draftError = error instanceof Error ? error.message : "Failed to save QA.";
6179
+ this.draftError = this.getCreateItemErrorMessage(
6180
+ error,
6181
+ Boolean(input.attachments?.length)
6182
+ );
5658
6183
  } finally {
5659
6184
  this.isCreatingItem = false;
5660
6185
  this.render();
5661
6186
  }
5662
6187
  }
6188
+ async uploadDraftAttachments(attachments, item) {
6189
+ if (!attachments?.length) return [];
6190
+ const uploadAttachment = this.adapter.uploadAttachment;
6191
+ if (!uploadAttachment) {
6192
+ throw new Error("Attachment upload adapter is not configured.");
6193
+ }
6194
+ return Promise.all(
6195
+ attachments.map(
6196
+ (attachment) => uploadAttachment({
6197
+ file: attachment.file,
6198
+ name: attachment.name,
6199
+ mime: attachment.mime,
6200
+ kind: attachment.kind,
6201
+ item,
6202
+ metadata: attachment.metadata
6203
+ })
6204
+ )
6205
+ );
6206
+ }
6207
+ getCreateItemErrorMessage(error, wasUploadingAttachments) {
6208
+ const message = this.getErrorMessage(error, "Failed to save QA.");
6209
+ const reason = error && typeof error === "object" && "reason" in error && typeof error.reason === "string" ? ` (${error.reason})` : "";
6210
+ return wasUploadingAttachments && reason ? `Attachment upload failed${reason}: ${message}` : message;
6211
+ }
6212
+ getErrorMessage(error, fallback) {
6213
+ if (error instanceof Error) return error.message;
6214
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
6215
+ return error.message;
6216
+ }
6217
+ return fallback;
6218
+ }
5663
6219
  async restoreItem(item) {
5664
6220
  this.setModeState("idle");
5665
- this.noteDraft = void 0;
5666
- this.areaDraft = void 0;
6221
+ this.clearDrafts();
5667
6222
  if (this.options.onRestoreItem) {
5668
6223
  await this.options.onRestoreItem(item);
5669
6224
  return;