@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.
@@ -1,11 +1,9 @@
1
1
  "use client";
2
2
  import * as React from 'react';
3
3
  import React__default, { useMemo } from 'react';
4
+ import { ImmersiveFeedProvider, ImmersiveViewer, useImmersiveFeed, ThumbnailCard } from '@page-speed/media-immersive';
4
5
  import { clsx } from 'clsx';
5
6
  import { twMerge } from 'tailwind-merge';
6
- import { Img } from '@page-speed/img';
7
- import { Video } from '@page-speed/video';
8
- import { Pressable } from '@page-speed/pressable';
9
7
  import { Icon } from '@page-speed/icon';
10
8
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
11
9
 
@@ -402,23 +400,323 @@ var Section = React__default.forwardRef(
402
400
  }
403
401
  );
404
402
  Section.displayName = "Section";
405
- function EngagementBadge({
403
+ var IG_TILE_WIDTH = 320;
404
+ var IG_TILE_STYLE = { width: "100%" };
405
+ var IG_TILE_RADIUS = 18;
406
+ var TILE_WRAPPER_STYLE = {
407
+ position: "relative",
408
+ borderRadius: IG_TILE_RADIUS,
409
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.08), 0 6px 16px rgba(15, 23, 42, 0.10)"
410
+ };
411
+ var CAPTION_SCRIM_STYLE = {
412
+ position: "absolute",
413
+ left: 0,
414
+ right: 0,
415
+ bottom: 0,
416
+ padding: "28px 14px 13px",
417
+ background: "linear-gradient(180deg, rgba(8,12,24,0) 0%, rgba(8,12,24,0.55) 45%, rgba(8,12,24,0.78) 100%)",
418
+ borderBottomLeftRadius: IG_TILE_RADIUS,
419
+ borderBottomRightRadius: IG_TILE_RADIUS,
420
+ pointerEvents: "none"
421
+ };
422
+ var CAPTION_TEXT_STYLE = {
423
+ color: "#fff",
424
+ fontSize: 13,
425
+ lineHeight: 1.4,
426
+ fontWeight: 600,
427
+ letterSpacing: "0.005em",
428
+ textShadow: "0 1px 2px rgba(0, 0, 0, 0.35)",
429
+ display: "-webkit-box",
430
+ WebkitLineClamp: 2,
431
+ WebkitBoxOrient: "vertical",
432
+ overflow: "hidden",
433
+ height: "2.8em"
434
+ };
435
+ var TITLE_MAX = 90;
436
+ function truncate(text, max = TITLE_MAX) {
437
+ const trimmed = text.trim();
438
+ if (trimmed.length <= max) return trimmed;
439
+ const slice = trimmed.slice(0, max);
440
+ const lastSpace = slice.lastIndexOf(" ");
441
+ const head = lastSpace > max * 0.6 ? slice.slice(0, lastSpace) : slice;
442
+ return `${head.trimEnd()}\u2026`;
443
+ }
444
+ function captionToString(caption) {
445
+ return typeof caption === "string" ? caption : "";
446
+ }
447
+ function toMediaItem(item) {
448
+ const isVideo = Boolean(item.isVideo && item.videoUrl);
449
+ const fullCaption = captionToString(item.caption);
450
+ const title = truncate(fullCaption) || item.imageAlt || "Instagram post";
451
+ const meta = {
452
+ href: item.href,
453
+ likeCount: item.likeCount,
454
+ commentCount: item.commentCount,
455
+ viewCount: item.viewCount,
456
+ date: item.date
457
+ };
458
+ return {
459
+ id: item.id,
460
+ type: isVideo ? "video" : "image",
461
+ poster: item.image,
462
+ // Video source only when the post is a real video (image posts ignore it).
463
+ src: isVideo ? item.videoUrl : void 0,
464
+ title,
465
+ caption: fullCaption || void 0,
466
+ kind: "Instagram",
467
+ meta
468
+ };
469
+ }
470
+ function openPermalink(item) {
471
+ if (typeof window === "undefined") return;
472
+ const href = item.meta?.href;
473
+ if (!href) return;
474
+ window.open(href, "_blank", "noopener,noreferrer");
475
+ }
476
+ function likeBadge(likeCount) {
477
+ return /* @__PURE__ */ jsxs(
478
+ "span",
479
+ {
480
+ "aria-label": `${likeCount.toLocaleString()} likes`,
481
+ style: {
482
+ display: "inline-flex",
483
+ alignItems: "center",
484
+ gap: 6,
485
+ // Breathing room from the tile's top-left corner (refinement #1).
486
+ margin: 6,
487
+ padding: "6px 11px",
488
+ borderRadius: 999,
489
+ background: "rgba(8, 12, 24, 0.55)",
490
+ backdropFilter: "blur(8px)",
491
+ WebkitBackdropFilter: "blur(8px)",
492
+ border: "1px solid rgba(255, 255, 255, 0.14)",
493
+ color: "#fff",
494
+ fontSize: 13,
495
+ fontWeight: 600,
496
+ lineHeight: 1,
497
+ letterSpacing: "0.01em"
498
+ },
499
+ children: [
500
+ /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/heart", size: 15, "aria-hidden": "true" }),
501
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: likeCount.toLocaleString() })
502
+ ]
503
+ }
504
+ );
505
+ }
506
+ var VIEWER_ACTIONS = [
507
+ {
508
+ id: "open-in-instagram",
509
+ icon: /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/external-link", size: 22, "aria-hidden": "true" }),
510
+ label: "Instagram",
511
+ ariaLabel: "Open in Instagram",
512
+ onPress: (item) => openPermalink(item)
513
+ }
514
+ ];
515
+ function RailStat({
406
516
  iconName,
407
517
  count,
408
- label
518
+ label,
519
+ onPress
409
520
  }) {
410
521
  return /* @__PURE__ */ jsxs(
411
- "span",
522
+ "button",
412
523
  {
413
- className: "inline-flex items-center gap-1",
524
+ type: "button",
414
525
  "aria-label": `${count.toLocaleString()} ${label}`,
526
+ onClick: (e) => {
527
+ e.stopPropagation();
528
+ onPress();
529
+ },
530
+ style: {
531
+ display: "flex",
532
+ flexDirection: "column",
533
+ alignItems: "center",
534
+ gap: 5,
535
+ padding: 0,
536
+ border: "none",
537
+ background: "transparent",
538
+ color: "inherit",
539
+ cursor: "pointer",
540
+ font: "inherit"
541
+ },
415
542
  children: [
416
- /* @__PURE__ */ jsx(DynamicIcon, { name: iconName, size: 16, "aria-hidden": "true" }),
417
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: count.toLocaleString() })
543
+ /* @__PURE__ */ jsx(
544
+ "span",
545
+ {
546
+ style: {
547
+ width: 46,
548
+ height: 46,
549
+ borderRadius: "50%",
550
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
551
+ backdropFilter: "blur(6px)",
552
+ WebkitBackdropFilter: "blur(6px)",
553
+ display: "flex",
554
+ alignItems: "center",
555
+ justifyContent: "center"
556
+ },
557
+ children: /* @__PURE__ */ jsx(DynamicIcon, { name: iconName, size: 22, "aria-hidden": "true" })
558
+ }
559
+ ),
560
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: count.toLocaleString() })
418
561
  ]
419
562
  }
420
563
  );
421
564
  }
565
+ function InstagramViewerRail({ item }) {
566
+ const meta = item.meta ?? {};
567
+ const open = () => openPermalink(item);
568
+ return /* @__PURE__ */ jsxs(
569
+ "div",
570
+ {
571
+ style: {
572
+ position: "absolute",
573
+ right: 11,
574
+ bottom: 135,
575
+ display: "flex",
576
+ flexDirection: "column",
577
+ alignItems: "center",
578
+ gap: 18,
579
+ color: "var(--psmi-chrome-fg, #fff)",
580
+ zIndex: 3
581
+ },
582
+ children: [
583
+ typeof meta.likeCount === "number" ? /* @__PURE__ */ jsx(
584
+ RailStat,
585
+ {
586
+ iconName: "lucide/heart",
587
+ count: meta.likeCount,
588
+ label: "likes",
589
+ onPress: open
590
+ }
591
+ ) : null,
592
+ typeof meta.commentCount === "number" ? /* @__PURE__ */ jsx(
593
+ RailStat,
594
+ {
595
+ iconName: "lucide/message-circle",
596
+ count: meta.commentCount,
597
+ label: "comments",
598
+ onPress: open
599
+ }
600
+ ) : null,
601
+ typeof meta.viewCount === "number" ? /* @__PURE__ */ jsx(
602
+ RailStat,
603
+ {
604
+ iconName: "lucide/eye",
605
+ count: meta.viewCount,
606
+ label: "views",
607
+ onPress: open
608
+ }
609
+ ) : null,
610
+ meta.href ? /* @__PURE__ */ jsxs(
611
+ "a",
612
+ {
613
+ href: meta.href,
614
+ target: "_blank",
615
+ rel: "noopener noreferrer",
616
+ "aria-label": "Open in Instagram",
617
+ onClick: (e) => e.stopPropagation(),
618
+ style: {
619
+ display: "flex",
620
+ flexDirection: "column",
621
+ alignItems: "center",
622
+ gap: 5,
623
+ padding: 0,
624
+ border: "none",
625
+ background: "transparent",
626
+ color: "inherit",
627
+ cursor: "pointer",
628
+ font: "inherit",
629
+ textDecoration: "none"
630
+ },
631
+ children: [
632
+ /* @__PURE__ */ jsx(
633
+ "span",
634
+ {
635
+ style: {
636
+ width: 46,
637
+ height: 46,
638
+ borderRadius: "50%",
639
+ background: "var(--psmi-chrome-bg, rgba(255,255,255,0.14))",
640
+ backdropFilter: "blur(6px)",
641
+ WebkitBackdropFilter: "blur(6px)",
642
+ display: "flex",
643
+ alignItems: "center",
644
+ justifyContent: "center"
645
+ },
646
+ children: /* @__PURE__ */ jsx(
647
+ DynamicIcon,
648
+ {
649
+ name: "lucide/external-link",
650
+ size: 22,
651
+ "aria-hidden": "true"
652
+ }
653
+ )
654
+ }
655
+ ),
656
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { fontSize: 11, fontWeight: 600 }, children: "Instagram" })
657
+ ]
658
+ }
659
+ ) : null
660
+ ]
661
+ }
662
+ );
663
+ }
664
+ function InstagramFeedGrid({
665
+ mediaItems,
666
+ gridClassName,
667
+ itemClassName,
668
+ imageClassName,
669
+ optixFlowConfig
670
+ }) {
671
+ const { open } = useImmersiveFeed();
672
+ const posterImgProps = useMemo(() => {
673
+ const next = {};
674
+ if (imageClassName) next.className = imageClassName;
675
+ if (optixFlowConfig) next.optixFlowConfig = optixFlowConfig;
676
+ return Object.keys(next).length > 0 ? next : void 0;
677
+ }, [imageClassName, optixFlowConfig]);
678
+ return /* @__PURE__ */ jsx(
679
+ "div",
680
+ {
681
+ className: cn(
682
+ "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
683
+ gridClassName
684
+ ),
685
+ children: mediaItems.map((mediaItem) => {
686
+ const likeCount = mediaItem.meta?.likeCount;
687
+ return (
688
+ // The wrapper owns the tile's outer chrome (subtle boundary shadow,
689
+ // matching radius) and the block's own caption overlay — the card's
690
+ // built-in caption is hidden so 1- and 2-line captions can share a
691
+ // fixed-height, top-aligned text box (annotated refinements #2–#4).
692
+ /* @__PURE__ */ jsxs("div", { className: itemClassName, style: TILE_WRAPPER_STYLE, children: [
693
+ /* @__PURE__ */ jsx(
694
+ ThumbnailCard,
695
+ {
696
+ item: mediaItem,
697
+ onOpen: open,
698
+ size: IG_TILE_WIDTH,
699
+ style: IG_TILE_STYLE,
700
+ elevated: false,
701
+ hideCaption: true,
702
+ hideProgressHint: true,
703
+ showDuration: false,
704
+ glyphMode: "hover",
705
+ posterImgProps,
706
+ badgeSlot: typeof likeCount === "number" ? likeBadge(likeCount) : void 0
707
+ }
708
+ ),
709
+ mediaItem.title ? (
710
+ // aria-hidden: the card already announces the post title; this
711
+ // overlay is purely visual. pointer-events pass through to the card.
712
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: CAPTION_SCRIM_STYLE, children: /* @__PURE__ */ jsx("div", { style: CAPTION_TEXT_STYLE, children: mediaItem.title }) })
713
+ ) : null
714
+ ] }, mediaItem.id)
715
+ );
716
+ })
717
+ }
718
+ );
719
+ }
422
720
  function InstagramPostGrid({
423
721
  sectionId = "instagram-post-grid",
424
722
  heading,
@@ -426,118 +724,32 @@ function InstagramPostGrid({
426
724
  items,
427
725
  itemsSlot,
428
726
  className,
429
- containerClassName,
727
+ containerClassName = "px-6 sm:px-6 md:px-8 lg:px-8",
728
+ spacing = "py-12 md:py-32",
430
729
  headerClassName,
431
730
  headingClassName,
432
731
  subheadingClassName,
433
732
  gridClassName,
434
733
  itemClassName,
435
734
  imageClassName,
735
+ optixFlowConfig,
436
736
  background,
437
- spacing,
438
737
  pattern,
439
738
  patternOpacity,
440
- patternClassName,
441
- optixFlowConfig
739
+ patternClassName
442
740
  }) {
443
- const tiles = useMemo(() => {
444
- if (itemsSlot) return itemsSlot;
445
- if (!items || items.length === 0) return null;
446
- const visibleItems = items.filter((item) => Boolean(item.image));
447
- return visibleItems.map((item) => {
448
- const altText = item.imageAlt || (typeof item.caption === "string" ? item.caption : "") || "Instagram post";
449
- const showVideo = Boolean(item.isVideo && item.videoUrl);
450
- return /* @__PURE__ */ jsxs(
451
- Pressable,
452
- {
453
- href: item.href,
454
- "aria-label": typeof item.caption === "string" && item.caption.length > 0 ? item.caption : "View Instagram post",
455
- className: cn(
456
- "group relative block aspect-square overflow-hidden rounded-md bg-muted",
457
- item.className,
458
- itemClassName
459
- ),
460
- children: [
461
- /* @__PURE__ */ jsx(
462
- Img,
463
- {
464
- src: item.image,
465
- alt: altText,
466
- loading: "lazy",
467
- className: cn(
468
- "h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105 motion-reduce:transform-none motion-reduce:transition-none",
469
- imageClassName
470
- ),
471
- optixFlowConfig
472
- }
473
- ),
474
- showVideo && /* @__PURE__ */ jsx(
475
- Video,
476
- {
477
- src: item.videoUrl,
478
- poster: item.image,
479
- controls: false,
480
- muted: true,
481
- loop: true,
482
- playsInline: true,
483
- preload: "metadata",
484
- onMouseEnter: (e) => {
485
- void e.currentTarget.play().catch(() => void 0);
486
- },
487
- onMouseLeave: (e) => {
488
- e.currentTarget.pause();
489
- e.currentTarget.currentTime = 0;
490
- },
491
- className: cn(
492
- "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",
493
- imageClassName
494
- )
495
- }
496
- ),
497
- item.isVideo && /* @__PURE__ */ jsx(
498
- "span",
499
- {
500
- className: "absolute right-2 top-2 text-white drop-shadow-md",
501
- "aria-hidden": "true",
502
- children: /* @__PURE__ */ jsx(DynamicIcon, { name: "lucide/play", size: 18 })
503
- }
504
- ),
505
- (item.caption || item.date || typeof item.likeCount === "number" || typeof item.commentCount === "number" || typeof item.viewCount === "number") && /* @__PURE__ */ 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: [
506
- item.caption && (typeof item.caption === "string" ? /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-xs leading-snug wrap-break-word", children: item.caption }) : /* @__PURE__ */ jsx("div", { className: "line-clamp-2 text-xs leading-snug", children: item.caption })),
507
- (typeof item.likeCount === "number" || typeof item.commentCount === "number" || typeof item.viewCount === "number" || item.date) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs font-medium", children: [
508
- typeof item.likeCount === "number" && /* @__PURE__ */ jsx(
509
- EngagementBadge,
510
- {
511
- iconName: "lucide/heart",
512
- count: item.likeCount,
513
- label: "likes"
514
- }
515
- ),
516
- typeof item.commentCount === "number" && /* @__PURE__ */ jsx(
517
- EngagementBadge,
518
- {
519
- iconName: "lucide/message-circle",
520
- count: item.commentCount,
521
- label: "comments"
522
- }
523
- ),
524
- typeof item.viewCount === "number" && /* @__PURE__ */ jsx(
525
- EngagementBadge,
526
- {
527
- iconName: "lucide/eye",
528
- count: item.viewCount,
529
- label: "views"
530
- }
531
- ),
532
- item.date && /* @__PURE__ */ jsx("span", { className: "ml-auto opacity-80", children: item.date })
533
- ] })
534
- ] })
535
- ]
536
- },
537
- item.id
741
+ const mediaItems = useMemo(() => {
742
+ if (itemsSlot || !items) return [];
743
+ const withImage = items.filter((item) => Boolean(item.image));
744
+ if (process.env.NODE_ENV !== "production" && withImage.length < items.length) {
745
+ const skipped = items.filter((item) => !item.image);
746
+ const skippedIds = skipped.map((item) => item.id).join(", ");
747
+ console.warn(
748
+ `[instagram-post-grid] skipped ${skipped.length} item(s) without a resolvable image: ${skippedIds}`
538
749
  );
539
- });
540
- }, [itemsSlot, items, itemClassName, imageClassName, optixFlowConfig]);
750
+ }
751
+ return withImage.map(toMediaItem);
752
+ }, [items, itemsSlot]);
541
753
  if (!itemsSlot && (!items || items.length === 0)) {
542
754
  return null;
543
755
  }
@@ -575,16 +787,34 @@ function InstagramPostGrid({
575
787
  }
576
788
  ) : /* @__PURE__ */ jsx("div", { className: subheadingClassName, children: subheading }))
577
789
  ] }),
578
- /* @__PURE__ */ jsx(
790
+ itemsSlot ? /* @__PURE__ */ jsx(
579
791
  "div",
580
792
  {
581
793
  className: cn(
582
- "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4",
794
+ "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4",
583
795
  gridClassName
584
796
  ),
585
- children: tiles
797
+ children: itemsSlot
586
798
  }
587
- )
799
+ ) : /* @__PURE__ */ jsxs(ImmersiveFeedProvider, { items: mediaItems, actions: VIEWER_ACTIONS, children: [
800
+ /* @__PURE__ */ jsx(
801
+ InstagramFeedGrid,
802
+ {
803
+ mediaItems,
804
+ gridClassName,
805
+ itemClassName,
806
+ imageClassName,
807
+ optixFlowConfig
808
+ }
809
+ ),
810
+ /* @__PURE__ */ jsx(
811
+ ImmersiveViewer,
812
+ {
813
+ ariaLabel: "Instagram post viewer",
814
+ renderActions: ({ item }) => /* @__PURE__ */ jsx(InstagramViewerRail, { item })
815
+ }
816
+ )
817
+ ] })
588
818
  ]
589
819
  }
590
820
  );