@opensite/ui 3.10.0 → 3.11.2

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.
@@ -2,11 +2,9 @@
2
2
  'use strict';
3
3
 
4
4
  var React = require('react');
5
+ var mediaImmersive = require('@page-speed/media-immersive');
5
6
  var clsx = require('clsx');
6
7
  var tailwindMerge = require('tailwind-merge');
7
- var img = require('@page-speed/img');
8
- var video = require('@page-speed/video');
9
- var pressable = require('@page-speed/pressable');
10
8
  var icon = require('@page-speed/icon');
11
9
  var jsxRuntime = require('react/jsx-runtime');
12
10
 
@@ -423,23 +421,323 @@ var Section = React__namespace.default.forwardRef(
423
421
  }
424
422
  );
425
423
  Section.displayName = "Section";
426
- function EngagementBadge({
424
+ var IG_TILE_WIDTH = 320;
425
+ var IG_TILE_STYLE = { width: "100%" };
426
+ var IG_TILE_RADIUS = 18;
427
+ var TILE_WRAPPER_STYLE = {
428
+ position: "relative",
429
+ borderRadius: IG_TILE_RADIUS,
430
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08), 0 6px 16px rgba(15, 23, 42, 0.10)"
431
+ };
432
+ var CAPTION_SCRIM_STYLE = {
433
+ position: "absolute",
434
+ left: 0,
435
+ right: 0,
436
+ bottom: 0,
437
+ padding: "28px 14px 13px",
438
+ background: "linear-gradient(180deg, rgba(8,12,24,0) 0%, rgba(8,12,24,0.55) 45%, rgba(8,12,24,0.78) 100%)",
439
+ borderBottomLeftRadius: IG_TILE_RADIUS,
440
+ borderBottomRightRadius: IG_TILE_RADIUS,
441
+ pointerEvents: "none"
442
+ };
443
+ var CAPTION_TEXT_STYLE = {
444
+ color: "#fff",
445
+ fontSize: 13,
446
+ lineHeight: 1.4,
447
+ fontWeight: 600,
448
+ letterSpacing: "0.005em",
449
+ textShadow: "0 1px 2px rgba(0, 0, 0, 0.35)",
450
+ display: "-webkit-box",
451
+ WebkitLineClamp: 2,
452
+ WebkitBoxOrient: "vertical",
453
+ overflow: "hidden",
454
+ height: "2.8em"
455
+ };
456
+ var TITLE_MAX = 90;
457
+ function truncate(text, max = TITLE_MAX) {
458
+ const trimmed = text.trim();
459
+ if (trimmed.length <= max) return trimmed;
460
+ const slice = trimmed.slice(0, max);
461
+ const lastSpace = slice.lastIndexOf(" ");
462
+ const head = lastSpace > max * 0.6 ? slice.slice(0, lastSpace) : slice;
463
+ return `${head.trimEnd()}\u2026`;
464
+ }
465
+ function captionToString(caption) {
466
+ return typeof caption === "string" ? caption : "";
467
+ }
468
+ function toMediaItem(item) {
469
+ const isVideo = Boolean(item.isVideo && item.videoUrl);
470
+ const fullCaption = captionToString(item.caption);
471
+ const title = truncate(fullCaption) || item.imageAlt || "Instagram post";
472
+ const meta = {
473
+ href: item.href,
474
+ likeCount: item.likeCount,
475
+ commentCount: item.commentCount,
476
+ viewCount: item.viewCount,
477
+ date: item.date
478
+ };
479
+ return {
480
+ id: item.id,
481
+ type: isVideo ? "video" : "image",
482
+ poster: item.image,
483
+ // Video source only when the post is a real video (image posts ignore it).
484
+ src: isVideo ? item.videoUrl : void 0,
485
+ title,
486
+ caption: fullCaption || void 0,
487
+ kind: "Instagram",
488
+ meta
489
+ };
490
+ }
491
+ function openPermalink(item) {
492
+ if (typeof window === "undefined") return;
493
+ const href = item.meta?.href;
494
+ if (!href) return;
495
+ window.open(href, "_blank", "noopener,noreferrer");
496
+ }
497
+ function likeBadge(likeCount) {
498
+ return /* @__PURE__ */ jsxRuntime.jsxs(
499
+ "span",
500
+ {
501
+ "aria-label": `${likeCount.toLocaleString()} likes`,
502
+ style: {
503
+ display: "inline-flex",
504
+ alignItems: "center",
505
+ gap: 6,
506
+ // Breathing room from the tile's top-left corner (refinement #1).
507
+ margin: 6,
508
+ padding: "6px 11px",
509
+ borderRadius: 999,
510
+ background: "rgba(8, 12, 24, 0.55)",
511
+ backdropFilter: "blur(8px)",
512
+ WebkitBackdropFilter: "blur(8px)",
513
+ border: "1px solid rgba(255, 255, 255, 0.14)",
514
+ color: "#fff",
515
+ fontSize: 13,
516
+ fontWeight: 600,
517
+ lineHeight: 1,
518
+ letterSpacing: "0.01em"
519
+ },
520
+ children: [
521
+ /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/heart", size: 15, "aria-hidden": "true" }),
522
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: likeCount.toLocaleString() })
523
+ ]
524
+ }
525
+ );
526
+ }
527
+ var VIEWER_ACTIONS = [
528
+ {
529
+ id: "open-in-instagram",
530
+ icon: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/external-link", size: 22, "aria-hidden": "true" }),
531
+ label: "Instagram",
532
+ ariaLabel: "Open in Instagram",
533
+ onPress: (item) => openPermalink(item)
534
+ }
535
+ ];
536
+ function RailStat({
427
537
  iconName,
428
538
  count,
429
- label
539
+ label,
540
+ onPress
430
541
  }) {
431
542
  return /* @__PURE__ */ jsxRuntime.jsxs(
432
- "span",
543
+ "button",
433
544
  {
434
- className: "inline-flex items-center gap-1",
545
+ type: "button",
435
546
  "aria-label": `${count.toLocaleString()} ${label}`,
547
+ onClick: (e) => {
548
+ e.stopPropagation();
549
+ onPress();
550
+ },
551
+ style: {
552
+ display: "flex",
553
+ flexDirection: "column",
554
+ alignItems: "center",
555
+ gap: 5,
556
+ padding: 0,
557
+ border: "none",
558
+ background: "transparent",
559
+ color: "inherit",
560
+ cursor: "pointer",
561
+ font: "inherit"
562
+ },
563
+ children: [
564
+ /* @__PURE__ */ jsxRuntime.jsx(
565
+ "span",
566
+ {
567
+ style: {
568
+ width: 46,
569
+ height: 46,
570
+ borderRadius: "50%",
571
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
572
+ backdropFilter: "blur(6px)",
573
+ WebkitBackdropFilter: "blur(6px)",
574
+ display: "flex",
575
+ alignItems: "center",
576
+ justifyContent: "center"
577
+ },
578
+ children: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: iconName, size: 22, "aria-hidden": "true" })
579
+ }
580
+ ),
581
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: count.toLocaleString() })
582
+ ]
583
+ }
584
+ );
585
+ }
586
+ function InstagramViewerRail({ item }) {
587
+ const meta = item.meta ?? {};
588
+ const open = () => openPermalink(item);
589
+ return /* @__PURE__ */ jsxRuntime.jsxs(
590
+ "div",
591
+ {
592
+ style: {
593
+ position: "absolute",
594
+ right: 11,
595
+ bottom: 135,
596
+ display: "flex",
597
+ flexDirection: "column",
598
+ alignItems: "center",
599
+ gap: 18,
600
+ color: "var(--psmi-chrome-fg, #fff)",
601
+ zIndex: 3
602
+ },
436
603
  children: [
437
- /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: iconName, size: 16, "aria-hidden": "true" }),
438
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: count.toLocaleString() })
604
+ typeof meta.likeCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
605
+ RailStat,
606
+ {
607
+ iconName: "lucide/heart",
608
+ count: meta.likeCount,
609
+ label: "likes",
610
+ onPress: open
611
+ }
612
+ ) : null,
613
+ typeof meta.commentCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
614
+ RailStat,
615
+ {
616
+ iconName: "lucide/message-circle",
617
+ count: meta.commentCount,
618
+ label: "comments",
619
+ onPress: open
620
+ }
621
+ ) : null,
622
+ typeof meta.viewCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
623
+ RailStat,
624
+ {
625
+ iconName: "lucide/eye",
626
+ count: meta.viewCount,
627
+ label: "views",
628
+ onPress: open
629
+ }
630
+ ) : null,
631
+ meta.href ? /* @__PURE__ */ jsxRuntime.jsxs(
632
+ "a",
633
+ {
634
+ href: meta.href,
635
+ target: "_blank",
636
+ rel: "noopener noreferrer",
637
+ "aria-label": "Open in Instagram",
638
+ onClick: (e) => e.stopPropagation(),
639
+ style: {
640
+ display: "flex",
641
+ flexDirection: "column",
642
+ alignItems: "center",
643
+ gap: 5,
644
+ padding: 0,
645
+ border: "none",
646
+ background: "transparent",
647
+ color: "inherit",
648
+ cursor: "pointer",
649
+ font: "inherit",
650
+ textDecoration: "none"
651
+ },
652
+ children: [
653
+ /* @__PURE__ */ jsxRuntime.jsx(
654
+ "span",
655
+ {
656
+ style: {
657
+ width: 46,
658
+ height: 46,
659
+ borderRadius: "50%",
660
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
661
+ backdropFilter: "blur(6px)",
662
+ WebkitBackdropFilter: "blur(6px)",
663
+ display: "flex",
664
+ alignItems: "center",
665
+ justifyContent: "center"
666
+ },
667
+ children: /* @__PURE__ */ jsxRuntime.jsx(
668
+ DynamicIcon,
669
+ {
670
+ name: "lucide/external-link",
671
+ size: 22,
672
+ "aria-hidden": "true"
673
+ }
674
+ )
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: "Instagram" })
678
+ ]
679
+ }
680
+ ) : null
439
681
  ]
440
682
  }
441
683
  );
442
684
  }
685
+ function InstagramFeedGrid({
686
+ mediaItems,
687
+ gridClassName,
688
+ itemClassName,
689
+ imageClassName,
690
+ optixFlowConfig
691
+ }) {
692
+ const { open } = mediaImmersive.useImmersiveFeed();
693
+ const posterImgProps = React.useMemo(() => {
694
+ const next = {};
695
+ if (imageClassName) next.className = imageClassName;
696
+ if (optixFlowConfig) next.optixFlowConfig = optixFlowConfig;
697
+ return Object.keys(next).length > 0 ? next : void 0;
698
+ }, [imageClassName, optixFlowConfig]);
699
+ return /* @__PURE__ */ jsxRuntime.jsx(
700
+ "div",
701
+ {
702
+ className: cn(
703
+ "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
704
+ gridClassName
705
+ ),
706
+ children: mediaItems.map((mediaItem) => {
707
+ const likeCount = mediaItem.meta?.likeCount;
708
+ return (
709
+ // The wrapper owns the tile's outer chrome (subtle boundary shadow,
710
+ // matching radius) and the block's own caption overlay — the card's
711
+ // built-in caption is hidden so 1- and 2-line captions can share a
712
+ // fixed-height, top-aligned text box (annotated refinements #2–#4).
713
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: itemClassName, style: TILE_WRAPPER_STYLE, children: [
714
+ /* @__PURE__ */ jsxRuntime.jsx(
715
+ mediaImmersive.ThumbnailCard,
716
+ {
717
+ item: mediaItem,
718
+ onOpen: open,
719
+ size: IG_TILE_WIDTH,
720
+ style: IG_TILE_STYLE,
721
+ elevated: false,
722
+ hideCaption: true,
723
+ hideProgressHint: true,
724
+ showDuration: false,
725
+ glyphMode: "hover",
726
+ posterImgProps,
727
+ badgeSlot: typeof likeCount === "number" ? likeBadge(likeCount) : void 0
728
+ }
729
+ ),
730
+ mediaItem.title ? (
731
+ // aria-hidden: the card already announces the post title; this
732
+ // overlay is purely visual. pointer-events pass through to the card.
733
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: CAPTION_SCRIM_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: CAPTION_TEXT_STYLE, children: mediaItem.title }) })
734
+ ) : null
735
+ ] }, mediaItem.id)
736
+ );
737
+ })
738
+ }
739
+ );
740
+ }
443
741
  function InstagramPostGrid({
444
742
  sectionId = "instagram-post-grid",
445
743
  heading,
@@ -447,118 +745,32 @@ function InstagramPostGrid({
447
745
  items,
448
746
  itemsSlot,
449
747
  className,
450
- containerClassName,
748
+ containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
749
+ spacing = "py-12 md:py-32",
451
750
  headerClassName,
452
751
  headingClassName,
453
752
  subheadingClassName,
454
753
  gridClassName,
455
754
  itemClassName,
456
755
  imageClassName,
756
+ optixFlowConfig,
457
757
  background,
458
- spacing,
459
758
  pattern,
460
759
  patternOpacity,
461
- patternClassName,
462
- optixFlowConfig
760
+ patternClassName
463
761
  }) {
464
- const tiles = React.useMemo(() => {
465
- if (itemsSlot) return itemsSlot;
466
- if (!items || items.length === 0) return null;
467
- const visibleItems = items.filter((item) => Boolean(item.image));
468
- return visibleItems.map((item) => {
469
- const altText = item.imageAlt || (typeof item.caption === "string" ? item.caption : "") || "Instagram post";
470
- const showVideo = Boolean(item.isVideo && item.videoUrl);
471
- return /* @__PURE__ */ jsxRuntime.jsxs(
472
- pressable.Pressable,
473
- {
474
- href: item.href,
475
- "aria-label": typeof item.caption === "string" && item.caption.length > 0 ? item.caption : "View Instagram post",
476
- className: cn(
477
- "group relative block aspect-square overflow-hidden rounded-md bg-muted",
478
- item.className,
479
- itemClassName
480
- ),
481
- children: [
482
- /* @__PURE__ */ jsxRuntime.jsx(
483
- img.Img,
484
- {
485
- src: item.image,
486
- alt: altText,
487
- loading: "lazy",
488
- className: cn(
489
- "h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105 motion-reduce:transform-none motion-reduce:transition-none",
490
- imageClassName
491
- ),
492
- optixFlowConfig
493
- }
494
- ),
495
- showVideo && /* @__PURE__ */ jsxRuntime.jsx(
496
- video.Video,
497
- {
498
- src: item.videoUrl,
499
- poster: item.image,
500
- controls: false,
501
- muted: true,
502
- loop: true,
503
- playsInline: true,
504
- preload: "metadata",
505
- onMouseEnter: (e) => {
506
- void e.currentTarget.play().catch(() => void 0);
507
- },
508
- onMouseLeave: (e) => {
509
- e.currentTarget.pause();
510
- e.currentTarget.currentTime = 0;
511
- },
512
- className: cn(
513
- "absolute inset-0 h-full w-full object-cover object-center opacity-0 transition-opacity duration-300 group-hover:opacity-100 motion-reduce:transition-none",
514
- imageClassName
515
- )
516
- }
517
- ),
518
- item.isVideo && /* @__PURE__ */ jsxRuntime.jsx(
519
- "span",
520
- {
521
- className: "absolute right-2 top-2 text-white drop-shadow-md",
522
- "aria-hidden": "true",
523
- children: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/play", size: 18 })
524
- }
525
- ),
526
- (item.caption || item.date || typeof item.likeCount === "number" || typeof item.commentCount === "number" || typeof item.viewCount === "number") && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-1 bg-linear-to-t from-black/70 to-transparent p-3 text-left text-white opacity-0 transition-opacity duration-300 group-hover:opacity-100 motion-reduce:transition-none", children: [
527
- item.caption && (typeof item.caption === "string" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-xs leading-snug wrap-break-word", children: item.caption }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "line-clamp-2 text-xs leading-snug", children: item.caption })),
528
- (typeof item.likeCount === "number" || typeof item.commentCount === "number" || typeof item.viewCount === "number" || item.date) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-xs font-medium", children: [
529
- typeof item.likeCount === "number" && /* @__PURE__ */ jsxRuntime.jsx(
530
- EngagementBadge,
531
- {
532
- iconName: "lucide/heart",
533
- count: item.likeCount,
534
- label: "likes"
535
- }
536
- ),
537
- typeof item.commentCount === "number" && /* @__PURE__ */ jsxRuntime.jsx(
538
- EngagementBadge,
539
- {
540
- iconName: "lucide/message-circle",
541
- count: item.commentCount,
542
- label: "comments"
543
- }
544
- ),
545
- typeof item.viewCount === "number" && /* @__PURE__ */ jsxRuntime.jsx(
546
- EngagementBadge,
547
- {
548
- iconName: "lucide/eye",
549
- count: item.viewCount,
550
- label: "views"
551
- }
552
- ),
553
- item.date && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto opacity-80", children: item.date })
554
- ] })
555
- ] })
556
- ]
557
- },
558
- item.id
762
+ const mediaItems = React.useMemo(() => {
763
+ if (itemsSlot || !items) return [];
764
+ const withImage = items.filter((item) => Boolean(item.image));
765
+ if (process.env.NODE_ENV !== "production" && withImage.length < items.length) {
766
+ const skipped = items.filter((item) => !item.image);
767
+ const skippedIds = skipped.map((item) => item.id).join(", ");
768
+ console.warn(
769
+ `[instagram-post-grid] skipped ${skipped.length} item(s) without a resolvable image: ${skippedIds}`
559
770
  );
560
- });
561
- }, [itemsSlot, items, itemClassName, imageClassName, optixFlowConfig]);
771
+ }
772
+ return withImage.map(toMediaItem);
773
+ }, [items, itemsSlot]);
562
774
  if (!itemsSlot && (!items || items.length === 0)) {
563
775
  return null;
564
776
  }
@@ -596,16 +808,34 @@ function InstagramPostGrid({
596
808
  }
597
809
  ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: subheadingClassName, children: subheading }))
598
810
  ] }),
599
- /* @__PURE__ */ jsxRuntime.jsx(
811
+ itemsSlot ? /* @__PURE__ */ jsxRuntime.jsx(
600
812
  "div",
601
813
  {
602
814
  className: cn(
603
- "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4",
815
+ "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
604
816
  gridClassName
605
817
  ),
606
- children: tiles
818
+ children: itemsSlot
607
819
  }
608
- )
820
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(mediaImmersive.ImmersiveFeedProvider, { items: mediaItems, actions: VIEWER_ACTIONS, children: [
821
+ /* @__PURE__ */ jsxRuntime.jsx(
822
+ InstagramFeedGrid,
823
+ {
824
+ mediaItems,
825
+ gridClassName,
826
+ itemClassName,
827
+ imageClassName,
828
+ optixFlowConfig
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsxRuntime.jsx(
832
+ mediaImmersive.ImmersiveViewer,
833
+ {
834
+ ariaLabel: "Instagram post viewer",
835
+ renderActions: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(InstagramViewerRail, { item })
836
+ }
837
+ )
838
+ ] })
609
839
  ]
610
840
  }
611
841
  );
@@ -31,7 +31,15 @@ interface InstagramPostItem {
31
31
  */
32
32
  imageAlt?: string;
33
33
  /**
34
- * Post caption (truncated for display)
34
+ * Post caption (truncated for display).
35
+ *
36
+ * NOTE: For immersive rendering the caption must be a **string** — the
37
+ * library's `MediaItem.title`/`caption` are string-typed, so a caption is
38
+ * flowed through as text. A rich `ReactNode` caption (hand-authored markup,
39
+ * only ever used by the legacy pre-immersive block) cannot be threaded into
40
+ * the immersive card/viewer; when supplied it is dropped and the tile/viewer
41
+ * title falls back to `imageAlt` (then `"Instagram post"`). Hydrated feeds
42
+ * always send plain strings, so this only affects hand-authored usage.
35
43
  */
36
44
  caption?: React.ReactNode;
37
45
  /**
@@ -140,17 +148,32 @@ interface InstagramPostGridProps {
140
148
  sectionId?: string;
141
149
  }
142
150
  /**
143
- * InstagramPostGrid displays a responsive, square-tiled grid of Instagram posts.
151
+ * InstagramPostGrid displays a website's Instagram feed as a responsive grid of
152
+ * vertical (9:16) tiles that open a fullscreen, swipeable immersive viewer.
144
153
  *
145
- * Each tile links out to the post's instagram.com permalink in a new tab. Images
146
- * render via `@page-speed/img`; video posts render an in-grid `@page-speed/video`
147
- * that plays muted on hover (progressive/on-interaction). Captions are truncated,
148
- * an optional date is shown, and like/comment/view badges appear only when the
149
- * corresponding count is present never fabricated. Posts without an image are
150
- * skipped; an empty or missing `items` array renders nothing.
154
+ * Composition is powered by `@page-speed/media-immersive`: each post maps to a
155
+ * `MediaItem` (photos are first-class images, reels are videos), tiles are
156
+ * `ThumbnailCard`s, and tapping one opens `ImmersiveViewer` a TikTok/Reels-
157
+ * style pager. Per the annotated design, each tile shows a like-count pill
158
+ * top-left (only when a numeric like count exists never fabricated), no
159
+ * duration timestamp, and a center glyph on hover only (a play triangle for
160
+ * reels, an expand icon for photos). The fullscreen viewer carries the caption
161
+ * plus a read-only engagement rail (likes/comments/views) and an explicit
162
+ * "Open in Instagram" egress rendered as a real `<a target="_blank"
163
+ * rel="noopener noreferrer">` (a crawlable, affordance-correct link — not a
164
+ * scripted `window.open` button); the old whole-tile link-out is REPLACED by
165
+ * opening the viewer, with the permalink egress living in the viewer.
151
166
  *
152
- * Data is hydrated from the toastability Instagram feed. Media URLs are the
153
- * re-hosted MediaRecord CDN URLs, never expiring Instagram CDN URLs.
167
+ * Consumer image concerns are honored: `imageClassName` and `optixFlowConfig`
168
+ * are forwarded onto each tile's poster image via the library's
169
+ * `posterImgProps` passthrough.
170
+ *
171
+ * Captions should be plain strings when used with immersive rendering; a rich
172
+ * `ReactNode` caption falls back to `imageAlt` (see the `caption` field doc).
173
+ *
174
+ * Posts without an image are skipped; an empty or missing `items` array renders
175
+ * nothing. Data is hydrated from the toastability Instagram feed — media URLs
176
+ * are the re-hosted MediaRecord CDN URLs, never expiring Instagram CDN URLs.
154
177
  *
155
178
  * @example
156
179
  * ```tsx
@@ -169,6 +192,6 @@ interface InstagramPostGridProps {
169
192
  * />
170
193
  * ```
171
194
  */
172
- declare function InstagramPostGrid({ sectionId, heading, subheading, items, itemsSlot, className, containerClassName, headerClassName, headingClassName, subheadingClassName, gridClassName, itemClassName, imageClassName, background, spacing, pattern, patternOpacity, patternClassName, optixFlowConfig, }: InstagramPostGridProps): React.JSX.Element | null;
195
+ declare function InstagramPostGrid({ sectionId, heading, subheading, items, itemsSlot, className, containerClassName, spacing, headerClassName, headingClassName, subheadingClassName, gridClassName, itemClassName, imageClassName, optixFlowConfig, background, pattern, patternOpacity, patternClassName, }: InstagramPostGridProps): React.JSX.Element | null;
173
196
 
174
197
  export { InstagramPostGrid, type InstagramPostGridProps, type InstagramPostItem };
@@ -31,7 +31,15 @@ interface InstagramPostItem {
31
31
  */
32
32
  imageAlt?: string;
33
33
  /**
34
- * Post caption (truncated for display)
34
+ * Post caption (truncated for display).
35
+ *
36
+ * NOTE: For immersive rendering the caption must be a **string** — the
37
+ * library's `MediaItem.title`/`caption` are string-typed, so a caption is
38
+ * flowed through as text. A rich `ReactNode` caption (hand-authored markup,
39
+ * only ever used by the legacy pre-immersive block) cannot be threaded into
40
+ * the immersive card/viewer; when supplied it is dropped and the tile/viewer
41
+ * title falls back to `imageAlt` (then `"Instagram post"`). Hydrated feeds
42
+ * always send plain strings, so this only affects hand-authored usage.
35
43
  */
36
44
  caption?: React.ReactNode;
37
45
  /**
@@ -140,17 +148,32 @@ interface InstagramPostGridProps {
140
148
  sectionId?: string;
141
149
  }
142
150
  /**
143
- * InstagramPostGrid displays a responsive, square-tiled grid of Instagram posts.
151
+ * InstagramPostGrid displays a website's Instagram feed as a responsive grid of
152
+ * vertical (9:16) tiles that open a fullscreen, swipeable immersive viewer.
144
153
  *
145
- * Each tile links out to the post's instagram.com permalink in a new tab. Images
146
- * render via `@page-speed/img`; video posts render an in-grid `@page-speed/video`
147
- * that plays muted on hover (progressive/on-interaction). Captions are truncated,
148
- * an optional date is shown, and like/comment/view badges appear only when the
149
- * corresponding count is present never fabricated. Posts without an image are
150
- * skipped; an empty or missing `items` array renders nothing.
154
+ * Composition is powered by `@page-speed/media-immersive`: each post maps to a
155
+ * `MediaItem` (photos are first-class images, reels are videos), tiles are
156
+ * `ThumbnailCard`s, and tapping one opens `ImmersiveViewer` a TikTok/Reels-
157
+ * style pager. Per the annotated design, each tile shows a like-count pill
158
+ * top-left (only when a numeric like count exists never fabricated), no
159
+ * duration timestamp, and a center glyph on hover only (a play triangle for
160
+ * reels, an expand icon for photos). The fullscreen viewer carries the caption
161
+ * plus a read-only engagement rail (likes/comments/views) and an explicit
162
+ * "Open in Instagram" egress rendered as a real `<a target="_blank"
163
+ * rel="noopener noreferrer">` (a crawlable, affordance-correct link — not a
164
+ * scripted `window.open` button); the old whole-tile link-out is REPLACED by
165
+ * opening the viewer, with the permalink egress living in the viewer.
151
166
  *
152
- * Data is hydrated from the toastability Instagram feed. Media URLs are the
153
- * re-hosted MediaRecord CDN URLs, never expiring Instagram CDN URLs.
167
+ * Consumer image concerns are honored: `imageClassName` and `optixFlowConfig`
168
+ * are forwarded onto each tile's poster image via the library's
169
+ * `posterImgProps` passthrough.
170
+ *
171
+ * Captions should be plain strings when used with immersive rendering; a rich
172
+ * `ReactNode` caption falls back to `imageAlt` (see the `caption` field doc).
173
+ *
174
+ * Posts without an image are skipped; an empty or missing `items` array renders
175
+ * nothing. Data is hydrated from the toastability Instagram feed — media URLs
176
+ * are the re-hosted MediaRecord CDN URLs, never expiring Instagram CDN URLs.
154
177
  *
155
178
  * @example
156
179
  * ```tsx
@@ -169,6 +192,6 @@ interface InstagramPostGridProps {
169
192
  * />
170
193
  * ```
171
194
  */
172
- declare function InstagramPostGrid({ sectionId, heading, subheading, items, itemsSlot, className, containerClassName, headerClassName, headingClassName, subheadingClassName, gridClassName, itemClassName, imageClassName, background, spacing, pattern, patternOpacity, patternClassName, optixFlowConfig, }: InstagramPostGridProps): React.JSX.Element | null;
195
+ declare function InstagramPostGrid({ sectionId, heading, subheading, items, itemsSlot, className, containerClassName, spacing, headerClassName, headingClassName, subheadingClassName, gridClassName, itemClassName, imageClassName, optixFlowConfig, background, pattern, patternOpacity, patternClassName, }: InstagramPostGridProps): React.JSX.Element | null;
173
196
 
174
197
  export { InstagramPostGrid, type InstagramPostGridProps, type InstagramPostItem };