@m13v/seo-components 0.38.4 → 0.38.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/seo-components",
3
- "version": "0.38.4",
3
+ "version": "0.38.5",
4
4
  "scripts": {
5
5
  "build:css": "tailwind -i src/_build.css -o dist/styles.css --minify",
6
6
  "lint:mobile-spans": "node scripts/lint-mobile-spans.mjs",
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState } from "react";
4
+ import { createPortal } from "react-dom";
4
5
  import { motion, AnimatePresence } from "framer-motion";
5
6
  import { trackGetStartedClick } from "../lib/track";
6
7
  import { useCapture } from "../lib/analytics-context";
@@ -133,6 +134,15 @@ export function InstallEmailGate({
133
134
  const [submitting, setSubmitting] = useState(false);
134
135
  const [error, setError] = useState("");
135
136
  const [copied, setCopied] = useState<"command" | "config" | null>(null);
137
+ // Mounted flag so we can portal the modal to document.body on the client
138
+ // without breaking SSR hydration. The modal escapes any transformed parent
139
+ // (e.g. .reveal-up) that would otherwise trap position: fixed inside that
140
+ // ancestor's stacking context, allowing clicks to leak through to the
141
+ // underlying page.
142
+ const [mounted, setMounted] = useState(false);
143
+ useEffect(() => {
144
+ setMounted(true);
145
+ }, []);
136
146
 
137
147
  useEffect(() => {
138
148
  if (stage === "closed") return;
@@ -286,28 +296,8 @@ export function InstallEmailGate({
286
296
  ? "group inline-flex h-11 items-center gap-2 rounded-md bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-800"
287
297
  : "inline-flex items-center justify-center rounded-md border border-zinc-300 bg-white px-5 py-2.5 text-sm font-medium text-zinc-800 transition-colors hover:border-zinc-400";
288
298
 
289
- return (
290
- <>
291
- {renderTrigger ? (
292
- renderTrigger({ onClick: onOpen })
293
- ) : (
294
- <button type="button" onClick={onOpen} className={`${buttonClass} ${className}`.trim()}>
295
- {label}
296
- {variant === "primary" && (
297
- <svg
298
- className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
299
- fill="none"
300
- viewBox="0 0 24 24"
301
- stroke="currentColor"
302
- strokeWidth={2}
303
- >
304
- <path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
305
- </svg>
306
- )}
307
- </button>
308
- )}
309
-
310
- <AnimatePresence>
299
+ const overlay = (
300
+ <AnimatePresence>
311
301
  {stage !== "closed" && (
312
302
  <motion.div
313
303
  key="backdrop"
@@ -505,6 +495,35 @@ export function InstallEmailGate({
505
495
  </motion.div>
506
496
  )}
507
497
  </AnimatePresence>
498
+ );
499
+
500
+ return (
501
+ <>
502
+ {renderTrigger ? (
503
+ renderTrigger({ onClick: onOpen })
504
+ ) : (
505
+ <button type="button" onClick={onOpen} className={`${buttonClass} ${className}`.trim()}>
506
+ {label}
507
+ {variant === "primary" && (
508
+ <svg
509
+ className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
510
+ fill="none"
511
+ viewBox="0 0 24 24"
512
+ stroke="currentColor"
513
+ strokeWidth={2}
514
+ >
515
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
516
+ </svg>
517
+ )}
518
+ </button>
519
+ )}
520
+ {/* Portal the modal to document.body so a transformed ancestor (e.g.
521
+ `.reveal-up` with `transform: translateY`) can't trap position:fixed
522
+ inside its own stacking context, which would let clicks leak through
523
+ to elements layered on top of the modal at viewport level. */}
524
+ {mounted && typeof document !== "undefined"
525
+ ? createPortal(overlay, document.body)
526
+ : null}
508
527
  </>
509
528
  );
510
529
  }