@sirendesign/markup 1.0.32 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8605,62 +8605,122 @@ const getElementInfo = (element) => {
8605
8605
  info += '>';
8606
8606
  return info;
8607
8607
  };
8608
+ /**
8609
+ * Generates a stable CSS selector for a DOM element, preferring IDs and
8610
+ * falling back to a tag + nth-of-type path anchored to the nearest ID ancestor.
8611
+ */
8612
+ const getElementSelector$1 = (element) => {
8613
+ // If the element itself has an id, that's unique enough.
8614
+ if (element.id) {
8615
+ return `#${element.id}`;
8616
+ }
8617
+ const parts = [];
8618
+ let el = element;
8619
+ while (el && el !== document.documentElement && el !== document.body) {
8620
+ let part = el.tagName.toLowerCase();
8621
+ if (el.id) {
8622
+ // Anchor the path here – an id is unique.
8623
+ parts.unshift(`#${el.id}`);
8624
+ break;
8625
+ }
8626
+ // Add nth-of-type disambiguator if there are siblings of the same tag.
8627
+ const parent = el.parentElement;
8628
+ if (parent) {
8629
+ const sameTagSiblings = Array.from(parent.children).filter((c) => c.tagName === el.tagName);
8630
+ if (sameTagSiblings.length > 1) {
8631
+ const index = sameTagSiblings.indexOf(el) + 1;
8632
+ part += `:nth-of-type(${index})`;
8633
+ }
8634
+ }
8635
+ parts.unshift(part);
8636
+ el = el.parentElement;
8637
+ }
8638
+ return parts.join(' > ') || element.tagName.toLowerCase();
8639
+ };
8608
8640
 
8609
8641
  async function captureScreenshot(clickElement) {
8610
8642
  try {
8611
- // Hide the markup widget before capturing
8643
+ // Hide the markup widget, pins overlay and viewport controls before capturing
8612
8644
  const widget = document.querySelector('[data-markup-widget]');
8613
- if (widget) {
8645
+ const pinsOverlay = document.querySelector('[data-markup-pins]');
8646
+ const viewportBar = document.querySelector('[data-markup-viewport]');
8647
+ if (widget)
8614
8648
  widget.style.visibility = 'hidden';
8615
- }
8649
+ if (pinsOverlay)
8650
+ pinsOverlay.style.visibility = 'hidden';
8651
+ if (viewportBar)
8652
+ viewportBar.style.visibility = 'hidden';
8653
+ // Use the constrained clientWidth so viewport-mode captures render at the
8654
+ // correct mobile/tablet width rather than the real browser window width.
8655
+ const captureWidth = document.documentElement.clientWidth || window.innerWidth;
8656
+ const captureHeight = document.documentElement.clientHeight || window.innerHeight;
8616
8657
  const canvas = await html2canvas(document.body, {
8617
8658
  allowTaint: true,
8618
8659
  useCORS: true,
8619
8660
  scale: window.devicePixelRatio,
8620
8661
  logging: false,
8621
- backgroundColor: null,
8622
- width: window.innerWidth,
8623
- height: window.innerHeight,
8662
+ backgroundColor: '#ffffff',
8663
+ width: captureWidth,
8664
+ height: captureHeight,
8624
8665
  x: window.scrollX,
8625
8666
  y: window.scrollY,
8626
- windowWidth: window.innerWidth,
8627
- windowHeight: window.innerHeight,
8667
+ windowWidth: captureWidth,
8668
+ windowHeight: captureHeight,
8628
8669
  ignoreElements: (element) => {
8629
- return element.hasAttribute('data-markup-widget');
8670
+ return (element.hasAttribute('data-markup-widget') ||
8671
+ element.hasAttribute('data-markup-pins') ||
8672
+ element.hasAttribute('data-markup-viewport'));
8630
8673
  },
8631
8674
  onclone: (clonedDoc) => {
8632
- // Process all style elements and inline styles to remove unsupported color functions
8633
- const processCSS = (css) => {
8634
- // Replace oklch/oklab/lch/lab color functions with transparent
8635
- return css.replace(/oklch\([^)]*\)|oklab\([^)]*\)|lch\([^)]*\)|lab\([^)]*\)/gi, 'transparent');
8636
- };
8637
- // Process <style> tags
8675
+ // Reset any viewport-mode inline constraints that were applied to <html>
8676
+ // so that html2canvas lays out the clone at the correct capture width.
8677
+ const clonedHtml = clonedDoc.documentElement;
8678
+ const clonedBody = clonedDoc.body;
8679
+ // Clear viewport-mode styles that would distort the clone layout
8680
+ clonedHtml.style.removeProperty('width');
8681
+ clonedHtml.style.removeProperty('max-width');
8682
+ clonedHtml.style.removeProperty('min-width');
8683
+ clonedHtml.style.removeProperty('margin');
8684
+ clonedHtml.style.removeProperty('border-radius');
8685
+ clonedHtml.style.removeProperty('box-shadow');
8686
+ // Restore body to a neutral white background so the backdrop colour
8687
+ // (#18181b when in responsive mode) does not bleed into the capture.
8688
+ clonedBody.style.removeProperty('background');
8689
+ clonedBody.style.removeProperty('min-height');
8690
+ // Process all style elements and inline styles to remove unsupported
8691
+ // CSS color functions (oklch etc.) that html2canvas cannot parse.
8692
+ const processCSS = (css) => css.replace(/oklch\([^)]*\)|oklab\([^)]*\)|lch\([^)]*\)|lab\([^)]*\)/gi, 'transparent');
8638
8693
  clonedDoc.querySelectorAll('style').forEach((style) => {
8639
- if (style.textContent) {
8694
+ if (style.textContent)
8640
8695
  style.textContent = processCSS(style.textContent);
8641
- }
8642
8696
  });
8643
- // Process inline styles
8644
8697
  clonedDoc.querySelectorAll('[style]').forEach((el) => {
8645
8698
  const htmlEl = el;
8646
- if (htmlEl.style.cssText) {
8699
+ if (htmlEl.style.cssText)
8647
8700
  htmlEl.style.cssText = processCSS(htmlEl.style.cssText);
8648
- }
8649
- });
8650
- // Process linked stylesheets (can't modify external, but can try)
8651
- clonedDoc.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
8652
- // We can't easily modify external stylesheets, but we can override with inline styles
8653
- // This is handled by the style tag processing above
8654
8701
  });
8655
8702
  },
8656
8703
  });
8657
- // Restore widget visibility
8658
- if (widget) {
8659
- widget.style.visibility = 'visible';
8660
- }
8704
+ // Restore visibility of all hidden overlay elements
8705
+ if (widget)
8706
+ widget.style.visibility = '';
8707
+ if (pinsOverlay)
8708
+ pinsOverlay.style.visibility = '';
8709
+ if (viewportBar)
8710
+ viewportBar.style.visibility = '';
8661
8711
  return canvas.toDataURL('image/png');
8662
8712
  }
8663
8713
  catch (error) {
8714
+ // Make sure overlays are always restored even on failure
8715
+ const widget = document.querySelector('[data-markup-widget]');
8716
+ const pinsOverlay = document.querySelector('[data-markup-pins]');
8717
+ const viewportBar = document.querySelector('[data-markup-viewport]');
8718
+ if (widget)
8719
+ widget.style.visibility = '';
8720
+ if (pinsOverlay)
8721
+ pinsOverlay.style.visibility = '';
8722
+ if (viewportBar)
8723
+ viewportBar.style.visibility = '';
8664
8724
  console.error('Failed to capture screenshot:', error);
8665
8725
  throw error;
8666
8726
  }
@@ -17207,7 +17267,7 @@ const AnnotationOverlay = ({ screenshot, onComplete, onCancel, }) => {
17207
17267
 
17208
17268
  const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastClickedElement, lastClickedText, }) => {
17209
17269
  var _a, _b;
17210
- const { config, currentScreenshot, setCurrentScreenshot, annotations, setAnnotations, clearAnnotations, setIsCapturing, isCapturing, currentUser, setIsOpen, } = useMarkupStore();
17270
+ const { config, currentScreenshot, setCurrentScreenshot, annotations, setAnnotations, clearAnnotations, setIsCapturing, isCapturing, currentUser, setIsOpen, viewportMode, } = useMarkupStore();
17211
17271
  const [title, setTitle] = React.useState((initialData === null || initialData === void 0 ? void 0 : initialData.title) || "");
17212
17272
  const [description, setDescription] = React.useState((initialData === null || initialData === void 0 ? void 0 : initialData.description) || "");
17213
17273
  const [priority, setPriority] = React.useState((initialData === null || initialData === void 0 ? void 0 : initialData.priority) || "medium");
@@ -17256,19 +17316,40 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17256
17316
  const handlePinClick = (e) => {
17257
17317
  e.preventDefault();
17258
17318
  e.stopPropagation();
17319
+ // Capture element info so pins can track the element when the layout changes
17320
+ let elementSelector;
17321
+ let elementOffsetX;
17322
+ let elementOffsetY;
17323
+ const target = e.target;
17324
+ if (target &&
17325
+ target !== document.documentElement &&
17326
+ target !== document.body) {
17327
+ try {
17328
+ elementSelector = getElementSelector$1(target);
17329
+ const rect = target.getBoundingClientRect();
17330
+ elementOffsetX = e.pageX - (rect.left + window.scrollX);
17331
+ elementOffsetY = e.pageY - (rect.top + window.scrollY);
17332
+ }
17333
+ catch (_) {
17334
+ // non-critical – fall back to percentage-based positioning
17335
+ }
17336
+ }
17259
17337
  setPinPosition({
17260
17338
  x: e.clientX,
17261
17339
  y: e.clientY,
17262
17340
  pageX: e.pageX,
17263
17341
  pageY: e.pageY,
17342
+ elementSelector,
17343
+ elementOffsetX,
17344
+ elementOffsetY,
17264
17345
  });
17265
17346
  setIsPlacingPin(false);
17266
17347
  };
17267
- document.addEventListener('click', handlePinClick, true);
17268
- document.body.style.cursor = 'crosshair';
17348
+ document.addEventListener("click", handlePinClick, true);
17349
+ document.body.style.cursor = "crosshair";
17269
17350
  return () => {
17270
- document.removeEventListener('click', handlePinClick, true);
17271
- document.body.style.cursor = '';
17351
+ document.removeEventListener("click", handlePinClick, true);
17352
+ document.body.style.cursor = "";
17272
17353
  // Reopen widget when pin placement is done
17273
17354
  setIsOpen(true);
17274
17355
  };
@@ -17284,34 +17365,64 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17284
17365
  e.stopPropagation();
17285
17366
  const target = e.target;
17286
17367
  // Check for img tag
17287
- if (target.tagName === 'IMG') {
17368
+ if (target.tagName === "IMG") {
17288
17369
  const img = target;
17289
17370
  setOriginalImageSrc(img.src);
17290
- setOriginalImageBackground('');
17371
+ setOriginalImageBackground("");
17291
17372
  // Place pin at image location
17373
+ let elSel;
17374
+ let elOffX;
17375
+ let elOffY;
17376
+ try {
17377
+ elSel = getElementSelector$1(target);
17378
+ const rect = target.getBoundingClientRect();
17379
+ elOffX = e.pageX - (rect.left + window.scrollX);
17380
+ elOffY = e.pageY - (rect.top + window.scrollY);
17381
+ }
17382
+ catch (_) {
17383
+ /* non-critical */
17384
+ }
17292
17385
  setPinPosition({
17293
17386
  x: e.clientX,
17294
17387
  y: e.clientY,
17295
17388
  pageX: e.pageX,
17296
17389
  pageY: e.pageY,
17390
+ elementSelector: elSel,
17391
+ elementOffsetX: elOffX,
17392
+ elementOffsetY: elOffY,
17297
17393
  });
17298
17394
  setIsSelectingImage(false);
17299
17395
  return;
17300
17396
  }
17301
17397
  // Check for background-image
17302
17398
  const bgImage = window.getComputedStyle(target).backgroundImage;
17303
- if (bgImage && bgImage !== 'none') {
17399
+ if (bgImage && bgImage !== "none") {
17304
17400
  // Extract URL from url("...") or url('...')
17305
17401
  const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
17306
17402
  if (urlMatch) {
17307
17403
  setOriginalImageBackground(urlMatch[1]);
17308
- setOriginalImageSrc('');
17404
+ setOriginalImageSrc("");
17309
17405
  // Place pin at element location
17406
+ let elSel;
17407
+ let elOffX;
17408
+ let elOffY;
17409
+ try {
17410
+ elSel = getElementSelector$1(target);
17411
+ const rect = target.getBoundingClientRect();
17412
+ elOffX = e.pageX - (rect.left + window.scrollX);
17413
+ elOffY = e.pageY - (rect.top + window.scrollY);
17414
+ }
17415
+ catch (_) {
17416
+ /* non-critical */
17417
+ }
17310
17418
  setPinPosition({
17311
17419
  x: e.clientX,
17312
17420
  y: e.clientY,
17313
17421
  pageX: e.pageX,
17314
17422
  pageY: e.pageY,
17423
+ elementSelector: elSel,
17424
+ elementOffsetX: elOffX,
17425
+ elementOffsetY: elOffY,
17315
17426
  });
17316
17427
  setIsSelectingImage(false);
17317
17428
  }
@@ -17320,27 +17431,27 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17320
17431
  // Add hover effect to show which element would be selected
17321
17432
  const handleImageHover = (e) => {
17322
17433
  const target = e.target;
17323
- const isImage = target.tagName === 'IMG';
17324
- const hasBgImage = window.getComputedStyle(target).backgroundImage !== 'none';
17434
+ const isImage = target.tagName === "IMG";
17435
+ const hasBgImage = window.getComputedStyle(target).backgroundImage !== "none";
17325
17436
  if (isImage || hasBgImage) {
17326
- target.style.outline = '3px solid #3b82f6';
17327
- target.style.outlineOffset = '2px';
17437
+ target.style.outline = "3px solid #3b82f6";
17438
+ target.style.outlineOffset = "2px";
17328
17439
  }
17329
17440
  };
17330
17441
  const handleImageHoverOut = (e) => {
17331
17442
  const target = e.target;
17332
- target.style.outline = '';
17333
- target.style.outlineOffset = '';
17443
+ target.style.outline = "";
17444
+ target.style.outlineOffset = "";
17334
17445
  };
17335
- document.addEventListener('click', handleImageClick, true);
17336
- document.addEventListener('mouseover', handleImageHover, true);
17337
- document.addEventListener('mouseout', handleImageHoverOut, true);
17338
- document.body.style.cursor = 'crosshair';
17446
+ document.addEventListener("click", handleImageClick, true);
17447
+ document.addEventListener("mouseover", handleImageHover, true);
17448
+ document.addEventListener("mouseout", handleImageHoverOut, true);
17449
+ document.body.style.cursor = "crosshair";
17339
17450
  return () => {
17340
- document.removeEventListener('click', handleImageClick, true);
17341
- document.removeEventListener('mouseover', handleImageHover, true);
17342
- document.removeEventListener('mouseout', handleImageHoverOut, true);
17343
- document.body.style.cursor = '';
17451
+ document.removeEventListener("click", handleImageClick, true);
17452
+ document.removeEventListener("mouseover", handleImageHover, true);
17453
+ document.removeEventListener("mouseout", handleImageHoverOut, true);
17454
+ document.body.style.cursor = "";
17344
17455
  // Reopen widget when image selection is done
17345
17456
  setIsOpen(true);
17346
17457
  };
@@ -17475,7 +17586,9 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17475
17586
  const handleSubmit = async (e) => {
17476
17587
  var _a, _b, _c, _d;
17477
17588
  e.preventDefault();
17478
- if (feedbackType !== "copy-amendment" && feedbackType !== "image-change" && !title.trim())
17589
+ if (feedbackType !== "copy-amendment" &&
17590
+ feedbackType !== "image-change" &&
17591
+ !title.trim())
17479
17592
  return;
17480
17593
  if (feedbackType === "copy-amendment" && !newCopy.trim())
17481
17594
  return;
@@ -17504,7 +17617,13 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17504
17617
  }
17505
17618
  const pageWidth = document.documentElement.scrollWidth;
17506
17619
  const pageHeight = document.documentElement.scrollHeight;
17507
- const pageMetadata = (initialData === null || initialData === void 0 ? void 0 : initialData.pageMetadata) || getPageMetadata(lastClickedElement || undefined);
17620
+ const rawMetadata = (initialData === null || initialData === void 0 ? void 0 : initialData.pageMetadata) ||
17621
+ getPageMetadata(lastClickedElement || undefined);
17622
+ // When in responsive/viewport mode, stamp the simulated device type so
17623
+ // reviewers know this feedback was captured at mobile/tablet dimensions.
17624
+ const pageMetadata = viewportMode
17625
+ ? { ...rawMetadata, deviceType: viewportMode.deviceType }
17626
+ : rawMetadata;
17508
17627
  const feedback = {
17509
17628
  id: (initialData === null || initialData === void 0 ? void 0 : initialData.id) || generateId(),
17510
17629
  type: feedbackType,
@@ -17550,14 +17669,19 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17550
17669
  pageHeight,
17551
17670
  scrollX: window.scrollX,
17552
17671
  scrollY: window.scrollY,
17553
- percentX: pageWidth ? (pinPosition.pageX / pageWidth) * 100 : undefined,
17554
- percentY: pageHeight ? (pinPosition.pageY / pageHeight) * 100 : undefined,
17672
+ percentX: pageWidth
17673
+ ? (pinPosition.pageX / pageWidth) * 100
17674
+ : undefined,
17675
+ percentY: pageHeight
17676
+ ? (pinPosition.pageY / pageHeight) * 100
17677
+ : undefined,
17678
+ elementSelector: pinPosition.elementSelector,
17679
+ elementOffsetX: pinPosition.elementOffsetX,
17680
+ elementOffsetY: pinPosition.elementOffsetY,
17555
17681
  }
17556
17682
  : undefined,
17557
17683
  // Image change data
17558
- originalImageSrc: feedbackType === "image-change"
17559
- ? originalImageSrc
17560
- : undefined,
17684
+ originalImageSrc: feedbackType === "image-change" ? originalImageSrc : undefined,
17561
17685
  originalImageBackground: feedbackType === "image-change"
17562
17686
  ? originalImageBackground
17563
17687
  : undefined,
@@ -17596,13 +17720,13 @@ const FeedbackForm = ({ onSubmit, onCancel, initialData, getSessionEvents, lastC
17596
17720
  if (isSelectingImage) {
17597
17721
  return (jsxRuntime.jsx("div", { className: "fixed top-4 left-1/2 -translate-x-1/2 z-[999999] pointer-events-none", children: jsxRuntime.jsxs("div", { className: "bg-white rounded-2xl shadow-2xl p-6 max-w-md mx-4 text-center border-2 border-blue-500", children: [jsxRuntime.jsx("div", { className: "w-12 h-12 mx-auto mb-3 bg-blue-100 rounded-full flex items-center justify-center", children: jsxRuntime.jsx("svg", { className: "w-6 h-6 text-blue-600", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }) }), jsxRuntime.jsx("h3", { className: "text-lg font-bold text-gray-900 mb-2", children: "Click on an Image" }), jsxRuntime.jsx("p", { className: "text-gray-600 text-sm mb-3", children: "Click on any image or element with a background image" }), jsxRuntime.jsx("button", { type: "button", onClick: () => setIsSelectingImage(false), className: "px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors pointer-events-auto", children: "Cancel" })] }) }));
17598
17722
  }
17599
- return (jsxRuntime.jsxs("form", { onSubmit: handleSubmit, children: [jsxRuntime.jsxs("div", { className: "mb-5", children: [jsxRuntime.jsx("label", { className: "block text-sm font-semibold mb-2 text-gray-800", children: "Type" }), jsxRuntime.jsx("div", { className: "flex gap-2 flex-wrap", children: [
17723
+ return (jsxRuntime.jsxs("form", { onSubmit: handleSubmit, children: [viewportMode && (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-5 px-3 py-2.5 rounded-xl bg-[#E6B6CF]/15 border border-[#E6B6CF]/40", children: [jsxRuntime.jsx("svg", { className: "w-4 h-4 text-[#d9a3c0] shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" }) }), jsxRuntime.jsxs("span", { className: "text-xs font-medium text-gray-700", children: [jsxRuntime.jsx("span", { className: "font-semibold", children: viewportMode.name }), jsxRuntime.jsxs("span", { className: "text-gray-500 ml-1", children: ["\u00B7 ", viewportMode.width, "\u00D7", viewportMode.height, " \u00B7 This feedback will be tagged as ", viewportMode.deviceType] })] })] })), jsxRuntime.jsxs("div", { className: "mb-5", children: [jsxRuntime.jsx("label", { className: "block text-sm font-semibold mb-2 text-gray-800", children: "Type" }), jsxRuntime.jsx("div", { className: "flex gap-2 flex-wrap", children: [
17600
17724
  { id: "general", label: "General" },
17601
17725
  { id: "copy-amendment", label: "Copy Change" },
17602
17726
  { id: "image-change", label: "Image Change" },
17603
17727
  ].map((type) => (jsxRuntime.jsx("div", { onClick: () => setFeedbackType(type.id), className: cn("flex-1 py-2 px-3 rounded-xl text-xs font-medium cursor-pointer transition-all text-center", feedbackType === type.id
17604
17728
  ? "bg-[#C2D1D9] text-black shadow-md shadow-[#C2D1D9]/30"
17605
- : "bg-gray-100 text-gray-700 hover:bg-gray-200 hover:shadow-sm"), children: type.label }, type.id))) })] }), feedbackType === "image-change" && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "mb-5", children: [jsxRuntime.jsx("label", { className: "block text-sm font-semibold mb-2 text-gray-800", children: "Select Image" }), jsxRuntime.jsxs("button", { type: "button", onClick: () => setIsSelectingImage(true), className: "w-full px-4 py-3 bg-[#c2d1d9] border border-blue-200 rounded-xl text-sm font-medium text-blue-700 hover:bg-blue-100 hover:border-blue-300 transition-all flex items-center justify-center gap-2", children: [jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" }) }), originalImageSrc || originalImageBackground
17729
+ : "bg-gray-100 text-gray-700 hover:bg-gray-200 hover:shadow-sm"), children: type.label }, type.id))) })] }), feedbackType === "image-change" && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "mb-5", children: [jsxRuntime.jsx("label", { className: "block text-sm font-semibold mb-2 text-gray-800", children: "Select Image" }), jsxRuntime.jsxs("button", { type: "button", onClick: () => setIsSelectingImage(true), className: "w-full px-4 py-3 bg-foreground border rounded-xl text-sm font-medium text-background transition-all flex items-center justify-center gap-2", children: [jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" }) }), originalImageSrc || originalImageBackground
17606
17730
  ? "Change Image"
17607
17731
  : "Click to Select Image"] }), (originalImageSrc || originalImageBackground) && (jsxRuntime.jsxs("div", { className: "mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200", children: [jsxRuntime.jsx("p", { className: "text-xs font-semibold text-gray-700 mb-2", children: "Current Image:" }), originalImageSrc ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("img", { src: originalImageSrc, alt: "Selected", className: "w-full h-32 object-cover rounded-lg mb-2" }), jsxRuntime.jsx("p", { className: "text-[10px] text-gray-500 font-mono break-all", children: originalImageSrc })] })) : (jsxRuntime.jsxs("p", { className: "text-[10px] text-gray-500 font-mono break-all", children: ["Background: ", originalImageBackground] }))] }))] }), jsxRuntime.jsxs("div", { className: "mb-5", children: [jsxRuntime.jsx("label", { className: "block text-sm font-semibold mb-2 text-gray-800", children: "Replacement Image URL *" }), jsxRuntime.jsx("input", { type: "url", placeholder: "https://example.com/new-image.jpg", value: replacementImageUrl, onChange: (e) => setReplacementImageUrl(e.target.value), required: feedbackType === "image-change", className: "w-full px-4 py-3 bg-green-50 border border-green-200 rounded-xl text-sm focus:outline-none focus:bg-white focus:border-green-500 focus:shadow-lg focus:shadow-green-500/10 transition-all" }), replacementImageUrl && (jsxRuntime.jsxs("div", { className: "mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200", children: [jsxRuntime.jsx("p", { className: "text-xs font-semibold text-gray-700 mb-2", children: "Preview:" }), jsxRuntime.jsx("img", { src: replacementImageUrl, alt: "Replacement Preview", className: "w-full h-32 object-cover rounded-lg", onError: (e) => {
17608
17732
  e.target.style.display = "none";
@@ -40273,161 +40397,190 @@ const AuthForm = ({ onSuccess }) => {
40273
40397
  : "Already have an account? Sign in" }) })] }), jsxRuntime.jsx("p", { className: "text-center text-xs text-gray-500 mt-6", children: "Your feedback will be associated with your account" })] }) }));
40274
40398
  };
40275
40399
 
40276
- const VIEWPORT_PRESETS = [
40400
+ const MOBILE_PRESETS = [
40277
40401
  { name: "iPhone SE", width: 375, height: 667, deviceType: "mobile" },
40278
- { name: "iPhone 12 Pro", width: 390, height: 844, deviceType: "mobile" },
40279
- { name: "iPhone 14 Pro Max", width: 430, height: 932, deviceType: "mobile" },
40402
+ { name: "iPhone 15 Pro", width: 393, height: 852, deviceType: "mobile" },
40403
+ { name: "iPhone 15 Pro Max", width: 430, height: 932, deviceType: "mobile" },
40404
+ { name: "Samsung S24", width: 360, height: 780, deviceType: "mobile" },
40405
+ ];
40406
+ const TABLET_PRESETS = [
40280
40407
  { name: "iPad Mini", width: 768, height: 1024, deviceType: "tablet" },
40281
- { name: "iPad Pro", width: 1024, height: 1366, deviceType: "tablet" },
40282
- { name: "Desktop", width: 1920, height: 1080, deviceType: "desktop" },
40408
+ { name: 'iPad Pro 12.9"', width: 1024, height: 1366, deviceType: "tablet" },
40283
40409
  ];
40410
+ const ALL_PRESETS = [...MOBILE_PRESETS, ...TABLET_PRESETS];
40411
+ const DeviceIcon = ({ deviceType, className }) => deviceType === "tablet"
40412
+ ? jsxRuntime.jsx(TabletIcon, { className: className })
40413
+ : jsxRuntime.jsx(MobileIcon, { className: className });
40284
40414
  const ViewportControls = () => {
40285
40415
  const { viewportMode, setViewportMode, isOpen: isWidgetOpen } = useMarkupStore();
40286
40416
  const [isExpanded, setIsExpanded] = React.useState(false);
40287
40417
  const dropdownRef = React.useRef(null);
40288
40418
  const handlePresetClick = (preset) => {
40289
- setViewportMode({ width: preset.width, height: preset.height });
40419
+ setViewportMode({ width: preset.width, height: preset.height, deviceType: preset.deviceType, name: preset.name });
40290
40420
  setIsExpanded(false);
40291
40421
  };
40292
40422
  const handleReset = () => {
40293
40423
  setViewportMode(null);
40294
40424
  setIsExpanded(false);
40295
40425
  };
40296
- const isActivePreset = (preset) => {
40297
- return (viewportMode === null || viewportMode === void 0 ? void 0 : viewportMode.width) === preset.width && (viewportMode === null || viewportMode === void 0 ? void 0 : viewportMode.height) === preset.height;
40298
- };
40299
- // Close dropdown on outside click
40300
40426
  React.useEffect(() => {
40301
40427
  if (!isExpanded)
40302
40428
  return;
40303
- const handleClickOutside = (event) => {
40304
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
40429
+ const onOutside = (e) => {
40430
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target))
40305
40431
  setIsExpanded(false);
40306
- }
40307
40432
  };
40308
- document.addEventListener('mousedown', handleClickOutside);
40309
- return () => document.removeEventListener('mousedown', handleClickOutside);
40433
+ document.addEventListener("mousedown", onOutside);
40434
+ return () => document.removeEventListener("mousedown", onOutside);
40310
40435
  }, [isExpanded]);
40311
- // Handle ESC key to close dropdown or reset viewport
40312
40436
  React.useEffect(() => {
40313
- const handleKeyDown = (event) => {
40314
- if (event.key === 'Escape') {
40315
- if (isExpanded) {
40316
- setIsExpanded(false);
40317
- event.preventDefault();
40318
- }
40319
- else if (viewportMode) {
40320
- handleReset();
40321
- event.preventDefault();
40322
- }
40437
+ const onKey = (e) => {
40438
+ if (e.key !== "Escape")
40439
+ return;
40440
+ if (isExpanded) {
40441
+ setIsExpanded(false);
40442
+ e.preventDefault();
40443
+ }
40444
+ else if (viewportMode) {
40445
+ handleReset();
40446
+ e.preventDefault();
40323
40447
  }
40324
40448
  };
40325
- document.addEventListener('keydown', handleKeyDown);
40326
- return () => document.removeEventListener('keydown', handleKeyDown);
40449
+ document.addEventListener("keydown", onKey);
40450
+ return () => document.removeEventListener("keydown", onKey);
40327
40451
  }, [isExpanded, viewportMode]);
40328
- // Don't show controls when widget is not open
40329
- if (!isWidgetOpen)
40452
+ if (!isWidgetOpen && !viewportMode)
40330
40453
  return null;
40331
- return (jsxRuntime.jsxs("div", { ref: dropdownRef, className: "fixed cursor-pointer top-4 left-1/2 -translate-x-1/2 z-[999998] flex items-center gap-2", children: [!viewportMode ? (jsxRuntime.jsxs("div", { onClick: () => setIsExpanded(!isExpanded), className: "bg-white border-2 border-gray-300 rounded-xl px-4 py-2 shadow-lg hover:shadow-xl transition-all flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 hover:border-gray-400", children: [jsxRuntime.jsx(DesktopIcon, { className: "w-4 h-4" }), "Responsive Mode"] })) : (jsxRuntime.jsxs("div", { className: "bg-white border-2 border-gray-300 rounded-xl px-4 py-2 shadow-lg flex items-center gap-3", children: [jsxRuntime.jsxs("span", { className: "text-sm font-medium text-gray-700", children: [viewportMode.width, " \u00D7 ", viewportMode.height] }), jsxRuntime.jsx("div", { onClick: () => setIsExpanded(!isExpanded), className: "text-gray-600 hover:text-gray-900 transition-colors", children: jsxRuntime.jsx(DesktopIcon, { className: "w-4 h-4" }) }), jsxRuntime.jsx("div", { onClick: handleReset, className: "px-2 py-1 text-xs font-medium text-red-600 hover:text-white hover:bg-red-600 border border-red-600 rounded transition-colors", children: "Reset" })] })), isExpanded && (jsxRuntime.jsx("div", { className: "absolute top-full left-1/2 -translate-x-1/2 mt-2 bg-white border-2 border-gray-200 rounded-xl shadow-2xl p-3 min-w-[240px]", children: jsxRuntime.jsxs("div", { className: "space-y-2", children: [jsxRuntime.jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide px-2 mb-2", children: "Mobile" }), VIEWPORT_PRESETS.filter((p) => p.deviceType === "mobile").map((preset) => (jsxRuntime.jsxs("div", { onClick: () => handlePresetClick(preset), className: cn("w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-left group", isActivePreset(preset)
40332
- ? "bg-blue-50 border border-blue-200"
40333
- : "hover:bg-gray-100"), children: [jsxRuntime.jsx(MobileIcon, { className: cn("w-4 h-4 transition-colors", isActivePreset(preset) ? "text-blue-600" : "text-gray-400 group-hover:text-gray-600") }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-700 group-hover:text-gray-900", children: preset.name }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500", children: [preset.width, " \u00D7 ", preset.height] })] })] }, preset.name))), jsxRuntime.jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide px-2 mt-4 mb-2", children: "Tablet" }), VIEWPORT_PRESETS.filter((p) => p.deviceType === "tablet").map((preset) => (jsxRuntime.jsxs("div", { onClick: () => handlePresetClick(preset), className: cn("w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-left group", isActivePreset(preset)
40334
- ? "bg-blue-50 border border-blue-200"
40335
- : "hover:bg-gray-100"), children: [jsxRuntime.jsx(TabletIcon, { className: cn("w-4 h-4 transition-colors", isActivePreset(preset) ? "text-blue-600" : "text-gray-400 group-hover:text-gray-600") }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-700 group-hover:text-gray-900", children: preset.name }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500", children: [preset.width, " \u00D7 ", preset.height] })] })] }, preset.name))), jsxRuntime.jsx("div", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide px-2 mt-4 mb-2", children: "Desktop" }), VIEWPORT_PRESETS.filter((p) => p.deviceType === "desktop").map((preset) => (jsxRuntime.jsxs("div", { onClick: () => handlePresetClick(preset), className: cn("w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-all text-left group", isActivePreset(preset)
40336
- ? "bg-blue-50 border border-blue-200"
40337
- : "hover:bg-gray-100"), children: [jsxRuntime.jsx(DesktopIcon, { className: cn("w-4 h-4 transition-colors", isActivePreset(preset) ? "text-blue-600" : "text-gray-400 group-hover:text-gray-600") }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "text-sm font-medium text-gray-700 group-hover:text-gray-900", children: preset.name }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500", children: [preset.width, " \u00D7 ", preset.height] })] })] }, preset.name)))] }) }))] }));
40338
- };
40339
-
40454
+ return (jsxRuntime.jsxs("div", { ref: dropdownRef, className: "fixed top-4 left-1/2 -translate-x-1/2 z-[999999] select-none", "data-markup-viewport": true, children: [!viewportMode && (jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { onClick: () => setIsExpanded(!isExpanded), className: cn("flex items-center gap-2 px-4 py-2 rounded-2xl text-sm font-medium bg-white border transition-all shadow-md", isExpanded
40455
+ ? "border-[#E6B6CF] text-black shadow-lg shadow-[#E6B6CF]/20"
40456
+ : "border-gray-200 text-gray-600 hover:border-[#E6B6CF] hover:text-black hover:shadow-lg hover:shadow-[#E6B6CF]/20"), children: [jsxRuntime.jsx(MobileIcon, { className: cn("w-4 h-4 transition-colors", isExpanded ? "text-[#d9a3c0]" : "text-gray-400 group-hover:text-[#d9a3c0]") }), jsxRuntime.jsx("span", { children: "Responsive" }), jsxRuntime.jsx("svg", { className: cn("w-3 h-3 text-gray-400 transition-transform duration-200", isExpanded && "rotate-180"), fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }), isExpanded && (jsxRuntime.jsxs("div", { className: "absolute top-full left-1/2 -translate-x-1/2 mt-2 bg-white border border-gray-200 rounded-2xl shadow-xl p-2 min-w-[260px]", children: [jsxRuntime.jsx("p", { className: "text-[10px] font-semibold text-gray-400 uppercase tracking-widest px-3 pt-2 pb-1", children: "Mobile" }), MOBILE_PRESETS.map(preset => (jsxRuntime.jsxs("button", { onClick: () => handlePresetClick(preset), className: "w-full flex items-center gap-3 px-3 py-2 rounded-xl hover:bg-[#E6B6CF]/10 transition-colors text-left", children: [jsxRuntime.jsx(MobileIcon, { className: "w-4 h-4 text-gray-400 shrink-0" }), jsxRuntime.jsx("span", { className: "flex-1 text-sm font-medium text-gray-700", children: preset.name }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-400 tabular-nums", children: [preset.width, "\u00D7", preset.height] })] }, preset.name))), jsxRuntime.jsx("div", { className: "my-2 border-t border-gray-100" }), jsxRuntime.jsx("p", { className: "text-[10px] font-semibold text-gray-400 uppercase tracking-widest px-3 pb-1", children: "Tablet" }), TABLET_PRESETS.map(preset => (jsxRuntime.jsxs("button", { onClick: () => handlePresetClick(preset), className: "w-full flex items-center gap-3 px-3 py-2 rounded-xl hover:bg-[#E6B6CF]/10 transition-colors text-left", children: [jsxRuntime.jsx(TabletIcon, { className: "w-4 h-4 text-gray-400 shrink-0" }), jsxRuntime.jsx("span", { className: "flex-1 text-sm font-medium text-gray-700", children: preset.name }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-400 tabular-nums", children: [preset.width, "\u00D7", preset.height] })] }, preset.name)))] }))] })), viewportMode && (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 bg-white border border-gray-200 rounded-2xl shadow-lg px-1.5 py-1.5", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 px-2.5 py-1 rounded-xl bg-[#E6B6CF]/20", children: [jsxRuntime.jsx(DeviceIcon, { deviceType: viewportMode.deviceType, className: "w-3.5 h-3.5 text-[#d9a3c0] shrink-0" }), jsxRuntime.jsx("span", { className: "text-xs font-semibold text-gray-800", children: viewportMode.name }), jsxRuntime.jsxs("span", { className: "text-[10px] text-gray-500 tabular-nums", children: [viewportMode.width, "\u00D7", viewportMode.height] })] }), jsxRuntime.jsx("div", { className: "w-px h-5 bg-gray-200 mx-0.5" }), ALL_PRESETS.map(preset => {
40457
+ const isActive = viewportMode.name === preset.name;
40458
+ return (jsxRuntime.jsx("button", { onClick: () => handlePresetClick(preset), title: `${preset.name} ${preset.width}×${preset.height}`, className: cn("flex items-center justify-center w-7 h-7 rounded-xl transition-all", isActive
40459
+ ? "bg-[#E6B6CF] text-black shadow-sm shadow-[#E6B6CF]/40"
40460
+ : "text-gray-400 hover:bg-[#E6B6CF]/15 hover:text-gray-700"), children: jsxRuntime.jsx(DeviceIcon, { deviceType: preset.deviceType, className: "w-3.5 h-3.5" }) }, preset.name));
40461
+ }), jsxRuntime.jsx("div", { className: "w-px h-5 bg-gray-200 mx-0.5" }), jsxRuntime.jsx("button", { onClick: handleReset, title: "Exit responsive mode (Esc)", className: "flex items-center justify-center w-7 h-7 rounded-xl text-gray-400 hover:bg-red-50 hover:text-red-500 transition-all", children: jsxRuntime.jsx(CloseIcon, { className: "w-3.5 h-3.5" }) })] }))] }));
40462
+ };
40463
+
40464
+ /**
40465
+ * Compute the CSS position style for a single pin.
40466
+ *
40467
+ * Priority:
40468
+ * 1. Element-anchored: if `elementSelector` + `elementOffsetX/Y` are stored,
40469
+ * query the element's current bounding rect and add the stored offset so the
40470
+ * pin sticks to the element regardless of scroll / resize / layout-reflow.
40471
+ * 2. Page-percentage fallback: use `percentX`/`percentY` (stored as % of the
40472
+ * full document dimensions at save-time). These already scale correctly
40473
+ * when the document resizes because the container uses the same dimensions.
40474
+ */
40475
+ function computePinStyle(pinLocation) {
40476
+ // --- Element-anchored positioning ---
40477
+ if (pinLocation.elementSelector &&
40478
+ pinLocation.elementOffsetX != null &&
40479
+ pinLocation.elementOffsetY != null) {
40480
+ try {
40481
+ const el = document.querySelector(pinLocation.elementSelector);
40482
+ if (el) {
40483
+ const rect = el.getBoundingClientRect();
40484
+ const elPageLeft = rect.left + window.scrollX;
40485
+ const elPageTop = rect.top + window.scrollY;
40486
+ return {
40487
+ position: "absolute",
40488
+ left: elPageLeft + pinLocation.elementOffsetX,
40489
+ top: elPageTop + pinLocation.elementOffsetY,
40490
+ transform: "translate(-50%, -50%)",
40491
+ pointerEvents: "auto",
40492
+ };
40493
+ }
40494
+ }
40495
+ catch (_) {
40496
+ // fall through to percentage fallback
40497
+ }
40498
+ }
40499
+ // --- Page-percentage fallback ---
40500
+ const pageWidth = pinLocation.pageWidth ||
40501
+ pinLocation.viewportWidth ||
40502
+ window.innerWidth;
40503
+ const pageHeight = pinLocation.pageHeight ||
40504
+ pinLocation.viewportHeight ||
40505
+ window.innerHeight;
40506
+ const leftPercent = typeof pinLocation.percentX === "number"
40507
+ ? pinLocation.percentX
40508
+ : pageWidth
40509
+ ? (pinLocation.pageX / pageWidth) * 100
40510
+ : 0;
40511
+ const topPercent = typeof pinLocation.percentY === "number"
40512
+ ? pinLocation.percentY
40513
+ : pageHeight
40514
+ ? (pinLocation.pageY / pageHeight) * 100
40515
+ : 0;
40516
+ return {
40517
+ position: "absolute",
40518
+ left: `${leftPercent}%`,
40519
+ top: `${topPercent}%`,
40520
+ transform: "translate(-50%, -50%)",
40521
+ pointerEvents: "auto",
40522
+ };
40523
+ }
40340
40524
  const FeedbackPins = ({ onPinClick }) => {
40341
40525
  const { feedbackItems, showPins } = useMarkupStore();
40342
- const [docSize, setDocSize] = React.useState({ width: 0, height: 0 });
40526
+ // Bump a counter on every scroll / resize so we re-render and re-query
40527
+ // element rects. We don't need to store the actual dimensions here because
40528
+ // computePinStyle reads from the DOM live each render.
40529
+ const [, setLayoutVersion] = React.useState(0);
40343
40530
  React.useEffect(() => {
40344
- const measure = () => {
40345
- const root = document.documentElement;
40346
- const body = document.body;
40347
- const width = root.clientWidth || body.clientWidth || window.innerWidth;
40348
- const height = Math.max(root.scrollHeight, body.scrollHeight, root.clientHeight, body.clientHeight);
40349
- setDocSize({ width, height });
40350
- };
40351
- measure();
40352
- const resizeObserver = new ResizeObserver(() => measure());
40531
+ const bump = () => setLayoutVersion((v) => v + 1);
40532
+ const resizeObserver = new ResizeObserver(bump);
40353
40533
  resizeObserver.observe(document.documentElement);
40354
40534
  resizeObserver.observe(document.body);
40355
- const handleResize = () => measure();
40356
- const handleScroll = () => measure();
40357
- window.addEventListener('resize', handleResize);
40358
- window.addEventListener('scroll', handleScroll, { passive: true });
40535
+ window.addEventListener("resize", bump);
40536
+ window.addEventListener("scroll", bump, { passive: true });
40359
40537
  return () => {
40360
40538
  resizeObserver.disconnect();
40361
- window.removeEventListener('resize', handleResize);
40362
- window.removeEventListener('scroll', handleScroll);
40539
+ window.removeEventListener("resize", bump);
40540
+ window.removeEventListener("scroll", bump);
40363
40541
  };
40364
40542
  }, []);
40365
40543
  // Filter pins for current page URL
40366
40544
  const currentPagePins = React.useMemo(() => {
40367
40545
  const currentUrl = window.location.href;
40368
- return feedbackItems.filter(item => {
40546
+ return feedbackItems.filter((item) => {
40369
40547
  var _a;
40370
- // Only show pins with pinLocation data
40371
40548
  if (!item.pinLocation)
40372
40549
  return false;
40373
- // Only show pins from the same page
40374
40550
  if (((_a = item.pageMetadata) === null || _a === void 0 ? void 0 : _a.url) !== currentUrl)
40375
40551
  return false;
40376
40552
  return true;
40377
40553
  });
40378
40554
  }, [feedbackItems]);
40379
- // Don't show pins if toggle is off
40380
- if (!showPins)
40381
- return null;
40382
- if (currentPagePins.length === 0)
40555
+ if (!showPins || currentPagePins.length === 0)
40383
40556
  return null;
40557
+ // The container covers the full scrollable document so that absolute-
40558
+ // positioned pins land in the right place regardless of scroll position.
40559
+ const root = document.documentElement;
40560
+ const body = document.body;
40561
+ const docWidth = root.clientWidth || body.clientWidth || window.innerWidth;
40562
+ const docHeight = Math.max(root.scrollHeight, body.scrollHeight, root.clientHeight, body.clientHeight);
40384
40563
  const containerStyle = {
40385
- position: 'absolute',
40564
+ position: "absolute",
40386
40565
  top: 0,
40387
40566
  left: 0,
40388
- width: docSize.width || '100%',
40389
- height: docSize.height || '100%',
40390
- pointerEvents: 'none',
40567
+ width: docWidth,
40568
+ height: docHeight,
40569
+ pointerEvents: "none",
40391
40570
  zIndex: 999998,
40392
40571
  };
40393
40572
  return (jsxRuntime.jsx("div", { className: "feedback-pins-container", "data-markup-pins": true, style: containerStyle, children: currentPagePins.map((feedback) => {
40394
40573
  const { pinLocation } = feedback;
40395
40574
  if (!pinLocation)
40396
40575
  return null;
40397
- // If we have an element selector, try to position relative to that element
40398
- if (pinLocation.elementSelector) {
40399
- try {
40400
- const pageWidth = docSize.width || pinLocation.pageWidth || pinLocation.viewportWidth || window.innerWidth;
40401
- const pageHeight = docSize.height || pinLocation.pageHeight || pinLocation.viewportHeight || window.innerHeight;
40402
- // Prefer stored percentages; fall back to computing from stored absolute coords
40403
- const leftPercent = typeof pinLocation.percentX === 'number'
40404
- ? pinLocation.percentX
40405
- : pageWidth ? (pinLocation.pageX / pageWidth) * 100 : 0;
40406
- const topPercent = typeof pinLocation.percentY === 'number'
40407
- ? pinLocation.percentY
40408
- : pageHeight ? (pinLocation.pageY / pageHeight) * 100 : 0;
40409
- const style = {
40410
- position: 'absolute',
40411
- left: `${leftPercent}%`,
40412
- top: `${topPercent}%`,
40413
- transform: 'translate(-50%, -50%)',
40414
- pointerEvents: 'auto',
40415
- };
40416
- return (jsxRuntime.jsx("div", { className: "feedback-pin-wrapper", style: style, onClick: () => onPinClick(feedback), children: jsxRuntime.jsxs("div", { className: "feedback-pin group relative cursor-pointer", children: [jsxRuntime.jsx("div", { className: `
40576
+ const pinStyle = computePinStyle(pinLocation);
40577
+ return (jsxRuntime.jsx("div", { className: "feedback-pin-wrapper", style: pinStyle, onClick: () => onPinClick(feedback), children: jsxRuntime.jsxs("div", { className: "feedback-pin group relative cursor-pointer", children: [jsxRuntime.jsx("div", { className: `
40417
40578
  w-8 h-8 rounded-full flex items-center justify-center
40418
40579
  shadow-lg hover:shadow-xl transition-all
40419
- ${feedback.status === 'open' ? 'bg-[#E6B6CF] hover:bg-[#d9a3c0]' : 'bg-green-500 hover:bg-green-600'}
40580
+ ${feedback.status === "open" ? "bg-[#E6B6CF] hover:bg-[#d9a3c0]" : "bg-green-500 hover:bg-green-600"}
40420
40581
  border-2 border-white
40421
40582
  hover:scale-110
40422
- `, children: jsxRuntime.jsx(MessageIcon, { className: "w-4 h-4 text-white" }) }), jsxRuntime.jsxs("div", { className: "\n absolute bottom-full left-1/2 -translate-x-1/2 mb-2\n opacity-0 group-hover:opacity-100\n pointer-events-none\n transition-opacity duration-200\n bg-gray-900 text-white text-xs rounded-lg py-2 px-3\n whitespace-nowrap shadow-xl\n max-w-[200px]\n ", children: [jsxRuntime.jsx("div", { className: "font-semibold truncate", children: feedback.title || 'Feedback' }), jsxRuntime.jsx("div", { className: "text-gray-300 text-[10px] mt-0.5", children: "Click to view" }), jsxRuntime.jsx("div", { className: "\n absolute top-full left-1/2 -translate-x-1/2\n w-0 h-0\n border-l-4 border-l-transparent\n border-r-4 border-r-transparent\n border-t-4 border-t-gray-900\n " })] })] }) }, feedback.id));
40423
- }
40424
- catch (e) {
40425
- // If anything goes wrong measuring/positioning, skip this pin
40426
- return null;
40427
- }
40428
- }
40429
- // If we didn't render a pin above, return null so map callback returns consistently
40430
- return null;
40583
+ `, children: jsxRuntime.jsx(MessageIcon, { className: "w-4 h-4 text-white" }) }), jsxRuntime.jsxs("div", { className: "\n absolute bottom-full left-1/2 -translate-x-1/2 mb-2\n opacity-0 group-hover:opacity-100\n pointer-events-none\n transition-opacity duration-200\n bg-gray-900 text-white text-xs rounded-lg py-2 px-3\n whitespace-nowrap shadow-xl\n max-w-[200px]\n ", children: [jsxRuntime.jsx("div", { className: "font-semibold truncate", children: feedback.title || "Feedback" }), jsxRuntime.jsx("div", { className: "text-gray-300 text-[10px] mt-0.5", children: "Click to view" }), jsxRuntime.jsx("div", { className: "\n absolute top-full left-1/2 -translate-x-1/2\n w-0 h-0\n border-l-4 border-l-transparent\n border-r-4 border-r-transparent\n border-t-4 border-t-gray-900\n " })] })] }) }, feedback.id));
40431
40584
  }) }));
40432
40585
  };
40433
40586
 
@@ -40686,32 +40839,52 @@ const MarkupWidget = ({ config: userConfig, }) => {
40686
40839
  }, []);
40687
40840
  // Apply viewport mode styling to document (like browser dev tools)
40688
40841
  React.useEffect(() => {
40689
- if (viewportMode) {
40690
- const root = document.documentElement;
40691
- const body = document.body;
40692
- // Apply dark background to body
40693
- body.style.background = 'rgba(0, 0, 0, 0.5)';
40694
- body.style.overflow = 'visible';
40695
- // Apply viewport constraints with smooth transition
40696
- root.style.transition = 'max-width 0.3s ease-in-out, margin 0.3s ease-in-out, background 0.2s ease-in-out';
40697
- root.style.maxWidth = `${viewportMode.width}px`;
40698
- root.style.margin = '0 auto';
40699
- root.style.border = '2px solid #999';
40700
- root.style.background = '#ffffff'; // Ensure content area has white background
40701
- root.style.boxShadow = '0 0 50px rgba(0, 0, 0, 0.3)';
40702
- return () => {
40703
- // Reset to full width
40704
- root.style.transition = '';
40705
- root.style.maxWidth = '';
40706
- root.style.margin = '';
40707
- root.style.border = '';
40708
- root.style.background = '';
40709
- root.style.boxShadow = '';
40710
- body.style.overflow = '';
40711
- body.style.background = '';
40712
- };
40713
- }
40714
- return undefined;
40842
+ if (!viewportMode)
40843
+ return;
40844
+ const root = document.documentElement;
40845
+ const body = document.body;
40846
+ // Snapshot every inline style property we're about to touch so we
40847
+ // can restore them exactly on cleanup.
40848
+ const saved = {
40849
+ htmlWidth: root.style.width,
40850
+ htmlMaxWidth: root.style.maxWidth,
40851
+ htmlMinWidth: root.style.minWidth,
40852
+ htmlMargin: root.style.margin,
40853
+ htmlBackground: root.style.background,
40854
+ htmlBorderRadius: root.style.borderRadius,
40855
+ htmlBoxShadow: root.style.boxShadow,
40856
+ bodyBackground: body.style.background,
40857
+ bodyMinHeight: body.style.minHeight,
40858
+ bodyOverflow: body.style.overflow,
40859
+ };
40860
+ // --- Constrain <html> to the device width so that flex/grid layouts
40861
+ // actually reflow at the target width (max-width alone isn't enough).
40862
+ root.style.width = `${viewportMode.width}px`;
40863
+ root.style.maxWidth = `${viewportMode.width}px`;
40864
+ root.style.minWidth = '0';
40865
+ root.style.margin = '24px auto';
40866
+ root.style.background = '#ffffff';
40867
+ root.style.borderRadius = '16px';
40868
+ root.style.boxShadow = '0 32px 96px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.08)';
40869
+ // --- Body background fills the full viewport and acts as the dark
40870
+ // backdrop (body background-painting area always covers the viewport).
40871
+ body.style.background = '#18181b';
40872
+ body.style.minHeight = '100vh';
40873
+ body.style.overflow = 'visible';
40874
+ // Scroll to top so the user sees the page from the start.
40875
+ window.scrollTo({ top: 0, behavior: 'smooth' });
40876
+ return () => {
40877
+ root.style.width = saved.htmlWidth;
40878
+ root.style.maxWidth = saved.htmlMaxWidth;
40879
+ root.style.minWidth = saved.htmlMinWidth;
40880
+ root.style.margin = saved.htmlMargin;
40881
+ root.style.background = saved.htmlBackground;
40882
+ root.style.borderRadius = saved.htmlBorderRadius;
40883
+ root.style.boxShadow = saved.htmlBoxShadow;
40884
+ body.style.background = saved.bodyBackground;
40885
+ body.style.minHeight = saved.bodyMinHeight;
40886
+ body.style.overflow = saved.bodyOverflow;
40887
+ };
40715
40888
  }, [viewportMode]);
40716
40889
  React.useEffect(() => {
40717
40890
  if (userConfig) {