@opensite/ui 3.10.0 → 3.11.0

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,276 @@ 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 TITLE_MAX = 90;
427
+ function truncate(text, max = TITLE_MAX) {
428
+ const trimmed = text.trim();
429
+ if (trimmed.length <= max) return trimmed;
430
+ const slice = trimmed.slice(0, max);
431
+ const lastSpace = slice.lastIndexOf(" ");
432
+ const head = lastSpace > max * 0.6 ? slice.slice(0, lastSpace) : slice;
433
+ return `${head.trimEnd()}\u2026`;
434
+ }
435
+ function captionToString(caption) {
436
+ return typeof caption === "string" ? caption : "";
437
+ }
438
+ function toMediaItem(item) {
439
+ const isVideo = Boolean(item.isVideo && item.videoUrl);
440
+ const fullCaption = captionToString(item.caption);
441
+ const title = truncate(fullCaption) || item.imageAlt || "Instagram post";
442
+ const meta = {
443
+ href: item.href,
444
+ likeCount: item.likeCount,
445
+ commentCount: item.commentCount,
446
+ viewCount: item.viewCount,
447
+ date: item.date
448
+ };
449
+ return {
450
+ id: item.id,
451
+ type: isVideo ? "video" : "image",
452
+ poster: item.image,
453
+ // Video source only when the post is a real video (image posts ignore it).
454
+ src: isVideo ? item.videoUrl : void 0,
455
+ title,
456
+ caption: fullCaption || void 0,
457
+ kind: "Instagram",
458
+ meta
459
+ };
460
+ }
461
+ function openPermalink(item) {
462
+ if (typeof window === "undefined") return;
463
+ const href = item.meta?.href;
464
+ if (!href) return;
465
+ window.open(href, "_blank", "noopener,noreferrer");
466
+ }
467
+ function likeBadge(likeCount) {
468
+ return /* @__PURE__ */ jsxRuntime.jsxs(
469
+ "span",
470
+ {
471
+ "aria-label": `${likeCount.toLocaleString()} likes`,
472
+ style: {
473
+ display: "inline-flex",
474
+ alignItems: "center",
475
+ gap: 4,
476
+ padding: "3px 8px",
477
+ borderRadius: 999,
478
+ background: "rgba(8,12,24,0.65)",
479
+ backdropFilter: "blur(6px)",
480
+ WebkitBackdropFilter: "blur(6px)",
481
+ color: "#fff",
482
+ fontSize: 11,
483
+ fontWeight: 700,
484
+ lineHeight: 1
485
+ },
486
+ children: [
487
+ /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/heart", size: 13, "aria-hidden": "true" }),
488
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: likeCount.toLocaleString() })
489
+ ]
490
+ }
491
+ );
492
+ }
493
+ var VIEWER_ACTIONS = [
494
+ {
495
+ id: "open-in-instagram",
496
+ icon: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/external-link", size: 22, "aria-hidden": "true" }),
497
+ label: "Instagram",
498
+ ariaLabel: "Open in Instagram",
499
+ onPress: (item) => openPermalink(item)
500
+ }
501
+ ];
502
+ function RailStat({
427
503
  iconName,
428
504
  count,
429
- label
505
+ label,
506
+ onPress
430
507
  }) {
431
508
  return /* @__PURE__ */ jsxRuntime.jsxs(
432
- "span",
509
+ "button",
433
510
  {
434
- className: "inline-flex items-center gap-1",
511
+ type: "button",
435
512
  "aria-label": `${count.toLocaleString()} ${label}`,
513
+ onClick: (e) => {
514
+ e.stopPropagation();
515
+ onPress();
516
+ },
517
+ style: {
518
+ display: "flex",
519
+ flexDirection: "column",
520
+ alignItems: "center",
521
+ gap: 5,
522
+ padding: 0,
523
+ border: "none",
524
+ background: "transparent",
525
+ color: "inherit",
526
+ cursor: "pointer",
527
+ font: "inherit"
528
+ },
436
529
  children: [
437
- /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: iconName, size: 16, "aria-hidden": "true" }),
438
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: count.toLocaleString() })
530
+ /* @__PURE__ */ jsxRuntime.jsx(
531
+ "span",
532
+ {
533
+ style: {
534
+ width: 46,
535
+ height: 46,
536
+ borderRadius: "50%",
537
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
538
+ backdropFilter: "blur(6px)",
539
+ WebkitBackdropFilter: "blur(6px)",
540
+ display: "flex",
541
+ alignItems: "center",
542
+ justifyContent: "center"
543
+ },
544
+ children: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: iconName, size: 22, "aria-hidden": "true" })
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: count.toLocaleString() })
439
548
  ]
440
549
  }
441
550
  );
442
551
  }
552
+ function InstagramViewerRail({ item }) {
553
+ const meta = item.meta ?? {};
554
+ const open = () => openPermalink(item);
555
+ return /* @__PURE__ */ jsxRuntime.jsxs(
556
+ "div",
557
+ {
558
+ style: {
559
+ position: "absolute",
560
+ right: 11,
561
+ bottom: 135,
562
+ display: "flex",
563
+ flexDirection: "column",
564
+ alignItems: "center",
565
+ gap: 18,
566
+ color: "var(--psmi-chrome-fg, #fff)",
567
+ zIndex: 3
568
+ },
569
+ children: [
570
+ typeof meta.likeCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
571
+ RailStat,
572
+ {
573
+ iconName: "lucide/heart",
574
+ count: meta.likeCount,
575
+ label: "likes",
576
+ onPress: open
577
+ }
578
+ ) : null,
579
+ typeof meta.commentCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
580
+ RailStat,
581
+ {
582
+ iconName: "lucide/message-circle",
583
+ count: meta.commentCount,
584
+ label: "comments",
585
+ onPress: open
586
+ }
587
+ ) : null,
588
+ typeof meta.viewCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
589
+ RailStat,
590
+ {
591
+ iconName: "lucide/eye",
592
+ count: meta.viewCount,
593
+ label: "views",
594
+ onPress: open
595
+ }
596
+ ) : null,
597
+ meta.href ? /* @__PURE__ */ jsxRuntime.jsxs(
598
+ "a",
599
+ {
600
+ href: meta.href,
601
+ target: "_blank",
602
+ rel: "noopener noreferrer",
603
+ "aria-label": "Open in Instagram",
604
+ onClick: (e) => e.stopPropagation(),
605
+ style: {
606
+ display: "flex",
607
+ flexDirection: "column",
608
+ alignItems: "center",
609
+ gap: 5,
610
+ padding: 0,
611
+ border: "none",
612
+ background: "transparent",
613
+ color: "inherit",
614
+ cursor: "pointer",
615
+ font: "inherit",
616
+ textDecoration: "none"
617
+ },
618
+ children: [
619
+ /* @__PURE__ */ jsxRuntime.jsx(
620
+ "span",
621
+ {
622
+ style: {
623
+ width: 46,
624
+ height: 46,
625
+ borderRadius: "50%",
626
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
627
+ backdropFilter: "blur(6px)",
628
+ WebkitBackdropFilter: "blur(6px)",
629
+ display: "flex",
630
+ alignItems: "center",
631
+ justifyContent: "center"
632
+ },
633
+ children: /* @__PURE__ */ jsxRuntime.jsx(
634
+ DynamicIcon,
635
+ {
636
+ name: "lucide/external-link",
637
+ size: 22,
638
+ "aria-hidden": "true"
639
+ }
640
+ )
641
+ }
642
+ ),
643
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: "Instagram" })
644
+ ]
645
+ }
646
+ ) : null
647
+ ]
648
+ }
649
+ );
650
+ }
651
+ function InstagramFeedGrid({
652
+ mediaItems,
653
+ gridClassName,
654
+ itemClassName,
655
+ imageClassName,
656
+ optixFlowConfig
657
+ }) {
658
+ const { open } = mediaImmersive.useImmersiveFeed();
659
+ const posterImgProps = React.useMemo(() => {
660
+ const next = {};
661
+ if (imageClassName) next.className = imageClassName;
662
+ if (optixFlowConfig) next.optixFlowConfig = optixFlowConfig;
663
+ return Object.keys(next).length > 0 ? next : void 0;
664
+ }, [imageClassName, optixFlowConfig]);
665
+ return /* @__PURE__ */ jsxRuntime.jsx(
666
+ "div",
667
+ {
668
+ className: cn(
669
+ "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4",
670
+ gridClassName
671
+ ),
672
+ children: mediaItems.map((mediaItem) => {
673
+ const likeCount = mediaItem.meta?.likeCount;
674
+ return /* @__PURE__ */ jsxRuntime.jsx(
675
+ mediaImmersive.ThumbnailCard,
676
+ {
677
+ item: mediaItem,
678
+ onOpen: open,
679
+ size: IG_TILE_WIDTH,
680
+ style: IG_TILE_STYLE,
681
+ className: itemClassName,
682
+ elevated: false,
683
+ showDuration: false,
684
+ glyphMode: "hover",
685
+ posterImgProps,
686
+ badgeSlot: typeof likeCount === "number" ? likeBadge(likeCount) : void 0
687
+ },
688
+ mediaItem.id
689
+ );
690
+ })
691
+ }
692
+ );
693
+ }
443
694
  function InstagramPostGrid({
444
695
  sectionId = "instagram-post-grid",
445
696
  heading,
@@ -454,111 +705,17 @@ function InstagramPostGrid({
454
705
  gridClassName,
455
706
  itemClassName,
456
707
  imageClassName,
708
+ optixFlowConfig,
457
709
  background,
458
710
  spacing,
459
711
  pattern,
460
712
  patternOpacity,
461
- patternClassName,
462
- optixFlowConfig
713
+ patternClassName
463
714
  }) {
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
559
- );
560
- });
561
- }, [itemsSlot, items, itemClassName, imageClassName, optixFlowConfig]);
715
+ const mediaItems = React.useMemo(() => {
716
+ if (itemsSlot || !items) return [];
717
+ return items.filter((item) => Boolean(item.image)).map(toMediaItem);
718
+ }, [items, itemsSlot]);
562
719
  if (!itemsSlot && (!items || items.length === 0)) {
563
720
  return null;
564
721
  }
@@ -596,16 +753,34 @@ function InstagramPostGrid({
596
753
  }
597
754
  ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: subheadingClassName, children: subheading }))
598
755
  ] }),
599
- /* @__PURE__ */ jsxRuntime.jsx(
756
+ itemsSlot ? /* @__PURE__ */ jsxRuntime.jsx(
600
757
  "div",
601
758
  {
602
759
  className: cn(
603
760
  "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4",
604
761
  gridClassName
605
762
  ),
606
- children: tiles
763
+ children: itemsSlot
607
764
  }
608
- )
765
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(mediaImmersive.ImmersiveFeedProvider, { items: mediaItems, actions: VIEWER_ACTIONS, children: [
766
+ /* @__PURE__ */ jsxRuntime.jsx(
767
+ InstagramFeedGrid,
768
+ {
769
+ mediaItems,
770
+ gridClassName,
771
+ itemClassName,
772
+ imageClassName,
773
+ optixFlowConfig
774
+ }
775
+ ),
776
+ /* @__PURE__ */ jsxRuntime.jsx(
777
+ mediaImmersive.ImmersiveViewer,
778
+ {
779
+ ariaLabel: "Instagram post viewer",
780
+ renderActions: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(InstagramViewerRail, { item })
781
+ }
782
+ )
783
+ ] })
609
784
  ]
610
785
  }
611
786
  );
@@ -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, headerClassName, headingClassName, subheadingClassName, gridClassName, itemClassName, imageClassName, optixFlowConfig, background, spacing, 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, headerClassName, headingClassName, subheadingClassName, gridClassName, itemClassName, imageClassName, optixFlowConfig, background, spacing, pattern, patternOpacity, patternClassName, }: InstagramPostGridProps): React.JSX.Element | null;
173
196
 
174
197
  export { InstagramPostGrid, type InstagramPostGridProps, type InstagramPostItem };