diff --git a/.gitignore b/.gitignore index c0adc37228..27b7ac9ded 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ bskyweb/static/media/*.webp bskyweb/static/media/*.jpg bskyweb/static/media/*.png bskyweb/static/media/*.svg + +# superpowers plugin plans/specs — local-only workspace +docs/superpowers/ diff --git a/docs/superpowers/plans/2026-04-21-lightbox-refresh.md b/docs/superpowers/plans/2026-04-21-lightbox-refresh.md deleted file mode 100644 index 3718fcc89b..0000000000 --- a/docs/superpowers/plans/2026-04-21-lightbox-refresh.md +++ /dev/null @@ -1,1042 +0,0 @@ -# Lightbox Refresh Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Co-locate all lightbox code under `/features/lightbox/` and refresh the chrome (native = full redesign; web = light reskin). - -**Architecture:** Phase 1 is a pure file migration. Phase 2 builds a shared circle button primitive. Phase 3 builds four new native chrome components (header, footer, menu, pager dots) and wires them into the existing pager. Phase 4 applies a light reskin on web. Phase 5 is end-to-end verification. Every phase leaves the app runnable. - -**Tech Stack:** React Native 0.81 / Expo 54, TypeScript, ALF design system, React Compiler, Reanimated V3, Gesture Handler, Lingui for i18n. Existing `#/components/ContextMenu` is reused for the native menu popover; existing `#/components/Menu` (radix dropdown) is reused on web. - -**Ticket:** APP-2046 · **Spec:** `docs/superpowers/specs/2026-04-21-lightbox-refresh-design.md` - -**Testing note:** This codebase has no UI rendering tests — existing Jest tests are for pure logic only. Verification for this work is `yarn typecheck` + `yarn lint` + `yarn test` + manual smoke on iOS simulator, Android emulator, and web. Do NOT invent new UI unit tests; rely on type-checking and visual verification. - ---- - -## Phase 1 — File migration (pure moves, no behavior change) - -### Task 1: Move state module and update all consumers - -**Files:** -- Create: `src/features/lightbox/state.tsx` (identical content to `src/state/lightbox.tsx`) -- Delete: `src/state/lightbox.tsx` -- Modify: 7 consumers listed below - -- [ ] **Step 1: Move the file with git mv** - -```bash -mkdir -p src/features/lightbox -git mv src/state/lightbox.tsx src/features/lightbox/state.tsx -``` - -- [ ] **Step 2: Update every consumer import** - -Change the import path in each of these files from `#/state/lightbox` to `#/features/lightbox/state`: - -| File | Line | Exports used | -|---|---|---| -| `src/App.native.tsx` | 34 | `Provider as LightboxStateProvider` | -| `src/App.web.tsx` | 27 | `Provider as LightboxStateProvider` | -| `src/state/util.ts` | 4 | `useLightboxControls` | -| `src/screens/Profile/Header/Shell.tsx` | 17 | `useLightboxControls` | -| `src/components/Post/Embed/ImageEmbed.tsx` | 5 | `useLightboxControls` | -| `src/view/com/profile/ProfileSubpageHeader.tsx` | 16 | `useLightboxControls` | -| `src/view/com/util/List.tsx` | 15 | `useLightbox` | - -Use find-and-replace (case-sensitive) across those files: -- Find: `'#/state/lightbox'` -- Replace: `'#/features/lightbox/state'` - -- [ ] **Step 3: Update references inside the still-existing lightbox files** - -Two files in the old location still reference `#/state/lightbox` by path — update them too so Phase 1 is self-consistent: - -- `src/view/com/lightbox/Lightbox.tsx:5` -- `src/view/com/lightbox/Lightbox.web.tsx:12` -- `src/view/com/lightbox/ImageViewing/index.tsx:47` - -Same find-and-replace. - -- [ ] **Step 4: Verify compile** - -```bash -yarn typecheck -``` - -Expected: no errors referencing `#/state/lightbox`. - -- [ ] **Step 5: Commit** - -```bash -git add -u src/features/lightbox/state.tsx src/App.native.tsx src/App.web.tsx src/state/util.ts src/screens/Profile/Header/Shell.tsx src/components/Post/Embed/ImageEmbed.tsx src/view/com/profile/ProfileSubpageHeader.tsx src/view/com/util/List.tsx src/view/com/lightbox/Lightbox.tsx src/view/com/lightbox/Lightbox.web.tsx src/view/com/lightbox/ImageViewing/index.tsx -git commit -m "refactor(lightbox): move state module to features/lightbox/state" -``` - ---- - -### Task 2: Move the pager and image-item files into `features/lightbox/pager/` - -**Files:** -- `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/ImageItem.tsx` → `src/features/lightbox/pager/ImageItem/ImageItem.tsx` -- `src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx` → `src/features/lightbox/pager/ImageItem/ImageItem.ios.tsx` -- `src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx` → `src/features/lightbox/pager/ImageItem/ImageItem.android.tsx` -- `src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx` → **keep in place for now** (will be deleted in Task 10 when replaced) - -- [ ] **Step 1: Create directories and move files** - -```bash -mkdir -p src/features/lightbox/pager/ImageItem -git mv src/view/com/lightbox/ImageViewing/index.tsx src/features/lightbox/pager/ImagePager.tsx -git mv src/view/com/lightbox/ImageViewing/transforms.ts src/features/lightbox/pager/transforms.ts -git mv src/view/com/lightbox/ImageViewing/@types/index.ts src/features/lightbox/types.ts -git mv src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx src/features/lightbox/pager/ImageItem/ImageItem.tsx -git mv src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx src/features/lightbox/pager/ImageItem/ImageItem.ios.tsx -git mv src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx src/features/lightbox/pager/ImageItem/ImageItem.android.tsx -``` - -- [ ] **Step 2: Fix internal imports inside moved files** - -In `src/features/lightbox/pager/ImagePager.tsx`, update relative imports: -- `from './@types'` or `from './@types/index'` → `from '../types'` -- `from './transforms'` stays as is (still adjacent) -- `from './components/ImageItem/ImageItem'` → `from './ImageItem/ImageItem'` -- `from './components/ImageDefaultHeader'` → `from '../../../view/com/lightbox/ImageViewing/components/ImageDefaultHeader'` - (yes, that's an ugly backward relative path — it is intentional and short-lived. `ImageDefaultHeader` is replaced in Task 10, at which point the import and the file both get deleted.) - -In each `ImageItem*.tsx`, update: -- `from '../../@types'` or similar → `from '../../types'` -- `from '../../transforms'` → `from '../transforms'` - -Use grep to double-check no stale relative paths remain: - -```bash -grep -rn "@types\|ImageViewing\|components/ImageItem" src/features/lightbox -``` - -Expected: no hits except the intentional `ImageDefaultHeader` import noted above (if any). - -- [ ] **Step 3: Fix the external import in `Lightbox.tsx` (still in old location)** - -The native `Lightbox.tsx` wrapper at `src/view/com/lightbox/Lightbox.tsx:6` imports: -```tsx -import ImageView from './ImageViewing' -``` -That path no longer resolves. Update to: -```tsx -import ImageView from '#/features/lightbox/pager/ImagePager' -``` - -- [ ] **Step 4: Verify compile** - -```bash -yarn typecheck -``` - -Expected: no errors. If errors mention missing `@types` or `ImageViewing`, fix the relative path and re-run. - -- [ ] **Step 5: Commit** - -```bash -git add -u -git commit -m "refactor(lightbox): move pager files to features/lightbox/pager" -``` - ---- - -### Task 3: Move `Lightbox.tsx` / `Lightbox.web.tsx` into `features/lightbox/` and update shell imports - -**Files:** -- `src/view/com/lightbox/Lightbox.tsx` → `src/features/lightbox/Lightbox.tsx` -- `src/view/com/lightbox/Lightbox.web.tsx` → `src/features/lightbox/Lightbox.web.tsx` -- Create: `src/features/lightbox/index.tsx` (platform-neutral re-export) -- Modify: `src/view/shell/index.tsx`, `src/view/shell/index.web.tsx` - -- [ ] **Step 1: Move the two Lightbox files** - -```bash -git mv src/view/com/lightbox/Lightbox.tsx src/features/lightbox/Lightbox.tsx -git mv src/view/com/lightbox/Lightbox.web.tsx src/features/lightbox/Lightbox.web.tsx -``` - -- [ ] **Step 2: Fix the import in the moved native `Lightbox.tsx`** - -The file previously imported from `#/features/lightbox/pager/ImagePager` (set in Task 2). That import remains correct — verify it still reads: - -```tsx -import ImageView from '#/features/lightbox/pager/ImagePager' -``` - -- [ ] **Step 3: Create `src/features/lightbox/index.tsx`** - -This file re-exports `Lightbox` so consumers can import from the directory root. Bundler resolves `Lightbox.tsx` / `Lightbox.web.tsx` automatically. - -```tsx -export {Lightbox} from './Lightbox' -``` - -- [ ] **Step 4: Update shell imports** - -In `src/view/shell/index.tsx:22` and `src/view/shell/index.web.tsx:13`, change: - -```tsx -import {Lightbox} from '#/view/com/lightbox/Lightbox' -``` - -to: - -```tsx -import {Lightbox} from '#/features/lightbox' -``` - -- [ ] **Step 5: Remove the now-empty old directory** - -```bash -# The only file that should remain is ImageDefaultHeader.tsx (deleted in Task 10). -ls src/view/com/lightbox/ -# Expected output: -# ImageViewing/ -# with ImageViewing/components/ImageDefaultHeader.tsx as the only file inside. -``` - -If there are empty intermediate dirs (e.g. `@types/`, `components/ImageItem/`), remove them: - -```bash -find src/view/com/lightbox -type d -empty -delete -``` - -- [ ] **Step 6: Verify compile and run the web build briefly** - -```bash -yarn typecheck -yarn lint src/features/lightbox src/view/shell -``` - -Expected: clean. - -- [ ] **Step 7: Commit** - -```bash -git add -u src/features/lightbox src/view/shell/index.tsx src/view/shell/index.web.tsx -git commit -m "refactor(lightbox): move Lightbox shell to features/lightbox" -``` - ---- - -### Task 4: Smoke-test the migration - -No code changes. This is a pure verification checkpoint before starting the reskin. - -- [ ] **Step 1: Typecheck and lint full repo** - -```bash -yarn typecheck -yarn lint -``` - -Expected: clean (or no NEW errors compared to `main`). - -- [ ] **Step 2: Run Jest** - -```bash -yarn test -``` - -Expected: pass rate identical to `main`. - -- [ ] **Step 3: Visual smoke** - -Run the app on iOS simulator and web. Open a post with an image, tap to open lightbox, swipe between images, tap Save + Share, tap close. Confirm everything behaves identically to `main` — this task ends the migration phase. - -No commit (verification only). If anything broke, go back and fix before proceeding. - ---- - -## Phase 2 — Shared chrome primitive - -### Task 5: Add `CircleChromeButton` - -The reusable translucent circle button used for `•••` and `✕` on both native and web. - -**Files:** -- Create: `src/features/lightbox/chrome/CircleChromeButton.tsx` - -- [ ] **Step 1: Create the directory** - -```bash -mkdir -p src/features/lightbox/chrome -``` - -- [ ] **Step 2: Write `CircleChromeButton.tsx`** - -```tsx -import {type ComponentType} from 'react' -import {Pressable, type PressableProps, StyleSheet} from 'react-native' - -import {HITSLOP_10} from '#/lib/constants' -import {type Props as IconProps} from '#/components/icons/common' - -type Props = { - icon: ComponentType - label: string - onPress?: PressableProps['onPress'] - testID?: string -} - -const SIZE = 36 -const BG = 'rgba(0, 0, 0, 0.45)' - -export function CircleChromeButton({icon: Icon, label, onPress, testID}: Props) { - return ( - [styles.root, pressed && styles.pressed]}> - - - ) -} - -const styles = StyleSheet.create({ - root: { - width: SIZE, - height: SIZE, - borderRadius: SIZE / 2, - backgroundColor: BG, - alignItems: 'center', - justifyContent: 'center', - }, - pressed: { - opacity: 0.85, - }, -}) -``` - -- [ ] **Step 3: Verify compile** - -```bash -yarn typecheck -``` - -Expected: clean. - -- [ ] **Step 4: Commit** - -```bash -git add src/features/lightbox/chrome/CircleChromeButton.tsx -git commit -m "feat(lightbox): add CircleChromeButton primitive" -``` - ---- - -## Phase 3 — Native chrome rebuild - -### Task 6: Add `ImageMenu` (the `•••` menu wrapper) - -Uses `#/components/ContextMenu` so the menu renders as a floating card anchored under the trigger on iOS/Android. Triggered on single tap. - -**Files:** -- Create: `src/features/lightbox/chrome/ImageMenu.tsx` - -- [ ] **Step 1: Write `ImageMenu.tsx`** - -```tsx -import {Pressable} from 'react-native' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' - -import {atoms as a} from '#/alf' -import * as ContextMenu from '#/components/ContextMenu' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox' -import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid' -import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' -import {CircleChromeButton} from './CircleChromeButton' - -type Props = { - onPressShare: () => void - onPressSave: () => void -} - -export function ImageMenu({onPressShare, onPressSave}: Props) { - const {_} = useLingui() - - return ( - - - {triggerProps => { - if (triggerProps.IS_NATIVE) { - return ( - triggerProps.control.open('full')} - style={a.self_start}> - - - ) - } - return ( - - ) - }} - - - - - - {_(msg`Share image`)} - - - - {_(msg`Save image`)} - - - - ) -} -``` - -Note the exact API shapes are taken from `src/components/dms/MessageContextMenu.tsx`. If the `TriggerChildProps` narrowing by `IS_NATIVE` gives typescript trouble, cross-check that file. - -- [ ] **Step 2: Verify compile** - -```bash -yarn typecheck -``` - -Expected: clean. - -- [ ] **Step 3: Commit** - -```bash -git add src/features/lightbox/chrome/ImageMenu.tsx -git commit -m "feat(lightbox): add ImageMenu for share/save actions" -``` - ---- - -### Task 7: Add `Header` (top chrome row) - -**Files:** -- Create: `src/features/lightbox/chrome/Header.tsx` - -- [ ] **Step 1: Write `Header.tsx`** - -```tsx -import {StyleSheet, View} from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' - -import {atoms as a} from '#/alf' -import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times' -import {CircleChromeButton} from './CircleChromeButton' -import {ImageMenu} from './ImageMenu' - -type Props = { - onRequestClose: () => void - onPressShare: () => void - onPressSave: () => void -} - -export function Header({onRequestClose, onPressShare, onPressSave}: Props) { - const {_} = useLingui() - const insets = useSafeAreaInsets() - - return ( - - - - - ) -} - -const styles = StyleSheet.create({ - root: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - }, -}) -``` - -- [ ] **Step 2: Verify compile** - -```bash -yarn typecheck -``` - -Expected: clean. - -- [ ] **Step 3: Commit** - -```bash -git add src/features/lightbox/chrome/Header.tsx -git commit -m "feat(lightbox): add new Header with ••• menu and close" -``` - ---- - -### Task 8: Add `PagerDots` - -**Files:** -- Create: `src/features/lightbox/chrome/PagerDots.tsx` - -- [ ] **Step 1: Write `PagerDots.tsx`** - -```tsx -import {StyleSheet, View} from 'react-native' - -import {atoms as a} from '#/alf' - -type Props = { - count: number - activeIndex: number -} - -const DOT = 6 -const GAP = 4 - -export function PagerDots({count, activeIndex}: Props) { - if (count <= 1) return null - return ( - - {Array.from({length: count}).map((_, i) => ( - - ))} - - ) -} - -const styles = StyleSheet.create({ - row: { - gap: GAP, - }, - dot: { - width: DOT, - height: DOT, - borderRadius: DOT / 2, - }, - active: { - backgroundColor: '#fff', - }, - inactive: { - backgroundColor: 'rgba(255, 255, 255, 0.4)', - }, -}) -``` - -- [ ] **Step 2: Verify compile** - -```bash -yarn typecheck -``` - -Expected: clean. - -- [ ] **Step 3: Commit** - -```bash -git add src/features/lightbox/chrome/PagerDots.tsx -git commit -m "feat(lightbox): add PagerDots indicator" -``` - ---- - -### Task 9: Add `Footer` (alt text + pager dots) - -**Files:** -- Create: `src/features/lightbox/chrome/Footer.tsx` - -- [ ] **Step 1: Write `Footer.tsx`** - -```tsx -import {useRef} from 'react' -import {LayoutAnimation, Pressable, ScrollView, StyleSheet, View} from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' - -import {atoms as a} from '#/alf' -import {Text} from '#/components/Typography' -import {PagerDots} from './PagerDots' - -type Props = { - altText: string | undefined - isAltExpanded: boolean - onToggleAltExpanded: () => void - imageCount: number - activeIndex: number -} - -export function Footer({ - altText, - isAltExpanded, - onToggleAltExpanded, - imageCount, - activeIndex, -}: Props) { - const {_} = useLingui() - const insets = useSafeAreaInsets() - const isMomentumScrolling = useRef(false) - - return ( - - {altText ? ( - - { - isMomentumScrolling.current = true - }} - onMomentumScrollEnd={() => { - isMomentumScrolling.current = false - }} - contentContainerStyle={[a.px_md, a.py_sm]}> - { - if (isMomentumScrolling.current) return - LayoutAnimation.configureNext({ - duration: 450, - update: {type: 'spring', springDamping: 1}, - }) - onToggleAltExpanded() - }}> - - {altText} - - - - - ) : null} - - - ) -} - -const styles = StyleSheet.create({ - root: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - }, - altWrap: { - backgroundColor: 'rgba(0, 0, 0, 0.45)', - borderRadius: 12, - overflow: 'hidden', - }, - altText: { - color: '#fff', - }, -}) -``` - -- [ ] **Step 2: Verify compile** - -```bash -yarn typecheck -``` - -Expected: clean. - -- [ ] **Step 3: Commit** - -```bash -git add src/features/lightbox/chrome/Footer.tsx -git commit -m "feat(lightbox): add new Footer with alt text and pager dots" -``` - ---- - -### Task 10: Wire new chrome into `ImagePager.tsx` and delete old chrome - -Replace the two chrome blocks currently in `src/features/lightbox/pager/ImagePager.tsx` at lines ~417-433. Delete `ImageDefaultHeader.tsx` and the `LightboxFooter` function inside `ImagePager.tsx`. - -**Files:** -- Modify: `src/features/lightbox/pager/ImagePager.tsx` -- Delete: `src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx` -- Delete: the now-empty `src/view/com/lightbox/` tree - -- [ ] **Step 1: Open `ImagePager.tsx` and find the chrome block** - -Near line 416 there is a `` with two animated children: `ImageDefaultHeader` and `LightboxFooter`. Replace both with the new `Header` and `Footer`. - -Replace this block: - -```tsx - - - - - - setIsAltExpanded(e => !e)} - onPressSave={onPressSave} - onPressShare={onPressShare} - /> - - -``` - -With: - -```tsx - - -
onPressShare(images[imageIndex].uri)} - onPressSave={() => onPressSave(images[imageIndex].uri)} - /> - - -