@designfever/web-review-kit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,12 +92,15 @@ Browser env must use a Supabase `anon` key only. Do not put `service_role` or Op
92
92
  ```ts
93
93
  import { createWebReviewKit, localAdapter } from '@designfever/web-review-kit';
94
94
  import { mountReviewShell } from '@designfever/web-review-kit/react-shell';
95
- import { reviewSourceLocator } from '@designfever/web-review-kit/vite';
95
+ import {
96
+ reviewDataLocator,
97
+ reviewSourceLocator,
98
+ } from '@designfever/web-review-kit/vite';
96
99
  ```
97
100
 
98
101
  - `@designfever/web-review-kit`: core API, adapters, shared types.
99
102
  - `@designfever/web-review-kit/react-shell`: review shell UI, presence adapters, page glob helper.
100
- - `@designfever/web-review-kit/vite`: dev-only JSX source locator for source file hints.
103
+ - `@designfever/web-review-kit/vite`: dev-only source/data locators for review hints.
101
104
  - `src/*` is not a public import path.
102
105
 
103
106
  ## Optional Source Locator
@@ -106,7 +109,10 @@ For local QA, add the Vite plugin to inject source hints into rendered DOM nodes
106
109
 
107
110
  ```ts
108
111
  import { defineConfig } from 'vite';
109
- import { reviewSourceLocator } from '@designfever/web-review-kit/vite';
112
+ import {
113
+ reviewDataLocator,
114
+ reviewSourceLocator,
115
+ } from '@designfever/web-review-kit/vite';
110
116
 
111
117
  export default defineConfig({
112
118
  plugins: [
@@ -115,11 +121,16 @@ export default defineConfig({
115
121
  include: ['src'],
116
122
  filePath: 'absolute',
117
123
  }),
124
+ reviewDataLocator({
125
+ enabled: true,
126
+ include: ['src/data'],
127
+ filePath: 'absolute',
128
+ }),
118
129
  ],
119
130
  });
120
131
  ```
121
132
 
122
- When source hints are available, hold `Option` over the review target to inspect source candidates from the DOM ancestry. Click the target to pin the candidate list, then choose a file to open. DOM QA cards also show a source action when the saved item has source hints. Keep this plugin disabled for production builds because it writes source paths into the DOM.
133
+ When source hints are available, hold `Option` over the review target to inspect source candidates from the DOM ancestry. Click the target to pin the candidate list, then choose a file to open. The side rail can also open a Source Tree panel with section/source/data links. DOM QA cards show a source action when the saved item has source hints. Keep these plugins disabled for production builds because they write source paths into the DOM.
123
134
 
124
135
  ```tsx
125
136
  mountReviewShell({
@@ -129,6 +140,10 @@ mountReviewShell({
129
140
  sourceRoot: import.meta.env.VITE_REVIEW_SOURCE_ROOT,
130
141
  sourceInspector: {
131
142
  editor: 'cursor', // 'vscode' | 'cursor' | 'webstorm' | 'custom'
143
+ maxDepth: 9,
144
+ hoverOutline: true,
145
+ includePlacer: false,
146
+ ignore: ['core.section', 'control.render'],
132
147
  // urlTemplate: 'my-editor://open?file={encodedPath}&line={line}&column={column}',
133
148
  },
134
149
  });
@@ -400,8 +400,22 @@ function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", enviro
400
400
  source: getDomSourceHint(target)
401
401
  };
402
402
  }
403
- function getElementViewportSelection(anchor, environment) {
404
- const element = getAnchorElement(anchor, environment);
403
+ function getDomAnchorFromElement(target, configuredAttribute = "data-qa-id", environment) {
404
+ if (target.ownerDocument !== environment.document) return void 0;
405
+ const candidates = createAnchorCandidates(target, configuredAttribute);
406
+ const primary = candidates[0];
407
+ if (!primary) return void 0;
408
+ return {
409
+ ...primary,
410
+ candidates,
411
+ htmlSnippet: getElementHtmlSnippet(
412
+ getAnchorSourceElement(target, primary, configuredAttribute) ?? target
413
+ ),
414
+ source: getDomSourceHint(target)
415
+ };
416
+ }
417
+ function getElementViewportSelection(anchor, environment, preferredSelection) {
418
+ const element = getAnchorElement(anchor, environment, preferredSelection);
405
419
  if (!element) return void 0;
406
420
  const rect = element.getBoundingClientRect();
407
421
  if (rect.width <= 0 || rect.height <= 0) return void 0;
@@ -440,22 +454,35 @@ function getAnchorCandidates(anchor) {
440
454
  ...anchor.candidates ?? []
441
455
  ]);
442
456
  }
443
- function resolveAnchorElement(anchor, environment) {
457
+ function resolveAnchorElement(anchor, environment, preferredSelection) {
444
458
  const matches = getAnchorCandidates(anchor).flatMap((candidate) => {
445
- const match = queryBestAnchorCandidate(
446
- candidate,
447
- candidate.textFingerprint ?? anchor.textFingerprint,
448
- environment
449
- );
450
- if (!match) return [];
451
- const confidence = roundRatio(
452
- (candidate.confidence ?? 0.5) * match.score
453
- );
454
- return [{
455
- element: match.element,
456
- candidate,
457
- confidence
458
- }];
459
+ const textFingerprint = candidate.textFingerprint ?? anchor.textFingerprint;
460
+ if (!preferredSelection) {
461
+ const match = queryBestAnchorCandidate(
462
+ candidate,
463
+ textFingerprint,
464
+ environment
465
+ );
466
+ if (!match) return [];
467
+ const confidence = roundRatio(
468
+ (candidate.confidence ?? 0.5) * match.score
469
+ );
470
+ return [{
471
+ element: match.element,
472
+ candidate,
473
+ confidence
474
+ }];
475
+ }
476
+ return queryAnchorElements(candidate.selector, environment).map((element) => {
477
+ const confidence = roundRatio(
478
+ (candidate.confidence ?? 0.5) * getTextFingerprintScore(textFingerprint, getTextFingerprint(element)) * getSelectionMatchScore(element, preferredSelection)
479
+ );
480
+ return {
481
+ element,
482
+ candidate,
483
+ confidence
484
+ };
485
+ });
459
486
  });
460
487
  return matches.sort((a, b) => b.confidence - a.confidence)[0];
461
488
  }
@@ -465,8 +492,8 @@ function cssEscape(value) {
465
492
  }
466
493
  return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
467
494
  }
468
- function getAnchorElement(anchor, environment) {
469
- return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment)?.element;
495
+ function getAnchorElement(anchor, environment, preferredSelection) {
496
+ return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment, preferredSelection)?.element;
470
497
  }
471
498
  function createAnchorCandidates(target, configuredAttribute) {
472
499
  const targetCandidates = [];
@@ -830,6 +857,38 @@ function getTextFingerprintScore(expected, actual) {
830
857
  const matches = expectedTokens.filter((token) => actualTokens.has(token));
831
858
  return clamp(matches.length / expectedTokens.length, 0.25, 0.76);
832
859
  }
860
+ function getSelectionMatchScore(element, selection) {
861
+ const rect = element.getBoundingClientRect();
862
+ if (rect.width <= 0 || rect.height <= 0) return 0.05;
863
+ const overlapLeft = Math.max(rect.left, selection.left);
864
+ const overlapTop = Math.max(rect.top, selection.top);
865
+ const overlapRight = Math.min(rect.right, selection.left + selection.width);
866
+ const overlapBottom = Math.min(rect.bottom, selection.top + selection.height);
867
+ const overlapWidth = Math.max(0, overlapRight - overlapLeft);
868
+ const overlapHeight = Math.max(0, overlapBottom - overlapTop);
869
+ const overlapArea = overlapWidth * overlapHeight;
870
+ if (overlapArea > 0) {
871
+ const selectionArea = Math.max(1, selection.width * selection.height);
872
+ const rectArea = Math.max(1, rect.width * rect.height);
873
+ return 1 + overlapArea / Math.min(selectionArea, rectArea);
874
+ }
875
+ const rectCenterX = rect.left + rect.width / 2;
876
+ const rectCenterY = rect.top + rect.height / 2;
877
+ const selectionCenterX = selection.left + selection.width / 2;
878
+ const selectionCenterY = selection.top + selection.height / 2;
879
+ const distance = Math.hypot(
880
+ rectCenterX - selectionCenterX,
881
+ rectCenterY - selectionCenterY
882
+ );
883
+ const basis = Math.max(
884
+ 1,
885
+ rect.width,
886
+ rect.height,
887
+ selection.width,
888
+ selection.height
889
+ );
890
+ return clamp(1 / (1 + distance / basis), 0.05, 0.95);
891
+ }
833
892
  function getFingerprintTokens(value) {
834
893
  return value.toLowerCase().split(/[\s/|,.:;()[\]{}"'`~!?<>]+/).map((token) => token.trim()).filter((token) => token.length > 1);
835
894
  }
@@ -1791,6 +1850,12 @@ function createStyleElement() {
1791
1850
  outline-offset: 1px;
1792
1851
  }
1793
1852
 
1853
+ @media (hover: none) and (pointer: coarse) {
1854
+ .dfwr-textarea {
1855
+ font-size: 16px;
1856
+ }
1857
+ }
1858
+
1794
1859
  .dfwr-adjust-panel {
1795
1860
  display: grid;
1796
1861
  gap: 4px;
@@ -1864,32 +1929,6 @@ function createStyleElement() {
1864
1929
  pointer-events: none;
1865
1930
  }
1866
1931
 
1867
- .dfwr-adjust-hud {
1868
- position: fixed;
1869
- z-index: 5;
1870
- display: inline-flex;
1871
- align-items: center;
1872
- min-height: 22px;
1873
- padding: 0 8px;
1874
- border: 1px solid rgba(99, 215, 199, 0.72);
1875
- border-radius: var(--df-review-radius-sm);
1876
- background: rgba(21, 25, 29, 0.92);
1877
- box-shadow:
1878
- 0 0 0 3px rgba(99, 215, 199, 0.14),
1879
- 0 8px 18px rgba(0, 0, 0, 0.26);
1880
- color: #63d7c7;
1881
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1882
- font-size: var(--df-review-font-size-2xs);
1883
- font-weight: 800;
1884
- line-height: 1;
1885
- pointer-events: none;
1886
- white-space: nowrap;
1887
- }
1888
-
1889
- .dfwr-adjust-hud[hidden] {
1890
- display: none;
1891
- }
1892
-
1893
1932
  .dfwr-empty,
1894
1933
  .dfwr-error {
1895
1934
  margin: 0;
@@ -2252,7 +2291,6 @@ var WebReviewKitView = class {
2252
2291
  if (event.button !== 0) return;
2253
2292
  cancel(event);
2254
2293
  });
2255
- layer.addEventListener("click", cancel);
2256
2294
  return layer;
2257
2295
  }
2258
2296
  getDraftAdjustmentMetrics(draft) {
@@ -2432,14 +2470,6 @@ var WebReviewKitView = class {
2432
2470
  if (!draft.selection) return void 0;
2433
2471
  return toViewportSelection(draft.selection.viewport);
2434
2472
  }
2435
- formatDraftAdjustmentStatus(draft) {
2436
- const metrics = this.getDraftAdjustmentMetrics(draft);
2437
- return [
2438
- `x ${this.formatSignedPx(metrics.x)}`,
2439
- `y ${this.formatSignedPx(metrics.y)}`,
2440
- `scale ${this.formatSignedPx(metrics.scale)}`
2441
- ].join(" / ");
2442
- }
2443
2473
  getDraftAdjustmentMetricLines(draft) {
2444
2474
  const metrics = this.getDraftAdjustmentMetrics(draft);
2445
2475
  return [
@@ -2451,6 +2481,7 @@ var WebReviewKitView = class {
2451
2481
  }
2452
2482
  withDraftAdjustmentComment(comment, draft) {
2453
2483
  if (!this.hasDraftAdjustment(draft)) return comment;
2484
+ const trimmedComment = comment.trim();
2454
2485
  const metrics = this.getDraftAdjustmentMetrics(draft);
2455
2486
  const adjustment = [
2456
2487
  `${this.getAdjustmentLabel()}: x ${this.formatSignedPx(
@@ -2462,12 +2493,20 @@ var WebReviewKitView = class {
2462
2493
  metrics.viewportWidth
2463
2494
  )}/design ${Math.round(metrics.designWidth)})`
2464
2495
  ].join(" ");
2465
- return `${comment.trim()}
2466
- ${adjustment}`;
2496
+ return trimmedComment ? `${trimmedComment}
2497
+ ${adjustment}` : adjustment;
2467
2498
  }
2468
2499
  getStyleableDraftElement(draft, environment) {
2500
+ if (draft.previewElement && draft.previewElement.ownerDocument === environment.document && "style" in draft.previewElement) {
2501
+ return draft.previewElement;
2502
+ }
2469
2503
  if (!draft.anchor) return void 0;
2470
- const element = resolveAnchorElement(draft.anchor, environment)?.element;
2504
+ const preferredSelection = draft.selection ? toViewportSelection(draft.selection.viewport) : void 0;
2505
+ const element = resolveAnchorElement(
2506
+ draft.anchor,
2507
+ environment,
2508
+ preferredSelection
2509
+ )?.element;
2471
2510
  if (!element) return void 0;
2472
2511
  if ("style" in element) return element;
2473
2512
  return void 0;
@@ -2487,39 +2526,77 @@ ${adjustment}`;
2487
2526
  this.restoreDraftPreview();
2488
2527
  }
2489
2528
  if (!this.draftPreview) {
2490
- const computedTransform = environment.window.getComputedStyle(element).transform;
2529
+ const computedStyle = environment.window.getComputedStyle(element);
2530
+ const clone = element.cloneNode(true);
2531
+ this.removeDuplicateIds(clone);
2532
+ this.copyComputedStyle(element, clone, environment);
2533
+ this.positionDraftPreviewClone(clone, element, computedStyle);
2534
+ environment.document.body?.appendChild(clone);
2491
2535
  this.draftPreview = {
2492
2536
  element,
2493
- transform: element.style.transform,
2494
- transformOrigin: element.style.transformOrigin,
2495
- transition: element.style.transition,
2496
- willChange: element.style.willChange,
2497
- baseTransform: element.style.transform || (computedTransform && computedTransform !== "none" ? computedTransform : "")
2537
+ clone,
2538
+ visibility: element.style.visibility
2498
2539
  };
2540
+ element.style.visibility = "hidden";
2499
2541
  }
2500
2542
  const metrics = this.getDraftAdjustmentMetrics(draft);
2501
2543
  const translate = `translate(${this.toCssNumber(metrics.cssX)}px, ${this.toCssNumber(
2502
2544
  metrics.cssY
2503
2545
  )}px)`;
2504
2546
  const scale = metrics.scaleFactor === 1 ? "" : `scale(${this.toCssNumber(metrics.scaleFactor)})`;
2505
- element.style.transition = "none";
2506
- element.style.willChange = "transform";
2507
- element.style.transformOrigin = "top left";
2508
- element.style.transform = [
2509
- this.draftPreview.baseTransform,
2510
- translate,
2511
- scale
2512
- ].filter(Boolean).join(" ");
2547
+ this.draftPreview.clone.style.transform = [translate, scale].filter(Boolean).join(" ");
2513
2548
  }
2514
2549
  restoreDraftPreview() {
2515
2550
  if (!this.draftPreview) return;
2516
- const { element, transform, transformOrigin, transition, willChange } = this.draftPreview;
2517
- element.style.transform = transform;
2518
- element.style.transformOrigin = transformOrigin;
2519
- element.style.transition = transition;
2520
- element.style.willChange = willChange;
2551
+ const { element, clone, visibility } = this.draftPreview;
2552
+ clone.remove();
2553
+ element.style.visibility = visibility;
2521
2554
  this.draftPreview = void 0;
2522
2555
  }
2556
+ positionDraftPreviewClone(clone, element, computedStyle) {
2557
+ const rect = element.getBoundingClientRect();
2558
+ clone.setAttribute("data-dfwr-adjust-preview", "true");
2559
+ clone.setAttribute("aria-hidden", "true");
2560
+ clone.style.position = "fixed";
2561
+ clone.style.left = `${this.toCssNumber(rect.left)}px`;
2562
+ clone.style.top = `${this.toCssNumber(rect.top)}px`;
2563
+ clone.style.right = "auto";
2564
+ clone.style.bottom = "auto";
2565
+ clone.style.width = `${this.toCssNumber(rect.width)}px`;
2566
+ clone.style.height = `${this.toCssNumber(rect.height)}px`;
2567
+ clone.style.maxWidth = "none";
2568
+ clone.style.maxHeight = "none";
2569
+ clone.style.margin = "0";
2570
+ clone.style.boxSizing = "border-box";
2571
+ clone.style.display = this.getDraftPreviewDisplay(computedStyle.display);
2572
+ clone.style.zIndex = "2147483646";
2573
+ clone.style.pointerEvents = "none";
2574
+ clone.style.transition = "none";
2575
+ clone.style.willChange = "transform";
2576
+ clone.style.transformOrigin = "top left";
2577
+ clone.style.transform = "none";
2578
+ }
2579
+ getDraftPreviewDisplay(display) {
2580
+ if (display === "inline" || display === "contents") return "inline-block";
2581
+ return display || "block";
2582
+ }
2583
+ copyComputedStyle(element, clone, environment) {
2584
+ const computedStyle = environment.window.getComputedStyle(element);
2585
+ for (let index = 0; index < computedStyle.length; index += 1) {
2586
+ const property = computedStyle.item(index);
2587
+ clone.style.setProperty(
2588
+ property,
2589
+ computedStyle.getPropertyValue(property),
2590
+ computedStyle.getPropertyPriority(property)
2591
+ );
2592
+ }
2593
+ }
2594
+ removeDuplicateIds(element) {
2595
+ element.removeAttribute("id");
2596
+ element.querySelectorAll("[id]").forEach((child) => {
2597
+ child.removeAttribute("id");
2598
+ });
2599
+ }
2523
2600
  toCssNumber(value) {
2524
2601
  return Math.round(value * 1e3) / 1e3;
2525
2602
  }
@@ -2681,8 +2758,8 @@ ${adjustment}`;
2681
2758
  });
2682
2759
  const saveDraft = () => {
2683
2760
  const comment = textarea.value.trim();
2684
- if (!comment) return;
2685
2761
  const currentDraft = this.state.noteDraft ?? draft;
2762
+ if (!comment && !this.hasDraftAdjustment(currentDraft)) return;
2686
2763
  void this.config.actions.createItem({
2687
2764
  kind: "note",
2688
2765
  comment: this.withDraftAdjustmentComment(comment, currentDraft),
@@ -2692,13 +2769,8 @@ ${adjustment}`;
2692
2769
  selection: currentDraft.selection
2693
2770
  });
2694
2771
  };
2695
- const adjustmentHud = isElementDraft ? this.createAdjustmentHud(draft, environment) : void 0;
2696
- if (adjustmentHud) {
2697
- group.append(adjustmentHud);
2698
- }
2699
2772
  const adjustmentControls = isElementDraft ? this.createAdjustmentControls({
2700
2773
  draft,
2701
- hud: adjustmentHud,
2702
2774
  pin,
2703
2775
  popover,
2704
2776
  selectionHighlight,
@@ -2826,7 +2898,6 @@ ${adjustment}`;
2826
2898
  }
2827
2899
  createAdjustmentControls({
2828
2900
  draft,
2829
- hud,
2830
2901
  pin,
2831
2902
  popover,
2832
2903
  selectionHighlight,
@@ -2864,7 +2935,6 @@ ${adjustment}`;
2864
2935
  scaleStatus.textContent = scaleLine;
2865
2936
  this.syncDraftAdjustmentUi({
2866
2937
  draft: nextDraft,
2867
- hud,
2868
2938
  pin,
2869
2939
  selectionHighlight
2870
2940
  });
@@ -2925,16 +2995,8 @@ ${adjustment}`;
2925
2995
  if (event.key.toLowerCase() === "s") return { x: 0, y: 0, scale: -step };
2926
2996
  return void 0;
2927
2997
  }
2928
- createAdjustmentHud(draft, environment) {
2929
- const hud = document.createElement("div");
2930
- hud.className = "dfwr-adjust-hud";
2931
- hud.setAttribute("aria-hidden", "true");
2932
- this.syncAdjustmentHud(hud, draft, environment);
2933
- return hud;
2934
- }
2935
2998
  syncDraftAdjustmentUi({
2936
2999
  draft,
2937
- hud,
2938
3000
  pin,
2939
3001
  selectionHighlight
2940
3002
  }) {
@@ -2959,26 +3021,8 @@ ${adjustment}`;
2959
3021
  selectionHighlight.style.width = `${rect.width}px`;
2960
3022
  selectionHighlight.style.height = `${rect.height}px`;
2961
3023
  }
2962
- if (hud) {
2963
- this.syncAdjustmentHud(hud, draft, environment);
2964
- }
2965
3024
  this.syncDraftPreview(draft);
2966
3025
  }
2967
- syncAdjustmentHud(hud, draft, environment) {
2968
- if (!draft.selection) return;
2969
- const rect = toHostSelection(
2970
- this.getAdjustedDraftSelection(
2971
- toViewportSelection(draft.selection.viewport),
2972
- draft
2973
- ),
2974
- environment
2975
- );
2976
- const isVisible = draft.adjustment?.isActive === true || this.hasDraftAdjustment(draft);
2977
- hud.hidden = !isVisible;
2978
- hud.textContent = this.formatDraftAdjustmentStatus(draft);
2979
- hud.style.left = `${Math.max(4, rect.left)}px`;
2980
- hud.style.top = `${Math.max(4, rect.top - 28)}px`;
2981
- }
2982
3026
  createAreaForm() {
2983
3027
  const form = document.createElement("form");
2984
3028
  form.className = "dfwr-form";
@@ -3102,12 +3146,18 @@ ${adjustment}`;
3102
3146
  save.className = "dfwr-button is-primary";
3103
3147
  save.type = "button";
3104
3148
  save.textContent = saveLabel;
3105
- save.addEventListener("click", onSave);
3149
+ save.addEventListener("click", (event) => {
3150
+ event.preventDefault();
3151
+ event.stopPropagation();
3152
+ onSave();
3153
+ });
3106
3154
  const cancel = document.createElement("button");
3107
3155
  cancel.className = "dfwr-button";
3108
3156
  cancel.type = "button";
3109
3157
  cancel.textContent = "Cancel";
3110
- cancel.addEventListener("click", () => {
3158
+ cancel.addEventListener("click", (event) => {
3159
+ event.preventDefault();
3160
+ event.stopPropagation();
3111
3161
  this.config.actions.setModeState("idle");
3112
3162
  this.config.actions.clearDrafts();
3113
3163
  this.config.actions.render();
@@ -3512,6 +3562,7 @@ function createWebReviewKit(options) {
3512
3562
  close: () => app.close(),
3513
3563
  toggle: () => app.toggle(),
3514
3564
  setMode: (mode) => app.setMode(mode),
3565
+ startElementReview: (element, comment) => app.startElementReview(element, comment),
3515
3566
  getMode: () => app.getMode(),
3516
3567
  highlightItem: (itemId) => app.highlightItem(itemId),
3517
3568
  setHiddenItemIds: (itemIds) => app.setHiddenItemIds(itemIds),
@@ -3642,6 +3693,16 @@ var WebReviewKitApp = class {
3642
3693
  this.areaDraft = void 0;
3643
3694
  this.render();
3644
3695
  }
3696
+ async startElementReview(element, comment) {
3697
+ if (!this.isOpen) {
3698
+ this.isOpen = true;
3699
+ }
3700
+ this.setModeState("element");
3701
+ this.noteDraft = void 0;
3702
+ this.areaDraft = void 0;
3703
+ this.isSelectingArea = false;
3704
+ await this.bindElementDraftToElement(element, comment);
3705
+ }
3645
3706
  getMode() {
3646
3707
  return this.mode;
3647
3708
  }
@@ -3805,13 +3866,26 @@ var WebReviewKitApp = class {
3805
3866
  const viewport = getViewportSize(environment);
3806
3867
  const nextPoint = clampPoint(point, environment);
3807
3868
  const draft = await this.withOverlayHidden(() => {
3869
+ const pointSelection = getPointSelection(nextPoint);
3870
+ const targetElement = environment.document.elementFromPoint(
3871
+ nextPoint.x,
3872
+ nextPoint.y
3873
+ );
3874
+ const previewElement = targetElement && "style" in targetElement ? targetElement : void 0;
3875
+ const targetRect = targetElement?.getBoundingClientRect();
3876
+ const clickedSelection = targetRect && targetRect.width > 0 && targetRect.height > 0 ? {
3877
+ left: targetRect.left,
3878
+ top: targetRect.top,
3879
+ width: targetRect.width,
3880
+ height: targetRect.height
3881
+ } : void 0;
3808
3882
  const anchor = getDomAnchorFromPoint(
3809
3883
  nextPoint,
3810
3884
  this.options.anchors?.attribute,
3811
3885
  environment
3812
3886
  );
3813
- const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
3814
- const selection = elementSelection ?? getPointSelection(nextPoint);
3887
+ const elementSelection = anchor ? clickedSelection ?? getElementViewportSelection(anchor, environment, pointSelection) : void 0;
3888
+ const selection = elementSelection ?? pointSelection;
3815
3889
  const markerPoint = elementSelection ? { x: selection.left, y: selection.top } : getSelectionCenter(selection);
3816
3890
  const reviewSelection = elementSelection ? {
3817
3891
  viewport: toPublicSelection(elementSelection),
@@ -3830,9 +3904,51 @@ var WebReviewKitApp = class {
3830
3904
  anchor,
3831
3905
  marker,
3832
3906
  selection: reviewSelection,
3833
- comment
3907
+ comment,
3908
+ previewElement
3909
+ };
3910
+ });
3911
+ this.noteDraft = draft;
3912
+ this.render();
3913
+ }
3914
+ async bindElementDraftToElement(element, comment) {
3915
+ const environment = this.getEnvironment();
3916
+ if (!environment || element.ownerDocument !== environment.document) return;
3917
+ const viewport = getViewportSize(environment);
3918
+ const draft = await this.withOverlayHidden(() => {
3919
+ const rect = element.getBoundingClientRect();
3920
+ if (rect.width <= 0 || rect.height <= 0) return void 0;
3921
+ const selection = {
3922
+ left: rect.left,
3923
+ top: rect.top,
3924
+ width: rect.width,
3925
+ height: rect.height
3926
+ };
3927
+ const anchor = getDomAnchorFromElement(
3928
+ element,
3929
+ this.options.anchors?.attribute,
3930
+ environment
3931
+ );
3932
+ const markerPoint = { x: selection.left, y: selection.top };
3933
+ const marker = {
3934
+ viewport: roundPoint(markerPoint),
3935
+ relative: anchor ? getRelativePoint(markerPoint, anchor, environment) : void 0
3936
+ };
3937
+ const reviewSelection = {
3938
+ viewport: toPublicSelection(selection),
3939
+ relative: anchor ? getRelativeSelection(selection, anchor, environment) : void 0
3940
+ };
3941
+ const previewElement = "style" in element ? element : void 0;
3942
+ return {
3943
+ viewport,
3944
+ anchor,
3945
+ marker,
3946
+ selection: reviewSelection,
3947
+ comment,
3948
+ previewElement
3834
3949
  };
3835
3950
  });
3951
+ if (!draft) return;
3836
3952
  this.noteDraft = draft;
3837
3953
  this.render();
3838
3954
  }
@@ -3940,6 +4056,8 @@ function createNoopController() {
3940
4056
  },
3941
4057
  setMode() {
3942
4058
  },
4059
+ async startElementReview() {
4060
+ },
3943
4061
  getMode() {
3944
4062
  return "idle";
3945
4063
  },
@@ -3962,12 +4080,14 @@ export {
3962
4080
  REVIEW_WORKFLOW_STATUS_OPTIONS,
3963
4081
  normalizeReviewItemStatus,
3964
4082
  localAdapter,
4083
+ clamp,
3965
4084
  DEFAULT_REVIEW_VIEWPORTS,
3966
4085
  findReviewViewportPreset,
3967
4086
  getReviewViewportScope,
3968
4087
  getReviewItemScope,
3969
4088
  getReviewItemScopeLabel,
3970
4089
  getNumberedReviewItems,
4090
+ runWithAutoScrollBehavior,
3971
4091
  createWebReviewKit
3972
4092
  };
3973
- //# sourceMappingURL=chunk-QFNYQCTA.js.map
4093
+ //# sourceMappingURL=chunk-TWCSIBMY.js.map