@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.
package/dist/registry.cjs CHANGED
@@ -27,6 +27,7 @@ var Autoplay = require('embla-carousel-autoplay');
27
27
  var ProgressPrimitive = require('@radix-ui/react-progress');
28
28
  var AvatarPrimitive = require('@radix-ui/react-avatar');
29
29
  var AutoScroll3 = require('embla-carousel-auto-scroll');
30
+ var mediaImmersive = require('@page-speed/media-immersive');
30
31
  var CheckboxPrimitive = require('@radix-ui/react-checkbox');
31
32
  var LabelPrimitive = require('@radix-ui/react-label');
32
33
  var socialShare = require('@page-speed/social-share');
@@ -35271,23 +35272,323 @@ function InteriorCarousel({
35271
35272
  }
35272
35273
  );
35273
35274
  }
35274
- function EngagementBadge({
35275
+ var IG_TILE_WIDTH = 320;
35276
+ var IG_TILE_STYLE = { width: "100%" };
35277
+ var IG_TILE_RADIUS = 18;
35278
+ var TILE_WRAPPER_STYLE = {
35279
+ position: "relative",
35280
+ borderRadius: IG_TILE_RADIUS,
35281
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08), 0 6px 16px rgba(15, 23, 42, 0.10)"
35282
+ };
35283
+ var CAPTION_SCRIM_STYLE = {
35284
+ position: "absolute",
35285
+ left: 0,
35286
+ right: 0,
35287
+ bottom: 0,
35288
+ padding: "28px 14px 13px",
35289
+ background: "linear-gradient(180deg, rgba(8,12,24,0) 0%, rgba(8,12,24,0.55) 45%, rgba(8,12,24,0.78) 100%)",
35290
+ borderBottomLeftRadius: IG_TILE_RADIUS,
35291
+ borderBottomRightRadius: IG_TILE_RADIUS,
35292
+ pointerEvents: "none"
35293
+ };
35294
+ var CAPTION_TEXT_STYLE = {
35295
+ color: "#fff",
35296
+ fontSize: 13,
35297
+ lineHeight: 1.4,
35298
+ fontWeight: 600,
35299
+ letterSpacing: "0.005em",
35300
+ textShadow: "0 1px 2px rgba(0, 0, 0, 0.35)",
35301
+ display: "-webkit-box",
35302
+ WebkitLineClamp: 2,
35303
+ WebkitBoxOrient: "vertical",
35304
+ overflow: "hidden",
35305
+ height: "2.8em"
35306
+ };
35307
+ var TITLE_MAX = 90;
35308
+ function truncate(text, max = TITLE_MAX) {
35309
+ const trimmed = text.trim();
35310
+ if (trimmed.length <= max) return trimmed;
35311
+ const slice = trimmed.slice(0, max);
35312
+ const lastSpace = slice.lastIndexOf(" ");
35313
+ const head = lastSpace > max * 0.6 ? slice.slice(0, lastSpace) : slice;
35314
+ return `${head.trimEnd()}\u2026`;
35315
+ }
35316
+ function captionToString(caption) {
35317
+ return typeof caption === "string" ? caption : "";
35318
+ }
35319
+ function toMediaItem(item) {
35320
+ const isVideo = Boolean(item.isVideo && item.videoUrl);
35321
+ const fullCaption = captionToString(item.caption);
35322
+ const title = truncate(fullCaption) || item.imageAlt || "Instagram post";
35323
+ const meta = {
35324
+ href: item.href,
35325
+ likeCount: item.likeCount,
35326
+ commentCount: item.commentCount,
35327
+ viewCount: item.viewCount,
35328
+ date: item.date
35329
+ };
35330
+ return {
35331
+ id: item.id,
35332
+ type: isVideo ? "video" : "image",
35333
+ poster: item.image,
35334
+ // Video source only when the post is a real video (image posts ignore it).
35335
+ src: isVideo ? item.videoUrl : void 0,
35336
+ title,
35337
+ caption: fullCaption || void 0,
35338
+ kind: "Instagram",
35339
+ meta
35340
+ };
35341
+ }
35342
+ function openPermalink(item) {
35343
+ if (typeof window === "undefined") return;
35344
+ const href = item.meta?.href;
35345
+ if (!href) return;
35346
+ window.open(href, "_blank", "noopener,noreferrer");
35347
+ }
35348
+ function likeBadge(likeCount) {
35349
+ return /* @__PURE__ */ jsxRuntime.jsxs(
35350
+ "span",
35351
+ {
35352
+ "aria-label": `${likeCount.toLocaleString()} likes`,
35353
+ style: {
35354
+ display: "inline-flex",
35355
+ alignItems: "center",
35356
+ gap: 6,
35357
+ // Breathing room from the tile's top-left corner (refinement #1).
35358
+ margin: 6,
35359
+ padding: "6px 11px",
35360
+ borderRadius: 999,
35361
+ background: "rgba(8, 12, 24, 0.55)",
35362
+ backdropFilter: "blur(8px)",
35363
+ WebkitBackdropFilter: "blur(8px)",
35364
+ border: "1px solid rgba(255, 255, 255, 0.14)",
35365
+ color: "#fff",
35366
+ fontSize: 13,
35367
+ fontWeight: 600,
35368
+ lineHeight: 1,
35369
+ letterSpacing: "0.01em"
35370
+ },
35371
+ children: [
35372
+ /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/heart", size: 15, "aria-hidden": "true" }),
35373
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: likeCount.toLocaleString() })
35374
+ ]
35375
+ }
35376
+ );
35377
+ }
35378
+ var VIEWER_ACTIONS = [
35379
+ {
35380
+ id: "open-in-instagram",
35381
+ icon: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/external-link", size: 22, "aria-hidden": "true" }),
35382
+ label: "Instagram",
35383
+ ariaLabel: "Open in Instagram",
35384
+ onPress: (item) => openPermalink(item)
35385
+ }
35386
+ ];
35387
+ function RailStat({
35275
35388
  iconName,
35276
35389
  count,
35277
- label
35390
+ label,
35391
+ onPress
35278
35392
  }) {
35279
35393
  return /* @__PURE__ */ jsxRuntime.jsxs(
35280
- "span",
35394
+ "button",
35281
35395
  {
35282
- className: "inline-flex items-center gap-1",
35396
+ type: "button",
35283
35397
  "aria-label": `${count.toLocaleString()} ${label}`,
35398
+ onClick: (e) => {
35399
+ e.stopPropagation();
35400
+ onPress();
35401
+ },
35402
+ style: {
35403
+ display: "flex",
35404
+ flexDirection: "column",
35405
+ alignItems: "center",
35406
+ gap: 5,
35407
+ padding: 0,
35408
+ border: "none",
35409
+ background: "transparent",
35410
+ color: "inherit",
35411
+ cursor: "pointer",
35412
+ font: "inherit"
35413
+ },
35414
+ children: [
35415
+ /* @__PURE__ */ jsxRuntime.jsx(
35416
+ "span",
35417
+ {
35418
+ style: {
35419
+ width: 46,
35420
+ height: 46,
35421
+ borderRadius: "50%",
35422
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
35423
+ backdropFilter: "blur(6px)",
35424
+ WebkitBackdropFilter: "blur(6px)",
35425
+ display: "flex",
35426
+ alignItems: "center",
35427
+ justifyContent: "center"
35428
+ },
35429
+ children: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: iconName, size: 22, "aria-hidden": "true" })
35430
+ }
35431
+ ),
35432
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: count.toLocaleString() })
35433
+ ]
35434
+ }
35435
+ );
35436
+ }
35437
+ function InstagramViewerRail({ item }) {
35438
+ const meta = item.meta ?? {};
35439
+ const open = () => openPermalink(item);
35440
+ return /* @__PURE__ */ jsxRuntime.jsxs(
35441
+ "div",
35442
+ {
35443
+ style: {
35444
+ position: "absolute",
35445
+ right: 11,
35446
+ bottom: 135,
35447
+ display: "flex",
35448
+ flexDirection: "column",
35449
+ alignItems: "center",
35450
+ gap: 18,
35451
+ color: "var(--psmi-chrome-fg, #fff)",
35452
+ zIndex: 3
35453
+ },
35284
35454
  children: [
35285
- /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: iconName, size: 16, "aria-hidden": "true" }),
35286
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: count.toLocaleString() })
35455
+ typeof meta.likeCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
35456
+ RailStat,
35457
+ {
35458
+ iconName: "lucide/heart",
35459
+ count: meta.likeCount,
35460
+ label: "likes",
35461
+ onPress: open
35462
+ }
35463
+ ) : null,
35464
+ typeof meta.commentCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
35465
+ RailStat,
35466
+ {
35467
+ iconName: "lucide/message-circle",
35468
+ count: meta.commentCount,
35469
+ label: "comments",
35470
+ onPress: open
35471
+ }
35472
+ ) : null,
35473
+ typeof meta.viewCount === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
35474
+ RailStat,
35475
+ {
35476
+ iconName: "lucide/eye",
35477
+ count: meta.viewCount,
35478
+ label: "views",
35479
+ onPress: open
35480
+ }
35481
+ ) : null,
35482
+ meta.href ? /* @__PURE__ */ jsxRuntime.jsxs(
35483
+ "a",
35484
+ {
35485
+ href: meta.href,
35486
+ target: "_blank",
35487
+ rel: "noopener noreferrer",
35488
+ "aria-label": "Open in Instagram",
35489
+ onClick: (e) => e.stopPropagation(),
35490
+ style: {
35491
+ display: "flex",
35492
+ flexDirection: "column",
35493
+ alignItems: "center",
35494
+ gap: 5,
35495
+ padding: 0,
35496
+ border: "none",
35497
+ background: "transparent",
35498
+ color: "inherit",
35499
+ cursor: "pointer",
35500
+ font: "inherit",
35501
+ textDecoration: "none"
35502
+ },
35503
+ children: [
35504
+ /* @__PURE__ */ jsxRuntime.jsx(
35505
+ "span",
35506
+ {
35507
+ style: {
35508
+ width: 46,
35509
+ height: 46,
35510
+ borderRadius: "50%",
35511
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
35512
+ backdropFilter: "blur(6px)",
35513
+ WebkitBackdropFilter: "blur(6px)",
35514
+ display: "flex",
35515
+ alignItems: "center",
35516
+ justifyContent: "center"
35517
+ },
35518
+ children: /* @__PURE__ */ jsxRuntime.jsx(
35519
+ DynamicIcon,
35520
+ {
35521
+ name: "lucide/external-link",
35522
+ size: 22,
35523
+ "aria-hidden": "true"
35524
+ }
35525
+ )
35526
+ }
35527
+ ),
35528
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: "Instagram" })
35529
+ ]
35530
+ }
35531
+ ) : null
35287
35532
  ]
35288
35533
  }
35289
35534
  );
35290
35535
  }
35536
+ function InstagramFeedGrid({
35537
+ mediaItems,
35538
+ gridClassName,
35539
+ itemClassName,
35540
+ imageClassName,
35541
+ optixFlowConfig
35542
+ }) {
35543
+ const { open } = mediaImmersive.useImmersiveFeed();
35544
+ const posterImgProps = React30.useMemo(() => {
35545
+ const next = {};
35546
+ if (imageClassName) next.className = imageClassName;
35547
+ if (optixFlowConfig) next.optixFlowConfig = optixFlowConfig;
35548
+ return Object.keys(next).length > 0 ? next : void 0;
35549
+ }, [imageClassName, optixFlowConfig]);
35550
+ return /* @__PURE__ */ jsxRuntime.jsx(
35551
+ "div",
35552
+ {
35553
+ className: cn(
35554
+ "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
35555
+ gridClassName
35556
+ ),
35557
+ children: mediaItems.map((mediaItem) => {
35558
+ const likeCount = mediaItem.meta?.likeCount;
35559
+ return (
35560
+ // The wrapper owns the tile's outer chrome (subtle boundary shadow,
35561
+ // matching radius) and the block's own caption overlay — the card's
35562
+ // built-in caption is hidden so 1- and 2-line captions can share a
35563
+ // fixed-height, top-aligned text box (annotated refinements #2–#4).
35564
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: itemClassName, style: TILE_WRAPPER_STYLE, children: [
35565
+ /* @__PURE__ */ jsxRuntime.jsx(
35566
+ mediaImmersive.ThumbnailCard,
35567
+ {
35568
+ item: mediaItem,
35569
+ onOpen: open,
35570
+ size: IG_TILE_WIDTH,
35571
+ style: IG_TILE_STYLE,
35572
+ elevated: false,
35573
+ hideCaption: true,
35574
+ hideProgressHint: true,
35575
+ showDuration: false,
35576
+ glyphMode: "hover",
35577
+ posterImgProps,
35578
+ badgeSlot: typeof likeCount === "number" ? likeBadge(likeCount) : void 0
35579
+ }
35580
+ ),
35581
+ mediaItem.title ? (
35582
+ // aria-hidden: the card already announces the post title; this
35583
+ // overlay is purely visual. pointer-events pass through to the card.
35584
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", style: CAPTION_SCRIM_STYLE, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: CAPTION_TEXT_STYLE, children: mediaItem.title }) })
35585
+ ) : null
35586
+ ] }, mediaItem.id)
35587
+ );
35588
+ })
35589
+ }
35590
+ );
35591
+ }
35291
35592
  function InstagramPostGrid({
35292
35593
  sectionId = "instagram-post-grid",
35293
35594
  heading,
@@ -35295,118 +35596,32 @@ function InstagramPostGrid({
35295
35596
  items,
35296
35597
  itemsSlot,
35297
35598
  className,
35298
- containerClassName,
35599
+ containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
35600
+ spacing = "py-12 md:py-32",
35299
35601
  headerClassName,
35300
35602
  headingClassName,
35301
35603
  subheadingClassName,
35302
35604
  gridClassName,
35303
35605
  itemClassName,
35304
35606
  imageClassName,
35607
+ optixFlowConfig,
35305
35608
  background,
35306
- spacing,
35307
35609
  pattern,
35308
35610
  patternOpacity,
35309
- patternClassName,
35310
- optixFlowConfig
35611
+ patternClassName
35311
35612
  }) {
35312
- const tiles = React30.useMemo(() => {
35313
- if (itemsSlot) return itemsSlot;
35314
- if (!items || items.length === 0) return null;
35315
- const visibleItems = items.filter((item) => Boolean(item.image));
35316
- return visibleItems.map((item) => {
35317
- const altText = item.imageAlt || (typeof item.caption === "string" ? item.caption : "") || "Instagram post";
35318
- const showVideo = Boolean(item.isVideo && item.videoUrl);
35319
- return /* @__PURE__ */ jsxRuntime.jsxs(
35320
- pressable.Pressable,
35321
- {
35322
- href: item.href,
35323
- "aria-label": typeof item.caption === "string" && item.caption.length > 0 ? item.caption : "View Instagram post",
35324
- className: cn(
35325
- "group relative block aspect-square overflow-hidden rounded-md bg-muted",
35326
- item.className,
35327
- itemClassName
35328
- ),
35329
- children: [
35330
- /* @__PURE__ */ jsxRuntime.jsx(
35331
- img$1.Img,
35332
- {
35333
- src: item.image,
35334
- alt: altText,
35335
- loading: "lazy",
35336
- className: cn(
35337
- "h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105 motion-reduce:transform-none motion-reduce:transition-none",
35338
- imageClassName
35339
- ),
35340
- optixFlowConfig
35341
- }
35342
- ),
35343
- showVideo && /* @__PURE__ */ jsxRuntime.jsx(
35344
- video.Video,
35345
- {
35346
- src: item.videoUrl,
35347
- poster: item.image,
35348
- controls: false,
35349
- muted: true,
35350
- loop: true,
35351
- playsInline: true,
35352
- preload: "metadata",
35353
- onMouseEnter: (e) => {
35354
- void e.currentTarget.play().catch(() => void 0);
35355
- },
35356
- onMouseLeave: (e) => {
35357
- e.currentTarget.pause();
35358
- e.currentTarget.currentTime = 0;
35359
- },
35360
- className: cn(
35361
- "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",
35362
- imageClassName
35363
- )
35364
- }
35365
- ),
35366
- item.isVideo && /* @__PURE__ */ jsxRuntime.jsx(
35367
- "span",
35368
- {
35369
- className: "absolute right-2 top-2 text-white drop-shadow-md",
35370
- "aria-hidden": "true",
35371
- children: /* @__PURE__ */ jsxRuntime.jsx(DynamicIcon, { name: "lucide/play", size: 18 })
35372
- }
35373
- ),
35374
- (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: [
35375
- 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 })),
35376
- (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: [
35377
- typeof item.likeCount === "number" && /* @__PURE__ */ jsxRuntime.jsx(
35378
- EngagementBadge,
35379
- {
35380
- iconName: "lucide/heart",
35381
- count: item.likeCount,
35382
- label: "likes"
35383
- }
35384
- ),
35385
- typeof item.commentCount === "number" && /* @__PURE__ */ jsxRuntime.jsx(
35386
- EngagementBadge,
35387
- {
35388
- iconName: "lucide/message-circle",
35389
- count: item.commentCount,
35390
- label: "comments"
35391
- }
35392
- ),
35393
- typeof item.viewCount === "number" && /* @__PURE__ */ jsxRuntime.jsx(
35394
- EngagementBadge,
35395
- {
35396
- iconName: "lucide/eye",
35397
- count: item.viewCount,
35398
- label: "views"
35399
- }
35400
- ),
35401
- item.date && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto opacity-80", children: item.date })
35402
- ] })
35403
- ] })
35404
- ]
35405
- },
35406
- item.id
35613
+ const mediaItems = React30.useMemo(() => {
35614
+ if (itemsSlot || !items) return [];
35615
+ const withImage = items.filter((item) => Boolean(item.image));
35616
+ if (process.env.NODE_ENV !== "production" && withImage.length < items.length) {
35617
+ const skipped = items.filter((item) => !item.image);
35618
+ const skippedIds = skipped.map((item) => item.id).join(", ");
35619
+ console.warn(
35620
+ `[instagram-post-grid] skipped ${skipped.length} item(s) without a resolvable image: ${skippedIds}`
35407
35621
  );
35408
- });
35409
- }, [itemsSlot, items, itemClassName, imageClassName, optixFlowConfig]);
35622
+ }
35623
+ return withImage.map(toMediaItem);
35624
+ }, [items, itemsSlot]);
35410
35625
  if (!itemsSlot && (!items || items.length === 0)) {
35411
35626
  return null;
35412
35627
  }
@@ -35444,16 +35659,34 @@ function InstagramPostGrid({
35444
35659
  }
35445
35660
  ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: subheadingClassName, children: subheading }))
35446
35661
  ] }),
35447
- /* @__PURE__ */ jsxRuntime.jsx(
35662
+ itemsSlot ? /* @__PURE__ */ jsxRuntime.jsx(
35448
35663
  "div",
35449
35664
  {
35450
35665
  className: cn(
35451
- "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4",
35666
+ "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
35452
35667
  gridClassName
35453
35668
  ),
35454
- children: tiles
35669
+ children: itemsSlot
35455
35670
  }
35456
- )
35671
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(mediaImmersive.ImmersiveFeedProvider, { items: mediaItems, actions: VIEWER_ACTIONS, children: [
35672
+ /* @__PURE__ */ jsxRuntime.jsx(
35673
+ InstagramFeedGrid,
35674
+ {
35675
+ mediaItems,
35676
+ gridClassName,
35677
+ itemClassName,
35678
+ imageClassName,
35679
+ optixFlowConfig
35680
+ }
35681
+ ),
35682
+ /* @__PURE__ */ jsxRuntime.jsx(
35683
+ mediaImmersive.ImmersiveViewer,
35684
+ {
35685
+ ariaLabel: "Instagram post viewer",
35686
+ renderActions: ({ item }) => /* @__PURE__ */ jsxRuntime.jsx(InstagramViewerRail, { item })
35687
+ }
35688
+ )
35689
+ ] })
35457
35690
  ]
35458
35691
  }
35459
35692
  );
@@ -111334,8 +111567,8 @@ var interiorCarousel = {
111334
111567
  }
111335
111568
  };
111336
111569
  var instagramPostGrid = {
111337
- exampleUsage: "Display a website's Instagram feed as a responsive grid of square tiles. Each tile links out to the post's instagram.com permalink in a new tab, shows the caption and post date on hover, and renders like/comment/view counts when available. Video posts play muted on hover. Data is hydrated from the toastability Instagram feed (dataSource type 'instagram_feed', bindTo 'items').",
111338
- importantUsageNotes: `${GALLERY_MEDIA_NOTE} This is a DYNAMIC feed block: its 'items' are hydrated at routing-build time from the connected Instagram profile \u2014 do NOT hand-author post content. 'items[].image' and 'items[].videoUrl' MUST be the re-hosted MediaRecord CDN URLs served by the feed; expiring Instagram CDN URLs must never be used. Engagement counts ('likeCount', 'commentCount', 'viewCount') render only when the value is present \u2014 never fabricate a zero for a missing metric. Each 'items[].href' is an external instagram.com permalink and opens in a new tab. Items without an 'image' are skipped; an empty or missing 'items' array renders nothing. Requires the site to have a connected, fully-imported Instagram profile ('instagram_media' capability).`,
111570
+ exampleUsage: "Display a website's Instagram feed as a responsive grid of vertical (9:16) tiles that open a fullscreen, swipeable immersive viewer (TikTok/Reels-style). Photos and reels are both first-class media \u2014 reels play a muted preview and open with playback; photos open as a full-bleed still. Each tile shows a like-count pill top-left (when available), no duration timestamp, and a center glyph on hover only (a play triangle for reels, an expand icon for photos). The fullscreen viewer carries the caption plus a read-only engagement rail (likes/comments/views) and an explicit 'Open in Instagram' link (a real anchor opening in a new tab). Data is hydrated from the toastability Instagram feed (dataSource type 'instagram_feed', bindTo 'items').",
111571
+ importantUsageNotes: `${GALLERY_MEDIA_NOTE} This is a DYNAMIC feed block: its 'items' are hydrated at routing-build time from the connected Instagram profile \u2014 do NOT hand-author post content. 'items[].image' and 'items[].videoUrl' MUST be the re-hosted MediaRecord CDN URLs served by the feed; expiring Instagram CDN URLs must never be used. Tiles are VERTICAL (9:16) and render both photos and reels; a post is treated as a reel only when 'isVideo' is true AND 'videoUrl' is present. Engagement counts ('likeCount', 'commentCount', 'viewCount') render only when the value is present \u2014 never fabricate a zero for a missing metric; 'likeCount' surfaces as the tile's top-left pill and, with comments/views, in the viewer rail. Tapping a tile OPENS THE FULLSCREEN VIEWER (the whole-tile link-out was replaced); the instagram.com permalink ('items[].href') opens from the viewer's 'Open in Instagram' egress, which is rendered as a REAL anchor (target=_blank, rel=noopener noreferrer) \u2014 a crawlable, affordance-correct link, not a scripted window.open button. 'items[].caption' should be a plain STRING when rendered immersively \u2014 it flows through as the tile/viewer title text; a rich ReactNode caption cannot be threaded through the immersive card/viewer and falls back to 'items[].imageAlt' (then a generic label). Consumer image styling is honored: 'imageClassName' and 'optixFlowConfig' are forwarded onto each tile's poster image. Items without an 'image' are skipped; an empty or missing 'items' array renders nothing. Requires the site to have a connected, fully-imported Instagram profile ('instagram_media' capability).`,
111339
111572
  usageRequirements: {
111340
111573
  requiredProps: ["items"],
111341
111574
  requiresSiteCapabilities: galleryCapabilities(
@@ -111343,9 +111576,12 @@ var instagramPostGrid = {
111343
111576
  "media_library"
111344
111577
  ),
111345
111578
  propConstraints: {
111579
+ heading: { maxLength: 60 },
111346
111580
  items: {
111347
- minItems: 3,
111348
- maxItems: 24
111581
+ required: true,
111582
+ minItems: 4,
111583
+ maxItems: 16,
111584
+ note: "Use multiples of the 4-column desktop grid (8 or 12) so rows fill cleanly. Every item must carry a re-hosted CDN image URL \u2014 imageless items are skipped at render (dev builds warn)."
111349
111585
  }
111350
111586
  },
111351
111587
  mediaSlots: {
@@ -111371,33 +111607,90 @@ var instagramPostGrid = {
111371
111607
  id: "1",
111372
111608
  href: "https://www.instagram.com/p/CxAmpLe001/",
111373
111609
  image: GALLERY_EXAMPLE_IMAGE_URL,
111374
- imageAlt: "Espresso being poured at the counter",
111610
+ imageAlt: "Espresso being pulled at the counter",
111375
111611
  caption: "Morning pours before the rush \u2615\uFE0F",
111376
- date: "Jul 1, 2026",
111377
- likeCount: 128,
111378
- commentCount: 12
111612
+ date: "Jul 6, 2026",
111613
+ likeCount: 428,
111614
+ commentCount: 34
111379
111615
  },
111380
111616
  {
111381
111617
  id: "2",
111382
111618
  href: "https://www.instagram.com/p/CxAmpLe002/",
111383
- image: GALLERY_EXAMPLE_IMAGE_URL,
111384
- imageAlt: "Behind the scenes reel",
111385
- caption: "Behind the scenes of today's bake #freshbread",
111386
- date: "Jun 28, 2026",
111619
+ image: "https://toastability-production.s3.amazonaws.com/z9u4sdrj2oq3eds0qyui0nxsus3j",
111620
+ imageAlt: "Line cook plating on a busy Friday service",
111621
+ caption: "Behind the pass on a Friday night service \u{1F525}",
111622
+ date: "Jul 4, 2026",
111387
111623
  isVideo: true,
111388
- videoUrl: "https://toastability-production.s3.amazonaws.com/4kox2ux0ye1wlqkdwg03s08a67i1",
111389
- likeCount: 342,
111390
- commentCount: 21,
111391
- viewCount: 4820
111624
+ videoUrl: "https://toastability-production.s3.amazonaws.com/b555hwjt7ltr81et05v5254q1ak6",
111625
+ likeCount: 1203,
111626
+ commentCount: 88,
111627
+ viewCount: 12840
111392
111628
  },
111393
111629
  {
111394
111630
  id: "3",
111395
111631
  href: "https://www.instagram.com/p/CxAmpLe003/",
111632
+ image: "https://toastability-production.s3.amazonaws.com/63aotyt2pb4gqpccej2kkw8reson",
111633
+ imageAlt: "House pancakes with maple butter",
111634
+ caption: "Weekend brunch is back \u2014 house pancakes + maple butter",
111635
+ date: "Jul 2, 2026",
111636
+ likeCount: 356,
111637
+ commentCount: 27
111638
+ },
111639
+ {
111640
+ id: "4",
111641
+ href: "https://www.instagram.com/p/CxAmpLe004/",
111642
+ image: "https://toastability-production.s3.amazonaws.com/we9r4e711an6d0bd3dwbl9tb9z7q",
111643
+ imageAlt: "Barista finishing latte art",
111644
+ caption: "60 seconds of latte art, no cuts \u2728",
111645
+ date: "Jun 29, 2026",
111646
+ isVideo: true,
111647
+ videoUrl: "https://toastability-production.s3.amazonaws.com/dvz0441h9fxjhh88lzqbwdoyxv52",
111648
+ likeCount: 942,
111649
+ commentCount: 61,
111650
+ viewCount: 8675
111651
+ },
111652
+ {
111653
+ id: "5",
111654
+ href: "https://www.instagram.com/p/CxAmpLe005/",
111396
111655
  image: GALLERY_EXAMPLE_IMAGE_URL,
111397
- imageAlt: "Weekend brunch spread",
111398
- caption: "Weekend brunch is back on the menu",
111656
+ imageAlt: "Fresh sourdough loaves out of the oven",
111657
+ caption: "Fresh sourdough out of the oven at 6am \u{1F956}",
111658
+ date: "Jun 27, 2026",
111659
+ likeCount: 512,
111660
+ commentCount: 41
111661
+ },
111662
+ {
111663
+ id: "6",
111664
+ href: "https://www.instagram.com/p/CxAmpLe006/",
111665
+ image: "https://toastability-production.s3.amazonaws.com/c4sgsy0g7o2rrjmvm9x7evxems82",
111666
+ imageAlt: "Chef plating a summer heirloom tomato salad",
111667
+ caption: "Plating the summer heirloom tomato salad \u{1F345}",
111399
111668
  date: "Jun 24, 2026",
111400
- likeCount: 96
111669
+ isVideo: true,
111670
+ videoUrl: "https://toastability-production.s3.amazonaws.com/jhjfvkmdzktacyijd9fh6acc7o2c",
111671
+ likeCount: 731,
111672
+ commentCount: 45,
111673
+ viewCount: 6390
111674
+ },
111675
+ {
111676
+ id: "7",
111677
+ href: "https://www.instagram.com/p/CxAmpLe007/",
111678
+ image: "https://toastability-production.s3.amazonaws.com/kh1p8y15v55ctp5ulobm4pd77etm",
111679
+ imageAlt: "Sunny patio seating with fresh flowers",
111680
+ caption: "Patio season is officially open \u2600\uFE0F",
111681
+ date: "Jun 21, 2026",
111682
+ likeCount: 289,
111683
+ commentCount: 19
111684
+ },
111685
+ {
111686
+ id: "8",
111687
+ href: "https://www.instagram.com/p/CxAmpLe008/",
111688
+ image: "https://toastability-production.s3.amazonaws.com/2d4k8d5shwg82276hzj2ztbj7mxq",
111689
+ imageAlt: "Charred corn elote on a plate",
111690
+ caption: "New on the menu: charred corn elote, our way \u{1F33D}",
111691
+ date: "Jun 18, 2026",
111692
+ likeCount: 634,
111693
+ commentCount: 52
111401
111694
  }
111402
111695
  ]
111403
111696
  }
@@ -124817,7 +125110,7 @@ var BLOCK_REGISTRY = {
124817
125110
  "instagram-post-grid": {
124818
125111
  id: "instagram-post-grid",
124819
125112
  name: "Instagram Post Grid",
124820
- description: "A responsive grid of square tiles rendering a website's Instagram feed. Each tile links out to the post's instagram.com permalink in a new tab, reveals the caption, date, and like/comment/view counts on hover, and plays video posts muted on hover. Content is hydrated dynamically from the connected Instagram profile. Ideal for social proof sections, footers, and community pages that showcase a live Instagram presence.",
125113
+ description: "A responsive grid of vertical (9:16) tiles rendering a website's Instagram feed. Tapping a tile opens a fullscreen, swipeable immersive viewer (TikTok/Reels-style) with the caption, a read-only engagement rail (likes/comments/views), and an 'Open in Instagram' action. Photos and reels are both first-class media; each tile shows a like-count pill and a hover-only center glyph (play for reels, expand for photos). Content is hydrated dynamically from the connected Instagram profile. Ideal for social proof sections, footers, and community pages that showcase a live Instagram presence.",
124821
125114
  semanticTags: [
124822
125115
  "gallery",
124823
125116
  "instagram",