feat: implement responsive design enhancements

- Add touch-optimized tap targets (44x44px minimum for iOS)
- Enhance mobile responsiveness across all components
- Improve button sizing and spacing for touch interactions
- Add responsive text sizing and layout adjustments
- Implement mobile-first modal behavior (bottom sheet on mobile)
- Enhance form controls with better touch targets
- Add active states for better touch feedback
- Improve grid layouts for various screen sizes
- Add smooth scrolling and better focus states
- Optimize FilterPanel for mobile with grid layouts
- Enhance all interactive elements with tap-target classes

Requirements: 7.1, 7.2
This commit is contained in:
2025-12-01 20:54:54 +00:00
parent 6cb9a593d4
commit a037117b15
22 changed files with 230 additions and 182 deletions

View File

@@ -92,27 +92,27 @@ export default function FilterPanel({
{/* Header */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
className="w-full flex items-center justify-between p-4 sm:p-5 hover:bg-gray-50 active:bg-gray-100 transition-colors tap-target"
>
<div className="flex items-center gap-2">
<Filter size={20} className="text-gray-600" />
<span className="font-medium text-gray-900">Filters & Sort</span>
<div className="flex items-center gap-2 sm:gap-3">
<Filter size={20} className="text-gray-600 flex-shrink-0" />
<span className="font-medium text-gray-900 text-sm sm:text-base">Filters & Sort</span>
{hasActiveFilters && (
<span className="bg-blue-100 text-blue-700 text-xs font-medium px-2 py-1 rounded">
<span className="bg-blue-100 text-blue-700 text-xs font-medium px-2 py-1 rounded whitespace-nowrap">
Active
</span>
)}
</div>
{isExpanded ? (
<ChevronUp size={20} className="text-gray-400" />
<ChevronUp size={20} className="text-gray-400 flex-shrink-0" />
) : (
<ChevronDown size={20} className="text-gray-400" />
<ChevronDown size={20} className="text-gray-400 flex-shrink-0" />
)}
</button>
{/* Filter Controls */}
{isExpanded && (
<div className="p-4 border-t border-gray-200 space-y-4">
<div className="p-4 sm:p-5 border-t border-gray-200 space-y-4 sm:space-y-5">
{/* Sort Controls */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
@@ -122,7 +122,7 @@ export default function FilterPanel({
<select
value={filters.sortBy}
onChange={(e) => handleSortByChange(e.target.value as FilterState['sortBy'])}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 px-3 sm:px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base tap-target"
>
<option value="createdAt">Created Date</option>
<option value="dueDate">Due Date</option>
@@ -131,8 +131,9 @@ export default function FilterPanel({
</select>
<button
onClick={handleSortOrderToggle}
className="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
className="px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors tap-target text-lg font-medium"
title={filters.sortOrder === 'asc' ? 'Ascending' : 'Descending'}
aria-label={filters.sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
>
{filters.sortOrder === 'asc' ? '↑' : '↓'}
</button>
@@ -144,43 +145,43 @@ export default function FilterPanel({
<label className="block text-sm font-medium text-gray-700 mb-2">
Priority
</label>
<div className="flex gap-2">
<div className="grid grid-cols-4 gap-2">
<button
onClick={() => handlePriorityChange(null)}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-2 sm:px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.priority === null
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
All
</button>
<button
onClick={() => handlePriorityChange('low')}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-2 sm:px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.priority === 'low'
? 'bg-green-50 border-green-500 text-green-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
Low
</button>
<button
onClick={() => handlePriorityChange('medium')}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-2 sm:px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.priority === 'medium'
? 'bg-yellow-50 border-yellow-500 text-yellow-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
Medium
Med
</button>
<button
onClick={() => handlePriorityChange('high')}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-2 sm:px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.priority === 'high'
? 'bg-red-50 border-red-500 text-red-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
High
@@ -193,36 +194,36 @@ export default function FilterPanel({
<label className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<div className="flex gap-2">
<div className="grid grid-cols-3 gap-2">
<button
onClick={() => handleCompletedChange(null)}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.completed === null
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
All
</button>
<button
onClick={() => handleCompletedChange(false)}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.completed === false
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
Incomplete
</button>
<button
onClick={() => handleCompletedChange(true)}
className={`flex-1 px-3 py-2 rounded-lg border transition-colors ${
className={`px-3 py-2.5 sm:py-3 rounded-lg border transition-colors tap-target text-xs sm:text-sm font-medium ${
filters.completed === true
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
Completed
Complete
</button>
</div>
</div>
@@ -238,10 +239,10 @@ export default function FilterPanel({
<button
key={tag}
onClick={() => handleTagToggle(tag)}
className={`px-3 py-1 rounded-full text-sm border transition-colors ${
className={`px-3 py-2 rounded-full text-xs sm:text-sm border transition-colors tap-target-sm font-medium ${
filters.tags.includes(tag)
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-300 hover:bg-gray-50'
: 'border-gray-300 hover:bg-gray-50 active:bg-gray-100'
}`}
>
{tag}
@@ -256,23 +257,23 @@ export default function FilterPanel({
<label className="block text-sm font-medium text-gray-700 mb-2">
Due Date Range
</label>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-500 mb-1">From</label>
<label className="block text-xs text-gray-500 mb-1.5">From</label>
<input
type="date"
value={filters.dueDateRange.start ? filters.dueDateRange.start.toISOString().split('T')[0] : ''}
onChange={(e) => handleDueDateStartChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 sm:px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base tap-target"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">To</label>
<label className="block text-xs text-gray-500 mb-1.5">To</label>
<input
type="date"
value={filters.dueDateRange.end ? filters.dueDateRange.end.toISOString().split('T')[0] : ''}
onChange={(e) => handleDueDateEndChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 sm:px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base tap-target"
/>
</div>
</div>
@@ -282,9 +283,9 @@ export default function FilterPanel({
{hasActiveFilters && (
<button
onClick={handleClearFilters}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 text-gray-700 rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
>
<X size={16} />
<X size={16} className="flex-shrink-0" />
Clear All Filters
</button>
)}