Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.7 KiB
Lightbox Refresh — Design Spec
Ticket: APP-2046
Figma: https://www.figma.com/design/TqFguaWhkvAPnCth5u7cvE/Everything?node-id=7221-2117
Branch: app-2046
PR strategy: one PR — migration + reskin together
Summary
Refresh lightbox chrome and co-locate all lightbox code under /features/lightbox/.
Native gets a new visual treatment (translucent circular buttons, dots pagination,
menu-based Share/Save). Web gets a minimal reskin of buttons and menu, no layout
changes. All gestures, animations, pager mechanics, and the underlying image
viewer are preserved.
Scope
In scope
- Move every lightbox file into
/features/lightbox/with a light reorg. - Rebuild native chrome (header, footer, pager dots, image menu).
- Reskin web chrome buttons to match native's translucent treatment.
- Add
Share imagemenu item on web (currently onlyDownload image). - Restyle alt-text strip on both platforms to match.
Out of scope
- Reaction bar (labeled "Inactive" in Figma).
- Web layout changes — keep chevron navigation, keep alt-text expand behavior, keep keyboard shortcuts.
- Gesture code (pinch/pan/zoom/swipe-to-dismiss) — file moves only.
- Open/close spring animation and backdrop fade — file moves only.
- Alt-text interaction model — kept, just restyled.
Target file structure
/src/features/lightbox/
├── index.tsx # re-exports Lightbox (platform split)
├── Lightbox.tsx # native shell (from view/com/lightbox/Lightbox.tsx)
├── Lightbox.web.tsx # web impl (from view/com/lightbox/Lightbox.web.tsx)
├── state.tsx # from state/lightbox.tsx
├── types.ts # from ImageViewing/@types/index.ts
├── chrome/
│ ├── CircleChromeButton.tsx # NEW — shared primitive for ••• and ✕
│ ├── Header.tsx # NEW — replaces ImageDefaultHeader
│ ├── Footer.tsx # NEW — replaces LightboxFooter
│ ├── PagerDots.tsx # NEW — dots indicator (native only)
│ └── ImageMenu.tsx # NEW — ContextMenu wrapper
└── pager/
├── ImagePager.tsx # from ImageViewing/index.tsx
├── transforms.ts # from ImageViewing/transforms.ts
└── ImageItem/
├── ImageItem.tsx
├── ImageItem.ios.tsx
└── ImageItem.android.tsx
Source-to-destination map
| From | To |
|---|---|
src/state/lightbox.tsx |
src/features/lightbox/state.tsx |
src/view/com/lightbox/Lightbox.tsx |
src/features/lightbox/Lightbox.tsx |
src/view/com/lightbox/Lightbox.web.tsx |
src/features/lightbox/Lightbox.web.tsx |
src/view/com/lightbox/ImageViewing/index.tsx |
src/features/lightbox/pager/ImagePager.tsx |
src/view/com/lightbox/ImageViewing/transforms.ts |
src/features/lightbox/pager/transforms.ts |
src/view/com/lightbox/ImageViewing/@types/index.ts |
src/features/lightbox/types.ts |
src/view/com/lightbox/ImageViewing/components/ImageItem/* |
src/features/lightbox/pager/ImageItem/* |
src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx |
deleted — replaced by chrome/Header.tsx |
Consumer import updates
All the imports that must move from #/state/lightbox → #/features/lightbox/state or
#/view/com/lightbox/Lightbox → #/features/lightbox:
State (LightboxStateProvider, useLightbox, useLightboxControls, type Lightbox):
src/App.native.tsxsrc/App.web.tsxsrc/state/util.tssrc/screens/Profile/Header/Shell.tsxsrc/components/Post/Embed/ImageEmbed.tsxsrc/view/com/profile/ProfileSubpageHeader.tsxsrc/view/com/util/List.tsx
Lightbox component itself:
src/view/shell/index.tsxsrc/view/shell/index.web.tsx
Internal imports within the lightbox files themselves will also update (e.g.,
ImagePager.tsx importing the type from sibling state.tsx).
Native chrome design
chrome/CircleChromeButton.tsx
- Shared primitive used by
•••and✕triggers, and reused on web. - 36 × 36 pressable circle.
- Background:
rgba(0, 0, 0, 0.45)— fixed, not theme-driven, because it sits over arbitrary images. - Icon: white, 20px.
hitSlop: 10.
chrome/Header.tsx
- Absolutely-positioned row,
top: insets.top,px_md,py_sm. - Left slot:
ImageMenutrigger (•••). - Right slot:
CircleChromeButtonwithXIcon, closes lightbox. - Show/hide driven by the existing
showControlsshared value inImagePager.tsx(opacity + translateY). No new gesture logic.
chrome/ImageMenu.tsx
- Wraps
ContextMenu.Outer+ContextMenu.Trigger. - Trigger child renders
CircleChromeButtonwithDotsHorizontalicon. onTaphandler callscontrol.open('full')so single tap opens — no press-and-hold.- Two items:
Share image→ShareIcon, calls existing share util.Save image→Download, calls existing save util.
- Menu card surface is theme-aware (white in light mode, dark in dark mode) —
inherited from
ContextMenu's defaults.
chrome/Footer.tsx
- Absolutely-positioned,
bottom: insets.bottom,px_md,pb_sm. - Renders
AltTextStrip(if current image has alt text) abovePagerDots. AltTextStrip:- Translucent dark surface:
rgba(0, 0, 0, 0.45). - White text,
text_sm. - Tap expands/collapses (port the expand/collapse state logic from
Lightbox.web.tsxlines 227-245).
- Translucent dark surface:
chrome/PagerDots.tsx
- Renders only when
images.length > 1. - Bottom-center row.
- Active dot:
6 × 6,bg: white. - Inactive dot:
6 × 6,bg: rgba(255,255,255,0.4). - Gap:
4px. - Active index driven by the existing
imageIndexshared value the pager exposes.
Web chrome design
Changes in Lightbox.web.tsx are visual only:
- Replace the existing menu trigger button (lines 251-296) and close button
(lines 297-312) with
CircleChromeButtonfrom the native chrome folder. (It's a plain styled Pressable — platform-agnostic.) - Menu items:
- Existing
Download imagestays. - NEW
Share image— uses Web Share API when available, falls back to copy-link-to-clipboard.
- Existing
- Restyle alt-text strip to match native's translucent treatment. Keep expand/collapse behavior.
Web out of scope (confirmed)
- Left/right chevron navigation (lines 184-225).
- Screen-reader "Image N of M" announcement (line 246-250).
- Keyboard shortcut registration / hotkey scope management.
- No dots indicator on web (chevrons + "Image N of M" already serve this role).
Preserved behaviors
Both platforms keep:
- Tap-to-toggle chrome visibility.
- Swipe-down-to-dismiss (native).
- Pinch/pan/double-tap zoom.
- Horizontal pager between images.
- Open/close spring from source thumbnail.
expo-screen-orientationlock on open, unlock on close.- Hotkey scope disable while lightbox is open.
Boundaries
chrome/depends only on#/componentsand the state module. It knows nothing about gestures or pager internals.pager/owns gestures, layout, animation. Chrome is rendered via props/slots.state.tsxexports are unchanged — pure move with updated import paths.CircleChromeButtonis reused across native header and web chrome — one visual source of truth.
Testing & verification
- Visual inspection on iOS simulator, Android emulator, web.
- Verify open/close animation unchanged from main.
- Verify tap-to-toggle chrome still works.
- Verify share and save still work end-to-end from the new menu.
- Verify alt text still displays and expands.
- Confirm no new TypeScript errors:
yarn typecheck. - Confirm no new lint errors:
yarn lint. - Confirm Jest still passes:
yarn test.