@designfever/web-review-kit 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +7 -4
  2. package/dist/{chunk-RPVLRULC.js → chunk-4ZP7B7R6.js} +2345 -1786
  3. package/dist/chunk-4ZP7B7R6.js.map +1 -0
  4. package/dist/{chunk-K26WLRRP.js → chunk-UNDQZ4Y2.js} +33 -9
  5. package/dist/chunk-UNDQZ4Y2.js.map +1 -0
  6. package/dist/index.cjs +2381 -1801
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +17 -5
  9. package/dist/index.d.ts +17 -5
  10. package/dist/index.js +11 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/react-shell.cjs +14508 -11099
  13. package/dist/react-shell.cjs.map +1 -1
  14. package/dist/react-shell.d.cts +5 -4
  15. package/dist/react-shell.d.ts +5 -4
  16. package/dist/react-shell.js +10634 -7710
  17. package/dist/react-shell.js.map +1 -1
  18. package/dist/{types-DT9Z66mV.d.cts → types-BM8E7BFV.d.cts} +63 -3
  19. package/dist/{types-DT9Z66mV.d.ts → types-BM8E7BFV.d.ts} +63 -3
  20. package/dist/vite.cjs +344 -48
  21. package/dist/vite.cjs.map +1 -1
  22. package/dist/vite.d.cts +2 -0
  23. package/dist/vite.d.ts +2 -0
  24. package/dist/vite.js +346 -50
  25. package/dist/vite.js.map +1 -1
  26. package/docs/README.md +19 -11
  27. package/docs/adapters.md +56 -4
  28. package/docs/adaptor.sample.ts +53 -3
  29. package/docs/architecture.md +117 -3
  30. package/docs/figma-overlay.md +51 -0
  31. package/docs/grid-overlay.md +5 -0
  32. package/docs/installation.md +23 -8
  33. package/docs/release-notes-0.7.3.md +30 -0
  34. package/docs/release-notes-0.8.0.md +175 -0
  35. package/docs/testing.md +68 -0
  36. package/package.json +14 -5
  37. package/dist/chunk-K26WLRRP.js.map +0 -1
  38. package/dist/chunk-RPVLRULC.js.map +0 -1
  39. package/docs/code-review-0.6.0.md +0 -232
  40. package/docs/figma-image-mvp-0.7.0.md +0 -330
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  REVIEW_WORKFLOW_STATUS_OPTIONS: () => REVIEW_WORKFLOW_STATUS_OPTIONS,
31
31
  ReviewFigmaTokenError: () => ReviewFigmaTokenError,
32
32
  collectReviewFigmaReleaseSnapshot: () => collectReviewFigmaReleaseSnapshot,
33
+ createEndpointReviewFigmaImageStore: () => createEndpointReviewFigmaImageStore,
33
34
  createRemoteReviewFigmaImageStore: () => createRemoteReviewFigmaImageStore,
34
35
  createReviewFigmaFrameUrl: () => createReviewFigmaFrameUrl,
35
36
  createReviewFigmaImageStoreClient: () => createReviewFigmaImageStoreClient,
@@ -156,8 +157,8 @@ function localAdapter(options = {}) {
156
157
  function normalizeStoredReviewItem(value) {
157
158
  if (!value || typeof value !== "object") return void 0;
158
159
  const raw = value;
159
- const kind = raw.kind === "text" ? "note" : raw.kind === "capture" ? "area" : raw.kind;
160
- 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;
161
162
  const { screenshot: _screenshot, reviewNumber: _reviewNumber, ...item } = raw;
162
163
  if (kind === raw.kind && _screenshot === void 0 && _reviewNumber === void 0) {
163
164
  return raw;
@@ -332,6 +333,8 @@ function rowToReviewItem(row, options) {
332
333
  const routeKey = row.route_key || item.routeKey || item.normalizedPath || "/";
333
334
  const viewport = item.viewport ?? { width: 390, height: 720 };
334
335
  const now = (/* @__PURE__ */ new Date()).toISOString();
336
+ const kind = normalizeReviewItemKind(item);
337
+ if (!kind) return null;
335
338
  return {
336
339
  ...item,
337
340
  id: row.id,
@@ -340,7 +343,7 @@ function rowToReviewItem(row, options) {
340
343
  routeKey,
341
344
  pageUrl: item.pageUrl || toAbsoluteReviewUrl(routeKey),
342
345
  normalizedPath: item.normalizedPath || routeKey,
343
- kind: item.kind === "area" ? "area" : "note",
346
+ kind,
344
347
  comment: item.comment || "",
345
348
  status,
346
349
  viewport,
@@ -357,6 +360,10 @@ function rowToReviewItem(row, options) {
357
360
  updatedAt: row.updated_at ?? item.updatedAt ?? now
358
361
  };
359
362
  }
363
+ function normalizeReviewItemKind(item) {
364
+ if (item.kind === "area" || item.kind === "dom") return item.kind;
365
+ return null;
366
+ }
360
367
  async function unwrapResponse(request, label) {
361
368
  const { data, error } = await request;
362
369
  if (error) {
@@ -534,8 +541,14 @@ function createReviewFigmaImageApiUrl({
534
541
  // src/figma/image.store.ts
535
542
  var DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT = "/__dfwr/figma-images";
536
543
  function createReviewFigmaImageStoreClient(options = {}) {
544
+ return createEndpointReviewFigmaImageStore(options);
545
+ }
546
+ function createEndpointReviewFigmaImageStore(options = {}) {
537
547
  const endpoint = options.endpoint ?? DEFAULT_REVIEW_FIGMA_IMAGE_STORE_ENDPOINT;
538
- const request = createReviewFigmaImageStoreRequest(endpoint, options.fetch);
548
+ const request = createReviewFigmaImageStoreRequest(
549
+ options.fetch,
550
+ options.headers
551
+ );
539
552
  return {
540
553
  listImages(target) {
541
554
  const url = `${endpoint}?target=${encodeURIComponent(
@@ -756,19 +769,27 @@ function getReviewFigmaImageMimeType(format) {
756
769
  if (format === "png") return "image/png";
757
770
  return "image/webp";
758
771
  }
759
- function createReviewFigmaImageStoreRequest(endpoint, fetchOption) {
772
+ function createReviewFigmaImageStoreRequest(fetchOption, headersProvider) {
760
773
  return async (input, init = {}) => {
761
774
  const requestFetch = fetchOption ?? globalThis.fetch;
762
775
  if (!requestFetch) throw new Error("Figma image store requires fetch.");
763
776
  const figmaToken = init.figmaToken ?? "";
764
- const { figmaToken: _figmaToken, ...requestInit } = init;
777
+ const {
778
+ figmaToken: _figmaToken,
779
+ headers: requestHeaders,
780
+ ...requestInit
781
+ } = init;
782
+ const headers = new Headers();
783
+ headers.set("Content-Type", "application/json");
784
+ if (figmaToken) headers.set("X-Figma-Token", figmaToken);
785
+ appendReviewFigmaImageStoreHeaders(
786
+ headers,
787
+ await readReviewFigmaImageStoreHeaders(headersProvider)
788
+ );
789
+ appendReviewFigmaImageStoreHeaders(headers, requestHeaders);
765
790
  const response = await requestFetch(input, {
766
791
  ...requestInit,
767
- headers: {
768
- "Content-Type": "application/json",
769
- ...figmaToken ? { "X-Figma-Token": figmaToken } : {},
770
- ...requestInit.headers ?? {}
771
- }
792
+ headers
772
793
  });
773
794
  const text = await response.text();
774
795
  const body = text ? JSON.parse(text) : null;
@@ -779,6 +800,15 @@ function createReviewFigmaImageStoreRequest(endpoint, fetchOption) {
779
800
  return body;
780
801
  };
781
802
  }
803
+ async function readReviewFigmaImageStoreHeaders(provider) {
804
+ return typeof provider === "function" ? provider() : provider;
805
+ }
806
+ function appendReviewFigmaImageStoreHeaders(target, source) {
807
+ if (!source) return;
808
+ new Headers(source).forEach((value, key) => {
809
+ target.set(key, value);
810
+ });
811
+ }
782
812
  function readReviewFigmaImageToken(provider) {
783
813
  const token = typeof provider === "function" ? provider() : provider;
784
814
  return typeof token === "string" ? token.trim() : "";
@@ -1297,26 +1327,34 @@ function getPopoverBounds(environment) {
1297
1327
  }
1298
1328
  return environment.overlayRect;
1299
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
+ }
1300
1335
  function toHostPoint(point, environment) {
1301
1336
  if (!environment) return point;
1337
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
1302
1338
  return {
1303
- x: point.x + environment.viewportRect.left,
1304
- y: point.y + environment.viewportRect.top
1339
+ x: point.x * scaleX + environment.viewportRect.left,
1340
+ y: point.y * scaleY + environment.viewportRect.top
1305
1341
  };
1306
1342
  }
1307
1343
  function toHostSelection(selection, environment) {
1344
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
1308
1345
  return {
1309
- left: selection.left + environment.viewportRect.left,
1310
- top: selection.top + environment.viewportRect.top,
1311
- width: selection.width,
1312
- 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
1313
1350
  };
1314
1351
  }
1315
1352
  function toTargetPoint(point, environment) {
1316
1353
  if (!environment) return point;
1354
+ const { scaleX, scaleY } = getEnvironmentScale(environment);
1317
1355
  return {
1318
- x: point.x - environment.viewportRect.left,
1319
- y: point.y - environment.viewportRect.top
1356
+ x: (point.x - environment.viewportRect.left) / scaleX,
1357
+ y: (point.y - environment.viewportRect.top) / scaleY
1320
1358
  };
1321
1359
  }
1322
1360
  function toTargetPointFromHostEvent(event, environment) {
@@ -1365,11 +1403,6 @@ var SEMANTIC_ANCHOR_ATTRIBUTES = [
1365
1403
  "name",
1366
1404
  "href"
1367
1405
  ];
1368
- function getDomAnchor(selection, configuredAttribute = "data-qa-id", environment) {
1369
- const x = selection.left + selection.width / 2;
1370
- const y = selection.top + selection.height / 2;
1371
- return getDomAnchorFromPoint({ x, y }, configuredAttribute, environment);
1372
- }
1373
1406
  function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", environment) {
1374
1407
  const target = environment.document.elementFromPoint(point.x, point.y);
1375
1408
  if (!target) return void 0;
@@ -1919,6 +1952,34 @@ function createId() {
1919
1952
  }
1920
1953
 
1921
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
+ };
1922
1983
  function isHotkey(event, hotkey) {
1923
1984
  const parts = hotkey.split("+").map((part) => part.trim().toLowerCase()).filter(Boolean);
1924
1985
  const key = parts.find(
@@ -1939,10 +2000,7 @@ function isHotkeyKey(event, key) {
1939
2000
  const normalizedKey = key.toLowerCase();
1940
2001
  if (event.key.toLowerCase() === normalizedKey) return true;
1941
2002
  if (getHotkeyCode(normalizedKey) === event.code) return true;
1942
- const aliases = {
1943
- q: ["\u3142", "\u3143"]
1944
- };
1945
- return aliases[normalizedKey]?.includes(event.key) ?? false;
2003
+ return HOTKEY_KEY_ALIASES[normalizedKey]?.includes(event.key) ?? false;
1946
2004
  }
1947
2005
  function getHotkeyCode(key) {
1948
2006
  if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
@@ -1984,8 +2042,7 @@ var REVIEW_SCOPE_LABELS = {
1984
2042
  mobile: "Mobile",
1985
2043
  tablet: "Tablet",
1986
2044
  desktop: "Desktop",
1987
- wide: "Wide",
1988
- dom: "Element"
2045
+ wide: "Wide"
1989
2046
  };
1990
2047
  var normalizeReviewItemScope = (value) => {
1991
2048
  if (value === "element") return "dom";
@@ -2031,7 +2088,6 @@ function getReviewItemScope(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
2031
2088
  }
2032
2089
  function getReviewItemScopeLabel(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
2033
2090
  const scope = getReviewItemScope(item, presets);
2034
- if (scope === "dom") return REVIEW_SCOPE_LABELS.dom;
2035
2091
  const preset = findReviewViewportPreset(item.viewport, presets);
2036
2092
  return preset.label || REVIEW_SCOPE_LABELS[scope];
2037
2093
  }
@@ -2073,7 +2129,7 @@ function normalizeReviewNumber(value) {
2073
2129
  function getBoundMarkerPoint(item, environment) {
2074
2130
  const marker = getItemMarker(item);
2075
2131
  if (!marker) return void 0;
2076
- if (item.kind !== "area" && item.anchor && marker.relative) {
2132
+ if (item.anchor && marker.relative) {
2077
2133
  const resolved = resolveAnchorElement(item.anchor, environment);
2078
2134
  const element = resolved?.element;
2079
2135
  if (element) {
@@ -2121,27 +2177,25 @@ function getItemHighlightSelection(item, environment) {
2121
2177
  environment
2122
2178
  );
2123
2179
  }
2124
- return getVisibleHighlightSelection(
2125
- [
2126
- getAnchorHighlightSelection(item, environment),
2127
- getBoundSelection(item, environment),
2128
- getPointHighlightSelection(item, environment)
2129
- ],
2130
- environment
2131
- );
2180
+ return void 0;
2132
2181
  }
2133
2182
  function getReviewItemHighlightMode(item) {
2134
2183
  if (isDomReviewItem(item)) return "dom";
2135
- if (item.kind === "area") return "area";
2136
- return "note";
2184
+ return "area";
2137
2185
  }
2138
2186
  function getItemMarker(item) {
2139
2187
  if (item.marker) return item.marker;
2140
2188
  const selection = getItemSelection(item);
2141
2189
  if (!selection?.viewport) return void 0;
2142
2190
  return {
2143
- viewport: roundPoint(getSelectionCenter(selection.viewport)),
2144
- 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
2145
2199
  };
2146
2200
  }
2147
2201
  function getItemSelection(item) {
@@ -2160,17 +2214,20 @@ function getItemSelection(item) {
2160
2214
  function shouldShowMarkerForScope(scope, currentScope) {
2161
2215
  return scope === currentScope;
2162
2216
  }
2163
- function createSelectionCenterMarker(selection, anchor, environment) {
2164
- const centerPoint = getSelectionCenter(selection);
2217
+ function createSelectionStartMarker(selection, anchor, environment) {
2218
+ const startPoint = {
2219
+ x: selection.left,
2220
+ y: selection.top
2221
+ };
2165
2222
  return {
2166
- viewport: roundPoint(centerPoint),
2167
- relative: anchor ? getRelativePoint(centerPoint, anchor, environment) : void 0
2223
+ viewport: roundPoint(startPoint),
2224
+ relative: anchor ? getRelativePoint(startPoint, anchor, environment) : void 0
2168
2225
  };
2169
2226
  }
2170
2227
  function getBoundSelection(item, environment) {
2171
2228
  const selection = getItemSelection(item);
2172
2229
  if (!selection?.viewport) return void 0;
2173
- if (item.kind !== "area" && item.anchor && selection.relative) {
2230
+ if (item.anchor && selection.relative) {
2174
2231
  const resolved = resolveAnchorElement(item.anchor, environment);
2175
2232
  const element = resolved?.element;
2176
2233
  if (element) {
@@ -2232,7 +2289,7 @@ function getVisibleHighlightSelection(candidates, environment) {
2232
2289
  );
2233
2290
  }
2234
2291
  function isDomReviewItem(item) {
2235
- return item.scope === "dom" || item.kind === "note" && Boolean(item.anchor && getItemSelection(item));
2292
+ return item.kind === "dom" || item.scope === "dom";
2236
2293
  }
2237
2294
 
2238
2295
  // src/core/scroll.ts
@@ -2329,6 +2386,117 @@ function getAdjustedDraftSelection(selection, draft, presets) {
2329
2386
  };
2330
2387
  }
2331
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
+
2332
2500
  // src/core/typography.tokens.ts
2333
2501
  var reviewTypographyTokens = `
2334
2502
  --df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -2652,6 +2820,36 @@ function createStyleElement() {
2652
2820
  --dfwr-item-color-rgb: 255, 143, 97;
2653
2821
  }
2654
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
+
2655
2853
  .dfwr-item-target-highlight {
2656
2854
  position: fixed;
2657
2855
  z-index: 2;
@@ -2660,7 +2858,6 @@ function createStyleElement() {
2660
2858
  background: rgba(var(--dfwr-item-color-rgb), 0.08);
2661
2859
  box-shadow:
2662
2860
  0 0 0 1px rgba(31, 36, 40, 0.78),
2663
- 0 0 0 9999px rgba(0, 0, 0, 0.08),
2664
2861
  0 12px 30px rgba(0, 0, 0, 0.24);
2665
2862
  pointer-events: none;
2666
2863
  }
@@ -2744,89 +2941,6 @@ function createStyleElement() {
2744
2941
  border-style: dashed;
2745
2942
  }
2746
2943
 
2747
- .dfwr-bound-marker.is-note-callout,
2748
- .dfwr-bound-marker.is-note-callout.is-highlighted {
2749
- --dfwr-scope: #7cc7ff;
2750
- --dfwr-scope-rgb: 124, 199, 255;
2751
- min-width: 0;
2752
- width: 0;
2753
- height: 0;
2754
- padding: 0;
2755
- transform: none;
2756
- border: 0;
2757
- border-radius: 0;
2758
- background: transparent;
2759
- box-shadow: none;
2760
- color: var(--dfwr-scope);
2761
- animation: none;
2762
- overflow: visible;
2763
- }
2764
-
2765
- .dfwr-bound-marker.is-note-callout::before {
2766
- content: "";
2767
- position: absolute;
2768
- left: 0;
2769
- top: 0;
2770
- width: 8px;
2771
- height: 8px;
2772
- transform: translate(-50%, -50%);
2773
- border: 2px solid #111820;
2774
- border-radius: var(--df-review-radius-pill);
2775
- background: var(--dfwr-scope);
2776
- box-shadow:
2777
- 0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.22),
2778
- 0 6px 16px rgba(0, 0, 0, 0.28);
2779
- }
2780
-
2781
- .dfwr-bound-marker.is-note-callout.is-highlighted::before {
2782
- animation: dfwr-note-dot-pulse 1000ms ease-in-out infinite;
2783
- }
2784
-
2785
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon {
2786
- position: absolute;
2787
- left: 0;
2788
- top: 0;
2789
- width: 31px;
2790
- height: 2px;
2791
- transform: rotate(-42deg);
2792
- transform-origin: left center;
2793
- border-radius: var(--df-review-radius-pill);
2794
- background: currentColor;
2795
- opacity: 1;
2796
- }
2797
-
2798
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::before,
2799
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::after {
2800
- display: none;
2801
- }
2802
-
2803
- .dfwr-bound-marker.is-note-callout .dfwr-bound-marker-number {
2804
- position: absolute;
2805
- left: 24px;
2806
- top: -41px;
2807
- display: inline-flex;
2808
- align-items: center;
2809
- justify-content: center;
2810
- min-width: 28px;
2811
- height: 20px;
2812
- padding: 0 7px;
2813
- border: 1px solid var(--dfwr-scope);
2814
- border-radius: 4px;
2815
- background: var(--dfwr-scope);
2816
- box-shadow:
2817
- 0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.18),
2818
- 0 8px 18px rgba(0, 0, 0, 0.28);
2819
- color: #111820;
2820
- text-align: center;
2821
- line-height: 1;
2822
- white-space: nowrap;
2823
- }
2824
-
2825
- .dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-icon,
2826
- .dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-number {
2827
- animation: dfwr-selected-blink 1000ms ease-in-out infinite;
2828
- }
2829
-
2830
2944
  .dfwr-area-preview-layer .dfwr-bound-marker {
2831
2945
  border-color: #63d7c7;
2832
2946
  background: var(--df-review-color-panel);
@@ -2897,14 +3011,14 @@ function createStyleElement() {
2897
3011
  line-height: 1;
2898
3012
  }
2899
3013
 
2900
- .dfwr-note-draft {
3014
+ .dfwr-dom-draft {
2901
3015
  position: fixed;
2902
3016
  inset: 0;
2903
3017
  z-index: 4;
2904
3018
  pointer-events: none;
2905
3019
  }
2906
3020
 
2907
- .dfwr-note-pin {
3021
+ .dfwr-dom-pin {
2908
3022
  appearance: none;
2909
3023
  position: fixed;
2910
3024
  z-index: 5;
@@ -2922,11 +3036,11 @@ function createStyleElement() {
2922
3036
  pointer-events: auto;
2923
3037
  }
2924
3038
 
2925
- .dfwr-note-pin:active {
3039
+ .dfwr-dom-pin:active {
2926
3040
  cursor: grabbing;
2927
3041
  }
2928
3042
 
2929
- .dfwr-note-popover {
3043
+ .dfwr-dom-popover {
2930
3044
  position: fixed;
2931
3045
  z-index: 4;
2932
3046
  width: min(320px, calc(100vw - 24px));
@@ -2939,14 +3053,14 @@ function createStyleElement() {
2939
3053
  box-shadow: var(--df-review-shadow-popover);
2940
3054
  }
2941
3055
 
2942
- .dfwr-note-popover.is-composer,
3056
+ .dfwr-dom-popover.is-composer,
2943
3057
  .dfwr-area-draft.is-composer {
2944
3058
  max-height: min(360px, calc(100vh - 32px));
2945
3059
  overflow: auto;
2946
3060
  border-color: rgba(99, 215, 199, 0.56);
2947
3061
  }
2948
3062
 
2949
- .dfwr-shell.is-docked-composer .dfwr-note-popover.is-docked-composer,
3063
+ .dfwr-shell.is-docked-composer .dfwr-dom-popover.is-docked-composer,
2950
3064
  .dfwr-shell.is-docked-composer .dfwr-area-draft.is-docked-composer {
2951
3065
  position: relative;
2952
3066
  left: auto;
@@ -2960,7 +3074,7 @@ function createStyleElement() {
2960
3074
  min-height: 184px;
2961
3075
  }
2962
3076
 
2963
- .dfwr-note-popover.is-dragging,
3077
+ .dfwr-dom-popover.is-dragging,
2964
3078
  .dfwr-area-draft.is-dragging {
2965
3079
  user-select: none;
2966
3080
  }
@@ -3004,7 +3118,7 @@ function createStyleElement() {
3004
3118
  box-shadow: var(--df-review-shadow-popover);
3005
3119
  }
3006
3120
 
3007
- .dfwr-note-popover .dfwr-actions {
3121
+ .dfwr-dom-popover .dfwr-actions {
3008
3122
  padding: 0;
3009
3123
  }
3010
3124
 
@@ -3052,14 +3166,6 @@ function createStyleElement() {
3052
3166
  height: 18px;
3053
3167
  }
3054
3168
 
3055
- .dfwr-note-actions {
3056
- justify-content: flex-end;
3057
- }
3058
-
3059
- .dfwr-note-actions .dfwr-button:first-child {
3060
- margin-right: auto;
3061
- }
3062
-
3063
3169
  .dfwr-area-draft .dfwr-actions {
3064
3170
  padding: 0;
3065
3171
  }
@@ -3077,6 +3183,82 @@ function createStyleElement() {
3077
3183
  overflow-wrap: anywhere;
3078
3184
  }
3079
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
+
3080
3262
  .dfwr-input,
3081
3263
  .dfwr-select,
3082
3264
  .dfwr-textarea {
@@ -3379,18 +3561,6 @@ function createStyleElement() {
3379
3561
  }
3380
3562
  }
3381
3563
 
3382
- @keyframes dfwr-note-dot-pulse {
3383
- 0% {
3384
- transform: translate(-50%, -50%) scale(0.88);
3385
- }
3386
- 45% {
3387
- transform: translate(-50%, -50%) scale(1.3);
3388
- }
3389
- 100% {
3390
- transform: translate(-50%, -50%) scale(1);
3391
- }
3392
- }
3393
-
3394
3564
  @keyframes dfwr-selected-blink {
3395
3565
  0%,
3396
3566
  100% {
@@ -3439,1071 +3609,848 @@ function createStyleElement() {
3439
3609
  return style;
3440
3610
  }
3441
3611
 
3442
- // src/core/review/format.ts
3443
- function formatNoteDraftMeta(draft) {
3444
- const parts = [
3445
- `viewport ${formatSize(draft.viewport)}`,
3446
- `point ${formatPoint(draft.marker.viewport)}`
3447
- ];
3448
- if (draft.anchor) {
3449
- parts.push(formatAnchorMeta(draft.anchor));
3450
- }
3451
- return parts.join(" / ");
3452
- }
3453
- function formatItemMeta(item) {
3454
- const parts = [formatDate(item.createdAt)];
3455
- const marker = getItemMarker(item);
3456
- const selection = getItemSelection(item);
3457
- if (item.viewport) {
3458
- parts.push(`viewport ${formatSize(item.viewport)}`);
3459
- }
3460
- if (marker) {
3461
- parts.push(`point ${formatPoint(marker.viewport)}`);
3462
- }
3463
- if (selection) {
3464
- parts.push(`rect ${formatSelection(selection.viewport)}`);
3465
- }
3466
- if (item.anchor) {
3467
- parts.push(formatAnchorMeta(item.anchor));
3468
- }
3469
- return parts.join(" / ");
3470
- }
3471
- function formatDate(value) {
3472
- const date = new Date(value);
3473
- if (Number.isNaN(date.getTime())) return value;
3474
- return date.toLocaleString(void 0, {
3475
- month: "2-digit",
3476
- day: "2-digit",
3477
- hour: "2-digit",
3478
- 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();
3479
3631
  });
3480
3632
  }
3481
- function formatSize(size) {
3482
- return `${Math.round(size.width)}x${Math.round(size.height)}`;
3483
- }
3484
- function formatPoint(point) {
3485
- return `${Math.round(point.x)},${Math.round(point.y)}`;
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
+ };
3486
3693
  }
3487
- function formatSelection(selection) {
3488
- return [
3489
- Math.round(selection.x),
3490
- Math.round(selection.y),
3491
- Math.round(selection.width),
3492
- Math.round(selection.height)
3493
- ].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
+ };
3494
3744
  }
3495
- function formatAnchorMeta(anchor) {
3496
- const parts = [`dom ${anchor.strategy}`];
3497
- if (typeof anchor.confidence === "number") {
3498
- parts.push(`${Math.round(anchor.confidence * 100)}%`);
3499
- }
3500
- const candidates = getAnchorCandidates(anchor);
3501
- if (candidates.length > 1) {
3502
- parts.push(`${candidates.length} candidates`);
3503
- }
3504
- 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
+ };
3505
3760
  }
3506
-
3507
- // src/core/web.review.kit.view.ts
3508
- var DEFAULT_ADJUSTMENT_LABEL = "Responsive CSS px adjustments";
3509
- var WebReviewKitView = class {
3510
- constructor(config) {
3511
- this.config = config;
3512
- }
3513
- clearDraftPreview() {
3514
- this.restoreDraftPreview();
3515
- this.clearShellComposer();
3516
- }
3517
- render(shadow, hiddenItemsStyle) {
3518
- const state = this.state;
3519
- this.syncDraftPreview(
3520
- state.isOpen && state.mode === "element" ? state.noteDraft : void 0
3521
- );
3522
- shadow.replaceChildren();
3523
- shadow.append(createStyleElement());
3524
- shadow.append(hiddenItemsStyle);
3525
- const hasDismissableDraft = Boolean(state.noteDraft || state.areaDraft);
3526
- const shouldDockComposer = this.config.options.ui?.panel === false && hasDismissableDraft && Boolean(this.getShellComposerHost());
3527
- let dockedComposer;
3528
- const shell = document.createElement("div");
3529
- shell.className = [
3530
- "dfwr-shell",
3531
- state.isOpen ? "is-open" : "",
3532
- hasDismissableDraft && !shouldDockComposer ? "has-dismissible-draft" : ""
3533
- ].filter(Boolean).join(" ");
3534
- shell.setAttribute("aria-hidden", state.isOpen ? "false" : "true");
3535
- if (this.config.options.ui?.panel !== false) {
3536
- const panel = document.createElement("div");
3537
- panel.className = "dfwr-panel";
3538
- panel.setAttribute("role", "dialog");
3539
- panel.setAttribute("aria-label", "Web review kit");
3540
- panel.append(
3541
- this.createHeader(),
3542
- this.createToolbar(),
3543
- this.createBody(),
3544
- this.createList()
3545
- );
3546
- shell.append(panel);
3547
- }
3548
- shell.append(this.createMarkerLayer());
3549
- if (state.isOpen && hasDismissableDraft && !shouldDockComposer) {
3550
- shell.append(this.createDraftCancelLayer());
3551
- }
3552
- if (state.isOpen && (state.mode === "note" || state.mode === "element")) {
3553
- if (state.noteDraft) {
3554
- const noteDraft = this.createNotePopover(state.noteDraft, {
3555
- dockComposer: shouldDockComposer
3556
- });
3557
- shell.append(noteDraft.layer);
3558
- dockedComposer = noteDraft.composer;
3559
- } else {
3560
- shell.append(
3561
- state.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
3562
- );
3563
- }
3564
- }
3565
- if (state.isOpen && state.mode === "area" && !state.areaDraft && !state.isSelectingArea) {
3566
- shell.append(this.createAreaLayer());
3567
- }
3568
- if (state.isOpen && state.mode === "area" && state.areaDraft && this.config.options.ui?.panel === false) {
3569
- if (state.areaDraft.selection) {
3570
- shell.append(this.createAreaDraftOverlay(state.areaDraft));
3571
- }
3572
- const areaComposer = this.createAreaDraftPopover(state.areaDraft, {
3573
- dockComposer: shouldDockComposer
3574
- });
3575
- if (shouldDockComposer) {
3576
- dockedComposer = areaComposer;
3577
- } else {
3578
- shell.append(areaComposer);
3579
- }
3580
- }
3581
- shadow.append(shell);
3582
- this.renderShellComposer(dockedComposer);
3583
- }
3584
- get state() {
3585
- return this.config.getState();
3586
- }
3587
- getShellComposerHost() {
3588
- const environment = this.config.getEnvironment();
3589
- if (this.config.options.ui?.panel !== false) return void 0;
3590
- return environment?.composerHost ?? void 0;
3591
- }
3592
- renderShellComposer(composer) {
3593
- const host = composer ? this.getShellComposerHost() : void 0;
3594
- if (!host || !composer) {
3595
- this.clearShellComposer();
3596
- return;
3597
- }
3598
- if (this.shellComposerHost && this.shellComposerHost !== host) {
3599
- this.clearShellComposer();
3600
- }
3601
- this.shellComposerHost = host;
3602
- host.dataset.hasDraftComposer = "true";
3603
- if (host.parentElement) {
3604
- host.parentElement.dataset.hasDraftComposer = "true";
3605
- }
3606
- const shell = document.createElement("div");
3607
- shell.className = "dfwr-shell is-open is-shell-draft is-docked-composer";
3608
- shell.append(composer);
3609
- host.replaceChildren(createStyleElement(), shell);
3610
- }
3611
- clearShellComposer() {
3612
- const host = this.shellComposerHost;
3613
- host?.replaceChildren();
3614
- if (host) {
3615
- delete host.dataset.hasDraftComposer;
3616
- delete host.parentElement?.dataset.hasDraftComposer;
3617
- }
3618
- this.shellComposerHost = void 0;
3619
- }
3620
- createDraftCancelLayer() {
3621
- const layer = document.createElement("div");
3622
- layer.className = "dfwr-draft-cancel-layer";
3623
- layer.setAttribute("aria-hidden", "true");
3624
- layer.addEventListener("pointerdown", (event) => {
3625
- if (event.button !== 0) return;
3626
- this.cancelDraft(event);
3627
- });
3628
- return layer;
3629
- }
3630
- cancelDraft(event) {
3631
- event?.preventDefault();
3632
- event?.stopPropagation();
3633
- event?.stopImmediatePropagation();
3634
- this.config.actions.setModeState("idle");
3635
- this.config.actions.clearDrafts();
3636
- this.config.actions.setSelectingArea(false);
3637
- this.config.actions.render();
3638
- }
3639
- // Draft adjustment geometry lives in draft.metrics.ts; these thin wrappers
3640
- // supply the configured viewport presets so call sites stay unchanged.
3641
- get viewportPresets() {
3642
- return this.config.options.viewports?.presets;
3643
- }
3644
- getDraftAdjustmentMetrics(draft) {
3645
- return getDraftAdjustmentMetrics(draft, this.viewportPresets);
3646
- }
3647
- hasDraftAdjustment(draft) {
3648
- return hasDraftAdjustment(draft, this.viewportPresets);
3649
- }
3650
- getAdjustedDraftPoint(point, draft) {
3651
- return getAdjustedDraftPoint(point, draft, this.viewportPresets);
3652
- }
3653
- getAdjustedDraftSelection(selection, draft) {
3654
- return getAdjustedDraftSelection(
3655
- selection,
3656
- draft,
3657
- this.viewportPresets
3658
- );
3659
- }
3660
- getDraftViewportScale(viewport) {
3661
- return getDraftViewportScale(viewport, this.viewportPresets);
3662
- }
3663
- getDraftComposerWidth(environment) {
3664
- const bounds = environment.overlayRect;
3665
- const margin = 12;
3666
- return Math.min(360, Math.max(240, bounds.width - margin * 2));
3667
- }
3668
- getClampedComposerPosition(position, environment, size, bounds = environment.overlayRect) {
3669
- const margin = 12;
3670
- const width = size?.width ?? this.getDraftComposerWidth(environment);
3671
- const height = size?.height ?? 236;
3672
- return {
3673
- x: clamp(
3674
- position.x,
3675
- bounds.left + margin,
3676
- bounds.left + bounds.width - width - margin
3677
- ),
3678
- y: clamp(
3679
- position.y,
3680
- bounds.top + margin,
3681
- bounds.top + bounds.height - height - margin
3682
- )
3683
- };
3684
- }
3685
- getHostComposerBounds() {
3686
- const root = document.documentElement;
3687
- return {
3688
- left: 0,
3689
- top: 0,
3690
- width: root.clientWidth || window.innerWidth,
3691
- height: root.clientHeight || window.innerHeight
3692
- };
3693
- }
3694
- getInitialDraftComposerPosition(selection, environment, size) {
3695
- const bounds = this.getHostComposerBounds();
3696
- const margin = 12;
3697
- const gap = 20;
3698
- if (!selection) {
3699
- return this.getClampedComposerPosition(
3700
- {
3701
- x: environment.overlayRect.left + margin,
3702
- y: environment.overlayRect.top + margin
3703
- },
3704
- environment,
3705
- size,
3706
- bounds
3707
- );
3708
- }
3709
- const preferredX = selection.left + selection.width + gap;
3710
- const maxX = bounds.left + bounds.width - size.width - margin;
3711
- const x = preferredX <= maxX ? preferredX : selection.left - size.width - gap;
3712
- return this.getClampedComposerPosition(
3761
+ function getInitialDraftComposerPosition(selection, environment, size) {
3762
+ const bounds = getHostComposerBounds();
3763
+ const gap = 20;
3764
+ if (!selection) {
3765
+ return getClampedComposerPosition(
3713
3766
  {
3714
- x,
3715
- y: selection.top
3767
+ x: environment.overlayRect.left + COMPOSER_MARGIN,
3768
+ y: environment.overlayRect.top + COMPOSER_MARGIN
3716
3769
  },
3717
3770
  environment,
3718
3771
  size,
3719
3772
  bounds
3720
3773
  );
3721
3774
  }
3722
- getDraftComposerPosition({
3723
- 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
+ },
3724
3783
  environment,
3725
- composerPosition,
3726
- estimatedHeight
3727
- }) {
3728
- const width = this.getDraftComposerWidth(environment);
3729
- if (composerPosition) {
3730
- const clamped = this.getClampedComposerPosition(
3731
- composerPosition,
3732
- environment,
3733
- { width, height: estimatedHeight },
3734
- this.getHostComposerBounds()
3735
- );
3736
- return { width, left: clamped.x, top: clamped.y };
3737
- }
3738
- const position = this.getInitialDraftComposerPosition(selection, environment, {
3739
- width,
3740
- height: estimatedHeight
3741
- });
3742
- 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 };
3743
3803
  }
3744
- getSelectionMqMetrics(selection, viewport) {
3745
- const { scale } = this.getDraftViewportScale(viewport);
3746
- 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) {
3747
3915
  return {
3748
- x: selection.left * ratio,
3749
- y: selection.top * ratio,
3750
- width: selection.width * ratio,
3751
- height: selection.height * ratio
3916
+ viewport: draft.viewport,
3917
+ marker: draft.marker,
3918
+ selection: draft.selection
3752
3919
  };
3753
3920
  }
3754
- formatSignedPx(value) {
3755
- if (value === 0) return "+0px";
3756
- return `${value > 0 ? "+" : ""}${value}px`;
3757
- }
3758
- formatRoundedPx(value) {
3759
- 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;
3760
4159
  }
3761
- getAdjustmentLabel() {
3762
- return this.config.options.adjustmentLabel?.trim() || DEFAULT_ADJUSTMENT_LABEL;
4160
+ if (beforeSave?.length || className) {
4161
+ actions.append(cancel, ...beforeSave ?? [], save);
4162
+ return actions;
3763
4163
  }
3764
- getSelectionMetricLines(selection, viewport) {
3765
- if (!selection) return ["area", "x none / y none", "w none / h none"];
3766
- const metrics = this.getSelectionMqMetrics(selection, viewport);
3767
- return [
3768
- "area",
3769
- `x ${this.formatRoundedPx(metrics.x)} / y ${this.formatRoundedPx(
3770
- metrics.y
3771
- )}`,
3772
- `w ${this.formatRoundedPx(metrics.width)} / h ${this.formatRoundedPx(
3773
- metrics.height
3774
- )}`
3775
- ];
3776
- }
3777
- getAreaDraftMetricSelection(draft) {
3778
- if (!draft.selection) return void 0;
3779
- return toViewportSelection(draft.selection.viewport);
3780
- }
3781
- getDraftAdjustmentMetricLines(draft) {
3782
- const metrics = this.getDraftAdjustmentMetrics(draft);
3783
- return [
3784
- `x ${this.formatSignedPx(metrics.x)} / y ${this.formatSignedPx(
3785
- metrics.y
3786
- )}`,
3787
- `scale ${this.formatSignedPx(metrics.scale)}`
3788
- ];
3789
- }
3790
- withDraftAdjustmentComment(comment, draft) {
3791
- if (!this.hasDraftAdjustment(draft)) return comment;
3792
- const trimmedComment = comment.trim();
3793
- const metrics = this.getDraftAdjustmentMetrics(draft);
3794
- const adjustment = [
3795
- `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
3796
- metrics.x
3797
- )}, y ${this.formatSignedPx(metrics.y)}, scale ${this.formatSignedPx(
3798
- metrics.scale
3799
- )}`,
3800
- `(${metrics.presetLabel} viewport, ${Math.round(
3801
- metrics.viewportWidth
3802
- )}/design ${Math.round(metrics.designWidth)})`
3803
- ].join(" ");
3804
- return trimmedComment ? `${trimmedComment}
3805
- ${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));
3806
4191
  }
3807
- getAssigneeOption(assigneeId) {
3808
- if (!assigneeId) return void 0;
3809
- return this.config.options.assigneeOptions?.find(
3810
- (option) => option.value === assigneeId
3811
- );
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)}`);
3812
4200
  }
3813
- getAssigneeName(assigneeId) {
3814
- return this.getAssigneeOption(assigneeId)?.label;
4201
+ if (marker) {
4202
+ parts.push(`point ${formatPoint(marker.viewport)}`);
3815
4203
  }
3816
- createDraftTitleInput(value, onInput) {
3817
- const input = document.createElement("input");
3818
- input.className = "dfwr-input";
3819
- input.placeholder = "Title";
3820
- input.type = "text";
3821
- input.value = value ?? "";
3822
- input.addEventListener("input", () => onInput(input.value));
3823
- return input;
4204
+ if (selection) {
4205
+ parts.push(`rect ${formatSelection(selection.viewport)}`);
3824
4206
  }
3825
- isTitleFieldEnabled() {
3826
- return this.config.options.fields?.title === true;
3827
- }
3828
- createDraftAssigneeSelect(value, fallbackLabel, onChange) {
3829
- const assigneeOptions = this.config.options.assigneeOptions ?? [];
3830
- if (assigneeOptions.length === 0) return void 0;
3831
- const assigneeTitle = this.config.options.assigneeTitle?.trim() || "Assignee";
3832
- const select = document.createElement("select");
3833
- select.className = "dfwr-select";
3834
- const emptyOption = document.createElement("option");
3835
- emptyOption.value = "";
3836
- emptyOption.textContent = assigneeTitle;
3837
- select.append(emptyOption);
3838
- const hasUnknownAssignee = Boolean(value) && !assigneeOptions.some((option) => option.value === value);
3839
- if (hasUnknownAssignee && value) {
3840
- const option = document.createElement("option");
3841
- option.value = value;
3842
- option.textContent = fallbackLabel ?? value;
3843
- select.append(option);
3844
- }
3845
- assigneeOptions.forEach((assigneeOption) => {
3846
- const option = document.createElement("option");
3847
- option.value = assigneeOption.value;
3848
- option.textContent = assigneeOption.label;
3849
- select.append(option);
3850
- });
3851
- select.value = value ?? "";
3852
- select.addEventListener("change", () => {
3853
- onChange(select.value || null, this.getAssigneeName(select.value));
3854
- });
3855
- return select;
4207
+ if (item.anchor) {
4208
+ parts.push(formatAnchorMeta(item.anchor));
3856
4209
  }
3857
- getDraftFields(titleInput, textarea, assigneeSelect) {
3858
- const title = titleInput?.value.trim();
3859
- const comment = textarea.value.trim();
3860
- const assigneeId = assigneeSelect?.value.trim() || void 0;
3861
- return {
3862
- title: title || void 0,
3863
- comment,
3864
- assigneeId,
3865
- assigneeName: this.getAssigneeName(assigneeId)
3866
- };
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)}%`);
3867
4240
  }
3868
- getStyleableDraftElement(draft, environment) {
3869
- if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
3870
- return draft.previewElement;
3871
- }
3872
- if (!draft.anchor) return void 0;
3873
- const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
3874
- const element = resolveAnchorElement(
3875
- draft.anchor,
3876
- environment,
3877
- preferredSelection
3878
- )?.element;
3879
- if (!element) return void 0;
3880
- if ("style" in element) return element;
3881
- return void 0;
4241
+ const candidates = getAnchorCandidates(anchor);
4242
+ if (candidates.length > 1) {
4243
+ parts.push(`${candidates.length} candidates`);
3882
4244
  }
3883
- syncDraftPreview(draft) {
3884
- const environment = this.config.getEnvironment();
3885
- if (!draft || !environment || !this.hasDraftAdjustment(draft)) {
3886
- this.restoreDraftPreview();
3887
- return;
3888
- }
3889
- const element = this.getStyleableDraftElement(draft, environment);
3890
- if (!element) {
3891
- 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)) {
3892
4328
  return;
3893
4329
  }
3894
- if (this.draftPreview?.element !== element) {
3895
- this.restoreDraftPreview();
3896
- }
3897
- if (!this.draftPreview) {
3898
- const computedStyle = environment.window.getComputedStyle(element);
3899
- const clone = element.cloneNode(true);
3900
- this.removeDuplicateIds(clone);
3901
- this.copyComputedStyle(element, clone, environment);
3902
- this.positionDraftPreviewClone(clone, element, computedStyle);
3903
- environment.document.body?.appendChild(clone);
3904
- this.draftPreview = {
3905
- element,
3906
- clone,
3907
- visibility: element.style.visibility
3908
- };
3909
- element.style.visibility = "hidden";
3910
- }
3911
- const metrics = this.getDraftAdjustmentMetrics(draft);
3912
- const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
3913
- metrics.cssY
3914
- )}px)`;
3915
- const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
3916
- this.draftPreview.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
3917
- }
3918
- restoreDraftPreview() {
3919
- if (!this.draftPreview) return;
3920
- const { element, clone, visibility } = this.draftPreview;
3921
- clone.remove();
3922
- element.style.visibility = visibility;
3923
- this.draftPreview = void 0;
3924
- }
3925
- positionDraftPreviewClone(clone, element, computedStyle) {
3926
- const rect = element.getBoundingClientRect();
3927
- clone.setAttribute("data-dfwr-adjust-preview", "true");
3928
- clone.setAttribute("aria-hidden", "true");
3929
- clone.style.position = "fixed";
3930
- clone.style.left = `${this.toCssNumber(rect.left)}px`;
3931
- clone.style.top = `${this.toCssNumber(rect.top)}px`;
3932
- clone.style.right = "auto";
3933
- clone.style.bottom = "auto";
3934
- clone.style.width = `${this.toCssNumber(rect.width)}px`;
3935
- clone.style.height = `${this.toCssNumber(rect.height)}px`;
3936
- clone.style.maxWidth = "none";
3937
- clone.style.maxHeight = "none";
3938
- clone.style.margin = "0";
3939
- clone.style.boxSizing = "border-box";
3940
- clone.style.display = this.getDraftPreviewDisplay(computedStyle.display);
3941
- clone.style.zIndex = "2147483646";
3942
- clone.style.pointerEvents = "none";
3943
- clone.style.transition = "none";
3944
- clone.style.willChange = "transform";
3945
- clone.style.transformOrigin = "top left";
3946
- clone.style.transform = "none";
3947
- }
3948
- getDraftPreviewDisplay(display) {
3949
- if (display === "inline" || display === "contents") return "inline-block";
3950
- return display || "block";
3951
- }
3952
- copyComputedStyle(element, clone, environment) {
3953
- const computedStyle = environment.window.getComputedStyle(element);
3954
- for (let index = 0; index < computedStyle.length; index += 1) {
3955
- const property = computedStyle.item(index);
3956
- clone.style.setProperty(
3957
- property,
3958
- computedStyle.getPropertyValue(property),
3959
- computedStyle.getPropertyPriority(property)
3960
- );
3961
- }
3962
- }
3963
- removeDuplicateIds(element) {
3964
- element.removeAttribute("id");
3965
- element.querySelectorAll("[id]").forEach((child) => {
3966
- child.removeAttribute("id");
3967
- });
3968
- }
3969
- toCssNumber(value) {
3970
- return Math.round(value * 1e3) / 1e3;
3971
- }
3972
- createHeader() {
3973
- const header = document.createElement("div");
3974
- header.className = "dfwr-header";
3975
- const title = document.createElement("div");
3976
- title.className = "dfwr-title";
3977
- title.textContent = "Review Kit";
3978
- const meta = document.createElement("div");
3979
- meta.className = "dfwr-meta";
3980
- meta.textContent = getRouteKey(this.config.getEnvironment());
3981
- const titleGroup = document.createElement("div");
3982
- titleGroup.append(title, meta);
3983
- const close = document.createElement("button");
3984
- close.className = "dfwr-icon-button";
3985
- close.type = "button";
3986
- close.textContent = "x";
3987
- close.setAttribute("aria-label", "Close");
3988
- close.addEventListener("click", () => this.config.actions.close());
3989
- header.append(titleGroup, close);
3990
- return header;
3991
- }
3992
- createToolbar() {
3993
- const toolbar = document.createElement("div");
3994
- toolbar.className = "dfwr-toolbar";
3995
- toolbar.append(
3996
- this.createToolbarButton("Note", this.state.mode === "note", () => {
3997
- const mode = this.state.mode;
3998
- this.config.actions.setModeState(mode === "note" ? "idle" : "note");
3999
- this.config.actions.clearDrafts();
4000
- this.config.actions.render();
4001
- }),
4002
- this.createToolbarButton("Element", this.state.mode === "element", () => {
4003
- const mode = this.state.mode;
4004
- this.config.actions.setModeState(
4005
- 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
+ )
4006
4344
  );
4007
- this.config.actions.clearDrafts();
4008
- this.config.actions.render();
4009
- }),
4010
- this.createToolbarButton(
4011
- this.state.isSelectingArea ? "Selecting" : "Area",
4012
- this.state.mode === "area",
4013
- () => {
4014
- const mode = this.state.mode;
4015
- this.config.actions.setModeState(mode === "area" ? "idle" : "area");
4016
- this.config.actions.clearDrafts();
4017
- this.config.actions.render();
4018
- }
4019
- ),
4020
- this.createToolbarButton("Refresh", false, () => {
4021
- void this.config.actions.reload();
4022
- })
4023
- );
4024
- return toolbar;
4025
- }
4026
- createToolbarButton(label, active, onClick) {
4027
- const button = document.createElement("button");
4028
- button.className = `dfwr-button${active ? " is-active" : ""}`;
4029
- button.type = "button";
4030
- button.textContent = label;
4031
- button.addEventListener("click", onClick);
4032
- return button;
4033
- }
4034
- createBody() {
4035
- const body = document.createElement("div");
4036
- body.className = "dfwr-body";
4037
- const state = this.state;
4038
- if (state.mode === "idle") {
4039
- const empty = document.createElement("p");
4040
- empty.className = "dfwr-empty";
4041
- empty.textContent = "Add a note or mark an area.";
4042
- body.append(empty);
4043
- return body;
4044
- }
4045
- if (state.mode === "note" || state.mode === "element") {
4046
- body.append(this.createNoteBody());
4047
- return body;
4048
- }
4049
- body.append(this.createAreaForm());
4050
- return body;
4051
- }
4052
- createNoteBody() {
4053
- const empty = document.createElement("p");
4054
- empty.className = "dfwr-empty";
4055
- 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.";
4056
- return empty;
4057
- }
4058
- // Builds the note draft layer: the on-page marker/highlight plus its composer
4059
- // popover. When dockComposer is set the composer renders into the side panel
4060
- // instead of floating next to the marker (used for the docked review mode).
4061
- createNotePopover(draft, options = {}) {
4062
- const environment = this.config.getEnvironment();
4063
- const group = document.createElement("div");
4064
- group.className = "dfwr-note-draft";
4065
- if (!environment) return { layer: group, composer: void 0 };
4066
- const isElementDraft = this.state.mode === "element" && Boolean(draft.selection);
4067
- const hostPoint = toHostPoint(
4068
- isElementDraft ? this.getAdjustedDraftPoint(draft.marker.viewport, draft) : draft.marker.viewport,
4069
- environment
4070
- );
4071
- let selectionHighlight;
4072
- if (draft.selection) {
4073
- const selection = toViewportSelection(draft.selection.viewport);
4074
- selectionHighlight = this.createSelectionHighlight(
4075
- isElementDraft ? this.getAdjustedDraftSelection(selection, draft) : selection,
4076
- environment,
4077
- true
4078
- );
4079
- group.append(selectionHighlight);
4080
- }
4081
- const pin = document.createElement("button");
4082
- pin.className = "dfwr-note-pin";
4083
- pin.type = "button";
4084
- pin.setAttribute("aria-label", "Move note point");
4085
- pin.style.left = `${hostPoint.x}px`;
4086
- pin.style.top = `${hostPoint.y}px`;
4087
- const popover = document.createElement("div");
4088
- const position = getPopoverPosition(hostPoint, environment);
4089
- popover.className = [
4090
- "dfwr-note-popover",
4091
- isElementDraft ? "is-composer" : "",
4092
- options.dockComposer ? "is-docked-composer" : ""
4093
- ].filter(Boolean).join(" ");
4094
- if (options.dockComposer) {
4095
- popover.style.width = "100%";
4096
- } else if (isElementDraft) {
4097
- const selection = draft.selection ? toHostSelection(
4098
- this.getAdjustedDraftSelection(
4099
- toViewportSelection(draft.selection.viewport),
4100
- draft
4101
- ),
4102
- environment
4103
- ) : void 0;
4104
- const composer = this.getDraftComposerPosition({
4105
- selection,
4106
- environment,
4107
- composerPosition: draft.composerPosition,
4108
- estimatedHeight: 252
4109
- });
4110
- popover.style.left = `${composer.left}px`;
4111
- popover.style.top = `${composer.top}px`;
4112
- popover.style.width = `${composer.width}px`;
4113
- } else {
4114
- popover.style.left = `${position.left}px`;
4115
- popover.style.top = `${position.top}px`;
4116
- }
4117
- const form = document.createElement("form");
4118
- form.className = "dfwr-form";
4119
- const meta = isElementDraft ? void 0 : document.createElement("div");
4120
- if (meta) {
4121
- meta.className = "dfwr-item-date";
4122
- meta.textContent = formatNoteDraftMeta(draft);
4123
- }
4124
- const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(draft.title, (title) => {
4125
- const noteDraft = this.state.noteDraft;
4126
- if (!noteDraft) return;
4127
- this.config.actions.setNoteDraft({
4128
- ...noteDraft,
4129
- title
4130
- });
4131
- }) : void 0;
4132
- const textarea = document.createElement("textarea");
4133
- textarea.className = "dfwr-textarea";
4134
- textarea.placeholder = "Review comment";
4135
- textarea.rows = 4;
4136
- textarea.value = draft.comment ?? "";
4137
- textarea.addEventListener("input", () => {
4138
- const noteDraft = this.state.noteDraft;
4139
- if (!noteDraft) return;
4140
- this.config.actions.setNoteDraft({
4141
- ...noteDraft,
4142
- comment: textarea.value
4143
- });
4144
- });
4145
- const assigneeSelect = this.createDraftAssigneeSelect(
4146
- draft.assigneeId,
4147
- draft.assigneeName,
4148
- (assigneeId, assigneeName) => {
4149
- const noteDraft = this.state.noteDraft;
4150
- if (!noteDraft) return;
4151
- this.config.actions.setNoteDraft({
4152
- ...noteDraft,
4153
- assigneeId,
4154
- assigneeName
4155
- });
4345
+ return;
4156
4346
  }
4157
- );
4158
- const saveDraft = () => {
4159
- const currentDraft = this.state.noteDraft ?? draft;
4160
- const fields = this.getDraftFields(titleInput, textarea, assigneeSelect);
4161
- const comment = fields.comment;
4162
- if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
4163
- void this.config.actions.createItem({
4164
- kind: "note",
4165
- title: fields.title,
4166
- comment: this.withDraftAdjustmentComment(comment, currentDraft),
4167
- assigneeId: fields.assigneeId,
4168
- assigneeName: fields.assigneeName,
4169
- viewport: currentDraft.viewport,
4170
- anchor: currentDraft.anchor,
4171
- marker: currentDraft.marker,
4172
- selection: currentDraft.selection
4173
- });
4174
- };
4175
- const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
4176
- draft,
4177
- pin,
4178
- popover,
4179
- selectionHighlight,
4180
- textarea,
4181
- dockToggle: options.dockComposer
4182
- }) : void 0;
4183
- const actions = this.createFormActions("Save note", saveDraft, {
4184
- leading: adjustmentControls?.actionButton ? [adjustmentControls.actionButton] : void 0
4185
- });
4186
- const error = this.createDraftError();
4187
- form.append(
4188
- ...meta ? [meta] : [],
4189
- ...adjustmentControls ? [adjustmentControls.panel] : [],
4190
- ...titleInput ? [titleInput] : [],
4191
- textarea,
4192
- ...assigneeSelect ? [assigneeSelect] : [],
4193
- ...error ? [error] : [],
4194
- actions
4195
- );
4196
- const dragHandle = isElementDraft && !options.dockComposer ? this.createDraftDragHandle("Move DOM composer") : void 0;
4197
- popover.append(
4198
- ...dragHandle ? [dragHandle] : [],
4199
- form
4200
- );
4201
- group.append(pin);
4202
- if (!options.dockComposer) {
4203
- group.append(popover);
4204
- }
4205
- if (dragHandle) {
4206
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
4207
- const noteDraft = this.state.noteDraft ?? draft;
4208
- this.config.actions.setNoteDraft({
4209
- ...noteDraft,
4210
- composerPosition,
4211
- comment: textarea.value
4212
- });
4213
- });
4214
4347
  }
4215
- this.attachDraftPinDrag(
4216
- pin,
4217
- isElementDraft || options.dockComposer ? void 0 : popover,
4218
- meta,
4219
- textarea
4220
- );
4221
- if (!options.dockComposer) {
4222
- window.setTimeout(() => {
4223
- if (draft.adjustment?.isActive) {
4224
- adjustmentControls?.focusTarget.focus();
4225
- return;
4226
- }
4227
- textarea.focus();
4228
- }, 0);
4348
+ if (!showCompactMarkers && !isHighlighted) {
4349
+ return;
4229
4350
  }
4230
- return {
4231
- layer: group,
4232
- composer: options.dockComposer ? popover : void 0
4233
- };
4234
- }
4235
- createDraftDragHandle(label) {
4236
- const handle = document.createElement("button");
4237
- handle.className = "dfwr-draft-drag-handle";
4238
- handle.type = "button";
4239
- handle.setAttribute("aria-label", label);
4240
- return handle;
4241
- }
4242
- createIcon(paths) {
4243
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
4244
- svg.setAttribute("aria-hidden", "true");
4245
- svg.setAttribute("viewBox", "0 0 24 24");
4246
- svg.setAttribute("fill", "none");
4247
- svg.setAttribute("stroke", "currentColor");
4248
- svg.setAttribute("stroke-width", "2.4");
4249
- svg.setAttribute("stroke-linecap", "round");
4250
- svg.setAttribute("stroke-linejoin", "round");
4251
- paths.forEach((d) => {
4252
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
4253
- path.setAttribute("d", d);
4254
- svg.append(path);
4255
- });
4256
- return svg;
4257
- }
4258
- setAdjustmentToggleIcon(button, isActive) {
4259
- const paths = isActive ? ["M20 6 9 17l-5-5"] : [
4260
- "M12 2v20",
4261
- "M2 12h20",
4262
- "m9 5 3-3 3 3",
4263
- "m9 19 3 3 3-3",
4264
- "m5 9-3 3 3 3",
4265
- "m19 9 3 3-3 3"
4266
- ];
4267
- button.replaceChildren(this.createIcon(paths));
4268
- }
4269
- attachDraftComposerDrag(popover, handle, onMove) {
4270
- let isDragging = false;
4271
- let offsetX = 0;
4272
- let offsetY = 0;
4273
- const movePopover = (event) => {
4274
- const environment = this.config.getEnvironment();
4275
- if (!environment) return;
4276
- const position = this.getClampedComposerPosition(
4277
- {
4278
- x: event.clientX - offsetX,
4279
- y: event.clientY - offsetY
4280
- },
4281
- environment,
4282
- {
4283
- width: popover.offsetWidth,
4284
- height: popover.offsetHeight
4285
- },
4286
- this.getHostComposerBounds()
4287
- );
4288
- popover.style.left = `${position.x}px`;
4289
- popover.style.top = `${position.y}px`;
4290
- onMove(position);
4291
- };
4292
- handle.addEventListener("pointerdown", (event) => {
4293
- if (event.button !== 0) return;
4294
- const rect = popover.getBoundingClientRect();
4295
- offsetX = event.clientX - rect.left;
4296
- offsetY = event.clientY - rect.top;
4297
- isDragging = true;
4298
- event.preventDefault();
4299
- event.stopPropagation();
4300
- handle.setPointerCapture(event.pointerId);
4301
- popover.classList.add("is-dragging");
4302
- });
4303
- handle.addEventListener("pointermove", (event) => {
4304
- if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
4305
- event.preventDefault();
4306
- movePopover(event);
4307
- });
4308
- const stopDrag = (event) => {
4309
- if (!isDragging || !handle.hasPointerCapture(event.pointerId)) return;
4310
- event.preventDefault();
4311
- event.stopPropagation();
4312
- isDragging = false;
4313
- handle.releasePointerCapture(event.pointerId);
4314
- popover.classList.remove("is-dragging");
4315
- movePopover(event);
4316
- };
4317
- handle.addEventListener("pointerup", stopDrag);
4318
- handle.addEventListener("pointercancel", stopDrag);
4319
- }
4320
- // Builds the element-adjustment controls (nudge the previewed element via
4321
- // arrow keys / buttons). Wires keyboard deltas to the draft transform and
4322
- // keeps the pin, popover, highlight and textarea in sync as the value changes.
4323
- createAdjustmentControls({
4324
- draft,
4325
- pin,
4326
- popover,
4327
- selectionHighlight,
4328
- textarea,
4329
- dockToggle
4330
- }) {
4331
- const panel = document.createElement("div");
4332
- panel.className = "dfwr-adjust-panel is-dom-adjust-panel";
4333
- const header = document.createElement("div");
4334
- header.className = "dfwr-adjust-panel-header";
4335
- const help = document.createElement("div");
4336
- help.className = "dfwr-adjust-help";
4337
- help.textContent = this.getAdjustmentLabel();
4338
- const adjust = document.createElement("button");
4339
- adjust.className = "dfwr-adjust-toggle";
4340
- adjust.type = "button";
4341
- adjust.title = "Adjust DOM element with keyboard arrows";
4342
- adjust.setAttribute("aria-label", "Adjust DOM element with keyboard arrows");
4343
- const xyStatus = document.createElement("div");
4344
- xyStatus.className = "dfwr-adjust-status";
4345
- const scaleStatus = document.createElement("div");
4346
- scaleStatus.className = "dfwr-adjust-status";
4347
- const syncControls = (nextDraft) => {
4348
- const isActive = nextDraft.adjustment?.isActive === true;
4349
- panel.classList.toggle("is-active", isActive);
4350
- adjust.classList.toggle("is-active", isActive);
4351
- adjust.setAttribute("aria-pressed", isActive ? "true" : "false");
4352
- this.setAdjustmentToggleIcon(adjust, isActive);
4353
- adjust.title = isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows";
4354
- adjust.setAttribute(
4355
- "aria-label",
4356
- isActive ? "Finish DOM adjustment" : "Adjust DOM element with keyboard arrows"
4357
- );
4358
- const [xyLine, scaleLine] = this.getDraftAdjustmentMetricLines(nextDraft);
4359
- xyStatus.textContent = xyLine;
4360
- scaleStatus.textContent = scaleLine;
4361
- this.syncDraftAdjustmentUi({
4362
- draft: nextDraft,
4363
- pin,
4364
- selectionHighlight
4365
- });
4366
- };
4367
- const updateDraft = (updater) => {
4368
- const currentDraft = this.state.noteDraft ?? draft;
4369
- const nextDraft = updater(currentDraft);
4370
- this.config.actions.setNoteDraft({
4371
- ...nextDraft,
4372
- comment: textarea.value
4373
- });
4374
- syncControls(nextDraft);
4375
- };
4376
- adjust.addEventListener("click", () => {
4377
- updateDraft((currentDraft) => ({
4378
- ...currentDraft,
4379
- adjustment: {
4380
- x: currentDraft.adjustment?.x ?? 0,
4381
- y: currentDraft.adjustment?.y ?? 0,
4382
- scale: currentDraft.adjustment?.scale ?? 0,
4383
- isActive: currentDraft.adjustment?.isActive !== true
4384
- }
4385
- }));
4386
- adjust.focus();
4387
- });
4388
- popover.addEventListener("keydown", (event) => {
4389
- const currentDraft = this.state.noteDraft ?? draft;
4390
- if (currentDraft.adjustment?.isActive !== true) return;
4391
- const keyDelta = this.getAdjustmentKeyDelta(event);
4392
- if (!keyDelta) return;
4393
- event.preventDefault();
4394
- event.stopPropagation();
4395
- updateDraft((activeDraft) => ({
4396
- ...activeDraft,
4397
- adjustment: {
4398
- x: (activeDraft.adjustment?.x ?? 0) + keyDelta.x,
4399
- y: (activeDraft.adjustment?.y ?? 0) + keyDelta.y,
4400
- scale: (activeDraft.adjustment?.scale ?? 0) + keyDelta.scale,
4401
- isActive: true
4402
- }
4403
- }));
4404
- });
4405
- header.append(help);
4406
- if (!dockToggle) {
4407
- header.append(adjust);
4351
+ const point = getBoundMarkerPoint(item, environment);
4352
+ if (!point || !isPointInViewport(point.viewport, environment)) {
4353
+ return;
4408
4354
  }
4409
- panel.append(header, xyStatus, scaleStatus);
4410
- syncControls(draft);
4411
- return {
4412
- panel,
4413
- focusTarget: adjust,
4414
- actionButton: dockToggle ? adjust : void 0
4415
- };
4416
- }
4417
- getAdjustmentKeyDelta(event) {
4418
- const step = event.shiftKey ? 10 : 1;
4419
- if (event.key === "ArrowLeft") return { x: -step, y: 0, scale: 0 };
4420
- if (event.key === "ArrowRight") return { x: step, y: 0, scale: 0 };
4421
- if (event.key === "ArrowUp") return { x: 0, y: -step, scale: 0 };
4422
- if (event.key === "ArrowDown") return { x: 0, y: step, scale: 0 };
4423
- if (event.key.toLowerCase() === "w") return { x: 0, y: 0, scale: step };
4424
- if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
4425
- return void 0;
4426
- }
4427
- syncDraftAdjustmentUi({
4428
- draft,
4429
- pin,
4430
- selectionHighlight
4431
- }) {
4432
- const environment = this.config.getEnvironment();
4433
- if (!environment) return;
4434
- const hostPoint = toHostPoint(
4435
- this.getAdjustedDraftPoint(draft.marker.viewport, draft),
4436
- 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
4437
4363
  );
4438
- pin.style.left = `${hostPoint.x}px`;
4439
- pin.style.top = `${hostPoint.y}px`;
4440
- if (draft.selection && selectionHighlight) {
4441
- const rect = toHostSelection(
4442
- this.getAdjustedDraftSelection(
4443
- toViewportSelection(draft.selection.viewport),
4444
- draft
4445
- ),
4446
- environment
4447
- );
4448
- selectionHighlight.style.left = `${rect.left}px`;
4449
- selectionHighlight.style.top = `${rect.top}px`;
4450
- selectionHighlight.style.width = `${rect.width}px`;
4451
- selectionHighlight.style.height = `${rect.height}px`;
4452
- }
4453
- this.syncDraftPreview(draft);
4454
- }
4455
- createAreaForm() {
4456
- const form = document.createElement("form");
4457
- form.className = "dfwr-form";
4458
- const areaDraft = this.state.areaDraft;
4459
- if (!areaDraft) {
4460
- const empty = document.createElement("p");
4461
- empty.className = "dfwr-empty";
4462
- empty.textContent = "Drag on the page to select an area.";
4463
- form.append(empty);
4464
- return form;
4465
- }
4466
- form.append(this.createAreaMetricsPanel(areaDraft));
4467
- const titleInput = this.isTitleFieldEnabled() ? this.createDraftTitleInput(areaDraft.title, (title) => {
4468
- const draft = this.state.areaDraft;
4469
- if (!draft) return;
4470
- 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({
4471
4419
  ...draft,
4472
- title
4420
+ comment
4473
4421
  });
4474
- }) : void 0;
4475
- const textarea = document.createElement("textarea");
4476
- textarea.className = "dfwr-textarea";
4477
- textarea.placeholder = "Area comment";
4478
- textarea.rows = 4;
4479
- textarea.value = areaDraft.comment ?? "";
4480
- textarea.addEventListener("input", () => {
4481
- 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;
4482
4431
  if (!draft) return;
4483
- this.config.actions.setAreaDraft({
4432
+ config.actions.setAreaDraft({
4484
4433
  ...draft,
4485
- comment: textarea.value
4434
+ assigneeId,
4435
+ assigneeName
4486
4436
  });
4487
- });
4488
- const assigneeSelect = this.createDraftAssigneeSelect(
4489
- areaDraft.assigneeId,
4490
- areaDraft.assigneeName,
4491
- (assigneeId, assigneeName) => {
4492
- const draft = this.state.areaDraft;
4493
- if (!draft) return;
4494
- this.config.actions.setAreaDraft({
4495
- ...draft,
4496
- assigneeId,
4497
- assigneeName
4498
- });
4499
- }
4500
- );
4501
- const actions = this.createFormActions("Save area", () => {
4502
- const draft = this.state.areaDraft;
4503
- 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
+ );
4504
4451
  const comment = fields.comment;
4505
- if (!comment || !draft) return;
4506
- void this.config.actions.createItem({
4452
+ if (!comment && !draft?.attachments?.length || !draft) return;
4453
+ void config.actions.createItem({
4507
4454
  kind: "area",
4508
4455
  title: fields.title,
4509
4456
  comment,
@@ -4512,603 +4459,1077 @@ ${adjustment}` : adjustment;
4512
4459
  viewport: draft.viewport,
4513
4460
  anchor: draft.anchor,
4514
4461
  marker: draft.marker,
4515
- selection: draft.selection
4462
+ selection: draft.selection,
4463
+ attachments: draft.attachments
4516
4464
  });
4517
- });
4518
- const error = this.createDraftError();
4519
- form.append(
4520
- ...titleInput ? [titleInput] : [],
4521
- textarea,
4522
- ...assigneeSelect ? [assigneeSelect] : [],
4523
- ...error ? [error] : [],
4524
- actions
4525
- );
4526
- return form;
4527
- }
4528
- createAreaMetricsPanel(draft) {
4529
- const panel = document.createElement("div");
4530
- panel.className = "dfwr-adjust-panel is-area-metrics-panel";
4531
- const help = document.createElement("div");
4532
- help.className = "dfwr-adjust-help";
4533
- const [labelLine, xyLine, sizeLine] = this.getSelectionMetricLines(
4534
- this.getAreaDraftMetricSelection(draft),
4535
- draft.viewport
4536
- );
4537
- help.textContent = labelLine;
4538
- const xyStatus = document.createElement("div");
4539
- xyStatus.className = "dfwr-adjust-status";
4540
- xyStatus.textContent = xyLine;
4541
- const sizeStatus = document.createElement("div");
4542
- sizeStatus.className = "dfwr-adjust-status";
4543
- sizeStatus.textContent = sizeLine;
4544
- panel.append(help, xyStatus, sizeStatus);
4545
- return panel;
4546
- }
4547
- createAreaDraftOverlay(draft) {
4548
- const layer = document.createElement("div");
4549
- layer.className = "dfwr-area-preview-layer";
4550
- const environment = this.config.getEnvironment();
4551
- if (!environment || !draft.selection) return layer;
4552
- const selection = toViewportSelection(draft.selection.viewport);
4553
- layer.append(this.createSelectionHighlight(selection, environment, true));
4554
- if (draft.marker) {
4555
- const hostPoint = toHostPoint(draft.marker.viewport, environment);
4556
- layer.append(
4557
- this.createMarkerElement(
4558
- void 0,
4559
- hostPoint,
4560
- "\u2022",
4561
- getReviewViewportScope(
4562
- draft.viewport,
4563
- this.config.options.viewports?.presets
4564
- ),
4565
- true,
4566
- true
4567
- )
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
4568
4482
  );
4483
+ config.actions.setAreaDraft({
4484
+ ...draft,
4485
+ comment: textarea.value,
4486
+ attachments: attachments.length > 0 ? attachments : void 0
4487
+ });
4488
+ config.actions.render();
4569
4489
  }
4570
- 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
+ );
4571
4544
  }
4572
- createAreaDraftPopover(draft, options = {}) {
4573
- const environment = this.config.getEnvironment();
4574
- const popover = document.createElement("div");
4575
- popover.className = [
4576
- "dfwr-area-draft",
4577
- "is-composer",
4578
- options.dockComposer ? "is-docked-composer" : ""
4579
- ].filter(Boolean).join(" ");
4580
- if (options.dockComposer) {
4581
- popover.style.width = "100%";
4582
- } else if (environment && draft.selection) {
4583
- const selection = toHostSelection(
4584
- toViewportSelection(draft.selection.viewport),
4585
- environment
4586
- );
4587
- const composer = this.getDraftComposerPosition({
4588
- selection,
4589
- environment,
4590
- composerPosition: draft.composerPosition,
4591
- estimatedHeight: 220
4592
- });
4593
- popover.style.left = `${composer.left}px`;
4594
- popover.style.top = `${composer.top}px`;
4595
- popover.style.width = `${composer.width}px`;
4596
- popover.style.right = "auto";
4597
- }
4598
- const dragHandle = options.dockComposer ? void 0 : this.createDraftDragHandle("Move area composer");
4599
- popover.append(
4600
- ...dragHandle ? [dragHandle] : [],
4601
- 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
4602
4562
  );
4603
- if (dragHandle) {
4604
- this.attachDraftComposerDrag(popover, dragHandle, (composerPosition) => {
4605
- const areaDraft = this.state.areaDraft ?? draft;
4606
- 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({
4607
4587
  ...areaDraft,
4608
4588
  composerPosition
4609
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
4610
4714
  });
4611
4715
  }
4612
- return popover;
4613
- }
4614
- createFormActions(saveLabel, onSave, options) {
4615
- const actions = document.createElement("div");
4616
- actions.className = ["dfwr-actions", options?.className].filter(Boolean).join(" ");
4617
- const isSaving = this.state.isCreatingItem;
4618
- const save = document.createElement("button");
4619
- save.className = "dfwr-button is-primary";
4620
- save.type = "button";
4621
- save.disabled = isSaving;
4622
- save.setAttribute("aria-busy", isSaving ? "true" : "false");
4623
- if (isSaving) {
4624
- save.append(this.createSpinner("dfwr-spinner"), "Saving...");
4625
- } else {
4626
- save.textContent = saveLabel;
4627
- }
4628
- save.addEventListener("click", (event) => {
4629
- event.preventDefault();
4630
- event.stopPropagation();
4631
- if (this.state.isCreatingItem) return;
4632
- 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
4633
4745
  });
4634
- const cancel = document.createElement("button");
4635
- cancel.className = "dfwr-button";
4636
- cancel.type = "button";
4637
- cancel.disabled = isSaving;
4638
- cancel.textContent = "Cancel";
4639
- cancel.addEventListener("click", (event) => {
4640
- 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
+ }
4641
4817
  });
4642
- if (options?.leading?.length) {
4643
- actions.classList.add("has-leading");
4644
- const leading = document.createElement("div");
4645
- leading.className = "dfwr-actions-leading";
4646
- leading.append(...options.leading);
4647
- const primary = document.createElement("div");
4648
- primary.className = "dfwr-actions-primary";
4649
- primary.append(save, cancel);
4650
- actions.append(leading, primary);
4651
- return actions;
4652
- }
4653
- if (options?.beforeSave?.length || options?.className) {
4654
- actions.append(cancel, ...options.beforeSave ?? [], save);
4655
- return actions;
4656
- }
4657
- actions.append(save, cancel);
4658
- return actions;
4659
4818
  }
4660
- createSpinner(className) {
4661
- const spinner = document.createElement("span");
4662
- spinner.className = className;
4663
- spinner.setAttribute("aria-hidden", "true");
4664
- return spinner;
4665
- }
4666
- createDraftError() {
4667
- if (!this.state.draftError) return void 0;
4668
- const error = document.createElement("p");
4669
- error.className = "dfwr-form-error";
4670
- error.setAttribute("role", "alert");
4671
- error.textContent = this.state.draftError;
4672
- return error;
4673
- }
4674
- createList() {
4675
- const section = document.createElement("div");
4676
- section.className = "dfwr-list";
4677
- const state = this.state;
4678
- const heading = document.createElement("div");
4679
- heading.className = "dfwr-list-heading";
4680
- heading.textContent = `Review items (${state.items.length})`;
4681
- section.append(heading);
4682
- if (state.items.length === 0) {
4683
- const empty = document.createElement("p");
4684
- empty.className = "dfwr-empty";
4685
- empty.textContent = "No review items on this page.";
4686
- section.append(empty);
4687
- return section;
4688
- }
4689
- for (const numberedItem of getNumberedReviewItems(
4690
- state.items,
4691
- this.config.options.viewports?.presets
4692
- )) {
4693
- section.append(this.createListItem(numberedItem));
4694
- }
4695
- 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);
4696
4833
  }
4697
- createListItem(numberedItem) {
4698
- const { item } = numberedItem;
4699
- const row = document.createElement("article");
4700
- row.className = "dfwr-item";
4701
- row.tabIndex = 0;
4702
- row.setAttribute("role", "button");
4703
- 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(
4704
4873
  "aria-label",
4705
- `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
4706
4879
  );
4707
- row.addEventListener("click", () => {
4708
- void this.config.actions.restoreItem(item);
4880
+ xyStatus.textContent = xyLine;
4881
+ scaleStatus.textContent = scaleLine;
4882
+ syncDraftAdjustmentUi(context, {
4883
+ draft: nextDraft,
4884
+ pin,
4885
+ selectionHighlight
4709
4886
  });
4710
- row.addEventListener("keydown", (event) => {
4711
- if (event.key !== "Enter" && event.key !== " ") return;
4712
- event.preventDefault();
4713
- 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
4714
4894
  });
4715
- const body = document.createElement("div");
4716
- body.className = "dfwr-item-body";
4717
- const badges = document.createElement("div");
4718
- badges.className = "dfwr-item-badges";
4719
- const scope = document.createElement("div");
4720
- scope.className = `dfwr-item-scope is-scope-${numberedItem.scope}`;
4721
- scope.textContent = numberedItem.displayLabel;
4722
- const kind = document.createElement("div");
4723
- kind.className = "dfwr-item-kind";
4724
- kind.textContent = item.kind;
4725
- badges.append(scope, kind);
4726
- const title = this.isTitleFieldEnabled() ? item.title?.trim() : "";
4727
- const titleElement = title ? document.createElement("strong") : void 0;
4728
- if (title && titleElement) {
4729
- titleElement.className = "dfwr-item-title";
4730
- titleElement.textContent = title;
4731
- }
4732
- const comment = document.createElement("p");
4733
- comment.className = `dfwr-item-comment${title ? "" : " is-primary"}`;
4734
- comment.textContent = item.comment;
4735
- const date = document.createElement("time");
4736
- date.className = "dfwr-item-date";
4737
- date.dateTime = item.createdAt;
4738
- date.textContent = formatItemMeta(item);
4739
- body.append(badges, ...titleElement ? [titleElement] : [], comment, date);
4740
- const actions = document.createElement("div");
4741
- actions.className = "dfwr-item-actions";
4742
- actions.addEventListener("click", (event) => event.stopPropagation());
4743
- actions.addEventListener("keydown", (event) => event.stopPropagation());
4744
- const remove = document.createElement("button");
4745
- remove.className = "dfwr-icon-button";
4746
- remove.type = "button";
4747
- remove.textContent = "x";
4748
- remove.setAttribute("aria-label", "Delete");
4749
- remove.addEventListener("click", (event) => {
4750
- event.stopPropagation();
4751
- 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
4752
5031
  });
4753
- actions.append(remove);
4754
- row.append(body, actions);
4755
- 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;
4756
5132
  }
4757
- createMarkerLayer() {
4758
- const layer = document.createElement("div");
4759
- layer.className = "dfwr-marker-layer";
4760
- const environment = this.config.getEnvironment();
4761
- if (!environment) return layer;
4762
- const currentScope = getReviewViewportScope(
4763
- getViewportSize(environment),
4764
- 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
+ )
4765
5138
  );
4766
- getNumberedReviewItems(
4767
- this.state.items,
4768
- this.config.options.viewports?.presets
4769
- ).forEach((numberedItem) => {
4770
- const { item, scope, displayLabel } = numberedItem;
4771
- if (!shouldShowMarkerForScope(scope, currentScope)) {
4772
- return;
4773
- }
4774
- const isHighlighted = item.id === this.state.highlightedItemId;
4775
- const highlightMode = getReviewItemHighlightMode(item);
4776
- if (highlightMode !== "note" && (!this.state.highlightedItemId || isHighlighted)) {
4777
- const selection = getItemHighlightSelection(item, environment);
4778
- if (selection) {
4779
- layer.append(
4780
- ...this.createItemHighlightElements(
4781
- selection.viewport,
4782
- environment,
4783
- item,
4784
- displayLabel,
4785
- selection.isBound,
4786
- isHighlighted
4787
- )
4788
- );
4789
- return;
4790
- }
4791
- }
4792
- const point = getBoundMarkerPoint(item, environment);
4793
- if (!point || !isPointInViewport(point.viewport, environment)) {
4794
- 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);
4795
5326
  }
4796
- const hostPoint = toHostPoint(point.viewport, environment);
4797
- const marker = this.createMarkerElement(
4798
- item.id,
4799
- hostPoint,
4800
- displayLabel,
4801
- scope,
4802
- point.isBound,
4803
- isHighlighted,
4804
- highlightMode === "note" ? "note" : "default"
4805
- );
4806
- marker.title = `${displayLabel} / ${item.comment}
4807
- ${formatItemMeta(item)}`;
4808
- 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())
4809
5394
  });
4810
- return layer;
5395
+ this.draftContext = {
5396
+ config: this.config,
5397
+ cancelDraft: (event) => this.cancelDraft(event),
5398
+ syncDraftPreview: (draft) => this.draftPreview.sync(draft)
5399
+ };
4811
5400
  }
4812
- createItemHighlightElements(selection, environment, item, label, isBound, isHighlighted) {
4813
- const rect = toHostSelection(selection, environment);
4814
- const mode = getReviewItemHighlightMode(item);
4815
- const highlight = document.createElement("div");
4816
- highlight.className = [
4817
- "dfwr-item-target-highlight",
4818
- `is-mode-${mode}`,
4819
- isBound ? "is-bound" : "is-fallback",
4820
- isHighlighted ? "is-highlighted" : ""
4821
- ].filter(Boolean).join(" ");
4822
- highlight.style.left = `${rect.left}px`;
4823
- highlight.style.top = `${rect.top}px`;
4824
- highlight.style.width = `${rect.width}px`;
4825
- highlight.style.height = `${rect.height}px`;
4826
- highlight.dataset.reviewItemId = item.id;
4827
- const labelElement = document.createElement("div");
4828
- labelElement.className = [
4829
- "dfwr-item-target-label",
4830
- `is-mode-${mode}`,
4831
- isHighlighted ? "is-highlighted" : ""
4832
- ].filter(Boolean).join(" ");
4833
- labelElement.textContent = label;
4834
- labelElement.style.left = `${Math.max(4, rect.left)}px`;
4835
- labelElement.style.top = `${Math.max(4, rect.top - 24)}px`;
4836
- labelElement.dataset.reviewItemId = item.id;
4837
- return [highlight, labelElement];
4838
- }
4839
- createSelectionHighlight(selection, environment, isDraft) {
4840
- const rect = toHostSelection(selection, environment);
4841
- const highlight = document.createElement("div");
4842
- highlight.className = `dfwr-selection-highlight${isDraft ? " is-draft" : ""}`;
4843
- highlight.style.left = `${rect.left}px`;
4844
- highlight.style.top = `${rect.top}px`;
4845
- highlight.style.width = `${rect.width}px`;
4846
- highlight.style.height = `${rect.height}px`;
4847
- return highlight;
4848
- }
4849
- createMarkerElement(itemId, hostPoint, label, scope, isBound, isHighlighted, variant = "default") {
4850
- const isNoteCallout = variant === "note";
4851
- const marker = document.createElement("div");
4852
- marker.className = [
4853
- "dfwr-bound-marker",
4854
- isNoteCallout ? "is-note-callout" : "",
4855
- `is-scope-${scope}`,
4856
- isBound ? "is-bound" : "is-fallback",
4857
- 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" : ""
4858
5422
  ].filter(Boolean).join(" ");
4859
- marker.style.left = `${hostPoint.x}px`;
4860
- marker.style.top = `${hostPoint.y}px`;
4861
- marker.dataset.scope = scope;
4862
- if (itemId) {
4863
- marker.dataset.reviewItemId = itemId;
4864
- }
4865
- const iconElement = document.createElement("span");
4866
- iconElement.className = "dfwr-bound-marker-icon";
4867
- iconElement.setAttribute("aria-hidden", "true");
4868
- const labelElement = document.createElement("span");
4869
- labelElement.className = "dfwr-bound-marker-number";
4870
- labelElement.textContent = label;
4871
- marker.append(iconElement, labelElement);
4872
- return marker;
4873
- }
4874
- attachDraftPinDrag(pin, popover, meta, textarea) {
4875
- let isDragging = false;
4876
- const moveDraftUi = (hostPoint) => {
4877
- const environment = this.config.getEnvironment();
4878
- if (!environment) return;
4879
- const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
4880
- const nextHostPoint = toHostPoint(nextPoint, environment);
4881
- pin.style.left = `${nextHostPoint.x}px`;
4882
- pin.style.top = `${nextHostPoint.y}px`;
4883
- if (popover) {
4884
- const position = getPopoverPosition(nextHostPoint, environment);
4885
- popover.style.left = `${position.left}px`;
4886
- 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));
4887
5453
  }
4888
- const noteDraft = this.state.noteDraft;
4889
- if (!noteDraft) return;
4890
- const nextDraft = {
4891
- ...noteDraft,
4892
- marker: {
4893
- ...noteDraft.marker,
4894
- viewport: roundPoint(nextPoint)
4895
- },
4896
- comment: textarea.value
4897
- };
4898
- this.config.actions.setNoteDraft(nextDraft);
4899
- if (meta) {
4900
- 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));
4901
5461
  }
4902
- };
4903
- pin.addEventListener("pointerdown", (event) => {
4904
- if (event.button !== 0) return;
4905
- event.preventDefault();
4906
- event.stopPropagation();
4907
- isDragging = true;
4908
- pin.setPointerCapture(event.pointerId);
4909
- });
4910
- pin.addEventListener("pointermove", (event) => {
4911
- if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
4912
- event.preventDefault();
4913
- moveDraftUi({
4914
- x: event.clientX,
4915
- y: event.clientY
4916
- });
4917
- });
4918
- pin.addEventListener("pointerup", (event) => {
4919
- if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
4920
- event.preventDefault();
4921
- event.stopPropagation();
4922
- isDragging = false;
4923
- pin.releasePointerCapture(event.pointerId);
4924
- const nextPoint = toTargetPointFromHostEvent(
4925
- event,
4926
- this.config.getEnvironment()
5462
+ const areaComposer = createAreaDraftPopover(
5463
+ this.draftContext,
5464
+ state.areaDraft,
5465
+ { dockComposer: shouldDockComposer }
4927
5466
  );
4928
- const currentDraft = this.state.noteDraft;
4929
- const fields = {
4930
- title: currentDraft?.title,
4931
- comment: textarea.value,
4932
- assigneeId: currentDraft?.assigneeId,
4933
- assigneeName: currentDraft?.assigneeName
4934
- };
4935
- void (this.state.mode === "element" ? this.config.actions.bindElementDraftToPoint(nextPoint, fields) : this.config.actions.bindNoteDraftToPoint(nextPoint, fields));
4936
- });
5467
+ if (shouldDockComposer) {
5468
+ dockedComposer = areaComposer;
5469
+ } else {
5470
+ shell.append(areaComposer);
5471
+ }
5472
+ }
5473
+ shadow.append(shell);
5474
+ this.renderShellComposer(dockedComposer);
4937
5475
  }
4938
- createNoteLayer() {
4939
- const layer = document.createElement("div");
4940
- layer.className = "dfwr-text-layer";
5476
+ get state() {
5477
+ return this.config.getState();
5478
+ }
5479
+ getShellComposerHost() {
4941
5480
  const environment = this.config.getEnvironment();
4942
- if (environment) {
4943
- 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;
4944
5490
  }
4945
- layer.addEventListener("pointerdown", (event) => {
4946
- if (event.button !== 0) return;
4947
- event.preventDefault();
4948
- void this.config.actions.bindNoteDraftToPoint(
4949
- toTargetPointFromHostEvent(event, this.config.getEnvironment())
4950
- );
4951
- });
4952
- 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);
4953
5503
  }
4954
- createElementLayer() {
4955
- const layer = document.createElement("div");
4956
- layer.className = "dfwr-element-layer";
4957
- const environment = this.config.getEnvironment();
4958
- const hover = document.createElement("div");
4959
- hover.className = "dfwr-dom-hover";
4960
- hover.hidden = true;
4961
- layer.append(hover);
4962
- if (environment) {
4963
- 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;
4964
5510
  }
4965
- const updateHover = (point) => {
4966
- const nextEnvironment = this.config.getEnvironment();
4967
- if (!nextEnvironment) return;
4968
- const anchor = getDomAnchorFromPoint(
4969
- clampPoint(point, nextEnvironment),
4970
- this.config.options.anchors?.attribute,
4971
- nextEnvironment
4972
- );
4973
- const selection = anchor ? getElementViewportSelection(anchor, nextEnvironment) : void 0;
4974
- if (!selection) {
4975
- hover.hidden = true;
4976
- return;
4977
- }
4978
- const rect = toHostSelection(selection, nextEnvironment);
4979
- hover.hidden = false;
4980
- hover.style.left = `${rect.left}px`;
4981
- hover.style.top = `${rect.top}px`;
4982
- hover.style.width = `${rect.width}px`;
4983
- hover.style.height = `${rect.height}px`;
4984
- };
4985
- layer.addEventListener("pointermove", (event) => {
4986
- updateHover(toTargetPointFromHostEvent(event, this.config.getEnvironment()));
4987
- });
4988
- layer.addEventListener("pointerleave", () => {
4989
- hover.hidden = true;
4990
- });
4991
- layer.addEventListener("pointerdown", (event) => {
4992
- if (event.button !== 0) return;
4993
- event.preventDefault();
4994
- void this.config.actions.bindElementDraftToPoint(
4995
- toTargetPointFromHostEvent(event, this.config.getEnvironment())
4996
- );
4997
- });
4998
- return layer;
5511
+ this.shellComposerHost = void 0;
4999
5512
  }
5000
- createAreaLayer() {
5513
+ /** Full-screen click-away layer that cancels the active draft. */
5514
+ createDraftCancelLayer() {
5001
5515
  const layer = document.createElement("div");
5002
- layer.className = "dfwr-area-layer";
5003
- const environment = this.config.getEnvironment();
5004
- if (environment) {
5005
- placeLayerOverTarget(layer, environment);
5006
- }
5007
- const box = document.createElement("div");
5008
- box.className = "dfwr-area-box";
5009
- layer.append(box);
5010
- let startX = 0;
5011
- let startY = 0;
5012
- let selection;
5013
- let activePointerId;
5014
- let isDragging = false;
5015
- const ownerWindow = layer.ownerDocument.defaultView ?? window;
5016
- const updateBox = (event) => {
5017
- const nextEnvironment = this.config.getEnvironment();
5018
- const nextPoint = toTargetPointFromHostEvent(
5019
- event,
5020
- nextEnvironment
5021
- );
5022
- const left = Math.min(startX, nextPoint.x);
5023
- const top = Math.min(startY, nextPoint.y);
5024
- const width = Math.abs(nextPoint.x - startX);
5025
- const height = Math.abs(nextPoint.y - startY);
5026
- const hostPoint = toHostPoint(
5027
- { x: left, y: top },
5028
- nextEnvironment
5029
- );
5030
- selection = { left, top, width, height };
5031
- box.style.left = `${hostPoint.x}px`;
5032
- box.style.top = `${hostPoint.y}px`;
5033
- box.style.width = `${width}px`;
5034
- box.style.height = `${height}px`;
5035
- };
5036
- const addDragListeners = () => {
5037
- ownerWindow.addEventListener("pointermove", handlePointerMove, true);
5038
- ownerWindow.addEventListener("pointerup", handlePointerUp, true);
5039
- ownerWindow.addEventListener("pointercancel", handlePointerCancel, true);
5040
- };
5041
- const removeDragListeners = () => {
5042
- ownerWindow.removeEventListener("pointermove", handlePointerMove, true);
5043
- ownerWindow.removeEventListener("pointerup", handlePointerUp, true);
5044
- ownerWindow.removeEventListener(
5045
- "pointercancel",
5046
- handlePointerCancel,
5047
- true
5048
- );
5049
- };
5050
- const releasePointerCapture = (event) => {
5051
- try {
5052
- if (layer.hasPointerCapture(event.pointerId)) {
5053
- layer.releasePointerCapture(event.pointerId);
5054
- }
5055
- } catch {
5056
- }
5057
- };
5058
- function isActivePointer(event) {
5059
- return isDragging && event.pointerId === activePointerId;
5060
- }
5061
- const finishAreaSelection = (event) => {
5062
- if (!isActivePointer(event)) return;
5063
- event.preventDefault();
5064
- updateBox(event);
5065
- releasePointerCapture(event);
5066
- removeDragListeners();
5067
- isDragging = false;
5068
- activePointerId = void 0;
5069
- if (!selection || selection.width < 8 || selection.height < 8) return;
5070
- this.config.actions.setSelectingArea(true);
5071
- this.config.actions.render();
5072
- void this.config.actions.createAreaDraft(selection);
5073
- };
5074
- function handlePointerMove(event) {
5075
- if (!isActivePointer(event)) return;
5076
- event.preventDefault();
5077
- updateBox(event);
5078
- }
5079
- const handlePointerUp = (event) => {
5080
- finishAreaSelection(event);
5081
- };
5082
- const handlePointerCancel = (event) => {
5083
- if (!isActivePointer(event)) return;
5084
- releasePointerCapture(event);
5085
- removeDragListeners();
5086
- isDragging = false;
5087
- activePointerId = void 0;
5088
- };
5516
+ layer.className = "dfwr-draft-cancel-layer";
5517
+ layer.setAttribute("aria-hidden", "true");
5089
5518
  layer.addEventListener("pointerdown", (event) => {
5090
5519
  if (event.button !== 0) return;
5091
- event.preventDefault();
5092
- activePointerId = event.pointerId;
5093
- isDragging = true;
5094
- try {
5095
- layer.setPointerCapture(event.pointerId);
5096
- } catch {
5097
- }
5098
- const startPoint = toTargetPointFromHostEvent(
5099
- event,
5100
- this.config.getEnvironment()
5101
- );
5102
- startX = startPoint.x;
5103
- startY = startPoint.y;
5104
- updateBox(event);
5105
- addDragListeners();
5520
+ this.cancelDraft(event);
5106
5521
  });
5107
- layer.addEventListener("pointermove", handlePointerMove);
5108
- layer.addEventListener("pointerup", handlePointerUp);
5109
- layer.addEventListener("pointercancel", handlePointerCancel);
5110
5522
  return layer;
5111
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
+ }
5112
5533
  };
5113
5534
 
5114
5535
  // src/core/web.review.kit.app.ts
@@ -5148,6 +5569,7 @@ var WebReviewKitApp = class {
5148
5569
  this.items = [];
5149
5570
  this.draftError = "";
5150
5571
  this.isCreatingItem = false;
5572
+ this.isCapturingViewport = false;
5151
5573
  this.isSelectingArea = false;
5152
5574
  this.handleKeyDown = (event) => {
5153
5575
  if (event.key === "Escape" && this.cancelMode()) {
@@ -5176,10 +5598,11 @@ var WebReviewKitApp = class {
5176
5598
  isOpen: this.isOpen,
5177
5599
  mode: this.mode,
5178
5600
  items: this.items,
5179
- noteDraft: this.noteDraft,
5601
+ domDraft: this.domDraft,
5180
5602
  areaDraft: this.areaDraft,
5181
5603
  draftError: this.draftError,
5182
5604
  isCreatingItem: this.isCreatingItem,
5605
+ isCapturingViewport: this.isCapturingViewport,
5183
5606
  isSelectingArea: this.isSelectingArea,
5184
5607
  highlightedItemId: this.highlightedItemId
5185
5608
  }),
@@ -5191,13 +5614,9 @@ var WebReviewKitApp = class {
5191
5614
  restoreItem: (item) => this.restoreItem(item),
5192
5615
  removeItem: (itemId) => this.adapter.remove(itemId),
5193
5616
  setModeState: (mode) => this.setModeState(mode),
5194
- clearDrafts: () => {
5195
- this.noteDraft = void 0;
5196
- this.areaDraft = void 0;
5197
- this.draftError = "";
5198
- },
5199
- setNoteDraft: (draft) => {
5200
- this.noteDraft = draft;
5617
+ clearDrafts: () => this.clearDrafts(),
5618
+ setDomDraft: (draft) => {
5619
+ this.domDraft = draft;
5201
5620
  this.draftError = "";
5202
5621
  },
5203
5622
  setAreaDraft: (draft) => {
@@ -5208,7 +5627,8 @@ var WebReviewKitApp = class {
5208
5627
  this.isSelectingArea = isSelectingArea;
5209
5628
  },
5210
5629
  createItem: (input) => this.createItem(input),
5211
- bindNoteDraftToPoint: (point, fields) => this.bindNoteDraftToPoint(point, fields),
5630
+ captureDomDraft: (input) => this.captureDomDraft(input),
5631
+ captureAreaDraft: (input) => this.captureAreaDraft(input),
5212
5632
  bindElementDraftToPoint: (point, fields) => this.bindElementDraftToPoint(point, fields),
5213
5633
  createAreaDraft: (selection) => this.createAreaDraft(selection)
5214
5634
  }
@@ -5230,6 +5650,7 @@ var WebReviewKitApp = class {
5230
5650
  }
5231
5651
  destroy() {
5232
5652
  this.view.clearDraftPreview();
5653
+ this.clearDrafts();
5233
5654
  document.removeEventListener("keydown", this.handleKeyDown, true);
5234
5655
  window.removeEventListener("scroll", this.handleViewportChange, true);
5235
5656
  window.removeEventListener("resize", this.handleViewportChange);
@@ -5249,8 +5670,7 @@ var WebReviewKitApp = class {
5249
5670
  close() {
5250
5671
  this.isOpen = false;
5251
5672
  this.setModeState("idle");
5252
- this.noteDraft = void 0;
5253
- this.areaDraft = void 0;
5673
+ this.clearDrafts();
5254
5674
  this.isSelectingArea = false;
5255
5675
  this.render();
5256
5676
  }
@@ -5266,8 +5686,7 @@ var WebReviewKitApp = class {
5266
5686
  this.isOpen = true;
5267
5687
  }
5268
5688
  this.setModeState(this.mode === mode ? "idle" : mode);
5269
- this.noteDraft = void 0;
5270
- this.areaDraft = void 0;
5689
+ this.clearDrafts();
5271
5690
  this.render();
5272
5691
  }
5273
5692
  async startElementReview(element, comment) {
@@ -5275,8 +5694,7 @@ var WebReviewKitApp = class {
5275
5694
  this.isOpen = true;
5276
5695
  }
5277
5696
  this.setModeState("element");
5278
- this.noteDraft = void 0;
5279
- this.areaDraft = void 0;
5697
+ this.clearDrafts();
5280
5698
  this.isSelectingArea = false;
5281
5699
  await this.bindElementDraftToElement(element, { comment });
5282
5700
  }
@@ -5301,6 +5719,20 @@ var WebReviewKitApp = class {
5301
5719
  this.hiddenItemIds = itemIds ? new Set(itemIds) : void 0;
5302
5720
  this.updateHiddenItemsStyle();
5303
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
+ }
5304
5736
  clearHighlightedItem() {
5305
5737
  if (!this.highlightedItemId) return;
5306
5738
  this.highlightedItemId = void 0;
@@ -5336,18 +5768,17 @@ var WebReviewKitApp = class {
5336
5768
  this.options.onModeChange?.(mode);
5337
5769
  }
5338
5770
  cancelMode() {
5339
- if (this.mode === "idle" && !this.noteDraft && !this.areaDraft && !this.isSelectingArea) {
5771
+ if (this.mode === "idle" && !this.domDraft && !this.areaDraft && !this.isSelectingArea) {
5340
5772
  return false;
5341
5773
  }
5342
5774
  this.setModeState("idle");
5343
- this.noteDraft = void 0;
5344
- this.areaDraft = void 0;
5775
+ this.clearDrafts();
5345
5776
  this.isSelectingArea = false;
5346
5777
  this.render();
5347
5778
  return true;
5348
5779
  }
5349
5780
  isDraftComposerFocused() {
5350
- if (!this.noteDraft && !this.areaDraft) return false;
5781
+ if (!this.domDraft && !this.areaDraft) return false;
5351
5782
  const composerHost = this.getEnvironment()?.composerHost;
5352
5783
  const activeElement = composerHost?.ownerDocument.activeElement;
5353
5784
  return Boolean(
@@ -5383,6 +5814,8 @@ var WebReviewKitApp = class {
5383
5814
  };
5384
5815
  const overlayRect = target.getOverlayRect?.() ?? rect;
5385
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;
5386
5819
  return {
5387
5820
  window: target.window,
5388
5821
  document: target.document,
@@ -5392,13 +5825,16 @@ var WebReviewKitApp = class {
5392
5825
  width: rect.width,
5393
5826
  height: rect.height
5394
5827
  },
5828
+ scaleX,
5829
+ scaleY,
5395
5830
  overlayRect: {
5396
5831
  left: overlayRect.left,
5397
5832
  top: overlayRect.top,
5398
5833
  width: overlayRect.width,
5399
5834
  height: overlayRect.height
5400
5835
  },
5401
- composerHost
5836
+ composerHost,
5837
+ captureViewport: target.captureViewport
5402
5838
  };
5403
5839
  } catch {
5404
5840
  return void 0;
@@ -5421,32 +5857,6 @@ var WebReviewKitApp = class {
5421
5857
  if (!this.shadow) return;
5422
5858
  this.view.render(this.shadow, this.createHiddenItemsStyleElement());
5423
5859
  }
5424
- async bindNoteDraftToPoint(point, fields = {}) {
5425
- const environment = this.getEnvironment();
5426
- if (!environment) return;
5427
- const viewport = getViewportSize(environment);
5428
- const nextPoint = clampPoint(point, environment);
5429
- const draft = await this.withOverlayHidden(() => {
5430
- const selection = getPointSelection(nextPoint);
5431
- const anchor = getDomAnchor(
5432
- selection,
5433
- this.options.anchors?.attribute,
5434
- environment
5435
- );
5436
- const marker = {
5437
- viewport: roundPoint(nextPoint),
5438
- relative: anchor ? getRelativePoint(nextPoint, anchor, environment) : void 0
5439
- };
5440
- return {
5441
- viewport,
5442
- anchor,
5443
- marker,
5444
- ...fields
5445
- };
5446
- });
5447
- this.noteDraft = draft;
5448
- this.render();
5449
- }
5450
5860
  async bindElementDraftToPoint(point, fields = {}) {
5451
5861
  const environment = this.getEnvironment();
5452
5862
  if (!environment) return;
@@ -5495,7 +5905,7 @@ var WebReviewKitApp = class {
5495
5905
  previewElement
5496
5906
  };
5497
5907
  });
5498
- this.noteDraft = draft;
5908
+ this.domDraft = draft;
5499
5909
  this.render();
5500
5910
  }
5501
5911
  async bindElementDraftToElement(element, fields = {}) {
@@ -5536,7 +5946,7 @@ var WebReviewKitApp = class {
5536
5946
  };
5537
5947
  });
5538
5948
  if (!draft) return;
5539
- this.noteDraft = draft;
5949
+ this.domDraft = draft;
5540
5950
  this.render();
5541
5951
  }
5542
5952
  async createAreaDraft(selection) {
@@ -5549,16 +5959,27 @@ var WebReviewKitApp = class {
5549
5959
  try {
5550
5960
  const viewport = getViewportSize(environment);
5551
5961
  this.areaDraft = await this.withOverlayHidden(() => {
5552
- 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(
5553
5972
  selection,
5554
- void 0,
5973
+ anchor,
5555
5974
  environment
5556
5975
  );
5557
5976
  const reviewSelection = {
5558
- viewport: toPublicSelection(selection)
5977
+ viewport: toPublicSelection(selection),
5978
+ relative: anchor ? getRelativeSelection(selection, anchor, environment) : void 0
5559
5979
  };
5560
5980
  return {
5561
5981
  viewport,
5982
+ anchor,
5562
5983
  marker,
5563
5984
  selection: reviewSelection
5564
5985
  };
@@ -5579,6 +6000,127 @@ var WebReviewKitApp = class {
5579
6000
  this.root.style.display = previousDisplay;
5580
6001
  }
5581
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
+ }
5582
6124
  async createItem(input) {
5583
6125
  const environment = this.getEnvironment();
5584
6126
  if (!environment || this.isCreatingItem) return;
@@ -5622,24 +6164,61 @@ var WebReviewKitApp = class {
5622
6164
  this.isCreatingItem = true;
5623
6165
  this.render();
5624
6166
  try {
5625
- 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);
5626
6173
  this.setModeState("idle");
5627
- this.noteDraft = void 0;
5628
- this.areaDraft = void 0;
6174
+ this.clearDrafts();
5629
6175
  this.highlightItem(createdItem.id);
5630
6176
  await this.reload();
5631
6177
  await this.options.onCreateItem?.(createdItem);
5632
6178
  } catch (error) {
5633
- this.draftError = error instanceof Error ? error.message : "Failed to save QA.";
6179
+ this.draftError = this.getCreateItemErrorMessage(
6180
+ error,
6181
+ Boolean(input.attachments?.length)
6182
+ );
5634
6183
  } finally {
5635
6184
  this.isCreatingItem = false;
5636
6185
  this.render();
5637
6186
  }
5638
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
+ }
5639
6219
  async restoreItem(item) {
5640
6220
  this.setModeState("idle");
5641
- this.noteDraft = void 0;
5642
- this.areaDraft = void 0;
6221
+ this.clearDrafts();
5643
6222
  if (this.options.onRestoreItem) {
5644
6223
  await this.options.onRestoreItem(item);
5645
6224
  return;
@@ -5698,6 +6277,7 @@ function createNoopController() {
5698
6277
  REVIEW_WORKFLOW_STATUS_OPTIONS,
5699
6278
  ReviewFigmaTokenError,
5700
6279
  collectReviewFigmaReleaseSnapshot,
6280
+ createEndpointReviewFigmaImageStore,
5701
6281
  createRemoteReviewFigmaImageStore,
5702
6282
  createReviewFigmaFrameUrl,
5703
6283
  createReviewFigmaImageStoreClient,