feat(todos): refonte formulaire + chips domaines + non-zoomable

- TodoForm : domaines en chips multi-select colorés, priorité en 3 boutons
  colorés (haute/moyenne/basse), date initialisée à aujourd'hui, description
  et URL toujours visibles, boutons photo et GPS
- TodosPage : suppression filtres domaine/priorité, tags colorés par domaine
  dans les lignes, userSelect:none, groupage multi-domaines
- todos.ts : ajout domains[], photo_path, gps_lat/lng dans les interfaces TS
- index.html : viewport maximum-scale=1.0, user-scalable=no

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 16:10:47 +02:00
co-authored by Claude Sonnet 4.6
parent e9dfb6e293
commit 925e077afe
4 changed files with 313 additions and 105 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="fr" data-theme="dark"> <html lang="fr" data-theme="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#fe8019" /> <meta name="theme-color" content="#fe8019" />
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
+12 -1
View File
@@ -4,13 +4,16 @@ export interface Todo {
title: string title: string
body: string | null body: string | null
url: string | null url: string | null
domain: string | null domains: string[]
category: string | null category: string | null
tags: string[] tags: string[]
status: 'pending' | 'done' | 'cancelled' status: 'pending' | 'done' | 'cancelled'
priority: 'low' | 'medium' | 'high' priority: 'low' | 'medium' | 'high'
due_date: string | null due_date: string | null
postponed_count: number postponed_count: number
photo_path: string | null
gps_lat: number | null
gps_lng: number | null
created_at: string created_at: string
updated_at: string | null updated_at: string | null
owner_id: string | null owner_id: string | null
@@ -21,11 +24,15 @@ export interface TodoCreate {
body?: string body?: string
url?: string url?: string
domain?: string domain?: string
domains?: string[]
category?: string category?: string
tags?: string[] tags?: string[]
status?: 'pending' | 'done' | 'cancelled' status?: 'pending' | 'done' | 'cancelled'
priority?: 'low' | 'medium' | 'high' priority?: 'low' | 'medium' | 'high'
due_date?: string due_date?: string
photo_path?: string
gps_lat?: number
gps_lng?: number
} }
export interface TodoUpdate { export interface TodoUpdate {
@@ -33,11 +40,15 @@ export interface TodoUpdate {
body?: string body?: string
url?: string url?: string
domain?: string domain?: string
domains?: string[]
category?: string category?: string
tags?: string[] tags?: string[]
status?: 'pending' | 'done' | 'cancelled' status?: 'pending' | 'done' | 'cancelled'
priority?: 'low' | 'medium' | 'high' priority?: 'low' | 'medium' | 'high'
due_date?: string due_date?: string
photo_path?: string
gps_lat?: number
gps_lng?: number
} }
export interface TodoFilters { export interface TodoFilters {
+202 -31
View File
@@ -1,4 +1,4 @@
import { useState } from 'react' import { useState, useRef } from 'react'
import type { TodoCreate } from '../../api/todos' import type { TodoCreate } from '../../api/todos'
const DOMAINS = [ const DOMAINS = [
@@ -6,10 +6,27 @@ const DOMAINS = [
'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux', 'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux',
] ]
export const DOMAIN_COLORS: Record<string, string> = {
informatique: 'var(--info)',
diy: 'var(--warn)',
electronique: 'var(--accent)',
domotique: 'var(--ok)',
bricolage: 'var(--warn)',
jardin: 'var(--ok)',
cuisine: 'var(--err)',
voyage: 'var(--info)',
animaux: 'var(--ok)',
}
const PRIORITY_CONFIG = [
{ value: 'high' as const, label: 'Haute', color: 'var(--err)', textColor: '#fff' },
{ value: 'medium' as const, label: 'Moyenne', color: 'var(--warn)', textColor: '#1d2021' },
{ value: 'low' as const, label: 'Basse', color: 'var(--bg-5)', textColor: 'var(--ink-2)' },
]
interface TodoFormProps { interface TodoFormProps {
onSubmit: (data: TodoCreate) => Promise<void> onSubmit: (data: TodoCreate) => Promise<void>
onCancel: () => void onCancel: () => void
extended?: boolean
initialValues?: Partial<TodoCreate> initialValues?: Partial<TodoCreate>
submitLabel?: string submitLabel?: string
} }
@@ -26,17 +43,67 @@ const inputStyle: React.CSSProperties = {
boxSizing: 'border-box', boxSizing: 'border-box',
} }
export default function TodoForm({ onSubmit, onCancel, extended = false, initialValues, submitLabel = 'Créer' }: TodoFormProps) { const labelStyle: React.CSSProperties = {
color: 'var(--ink-3)',
fontSize: 11,
fontFamily: 'var(--font-ui)',
marginBottom: 6,
textTransform: 'uppercase',
letterSpacing: 0.5,
}
export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabel = 'Créer' }: TodoFormProps) {
const today = new Date().toISOString().slice(0, 10)
const [title, setTitle] = useState(initialValues?.title ?? '') const [title, setTitle] = useState(initialValues?.title ?? '')
const [domain, setDomain] = useState(initialValues?.domain ?? '') const [selectedDomains, setSelectedDomains] = useState<string[]>(initialValues?.domains ?? [])
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>(initialValues?.priority ?? 'medium') const [priority, setPriority] = useState<'low' | 'medium' | 'high'>(initialValues?.priority ?? 'medium')
const [dueDate, setDueDate] = useState( const [dueDate, setDueDate] = useState(
initialValues?.due_date ? initialValues.due_date.slice(0, 10) : '' initialValues?.due_date ? initialValues.due_date.slice(0, 10) : today
) )
const [body, setBody] = useState(initialValues?.body ?? '') const [body, setBody] = useState(initialValues?.body ?? '')
const [url, setUrl] = useState(initialValues?.url ?? '') const [url, setUrl] = useState(initialValues?.url ?? '')
const [tags, setTags] = useState((initialValues?.tags ?? []).join(', ')) const [tags, setTags] = useState((initialValues?.tags ?? []).join(', '))
const [gpsLat, setGpsLat] = useState<number | undefined>(initialValues?.gps_lat ?? undefined)
const [gpsLng, setGpsLng] = useState<number | undefined>(initialValues?.gps_lng ?? undefined)
const [photoPath, setPhotoPath] = useState<string | undefined>(initialValues?.photo_path ?? undefined)
const [photoLoading, setPhotoLoading] = useState(false)
const [gpsLoading, setGpsLoading] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const fileRef = useRef<HTMLInputElement>(null)
function toggleDomain(d: string) {
setSelectedDomains(prev => prev.includes(d) ? prev.filter(x => x !== d) : [...prev, d])
}
async function handlePhotoCapture(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
setPhotoLoading(true)
try {
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/media/upload', { method: 'POST', body: formData })
if (res.ok) {
const data = await res.json() as { file_path: string }
setPhotoPath(data.file_path)
}
} finally {
setPhotoLoading(false)
}
}
function handleGps() {
if (!navigator.geolocation) return
setGpsLoading(true)
navigator.geolocation.getCurrentPosition(
pos => {
setGpsLat(pos.coords.latitude)
setGpsLng(pos.coords.longitude)
setGpsLoading(false)
},
() => setGpsLoading(false)
)
}
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault() e.preventDefault()
@@ -45,12 +112,15 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial
try { try {
await onSubmit({ await onSubmit({
title: title.trim(), title: title.trim(),
domain: domain || undefined, domains: selectedDomains,
priority, priority,
due_date: dueDate ? new Date(dueDate).toISOString() : undefined, due_date: dueDate ? new Date(dueDate).toISOString() : undefined,
body: body.trim() || undefined, body: body.trim() || undefined,
url: url.trim() || undefined, url: url.trim() || undefined,
tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [], tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [],
photo_path: photoPath,
gps_lat: gpsLat,
gps_lng: gpsLng,
}) })
} finally { } finally {
setLoading(false) setLoading(false)
@@ -58,7 +128,7 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial
} }
return ( return (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<input <input
style={inputStyle} style={inputStyle}
placeholder="Titre de la tâche *" placeholder="Titre de la tâche *"
@@ -68,49 +138,154 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial
required required
/> />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}> {/* Domaines — chips multi-select */}
<select style={inputStyle} value={domain} onChange={e => setDomain(e.target.value)}> <div>
<option value="">Domaine</option> <div style={labelStyle}>Domaines</div>
{DOMAINS.map(d => <option key={d} value={d}>{d}</option>)} <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
</select> {DOMAINS.map(d => {
<select style={inputStyle} value={priority} onChange={e => setPriority(e.target.value as 'low' | 'medium' | 'high')}> const selected = selectedDomains.includes(d)
<option value="low">Priorité basse</option> const color = DOMAIN_COLORS[d]
<option value="medium">Priorité moyenne</option> return (
<option value="high">Priorité haute</option> <button
</select> key={d}
type="button"
onClick={() => toggleDomain(d)}
style={{
padding: '5px 12px',
borderRadius: 999,
border: selected ? 'none' : '1px solid var(--bg-5)',
background: selected ? color : 'var(--bg-4)',
color: selected ? '#1d2021' : 'var(--ink-3)',
fontFamily: 'var(--font-ui)',
fontSize: 12,
cursor: 'pointer',
fontWeight: selected ? 600 : 400,
userSelect: 'none',
}}
>{d}</button>
)
})}
</div>
</div> </div>
{/* Priorité — boutons radio colorés */}
<div>
<div style={labelStyle}>Priorité</div>
<div style={{ display: 'flex', gap: 8 }}>
{PRIORITY_CONFIG.map(p => {
const selected = priority === p.value
return (
<button
key={p.value}
type="button"
onClick={() => setPriority(p.value)}
style={{
flex: 1,
padding: '8px 0',
borderRadius: 8,
border: selected ? 'none' : '1px solid var(--bg-5)',
background: selected ? p.color : 'var(--bg-4)',
color: selected ? p.textColor : 'var(--ink-3)',
fontFamily: 'var(--font-ui)',
fontSize: 13,
fontWeight: selected ? 600 : 400,
cursor: 'pointer',
minHeight: 40,
userSelect: 'none',
}}
>{p.label}</button>
)
})}
</div>
</div>
{/* Date objectif */}
<div>
<div style={labelStyle}>Date objectif</div>
<input <input
style={inputStyle} style={inputStyle}
type="date" type="date"
value={dueDate} value={dueDate}
onChange={e => setDueDate(e.target.value)} onChange={e => setDueDate(e.target.value)}
/> />
</div>
{extended && ( {/* Description */}
<>
<textarea <textarea
style={{ ...inputStyle, minHeight: 80, resize: 'vertical' }} style={{ ...inputStyle, minHeight: 72, resize: 'vertical' }}
placeholder="Description" placeholder="Description (optionnel)"
value={body} value={body}
onChange={e => setBody(e.target.value)} onChange={e => setBody(e.target.value)}
/> />
{/* URL */}
<input <input
style={inputStyle} style={inputStyle}
placeholder="URL (lien externe optionnel)" placeholder="URL (lien externe)"
value={url} value={url}
onChange={e => setUrl(e.target.value)} onChange={e => setUrl(e.target.value)}
type="url"
/> />
{/* Tags */}
<input <input
style={inputStyle} style={inputStyle}
placeholder="Tags (séparés par virgule)" placeholder="Tags (séparés par virgule)"
value={tags} value={tags}
onChange={e => setTags(e.target.value)} onChange={e => setTags(e.target.value)}
/> />
</>
)}
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}> {/* Photo + GPS */}
<div style={{ display: 'flex', gap: 8 }}>
<input
ref={fileRef}
type="file"
accept="image/*"
capture="environment"
style={{ display: 'none' }}
onChange={handlePhotoCapture}
/>
<button
type="button"
onClick={() => fileRef.current?.click()}
style={{
flex: 1,
padding: '8px',
borderRadius: 8,
border: '1px solid var(--bg-5)',
background: photoPath ? 'var(--ok)' : 'var(--bg-4)',
color: photoPath ? '#1d2021' : 'var(--ink-3)',
fontFamily: 'var(--font-ui)',
fontSize: 13,
cursor: 'pointer',
minHeight: 40,
}}
>
{photoLoading ? '…' : photoPath ? 'Photo ajoutée' : 'Ajouter une photo'}
</button>
<button
type="button"
onClick={handleGps}
disabled={!navigator.geolocation}
style={{
flex: 1,
padding: '8px',
borderRadius: 8,
border: '1px solid var(--bg-5)',
background: gpsLat != null ? 'var(--info)' : 'var(--bg-4)',
color: gpsLat != null ? '#fff' : 'var(--ink-3)',
fontFamily: 'var(--font-ui)',
fontSize: 13,
cursor: navigator.geolocation ? 'pointer' : 'default',
minHeight: 40,
}}
>
{gpsLoading ? '…' : gpsLat != null ? `GPS : ${gpsLat.toFixed(4)}` : 'Position GPS'}
</button>
</div>
{/* Boutons */}
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}
@@ -124,9 +299,7 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial
fontFamily: 'var(--font-ui)', fontFamily: 'var(--font-ui)',
minHeight: 48, minHeight: 48,
}} }}
> >Annuler</button>
Annuler
</button>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
@@ -141,9 +314,7 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial
fontWeight: 600, fontWeight: 600,
minHeight: 48, minHeight: 48,
}} }}
> >{loading ? '…' : submitLabel}</button>
{loading ? '…' : submitLabel}
</button>
</div> </div>
</form> </form>
) )
+74 -48
View File
@@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'
import type { Todo, TodoCreate, TodoFilters } from '../api/todos' import type { Todo, TodoCreate, TodoFilters } from '../api/todos'
import { fetchTodos, createTodo, updateTodo, deleteTodo, postponeTodo } from '../api/todos' import { fetchTodos, createTodo, updateTodo, deleteTodo, postponeTodo } from '../api/todos'
import SwipeableRow from '../components/todos/SwipeableRow' import SwipeableRow from '../components/todos/SwipeableRow'
import TodoForm from '../components/todos/TodoForm' import TodoForm, { DOMAIN_COLORS } from '../components/todos/TodoForm'
import Modal from '../components/Modal' import Modal from '../components/Modal'
type EditingTodo = Todo | null type EditingTodo = Todo | null
@@ -12,9 +12,11 @@ const DOMAINS = [
'informatique', 'diy', 'electronique', 'domotique', 'informatique', 'diy', 'electronique', 'domotique',
'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux', 'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux',
] ]
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
pending: 'En cours', done: 'Terminé', cancelled: 'Annulé', pending: 'En cours', done: 'Terminé', cancelled: 'Annulé',
} }
const PRIORITY_COLORS: Record<string, string> = { const PRIORITY_COLORS: Record<string, string> = {
high: 'var(--err)', medium: 'var(--warn)', low: 'var(--ink-3)', high: 'var(--err)', medium: 'var(--warn)', low: 'var(--ink-3)',
} }
@@ -29,6 +31,8 @@ const selectStyle: React.CSSProperties = {
fontSize: 13, fontSize: 13,
} }
const noSelect: React.CSSProperties = { userSelect: 'none' }
export default function TodosPage() { export default function TodosPage() {
const [todos, setTodos] = useState<Todo[]>([]) const [todos, setTodos] = useState<Todo[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -98,20 +102,20 @@ export default function TodosPage() {
} }
} }
// Grouper par domaine pour la vue mobile // Grouper par domaine (un todo peut apparaître dans plusieurs groupes)
const grouped = DOMAINS.reduce<Record<string, Todo[]>>((acc, d) => { const grouped = DOMAINS.reduce<Record<string, Todo[]>>((acc, d) => {
const items = todos.filter(t => t.domain === d) const items = todos.filter(t => t.domains.includes(d))
if (items.length > 0) acc[d] = items if (items.length > 0) acc[d] = items
return acc return acc
}, {}) }, {})
const sansDomaine = todos.filter(t => !t.domain) const sansDomaine = todos.filter(t => t.domains.length === 0)
if (sansDomaine.length > 0) grouped['—'] = sansDomaine if (sansDomaine.length > 0) grouped['—'] = sansDomaine
return ( return (
<div className="p-4"> <div className="p-4">
{/* En-tête + filtres */} {/* En-tête + filtre statut uniquement */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
<h1 style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)', margin: 0, flex: 1, minWidth: 100 }}> <h1 style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)', margin: 0, flex: 1, minWidth: 100, ...noSelect }}>
Tâches Tâches
</h1> </h1>
<select <select
@@ -124,24 +128,6 @@ export default function TodosPage() {
<option value="cancelled">Annulé</option> <option value="cancelled">Annulé</option>
<option value="">Tous</option> <option value="">Tous</option>
</select> </select>
<select
style={selectStyle}
value={filters.domain ?? ''}
onChange={e => setFilters(f => ({ ...f, domain: e.target.value || undefined }))}
>
<option value="">Tous domaines</option>
{DOMAINS.map(d => <option key={d} value={d}>{d}</option>)}
</select>
<select
style={selectStyle}
value={filters.priority ?? ''}
onChange={e => setFilters(f => ({ ...f, priority: e.target.value || undefined }))}
>
<option value="">Toutes priorités</option>
<option value="high">Haute</option>
<option value="medium">Moyenne</option>
<option value="low">Basse</option>
</select>
</div> </div>
{error && ( {error && (
@@ -150,29 +136,31 @@ export default function TodosPage() {
</p> </p>
)} )}
{/* Formulaire de création */} {/* Modal création */}
{showForm && ( {showForm && (
<Modal title="Nouvelle tâche" onClose={() => setShowForm(false)}> <Modal title="Nouvelle tâche" onClose={() => setShowForm(false)}>
<TodoForm onSubmit={handleCreate} onCancel={() => setShowForm(false)} extended /> <TodoForm onSubmit={handleCreate} onCancel={() => setShowForm(false)} />
</Modal> </Modal>
)} )}
{/* Formulaire d'édition */} {/* Modal édition */}
{editingTodo && ( {editingTodo && (
<Modal title="Modifier la tâche" onClose={() => setEditingTodo(null)}> <Modal title="Modifier la tâche" onClose={() => setEditingTodo(null)}>
<TodoForm <TodoForm
onSubmit={data => handleUpdate(editingTodo.id, data)} onSubmit={data => handleUpdate(editingTodo.id, data)}
onCancel={() => setEditingTodo(null)} onCancel={() => setEditingTodo(null)}
extended
submitLabel="Enregistrer" submitLabel="Enregistrer"
initialValues={{ initialValues={{
title: editingTodo.title, title: editingTodo.title,
domain: editingTodo.domain ?? undefined, domains: editingTodo.domains,
priority: editingTodo.priority, priority: editingTodo.priority,
due_date: editingTodo.due_date ?? undefined, due_date: editingTodo.due_date ?? undefined,
body: editingTodo.body ?? undefined, body: editingTodo.body ?? undefined,
url: editingTodo.url ?? undefined, url: editingTodo.url ?? undefined,
tags: editingTodo.tags, tags: editingTodo.tags,
photo_path: editingTodo.photo_path ?? undefined,
gps_lat: editingTodo.gps_lat ?? undefined,
gps_lng: editingTodo.gps_lng ?? undefined,
}} }}
/> />
</Modal> </Modal>
@@ -186,11 +174,13 @@ export default function TodosPage() {
<div className="block lg:hidden"> <div className="block lg:hidden">
{!loading && Object.entries(grouped).map(([domain, items]) => ( {!loading && Object.entries(grouped).map(([domain, items]) => (
<div key={domain} style={{ marginBottom: 20 }}> <div key={domain} style={{ marginBottom: 20 }}>
{/* Entête de groupe avec badge compteur */} <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, ...noSelect }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ <span style={{
color: 'var(--accent)', fontFamily: 'var(--font-mono)', color: DOMAIN_COLORS[domain] ?? 'var(--accent)',
fontSize: 12, textTransform: 'uppercase', letterSpacing: 1, fontFamily: 'var(--font-mono)',
fontSize: 12,
textTransform: 'uppercase',
letterSpacing: 1,
}}> }}>
{domain} {domain}
</span> </span>
@@ -232,20 +222,37 @@ export default function TodosPage() {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 10, gap: 10,
...noSelect,
}}> }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[todo.priority], flexShrink: 0 }} /> <div style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[todo.priority], flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: 'var(--ink-1)', fontSize: 14, fontFamily: 'var(--font-ui)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <div style={{ color: 'var(--ink-1)', fontSize: 14, fontFamily: 'var(--font-ui)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{todo.title} {todo.title}
</div> </div>
<div style={{ display: 'flex', gap: 4, marginTop: 3, flexWrap: 'wrap', alignItems: 'center' }}>
{todo.domains.map(d => (
<span
key={d}
style={{
background: DOMAIN_COLORS[d] ?? 'var(--bg-5)',
color: '#1d2021',
fontSize: 10,
fontFamily: 'var(--font-ui)',
fontWeight: 600,
borderRadius: 999,
padding: '1px 7px',
}}
>{d}</span>
))}
{todo.due_date && ( {todo.due_date && (
<div style={{ color: 'var(--ink-3)', fontSize: 11, marginTop: 2 }}> <span style={{ color: 'var(--ink-3)', fontSize: 11 }}>
{new Date(todo.due_date).toLocaleDateString('fr-FR')} {new Date(todo.due_date).toLocaleDateString('fr-FR')}
{todo.postponed_count > 0 && ` · reporté ${todo.postponed_count}×`} {todo.postponed_count > 0 && ` · reporté ${todo.postponed_count}×`}
</div> </span>
)} )}
</div> </div>
</div> </div>
</div>
</SwipeableRow> </SwipeableRow>
))} ))}
</div> </div>
@@ -253,7 +260,7 @@ export default function TodosPage() {
))} ))}
{!loading && todos.length === 0 && ( {!loading && todos.length === 0 && (
<p style={{ color: 'var(--ink-3)', textAlign: 'center', marginTop: 40 }}>Aucune tâche</p> <p style={{ color: 'var(--ink-3)', textAlign: 'center', marginTop: 40, ...noSelect }}>Aucune tâche</p>
)} )}
</div> </div>
@@ -261,9 +268,9 @@ export default function TodosPage() {
<div className="hidden lg:block"> <div className="hidden lg:block">
{!loading && ( {!loading && (
<> <>
{/* Filtre période (laptop uniquement) */} {/* Filtre période */}
<div style={{ display: 'flex', gap: 8, marginBottom: 12, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 8, marginBottom: 12, alignItems: 'center' }}>
<span style={{ color: 'var(--ink-3)', fontSize: 12, fontFamily: 'var(--font-ui)' }}>Période :</span> <span style={{ color: 'var(--ink-3)', fontSize: 12, fontFamily: 'var(--font-ui)', ...noSelect }}>Période :</span>
<input <input
type="date" type="date"
style={selectStyle} style={selectStyle}
@@ -282,8 +289,8 @@ export default function TodosPage() {
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-ui)', fontSize: 13 }}> <table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-ui)', fontSize: 13 }}>
<thead> <thead>
<tr style={{ borderBottom: '1px solid var(--bg-4)' }}> <tr style={{ borderBottom: '1px solid var(--bg-4)' }}>
{['Titre', 'Domaine', 'Priorité', 'Statut', 'Date objectif', 'Reports', 'Actions'].map(h => ( {['Titre', 'Domaines', 'Priorité', 'Statut', 'Date objectif', 'Reports', 'Actions'].map(h => (
<th key={h} style={{ padding: '10px 14px', textAlign: 'left', color: 'var(--ink-3)', fontWeight: 500 }}>{h}</th> <th key={h} style={{ padding: '10px 14px', textAlign: 'left', color: 'var(--ink-3)', fontWeight: 500, ...noSelect }}>{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
@@ -297,7 +304,7 @@ export default function TodosPage() {
}} }}
> >
<td <td
style={{ padding: '10px 14px', color: 'var(--ink-1)', maxWidth: 280, cursor: 'pointer' }} style={{ padding: '10px 14px', color: 'var(--ink-1)', maxWidth: 280, cursor: 'pointer', ...noSelect }}
onDoubleClick={() => setEditingTodo(todo)} onDoubleClick={() => setEditingTodo(todo)}
title="Double-clic pour modifier" title="Double-clic pour modifier"
> >
@@ -314,19 +321,38 @@ export default function TodosPage() {
</div> </div>
)} )}
</td> </td>
<td style={{ padding: '10px 14px', color: 'var(--ink-2)' }}>{todo.domain ?? '—'}</td> <td style={{ padding: '10px 14px', ...noSelect }}>
<td style={{ padding: '10px 14px' }}> <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{todo.domains.length === 0 ? (
<span style={{ color: 'var(--ink-4)' }}>—</span>
) : todo.domains.map(d => (
<span
key={d}
style={{
background: DOMAIN_COLORS[d] ?? 'var(--bg-5)',
color: '#1d2021',
fontSize: 11,
fontFamily: 'var(--font-ui)',
fontWeight: 600,
borderRadius: 999,
padding: '2px 8px',
}}
>{d}</span>
))}
</div>
</td>
<td style={{ padding: '10px 14px', ...noSelect }}>
<span style={{ color: PRIORITY_COLORS[todo.priority], fontFamily: 'var(--font-mono)', fontSize: 12 }}> <span style={{ color: PRIORITY_COLORS[todo.priority], fontFamily: 'var(--font-mono)', fontSize: 12 }}>
{todo.priority} {todo.priority}
</span> </span>
</td> </td>
<td style={{ padding: '10px 14px', color: 'var(--ink-2)' }}> <td style={{ padding: '10px 14px', color: 'var(--ink-2)', ...noSelect }}>
{STATUS_LABELS[todo.status] ?? todo.status} {STATUS_LABELS[todo.status] ?? todo.status}
</td> </td>
<td style={{ padding: '10px 14px', color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', fontSize: 12 }}> <td style={{ padding: '10px 14px', color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', fontSize: 12, ...noSelect }}>
{todo.due_date ? new Date(todo.due_date).toLocaleDateString('fr-FR') : '—'} {todo.due_date ? new Date(todo.due_date).toLocaleDateString('fr-FR') : '—'}
</td> </td>
<td style={{ padding: '10px 14px', color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}> <td style={{ padding: '10px 14px', color: 'var(--ink-3)', fontFamily: 'var(--font-mono)', ...noSelect }}>
{todo.postponed_count > 0 ? `${todo.postponed_count}×` : '—'} {todo.postponed_count > 0 ? `${todo.postponed_count}×` : '—'}
</td> </td>
<td style={{ padding: '10px 14px' }}> <td style={{ padding: '10px 14px' }}>
@@ -362,14 +388,14 @@ export default function TodosPage() {
</tbody> </tbody>
</table> </table>
{todos.length === 0 && ( {todos.length === 0 && (
<p style={{ padding: 24, color: 'var(--ink-3)', textAlign: 'center' }}>Aucune tâche</p> <p style={{ padding: 24, color: 'var(--ink-3)', textAlign: 'center', ...noSelect }}>Aucune tâche</p>
)} )}
</div> </div>
</> </>
)} )}
</div> </div>
{/* FAB mobile (au-dessus de la barre de navigation) */} {/* FAB mobile */}
<button <button
className="lg:hidden" className="lg:hidden"
onClick={() => setShowForm(true)} onClick={() => setShowForm(true)}