You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
knowfoolery/docs/gluestack-ui-strategy.md

39 KiB

Know Foolery - Cross-Platform UI Strategy with Gluestack UI

Overview

Gluestack UI provides a comprehensive solution for building consistent, performant user interfaces across web (React) and mobile (React Native) platforms. This document outlines the strategy for implementing Know Foolery's cross-platform UI using Gluestack UI with NativeWind/Tailwind CSS.

Gluestack UI Architecture

Cross-Platform Component Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                        Shared UI Layer                                   │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │   Design    │  │  Component  │  │   Shared    │  │   Theme     │    │
│  │   Tokens    │  │   Library   │  │   Logic     │  │  System     │    │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘    │
└─────────────────────────────────────────────────────────────────────────┘
                                   │
                         Platform Adaptation
                                   │
┌─────────────────────────────────────────────────────────────────────────┐
│                      Platform Renderers                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐    │
│  │   Web DOM   │  │   iOS       │  │   Android   │  │   Desktop   │    │
│  │  (React)    │  │ (RN iOS)    │  │ (RN Android)│  │   (Wails)   │    │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘    │
└─────────────────────────────────────────────────────────────────────────┘

Project Structure for Cross-Platform UI

frontend/
├── packages/
│   ├── ui-components/                # Shared Gluestack UI components
│   │   ├── src/
│   │   │   ├── components/
│   │   │   │   ├── GameCard/
│   │   │   │   │   ├── GameCard.tsx
│   │   │   │   │   ├── GameCard.stories.tsx
│   │   │   │   │   ├── GameCard.test.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   ├── Leaderboard/
│   │   │   │   ├── Timer/
│   │   │   │   ├── ScoreDisplay/
│   │   │   │   ├── AdminPanel/
│   │   │   │   └── index.ts
│   │   │   ├── theme/
│   │   │   │   ├── tokens.ts         # Design tokens
│   │   │   │   ├── colors.ts
│   │   │   │   ├── typography.ts
│   │   │   │   ├── spacing.ts
│   │   │   │   └── index.ts
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── gluestack-ui.config.ts
│   ├── shared-logic/                 # Business logic
│   │   ├── src/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   ├── utils/
│   │   │   └── types/
│   │   └── package.json
│   └── shared-types/                 # TypeScript types
│       ├── src/
│       │   ├── api.ts
│       │   ├── game.ts
│       │   └── index.ts
│       └── package.json
├── apps/
│   ├── web/                          # React web app
│   │   ├── src/
│   │   │   ├── pages/
│   │   │   ├── components/           # Web-specific components
│   │   │   └── main.tsx
│   │   ├── vite.config.ts
│   │   └── package.json
│   ├── mobile/                       # React Native app
│   │   ├── src/
│   │   │   ├── screens/
│   │   │   ├── components/           # Mobile-specific components
│   │   │   └── App.tsx
│   │   ├── metro.config.js
│   │   └── package.json
│   └── desktop/                      # Wails desktop app
│       ├── frontend/
│       ├── app/
│       └── wails.json
└── storybook/                        # Component documentation
    ├── stories/
    └── .storybook/

Theme System Implementation

Design Tokens with Quiz Game Branding

// packages/ui-components/src/theme/tokens.ts
export const designTokens = {
  colors: {
    // Brand colors for Know Foolery
    brand: {
      50: '#eff6ff',   // Very light blue
      100: '#dbeafe',  // Light blue
      200: '#bfdbfe',  // Lighter blue
      300: '#93c5fd',  // Light blue
      400: '#60a5fa',  // Medium blue
      500: '#3b82f6',  // Primary blue (main brand)
      600: '#2563eb',  // Darker blue
      700: '#1d4ed8',  // Dark blue
      800: '#1e40af',  // Very dark blue
      900: '#1e3a8a',  // Darkest blue
    },
    
    // Semantic colors for game states
    success: {
      50: '#f0fdf4',   // Correct answer background
      500: '#22c55e',  // Correct answer green
      600: '#16a34a',  // Darker correct green
    },
    
    error: {
      50: '#fef2f2',   // Wrong answer background
      500: '#ef4444',  // Wrong answer red
      600: '#dc2626',  // Darker wrong red
    },
    
    warning: {
      50: '#fffbeb',   // Hint background
      500: '#f59e0b',  // Hint orange/yellow
      600: '#d97706',  // Darker hint color
    },
    
    // Neutral colors
    gray: {
      50: '#f9fafb',
      100: '#f3f4f6',
      200: '#e5e7eb',
      300: '#d1d5db',
      400: '#9ca3af',
      500: '#6b7280',
      600: '#4b5563',
      700: '#374151',
      800: '#1f2937',
      900: '#111827',
    },
  },
  
  spacing: {
    0: '0px',
    1: '4px',
    2: '8px',
    3: '12px',
    4: '16px',
    5: '20px',
    6: '24px',
    8: '32px',
    10: '40px',
    12: '48px',
    16: '64px',
    20: '80px',
    24: '96px',
  },
  
  typography: {
    fontSizes: {
      xs: '12px',
      sm: '14px',
      md: '16px',
      lg: '18px',
      xl: '20px',
      '2xl': '24px',
      '3xl': '30px',
      '4xl': '36px',
    },
    
    fontWeights: {
      normal: '400',
      medium: '500',
      semibold: '600',
      bold: '700',
      extrabold: '800',
    },
    
    lineHeights: {
      tight: '1.25',
      normal: '1.5',
      relaxed: '1.75',
    },
    
    letterSpacings: {
      tight: '-0.025em',
      normal: '0em',
      wide: '0.025em',
    },
  },
  
  borderRadius: {
    none: '0px',
    sm: '4px',
    md: '8px',
    lg: '12px',
    xl: '16px',
    full: '9999px',
  },
  
  shadows: {
    sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
    md: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
    lg: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
    xl: '0 20px 25px -5px rgb(0 0 0 / 0.1)',
  },
}

// Custom semantic tokens for game elements
export const gameTokens = {
  // Question difficulty indicators
  difficulty: {
    easy: designTokens.colors.success[500],
    medium: designTokens.colors.warning[500],
    hard: designTokens.colors.error[500],
  },
  
  // Timer states
  timer: {
    normal: designTokens.colors.gray[600],
    warning: designTokens.colors.warning[500],  // 5 minutes left
    critical: designTokens.colors.error[500],   // 1 minute left
  },
  
  // Score indicators
  score: {
    excellent: designTokens.colors.success[500], // 80%+ correct
    good: designTokens.colors.brand[500],        // 60-80% correct
    average: designTokens.colors.warning[500],   // 40-60% correct
    poor: designTokens.colors.error[500],        // <40% correct
  },
  
  // Leaderboard positions
  leaderboard: {
    first: '#ffd700',    // Gold
    second: '#c0c0c0',   // Silver
    third: '#cd7f32',    // Bronze
    other: designTokens.colors.gray[600],
  },
}

// Export combined theme
export const knowFooleryTheme = {
  ...designTokens,
  game: gameTokens,
}

Gluestack UI Configuration

// packages/ui-components/gluestack-ui.config.ts
import { createConfig } from '@gluestack-ui/themed'
import { knowFooleryTheme } from './src/theme'

export const config = createConfig({
  aliases: {
    bg: 'backgroundColor',
    p: 'padding',
    m: 'margin',
    w: 'width',
    h: 'height',
  },
  
  tokens: {
    colors: knowFooleryTheme.colors,
    space: knowFooleryTheme.spacing,
    fontSizes: knowFooleryTheme.typography.fontSizes,
    fontWeights: knowFooleryTheme.typography.fontWeights,
    lineHeights: knowFooleryTheme.typography.lineHeights,
    letterSpacings: knowFooleryTheme.typography.letterSpacings,
    radii: knowFooleryTheme.borderRadius,
    shadows: knowFooleryTheme.shadows,
  },
  
  globalStyle: {
    variants: {
      hardShadow: {
        shadowColor: '$backgroundLight800',
        shadowOffset: {
          width: 2,
          height: 2,
        },
        shadowOpacity: 0.6,
        shadowRadius: 8,
        elevation: 10,
      },
    },
  },
  
  // Custom component variants
  components: {
    Button: {
      theme: {
        variants: {
          solid: {
            'bg': '$brand500',
            '_text': {
              'color': '$white',
              'fontWeight': '$semibold',
            },
            '_hover': {
              'bg': '$brand600',
            },
            '_pressed': {
              'bg': '$brand700',
            },
          },
          
          outline: {
            'borderWidth': 2,
            'borderColor': '$brand500',
            '_text': {
              'color': '$brand500',
              'fontWeight': '$semibold',
            },
            '_hover': {
              'bg': '$brand50',
            },
          },
          
          ghost: {
            '_text': {
              'color': '$brand500',
            },
            '_hover': {
              'bg': '$brand50',
            },
          },
        },
        
        sizes: {
          sm: {
            'px': '$3',
            'py': '$2',
            '_text': {
              'fontSize': '$sm',
            },
          },
          md: {
            'px': '$4',
            'py': '$3',
            '_text': {
              'fontSize': '$md',
            },
          },
          lg: {
            'px': '$6',
            'py': '$4',
            '_text': {
              'fontSize': '$lg',
            },
          },
        },
      },
    },
    
    Card: {
      theme: {
        'bg': '$white',
        'rounded': '$lg',
        'shadowColor': '$backgroundLight800',
        'shadowOffset': {
          width: 0,
          height: 2,
        },
        'shadowOpacity': 0.1,
        'shadowRadius': 8,
        'elevation': 3,
        
        variants: {
          elevated: {
            'shadowOpacity': 0.15,
            'elevation': 6,
          },
          
          outlined: {
            'borderWidth': 1,
            'borderColor': '$gray200',
            'shadowOpacity': 0,
            'elevation': 0,
          },
        },
      },
    },
    
    Badge: {
      theme: {
        'rounded': '$full',
        'px': '$3',
        'py': '$1',
        
        variants: {
          solid: {
            'bg': '$brand500',
            '_text': {
              'color': '$white',
              'fontSize': '$sm',
              'fontWeight': '$semibold',
            },
          },
          
          outline: {
            'borderWidth': 1,
            'borderColor': '$brand500',
            '_text': {
              'color': '$brand500',
              'fontSize': '$sm',
              'fontWeight': '$semibold',
            },
          },
          
          subtle: {
            'bg': '$brand50',
            '_text': {
              'color': '$brand700',
              'fontSize': '$sm',
              'fontWeight': '$semibold',
            },
          },
        },
      },
    },
  },
})

export type Config = typeof config

Core Game Components

GameCard Component - The Heart of the Quiz

// packages/ui-components/src/components/GameCard/GameCard.tsx
import React, { useState, useEffect, useCallback } from 'react'
import {
  Card,
  VStack,
  HStack,
  Text,
  Input,
  InputField,
  Button,
  ButtonText,
  Badge,
  Progress,
  ProgressFilledTrack,
  Box,
  Pressable,
} from '@gluestack-ui/themed'
import { knowFooleryTheme } from '../../theme'

export interface GameCardProps {
  // Game data
  question: string
  theme: string
  difficulty?: 'easy' | 'medium' | 'hard'
  
  // Game state
  timeRemaining: number
  attemptsLeft: number
  currentScore: number
  hintUsed: boolean
  
  // Interaction handlers
  onSubmitAnswer: (answer: string) => Promise<void>
  onRequestHint: () => Promise<void>
  onTimeExpire?: () => void
  
  // UI state
  isLoading?: boolean
  hint?: string
  showHint?: boolean
}

export const GameCard: React.FC<GameCardProps> = ({
  question,
  theme,
  difficulty = 'medium',
  timeRemaining,
  attemptsLeft,
  currentScore,
  hintUsed,
  onSubmitAnswer,
  onRequestHint,
  onTimeExpire,
  isLoading = false,
  hint,
  showHint = false,
}) => {
  const [answer, setAnswer] = useState<string>('')
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
  
  // Timer formatting
  const formatTime = useCallback((seconds: number): string => {
    const minutes = Math.floor(seconds / 60)
    const remainingSeconds = seconds % 60
    return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
  }, [])
  
  // Handle answer submission
  const handleSubmit = useCallback(async () => {
    if (!answer.trim() || isSubmitting || isLoading) return
    
    setIsSubmitting(true)
    try {
      await onSubmitAnswer(answer.trim())
      setAnswer('') // Clear input after successful submission
    } catch (error) {
      // Error handling managed by parent component
      console.error('Failed to submit answer:', error)
    } finally {
      setIsSubmitting(false)
    }
  }, [answer, isSubmitting, isLoading, onSubmitAnswer])
  
  // Handle hint request
  const handleHintRequest = useCallback(async () => {
    if (isLoading || isSubmitting || hintUsed) return
    
    try {
      await onRequestHint()
    } catch (error) {
      console.error('Failed to request hint:', error)
    }
  }, [isLoading, isSubmitting, hintUsed, onRequestHint])
  
  // Time expiration effect
  useEffect(() => {
    if (timeRemaining === 0 && onTimeExpire) {
      onTimeExpire()
    }
  }, [timeRemaining, onTimeExpire])
  
  // Computed values
  const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100
  const isTimeWarning = timeRemaining <= 300 // 5 minutes
  const isTimeCritical = timeRemaining <= 60  // 1 minute
  const difficultyColor = knowFooleryTheme.game.difficulty[difficulty]
  const timerColor = isTimeCritical 
    ? knowFooleryTheme.game.timer.critical
    : isTimeWarning 
    ? knowFooleryTheme.game.timer.warning 
    : knowFooleryTheme.game.timer.normal
  
  return (
    <Card size="lg" variant="elevated" m="$4" testID="game-card">
      <VStack space="md" p="$6">
        {/* Header with theme, difficulty, and timer */}
        <HStack justifyContent="space-between" alignItems="center">
          <HStack space="sm" alignItems="center">
            <Badge variant="solid" bg={difficultyColor} testID="theme-badge">
              <Text color="$white" fontSize="$sm" fontWeight="$semibold">
                {theme}
              </Text>
            </Badge>
            
            {difficulty && (
              <Badge variant="outline" borderColor={difficultyColor} testID="difficulty-badge">
                <Text color={difficultyColor} fontSize="$xs" fontWeight="$medium">
                  {difficulty.toUpperCase()}
                </Text>
              </Badge>
            )}
          </HStack>
          
          <HStack space="sm" alignItems="center">
            <Text 
              fontSize="$sm" 
              color={timerColor}
              fontWeight="$semibold"
              testID="timer-display"
            >
              ⏱️ {formatTime(timeRemaining)}
            </Text>
          </HStack>
        </HStack>
        
        {/* Session progress indicator */}
        <Progress value={progressValue} size="sm" testID="session-progress">
          <ProgressFilledTrack bg="$brand500" />
        </Progress>
        
        {/* Question display */}
        <Box>
          <Text 
            fontSize="$xl" 
            fontWeight="$semibold" 
            lineHeight="$relaxed"
            color="$gray900"
            testID="question-text"
          >
            {question}
          </Text>
        </Box>
        
        {/* Hint display */}
        {showHint && hint && (
          <Box 
            bg="$warning50" 
            p="$4" 
            rounded="$md" 
            borderLeftWidth={4} 
            borderLeftColor="$warning500"
            testID="hint-display"
          >
            <HStack space="sm" alignItems="flex-start">
              <Text fontSize="$lg">💡</Text>
              <Text fontSize="$md" color="$gray700" flex={1}>
                {hint}
              </Text>
            </HStack>
          </Box>
        )}
        
        {/* Answer input */}
        <Input 
          size="lg" 
          isDisabled={isLoading || isSubmitting}
          testID="answer-input"
        >
          <InputField
            placeholder="Enter your answer..."
            value={answer}
            onChangeText={setAnswer}
            autoCapitalize="none"
            autoCorrect={false}
            onSubmitEditing={handleSubmit}
            returnKeyType="send"
            fontSize="$md"
          />
        </Input>
        
        {/* Game statistics */}
        <HStack justifyContent="space-between" alignItems="center">
          <Text fontSize="$sm" color="$gray600" testID="attempts-counter">
            <Text fontWeight="$semibold">{attemptsLeft}</Text> attempts left
          </Text>
          <Text fontSize="$sm" color="$gray600" testID="score-display">
            Score: <Text fontWeight="$semibold" color="$brand600">{currentScore}</Text> points
          </Text>
        </HStack>
        
        {/* Action buttons */}
        <HStack space="sm">
          <Button
            size="lg"
            variant="solid"
            flex={1}
            onPress={handleSubmit}
            isDisabled={!answer.trim() || isLoading || isSubmitting}
            testID="submit-button"
          >
            <ButtonText>
              {isSubmitting ? 'Submitting...' : 'Submit Answer'}
            </ButtonText>
          </Button>
          
          <Button
            size="lg"
            variant="outline"
            onPress={handleHintRequest}
            isDisabled={isLoading || isSubmitting || hintUsed}
            testID="hint-button"
            px="$4"
          >
            <ButtonText>
              {hintUsed ? '💡 Used' : '💡 Hint'}
            </ButtonText>
          </Button>
        </HStack>
        
        {/* Hint usage warning */}
        {!hintUsed && (
          <Text fontSize="$xs" color="$gray500" textAlign="center">
            Using a hint reduces your score to 1 point for this question
          </Text>
        )}
      </VStack>
    </Card>
  )
}

export default GameCard

Leaderboard Component

// packages/ui-components/src/components/Leaderboard/Leaderboard.tsx
import React from 'react'
import {
  Card,
  VStack,
  HStack,
  Text,
  Box,
  Badge,
  Divider,
  ScrollView,
} from '@gluestack-ui/themed'
import { knowFooleryTheme } from '../../theme'

export interface LeaderboardEntry {
  position: number
  playerName: string
  score: number
  questionsAnswered: number
  successRate: number
  completedAt: string
  isCurrentUser?: boolean
}

export interface LeaderboardProps {
  entries: LeaderboardEntry[]
  currentUserPosition?: number
  isLoading?: boolean
  title?: string
}

export const Leaderboard: React.FC<LeaderboardProps> = ({
  entries,
  currentUserPosition,
  isLoading = false,
  title = "🏆 Leaderboard",
}) => {
  const getPositionColor = (position: number) => {
    switch (position) {
      case 1: return knowFooleryTheme.game.leaderboard.first
      case 2: return knowFooleryTheme.game.leaderboard.second
      case 3: return knowFooleryTheme.game.leaderboard.third
      default: return knowFooleryTheme.game.leaderboard.other
    }
  }
  
  const getPositionIcon = (position: number) => {
    switch (position) {
      case 1: return '🥇'
      case 2: return '🥈'
      case 3: return '🥉'
      default: return `#${position}`
    }
  }
  
  const formatSuccessRate = (rate: number) => `${Math.round(rate)}%`
  
  if (isLoading) {
    return (
      <Card size="lg" m="$4" testID="leaderboard-loading">
        <VStack space="md" p="$6">
          <Text fontSize="$xl" fontWeight="$bold" textAlign="center">
            {title}
          </Text>
          <Text textAlign="center" color="$gray500">
            Loading leaderboard...
          </Text>
        </VStack>
      </Card>
    )
  }
  
  return (
    <Card size="lg" m="$4" testID="leaderboard">
      <VStack space="md" p="$6">
        {/* Header */}
        <HStack justifyContent="space-between" alignItems="center">
          <Text fontSize="$xl" fontWeight="$bold" testID="leaderboard-title">
            {title}
          </Text>
          {currentUserPosition && (
            <Badge variant="subtle" testID="user-position">
              <Text fontSize="$sm" fontWeight="$semibold">
                Your Rank: #{currentUserPosition}
              </Text>
            </Badge>
          )}
        </HStack>
        
        <Divider />
        
        {/* Leaderboard entries */}
        <ScrollView maxHeight={400} showsVerticalScrollIndicator={false}>
          <VStack space="sm">
            {entries.map((entry, index) => (
              <LeaderboardRow 
                key={`${entry.position}-${entry.playerName}`}
                entry={entry}
                positionColor={getPositionColor(entry.position)}
                positionIcon={getPositionIcon(entry.position)}
              />
            ))}
            
            {entries.length === 0 && (
              <Box py="$8">
                <Text textAlign="center" color="$gray500" fontSize="$md">
                  No scores yet. Be the first to play!
                </Text>
              </Box>
            )}
          </VStack>
        </ScrollView>
      </VStack>
    </Card>
  )
}

interface LeaderboardRowProps {
  entry: LeaderboardEntry
  positionColor: string
  positionIcon: string
}

const LeaderboardRow: React.FC<LeaderboardRowProps> = ({ 
  entry, 
  positionColor,
  positionIcon 
}) => {
  return (
    <Box
      bg={entry.isCurrentUser ? '$brand50' : '$white'}
      p="$3"
      rounded="$md"
      borderWidth={entry.isCurrentUser ? 2 : 1}
      borderColor={entry.isCurrentUser ? '$brand200' : '$gray100'}
      testID={`leaderboard-row-${entry.position}`}
    >
      <HStack space="md" alignItems="center">
        {/* Position */}
        <Box minWidth={40}>
          <Text 
            fontSize="$lg" 
            fontWeight="$bold" 
            color={positionColor}
            textAlign="center"
          >
            {positionIcon}
          </Text>
        </Box>
        
        {/* Player info */}
        <VStack flex={1} space="xs">
          <HStack justifyContent="space-between" alignItems="center">
            <Text 
              fontSize="$md" 
              fontWeight="$semibold"
              color={entry.isCurrentUser ? '$brand700' : '$gray900'}
              numberOfLines={1}
              ellipsizeMode="tail"
            >
              {entry.playerName}
              {entry.isCurrentUser && (
                <Text fontSize="$sm" color="$brand500"> (You)</Text>
              )}
            </Text>
            <Text fontSize="$lg" fontWeight="$bold" color="$brand600">
              {entry.score}
            </Text>
          </HStack>
          
          <HStack justifyContent="space-between">
            <Text fontSize="$sm" color="$gray600">
              {entry.questionsAnswered} questions
            </Text>
            <Text fontSize="$sm" color="$gray600">
              {formatSuccessRate(entry.successRate)} accuracy
            </Text>
          </HStack>
        </VStack>
      </HStack>
    </Box>
  )
}

export default Leaderboard

Timer Component

// packages/ui-components/src/components/Timer/Timer.tsx
import React, { useEffect, useState } from 'react'
import {
  HStack,
  VStack,
  Text,
  Box,
  Progress,
  ProgressFilledTrack,
} from '@gluestack-ui/themed'
import { knowFooleryTheme } from '../../theme'

export interface TimerProps {
  timeRemaining: number // in seconds
  totalTime?: number // in seconds, defaults to 30 minutes
  onTimeExpire?: () => void
  onWarning?: (timeLeft: number) => void
  showProgress?: boolean
  size?: 'sm' | 'md' | 'lg'
  variant?: 'compact' | 'detailed'
}

export const Timer: React.FC<TimerProps> = ({
  timeRemaining,
  totalTime = 30 * 60, // 30 minutes default
  onTimeExpire,
  onWarning,
  showProgress = true,
  size = 'md',
  variant = 'detailed',
}) => {
  const [hasWarned5Min, setHasWarned5Min] = useState(false)
  const [hasWarned1Min, setHasWarned1Min] = useState(false)
  
  // Format time display
  const formatTime = (seconds: number): string => {
    const minutes = Math.floor(seconds / 60)
    const remainingSeconds = seconds % 60
    return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
  }
  
  // Calculate progress percentage
  const progressValue = ((totalTime - timeRemaining) / totalTime) * 100
  
  // Determine timer state and colors
  const isTimeCritical = timeRemaining <= 60  // 1 minute
  const isTimeWarning = timeRemaining <= 300  // 5 minutes
  
  const timerColor = isTimeCritical 
    ? knowFooleryTheme.game.timer.critical
    : isTimeWarning 
    ? knowFooleryTheme.game.timer.warning 
    : knowFooleryTheme.game.timer.normal
    
  const progressColor = isTimeCritical 
    ? '$error500'
    : isTimeWarning 
    ? '$warning500' 
    : '$brand500'
  
  // Font sizes based on size prop
  const fontSizes = {
    sm: { time: '$md', label: '$xs' },
    md: { time: '$lg', label: '$sm' },
    lg: { time: '$2xl', label: '$md' },
  }
  
  // Handle warnings and expiration
  useEffect(() => {
    if (timeRemaining === 0 && onTimeExpire) {
      onTimeExpire()
    } else if (timeRemaining <= 60 && !hasWarned1Min && onWarning) {
      setHasWarned1Min(true)
      onWarning(timeRemaining)
    } else if (timeRemaining <= 300 && !hasWarned5Min && onWarning) {
      setHasWarned5Min(true)
      onWarning(timeRemaining)
    }
  }, [timeRemaining, onTimeExpire, onWarning, hasWarned1Min, hasWarned5Min])
  
  if (variant === 'compact') {
    return (
      <HStack space="xs" alignItems="center" testID="timer-compact">
        <Text fontSize="$md">⏱️</Text>
        <Text 
          fontSize={fontSizes[size].time}
          fontWeight="$semibold"
          color={timerColor}
        >
          {formatTime(timeRemaining)}
        </Text>
      </HStack>
    )
  }
  
  return (
    <VStack space="sm" testID="timer-detailed">
      {/* Time display */}
      <HStack justifyContent="center" alignItems="center" space="sm">
        <Text fontSize="$lg">⏱️</Text>
        <VStack space="xs" alignItems="center">
          <Text 
            fontSize={fontSizes[size].time}
            fontWeight="$bold"
            color={timerColor}
            testID="timer-display"
          >
            {formatTime(timeRemaining)}
          </Text>
          <Text 
            fontSize={fontSizes[size].label}
            color="$gray600"
            fontWeight="$medium"
          >
            Time Remaining
          </Text>
        </VStack>
      </HStack>
      
      {/* Progress bar */}
      {showProgress && (
        <Box>
          <Progress value={progressValue} size="md" testID="timer-progress">
            <ProgressFilledTrack bg={progressColor} />
          </Progress>
          {variant === 'detailed' && (
            <HStack justifyContent="space-between" mt="$1">
              <Text fontSize="$xs" color="$gray500">
                0:00
              </Text>
              <Text fontSize="$xs" color="$gray500">
                {formatTime(totalTime)}
              </Text>
            </HStack>
          )}
        </Box>
      )}
      
      {/* Warning messages */}
      {isTimeCritical && (
        <Box 
          bg="$error50" 
          p="$2" 
          rounded="$md" 
          borderLeftWidth={3} 
          borderLeftColor="$error500"
        >
          <Text fontSize="$sm" color="$error700" fontWeight="$semibold" textAlign="center">
            ⚠️ Less than 1 minute remaining!
          </Text>
        </Box>
      )}
      
      {isTimeWarning && !isTimeCritical && (
        <Box 
          bg="$warning50" 
          p="$2" 
          rounded="$md" 
          borderLeftWidth={3} 
          borderLeftColor="$warning500"
        >
          <Text fontSize="$sm" color="$warning700" fontWeight="$semibold" textAlign="center">
             5 minutes or less remaining
          </Text>
        </Box>
      )}
    </VStack>
  )
}

export default Timer

Platform-Specific Adaptations

Web-Specific Optimizations

// apps/web/src/components/WebGameCard.tsx
import React from 'react'
import { GameCard, GameCardProps } from '@knowfoolery/ui-components'
import { useHotkeys } from 'react-hotkeys-hook'

interface WebGameCardProps extends GameCardProps {
  enableKeyboardShortcuts?: boolean
}

export const WebGameCard: React.FC<WebGameCardProps> = ({
  enableKeyboardShortcuts = true,
  onSubmitAnswer,
  onRequestHint,
  ...props
}) => {
  // Keyboard shortcuts for web
  useHotkeys('enter', () => {
    const answerInput = document.querySelector('[data-testid="answer-input"] input') as HTMLInputElement
    if (answerInput && answerInput.value.trim()) {
      onSubmitAnswer(answerInput.value.trim())
    }
  }, { enabled: enableKeyboardShortcuts })
  
  useHotkeys('ctrl+h, cmd+h', (event) => {
    event.preventDefault()
    onRequestHint()
  }, { enabled: enableKeyboardShortcuts })
  
  return (
    <div className="web-game-card-wrapper">
      <GameCard 
        {...props}
        onSubmitAnswer={onSubmitAnswer}
        onRequestHint={onRequestHint}
      />
      
      {enableKeyboardShortcuts && (
        <div className="keyboard-hints">
          <small className="text-gray-500">
            Press Enter to submit  Ctrl/Cmd + H for hint
          </small>
        </div>
      )}
    </div>
  )
}

Mobile-Specific Optimizations

// apps/mobile/src/components/MobileGameCard.tsx
import React from 'react'
import { Vibration, Platform } from 'react-native'
import { GameCard, GameCardProps } from '@knowfoolery/ui-components'

interface MobileGameCardProps extends GameCardProps {
  enableHaptics?: boolean
}

export const MobileGameCard: React.FC<MobileGameCardProps> = ({
  enableHaptics = true,
  onSubmitAnswer,
  onRequestHint,
  onTimeExpire,
  ...props
}) => {
  const handleSubmitWithHaptics = async (answer: string) => {
    if (enableHaptics && Platform.OS === 'ios') {
      // Use iOS haptics
      const { ImpactFeedbackGenerator } = require('expo-haptics')
      ImpactFeedbackGenerator.impactAsync(ImpactFeedbackGenerator.ImpactFeedbackStyle.Medium)
    } else if (enableHaptics && Platform.OS === 'android') {
      // Use Android vibration
      Vibration.vibrate(50)
    }
    
    await onSubmitAnswer(answer)
  }
  
  const handleHintWithHaptics = async () => {
    if (enableHaptics) {
      if (Platform.OS === 'ios') {
        const { NotificationFeedbackGenerator } = require('expo-haptics')
        NotificationFeedbackGenerator.notificationAsync(
          NotificationFeedbackGenerator.NotificationFeedbackType.Warning
        )
      } else if (Platform.OS === 'android') {
        Vibration.vibrate([0, 100, 50, 100])
      }
    }
    
    await onRequestHint()
  }
  
  const handleTimeExpireWithHaptics = () => {
    if (enableHaptics) {
      if (Platform.OS === 'ios') {
        const { NotificationFeedbackGenerator } = require('expo-haptics')
        NotificationFeedbackGenerator.notificationAsync(
          NotificationFeedbackGenerator.NotificationFeedbackType.Error
        )
      } else if (Platform.OS === 'android') {
        Vibration.vibrate([0, 200, 100, 200, 100, 200])
      }
    }
    
    onTimeExpire?.()
  }
  
  return (
    <GameCard 
      {...props}
      onSubmitAnswer={handleSubmitWithHaptics}
      onRequestHint={handleHintWithHaptics}
      onTimeExpire={handleTimeExpireWithHaptics}
    />
  )
}

Responsive Design Strategy

Breakpoint System

// packages/ui-components/src/theme/breakpoints.ts
export const breakpoints = {
  sm: 640,   // Mobile landscape
  md: 768,   // Tablet portrait
  lg: 1024,  // Tablet landscape / Desktop
  xl: 1280,  // Large desktop
  '2xl': 1536, // Extra large desktop
}

export const useResponsiveValue = <T>(values: {
  base: T
  sm?: T
  md?: T
  lg?: T
  xl?: T
}) => {
  const [screenWidth, setScreenWidth] = useState(
    typeof window !== 'undefined' ? window.innerWidth : breakpoints.lg
  )
  
  useEffect(() => {
    if (typeof window === 'undefined') return
    
    const handleResize = () => setScreenWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    
    return () => window.removeEventListener('resize', handleResize)
  }, [])
  
  if (screenWidth >= breakpoints.xl && values.xl !== undefined) return values.xl
  if (screenWidth >= breakpoints.lg && values.lg !== undefined) return values.lg
  if (screenWidth >= breakpoints.md && values.md !== undefined) return values.md
  if (screenWidth >= breakpoints.sm && values.sm !== undefined) return values.sm
  
  return values.base
}

// Usage example
export const ResponsiveGameLayout: React.FC = ({ children }) => {
  const padding = useResponsiveValue({
    base: '$4',
    md: '$6',
    lg: '$8',
  })
  
  const columns = useResponsiveValue({
    base: 1,
    lg: 2,
  })
  
  return (
    <Box p={padding}>
      <VStack space="lg" flex={1}>
        {children}
      </VStack>
    </Box>
  )
}

Testing Strategy for Cross-Platform Components

Component Testing

// packages/ui-components/src/components/GameCard/GameCard.test.tsx
import React from 'react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { GluestackUIProvider } from '@gluestack-ui/themed'
import { config } from '../../../gluestack-ui.config'
import { GameCard, GameCardProps } from './GameCard'

const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <GluestackUIProvider config={config}>
    {children}
  </GluestackUIProvider>
)

const defaultProps: GameCardProps = {
  question: "What is the capital of France?",
  theme: "Geography",
  difficulty: "medium",
  timeRemaining: 1800,
  attemptsLeft: 3,
  currentScore: 0,
  hintUsed: false,
  onSubmitAnswer: jest.fn(),
  onRequestHint: jest.fn(),
}

describe('GameCard Component', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })
  
  describe('Cross-Platform Rendering', () => {
    it('renders correctly with all required props', () => {
      render(<GameCard {...defaultProps} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('game-card')).toBeInTheDocument()
      expect(screen.getByTestId('question-text')).toHaveTextContent(defaultProps.question)
      expect(screen.getByTestId('theme-badge')).toHaveTextContent(defaultProps.theme)
      expect(screen.getByTestId('timer-display')).toBeInTheDocument()
    })
    
    it('handles different screen sizes appropriately', () => {
      // Mock different viewport sizes
      const viewports = [
        { width: 375, height: 667 },   // Mobile
        { width: 768, height: 1024 },  // Tablet
        { width: 1440, height: 900 },  // Desktop
      ]
      
      viewports.forEach(viewport => {
        // Mock window dimensions
        Object.defineProperty(window, 'innerWidth', {
          writable: true,
          configurable: true,
          value: viewport.width,
        })
        
        render(<GameCard {...defaultProps} />, { wrapper: TestWrapper })
        
        const gameCard = screen.getByTestId('game-card')
        expect(gameCard).toBeInTheDocument()
        
        // Component should maintain functionality across all sizes
        expect(screen.getByTestId('answer-input')).toBeInTheDocument()
        expect(screen.getByTestId('submit-button')).toBeInTheDocument()
      })
    })
  })
  
  describe('Interactive Functionality', () => {
    it('submits answer when submit button is pressed', async () => {
      const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined)
      
      render(
        <GameCard 
          {...defaultProps} 
          onSubmitAnswer={mockOnSubmitAnswer}
        />, 
        { wrapper: TestWrapper }
      )
      
      const input = screen.getByTestId('answer-input')
      const submitButton = screen.getByTestId('submit-button')
      
      // Enter answer
      fireEvent.changeText(input, 'Paris')
      
      // Submit answer
      fireEvent.press(submitButton)
      
      await waitFor(() => {
        expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris')
      })
    })
    
    it('requests hint when hint button is pressed', async () => {
      const mockOnRequestHint = jest.fn().mockResolvedValue(undefined)
      
      render(
        <GameCard 
          {...defaultProps} 
          onRequestHint={mockOnRequestHint}
        />, 
        { wrapper: TestWrapper }
      )
      
      const hintButton = screen.getByTestId('hint-button')
      fireEvent.press(hintButton)
      
      await waitFor(() => {
        expect(mockOnRequestHint).toHaveBeenCalled()
      })
    })
  })
  
  describe('Timer and State Management', () => {
    it('displays timer with correct formatting', () => {
      render(<GameCard {...defaultProps} timeRemaining={125} />, { wrapper: TestWrapper })
      
      expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05')
    })
    
    it('shows warning colors when time is low', () => {
      render(<GameCard {...defaultProps} timeRemaining={30} />, { wrapper: TestWrapper })
      
      const timer = screen.getByTestId('timer-display')
      // Check if timer has warning/critical styling applied
      expect(timer).toHaveStyle({ color: expect.stringContaining('#ef4444') })
    })
  })
})

This comprehensive Gluestack UI strategy ensures Know Foolery delivers a consistent, performant, and maintainable user experience across all platforms while leveraging the power of a unified component system.