@sirendesign/markup 1.0.32 → 1.0.36

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