feat: Add responsive styling and mobile support

- Applied Tailwind CSS responsive classes across all components
- Implemented mobile-first design approach with breakpoints (sm, md, lg)
- Added touch-friendly interactions with min-h-[44px] touch targets
- Enhanced App.tsx with responsive padding, headings, and alerts
- Updated TextInputSection with responsive textarea and full-width mobile button
- Improved VoiceAssignmentSection with flexible layouts and mobile-optimized controls
- Enhanced AudioPlayerSection with responsive sizing and touch support
- Added custom CSS for range slider with larger thumb for mobile
- Improved select dropdowns with visible arrows and proper padding
- Added touch-manipulation class to prevent double-tap zoom
- Added active states for better mobile feedback
- Updated index.html with proper title and meta description
- Created RESPONSIVE_IMPROVEMENTS.md documentation

Requirements: 12.1, 12.2, 12.3, 12.4
This commit is contained in:
2025-12-09 09:34:23 +00:00
parent e5bd7dcd74
commit 50b0ef792a
9 changed files with 224 additions and 71 deletions

View File

@@ -0,0 +1,101 @@
# Responsive Design Improvements
## Overview
This document outlines the responsive styling and mobile support improvements made to the Text to Audiobook application.
## Changes Made
### 1. App Component (App.tsx)
- Added responsive padding: `px-4 sm:px-6 lg:px-8`
- Responsive heading sizes: `text-2xl sm:text-3xl md:text-4xl`
- Responsive spacing: `mb-4 sm:mb-6 md:mb-8`
- Responsive text sizes for alerts: `text-sm sm:text-base`
- Responsive padding for alert boxes: `p-3 sm:p-4`
### 2. TextInputSection Component
- Responsive padding: `p-4 sm:p-6`
- Responsive heading: `text-xl sm:text-2xl`
- Responsive textarea height: `h-64 sm:h-80 md:h-96`
- Responsive padding for textarea: `p-3 sm:p-4`
- Full-width button on mobile, auto-width on desktop: `w-full sm:w-auto`
- Added `touch-manipulation` class for better touch response
- Minimum touch target height: `min-h-[44px]` (meets accessibility standards)
- Added `active:` states for better mobile feedback
### 3. VoiceAssignmentSection Component
- Responsive padding: `p-4 sm:p-6`
- Responsive headings: `text-lg sm:text-xl` and `text-xl sm:text-2xl`
- Flexible layout for role assignments: `flex-col sm:flex-row`
- Responsive label width: `sm:w-40`
- Responsive select padding: `p-3 sm:p-2`
- Minimum touch target height for all interactive elements: `min-h-[44px]`
- Responsive slider with larger touch area on mobile: `h-8 sm:h-auto`
- Full-width button on mobile: `w-full sm:w-auto`
- Added `touch-manipulation` for all interactive elements
- Responsive text sizes: `text-xs sm:text-sm` and `text-sm sm:text-base`
### 4. AudioPlayerSection Component
- Responsive padding: `p-4 sm:p-6`
- Responsive headings: `text-xl sm:text-2xl` and `text-base sm:text-lg`
- Responsive spacing: `mb-4 sm:mb-6`
- Full-width button on mobile: `w-full sm:w-auto`
- Minimum touch target height: `min-h-[44px]`
- Added `touch-manipulation` for button
- Added `active:` state for better mobile feedback
### 5. App.css Enhancements
- Custom range slider styling with larger thumb (24px) for better mobile interaction
- Touch-friendly audio controls with minimum height
- Custom select dropdown styling with visible arrow
- Responsive font sizing for small screens
- Proper spacing adjustments for very small screens (< 375px)
### 6. index.html
- Updated page title to "Text to Audiobook"
- Added meta description for better SEO
- Viewport meta tag already properly configured
## Mobile-First Approach
All components follow a mobile-first design approach:
1. Base styles are optimized for mobile devices
2. Responsive breakpoints add enhancements for larger screens
3. Touch targets meet WCAG 2.1 AA standards (minimum 44x44px)
4. Text remains readable on all screen sizes
## Breakpoints Used
- **sm**: 640px and up (tablets)
- **md**: 768px and up (small laptops)
- **lg**: 1024px and up (desktops)
## Accessibility Features
1. **Touch Targets**: All interactive elements have minimum 44x44px touch targets
2. **Touch Manipulation**: Added `touch-action: manipulation` to prevent double-tap zoom
3. **Active States**: Visual feedback on touch for better user experience
4. **Readable Text**: Responsive font sizes ensure readability on all devices
5. **Proper Spacing**: Adequate spacing between interactive elements on mobile
## Testing Recommendations
Test the application on:
- Mobile devices (320px - 480px width)
- Tablets (481px - 768px width)
- Laptops (769px - 1024px width)
- Desktops (1025px and above)
Test interactions:
- Touch gestures on mobile devices
- Textarea resizing
- Dropdown selections
- Range slider adjustments
- Button taps
- Audio player controls
## Browser Compatibility
The responsive design works on:
- Modern mobile browsers (iOS Safari, Chrome, Firefox)
- Desktop browsers (Chrome, Firefox, Safari, Edge)
- Tablets (iPad, Android tablets)

View File

@@ -4,7 +4,8 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<meta name="description" content="Convert text to multi-voice audiobooks using AI" />
<title>Text to Audiobook</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,42 +1,89 @@
/* Mobile-first responsive styles */
#root {
max-width: 1280px;
width: 100%;
margin: 0 auto;
padding: 2rem;
text-align: center;
padding: 0;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
/* Ensure touch-friendly interactions */
.touch-manipulation {
touch-action: manipulation;
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
/* Custom range slider styling for better mobile support */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
border-radius: 4px;
background: #d1d5db;
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #2563eb;
cursor: pointer;
transition: background 0.15s ease-in-out;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #1d4ed8;
}
input[type="range"]::-webkit-slider-thumb:active {
background: #1e40af;
}
input[type="range"]::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: #2563eb;
cursor: pointer;
border: none;
transition: background 0.15s ease-in-out;
}
input[type="range"]::-moz-range-thumb:hover {
background: #1d4ed8;
}
input[type="range"]::-moz-range-thumb:active {
background: #1e40af;
}
/* Ensure audio controls are touch-friendly on mobile */
audio {
min-height: 44px;
}
/* Improve select dropdown appearance on mobile */
select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
/* Responsive text sizing */
@media (max-width: 640px) {
body {
font-size: 14px;
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
/* Ensure proper spacing on very small screens */
@media (max-width: 375px) {
.container {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -113,11 +113,13 @@ function App() {
return (
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto py-8">
<h1 className="text-4xl font-bold text-center mb-8">Text to Audiobook</h1>
<div className="container mx-auto py-4 px-4 sm:py-8 sm:px-6 lg:px-8">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-center mb-4 sm:mb-6 md:mb-8">
Text to Audiobook
</h1>
{error && (
<div className="max-w-4xl mx-auto mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
<div className="max-w-4xl mx-auto mb-4 p-3 sm:p-4 bg-red-100 border border-red-400 text-red-700 rounded text-sm sm:text-base">
{error}
</div>
)}
@@ -135,7 +137,7 @@ function App() {
{phase === 'assignment' && (
<>
{isLongText && (
<div className="max-w-4xl mx-auto mb-4 p-4 bg-yellow-100 border border-yellow-400 text-yellow-800 rounded">
<div className="max-w-4xl mx-auto mb-4 p-3 sm:p-4 bg-yellow-100 border border-yellow-400 text-yellow-800 rounded text-sm sm:text-base">
Generation may take several minutes for very long content.
</div>
)}
@@ -162,7 +164,7 @@ function App() {
)}
{isGenerating && isLongText && (
<div className="max-w-4xl mx-auto mt-4 p-4 bg-blue-100 border border-blue-400 text-blue-800 rounded text-center">
<div className="max-w-4xl mx-auto mt-4 p-3 sm:p-4 bg-blue-100 border border-blue-400 text-blue-800 rounded text-center text-sm sm:text-base">
Generation may take several minutes for very long content. Please be patient...
</div>
)}

View File

@@ -8,15 +8,15 @@ export default function AudioPlayerSection({
onDownload
}: AudioPlayerSectionProps) {
return (
<div className="w-full max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-6">Your Audiobook is Ready!</h2>
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Your Audiobook is Ready!</h2>
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h3 className="text-lg font-semibold mb-4">Listen to your audiobook</h3>
<div className="bg-white rounded-lg shadow-md p-4 sm:p-6 mb-4 sm:mb-6">
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Listen to your audiobook</h3>
<audio
controls
src={audioUrl}
className="w-full"
className="w-full min-h-[44px]"
preload="metadata"
>
Your browser does not support the audio element.
@@ -26,7 +26,7 @@ export default function AudioPlayerSection({
<div className="flex justify-center">
<button
onClick={onDownload}
className="px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors cursor-pointer"
className="w-full sm:w-auto px-6 py-3 min-h-[44px] bg-green-600 hover:bg-green-700 active:bg-green-800 text-white font-semibold rounded-lg transition-colors cursor-pointer touch-manipulation"
>
Download audiobook
</button>

View File

@@ -36,21 +36,21 @@ export default function TextInputSection({
const isButtonDisabled = disabled || isLoading || text.trim() === '';
return (
<div className="w-full max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-4">Enter Your Text</h2>
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-bold mb-3 sm:mb-4">Enter Your Text</h2>
<div className="mb-4">
<textarea
value={text}
onChange={handleTextChange}
placeholder="Enter your text here. Use patterns like 'Name:' or '[Name]' to mark different speakers..."
className="w-full h-96 p-4 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none resize-y text-base"
className="w-full h-64 sm:h-80 md:h-96 p-3 sm:p-4 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none resize-y text-sm sm:text-base touch-manipulation"
disabled={disabled || isLoading}
/>
</div>
{showValidation && text.trim() === '' && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm sm:text-base">
Please enter some text before proceeding.
</div>
)}
@@ -58,10 +58,10 @@ export default function TextInputSection({
<button
onClick={handleNext}
disabled={isButtonDisabled}
className={`px-6 py-3 rounded-lg font-semibold text-white transition-colors ${
className={`w-full sm:w-auto px-6 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${
isButtonDisabled
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 cursor-pointer'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 cursor-pointer'
}`}
>
{isLoading ? 'Processing...' : 'Next: Assign voices'}

View File

@@ -73,22 +73,22 @@ export default function VoiceAssignmentSection({
const isGenerateDisabled = !allRolesAssigned || isGenerating;
return (
<div className="w-full max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-6">Assign voices to roles</h2>
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Assign voices to roles</h2>
{/* Role-to-Voice Mapping Section */}
<div className="mb-8">
<h3 className="text-xl font-semibold mb-4">Voice Assignments</h3>
<div className="space-y-4">
<div className="mb-6 sm:mb-8">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Voice Assignments</h3>
<div className="space-y-3 sm:space-y-4">
{roles.map((role) => (
<div key={role.id} className="flex items-center gap-4">
<label className="w-40 font-medium text-gray-700">
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<label className="font-medium text-gray-700 text-sm sm:text-base sm:w-40">
{role.label}:
</label>
<select
value={voiceAssignments.get(role.id) || ''}
onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
className="flex-1 p-2 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
className="flex-1 p-3 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation"
>
<option value="">Select a voice...</option>
{KOKORO_VOICES.map((voice) => (
@@ -104,11 +104,11 @@ export default function VoiceAssignmentSection({
{/* Language Selector */}
<div className="mb-6">
<h3 className="text-xl font-semibold mb-4">Language</h3>
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Language</h3>
<select
value={language}
onChange={handleLanguageSelect}
className="w-full p-2 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none"
className="w-full p-3 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation"
>
{LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
@@ -119,10 +119,10 @@ export default function VoiceAssignmentSection({
</div>
{/* Speed Slider */}
<div className="mb-8">
<h3 className="text-xl font-semibold mb-4">Playback Speed</h3>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600 w-12">0.5x</span>
<div className="mb-6 sm:mb-8">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Playback Speed</h3>
<div className="flex items-center gap-2 sm:gap-4">
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">0.5x</span>
<input
type="range"
min="0.5"
@@ -130,10 +130,10 @@ export default function VoiceAssignmentSection({
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="flex-1"
className="flex-1 h-8 sm:h-auto touch-manipulation"
/>
<span className="text-sm text-gray-600 w-12">1.5x</span>
<span className="font-semibold text-gray-800 w-16 text-center">
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">1.5x</span>
<span className="font-semibold text-gray-800 w-12 sm:w-16 text-center text-sm sm:text-base">
{speed.toFixed(1)}x
</span>
</div>
@@ -144,10 +144,10 @@ export default function VoiceAssignmentSection({
<button
onClick={onGenerate}
disabled={isGenerateDisabled}
className={`px-8 py-3 rounded-lg font-semibold text-white transition-colors ${
className={`w-full sm:w-auto px-8 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${
isGenerateDisabled
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700 cursor-pointer'
: 'bg-green-600 hover:bg-green-700 active:bg-green-800 cursor-pointer'
}`}
>
{isGenerating ? 'Generating...' : 'Generate Audiobook'}
@@ -156,7 +156,7 @@ export default function VoiceAssignmentSection({
{/* Validation Message */}
{!allRolesAssigned && !isGenerating && (
<div className="mt-4 p-3 bg-yellow-100 border border-yellow-400 text-yellow-800 rounded text-center">
<div className="mt-4 p-3 bg-yellow-100 border border-yellow-400 text-yellow-800 rounded text-center text-sm sm:text-base">
Please assign voices to all roles before generating the audiobook.
</div>
)}