Skip to content

ai-partner: FAB + peek sheet component #1462

@CraigBuckmaster

Description

@CraigBuckmaster

Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 3 · Size: M · Depends on: #1460 (access hook), #1474 (chip selection)

Build the floating action button and the peek sheet that expands on tap. The FAB is Amicus's always-on surface — visible on every primary screen, context-aware, premium-gated.


Files to create

  • app/src/components/amicus/AmicusFab.tsx — the FAB button itself
  • app/src/components/amicus/AmicusPeekSheet.tsx — bottom-sheet that expands on tap
  • app/src/contexts/AmicusFabContext.tsx — provider + hook for screens to opt in/out of showing the FAB
  • app/src/components/amicus/__tests__/AmicusFab.test.tsx
  • app/src/components/amicus/__tests__/AmicusPeekSheet.test.tsx

Files to modify

  • app/src/navigation/RootNavigator.tsx — mount <AmicusFabContext.Provider> + <AmicusFab /> at root level (above the TabNavigator but inside SafeAreaProvider)
  • Screens that want to suppress the FAB (auth, onboarding, modals) — call useAmicusFab().hide() on mount, show() on unmount

Conventions to follow

  • Bottom-sheet pattern: follow QnavOverlay.tsx and LinkedJourneySheet.tsx@gorhom/bottom-sheet with snap points, sheetRef.current?.snapToIndex(0)
  • Theme imports: useTheme, spacing, radii, fontFamily, MIN_TOUCH_TARGET from @/theme
  • Android back handler via BackHandler.addEventListener('hardwareBackPress', ...) — closes sheet not app
  • Safe-area via react-native-safe-area-context
  • Keyboard-aware: KeyboardAvoidingView or @gorhom built-in
  • Logger, not console.log; strict TS

AmicusFab component

Visual

  • Circular button, 56×56 px (matches Material FAB standard)
  • Positioned bottom: safeBottom + 16 + tabBarHeight, right: 16
  • Background: base.gold, icon: MessageSquare from lucide-react-native at 24px in dark foreground
  • Subtle elevation/shadow (iOS: shadowColor, Android: elevation: 4)
  • On tap: opens peek sheet (see below); also triggers light haptic (expo-haptics if available)
  • When user has unread proactive insight: show small gold dot pulse on top-right corner of FAB
  • When non-premium: FAB shows small lock icon overlay; tap → paywall (reuse ai-partner: premium gating via RevenueCat #1460 access logic)

Visibility rules (composed via AmicusFabContext)

  • Hidden on: modals, auth screens, onboarding flow, the Amicus tab itself (redundant there)
  • Hidden when amicusEnabled preference is false (see ai-partner: Settings controls #1459)
  • Hidden when useAmicusAccess().reason === 'disabled_in_settings'

Context API

export interface AmicusFabContextValue {
  isVisible: boolean;
  hide: () => void;   // screen asks FAB to hide while mounted
  show: () => void;   // back to default
}

export function useAmicusFab(): AmicusFabContextValue;

Screens that want to suppress: call hide() on mount in useEffect, show() in cleanup.


AmicusPeekSheet component

Snap points

['50%', '85%'] — 50% for initial peek, 85% if user drags up or expands a mini-conversation

Layout (top → bottom)

  1. Drag handle (gold-tinted pill, centered, 4px × 40px)
  2. Header — "Amicus" title (Cinzel), subtitle reads the current context (e.g. "Reading Romans 9" / "Browsing debates" / "Ready to help")
  3. Chip area — 2-3 tappable chips loaded from ai-partner: context-aware chip selection logic #1474's chip selection
  4. Free-text input — always visible at bottom, sticky above keyboard
  5. Send button (gold circle with arrow icon)

Behavior

State

Mini-conversation integration

Peek sheet hosts the mini-conversation component (#1463) when a chip or input is activated. Before activation, only chips + input are visible.


Integration with navigation context

The FAB reads navigation state to determine current screen context and pass to #1474's chip selector:

const route = useNavigationState(state => getCurrentRoute(state));
const chapterRef = extractChapterRefFromRoute(route);  // null if not on a chapter
// Pass to useAmicusChips(chapterRef) in #1474

Animation & performance

  • FAB fade-in on first render after hydration (not jarring)
  • Peek open uses @gorhom default spring
  • All animations use react-native-reanimated (match existing usage); no JS-thread animations
  • FAB render should not trigger re-renders in host screens — isolated via memoization

Acceptance criteria

  • FAB visible on all primary screens when premium + enabled
  • FAB hidden on modals, auth, onboarding, Amicus tab
  • FAB hidden when amicus_enabled preference is false
  • Non-premium users see lock icon; tap → paywall
  • Peek sheet opens on FAB tap with 50% snap
  • Drag-to-85% works; backdrop-tap closes
  • Chips load from ai-partner: context-aware chip selection logic #1474 based on current route context
  • Free-text input + send button functional
  • Android hardware back closes sheet (not app)
  • Keyboard avoidance works on both platforms
  • No render cascades into host screens (profiler check)
  • Component tests cover visibility rules, tap handling, context suppression
  • No any types; lint clean

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions