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

@@ -184,7 +184,7 @@
- Test "Add to Home Screen" functionality in Safari - Test "Add to Home Screen" functionality in Safari
- _Requirements: 4.1, 4.2_ - _Requirements: 4.1, 4.2_
- [ ] 14. Implement responsive design enhancements - [x] 14. Implement responsive design enhancements
- Ensure all components are mobile-responsive - Ensure all components are mobile-responsive
- Add touch-optimized controls (larger tap targets) - Add touch-optimized controls (larger tap targets)
- Test on various screen sizes - Test on various screen sizes

View File

@@ -9,4 +9,48 @@ mark {
padding: 0 2px; padding: 0 2px;
border-radius: 2px; border-radius: 2px;
font-weight: 500; font-weight: 500;
}
/* Touch-optimized tap targets - minimum 44x44px for iOS */
@layer utilities {
.tap-target {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tap-target-sm {
min-width: 40px;
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
/* Smooth scrolling for better mobile experience */
@layer base {
html {
-webkit-tap-highlight-color: transparent;
scroll-behavior: smooth;
}
/* Improve touch scrolling on iOS */
body {
-webkit-overflow-scrolling: touch;
}
/* Better focus states for accessibility */
*:focus-visible {
outline: 2px solid #3B82F6;
outline-offset: 2px;
}
/* Prevent text selection on buttons for better mobile UX */
button {
-webkit-tap-highlight-color: transparent;
user-select: none;
}
} }

View File

@@ -19,21 +19,21 @@ export default async function EditItemPage({ params }: PageProps) {
} }
return ( return (
<main className="container mx-auto px-4 py-8 max-w-2xl"> <main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-3xl">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-3 sm:gap-4 mb-6">
<Link <Link
href={`/list/${params.id}/item/${params.itemId}`} href={`/list/${params.id}/item/${params.itemId}`}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors" className="flex items-center gap-2 text-gray-600 hover:text-gray-900 active:text-black transition-colors tap-target-sm text-sm sm:text-base"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} className="flex-shrink-0" />
<span>Back</span> <span>Back</span>
</Link> </Link>
<h1 className="text-2xl font-bold text-gray-900">Edit Task</h1> <h1 className="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900">Edit Task</h1>
</div> </div>
{/* Form */} {/* Form */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-6 lg:p-8">
<ItemForm listId={params.id} item={item} mode="edit" /> <ItemForm listId={params.id} item={item} mode="edit" />
</div> </div>
</main> </main>

View File

@@ -2,7 +2,7 @@ import { notFound } from 'next/navigation'
import { getItem } from '@/lib/actions' import { getItem } from '@/lib/actions'
import ItemDetail from '@/components/ItemDetail' import ItemDetail from '@/components/ItemDetail'
import Link from 'next/link' import Link from 'next/link'
import { ArrowLeft, Edit, Trash2 } from 'lucide-react' import { ArrowLeft, Edit } from 'lucide-react'
import DeleteItemButton from '@/components/DeleteItemButton' import DeleteItemButton from '@/components/DeleteItemButton'
import ToggleItemButton from '@/components/ToggleItemButton' import ToggleItemButton from '@/components/ToggleItemButton'
@@ -21,24 +21,24 @@ export default async function ItemDetailPage({ params }: PageProps) {
} }
return ( return (
<main className="container mx-auto px-4 py-8 max-w-3xl"> <main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-4xl">
{/* Header Actions */} {/* Header Actions */}
<div className="flex items-center justify-between mb-6"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<Link <Link
href={`/list/${params.id}`} href={`/list/${params.id}`}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors" className="flex items-center gap-2 text-gray-600 hover:text-gray-900 active:text-black transition-colors tap-target-sm text-sm sm:text-base"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} className="flex-shrink-0" />
<span>Back to List</span> <span>Back to List</span>
</Link> </Link>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full sm:w-auto">
<ToggleItemButton itemId={item.id} completed={item.completed} /> <ToggleItemButton itemId={item.id} completed={item.completed} />
<Link <Link
href={`/list/${params.id}/item/${params.itemId}/edit`} href={`/list/${params.id}/item/${params.itemId}/edit`}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors" className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
> >
<Edit size={18} /> <Edit size={18} className="flex-shrink-0" />
<span>Edit</span> <span>Edit</span>
</Link> </Link>
<DeleteItemButton itemId={item.id} listId={params.id} /> <DeleteItemButton itemId={item.id} listId={params.id} />
@@ -46,7 +46,7 @@ export default async function ItemDetailPage({ params }: PageProps) {
</div> </div>
{/* Item Detail */} {/* Item Detail */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-6 lg:p-8">
<ItemDetail item={item} listTitle={item.list.title} /> <ItemDetail item={item} listTitle={item.list.title} />
</div> </div>
</main> </main>

View File

@@ -15,32 +15,32 @@ export default async function ListDetailPage({ params }: { params: { id: string
const completedItems = list.items.filter(item => item.completed) const completedItems = list.items.filter(item => item.completed)
return ( return (
<main className="container mx-auto px-4 py-8 max-w-4xl"> <main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-6xl">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<Link <Link
href="/" href="/"
className="inline-flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4 transition-colors" className="inline-flex items-center gap-2 text-gray-600 hover:text-gray-900 active:text-black mb-4 transition-colors tap-target-sm text-sm sm:text-base"
> >
<ArrowLeft size={20} /> <ArrowLeft size={20} className="flex-shrink-0" />
<span>Back to Lists</span> <span>Back to Lists</span>
</Link> </Link>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-2 sm:gap-3 mb-2">
<div <div
className="w-6 h-6 rounded-full flex-shrink-0" className="w-5 h-5 sm:w-6 sm:h-6 rounded-full flex-shrink-0"
style={{ backgroundColor: list.color }} style={{ backgroundColor: list.color }}
/> />
<h1 className="text-3xl font-bold text-gray-900">{list.title}</h1> <h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 truncate">{list.title}</h1>
</div> </div>
<p className="text-gray-600"> <p className="text-gray-600 text-sm sm:text-base">
{completedItems.length} of {list.items.length} tasks completed {completedItems.length} of {list.items.length} tasks completed
</p> </p>
</div> </div>
{/* Inline Item Creation */} {/* Inline Item Creation */}
<div className="mb-6"> <div className="mb-6 sm:mb-8">
<InlineItemCreate listId={list.id} /> <InlineItemCreate listId={list.id} />
</div> </div>

View File

@@ -7,27 +7,27 @@ export default async function Home() {
const lists = await getLists() const lists = await getLists()
return ( return (
<main className="container mx-auto px-4 py-8 max-w-4xl"> <main className="container mx-auto px-4 sm:px-6 py-6 sm:py-8 max-w-7xl">
<div className="flex items-center justify-between mb-8"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6 sm:mb-8">
<h1 className="text-3xl font-bold text-gray-900">Ultimate Lists</h1> <h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900">Ultimate Lists</h1>
<NewListButton /> <NewListButton />
</div> </div>
{/* Global Search */} {/* Global Search */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<GlobalSearch /> <GlobalSearch />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
{lists.map((list) => ( {lists.map((list) => (
<ListCard key={list.id} list={list} /> <ListCard key={list.id} list={list} />
))} ))}
</div> </div>
{lists.length === 0 && ( {lists.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12 sm:py-16">
<p className="text-gray-500 text-lg mb-4">No lists yet</p> <p className="text-gray-500 text-base sm:text-lg mb-3 sm:mb-4">No lists yet</p>
<p className="text-gray-400">Create your first list to get started</p> <p className="text-gray-400 text-sm sm:text-base">Create your first list to get started</p>
</div> </div>
)} )}
</main> </main>

View File

@@ -52,14 +52,14 @@ export default function DeleteItemButton({ itemId, listId, variant = 'full' }: D
<button <button
onClick={handleDelete} onClick={handleDelete}
disabled={isDeleting} disabled={isDeleting}
className="px-3 py-2 bg-red-500 hover:bg-red-600 disabled:bg-red-300 text-white rounded-lg text-sm transition-colors" className="px-3 sm:px-4 py-2 sm:py-2.5 bg-red-500 hover:bg-red-600 active:bg-red-700 disabled:bg-red-300 text-white rounded-lg text-xs sm:text-sm transition-colors tap-target-sm font-medium"
> >
{isDeleting ? 'Deleting...' : 'Confirm'} {isDeleting ? 'Deleting...' : 'Confirm'}
</button> </button>
<button <button
onClick={handleCancel} onClick={handleCancel}
disabled={isDeleting} disabled={isDeleting}
className="px-3 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg text-sm transition-colors" className="px-3 sm:px-4 py-2 sm:py-2.5 bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 rounded-lg text-xs sm:text-sm transition-colors tap-target-sm font-medium"
> >
Cancel Cancel
</button> </button>
@@ -71,7 +71,7 @@ export default function DeleteItemButton({ itemId, listId, variant = 'full' }: D
return ( return (
<button <button
onClick={handleShowConfirm} onClick={handleShowConfirm}
className="text-gray-400 hover:text-red-600 transition-colors p-2 hover:bg-red-50 rounded-lg" className="text-gray-400 hover:text-red-600 active:text-red-700 transition-colors tap-target-sm hover:bg-red-50 active:bg-red-100 rounded-lg"
aria-label="Delete item" aria-label="Delete item"
> >
<Trash2 size={18} /> <Trash2 size={18} />
@@ -82,9 +82,9 @@ export default function DeleteItemButton({ itemId, listId, variant = 'full' }: D
return ( return (
<button <button
onClick={handleShowConfirm} onClick={handleShowConfirm}
className="flex items-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg transition-colors" className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
> >
<Trash2 size={18} /> <Trash2 size={18} className="flex-shrink-0" />
<span>Delete</span> <span>Delete</span>
</button> </button>
) )

View File

@@ -45,7 +45,7 @@ export default function DeleteListButton({
<> <>
<button <button
onClick={() => setIsConfirmOpen(true)} onClick={() => setIsConfirmOpen(true)}
className="text-red-600 hover:text-red-700 transition-colors p-2 hover:bg-red-50 rounded-lg" className="text-red-600 hover:text-red-700 active:text-red-800 transition-colors tap-target-sm hover:bg-red-50 active:bg-red-100 rounded-lg"
aria-label="Delete list" aria-label="Delete list"
> >
<Trash2 size={20} /> <Trash2 size={20} />
@@ -57,26 +57,26 @@ export default function DeleteListButton({
title="Delete List" title="Delete List"
> >
<div className="space-y-4"> <div className="space-y-4">
<p className="text-gray-700"> <p className="text-gray-700 text-sm sm:text-base">
Are you sure you want to delete <strong>{listTitle}</strong>? Are you sure you want to delete <strong>{listTitle}</strong>?
</p> </p>
{itemCount > 0 && ( {itemCount > 0 && (
<p className="text-red-600 text-sm"> <p className="text-red-600 text-xs sm:text-sm font-medium">
This will permanently delete {itemCount} {itemCount === 1 ? 'item' : 'items'}. This will permanently delete {itemCount} {itemCount === 1 ? 'item' : 'items'}.
</p> </p>
)} )}
<div className="flex gap-2 justify-end pt-2"> <div className="flex flex-col-reverse sm:flex-row gap-2 sm:gap-3 justify-end pt-2">
<button <button
onClick={() => setIsConfirmOpen(false)} onClick={() => setIsConfirmOpen(false)}
disabled={isDeleting} disabled={isDeleting}
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50" className="w-full sm:w-auto px-4 sm:px-5 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 rounded-lg transition-colors disabled:opacity-50 tap-target font-medium text-sm sm:text-base"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleDelete} onClick={handleDelete}
disabled={isDeleting} disabled={isDeleting}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors disabled:opacity-50" className="w-full sm:w-auto px-4 sm:px-5 py-3 bg-red-600 hover:bg-red-700 active:bg-red-800 text-white rounded-lg transition-colors disabled:opacity-50 tap-target font-medium text-sm sm:text-base"
> >
{isDeleting ? 'Deleting...' : 'Delete'} {isDeleting ? 'Deleting...' : 'Delete'}
</button> </button>

View File

@@ -92,27 +92,27 @@ export default function FilterPanel({
{/* Header */} {/* Header */}
<button <button
onClick={() => setIsExpanded(!isExpanded)} 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"> <div className="flex items-center gap-2 sm:gap-3">
<Filter size={20} className="text-gray-600" /> <Filter size={20} className="text-gray-600 flex-shrink-0" />
<span className="font-medium text-gray-900">Filters & Sort</span> <span className="font-medium text-gray-900 text-sm sm:text-base">Filters & Sort</span>
{hasActiveFilters && ( {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 Active
</span> </span>
)} )}
</div> </div>
{isExpanded ? ( {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> </button>
{/* Filter Controls */} {/* Filter Controls */}
{isExpanded && ( {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 */} {/* Sort Controls */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
@@ -122,7 +122,7 @@ export default function FilterPanel({
<select <select
value={filters.sortBy} value={filters.sortBy}
onChange={(e) => handleSortByChange(e.target.value as FilterState['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="createdAt">Created Date</option>
<option value="dueDate">Due Date</option> <option value="dueDate">Due Date</option>
@@ -131,8 +131,9 @@ export default function FilterPanel({
</select> </select>
<button <button
onClick={handleSortOrderToggle} 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'} title={filters.sortOrder === 'asc' ? 'Ascending' : 'Descending'}
aria-label={filters.sortOrder === 'asc' ? 'Sort ascending' : 'Sort descending'}
> >
{filters.sortOrder === 'asc' ? '↑' : '↓'} {filters.sortOrder === 'asc' ? '↑' : '↓'}
</button> </button>
@@ -144,43 +145,43 @@ export default function FilterPanel({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Priority Priority
</label> </label>
<div className="flex gap-2"> <div className="grid grid-cols-4 gap-2">
<button <button
onClick={() => handlePriorityChange(null)} 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 filters.priority === null
? 'bg-blue-50 border-blue-500 text-blue-700' ? '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 All
</button> </button>
<button <button
onClick={() => handlePriorityChange('low')} 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' filters.priority === 'low'
? 'bg-green-50 border-green-500 text-green-700' ? '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 Low
</button> </button>
<button <button
onClick={() => handlePriorityChange('medium')} 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' filters.priority === 'medium'
? 'bg-yellow-50 border-yellow-500 text-yellow-700' ? '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>
<button <button
onClick={() => handlePriorityChange('high')} 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' filters.priority === 'high'
? 'bg-red-50 border-red-500 text-red-700' ? '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 High
@@ -193,36 +194,36 @@ export default function FilterPanel({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Status Status
</label> </label>
<div className="flex gap-2"> <div className="grid grid-cols-3 gap-2">
<button <button
onClick={() => handleCompletedChange(null)} 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 filters.completed === null
? 'bg-blue-50 border-blue-500 text-blue-700' ? '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 All
</button> </button>
<button <button
onClick={() => handleCompletedChange(false)} 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 filters.completed === false
? 'bg-blue-50 border-blue-500 text-blue-700' ? '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 Incomplete
</button> </button>
<button <button
onClick={() => handleCompletedChange(true)} 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 filters.completed === true
? 'bg-blue-50 border-blue-500 text-blue-700' ? '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> </button>
</div> </div>
</div> </div>
@@ -238,10 +239,10 @@ export default function FilterPanel({
<button <button
key={tag} key={tag}
onClick={() => handleTagToggle(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) filters.tags.includes(tag)
? 'bg-blue-50 border-blue-500 text-blue-700' ? '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} {tag}
@@ -256,23 +257,23 @@ export default function FilterPanel({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Due Date Range Due Date Range
</label> </label>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <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 <input
type="date" type="date"
value={filters.dueDateRange.start ? filters.dueDateRange.start.toISOString().split('T')[0] : ''} value={filters.dueDateRange.start ? filters.dueDateRange.start.toISOString().split('T')[0] : ''}
onChange={(e) => handleDueDateStartChange(e.target.value)} 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>
<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 <input
type="date" type="date"
value={filters.dueDateRange.end ? filters.dueDateRange.end.toISOString().split('T')[0] : ''} value={filters.dueDateRange.end ? filters.dueDateRange.end.toISOString().split('T')[0] : ''}
onChange={(e) => handleDueDateEndChange(e.target.value)} 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>
</div> </div>
@@ -282,9 +283,9 @@ export default function FilterPanel({
{hasActiveFilters && ( {hasActiveFilters && (
<button <button
onClick={handleClearFilters} 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 Clear All Filters
</button> </button>
)} )}

View File

@@ -51,16 +51,16 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
return ( return (
<button <button
onClick={() => setIsCreating(true)} onClick={() => setIsCreating(true)}
className="w-full flex items-center gap-2 px-4 py-3 bg-white border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-blue-400 hover:text-blue-600 transition-colors" className="w-full flex items-center gap-2 sm:gap-3 px-4 sm:px-5 py-3 sm:py-4 bg-white border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-blue-400 hover:text-blue-600 active:border-blue-500 active:text-blue-700 transition-colors tap-target text-sm sm:text-base font-medium"
> >
<Plus size={20} /> <Plus size={20} className="flex-shrink-0" />
<span>Add a task</span> <span>Add a task</span>
</button> </button>
) )
} }
return ( return (
<form onSubmit={handleSubmit} className="bg-white border-2 border-blue-400 rounded-lg p-4"> <form onSubmit={handleSubmit} className="bg-white border-2 border-blue-400 rounded-lg p-4 sm:p-5 shadow-sm">
<input <input
type="text" type="text"
value={title} value={title}
@@ -68,23 +68,23 @@ export default function InlineItemCreate({ listId }: InlineItemCreateProps) {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Enter task title..." placeholder="Enter task title..."
autoFocus autoFocus
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 mb-3" 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 mb-3 sm:mb-4 text-sm sm:text-base"
/> />
<div className="flex gap-2"> <div className="flex flex-col-reverse sm:flex-row gap-2 sm:gap-3">
<button
type="submit"
disabled={!title.trim() || isSubmitting}
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white rounded-lg transition-colors text-sm"
>
{isSubmitting ? 'Adding...' : 'Add Task'}
</button>
<button <button
type="button" type="button"
onClick={handleCancel} onClick={handleCancel}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors text-sm" className="w-full sm:w-auto px-4 sm:px-5 py-2.5 sm:py-3 bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
> >
Cancel Cancel
</button> </button>
<button
type="submit"
disabled={!title.trim() || isSubmitting}
className="w-full sm:w-auto px-4 sm:px-5 py-2.5 sm:py-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-300 text-white rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
>
{isSubmitting ? 'Adding...' : 'Add Task'}
</button>
</div> </div>
</form> </form>
) )

View File

@@ -1,8 +1,7 @@
'use client' 'use client'
import { useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Calendar, Tag, AlertCircle, Trash2 } from 'lucide-react' import { Calendar, Tag, AlertCircle } from 'lucide-react'
import ToggleItemButton from './ToggleItemButton' import ToggleItemButton from './ToggleItemButton'
import DeleteItemButton from './DeleteItemButton' import DeleteItemButton from './DeleteItemButton'
@@ -33,24 +32,24 @@ export default function ItemCard({ item, listId }: ItemCardProps) {
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || priorityColors.medium const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || priorityColors.medium
return ( return (
<div className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow group"> <div className="bg-white rounded-lg border border-gray-200 p-4 sm:p-5 hover:shadow-md active:shadow-lg transition-all group">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3 sm:gap-4">
{/* Completion Toggle */} {/* Completion Toggle */}
<div className="pt-1"> <div className="pt-0.5 sm:pt-1 flex-shrink-0">
<ToggleItemButton itemId={item.id} completed={item.completed} variant="icon" /> <ToggleItemButton itemId={item.id} completed={item.completed} variant="icon" />
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<Link href={`/list/${listId}/item/${item.id}`} className="block"> <Link href={`/list/${listId}/item/${item.id}`} className="block">
<h3 className={`font-medium mb-1 ${ <h3 className={`font-medium mb-1 text-sm sm:text-base ${
item.completed ? 'text-gray-500 line-through' : 'text-gray-900' item.completed ? 'text-gray-500 line-through' : 'text-gray-900'
}`}> }`}>
{item.title} {item.title}
</h3> </h3>
{item.description && ( {item.description && (
<p className={`text-sm mb-2 line-clamp-2 ${ <p className={`text-xs sm:text-sm mb-2 line-clamp-2 ${
item.completed ? 'text-gray-400' : 'text-gray-600' item.completed ? 'text-gray-400' : 'text-gray-600'
}`}> }`}>
{item.description} {item.description}
@@ -58,34 +57,36 @@ export default function ItemCard({ item, listId }: ItemCardProps) {
)} )}
{/* Metadata */} {/* Metadata */}
<div className="flex flex-wrap items-center gap-2 mt-2"> <div className="flex flex-wrap items-center gap-1.5 sm:gap-2 mt-2">
{/* Priority Badge */} {/* Priority Badge */}
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium border ${priorityColor}`}> <span className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium border ${priorityColor}`}>
<AlertCircle size={12} /> <AlertCircle size={12} className="flex-shrink-0" />
{item.priority.charAt(0).toUpperCase() + item.priority.slice(1)} <span className="hidden xs:inline sm:inline">{item.priority.charAt(0).toUpperCase() + item.priority.slice(1)}</span>
<span className="xs:hidden sm:hidden">{item.priority.charAt(0).toUpperCase()}</span>
</span> </span>
{/* Due Date */} {/* Due Date */}
{item.dueDate && ( {item.dueDate && (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs ${ <span className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs whitespace-nowrap ${
isOverdue isOverdue
? 'bg-red-100 text-red-700 border border-red-200 font-semibold' ? 'bg-red-100 text-red-700 border border-red-200 font-semibold'
: 'bg-gray-100 text-gray-700' : 'bg-gray-100 text-gray-700'
}`}> }`}>
<Calendar size={12} /> <Calendar size={12} className="flex-shrink-0" />
{new Date(item.dueDate).toLocaleDateString('en-US', { {new Date(item.dueDate).toLocaleDateString('en-US', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
})} })}
{isOverdue && ' (Overdue)'} {isOverdue && <span className="hidden sm:inline"> (Overdue)</span>}
{isOverdue && <span className="sm:hidden">!</span>}
</span> </span>
)} )}
{/* Tags */} {/* Tags */}
{tags.length > 0 && ( {tags.length > 0 && (
<div className="inline-flex items-center gap-1"> <div className="inline-flex items-center gap-1">
<Tag size={12} className="text-gray-400" /> <Tag size={12} className="text-gray-400 flex-shrink-0" />
<span className="text-xs text-gray-600"> <span className="text-xs text-gray-600 truncate max-w-[120px] sm:max-w-none">
{tags.slice(0, 2).join(', ')} {tags.slice(0, 2).join(', ')}
{tags.length > 2 && ` +${tags.length - 2}`} {tags.length > 2 && ` +${tags.length - 2}`}
</span> </span>
@@ -96,7 +97,7 @@ export default function ItemCard({ item, listId }: ItemCardProps) {
</div> </div>
{/* Delete Button */} {/* Delete Button */}
<div className="opacity-0 group-hover:opacity-100 transition-opacity"> <div className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity flex-shrink-0">
<DeleteItemButton itemId={item.id} variant="icon" /> <DeleteItemButton itemId={item.id} variant="icon" />
</div> </div>
</div> </div>

View File

@@ -112,10 +112,10 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
} }
return ( return (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-5 sm:space-y-6">
{/* Title */} {/* Title */}
<div> <div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
Title * Title *
</label> </label>
<input <input
@@ -123,19 +123,19 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
id="title" id="title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base ${
errors.title ? 'border-red-500' : 'border-gray-300' errors.title ? 'border-red-500' : 'border-gray-300'
}`} }`}
placeholder="Enter task title" placeholder="Enter task title"
/> />
{errors.title && ( {errors.title && (
<p className="mt-1 text-sm text-red-600">{errors.title}</p> <p className="mt-1.5 text-xs sm:text-sm text-red-600">{errors.title}</p>
)} )}
</div> </div>
{/* Description */} {/* Description */}
<div> <div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
Description Description
</label> </label>
<textarea <textarea
@@ -143,21 +143,21 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
rows={3} rows={3}
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 resize-y"
placeholder="Add a description" placeholder="Add a description"
/> />
</div> </div>
{/* Priority */} {/* Priority */}
<div> <div>
<label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-2">
Priority Priority
</label> </label>
<select <select
id="priority" id="priority"
value={priority} value={priority}
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')} onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')}
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"
> >
<option value="low">Low</option> <option value="low">Low</option>
<option value="medium">Medium</option> <option value="medium">Medium</option>
@@ -167,7 +167,7 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
{/* Due Date */} {/* Due Date */}
<div> <div>
<label htmlFor="dueDate" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="dueDate" className="block text-sm font-medium text-gray-700 mb-2">
Due Date Due Date
</label> </label>
<input <input
@@ -175,29 +175,29 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
id="dueDate" id="dueDate"
value={dueDate} value={dueDate}
onChange={(e) => setDueDate(e.target.value)} onChange={(e) => setDueDate(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>
{/* Tags */} {/* Tags */}
<div> <div>
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-2">
Tags Tags
</label> </label>
<div className="flex gap-2 mb-2"> <div className="flex gap-2 mb-2 sm:mb-3">
<input <input
type="text" type="text"
id="tags" id="tags"
value={tagInput} value={tagInput}
onChange={(e) => setTagInput(e.target.value)} onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
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"
placeholder="Add a tag" placeholder="Add a tag"
/> />
<button <button
type="button" type="button"
onClick={handleAddTag} onClick={handleAddTag}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors" className="px-4 sm:px-5 py-3 bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
> >
Add Add
</button> </button>
@@ -207,13 +207,14 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
{tags.map((tag) => ( {tags.map((tag) => (
<span <span
key={tag} key={tag}
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm" className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-blue-100 text-blue-800 rounded-full text-xs sm:text-sm"
> >
{tag} {tag}
<button <button
type="button" type="button"
onClick={() => handleRemoveTag(tag)} onClick={() => handleRemoveTag(tag)}
className="hover:text-blue-600" className="hover:text-blue-600 active:text-blue-700 tap-target-sm text-lg leading-none"
aria-label={`Remove ${tag} tag`}
> >
× ×
</button> </button>
@@ -225,7 +226,7 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
{/* Notes */} {/* Notes */}
<div> <div>
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-2">
Notes Notes
</label> </label>
<textarea <textarea
@@ -233,34 +234,34 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps)
value={notes} value={notes}
onChange={(e) => setNotes(e.target.value)} onChange={(e) => setNotes(e.target.value)}
rows={5} rows={5}
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 resize-y"
placeholder="Add additional notes" placeholder="Add additional notes"
/> />
</div> </div>
{/* Error message */} {/* Error message */}
{errors.submit && ( {errors.submit && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="p-3 sm:p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{errors.submit}</p> <p className="text-xs sm:text-sm text-red-600">{errors.submit}</p>
</div> </div>
)} )}
{/* Actions */} {/* Actions */}
<div className="flex gap-3"> <div className="flex flex-col-reverse sm:flex-row gap-2 sm:gap-3 pt-2">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-4 py-2 rounded-lg transition-colors"
>
{isSubmitting ? 'Saving...' : mode === 'create' ? 'Create Task' : 'Save Changes'}
</button>
<button <button
type="button" type="button"
onClick={() => router.back()} onClick={() => router.back()}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors" className="w-full sm:w-auto px-4 sm:px-5 py-3 bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 rounded-lg transition-colors tap-target font-medium text-sm sm:text-base"
> >
Cancel Cancel
</button> </button>
<button
type="submit"
disabled={isSubmitting}
className="w-full sm:flex-1 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-300 text-white px-4 sm:px-5 py-3 rounded-lg transition-colors tap-target font-medium text-sm sm:text-base"
>
{isSubmitting ? 'Saving...' : mode === 'create' ? 'Create Task' : 'Save Changes'}
</button>
</div> </div>
</form> </form>
) )

View File

@@ -37,19 +37,19 @@ export default function ListCard({ list }: { list: List }) {
return ( return (
<> <>
<Link href={`/list/${list.id}`}> <Link href={`/list/${list.id}`}>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow cursor-pointer group"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-6 hover:shadow-md active:shadow-lg transition-all cursor-pointer group min-h-[140px] sm:min-h-[160px]">
<div className="flex items-center justify-between mb-4"> <div className="flex items-start justify-between mb-4 gap-2">
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
<div <div
className="w-4 h-4 rounded-full flex-shrink-0" className="w-3 h-3 sm:w-4 sm:h-4 rounded-full flex-shrink-0"
style={{ backgroundColor: list.color }} style={{ backgroundColor: list.color }}
/> />
<h3 className="font-semibold text-gray-900 truncate">{list.title}</h3> <h3 className="font-semibold text-gray-900 truncate text-base sm:text-lg">{list.title}</h3>
</div> </div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity" onClick={handleDeleteClick}> <div className="flex items-center gap-0.5 sm:gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity flex-shrink-0" onClick={handleDeleteClick}>
<button <button
onClick={handleEditClick} onClick={handleEditClick}
className="text-gray-600 hover:text-gray-900 transition-colors p-2 hover:bg-gray-100 rounded-lg" className="text-gray-600 hover:text-gray-900 active:text-black transition-colors tap-target-sm hover:bg-gray-100 active:bg-gray-200 rounded-lg"
aria-label="Edit list" aria-label="Edit list"
> >
<Edit2 size={18} /> <Edit2 size={18} />
@@ -62,15 +62,15 @@ export default function ListCard({ list }: { list: List }) {
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-2.5 sm:space-y-3">
<div className="flex items-center justify-between text-sm text-gray-600"> <div className="flex items-center justify-between text-xs sm:text-sm text-gray-600">
<span>{completedCount} of {totalCount} completed</span> <span>{completedCount} of {totalCount} completed</span>
<span>{Math.round(progress)}%</span> <span className="font-medium">{Math.round(progress)}%</span>
</div> </div>
<div className="w-full bg-gray-200 rounded-full h-2"> <div className="w-full bg-gray-200 rounded-full h-2 sm:h-2.5">
<div <div
className="h-2 rounded-full transition-all duration-300" className="h-2 sm:h-2.5 rounded-full transition-all duration-300"
style={{ style={{
width: `${progress}%`, width: `${progress}%`,
backgroundColor: list.color backgroundColor: list.color

View File

@@ -66,9 +66,9 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
} }
return ( return (
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
<div> <div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
List Title List Title
</label> </label>
<input <input
@@ -76,7 +76,7 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
id="title" id="title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(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"
placeholder="Enter list title" placeholder="Enter list title"
disabled={isSubmitting} disabled={isSubmitting}
autoFocus autoFocus
@@ -84,19 +84,19 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2 sm:mb-3">
Color Color
</label> </label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 sm:gap-3 flex-wrap">
{PRESET_COLORS.map((presetColor) => ( {PRESET_COLORS.map((presetColor) => (
<button <button
key={presetColor} key={presetColor}
type="button" type="button"
onClick={() => setColor(presetColor)} onClick={() => setColor(presetColor)}
className={`w-10 h-10 rounded-lg transition-all ${ className={`w-11 h-11 sm:w-12 sm:h-12 rounded-lg transition-all tap-target-sm ${
color === presetColor color === presetColor
? 'ring-2 ring-offset-2 ring-gray-400 scale-110' ? 'ring-2 ring-offset-2 ring-gray-400 scale-110'
: 'hover:scale-105' : 'hover:scale-105 active:scale-95'
}`} }`}
style={{ backgroundColor: presetColor }} style={{ backgroundColor: presetColor }}
disabled={isSubmitting} disabled={isSubmitting}
@@ -107,16 +107,16 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
</div> </div>
{error && ( {error && (
<div className="text-red-600 text-sm">{error}</div> <div className="text-red-600 text-xs sm:text-sm font-medium">{error}</div>
)} )}
<div className="flex gap-2 justify-end pt-2"> <div className="flex flex-col-reverse sm:flex-row gap-2 sm:gap-3 justify-end pt-2">
{onCancel && ( {onCancel && (
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
disabled={isSubmitting} disabled={isSubmitting}
className="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50" className="w-full sm:w-auto px-4 sm:px-5 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 rounded-lg transition-colors disabled:opacity-50 tap-target font-medium text-sm sm:text-base"
> >
Cancel Cancel
</button> </button>
@@ -124,7 +124,7 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors disabled:opacity-50" className="w-full sm:w-auto px-4 sm:px-5 py-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50 tap-target font-medium text-sm sm:text-base"
> >
{isSubmitting ? 'Saving...' : list ? 'Update List' : 'Create List'} {isSubmitting ? 'Saving...' : list ? 'Update List' : 'Create List'}
</button> </button>

View File

@@ -42,27 +42,27 @@ export default function Modal({ isOpen, onClose, title, children }: ModalProps)
if (!isOpen) return null if (!isOpen) return null
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
{/* Backdrop */} {/* Backdrop */}
<div <div
className="absolute inset-0 bg-black bg-opacity-50" className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
onClick={onClose} onClick={onClose}
/> />
{/* Modal */} {/* Modal */}
<div className="relative bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"> <div className="relative bg-white rounded-t-2xl sm:rounded-lg shadow-xl max-w-md w-full mx-0 sm:mx-4 max-h-[90vh] overflow-y-auto transform transition-transform">
<div className="flex items-center justify-between p-4 border-b border-gray-200"> <div className="flex items-center justify-between p-4 sm:p-5 border-b border-gray-200 sticky top-0 bg-white z-10">
<h2 className="text-xl font-semibold text-gray-900">{title}</h2> <h2 className="text-lg sm:text-xl font-semibold text-gray-900">{title}</h2>
<button <button
onClick={onClose} onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors" className="text-gray-400 hover:text-gray-600 active:text-gray-700 transition-colors tap-target-sm -mr-2"
aria-label="Close modal" aria-label="Close modal"
> >
<X size={24} /> <X size={24} />
</button> </button>
</div> </div>
<div className="p-4"> <div className="p-4 sm:p-5">
{children} {children}
</div> </div>
</div> </div>

View File

@@ -12,10 +12,11 @@ export default function NewListButton() {
<> <>
<button <button
onClick={() => setIsModalOpen(true)} onClick={() => setIsModalOpen(true)}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white px-4 sm:px-6 py-3 rounded-lg flex items-center gap-2 transition-colors tap-target shadow-sm hover:shadow-md text-sm sm:text-base font-medium"
> >
<PlusIcon size={20} /> <PlusIcon size={20} className="flex-shrink-0" />
New List <span className="hidden xs:inline sm:inline">New List</span>
<span className="xs:hidden sm:hidden">New</span>
</button> </button>
<Modal <Modal

View File

@@ -25,7 +25,7 @@ export default function SearchBar({
<div className="relative"> <div className="relative">
<div className="relative"> <div className="relative">
<Search <Search
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" className="absolute left-3 sm:left-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none"
size={20} size={20}
/> />
<input <input
@@ -33,19 +33,19 @@ export default function SearchBar({
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
className="w-full pl-10 pr-10 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full pl-10 sm:pl-12 pr-10 sm:pr-12 py-3 sm:py-3.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm sm:text-base transition-shadow"
/> />
{query && ( {query && (
<button <button
onClick={clearSearch} onClick={clearSearch}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors" className="absolute right-3 sm:right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 active:text-gray-700 transition-colors tap-target-sm"
aria-label="Clear search" aria-label="Clear search"
> >
<X size={20} /> <X size={20} />
</button> </button>
)} )}
{isDebouncing && ( {isDebouncing && (
<div className="absolute right-10 top-1/2 -translate-y-1/2"> <div className="absolute right-12 sm:right-14 top-1/2 -translate-y-1/2">
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
</div> </div>
)} )}

View File

@@ -31,13 +31,13 @@ export default function ToggleItemButton({ itemId, completed, variant = 'full' }
<button <button
onClick={handleToggle} onClick={handleToggle}
disabled={isToggling} disabled={isToggling}
className="hover:scale-110 transition-transform disabled:opacity-50" className="hover:scale-110 active:scale-95 transition-transform disabled:opacity-50 tap-target-sm"
aria-label={completed ? 'Mark incomplete' : 'Mark complete'} aria-label={completed ? 'Mark incomplete' : 'Mark complete'}
> >
{completed ? ( {completed ? (
<CheckCircle2 size={20} className="text-green-500" /> <CheckCircle2 size={22} className="text-green-500" />
) : ( ) : (
<Circle size={20} className="text-gray-400 hover:text-gray-600" /> <Circle size={22} className="text-gray-400 hover:text-gray-600 active:text-gray-700" />
)} )}
</button> </button>
) )
@@ -47,16 +47,16 @@ export default function ToggleItemButton({ itemId, completed, variant = 'full' }
<button <button
onClick={handleToggle} onClick={handleToggle}
disabled={isToggling} disabled={isToggling}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 text-gray-700 rounded-lg transition-colors" className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 disabled:bg-gray-50 text-gray-700 rounded-lg transition-colors tap-target text-sm sm:text-base font-medium"
> >
{completed ? ( {completed ? (
<> <>
<CheckCircle2 size={18} className="text-green-500" /> <CheckCircle2 size={18} className="text-green-500 flex-shrink-0" />
<span>Mark Incomplete</span> <span>Mark Incomplete</span>
</> </>
) : ( ) : (
<> <>
<Circle size={18} /> <Circle size={18} className="flex-shrink-0" />
<span>Mark Complete</span> <span>Mark Complete</span>
</> </>
)} )}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

BIN
public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB