@frontlit/page-builder 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/blocks/index.d.ts +18 -3
  2. package/dist/blocks/index.js +3 -3
  3. package/dist/builder/index.d.ts +4 -1
  4. package/dist/builder/index.js +6 -6
  5. package/dist/{chunk-BBI7UHOA.js → chunk-BWOGREWJ.js} +3 -3
  6. package/dist/{chunk-BBI7UHOA.js.map → chunk-BWOGREWJ.js.map} +1 -1
  7. package/dist/{chunk-K4WCLHMS.js → chunk-FZBLE44D.js} +317 -66
  8. package/dist/chunk-FZBLE44D.js.map +1 -0
  9. package/dist/{chunk-J7Z4UW45.js → chunk-OEQJQ4OA.js} +112 -5
  10. package/dist/chunk-OEQJQ4OA.js.map +1 -0
  11. package/dist/{chunk-GIKTOAQS.js → chunk-PQTDUFKU.js} +9 -9
  12. package/dist/chunk-PQTDUFKU.js.map +1 -0
  13. package/dist/{chunk-36QHCIPM.js → chunk-QMUIISN3.js} +5 -93
  14. package/dist/chunk-QMUIISN3.js.map +1 -0
  15. package/dist/{chunk-IU4KATI2.js → chunk-YXLOVEQZ.js} +6 -6
  16. package/dist/{chunk-IU4KATI2.js.map → chunk-YXLOVEQZ.js.map} +1 -1
  17. package/dist/components/index.d.ts +1 -1
  18. package/dist/components/index.js +3 -3
  19. package/dist/{image-Bk8kPXwP.d.ts → image-Cv0aSUHD.d.ts} +3 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +6 -6
  22. package/dist/primitives/index.d.ts +2 -0
  23. package/dist/primitives/index.js +1 -1
  24. package/dist/renderer/index.js +5 -5
  25. package/dist/server/index.js +151 -53
  26. package/dist/server/index.js.map +1 -1
  27. package/dist/server/islands/header-github-stars.d.ts +12 -0
  28. package/dist/server/islands/header-github-stars.js +31 -0
  29. package/dist/server/islands/header-github-stars.js.map +1 -0
  30. package/dist/server/islands/pricing-period-switch.d.ts +20 -0
  31. package/dist/server/islands/pricing-period-switch.js +151 -0
  32. package/dist/server/islands/pricing-period-switch.js.map +1 -0
  33. package/dist/server/primitives.d.ts +2 -0
  34. package/dist/server/primitives.js +53 -30
  35. package/dist/server/primitives.js.map +1 -1
  36. package/dist/server/schema.d.ts +38 -0
  37. package/dist/server/schema.js +17 -2
  38. package/dist/server/schema.js.map +1 -1
  39. package/dist/styles.css +12 -0
  40. package/package.json +1 -1
  41. package/schema/page.v1.json +29 -1
  42. package/schema/site.v1.json +8 -0
  43. package/dist/chunk-36QHCIPM.js.map +0 -1
  44. package/dist/chunk-GIKTOAQS.js.map +0 -1
  45. package/dist/chunk-J7Z4UW45.js.map +0 -1
  46. package/dist/chunk-K4WCLHMS.js.map +0 -1
package/dist/styles.css CHANGED
@@ -4642,6 +4642,12 @@
4642
4642
  opacity: 100%;
4643
4643
  }
4644
4644
  }
4645
+ .group-data-\[period\=yearly\]\/pricing\:block:is(:where(.group\/pricing)[data-period="yearly"] *) {
4646
+ display: block;
4647
+ }
4648
+ .group-data-\[period\=yearly\]\/pricing\:hidden:is(:where(.group\/pricing)[data-period="yearly"] *) {
4649
+ display: none;
4650
+ }
4645
4651
  .peer-focus-visible\:ring-2:is(:where(.peer):focus-visible ~ *) {
4646
4652
  --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4647
4653
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -8496,12 +8502,18 @@
8496
8502
  .lg\:\!grow {
8497
8503
  flex-grow: 1 !important;
8498
8504
  }
8505
+ .lg\:grid-cols-1 {
8506
+ grid-template-columns: repeat(1, minmax(0, 1fr));
8507
+ }
8499
8508
  .lg\:grid-cols-2 {
8500
8509
  grid-template-columns: repeat(2, minmax(0, 1fr));
8501
8510
  }
8502
8511
  .lg\:grid-cols-3 {
8503
8512
  grid-template-columns: repeat(3, minmax(0, 1fr));
8504
8513
  }
8514
+ .lg\:grid-cols-4 {
8515
+ grid-template-columns: repeat(4, minmax(0, 1fr));
8516
+ }
8505
8517
  .lg\:\!flex-row {
8506
8518
  flex-direction: row !important;
8507
8519
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontlit/page-builder",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "Block-based WYSIWYG page builder and renderer for React",
5
5
  "author": "FrontLit",
6
6
  "license": "MIT",
@@ -2243,6 +2243,26 @@
2243
2243
  ],
2244
2244
  "default": "center"
2245
2245
  },
2246
+ "pricingSwitcher": {
2247
+ "type": "boolean",
2248
+ "description": "Show a Monthly/Yearly switch above the plans."
2249
+ },
2250
+ "monthlyPriceCaption": {
2251
+ "type": "string",
2252
+ "description": "Defaults to \"Monthly\"."
2253
+ },
2254
+ "yearlyPriceCaption": {
2255
+ "type": "string",
2256
+ "description": "Defaults to \"Yearly\", e.g. \"Yearly (2 months free)\"."
2257
+ },
2258
+ "defaultPeriod": {
2259
+ "type": "string",
2260
+ "enum": [
2261
+ "monthly",
2262
+ "yearly"
2263
+ ],
2264
+ "description": "Period selected on load. Defaults to yearly."
2265
+ },
2246
2266
  "tiers": {
2247
2267
  "type": "array",
2248
2268
  "items": {
@@ -2253,7 +2273,11 @@
2253
2273
  },
2254
2274
  "price": {
2255
2275
  "type": "string",
2256
- "description": "Display string, e.g. \"$29/mo\"."
2276
+ "description": "Display string, e.g. \"$29/month\". The monthly price when pricingSwitcher is on."
2277
+ },
2278
+ "priceYearly": {
2279
+ "type": "string",
2280
+ "description": "Shown when the switch is on Yearly, e.g. \"$290/year\". Falls back to price."
2257
2281
  },
2258
2282
  "description": {
2259
2283
  "type": "string"
@@ -2271,6 +2295,10 @@
2271
2295
  "type": "string",
2272
2296
  "description": "A site path (\"/pricing\"), anchor (\"/#faq\") or absolute URL."
2273
2297
  },
2298
+ "buttonActionYearly": {
2299
+ "type": "string",
2300
+ "description": "A site path (\"/pricing\"), anchor (\"/#faq\") or absolute URL."
2301
+ },
2274
2302
  "highlighted": {
2275
2303
  "type": "boolean"
2276
2304
  }
@@ -3489,6 +3489,14 @@
3489
3489
  "minimum": 0,
3490
3490
  "maximum": 8,
3491
3491
  "default": 2
3492
+ },
3493
+ "githubRepo": {
3494
+ "type": "string",
3495
+ "description": "GitHub repository as \"owner/repo\" or a github.com URL. Shows a GitHub icon link; other values are ignored."
3496
+ },
3497
+ "showGithubStars": {
3498
+ "type": "boolean",
3499
+ "description": "Show the repository's star count next to the GitHub icon."
3492
3500
  }
3493
3501
  },
3494
3502
  "additionalProperties": false,
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/image.tsx","../src/components/nav-link.tsx","../src/utils/extract-video-id.ts","../src/components/video-with-preview.tsx","../src/components/inline-embed.tsx"],"names":["jsx","useEffect"],"mappings":"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","file":"chunk-36QHCIPM.js","sourcesContent":["import type React from \"react\";\nimport { cn } from \"../primitives/lib/utils\";\n\nexport interface ImageAttributesInput {\n src: string;\n alt: string;\n sizes?: string;\n /** Above-the-fold image (e.g. LCP): fetched eagerly at high priority. */\n priority: boolean;\n}\n\n/**\n * Lets a host swap an image's `src`/`srcSet` for its own optimizer's URLs\n * (e.g. Next.js's `getImageProps`) without this package depending on the\n * host framework. Return `null` to keep the original URL.\n */\nexport type ImageAttributesResolver = (\n input: ImageAttributesInput,\n) => React.ImgHTMLAttributes<HTMLImageElement> | null;\n\nlet imageAttributesResolver: ImageAttributesResolver | null = null;\n\n/**\n * Registers the host's resolver. Call once at server start-up; renders that\n * run without one emit the original URL.\n */\nexport function setImageAttributesResolver(resolver: ImageAttributesResolver | null) {\n imageAttributesResolver = resolver;\n}\n\nexport type ImageObjectFit = \"cover\" | \"contain\" | \"fill\" | \"none\" | \"scale-down\";\n\nexport const roundedMap: Record<number, string> = {\n 0: \"rounded-none\",\n 1: \"rounded-sm\",\n 2: \"rounded\",\n 3: \"rounded-md\",\n 4: \"rounded-lg\",\n 5: \"rounded-xl\",\n 6: \"rounded-2xl\",\n 7: \"rounded-3xl\",\n 8: \"rounded-full\",\n};\n\ninterface ImgProps {\n src: string;\n classes?: string;\n alt?: string;\n defaultImage?: string;\n loading?: \"eager\" | \"lazy\";\n /** Marks the image as likely LCP: eager load with high fetch priority. */\n priority?: boolean;\n height?: string;\n sizes?: string;\n width?: string;\n borderRadius?: number | string;\n noDefaultImage?: boolean;\n className?: string;\n objectFit?: ImageObjectFit;\n}\n\nconst objectFitClass: Record<ImageObjectFit, string> = {\n cover: \"object-cover\",\n contain: \"object-contain\",\n fill: \"object-fill\",\n none: \"object-none\",\n \"scale-down\": \"object-scale-down\",\n};\n\nexport const Image = (props: ImgProps) => {\n const {\n src,\n alt = \"\",\n defaultImage,\n priority = false,\n loading = priority ? \"eager\" : \"lazy\",\n sizes,\n noDefaultImage = false,\n className = \"\",\n classes = \"\",\n width = \"w-full\",\n height = \"h-auto\",\n objectFit = \"cover\",\n borderRadius,\n } = props;\n const source = src || defaultImage;\n\n const radiusClass =\n typeof borderRadius === \"number\"\n ? (roundedMap[borderRadius] ?? \"rounded\")\n : borderRadius;\n\n if (!source) {\n if (noDefaultImage) return null;\n return (\n <div\n className={cn(\n \"relative aspect-video overflow-hidden bg-muted\",\n width,\n height,\n radiusClass,\n classes,\n className,\n )}\n />\n );\n }\n\n const optimized = imageAttributesResolver?.({ src: source, alt, sizes, priority });\n\n return (\n <div\n className={cn(\n \"relative aspect-video overflow-hidden\",\n width,\n height,\n radiusClass,\n classes,\n className,\n )}\n >\n <img\n src={source}\n alt={alt}\n sizes={sizes}\n loading={loading}\n decoding=\"async\"\n fetchPriority={priority ? \"high\" : undefined}\n {...optimized}\n className={cn(\n \"absolute inset-0 h-full w-full\",\n radiusClass,\n objectFitClass[objectFit],\n )}\n />\n </div>\n );\n};\n","\"use client\";\n\nimport * as React from \"react\";\n\nexport interface PageBuilderLinkProps {\n href: string;\n children?: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n onClick?: React.MouseEventHandler<HTMLAnchorElement>;\n target?: string;\n rel?: string;\n}\n\n/**\n * Optional host-supplied link. Next.js apps can pass `next/link`; Vite / Remix\n * / plain React hosts can omit it and NavLink will render a native `<a>`.\n */\nexport type PageBuilderLinkComponent = React.ComponentType<PageBuilderLinkProps>;\n\n// Server-rendered pages load this module more than once (each client island is\n// bundled on its own), so the context is shared through the global registry\n// rather than created per copy. Otherwise a provider from one copy would be\n// invisible to a NavLink from another.\nconst LINK_CONTEXT_KEY = Symbol.for(\"@frontlit/page-builder/link-context\");\nconst globalRegistry = globalThis as typeof globalThis & {\n [LINK_CONTEXT_KEY]?: React.Context<PageBuilderLinkComponent | null>;\n};\nconst PageBuilderLinkContext = (globalRegistry[LINK_CONTEXT_KEY] ??=\n React.createContext<PageBuilderLinkComponent | null>(null));\n\nexport function PageBuilderLinkProvider({\n component,\n children,\n}: {\n component?: PageBuilderLinkComponent | null;\n children: React.ReactNode;\n}) {\n return (\n <PageBuilderLinkContext.Provider value={component ?? null}>\n {children}\n </PageBuilderLinkContext.Provider>\n );\n}\n\nexport function usePageBuilderLinkComponent() {\n return React.useContext(PageBuilderLinkContext);\n}\n\ninterface NavLinkProps {\n href: string;\n children?: React.ReactNode;\n openInSameTab?: boolean;\n style?: React.CSSProperties;\n className?: string;\n onClick?: () => void;\n}\n\n/**\n * A navigation-aware link used inside block widgets. In-page anchors scroll\n * to the target. Internal paths (`/...`) use the host `linkComponent` when\n * one is provided (e.g. Next.js `Link`); everything else is a native `<a>`.\n */\nexport function NavLink({\n href,\n children,\n openInSameTab = true,\n style,\n className = \"\",\n onClick,\n}: NavLinkProps) {\n const LinkComponent = usePageBuilderLinkComponent();\n const isInternal = Boolean(href && href.startsWith(\"/\"));\n const isInPageNavigation = Boolean(href && href.startsWith(\"#\"));\n const target = openInSameTab ? undefined : \"_blank\";\n const rel = target === \"_blank\" ? \"noopener noreferrer\" : undefined;\n const handleClick: React.MouseEventHandler<HTMLAnchorElement> = () => {\n onClick?.();\n };\n\n if (!isInPageNavigation && isInternal && LinkComponent) {\n return (\n <LinkComponent\n href={href}\n style={style}\n className={className}\n onClick={handleClick}\n target={target}\n rel={rel}\n >\n {children}\n </LinkComponent>\n );\n }\n\n return (\n <a\n href={href}\n style={style}\n className={className}\n target={target}\n rel={rel}\n onClick={handleClick}\n >\n {children}\n </a>\n );\n}\n","export type SupportedVideoPlatform = \"youtube\" | \"vimeo\";\n\nconst YOUTUBE_HOSTNAMES = new Set([\n \"youtube.com\",\n \"www.youtube.com\",\n \"m.youtube.com\",\n \"music.youtube.com\",\n \"youtube-nocookie.com\",\n \"www.youtube-nocookie.com\",\n \"youtu.be\",\n]);\n\nconst VIMEO_HOSTNAMES = new Set([\"vimeo.com\", \"www.vimeo.com\", \"player.vimeo.com\"]);\n\nconst isNumeric = (value: string) => /^\\d+$/.test(value);\n\nconst extractYouTubeVideoId = (parsedUrl: URL) => {\n const hostname = parsedUrl.hostname.toLowerCase();\n\n if (!YOUTUBE_HOSTNAMES.has(hostname)) {\n return null;\n }\n\n if (hostname === \"youtu.be\") {\n return parsedUrl.pathname.split(\"/\").filter(Boolean)[0] || null;\n }\n\n if (parsedUrl.pathname === \"/watch\") {\n return parsedUrl.searchParams.get(\"v\");\n }\n\n const pathParts = parsedUrl.pathname.split(\"/\").filter(Boolean);\n const firstPart = pathParts[0];\n const secondPart = pathParts[1];\n\n if (\n (firstPart === \"embed\" ||\n firstPart === \"v\" ||\n firstPart === \"shorts\" ||\n firstPart === \"live\") &&\n secondPart\n ) {\n return secondPart;\n }\n\n return null;\n};\n\nconst extractVimeoVideoId = (parsedUrl: URL) => {\n const hostname = parsedUrl.hostname.toLowerCase();\n\n if (!VIMEO_HOSTNAMES.has(hostname)) {\n return null;\n }\n\n const pathParts = parsedUrl.pathname.split(\"/\").filter(Boolean);\n\n for (let index = pathParts.length - 1; index >= 0; index--) {\n if (isNumeric(pathParts[index])) {\n return pathParts[index];\n }\n }\n\n return null;\n};\n\nexport function extractVideoId(url?: string, platform?: SupportedVideoPlatform) {\n if (!url) {\n return null;\n }\n\n const normalizedUrl = /^[a-z][a-z\\d+\\-.]*:\\/\\//i.test(url) ? url : `https://${url}`;\n\n try {\n const parsedUrl = new URL(normalizedUrl);\n\n if (platform === \"youtube\") {\n return extractYouTubeVideoId(parsedUrl);\n }\n\n if (platform === \"vimeo\") {\n return extractVimeoVideoId(parsedUrl);\n }\n\n return extractYouTubeVideoId(parsedUrl) || extractVimeoVideoId(parsedUrl);\n } catch {\n return null;\n }\n}\n","import { useState, useEffect } from \"react\";\nimport { Play, X } from \"lucide-react\";\nimport { extractVideoId } from \"../utils\";\n\ntype VideoType = \"youtube\" | \"vimeo\" | \"self-hosted\";\n\nexport type AspectRatio =\n | \"16/9\" // Widescreen\n | \"4/3\" // Standard\n | \"1/1\" // Square\n | \"21/9\" // Ultrawide\n | \"9/16\" // Vertical\n | \"2/1\" // Panoramic\n | \"3/2\" // Classic\n | string; // Allow custom aspect ratios\n\nexport interface VideoThumbnailProps {\n title?: string;\n thumbnailUrl?: string;\n videoUrl: string;\n aspectRatio?: AspectRatio;\n modal?: boolean;\n}\n\nexport function VideoWithPreview({\n title = \"Video\",\n thumbnailUrl,\n videoUrl = \"https://www.youtube.com/watch?v=7OP2bU9RWVE\",\n aspectRatio = \"16/9\",\n modal = false,\n}: VideoThumbnailProps) {\n const [isModalOpen, setIsModalOpen] = useState(false);\n const [isPlaying, setIsPlaying] = useState(false);\n const [vimeoThumbnail, setVimeoThumbnail] = useState<string | null>(null);\n\n const detectVideoType = (): VideoType => {\n if (extractVideoId(videoUrl, \"youtube\")) {\n return \"youtube\";\n }\n\n if (extractVideoId(videoUrl, \"vimeo\")) {\n return \"vimeo\";\n }\n\n return \"self-hosted\";\n };\n\n const videoType = detectVideoType();\n\n const getVideoId = () => {\n if (videoType === \"youtube\") {\n return extractVideoId(videoUrl, \"youtube\");\n }\n\n if (videoType === \"vimeo\") {\n return extractVideoId(videoUrl, \"vimeo\");\n }\n\n return null;\n };\n\n // Fetch Vimeo thumbnail if needed\n useEffect(() => {\n const fetchVimeoThumbnail = async () => {\n // Skip fetching if a thumbnail URL is already provided\n if (thumbnailUrl || videoType !== \"vimeo\") return;\n\n const videoId = getVideoId();\n if (!videoId) return;\n\n try {\n // Use Vimeo's oEmbed API to get thumbnail (no token required)\n const response = await fetch(\n `https://vimeo.com/api/oembed.json?url=https://vimeo.com/${videoId}`,\n );\n\n if (response.ok) {\n const data = await response.json();\n setVimeoThumbnail(data.thumbnail_url);\n }\n } catch (error) {\n console.error(\"Error fetching Vimeo thumbnail:\", error);\n }\n };\n\n fetchVimeoThumbnail();\n }, [videoUrl, videoType, thumbnailUrl]);\n\n // Get the appropriate thumbnail URL\n const getThumbnailUrl = () => {\n // If a thumbnail URL is explicitly provided, use that\n if (thumbnailUrl) return thumbnailUrl;\n\n // For YouTube videos, generate thumbnail URL\n if (videoType === \"youtube\") {\n const videoId = getVideoId();\n if (videoId) {\n return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;\n }\n }\n\n // For Vimeo videos, use the fetched thumbnail\n if (videoType === \"vimeo\" && vimeoThumbnail) {\n return vimeoThumbnail;\n }\n\n // Fall back to placeholder\n return \"/placeholder.svg?height=480&width=854\";\n };\n\n // Lock body scroll when modal is open\n useEffect(() => {\n if (isModalOpen) {\n document.body.style.overflow = \"hidden\";\n } else {\n document.body.style.overflow = \"\";\n }\n\n return () => {\n document.body.style.overflow = \"\";\n };\n }, [isModalOpen]);\n\n // Get embed URL based on video type\n const getEmbedUrl = () => {\n const videoId = getVideoId();\n // Add autoplay parameter for in-place videos\n const shouldAutoplay = isPlaying;\n\n if (videoType === \"youtube\") {\n if (videoId) {\n return `https://www.youtube.com/embed/${videoId}${shouldAutoplay ? \"?autoplay=1\" : \"\"}`;\n }\n\n // If we couldn't extract the ID but it's a YouTube URL, try to use it directly\n const hasParams = videoUrl.includes(\"?\");\n return (\n videoUrl +\n (shouldAutoplay && !hasParams\n ? \"?autoplay=1\"\n : shouldAutoplay && hasParams\n ? \"&autoplay=1\"\n : \"\")\n );\n }\n\n if (videoType === \"vimeo\" && videoId) {\n return `https://player.vimeo.com/video/${videoId}${shouldAutoplay ? \"?autoplay=1\" : \"\"}`;\n }\n\n // For self-hosted videos, return the URL as is\n return videoUrl;\n };\n\n // Calculate aspect ratio style\n const aspectRatioStyle = () => {\n if (!aspectRatio.includes(\"/\")) return {};\n\n const [width, height] = aspectRatio.split(\"/\").map(Number);\n if (!width || !height) return {};\n\n return {\n aspectRatio: `${width}/${height}`,\n };\n };\n\n // Handle thumbnail click\n const handleThumbnailClick = () => {\n if (modal) {\n setIsModalOpen(true);\n } else {\n setIsPlaying(true);\n }\n };\n\n return (\n <div className={aspectRatio === \"9/16\" ? \"max-w-xs mx-auto\" : \"\"}>\n {/* Thumbnail with play button or in-place video */}\n <div className=\"relative overflow-hidden\" style={aspectRatioStyle()}>\n {!modal && isPlaying ? (\n // In-place video player\n <div className=\"w-full h-full\">\n {videoType !== \"self-hosted\" ? (\n <iframe\n src={getEmbedUrl()}\n className=\"w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n ></iframe>\n ) : (\n <video\n src={videoUrl}\n controls\n controlsList=\"nodownload\" // eslint-disable-line react/no-unknown-property\n onContextMenu={(e) => e.preventDefault()}\n autoPlay\n className=\"w-full h-full\"\n >\n Your browser does not support the video tag.\n </video>\n )}\n </div>\n ) : (\n // Thumbnail with play button\n <div\n className=\"cursor-pointer group w-full h-full\"\n onClick={handleThumbnailClick}\n >\n <img\n src={getThumbnailUrl() || \"/placeholder.svg\"}\n alt={`Thumbnail for ${title}`}\n className=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\"\n />\n <div className=\"absolute inset-0 bg-black/30 flex items-center justify-center\">\n <div className=\"relative\">\n {/* Smaller grayish concentric wave animation */}\n <span className=\"absolute -inset-1.5 rounded-full bg-gray-200/50 animate-pulse duration-[3000ms]\"></span>\n\n {/* Play button */}\n <div className=\"relative z-1 rounded-full bg-primary p-4 text-primary-foreground\">\n <Play className=\"h-8 w-8\" fill=\"currentColor\" />\n </div>\n </div>\n </div>\n </div>\n )}\n </div>\n\n {/* Custom Video Modal (only used when modal is true) */}\n {modal && isModalOpen && (\n <div className=\"fixed inset-0 bg-black/95 z-50 flex items-center justify-center p-4 sm:p-6 md:p-10\">\n <button\n onClick={() => setIsModalOpen(false)}\n className=\"absolute top-2 right-2 sm:top-4 sm:right-4 text-white hover:text-gray-300 focus:outline-none z-10\"\n aria-label=\"Close\"\n >\n <X className=\"h-6 w-6 sm:h-8 sm:w-8\" />\n </button>\n\n <div\n className=\"w-full max-w-xl sm:max-w-2xl md:max-w-3xl\"\n style={aspectRatioStyle()}\n >\n {videoType !== \"self-hosted\" ? (\n // YouTube or Vimeo video\n <iframe\n src={getEmbedUrl()}\n className=\"w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n ></iframe>\n ) : (\n // Self-hosted video\n <video\n src={videoUrl}\n controls\n controlsList=\"nodownload\" // eslint-disable-line react/no-unknown-property\n onContextMenu={(e) => e.preventDefault()}\n autoPlay\n className=\"w-full h-full\"\n >\n Your browser does not support the video tag.\n </video>\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n","\"use client\";\n\nimport type React from \"react\";\nimport { useEffect, useRef } from \"react\";\n\ninterface InlineEmbedProps {\n content: string;\n className?: string;\n style?: React.CSSProperties;\n}\n\n// Font stylesheets and connection hints only work at document level (a\n// shadow root ignores @font-face), so these links are hoisted out of it.\nconst HOISTED_LINK =\n /<link\\b[^>]*(?:rel=[\"']?(?:preconnect|dns-prefetch|preload)|href=[\"'][^\"']*fonts?\\.)[^>]*>/gi;\n\n/**\n * Snippets are usually written as a standalone document (they used to live in\n * an iframe). Recreate that inside the shadow root: the host plays `html` and\n * is a size container, a wrapper plays `body`, and viewport units and width\n * media queries are rewritten to measure the embed (as they measured the\n * iframe) instead of the browser window. \"Fill the frame\" heights are dropped,\n * as are scripts that only synced state with `window.parent`, which the shadow\n * host handles itself.\n */\nexport function prepareEmbed(content: string) {\n const links = content.match(HOISTED_LINK)?.join(\"\") ?? \"\";\n const markup = content\n .replace(HOISTED_LINK, \"\")\n .replace(/<!doctype[^>]*>|<\\/?(?:html|head|body)\\b[^>]*>/gi, \"\")\n .replace(\n /<script\\b[^>]*>(?:(?!<\\/script>)[\\s\\S])*window\\.parent[\\s\\S]*?<\\/script>/gi,\n \"\",\n )\n .replace(\n /<style\\b([^>]*)>([\\s\\S]*?)<\\/style>/gi,\n (_, attrs: string, css: string) => {\n const scoped = css\n .replace(/:root\\b/g, \":host\")\n .replace(/(^|[\\s,{}])html\\.dark\\b/g, \"$1:host(.dark)\")\n .replace(/(^|[\\s,{}])html(?=[\\s,{:.[>~+])/g, \"$1:host\")\n .replace(/(^|[\\s,{}])body(?=[\\s,{:.[>~+])/g, \"$1[data-embed-body]\")\n .replace(/min-height\\s*:\\s*100d?vh/gi, \"min-height:0\")\n .replace(/(\\d*\\.?\\d+)vw\\b/gi, \"$1cqw\")\n .replace(\n /@media\\s+(?:only\\s+)?(?:screen\\s+and\\s+)?(\\([^{]*\\bwidth\\b[^{]*)\\{/gi,\n \"@container $1{\",\n );\n return `<style${attrs}>${scoped}</style>`;\n },\n );\n\n const body = `<style>:host{display:block;container-type:inline-size;overflow-x:clip}</style><div data-embed-body>${markup}</div>`;\n return { links, body };\n}\n\nfunction runScripts(root: ShadowRoot) {\n for (const old of root.querySelectorAll(\"script\")) {\n const script = document.createElement(\"script\");\n for (const { name, value } of old.attributes) script.setAttribute(name, value);\n script.textContent = old.textContent;\n old.replaceWith(script);\n }\n}\n\n/**\n * Renders a raw HTML snippet inline, inside a declarative shadow root. The\n * markup is part of the server-rendered page (so crawlers index it) while its\n * styles stay isolated from the site's in both directions.\n */\nexport const InlineEmbed = ({ content, className, style }: InlineEmbedProps) => {\n const hostRef = useRef<HTMLDivElement>(null);\n const appliedRef = useRef<string | null>(null);\n const { links, body } = prepareEmbed(content);\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n\n // A server-rendered host already has its shadow root from the HTML\n // parser; a client-rendered one (editor, client navigation) does not,\n // because innerHTML ignores declarative shadow roots.\n const root = host.shadowRoot ?? host.attachShadow({ mode: \"open\" });\n if (appliedRef.current === body) return;\n // The HTML parser already ran the scripts of a server-rendered shadow\n // root; only markup set through innerHTML (which never runs scripts)\n // needs them re-created, or they'd run twice.\n if (appliedRef.current !== null || !root.hasChildNodes()) {\n root.innerHTML = body;\n runScripts(root);\n }\n appliedRef.current = body;\n }, [body]);\n\n // Mirror the site's color mode onto the host so snippets can use\n // `:host(.dark)`.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const sync = () =>\n host.classList.toggle(\n \"dark\",\n !!host.parentElement?.closest(\".dark\") ||\n document.documentElement.classList.contains(\"dark\"),\n );\n sync();\n const observer = new MutationObserver(sync);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n subtree: true,\n });\n return () => observer.disconnect();\n }, []);\n\n return (\n <div\n ref={hostRef}\n className={className}\n style={style}\n suppressHydrationWarning\n dangerouslySetInnerHTML={{\n // Only the HTML parser turns the template into a shadow root; on the\n // client the effect attaches it instead.\n __html:\n typeof window === \"undefined\"\n ? `${links}<template shadowrootmode=\"open\">${body}</template>`\n : links,\n }}\n />\n );\n};\n"]}