feat: enhance mobile and desktop layouts in Daily and Kanban pages
- Refactored `DailyPageClient` to prioritize mobile layout with today's section first and calendar at the bottom for better usability. - Updated `KanbanPageClient` to include responsive controls for mobile, improving task management experience. - Adjusted `DailyCheckboxItem` and `DailySection` for better touch targets and responsive design. - Cleaned up `TODO.md` to reflect changes in mobile interface considerations and task management features.
This commit is contained in:
@@ -74,7 +74,7 @@ export function DailyCheckboxItem({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 rounded border transition-colors group ${
|
||||
<div className={`flex items-center gap-3 px-3 py-2 sm:py-1.5 sm:gap-2 rounded border transition-colors group ${
|
||||
checkbox.type === 'meeting'
|
||||
? 'border-l-4 border-l-blue-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]'
|
||||
: 'border-l-4 border-l-green-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]'
|
||||
@@ -85,7 +85,7 @@ export function DailyCheckboxItem({
|
||||
checked={checkbox.isChecked}
|
||||
onChange={() => onToggle(checkbox.id)}
|
||||
disabled={saving}
|
||||
className="w-3.5 h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1"
|
||||
className="w-4 h-4 md:w-3.5 md:h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1"
|
||||
/>
|
||||
|
||||
{/* Contenu principal */}
|
||||
@@ -102,7 +102,7 @@ export function DailyCheckboxItem({
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
{/* Texte cliquable pour édition inline */}
|
||||
<span
|
||||
className={`flex-1 text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${
|
||||
className={`flex-1 text-sm sm:text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${
|
||||
checkbox.isChecked
|
||||
? 'line-through text-[var(--muted-foreground)]'
|
||||
: 'text-[var(--foreground)]'
|
||||
|
||||
@@ -87,7 +87,7 @@ export function DailySection({
|
||||
onDragEnd={handleDragEnd}
|
||||
id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||
>
|
||||
<Card className="p-0 flex flex-col h-[600px]">
|
||||
<Card className="p-0 flex flex-col h-[80vh] sm:h-[600px]">
|
||||
{/* Header */}
|
||||
<div className="p-4 pb-0">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PrioritySwimlanesBoard } from './PrioritySwimlanesBoard';
|
||||
import { Task, TaskStatus } from '@/lib/types';
|
||||
import { CreateTaskData } from '@/clients/tasks-client';
|
||||
import { KanbanFilters } from './KanbanFilters';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
|
||||
interface BoardRouterProps {
|
||||
tasks: Task[];
|
||||
@@ -26,8 +27,13 @@ export function BoardRouter({
|
||||
visibleStatuses,
|
||||
loading
|
||||
}: BoardRouterProps) {
|
||||
const isMobile = useIsMobile(768); // Tailwind md breakpoint
|
||||
|
||||
// Sur mobile, toujours utiliser le board standard pour une meilleure UX
|
||||
const shouldUseSwimlanes = kanbanFilters.swimlanesByTags && !isMobile;
|
||||
|
||||
// Logique de routage des boards selon les filtres
|
||||
if (kanbanFilters.swimlanesByTags) {
|
||||
if (shouldUseSwimlanes) {
|
||||
if (kanbanFilters.swimlanesMode === 'priority') {
|
||||
return (
|
||||
<PrioritySwimlanesBoard
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
|
||||
import { SORT_OPTIONS } from '@/lib/sort-config';
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
||||
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
|
||||
export interface KanbanFilters {
|
||||
search?: string;
|
||||
@@ -44,6 +45,7 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
||||
const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility;
|
||||
const [isSortExpanded, setIsSortExpanded] = useState(false);
|
||||
const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false);
|
||||
const isMobile = useIsMobile(768); // Tailwind md breakpoint
|
||||
const sortDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const swimlaneModeDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const sortButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -262,52 +264,54 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Menu swimlanes */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={filters.swimlanesByTags ? "primary" : "ghost"}
|
||||
onClick={handleSwimlanesToggle}
|
||||
className="flex items-center gap-2"
|
||||
title="Mode d'affichage"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{filters.swimlanesByTags ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
)}
|
||||
</svg>
|
||||
{!filters.swimlanesByTags
|
||||
? 'Normal'
|
||||
: filters.swimlanesMode === 'priority'
|
||||
? 'Par priorité'
|
||||
: 'Par tags'
|
||||
}
|
||||
</Button>
|
||||
|
||||
{/* Bouton pour changer le mode des swimlanes */}
|
||||
{filters.swimlanesByTags && (
|
||||
{/* Menu swimlanes - masqué sur mobile */}
|
||||
{!isMobile && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleSwimlaneModeToggle}
|
||||
className="flex items-center gap-1 px-2"
|
||||
variant={filters.swimlanesByTags ? "primary" : "ghost"}
|
||||
onClick={handleSwimlanesToggle}
|
||||
className="flex items-center gap-2"
|
||||
title="Mode d'affichage"
|
||||
>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${isSwimlaneModeExpanded ? 'rotate-180' : ''}`}
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
{filters.swimlanesByTags ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
)}
|
||||
</svg>
|
||||
{!filters.swimlanesByTags
|
||||
? 'Normal'
|
||||
: filters.swimlanesMode === 'priority'
|
||||
? 'Par priorité'
|
||||
: 'Par tags'
|
||||
}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bouton pour changer le mode des swimlanes */}
|
||||
{filters.swimlanesByTags && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleSwimlaneModeToggle}
|
||||
className="flex items-center gap-1 px-2"
|
||||
>
|
||||
<svg
|
||||
className={`w-3 h-3 transition-transform ${isSwimlaneModeExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Bouton de tri */}
|
||||
@@ -600,8 +604,8 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index */}
|
||||
{isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal(
|
||||
{/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index - masqué sur mobile */}
|
||||
{!isMobile && isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal(
|
||||
<div
|
||||
ref={swimlaneModeDropdownRef}
|
||||
className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] min-w-[140px]"
|
||||
|
||||
165
src/components/kanban/MobileControls.tsx
Normal file
165
src/components/kanban/MobileControls.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { JiraQuickFilter } from '@/components/kanban/JiraQuickFilter';
|
||||
import { FontSizeToggle } from '@/components/ui/FontSizeToggle';
|
||||
import { KanbanFilters } from '@/components/kanban/KanbanFilters';
|
||||
|
||||
interface MobileControlsProps {
|
||||
showFilters: boolean;
|
||||
showObjectives: boolean;
|
||||
compactView: boolean;
|
||||
activeFiltersCount: number;
|
||||
kanbanFilters: KanbanFilters;
|
||||
onToggleFilters: () => void;
|
||||
onToggleObjectives: () => void;
|
||||
onToggleCompactView: () => void;
|
||||
onFiltersChange: (filters: KanbanFilters) => void;
|
||||
onCreateTask: () => void;
|
||||
}
|
||||
|
||||
export function MobileControls({
|
||||
showFilters,
|
||||
showObjectives,
|
||||
compactView,
|
||||
activeFiltersCount,
|
||||
kanbanFilters,
|
||||
onToggleFilters,
|
||||
onToggleObjectives,
|
||||
onToggleCompactView,
|
||||
onFiltersChange,
|
||||
onCreateTask,
|
||||
}: MobileControlsProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--card)]/30 border-b border-[var(--border)]/30">
|
||||
<div className="px-4 py-2">
|
||||
{/* Barre principale mobile */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Bouton menu hamburger */}
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md bg-[var(--card)] border border-[var(--border)] text-[var(--muted-foreground)] hover:border-[var(--primary)]/50 transition-all"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<span className="text-sm font-mono">Options</span>
|
||||
{activeFiltersCount > 0 && (
|
||||
<span className="bg-[var(--primary)]/20 text-[var(--primary)] text-xs px-1.5 py-0.5 rounded-full font-mono">
|
||||
{activeFiltersCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Bouton d'ajout de tâche */}
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onCreateTask}
|
||||
className="flex items-center gap-2"
|
||||
size="sm"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<span className="hidden xs:inline">Nouvelle</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Menu déroulant */}
|
||||
{isMenuOpen && (
|
||||
<div className="mt-3 p-3 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg">
|
||||
{/* Section Affichage */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
||||
Affichage
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onToggleFilters();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
|
||||
showFilters
|
||||
? 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30'
|
||||
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
|
||||
</svg>
|
||||
Filtres
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
onToggleObjectives();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
|
||||
showObjectives
|
||||
? 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30'
|
||||
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
||||
</svg>
|
||||
Objectifs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Paramètres */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
||||
Paramètres
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onToggleCompactView();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm font-mono transition-all ${
|
||||
compactView
|
||||
? 'bg-[var(--secondary)]/20 text-[var(--secondary)] border border-[var(--secondary)]/30'
|
||||
: 'bg-[var(--muted)]/30 text-[var(--muted-foreground)] border border-[var(--border)]'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{compactView ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
)}
|
||||
</svg>
|
||||
Vue {compactView ? 'détaillée' : 'compacte'}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-[var(--muted)]/30 border border-[var(--border)] rounded-md">
|
||||
<span className="text-sm font-mono text-[var(--muted-foreground)]">Taille police</span>
|
||||
<FontSizeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Jira */}
|
||||
<div>
|
||||
<h3 className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide mb-2">
|
||||
Raccourcis Jira
|
||||
</h3>
|
||||
<JiraQuickFilter
|
||||
filters={kanbanFilters}
|
||||
onFiltersChange={onFiltersChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user