From d0b15bb90d4764f5dddc5d85e8274a99372500c6 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Thu, 4 Jun 2026 12:26:49 -0400 Subject: [PATCH] Per-image carousel badge + glass pager dots Move the in-feed carousel "{n}/{total}" badge from the carousel container's top-right onto each image's top-right so every slide carries its own index, and give the Lightbox PagerDots the same blurred dark pill background as CircleChromeButton. --- src/components/Lightbox/chrome/PagerDots.tsx | 43 +++++---- .../Lightbox/chrome/PagerDots.web.tsx | 62 ++++++++++++ src/components/images/Gallery/index.tsx | 96 ++++++------------- 3 files changed, 117 insertions(+), 84 deletions(-) create mode 100644 src/components/Lightbox/chrome/PagerDots.web.tsx diff --git a/src/components/Lightbox/chrome/PagerDots.tsx b/src/components/Lightbox/chrome/PagerDots.tsx index a5fea5d4de..ea3cbc7f17 100644 --- a/src/components/Lightbox/chrome/PagerDots.tsx +++ b/src/components/Lightbox/chrome/PagerDots.tsx @@ -1,6 +1,5 @@ import {StyleSheet, View} from 'react-native' - -import {atoms as a} from '#/alf' +import {BlurView} from 'expo-blur' type Props = { count: number @@ -14,26 +13,38 @@ const GAP = 5 export function PagerDots({count, activeIndex}: Props) { if (count <= 1) return null return ( - - {Array.from({length: count}).map((_, i) => { - const isActive = i === activeIndex - return ( - - ) - })} + + + {Array.from({length: count}).map((_, i) => { + const isActive = i === activeIndex + return ( + + ) + })} + ) } const styles = StyleSheet.create({ - row: { + root: { + borderRadius: 999, + overflow: 'hidden', + }, + inner: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', gap: GAP, + paddingHorizontal: 10, + paddingVertical: 6, + backgroundColor: 'rgba(0, 0, 0, 0.5)', }, activeDot: { width: ACTIVE, diff --git a/src/components/Lightbox/chrome/PagerDots.web.tsx b/src/components/Lightbox/chrome/PagerDots.web.tsx new file mode 100644 index 0000000000..370917254f --- /dev/null +++ b/src/components/Lightbox/chrome/PagerDots.web.tsx @@ -0,0 +1,62 @@ +import {StyleSheet, View} from 'react-native' + +type Props = { + count: number + activeIndex: number +} + +const ACTIVE = 6 +const INACTIVE = 4 +const GAP = 5 + +export function PagerDots({count, activeIndex}: Props) { + if (count <= 1) return null + return ( + + {Array.from({length: count}).map((_, i) => { + const isActive = i === activeIndex + return ( + + ) + })} + + ) +} + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: GAP, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 999, + backgroundColor: 'rgba(0, 0, 0, 0.75)', + // @ts-expect-error web-only + backdropFilter: 'blur(8px)', + WebkitBackdropFilter: 'blur(8px)', + }, + activeDot: { + width: ACTIVE, + height: ACTIVE, + borderRadius: ACTIVE / 2, + }, + inactiveDot: { + width: INACTIVE, + height: INACTIVE, + borderRadius: INACTIVE / 2, + }, + active: { + backgroundColor: '#fff', + }, + inactive: { + backgroundColor: 'rgba(255, 255, 255, 0.4)', + }, +}) diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 8f722959cf..58df7fe431 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -165,13 +165,6 @@ export function Gallery({ const containerRefsRef = useRef>>(new Map()) const thumbDimsRef = useRef>(new Map()) const currentIndexRef = useRef(0) - /* - * Reactive mirror of currentIndexRef for the numbering badge. The ref drives - * scroll/keyboard logic without re-rendering; this state only updates when - * the index actually changes (once per image, not per scroll frame), so the - * badge stays live and cheap. - */ - const [currentIndex, setCurrentIndexState] = useState(0) const emitSwipeMetric = useMemo( () => @@ -189,7 +182,6 @@ export function Gallery({ const prev = currentIndexRef.current if (prev !== index) { currentIndexRef.current = index - setCurrentIndexState(index) emitSwipeMetric(prev, index) } } @@ -356,70 +348,10 @@ export function Gallery({ }} /> - - {!hideBadges && images.length > 1 ? ( - - ) : null} ) } -/* - * Top-right "{n}/{total}" badge for the in-feed carousel. The visible pill is - * decorative (accessible={false}) since the digits are redundant for screen - * reader users. - * - * On native, screen reader users never reach this carousel branch - the Gallery - * renders a separate per-image stack early when screenReaderEnabled is true, - * and each slide there is labelled "Image N of M". So the live region only - * needs to exist on web, where screenReaderEnabled is always false and the - * carousel is the only branch. There we announce position changes via a - * visually-hidden aria-live region. - */ -function GalleryCounter({ - currentIndex, - imageCount, -}: { - currentIndex: number - imageCount: number -}) { - const {t: l} = useLingui() - - return ( - <> - - - {currentIndex + 1}/{imageCount} - - - - {IS_WEB ? ( - - {l`Image ${currentIndex + 1} of ${imageCount}`} - - ) : null} - - ) -} - function computeDims({ height, aspectRatio, @@ -551,6 +483,34 @@ function GalleryImage({ useAppleWebpCodec /> + {!hideBadges && imageCount > 1 ? ( + + + {index + 1}/{imageCount} + + + ) : null} + {(hasAlt || isCropped) && !hideBadges ? (