This commit is contained in:
vineyardbovines
2026-04-15 08:32:07 -04:00
parent 91b64a2844
commit 651ab90aa1
9 changed files with 603 additions and 2 deletions
@@ -0,0 +1,167 @@
<h2>GIF Category Pills — Full Design</h2>
<p class="subtitle">Horizontal scrollable pill row on the idle GIF picker screen</p>
<div class="section">
<h3>Interaction Flow</h3>
<div style="display:flex;gap:16px;flex-wrap:wrap">
<!-- State 1: Idle / Trending selected -->
<div style="flex:1;min-width:220px">
<div class="mockup">
<div class="mockup-header">1. Idle — "Trending" active</div>
<div class="mockup-body" style="padding:12px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0;font-size:13px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="opacity:0.4">&#128269;</span>
<span style="opacity:0.4">Search GIFs</span>
</div>
<div style="display:flex;gap:6px;margin-bottom:10px;overflow-x:auto;font-size:12px">
<div style="background:#4a4a7a;border-radius:16px;padding:5px 12px;white-space:nowrap">&#128293; Trending</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:16px;padding:5px 12px;white-space:nowrap">&#10084;&#65039; Love</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:16px;padding:5px 12px;white-space:nowrap">&#128516; Happy</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:16px;padding:5px 12px;white-space:nowrap;opacity:0.5">...</div>
</div>
<div style="display:flex;gap:6px">
<div style="flex:1;background:#444;border-radius:6px;height:50px"></div>
<div style="flex:1;background:#444;border-radius:6px;height:40px"></div>
</div>
<div style="text-align:center;margin-top:6px;font-size:10px;opacity:0.4">Featured GIFs</div>
</div>
</div>
</div>
<!-- State 2: Category selected -->
<div style="flex:1;min-width:220px">
<div class="mockup">
<div class="mockup-header">2. "Happy" tapped</div>
<div class="mockup-body" style="padding:12px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0;font-size:13px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="opacity:0.4">&#128269;</span>
<span style="opacity:0.4">Search GIFs</span>
</div>
<div style="display:flex;gap:6px;margin-bottom:10px;overflow-x:auto;font-size:12px">
<div style="background:#2a2a4a;border:1px solid #444;border-radius:16px;padding:5px 12px;white-space:nowrap">&#128293; Trending</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:16px;padding:5px 12px;white-space:nowrap">&#10084;&#65039; Love</div>
<div style="background:#4a4a7a;border-radius:16px;padding:5px 12px;white-space:nowrap">&#128516; Happy</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:16px;padding:5px 12px;white-space:nowrap;opacity:0.5">...</div>
</div>
<div style="display:flex;gap:6px">
<div style="flex:1;background:#555;border-radius:6px;height:50px"></div>
<div style="flex:1;background:#555;border-radius:6px;height:40px"></div>
</div>
<div style="text-align:center;margin-top:6px;font-size:10px;opacity:0.4">Search results for "happy"</div>
</div>
</div>
</div>
<!-- State 3: User typing -->
<div style="flex:1;min-width:220px">
<div class="mockup">
<div class="mockup-header">3. Typing — pills hidden</div>
<div class="mockup-body" style="padding:12px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0;font-size:13px">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="opacity:0.4">&#128269;</span>
<span>sponge<span style="opacity:0.3">|</span></span>
</div>
<div style="display:flex;gap:6px">
<div style="flex:1;display:flex;flex-direction:column;gap:6px">
<div style="background:#555;border-radius:6px;height:50px"></div>
<div style="background:#555;border-radius:6px;height:40px"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:6px">
<div style="background:#555;border-radius:6px;height:40px"></div>
<div style="background:#555;border-radius:6px;height:50px"></div>
</div>
</div>
<div style="text-align:center;margin-top:6px;font-size:10px;opacity:0.4">Search results</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h3>Architecture</h3>
<div class="mockup">
<div class="mockup-header">Component & Data Flow</div>
<div class="mockup-body" style="padding:20px;font-family:system-ui;font-size:14px;line-height:1.6">
<div style="display:flex;gap:32px;flex-wrap:wrap">
<div style="flex:1;min-width:280px">
<div style="font-weight:600;margin-bottom:10px;color:#7c7cff;text-transform:uppercase;font-size:11px;letter-spacing:1px">Component Tree</div>
<div style="font-family:monospace;font-size:13px;line-height:2">
<div>GifPickerBody</div>
<div style="padding-left:20px">├─ GifPickerHeader</div>
<div style="padding-left:40px">│ └─ TextField.Input</div>
<div style="padding-left:20px">├─ <span style="color:#5cdb5c;font-weight:600">GifCategoryPills</span> <span style="color:#5cdb5c;opacity:0.7">← NEW</span></div>
<div style="padding-left:40px"><span style="opacity:0.5">(hidden when typing)</span></div>
<div style="padding-left:20px">├─ GifPickerPlaceholder</div>
<div style="padding-left:20px">└─ GifPickerGrid</div>
</div>
</div>
<div style="flex:1;min-width:280px">
<div style="font-weight:600;margin-bottom:10px;color:#ff7c7c;text-transform:uppercase;font-size:11px;letter-spacing:1px">Data Flow</div>
<div style="font-family:monospace;font-size:13px;line-height:2">
<div><span style="opacity:0.5">1.</span> Idle → "Trending" active → featured endpoint</div>
<div><span style="opacity:0.5">2.</span> Tap pill → set search to pill's searchterm</div>
<div><span style="opacity:0.5">3.</span> useGifPickerData sees search → fires GIF search</div>
<div><span style="opacity:0.5">4.</span> User types → pills hide, search takes over</div>
<div><span style="opacity:0.5">5.</span> User clears input → pills reappear, "Trending" resets</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h3>File Changes</h3>
<div style="font-family:monospace;font-size:13px;line-height:2.2;padding:0 8px">
<div><span style="color:#5cdb5c">+</span> <strong>src/features/gifPicker/components/GifCategoryPills.tsx</strong> — pill row component</div>
<div><span style="color:#ffcc00">~</span> <strong>src/features/gifPicker/GifPickerDialog.tsx</strong> — add category state, render pills in header, connect to search</div>
</div>
</div>
<div class="section">
<h3>Curated Categories</h3>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#128293; Trending</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#10084;&#65039; Love</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#128516; Happy</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#128546; Sad</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#127881; Party</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#128077; Yes</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#128514; LOL</div>
<div style="background:#2a2a4a;border:1px solid #555;border-radius:20px;padding:6px 14px;font-size:13px;font-family:system-ui">&#129321; Excited</div>
</div>
</div>
<div class="section">
<h3>Key Design Decisions</h3>
<div style="font-family:system-ui;font-size:14px;line-height:1.8;padding:0 8px">
<div><strong>No new API endpoint needed</strong> — tapping a category pill just sets the search query to the category's searchterm and uses the existing search flow</div>
<div><strong>"Trending" = featured endpoint</strong> — the default active pill uses the existing featured GIFs, no search query</div>
<div><strong>Pills hide while typing</strong> — simple visibility toggle based on whether rawSearch has content</div>
<div><strong>Clearing input resets to Trending</strong> — pills reappear with "Trending" active again</div>
<div><strong>Hardcoded category list</strong> — curated for quality, no extra API call needed</div>
</div>
</div>
<div class="section">
<h3>Scope</h3>
<div class="pros-cons">
<div class="pros">
<h4>In Scope</h4>
<ul>
<li>GifCategoryPills component (horizontal scroll, active state)</li>
<li>8 curated category pills with emoji + label</li>
<li>Tap pill → search GIFs for that category</li>
<li>Pills hide when typing, reappear when cleared</li>
<li>Active pill highlight styling</li>
</ul>
</div>
<div class="cons">
<h4>Out of Scope</h4>
<ul>
<li>Recent / Favorited (needs client-side persistence)</li>
<li>Dynamic categories from API</li>
<li>Category GIF thumbnails on the pills</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,106 @@
<h2>Category pills — where and how?</h2>
<p class="subtitle">These appear on the idle/featured GIF screen, before the user starts typing</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Horizontal scrollable row of text pills</h3>
<p>A single row of pill buttons below the search input. Scrolls horizontally. Compact, leaves maximum space for the GIF grid. Like how Threads does it.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Text pills — horizontal scroll</div>
<div class="mockup-body" style="padding:16px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="opacity:0.4">&#128269;</span>
<span style="opacity:0.4">Search GIFs</span>
</div>
<div style="display:flex;gap:8px;margin-bottom:14px;overflow-x:auto;padding-bottom:4px">
<div style="background:#3a3a5a;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128293; Trending</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#x2764;&#xFE0F; Love</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128516; Happy</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128546; Sad</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#127881; Party</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128077; Yes</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128557; LOL</div>
</div>
<div style="display:flex;gap:8px">
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#444;border-radius:8px;height:80px"></div>
<div style="background:#444;border-radius:8px;height:60px"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#444;border-radius:8px;height:60px"></div>
<div style="background:#444;border-radius:8px;height:80px"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Grid of GIF thumbnail tiles with labels</h3>
<p>A 2-3 column grid of category tiles, each showing the category's preview GIF with a text label overlay. More visual, but takes up more space and replaces the featured GIFs initially.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">GIF thumbnail grid</div>
<div class="mockup-body" style="padding:16px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="opacity:0.4">&#128269;</span>
<span style="opacity:0.4">Search GIFs</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:14px">
<div style="background:#333;border-radius:8px;height:70px;display:flex;align-items:flex-end;justify-content:center;padding:6px;font-size:12px;font-weight:600">&#128293; Trending</div>
<div style="background:#333;border-radius:8px;height:70px;display:flex;align-items:flex-end;justify-content:center;padding:6px;font-size:12px;font-weight:600">&#x2764;&#xFE0F; Love</div>
<div style="background:#333;border-radius:8px;height:70px;display:flex;align-items:flex-end;justify-content:center;padding:6px;font-size:12px;font-weight:600">&#128516; Happy</div>
<div style="background:#333;border-radius:8px;height:70px;display:flex;align-items:flex-end;justify-content:center;padding:6px;font-size:12px;font-weight:600">&#128546; Sad</div>
<div style="background:#333;border-radius:8px;height:70px;display:flex;align-items:flex-end;justify-content:center;padding:6px;font-size:12px;font-weight:600">&#127881; Party</div>
<div style="background:#333;border-radius:8px;height:70px;display:flex;align-items:flex-end;justify-content:center;padding:6px;font-size:12px;font-weight:600">&#128077; Yes</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="c" onclick="toggleSelect(this)">
<div class="letter">C</div>
<div class="content">
<h3>Horizontal pills + featured GIFs below (Threads-style)</h3>
<p>Exactly like Threads: category pills as a scrollable row, with featured/trending GIFs filling the grid below. Tapping a pill replaces the grid with results for that category. Best of both worlds.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Pills + featured grid (Threads)</div>
<div class="mockup-body" style="padding:16px;font-family:system-ui;background:#1a1a2e;color:#e0e0e0">
<div style="background:#2a2a4a;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;margin-bottom:12px">
<span style="opacity:0.4">&#128269;</span>
<span style="opacity:0.4">Search GIFs</span>
</div>
<div style="display:flex;gap:8px;margin-bottom:14px;overflow-x:auto;padding-bottom:4px">
<div style="background:#3a3a5a;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128293; Trending</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#x2764;&#xFE0F; Love</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128516; Happy</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128546; Sad</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#127881; Party</div>
<div style="background:#2a2a4a;border:1px solid #444;border-radius:20px;padding:6px 14px;white-space:nowrap;font-size:13px;cursor:pointer">&#128077; Yes</div>
</div>
<div style="display:flex;gap:8px">
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#444;border-radius:8px;height:90px"></div>
<div style="background:#444;border-radius:8px;height:70px"></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:8px">
<div style="background:#444;border-radius:8px;height:70px"></div>
<div style="background:#444;border-radius:8px;height:90px"></div>
</div>
</div>
<div style="text-align:center;margin-top:8px;font-size:11px;opacity:0.5">Featured / Trending GIFs</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top:20px">
<h3>My recommendation: Option C</h3>
<p>Matches the Threads pattern exactly. Pills provide quick category access, the featured GIF grid fills the space below. "Trending" is the default selected pill, tapping another pill swaps the grid to that category's results. When the user starts typing, the pills hide and the search takes over.</p>
</div>
@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>
@@ -0,0 +1 @@
{"reason":"owner process exited","timestamp":1776198924022}
@@ -0,0 +1,6 @@
{"type":"server-started","port":53072,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:53072","screen_dir":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/71603-1776195983/content","state_dir":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/71603-1776195983/state"}
{"type":"screen-added","file":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/71603-1776195983/content/category-layout.html"}
{"source":"user-event","type":"click","text":"C\n \n Horizontal pills + featured GIFs below (Threads-style)\n Exactly like Threads: category pills as a scrollable row, with featured/trending GIFs filling the grid below. Tapping a pill replaces the grid with results for that category. Best of both worlds.\n \n Pills + featured grid (Threads)\n \n \n 🔍\n Search GIFs\n \n \n 🔥 Trending\n ❤️ Love\n 😄 Happy\n 😢 Sad\n 🎉 Party\n 👍 Yes\n \n \n \n \n \n \n \n \n \n \n \n Featured / Trending GIFs","choice":"c","id":null,"timestamp":1776196240353}
{"type":"screen-added","file":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/71603-1776195983/content/waiting.html"}
{"type":"screen-added","file":"/Users/pope/dev/bsky/social-app/.superpowers/brainstorm/71603-1776195983/content/category-design.html"}
{"type":"server-stopped","reason":"owner process exited"}
@@ -0,0 +1 @@
71612
@@ -0,0 +1,315 @@
# GIF Category Pills 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:** Add a horizontal scrollable row of category pills (Trending, Love, Happy, Sad, etc.) to the GIF picker's idle screen so users can browse GIFs by emotion/reaction without typing.
**Architecture:** A new `GifCategoryPills` component renders a horizontal `ScrollView` of `Pressable` pills. `GifPickerDialog` manages `activeCategory` state — when a non-trending pill is tapped, its searchterm is passed to the existing `useGifPickerData` hook. Pills hide when the user types and reappear when the input is cleared.
**Tech Stack:** React Native, TypeScript, ALF design system, Lingui (i18n)
**Spec:** `docs/superpowers/specs/2026-04-14-gif-category-pills-design.md`
---
## File Map
| File | Action | Responsibility |
|------|--------|----------------|
| `src/features/gifPicker/components/GifCategoryPills.tsx` | Create | Horizontal scrollable pill row with active state |
| `src/features/gifPicker/GifPickerDialog.tsx` | Modify | Add category state, render pills, connect to search |
---
### Task 1: Create `GifCategoryPills` Component
**Files:**
- Create: `src/features/gifPicker/components/GifCategoryPills.tsx`
- [ ] **Step 1: Create the component**
Create `src/features/gifPicker/components/GifCategoryPills.tsx`:
```tsx
import {ScrollView, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
export type GifCategory = {
id: string
emoji: string
label: ReturnType<typeof msg>
searchterm: string | null // null = trending (uses featured endpoint)
}
export const GIF_CATEGORIES: readonly GifCategory[] = [
{id: 'trending', emoji: '🔥', label: msg`Trending`, searchterm: null},
{id: 'love', emoji: '❤️', label: msg`Love`, searchterm: 'love'},
{id: 'happy', emoji: '😄', label: msg`Happy`, searchterm: 'happy'},
{id: 'sad', emoji: '😢', label: msg`Sad`, searchterm: 'cry'},
{id: 'party', emoji: '🎉', label: msg`Party`, searchterm: 'congratulations'},
{id: 'yes', emoji: '👍', label: msg`Yes`, searchterm: 'yes'},
{id: 'lol', emoji: '😂', label: msg`LOL`, searchterm: 'lol'},
{id: 'excited', emoji: '🤩', label: msg`Excited`, searchterm: 'excited'},
] as const
export function GifCategoryPills({
activeId,
onSelect,
}: {
activeId: string
onSelect: (category: GifCategory) => void
}) {
const {_} = useLingui()
const t = useTheme()
return (
<View style={[a.mb_sm]}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={[a.flex_row, a.gap_xs, a.px_xl]}>
{GIF_CATEGORIES.map(category => {
const isActive = category.id === activeId
return (
<Button
key={category.id}
label={_(category.label)}
onPress={() => onSelect(category)}
size="small"
variant={isActive ? 'solid' : 'outline'}
color={isActive ? 'primary' : 'secondary'}
shape="default">
<ButtonText>
<Text emoji>{category.emoji}</Text> {_(category.label)}
</ButtonText>
</Button>
)
})}
</ScrollView>
</View>
)
}
```
Notes on the implementation:
- Uses the existing `Button` component with `variant="solid"` for active and `variant="outline"` for inactive, matching ALF patterns
- `shape="default"` gives pill-shaped buttons (the default shape is a pill/rounded)
- `ScrollView` with `horizontal` and `showsHorizontalScrollIndicator={false}`
- `px_xl` padding matches the GIF grid's `contentContainerStyle` on native
- The `GIF_CATEGORIES` array and `GifCategory` type are exported so `GifPickerDialog` can reference them
- `searchterm: null` for trending means "use the featured endpoint" — handled by the consumer
- Emoji is wrapped in `<Text emoji>` per codebase convention for emoji rendering
- [ ] **Step 2: Verify typecheck passes**
Run: `yarn typecheck`
Expected: No new errors
- [ ] **Step 3: Commit**
```bash
git add src/features/gifPicker/components/GifCategoryPills.tsx
git commit -m "feat(gif): add GifCategoryPills component"
```
---
### Task 2: Wire Category Pills into `GifPickerDialog`
**Files:**
- Modify: `src/features/gifPicker/GifPickerDialog.tsx`
- [ ] **Step 1: Add imports**
At the top of `src/features/gifPicker/GifPickerDialog.tsx`, add after the existing imports:
```ts
import {
GIF_CATEGORIES,
GifCategoryPills,
type GifCategory,
} from '#/features/gifPicker/components/GifCategoryPills'
```
- [ ] **Step 2: Replace `GifPickerBody`**
Replace the entire `GifPickerBody` function (lines 56-140) with:
```tsx
function GifPickerBody({
control,
onSelectGif,
}: {
control: Dialog.DialogControlProps
onSelectGif: (gif: Gif) => void
}) {
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<ListMethods>(null)
const [rawSearch, setRawSearch] = useState('')
const [activeCategory, setActiveCategory] = useState<string>('trending')
const search = useThrottledValue(rawSearch, 750)
// Determine the effective search query:
// - If user is typing, use the typed text
// - If a non-trending category is active, use its searchterm
// - Otherwise (trending), empty string triggers the featured endpoint
const activeCategorySearchterm =
GIF_CATEGORIES.find(c => c.id === activeCategory)?.searchterm ?? ''
const effectiveSearch = search.length > 0 ? search : activeCategorySearchterm
const {
data,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
error,
isPending,
isError,
isSearching,
refetch,
} = useGifPickerData(effectiveSearch)
const items = data?.pages.flatMap(page => page.results) ?? []
const hasData = items.length > 0
const onEndReached = () => {
if (isFetchingNextPage || !hasNextPage || error) return
void fetchNextPage()
}
const onGoBack = () => {
if (isSearching) {
textInputRef.current?.clear()
setRawSearch('')
setActiveCategory('trending')
} else {
control.close()
}
}
const onChangeSearch = (text: string) => {
setRawSearch(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}
const onSelectCategory = (category: GifCategory) => {
setActiveCategory(category.id)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}
const showPills = rawSearch.length === 0
const header = (
<>
<GifPickerHeader
inputRef={textInputRef}
onChangeText={onChangeSearch}
onClose={() => control.close()}
onEscape={() => control.close()}
/>
{showPills && (
<GifCategoryPills
activeId={activeCategory}
onSelect={onSelectCategory}
/>
)}
{!hasData && (
<GifPickerPlaceholder
isLoading={isPending}
isError={isError}
isSearching={isSearching}
query={effectiveSearch}
onRetry={refetch}
onGoBack={onGoBack}
/>
)}
</>
)
return (
<>
{gtMobile && <Dialog.Close />}
<GifPickerGrid
ref={listRef}
items={items}
header={header}
hasData={hasData}
isFetchingNextPage={isFetchingNextPage}
error={error}
fetchNextPage={fetchNextPage}
onEndReached={onEndReached}
onSelectGif={onSelectGif}
/>
</>
)
}
```
Key changes from the current version:
- Added `activeCategory` state (defaults to `'trending'`)
- Added `effectiveSearch` that resolves the search query from either typed text or the active category's searchterm
- Added `onSelectCategory` handler
- Added `showPills` flag — `true` when `rawSearch` is empty
- Render `GifCategoryPills` between the header and placeholder, conditionally on `showPills`
- `onGoBack` now also resets `activeCategory` to `'trending'`
- `GifPickerPlaceholder` receives `effectiveSearch` instead of `search` so error messages reflect the actual query
- [ ] **Step 3: Verify typecheck passes**
Run: `yarn typecheck`
Expected: No new errors
- [ ] **Step 4: Verify lint passes**
Run: `yarn lint`
Expected: No new errors
- [ ] **Step 5: Commit**
```bash
git add src/features/gifPicker/GifPickerDialog.tsx
git commit -m "feat(gif): wire category pills into GIF picker dialog"
```
---
### Task 3: Manual Testing
- [ ] **Step 1: Test on web (CORS-disabled Chrome) or iOS simulator**
1. Open the composer and click the GIF button
2. Verify the pill row appears below the search input with "Trending" highlighted
3. Verify featured GIFs load in the grid below
- [ ] **Step 2: Test category selection**
1. Tap "Happy" — verify the pill highlights, grid shows happy GIFs
2. Tap "Sad" — verify it switches, grid shows sad GIFs
3. Tap "Trending" — verify it goes back to featured GIFs
4. Tap several pills quickly — verify no visual glitches
- [ ] **Step 3: Test interaction with search**
1. Start typing in the search box — verify pills hide
2. Clear the search box — verify pills reappear with "Trending" active
3. Select a category, then type — verify pills hide and search results show
4. Clear again — verify pills return with "Trending" (not the previously selected category)
- [ ] **Step 4: Test pill scrolling**
1. On a narrow screen (mobile or narrow browser), verify the pill row scrolls horizontally
2. Verify all 8 pills are reachable by scrolling
3. Verify no horizontal scroll indicator is visible
- [ ] **Step 5: Commit any fixes from testing**
```bash
git add -A
git commit -m "fix(gif): address issues found during category pills testing"
```
@@ -28,7 +28,8 @@ export function useGifAutocomplete({
onSelectSuggestion: (text: string) => void
}): GifAutocompleteState {
const ax = useAnalytics()
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
// TODO: revert — hardcoded for local Klipy testing
const useKlipy = true // ax.features.enabled(ax.features.KlipyGifProviderEnable)
const [rawText, setRawText] = useState('')
const [dismissed, setDismissed] = useState(false)
@@ -21,7 +21,8 @@ export function useGifPickerData(
{enabled = true}: {enabled?: boolean} = {},
) {
const ax = useAnalytics()
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
// TODO: revert — hardcoded for local Klipy testing
const useKlipy = true // ax.features.enabled(ax.features.KlipyGifProviderEnable)
const isSearching = query.length > 0
const provider: GifPickerProvider = useKlipy ? 'klipy' : 'tenor'