diff --git a/docs/STYLE_OPTIMIZATION.md b/docs/STYLE_OPTIMIZATION.md
new file mode 100644
index 0000000..75eefe5
--- /dev/null
+++ b/docs/STYLE_OPTIMIZATION.md
@@ -0,0 +1,99 @@
+# Styling Optimization & Atomic CSS Guide
+
+This document describes the CSS-in-JS style audit, NativeWind atomic CSS verification, and styling optimization results implemented for teachLink Mobile.
+
+## 1. Style Audit Overview
+
+Our audit of the teachLink Mobile codebase identified a hybrid styling architecture:
+- **Newer layouts and navigation components** utilized utility-first Tailwind classes via **NativeWind**.
+- **Core interactive widgets and form elements** heavily relied on traditional React Native `StyleSheet.create` declarations and inline style objects.
+
+### Performance Impact of Mixed Styles
+Traditional React Native `StyleSheet.create` rules are resolved dynamically at runtime and compiled into individual JavaScript style objects. Having dozens of components declaring near-identical properties (e.g., `flexDirection: 'row'`, `alignItems: 'center'`, padding, margins, colors) results in:
+1. **Bloated JS Bundle**: Repetitive JavaScript objects are duplicated throughout the compiled bundle.
+2. **Dynamic Style Overhead**: The React Native bridge must process and transfer duplicate style dictionaries at runtime.
+3. **Poorer Cache-ability**: Styles cannot be effectively cached as individual, atomic instructions.
+
+---
+
+## 2. NativeWind Atomic Styling Verification
+
+NativeWind v4 implements an **atomic CSS** compiler. It parses tailwind classes (e.g., `flex-row`, `items-center`, `bg-white`) inside JSX `className` properties at compile-time and maps them to a single, shared sheet of atomic React Native style definitions.
+
+### Advantages of the Atomic Approach
+- **De-duplication**: The style `items-center` is defined exactly once in the NativeWind global registry and shared by all components.
+- **Smaller Payload**: Instead of bundling massive stylesheet objects, components only bundle a string of class names (e.g., `"flex-row items-center px-4"`).
+- **Fast Application**: Resolving pre-cached atomic styles is significantly faster than parsing and applying large nested style dictionaries.
+
+---
+
+## 3. Style Refactoring Strategy
+
+We systematically refactored core components from `StyleSheet.create` to utility classes. We established a strict separation of concerns for styling:
+
+1. **Static Styles (Tailwind Classes)**: All layout, alignment, structure, flexbox, border widths, static background colors, and margins are defined via `className`.
+2. **Dynamic Styles (Inline style)**: Runtime values computed dynamically (e.g., layout values scaled with `useDynamicFontSize`, theme-dependent colors, animated translations/opacities) are kept as minimal inline `style` objects.
+
+### Example Refactoring (`AccessibleButton.tsx`)
+**Before (CSS-in-JS):**
+```tsx
+const AccessibleButton = ({ style, children }) => (
+
+ {children}
+
+);
+
+const styles = StyleSheet.create({
+ base: {
+ minWidth: 44,
+ minHeight: 44,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+});
+```
+
+**After (Atomic NativeWind):**
+```tsx
+const AccessibleButton = ({ className, style, children }) => (
+
+ {children}
+
+);
+```
+
+---
+
+## 4. Optimization Metrics & Results
+
+To measure the impact, we analyzed the codebase before and after our refactoring using a style-auditing script.
+
+| Metric | Before Optimization | After Optimization | Change |
+| :--- | :---: | :---: | :---: |
+| **StyleSheet.create() Calls** | 54 | 47 | **-7 (-13.0%)** |
+| **Estimated CSS-in-JS Style Rules** | 3,769 | 3,488 | **-281 (-7.5%)** |
+| **NativeWind className Attributes** | 157 | 234 | **+77 (+49.0%)** |
+
+### Optimized Components
+The following heavy components were migrated completely away from local stylesheets:
+1. `AccessibleButton.tsx` (Touch target sizing, centering)
+2. `MobileFormInput.tsx` (Labels, icons, inputs, errors, focused/dark-mode colors)
+3. `PrimaryButton.tsx` (Sizes, solid/outline/gradient layouts, text weight)
+4. `PullToRefresh.tsx` (Containers, animated refresh indicators, accessibility fallbacks)
+5. `VoiceSearch.tsx` (Compact/full mic triggers, action buttons, transcript bars)
+6. `SearchResultCard.tsx` (List card layout, icon wraps, text sizing, metadata tags)
+7. `VideoControls.tsx` (Seek bars, progress overlays, control bars, playback speed menus)
+
+---
+
+## 5. Developer Best Practices
+
+To maintain styling consistency and performance, developers should adhere to the following guidelines:
+
+- **Prefer Tailwind Classes**: Always check if a design style can be represented by a Tailwind utility class before resorting to a stylesheet or inline style.
+- **Support `className` in Shared Wrappers**: When writing reusable wrappers or atomic components, expose a `className` prop to allow consumers to pass custom utility classes.
+- **Isolate Dynamic Styles**: Keep inline style values strictly limited to dynamic runtime parameters (e.g., animation outputs, scaled fonts, colors loaded from backend).
+- **Consult Design Tokens**: Refer to `tailwind.config.js` for custom color palettes, font weights, and spacing scales.
diff --git a/scripts/measure_styles.js b/scripts/measure_styles.js
new file mode 100644
index 0000000..5489d62
--- /dev/null
+++ b/scripts/measure_styles.js
@@ -0,0 +1,72 @@
+const fs = require('fs');
+const path = require('path');
+
+const srcDir = path.join(__dirname, '..', 'src');
+const appDir = path.join(__dirname, '..', 'app');
+
+function walkDir(dir, filter) {
+ let results = [];
+ const list = fs.readdirSync(dir);
+ list.forEach(file => {
+ const filePath = path.join(dir, file);
+ const stat = fs.statSync(filePath);
+ if (stat && stat.isDirectory()) {
+ results = results.concat(walkDir(filePath, filter));
+ } else if (filter(filePath)) {
+ results.push(filePath);
+ }
+ });
+ return results;
+}
+
+const files = [
+ ...walkDir(srcDir, f => /\.(tsx|ts|jsx|js)$/.test(f)),
+ ...walkDir(appDir, f => /\.(tsx|ts|jsx|js)$/.test(f))
+];
+
+let totalStyleSheetCalls = 0;
+let totalStyleRules = 0;
+let totalClassNames = 0;
+let stylesheetDetails = [];
+
+files.forEach(file => {
+ const content = fs.readFileSync(file, 'utf8');
+ const relPath = path.relative(path.join(__dirname, '..'), file);
+
+ // Match StyleSheet.create(...)
+ if (content.includes('StyleSheet.create')) {
+ totalStyleSheetCalls++;
+ // Simple estimation of number of rules inside StyleSheet.create
+ const ssMatch = content.match(/StyleSheet\.create\(\s*\{([\s\S]*?)\}\s*\)/g);
+ if (ssMatch) {
+ ssMatch.forEach(match => {
+ // Count top-level properties inside the object
+ const innerContent = match.replace(/StyleSheet\.create\(\s*\{/, '').replace(/\}\s*\)/, '');
+ // Match properties like name: { ... } or "name": { ... }
+ // We can approximate by counting blocks of braces or keys
+ const keys = innerContent.match(/^\s*[\w"']+\s*:/gm) || [];
+ totalStyleRules += keys.length;
+ stylesheetDetails.push({ file: relPath, rules: keys.length });
+ });
+ }
+ }
+
+ // Match className="..." or className={`...`}
+ const classNameMatches = content.match(/className\s*=\s*(?:['"]([^'"]+)['"]|\{[\s\S]*?\})/g) || [];
+ totalClassNames += classNameMatches.length;
+});
+
+console.log('\nš STYLING AUDIT REPORT š');
+console.log('============================');
+console.log(`Total Files Audited: ${files.length}`);
+console.log(`StyleSheet.create() Calls: ${totalStyleSheetCalls}`);
+console.log(`Estimated CSS-in-JS Style Rules: ${totalStyleRules}`);
+console.log(`NativeWind className Attributes: ${totalClassNames}`);
+console.log('\nTop CSS-in-JS Stylesheets (by number of rules):');
+stylesheetDetails
+ .sort((a, b) => b.rules - a.rules)
+ .slice(0, 15)
+ .forEach(item => {
+ console.log(`- ${item.file}: ${item.rules} rules`);
+ });
+console.log('============================\n');
diff --git a/src/components/common/PrimaryButton.tsx b/src/components/common/PrimaryButton.tsx
index 297d2d5..b9450a4 100644
--- a/src/components/common/PrimaryButton.tsx
+++ b/src/components/common/PrimaryButton.tsx
@@ -6,7 +6,6 @@ import {
View,
ViewStyle,
TextStyle,
- StyleSheet,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useDynamicFontSize } from '../../hooks';
@@ -93,15 +92,12 @@ export default function PrimaryButton({
colors={['#20afe7', '#2c8aec', '#586ce9']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
- style={[
- styles.button,
- styles.gradientButton,
- {
- paddingHorizontal: config.paddingHorizontal,
- paddingVertical: config.paddingVertical,
- borderRadius: config.borderRadius,
- },
- ]}
+ className="flex-row items-center justify-center gap-2 shadow-sm shadow-[#20afe7]/30 elevation-4"
+ style={{
+ paddingHorizontal: config.paddingHorizontal,
+ paddingVertical: config.paddingVertical,
+ borderRadius: config.borderRadius,
+ }}
>
{loading ? (
@@ -110,8 +106,8 @@ export default function PrimaryButton({
{icon}
{title}
@@ -209,21 +209,3 @@ export default function PrimaryButton({
);
}
-const styles = StyleSheet.create({
- button: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 8,
- },
- gradientButton: {
- shadowColor: '#20afe7',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 8,
- elevation: 4,
- },
- buttonText: {
- fontWeight: '600',
- },
-});
diff --git a/src/components/mobile/AccessibleButton.tsx b/src/components/mobile/AccessibleButton.tsx
index c113908..21df836 100644
--- a/src/components/mobile/AccessibleButton.tsx
+++ b/src/components/mobile/AccessibleButton.tsx
@@ -2,9 +2,8 @@ import React from 'react';
import {
TouchableOpacity,
TouchableOpacityProps,
- StyleSheet,
- ViewStyle,
StyleProp,
+ ViewStyle,
} from 'react-native';
import { getAccessibilityProps } from '../../utils/accessibility';
@@ -20,6 +19,8 @@ interface AccessibleButtonProps extends TouchableOpacityProps {
role?: 'button' | 'link';
/** Optional custom styles for the button container */
containerStyle?: StyleProp;
+ /** Optional NativeWind className */
+ className?: string;
}
/**
@@ -35,6 +36,7 @@ export const AccessibleButton: React.FC = ({
containerStyle,
disabled,
activeOpacity = 0.7,
+ className,
...rest
}: AccessibleButtonProps) => {
const accessibilityProps = getAccessibilityProps(label, role as 'button' | 'link', hint, {
@@ -47,18 +49,11 @@ export const AccessibleButton: React.FC = ({
{...accessibilityProps}
disabled={disabled}
activeOpacity={activeOpacity}
- style={[styles.base, containerStyle, style]}
+ className={`min-touch-target justify-center items-center ${className || ''}`}
+ style={[containerStyle, style]}
>
{children}
);
};
-const styles = StyleSheet.create({
- base: {
- minWidth: 44,
- minHeight: 44,
- justifyContent: 'center',
- alignItems: 'center',
- },
-});
diff --git a/src/components/mobile/MobileFormInput.tsx b/src/components/mobile/MobileFormInput.tsx
index e34cfb5..d33a8ec 100644
--- a/src/components/mobile/MobileFormInput.tsx
+++ b/src/components/mobile/MobileFormInput.tsx
@@ -1,6 +1,6 @@
import { Eye, EyeOff, AlertCircle } from 'lucide-react-native';
import React, { useCallback, useEffect, useState } from 'react';
-import { View, TextInput, TextInputProps, TouchableOpacity, StyleSheet } from 'react-native';
+import { View, TextInput, TextInputProps, TouchableOpacity } from 'react-native';
import { useDynamicFontSize } from '../../hooks';
import {
@@ -105,44 +105,46 @@ export const MobileFormInput: React.FC = ({
const labelColor = error ? '#ef4444' : isDark ? '#94a3b8' : '#64748b';
return (
-
-
-
+
+
+
{label}
- {required && *}
+ {required && (
+
+ {' '}
+ *
+
+ )}
{hint && !error && (
-
+
{hint}
)}
- {leftIcon && {leftIcon}}
+ {leftIcon && {leftIcon}}
= ({
/>
{isPassword && (
-
+
{showPassword ? (
) : (
@@ -169,30 +171,24 @@ export const MobileFormInput: React.FC = ({
{suggestion && !error && (
Use saved:
{suggestion}
@@ -201,86 +197,14 @@ export const MobileFormInput: React.FC = ({
)}
{error && (
-
+
- {error}
+
+ {error}
+
)}
);
};
-const styles = StyleSheet.create({
- container: {
- marginBottom: 16,
- width: '100%',
- },
- labelRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 6,
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- letterSpacing: 0.1,
- },
- required: {
- color: '#ef4444',
- },
- hint: {
- fontSize: 12,
- },
- inputWrapper: {
- flexDirection: 'row',
- alignItems: 'center',
- borderWidth: 1.5,
- borderRadius: 12,
- overflow: 'hidden',
- },
- leftIconWrapper: {
- paddingHorizontal: 14,
- justifyContent: 'center',
- alignItems: 'center',
- },
- input: {
- flex: 1,
- fontSize: 15,
- paddingVertical: 14,
- paddingRight: 16,
- },
- rightIcon: {
- paddingHorizontal: 14,
- },
- errorRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- marginTop: 4,
- },
- errorText: {
- fontSize: 12,
- color: '#ef4444',
- flex: 1,
- },
- suggestionRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- marginTop: 6,
- paddingHorizontal: 12,
- paddingVertical: 8,
- borderRadius: 10,
- borderWidth: 1,
- },
- suggestionLabel: {
- fontSize: 12,
- fontWeight: '500',
- },
- suggestionValue: {
- flex: 1,
- fontSize: 12,
- fontWeight: '600',
- },
-});
diff --git a/src/components/mobile/PullToRefresh.tsx b/src/components/mobile/PullToRefresh.tsx
index 0cf9469..8acb0f3 100644
--- a/src/components/mobile/PullToRefresh.tsx
+++ b/src/components/mobile/PullToRefresh.tsx
@@ -6,7 +6,6 @@ import {
Easing,
Pressable,
StyleProp,
- StyleSheet,
View,
ViewStyle,
} from 'react-native';
@@ -211,9 +210,9 @@ export function PullToRefresh(props: PullToRefreshProps) {
});
return (
-
+
{showA11yFallbackButton && screenReaderEnabled ? (
-
+
- Refresh
+ Refresh
) : null}
);
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- overflow: 'hidden',
- },
- indicator: {
- position: 'absolute',
- top: -44,
- left: 0,
- right: 0,
- height: 44,
- alignItems: 'center',
- justifyContent: 'center',
- },
- a11yRow: {
- paddingHorizontal: 12,
- paddingTop: 8,
- paddingBottom: 4,
- },
- a11yButton: {
- alignSelf: 'flex-start',
- paddingHorizontal: 12,
- paddingVertical: 8,
- borderRadius: 10,
- backgroundColor: '#e5e7eb',
- },
- a11yButtonText: {
- fontSize: 14,
- fontWeight: '600',
- color: '#111827',
- },
-});
diff --git a/src/components/mobile/SearchResultCard.tsx b/src/components/mobile/SearchResultCard.tsx
index 829f200..1348ce2 100644
--- a/src/components/mobile/SearchResultCard.tsx
+++ b/src/components/mobile/SearchResultCard.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
+import { View, Text, TouchableOpacity } from 'react-native';
import { BookOpen, Clock } from 'lucide-react-native';
export interface SearchResultItem {
@@ -31,31 +31,31 @@ export const SearchResultCard = React.memo(function SearchResultCard({
return (
-
+
-
-
+
+
{item.title}
{(item.description || item.subtitle) && (
-
+
{item.description || item.subtitle}
)}
-
- {metaText ? {metaText} : null}
+
+ {metaText ? {metaText} : null}
{item.duration != null && item.duration > 0 && (
-
+
- {item.duration} min
+ {item.duration} min
)}
@@ -72,66 +72,3 @@ export const SearchResultCard = React.memo(function SearchResultCard({
&& prev.item.level === next.item.level;
});
-const styles = StyleSheet.create({
- card: {
- flexDirection: 'row',
- alignItems: 'flex-start',
- backgroundColor: '#fff',
- borderRadius: 12,
- padding: 14,
- marginHorizontal: 16,
- marginBottom: 10,
- borderWidth: 1,
- borderColor: '#E5E7EB',
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.05,
- shadowRadius: 2,
- elevation: 1,
- },
- iconWrap: {
- width: 44,
- height: 44,
- borderRadius: 10,
- backgroundColor: '#E0F2FE',
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 12,
- },
- body: {
- flex: 1,
- minWidth: 0,
- },
- title: {
- fontSize: 16,
- fontWeight: '600',
- color: '#111827',
- marginBottom: 4,
- },
- description: {
- fontSize: 13,
- color: '#6B7280',
- lineHeight: 18,
- marginBottom: 6,
- },
- metaRow: {
- flexDirection: 'row',
- alignItems: 'center',
- flexWrap: 'wrap',
- gap: 8,
- },
- meta: {
- fontSize: 12,
- color: '#6B7280',
- fontWeight: '500',
- },
- durationWrap: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- },
- duration: {
- fontSize: 12,
- color: '#6B7280',
- },
-});
diff --git a/src/components/mobile/VideoControls.tsx b/src/components/mobile/VideoControls.tsx
index 577fecc..5dc2953 100644
--- a/src/components/mobile/VideoControls.tsx
+++ b/src/components/mobile/VideoControls.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
+import { Animated, Pressable, Text, View } from 'react-native';
import type { QualityOption } from '../../services/videoQuality';
type VideoControlsProps = {
@@ -156,55 +156,59 @@ const VideoControls = ({
}, [onSelectQuality]);
return (
-
-
-
+
+
+
{isPiPSupported ? (
- {isPiPActive ? 'PiP On' : 'PiP'}
+ {isPiPActive ? 'PiP On' : 'PiP'}
) : null}
- {isFullscreen ? 'Exit' : 'Full'}
+ {isFullscreen ? 'Exit' : 'Full'}
-
+
- {isPlaying ? 'Pause' : 'Play'}
+ {isPlaying ? 'Pause' : 'Play'}
-
+
{previewPositionMillis != null ? (
-
-
+
+
{formatTime(displayPosition)} / {formatTime(durationMillis)}
) : null}
-
- {formatTime(displayPosition)}
- {formatTime(durationMillis)}
+
+ {formatTime(displayPosition)}
+ {formatTime(durationMillis)}
true}
onResponderGrant={handleSeekGrant}
@@ -212,25 +216,33 @@ const VideoControls = ({
onResponderRelease={handleSeekRelease}
onResponderTerminate={handleSeekTerminate}
>
-
-
-
+
+
+
-
+
- {formatRate(playbackRate)}
+ {formatRate(playbackRate)}
- {qualityLabel}
+ {qualityLabel}
{isPiPSupported ? (
- {isPiPActive ? 'PiP On' : 'PiP'}
+ {isPiPActive ? 'PiP On' : 'PiP'}
) : null}
- {isFullscreen ? 'Exit' : 'Full'}
+ {isFullscreen ? 'Exit' : 'Full'}
{(isSpeedMenuOpen || isQualityMenuOpen) && (
-
+
{isSpeedMenuOpen ? (
-
+
{rateOptions.map(rate => (
handleSelectRate(rate)}
- style={styles.menuItem}
+ className="px-3 py-2"
>
{formatRate(rate)}
@@ -277,19 +288,18 @@ const VideoControls = ({
) : null}
{isQualityMenuOpen ? (
-
+
{qualityOptions.map(option => (
handleSelectQualityOption(option.id)}
- style={styles.menuItem}
+ className="px-3 py-2"
>
{option.label}
@@ -301,8 +311,8 @@ const VideoControls = ({
)}
{(isLoading || isBuffering || isSwitchingQuality) && (
-
-
+
+
{isSwitchingQuality ? 'Switching quality' : isBuffering ? 'Buffering' : 'Loading'}
@@ -338,131 +348,5 @@ function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
-const styles = StyleSheet.create({
- overlay: {
- ...StyleSheet.absoluteFillObject,
- justifyContent: 'space-between',
- backgroundColor: 'rgba(0, 0, 0, 0.35)',
- },
- topRow: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 12,
- paddingTop: 10,
- },
- topSpacer: {
- flex: 1,
- },
- centerRow: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- bottomRow: {
- paddingHorizontal: 12,
- paddingBottom: 12,
- },
- playButton: {
- backgroundColor: 'rgba(0, 0, 0, 0.55)',
- paddingHorizontal: 18,
- paddingVertical: 10,
- borderRadius: 22,
- },
- playText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- timeRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginBottom: 6,
- },
- timeText: {
- color: '#f1f1f1',
- fontSize: 12,
- },
- seekBar: {
- height: 22,
- borderRadius: 10,
- backgroundColor: 'rgba(255, 255, 255, 0.15)',
- justifyContent: 'center',
- marginBottom: 10,
- overflow: 'hidden',
- },
- seekBuffered: {
- position: 'absolute',
- left: 0,
- top: 0,
- bottom: 0,
- backgroundColor: 'rgba(255, 255, 255, 0.3)',
- },
- seekProgress: {
- position: 'absolute',
- left: 0,
- top: 0,
- bottom: 0,
- backgroundColor: '#fff',
- },
- seekThumb: {
- position: 'absolute',
- width: SEEK_THUMB_SIZE,
- height: SEEK_THUMB_SIZE,
- borderRadius: SEEK_THUMB_SIZE / 2,
- backgroundColor: '#fff',
- },
- controlsRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- controlButton: {
- paddingHorizontal: 10,
- paddingVertical: 6,
- },
- controlText: {
- color: '#fff',
- fontSize: 12,
- fontWeight: '600',
- },
- menuBackdrop: {
- marginTop: 8,
- },
- menu: {
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
- borderRadius: 8,
- paddingVertical: 6,
- },
- menuItem: {
- paddingHorizontal: 12,
- paddingVertical: 8,
- },
- menuText: {
- color: '#dcdcdc',
- fontSize: 13,
- },
- menuTextActive: {
- color: '#fff',
- fontWeight: '700',
- },
- previewBubble: {
- alignSelf: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.65)',
- paddingHorizontal: 10,
- paddingVertical: 4,
- borderRadius: 12,
- marginBottom: 8,
- },
- previewText: {
- color: '#fff',
- fontSize: 12,
- },
- statusRow: {
- marginTop: 6,
- },
- statusText: {
- color: '#fff',
- fontSize: 11,
- },
-});
-
export default VideoControls;
+
diff --git a/src/components/mobile/VoiceSearch.tsx b/src/components/mobile/VoiceSearch.tsx
index af5ff0f..8d51206 100644
--- a/src/components/mobile/VoiceSearch.tsx
+++ b/src/components/mobile/VoiceSearch.tsx
@@ -1,6 +1,6 @@
import { Mic, Square } from 'lucide-react-native';
import React, { useEffect } from 'react';
-import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';
import * as hooks from '../../hooks';
@@ -74,7 +74,7 @@ export const VoiceSearch = ({
@@ -88,38 +88,42 @@ export const VoiceSearch = ({
}
return (
-
+
{error ? (
-
+
{error}
) : null}
{isListening ? (
<>
- Stop
+ Stop
>
) : (
<>
- Voice
+
+ Voice
+
>
)}
{isListening && (
-
+
-
+
{transcript || 'Listening...'}
@@ -128,61 +132,3 @@ export const VoiceSearch = ({
);
};
-const styles = StyleSheet.create({
- wrapper: {
- alignItems: 'center',
- },
- button: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 6,
- paddingVertical: 10,
- paddingHorizontal: 16,
- borderRadius: 12,
- backgroundColor: '#F3F4F6',
- minWidth: 88,
- },
- buttonActive: {
- backgroundColor: '#19c3e6',
- },
- buttonDisabled: {
- opacity: 0.5,
- },
- buttonLabel: {
- fontSize: 14,
- fontWeight: '600',
- color: '#111827',
- },
- buttonLabelMuted: {
- color: '#9CA3AF',
- },
- error: {
- fontSize: 11,
- color: '#EF4444',
- marginBottom: 4,
- textAlign: 'center',
- maxWidth: 140,
- },
- listeningBar: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- marginTop: 8,
- paddingHorizontal: 12,
- paddingVertical: 6,
- backgroundColor: '#E0F2FE',
- borderRadius: 8,
- maxWidth: '100%',
- },
- listeningText: {
- fontSize: 13,
- color: '#0369A1',
- flex: 1,
- },
- micBtn: {
- padding: 10,
- justifyContent: 'center',
- alignItems: 'center',
- },
-});