feat(todos): assignés en pastilles nominatives (moi en foncé, contraste AA — l'accent-2 plein du design ne tenait que 3.1:1) et flux « effectuée » en bottom sheet par-dessus la liste (fetch + injection, Échap, retour de focus ; la page reste le fallback sans JS)Unverified
Claude-Session: https://claude.ai/code/session_01FPKHKGfqvnvXKY9tNhYmkh
c538051 parent: 225c26f modified
assets/style.css +15 -4 | @@ -392,9 +392,12 @@ a.team-pill:hover { filter: brightness(.96); } | ||
| 392 | 392 | légèrement sur la proportion visuelle pour les petits segments. */ |
| 393 | 393 | .col-bar a { display: block; min-height: 24px; width: 100%; } |
| 394 | 394 | |
| 395 | -/* — bottom sheet (zéro JS : :target) — */ | |
| 395 | +/* — bottom sheet (zéro JS : :target ; .open = variante injectée en JS) — */ | |
| 396 | 396 | .sheet-wrap { position: absolute; inset: 0; display: none; z-index: 10; } |
| 397 | -.sheet-wrap:target { display: flex; align-items: flex-end; justify-content: center; } | |
| 397 | +.sheet-wrap:target, .sheet-wrap.open { display: flex; align-items: flex-end; justify-content: center; } | |
| 398 | +/* Contenu long (flux « effectuée ») : la sheet scrolle elle-même. */ | |
| 399 | +.sheet-scroll { max-height: 86%; overflow-y: auto; -webkit-overflow-scrolling: touch; } | |
| 400 | +.sheet-scroll .done-title { margin-bottom: var(--space-4); } | |
| 398 | 401 | .sheet-backdrop { |
| 399 | 402 | position: absolute; inset: 0; |
| 400 | 403 | background: color-mix(in srgb, var(--color-neutral-900) 45%, transparent); |
| @@ -478,8 +481,16 @@ a.team-pill:hover { filter: brightness(.96); } | ||
| 478 | 481 | .todo-name { font-size: 16px; } |
| 479 | 482 | .todo-meta { font-size: 13px; opacity: .7; } |
| 480 | 483 | .todo-meta.late { opacity: 1; color: var(--color-accent-700); font-weight: 600; } |
| 481 | -.todo-for { font-size: 13px; margin-top: 2px; opacity: .7; } | |
| 482 | -.todo-for.forme { opacity: 1; color: var(--color-accent-2-900); font-weight: 600; } | |
| 484 | +/* Pastilles nominatives des assignés — « moi » en foncé. Couleurs ajustées | |
| 485 | + vs le design (accent-2 plein ne fait que 3.1:1 avec la crème à 12 px). */ | |
| 486 | +.asg-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } | |
| 487 | +.asg-chip { | |
| 488 | + height: 26px; border-radius: 999px; flex: none; padding: 0 11px; | |
| 489 | + display: inline-flex; align-items: center; justify-content: center; | |
| 490 | + font-size: 12px; font-weight: 600; | |
| 491 | + background: var(--color-accent-2-200); color: var(--color-accent-2-900); | |
| 492 | +} | |
| 493 | +.asg-chip.me { background: var(--color-accent-2-900); color: var(--color-bg); } | |
| 483 | 494 | .todo-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); } |
| 484 | 495 | .todo-actions form { display: contents; } |
| 485 | 496 | .todo-actions .btn { min-height: 44px; flex: 1; } |
| @@ -392,9 +392,12 @@ a.team-pill:hover { filter: brightness(.96); } | |||
| 392 | légèrement sur la proportion visuelle pour les petits segments. */ | 392 | légèrement sur la proportion visuelle pour les petits segments. */ |
| 393 | .col-bar a { display: block; min-height: 24px; width: 100%; } | 393 | .col-bar a { display: block; min-height: 24px; width: 100%; } |
| 394 | 394 | ||
| 395 | -/* — bottom sheet (zéro JS : :target) — */ | 395 | +/* — bottom sheet (zéro JS : :target ; .open = variante injectée en JS) — */ |
| 396 | .sheet-wrap { position: absolute; inset: 0; display: none; z-index: 10; } | 396 | .sheet-wrap { position: absolute; inset: 0; display: none; z-index: 10; } |
| 397 | -.sheet-wrap:target { display: flex; align-items: flex-end; justify-content: center; } | 397 | +.sheet-wrap:target, .sheet-wrap.open { display: flex; align-items: flex-end; justify-content: center; } |
| 398 | +/* Contenu long (flux « effectuée ») : la sheet scrolle elle-même. */ | ||
| 399 | +.sheet-scroll { max-height: 86%; overflow-y: auto; -webkit-overflow-scrolling: touch; } | ||
| 400 | +.sheet-scroll .done-title { margin-bottom: var(--space-4); } | ||
| 398 | .sheet-backdrop { | 401 | .sheet-backdrop { |
| 399 | position: absolute; inset: 0; | 402 | position: absolute; inset: 0; |
| 400 | background: color-mix(in srgb, var(--color-neutral-900) 45%, transparent); | 403 | background: color-mix(in srgb, var(--color-neutral-900) 45%, transparent); |
| @@ -478,8 +481,16 @@ a.team-pill:hover { filter: brightness(.96); } | |||
| 478 | .todo-name { font-size: 16px; } | 481 | .todo-name { font-size: 16px; } |
| 479 | .todo-meta { font-size: 13px; opacity: .7; } | 482 | .todo-meta { font-size: 13px; opacity: .7; } |
| 480 | .todo-meta.late { opacity: 1; color: var(--color-accent-700); font-weight: 600; } | 483 | .todo-meta.late { opacity: 1; color: var(--color-accent-700); font-weight: 600; } |
| 481 | -.todo-for { font-size: 13px; margin-top: 2px; opacity: .7; } | 484 | +/* Pastilles nominatives des assignés — « moi » en foncé. Couleurs ajustées |
| 482 | -.todo-for.forme { opacity: 1; color: var(--color-accent-2-900); font-weight: 600; } | 485 | + vs le design (accent-2 plein ne fait que 3.1:1 avec la crème à 12 px). */ |
| 486 | +.asg-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } | ||
| 487 | +.asg-chip { | ||
| 488 | + height: 26px; border-radius: 999px; flex: none; padding: 0 11px; | ||
| 489 | + display: inline-flex; align-items: center; justify-content: center; | ||
| 490 | + font-size: 12px; font-weight: 600; | ||
| 491 | + background: var(--color-accent-2-200); color: var(--color-accent-2-900); | ||
| 492 | +} | ||
| 493 | +.asg-chip.me { background: var(--color-accent-2-900); color: var(--color-bg); } | ||
| 483 | .todo-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); } | 494 | .todo-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); } |
| 484 | .todo-actions form { display: contents; } | 495 | .todo-actions form { display: contents; } |
| 485 | .todo-actions .btn { min-height: 44px; flex: 1; } | 496 | .todo-actions .btn { min-height: 44px; flex: 1; } |
modified
src/routes/todos.rs +20 -19 | @@ -23,6 +23,13 @@ use crate::team::ActiveTeam; | ||
| 23 | 23 | |
| 24 | 24 | /// ─── Écran liste ─── |
| 25 | 25 | |
| 26 | +/// Pastille nominative d'assigné (variante « pastilles noms » du design). | |
| 27 | +pub struct AsgChip { | |
| 28 | + pub name: String, | |
| 29 | + /// Moi → pastille foncée. | |
| 30 | + pub me: bool, | |
| 31 | +} | |
| 32 | + | |
| 26 | 33 | pub struct TodoCard { |
| 27 | 34 | pub id: Uuid, |
| 28 | 35 | pub name: String, |
| @@ -35,9 +42,9 @@ pub struct TodoCard { | ||
| 35 | 42 | pub mine: bool, |
| 36 | 43 | /// Bloc en cours : prise par quelqu'un d'autre (carte estompée). |
| 37 | 44 | pub other: bool, |
| 38 | - /// « pour blandine et gisèle » (les autres assignés, jamais moi) — vide si rien à dire. | |
| 39 | - pub assignee_line: String, | |
| 40 | - /// Je suis assigné → liseré + ligne d'assignés en évidence. | |
| 45 | + /// Tous les assignés, moi compris, en pastilles nominatives. | |
| 46 | + pub assignees: Vec<AsgChip>, | |
| 47 | + /// Je suis assigné → liseré sur la carte. | |
| 41 | 48 | pub for_me: bool, |
| 42 | 49 | } |
| 43 | 50 | |
| @@ -92,16 +99,6 @@ fn list_href(doing_on: bool, open_on: bool) -> String { | ||
| 92 | 99 | } |
| 93 | 100 | } |
| 94 | 101 | |
| 95 | -/// « pour blandine », « pour blandine et gisèle » — moi exclu (le liseré dit | |
| 96 | -/// déjà que c'est pour moi). | |
| 97 | -fn assignee_line(names: &[&str]) -> String { | |
| 98 | - match names { | |
| 99 | - [] => String::new(), | |
| 100 | - [a] => format!("pour {a}"), | |
| 101 | - [rest @ .., last] => format!("pour {} et {last}", rest.join(", ")), | |
| 102 | - } | |
| 103 | -} | |
| 104 | - | |
| 105 | 102 | pub async fn todos_page( |
| 106 | 103 | State(state): State<AppState>, |
| 107 | 104 | ctx: ActiveTeam, |
| @@ -123,11 +120,6 @@ pub async fn todos_page( | ||
| 123 | 120 | let card = |t: &TodoRow| { |
| 124 | 121 | let assignees = by_todo.get(&t.id).map(Vec::as_slice).unwrap_or(&[]); |
| 125 | 122 | let for_me = assignees.iter().any(|(id, _)| *id == me); |
| 126 | - let others: Vec<&str> = assignees | |
| 127 | - .iter() | |
| 128 | - .filter(|(id, _)| *id != me) | |
| 129 | - .map(|(_, n)| n.as_str()) | |
| 130 | - .collect(); | |
| 131 | 123 | let mine = t.taker_id == Some(me); |
| 132 | 124 | let late = t.state_id == STATE_TODO && t.activation_date < today; |
| 133 | 125 | let meta = if t.state_id == STATE_IN_PROGRESS { |
| @@ -151,7 +143,13 @@ pub async fn todos_page( | ||
| 151 | 143 | late, |
| 152 | 144 | mine, |
| 153 | 145 | other: t.state_id == STATE_IN_PROGRESS && !mine, |
| 154 | - assignee_line: assignee_line(&others), | |
| 146 | + assignees: assignees | |
| 147 | + .iter() | |
| 148 | + .map(|(id, n)| AsgChip { | |
| 149 | + name: n.clone(), | |
| 150 | + me: *id == me, | |
| 151 | + }) | |
| 152 | + .collect(), | |
| 155 | 153 | for_me, |
| 156 | 154 | } |
| 157 | 155 | }; |
| @@ -450,6 +448,8 @@ pub struct TodoDonePage { | ||
| 450 | 448 | pub plus_href: String, |
| 451 | 449 | pub presets: Vec<DonePreset>, |
| 452 | 450 | pub date: NaiveDate, |
| 451 | + /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input. | |
| 452 | + pub today: NaiveDate, | |
| 453 | 453 | pub day_prev: String, |
| 454 | 454 | /// None = aujourd'hui (flèche désactivée). |
| 455 | 455 | pub day_next: Option<String>, |
| @@ -530,6 +530,7 @@ pub async fn todo_done_page( | ||
| 530 | 530 | }) |
| 531 | 531 | .collect(), |
| 532 | 532 | date, |
| 533 | + today, | |
| 533 | 534 | day_prev: done_href(id, minutes, date - Duration::days(1), next), |
| 534 | 535 | day_next: (date < today).then(|| done_href(id, minutes, date + Duration::days(1), next)), |
| 535 | 536 | next, |
| @@ -23,6 +23,13 @@ use crate::team::ActiveTeam; | |||
| 23 | 23 | ||
| 24 | /// ─── Écran liste ─── | 24 | /// ─── Écran liste ─── |
| 25 | 25 | ||
| 26 | +/// Pastille nominative d'assigné (variante « pastilles noms » du design). | ||
| 27 | +pub struct AsgChip { | ||
| 28 | + pub name: String, | ||
| 29 | + /// Moi → pastille foncée. | ||
| 30 | + pub me: bool, | ||
| 31 | +} | ||
| 32 | + | ||
| 26 | pub struct TodoCard { | 33 | pub struct TodoCard { |
| 27 | pub id: Uuid, | 34 | pub id: Uuid, |
| 28 | pub name: String, | 35 | pub name: String, |
| @@ -35,9 +42,9 @@ pub struct TodoCard { | |||
| 35 | pub mine: bool, | 42 | pub mine: bool, |
| 36 | /// Bloc en cours : prise par quelqu'un d'autre (carte estompée). | 43 | /// Bloc en cours : prise par quelqu'un d'autre (carte estompée). |
| 37 | pub other: bool, | 44 | pub other: bool, |
| 38 | - /// « pour blandine et gisèle » (les autres assignés, jamais moi) — vide si rien à dire. | 45 | + /// Tous les assignés, moi compris, en pastilles nominatives. |
| 39 | - pub assignee_line: String, | 46 | + pub assignees: Vec<AsgChip>, |
| 40 | - /// Je suis assigné → liseré + ligne d'assignés en évidence. | 47 | + /// Je suis assigné → liseré sur la carte. |
| 41 | pub for_me: bool, | 48 | pub for_me: bool, |
| 42 | } | 49 | } |
| 43 | 50 | ||
| @@ -92,16 +99,6 @@ fn list_href(doing_on: bool, open_on: bool) -> String { | |||
| 92 | } | 99 | } |
| 93 | } | 100 | } |
| 94 | 101 | ||
| 95 | -/// « pour blandine », « pour blandine et gisèle » — moi exclu (le liseré dit | ||
| 96 | -/// déjà que c'est pour moi). | ||
| 97 | -fn assignee_line(names: &[&str]) -> String { | ||
| 98 | - match names { | ||
| 99 | - [] => String::new(), | ||
| 100 | - [a] => format!("pour {a}"), | ||
| 101 | - [rest @ .., last] => format!("pour {} et {last}", rest.join(", ")), | ||
| 102 | - } | ||
| 103 | -} | ||
| 104 | - | ||
| 105 | pub async fn todos_page( | 102 | pub async fn todos_page( |
| 106 | State(state): State<AppState>, | 103 | State(state): State<AppState>, |
| 107 | ctx: ActiveTeam, | 104 | ctx: ActiveTeam, |
| @@ -123,11 +120,6 @@ pub async fn todos_page( | |||
| 123 | let card = |t: &TodoRow| { | 120 | let card = |t: &TodoRow| { |
| 124 | let assignees = by_todo.get(&t.id).map(Vec::as_slice).unwrap_or(&[]); | 121 | let assignees = by_todo.get(&t.id).map(Vec::as_slice).unwrap_or(&[]); |
| 125 | let for_me = assignees.iter().any(|(id, _)| *id == me); | 122 | let for_me = assignees.iter().any(|(id, _)| *id == me); |
| 126 | - let others: Vec<&str> = assignees | ||
| 127 | - .iter() | ||
| 128 | - .filter(|(id, _)| *id != me) | ||
| 129 | - .map(|(_, n)| n.as_str()) | ||
| 130 | - .collect(); | ||
| 131 | let mine = t.taker_id == Some(me); | 123 | let mine = t.taker_id == Some(me); |
| 132 | let late = t.state_id == STATE_TODO && t.activation_date < today; | 124 | let late = t.state_id == STATE_TODO && t.activation_date < today; |
| 133 | let meta = if t.state_id == STATE_IN_PROGRESS { | 125 | let meta = if t.state_id == STATE_IN_PROGRESS { |
| @@ -151,7 +143,13 @@ pub async fn todos_page( | |||
| 151 | late, | 143 | late, |
| 152 | mine, | 144 | mine, |
| 153 | other: t.state_id == STATE_IN_PROGRESS && !mine, | 145 | other: t.state_id == STATE_IN_PROGRESS && !mine, |
| 154 | - assignee_line: assignee_line(&others), | 146 | + assignees: assignees |
| 147 | + .iter() | ||
| 148 | + .map(|(id, n)| AsgChip { | ||
| 149 | + name: n.clone(), | ||
| 150 | + me: *id == me, | ||
| 151 | + }) | ||
| 152 | + .collect(), | ||
| 155 | for_me, | 153 | for_me, |
| 156 | } | 154 | } |
| 157 | }; | 155 | }; |
| @@ -450,6 +448,8 @@ pub struct TodoDonePage { | |||
| 450 | pub plus_href: String, | 448 | pub plus_href: String, |
| 451 | pub presets: Vec<DonePreset>, | 449 | pub presets: Vec<DonePreset>, |
| 452 | pub date: NaiveDate, | 450 | pub date: NaiveDate, |
| 451 | + /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input. | ||
| 452 | + pub today: NaiveDate, | ||
| 453 | pub day_prev: String, | 453 | pub day_prev: String, |
| 454 | /// None = aujourd'hui (flèche désactivée). | 454 | /// None = aujourd'hui (flèche désactivée). |
| 455 | pub day_next: Option<String>, | 455 | pub day_next: Option<String>, |
| @@ -530,6 +530,7 @@ pub async fn todo_done_page( | |||
| 530 | }) | 530 | }) |
| 531 | .collect(), | 531 | .collect(), |
| 532 | date, | 532 | date, |
| 533 | + today, | ||
| 533 | day_prev: done_href(id, minutes, date - Duration::days(1), next), | 534 | day_prev: done_href(id, minutes, date - Duration::days(1), next), |
| 534 | day_next: (date < today).then(|| done_href(id, minutes, date + Duration::days(1), next)), | 535 | day_next: (date < today).then(|| done_href(id, minutes, date + Duration::days(1), next)), |
| 535 | next, | 536 | next, |
modified
templates/app.html +42 -0 | @@ -158,6 +158,48 @@ | ||
| 158 | 158 | if (e.key === 'Escape' && openSheet()) location.hash = 'fermer'; |
| 159 | 159 | }); |
| 160 | 160 | })(); |
| 161 | +// Amélioration progressive du formulaire « tâche effectuée » (page dédiée ou | |
| 162 | +// sheet injectée par l'écran Tâches) : steppers, presets et libellé sans | |
| 163 | +// recharger. Délégation : fonctionne aussi sur du contenu injecté. | |
| 164 | +(function () { | |
| 165 | + function refreshMinutes(root) { | |
| 166 | + var min = root.querySelector('#minutes'); | |
| 167 | + var v = Math.round(+min.value) || 0; | |
| 168 | + var h = Math.floor(v / 60), r = v % 60; | |
| 169 | + root.querySelector('#minutes-pretty').textContent = | |
| 170 | + !v ? '0 min' : !h ? r + ' min' : r ? h + ' h ' + (r < 10 ? '0' + r : r) : h + ' h'; | |
| 171 | + root.querySelectorAll('.preset[data-minutes]').forEach(function (p) { | |
| 172 | + if (+p.dataset.minutes === v) { p.setAttribute('aria-current', 'true'); } | |
| 173 | + else { p.removeAttribute('aria-current'); } | |
| 174 | + }); | |
| 175 | + } | |
| 176 | + document.addEventListener('click', function (e) { | |
| 177 | + if (!e.target.closest) return; | |
| 178 | + var root = e.target.closest('form[data-done]'); | |
| 179 | + if (!root) return; | |
| 180 | + var b = e.target.closest('.stepper-btn[data-step], .preset[data-minutes], a[data-date-step]'); | |
| 181 | + if (!b) return; | |
| 182 | + e.preventDefault(); | |
| 183 | + if (b.dataset.dateStep) { | |
| 184 | + var inp = root.querySelector('#' + b.dataset.dateFor); | |
| 185 | + var d = inp.valueAsDate || new Date(); | |
| 186 | + d.setDate(d.getDate() + (+b.dataset.dateStep)); | |
| 187 | + var v = d.toISOString().slice(0, 10); | |
| 188 | + if (inp.max && v > inp.max) v = inp.max; | |
| 189 | + inp.value = v; | |
| 190 | + return; | |
| 191 | + } | |
| 192 | + var min = root.querySelector('#minutes'); | |
| 193 | + if (b.dataset.step) { min.value = Math.max(5, (Math.round(+min.value) || 0) + (+b.dataset.step)); } | |
| 194 | + else { min.value = +b.dataset.minutes; } | |
| 195 | + refreshMinutes(root); | |
| 196 | + }); | |
| 197 | + document.addEventListener('input', function (e) { | |
| 198 | + if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-done]')) { | |
| 199 | + refreshMinutes(e.target.closest('form[data-done]')); | |
| 200 | + } | |
| 201 | + }); | |
| 202 | +})(); | |
| 161 | 203 | // Amélioration progressive : le temps écoulé rendu par le serveur reste |
| 162 | 204 | // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger. |
| 163 | 205 | (function () { |
| @@ -158,6 +158,48 @@ | |||
| 158 | if (e.key === 'Escape' && openSheet()) location.hash = 'fermer'; | 158 | if (e.key === 'Escape' && openSheet()) location.hash = 'fermer'; |
| 159 | }); | 159 | }); |
| 160 | })(); | 160 | })(); |
| 161 | +// Amélioration progressive du formulaire « tâche effectuée » (page dédiée ou | ||
| 162 | +// sheet injectée par l'écran Tâches) : steppers, presets et libellé sans | ||
| 163 | +// recharger. Délégation : fonctionne aussi sur du contenu injecté. | ||
| 164 | +(function () { | ||
| 165 | + function refreshMinutes(root) { | ||
| 166 | + var min = root.querySelector('#minutes'); | ||
| 167 | + var v = Math.round(+min.value) || 0; | ||
| 168 | + var h = Math.floor(v / 60), r = v % 60; | ||
| 169 | + root.querySelector('#minutes-pretty').textContent = | ||
| 170 | + !v ? '0 min' : !h ? r + ' min' : r ? h + ' h ' + (r < 10 ? '0' + r : r) : h + ' h'; | ||
| 171 | + root.querySelectorAll('.preset[data-minutes]').forEach(function (p) { | ||
| 172 | + if (+p.dataset.minutes === v) { p.setAttribute('aria-current', 'true'); } | ||
| 173 | + else { p.removeAttribute('aria-current'); } | ||
| 174 | + }); | ||
| 175 | + } | ||
| 176 | + document.addEventListener('click', function (e) { | ||
| 177 | + if (!e.target.closest) return; | ||
| 178 | + var root = e.target.closest('form[data-done]'); | ||
| 179 | + if (!root) return; | ||
| 180 | + var b = e.target.closest('.stepper-btn[data-step], .preset[data-minutes], a[data-date-step]'); | ||
| 181 | + if (!b) return; | ||
| 182 | + e.preventDefault(); | ||
| 183 | + if (b.dataset.dateStep) { | ||
| 184 | + var inp = root.querySelector('#' + b.dataset.dateFor); | ||
| 185 | + var d = inp.valueAsDate || new Date(); | ||
| 186 | + d.setDate(d.getDate() + (+b.dataset.dateStep)); | ||
| 187 | + var v = d.toISOString().slice(0, 10); | ||
| 188 | + if (inp.max && v > inp.max) v = inp.max; | ||
| 189 | + inp.value = v; | ||
| 190 | + return; | ||
| 191 | + } | ||
| 192 | + var min = root.querySelector('#minutes'); | ||
| 193 | + if (b.dataset.step) { min.value = Math.max(5, (Math.round(+min.value) || 0) + (+b.dataset.step)); } | ||
| 194 | + else { min.value = +b.dataset.minutes; } | ||
| 195 | + refreshMinutes(root); | ||
| 196 | + }); | ||
| 197 | + document.addEventListener('input', function (e) { | ||
| 198 | + if (e.target.id === 'minutes' && e.target.closest && e.target.closest('form[data-done]')) { | ||
| 199 | + refreshMinutes(e.target.closest('form[data-done]')); | ||
| 200 | + } | ||
| 201 | + }); | ||
| 202 | +})(); | ||
| 161 | // Amélioration progressive : le temps écoulé rendu par le serveur reste | 203 | // Amélioration progressive : le temps écoulé rendu par le serveur reste |
| 162 | // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger. | 204 | // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger. |
| 163 | (function () { | 205 | (function () { |
modified
templates/todo_done.html +7 -40 | @@ -10,7 +10,7 @@ | ||
| 10 | 10 | <span class="done-name">{{ name }}</span> |
| 11 | 11 | </div> |
| 12 | 12 | |
| 13 | -<form method="post" action="/taches/{{ id }}/terminer"> | |
| 13 | +<form method="post" action="/taches/{{ id }}/terminer" data-done> | |
| 14 | 14 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 15 | 15 | |
| 16 | 16 | <section class="done-sec"> |
| @@ -46,12 +46,12 @@ | ||
| 46 | 46 | </div> |
| 47 | 47 | <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">Date du temps</label> |
| 48 | 48 | <div class="date-row"> |
| 49 | - <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="Jour précédent"> | |
| 49 | + <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" data-date-step="-1" data-date-for="done-date" aria-label="Jour précédent"> | |
| 50 | 50 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> |
| 51 | 51 | </a> |
| 52 | - <input class="input" id="done-date" name="date" type="date" value="{{ date }}" required> | |
| 52 | + <input class="input" id="done-date" name="date" type="date" value="{{ date }}" max="{{ today }}" required> | |
| 53 | 53 | {% if let Some(next_href) = day_next %} |
| 54 | - <a class="btn btn-secondary btn-icon" href="{{ next_href }}" aria-label="Jour suivant"> | |
| 54 | + <a class="btn btn-secondary btn-icon" href="{{ next_href }}" data-date-step="1" data-date-for="done-date" aria-label="Jour suivant"> | |
| 55 | 55 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> |
| 56 | 56 | </a> |
| 57 | 57 | {% else %} |
| @@ -75,11 +75,11 @@ | ||
| 75 | 75 | <div class="done-body"> |
| 76 | 76 | <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p> |
| 77 | 77 | <div class="date-row"> |
| 78 | - <a class="btn btn-secondary btn-icon" href="{{ next_prev }}" aria-label="Jour précédent"> | |
| 78 | + <a class="btn btn-secondary btn-icon" href="{{ next_prev }}" data-date-step="-1" data-date-for="next-date" aria-label="Jour précédent"> | |
| 79 | 79 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> |
| 80 | 80 | </a> |
| 81 | - <input class="input" name="prochaine" type="date" value="{{ next }}" required aria-label="Prochaine activation"> | |
| 82 | - <a class="btn btn-secondary btn-icon" href="{{ next_next }}" aria-label="Jour suivant"> | |
| 81 | + <input class="input" id="next-date" name="prochaine" type="date" value="{{ next }}" required aria-label="Prochaine activation"> | |
| 82 | + <a class="btn btn-secondary btn-icon" href="{{ next_next }}" data-date-step="1" data-date-for="next-date" aria-label="Jour suivant"> | |
| 83 | 83 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> |
| 84 | 84 | </a> |
| 85 | 85 | </div> |
| @@ -90,37 +90,4 @@ | ||
| 90 | 90 | <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px;margin-top:var(--space-8)">Valider</button> |
| 91 | 91 | </form> |
| 92 | 92 | |
| 93 | -<script> | |
| 94 | -// Amélioration progressive : ±5 min et presets sans recharger (mêmes liens | |
| 95 | -// href en secours sans JS, comme l'étape 2 d'ajout de temps). | |
| 96 | -(function () { | |
| 97 | - var input = document.getElementById('minutes'); | |
| 98 | - var pretty = document.getElementById('minutes-pretty'); | |
| 99 | - function dur(m) { | |
| 100 | - if (!m || m < 1) return '0 min'; | |
| 101 | - var h = Math.floor(m / 60), r = m % 60; | |
| 102 | - if (!h) return r + ' min'; | |
| 103 | - return r ? h + ' h ' + (r < 10 ? '0' + r : r) : h + ' h'; | |
| 104 | - } | |
| 105 | - function refresh() { | |
| 106 | - var v = Math.round(+input.value) || 0; | |
| 107 | - pretty.textContent = dur(v); | |
| 108 | - document.querySelectorAll('.preset[data-minutes]').forEach(function (p) { | |
| 109 | - if (+p.dataset.minutes === v) { p.setAttribute('aria-current', 'true'); } | |
| 110 | - else { p.removeAttribute('aria-current'); } | |
| 111 | - }); | |
| 112 | - } | |
| 113 | - document.querySelectorAll('.stepper-btn[data-step]').forEach(function (btn) { | |
| 114 | - btn.addEventListener('click', function (e) { | |
| 115 | - e.preventDefault(); | |
| 116 | - input.value = Math.max(5, (Math.round(+input.value) || 0) + (+btn.dataset.step)); | |
| 117 | - refresh(); | |
| 118 | - }); | |
| 119 | - }); | |
| 120 | - document.querySelectorAll('.preset[data-minutes]').forEach(function (p) { | |
| 121 | - p.addEventListener('click', function (e) { e.preventDefault(); input.value = +p.dataset.minutes; refresh(); }); | |
| 122 | - }); | |
| 123 | - input.addEventListener('input', refresh); | |
| 124 | -})(); | |
| 125 | -</script> | |
| 126 | 93 | {% endblock %} |
| @@ -10,7 +10,7 @@ | |||
| 10 | <span class="done-name">{{ name }}</span> | 10 | <span class="done-name">{{ name }}</span> |
| 11 | </div> | 11 | </div> |
| 12 | 12 | ||
| 13 | -<form method="post" action="/taches/{{ id }}/terminer"> | 13 | +<form method="post" action="/taches/{{ id }}/terminer" data-done> |
| 14 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 14 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 15 | 15 | ||
| 16 | <section class="done-sec"> | 16 | <section class="done-sec"> |
| @@ -46,12 +46,12 @@ | |||
| 46 | </div> | 46 | </div> |
| 47 | <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">Date du temps</label> | 47 | <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">Date du temps</label> |
| 48 | <div class="date-row"> | 48 | <div class="date-row"> |
| 49 | - <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" aria-label="Jour précédent"> | 49 | + <a class="btn btn-secondary btn-icon" href="{{ day_prev }}" data-date-step="-1" data-date-for="done-date" aria-label="Jour précédent"> |
| 50 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> | 50 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> |
| 51 | </a> | 51 | </a> |
| 52 | - <input class="input" id="done-date" name="date" type="date" value="{{ date }}" required> | 52 | + <input class="input" id="done-date" name="date" type="date" value="{{ date }}" max="{{ today }}" required> |
| 53 | {% if let Some(next_href) = day_next %} | 53 | {% if let Some(next_href) = day_next %} |
| 54 | - <a class="btn btn-secondary btn-icon" href="{{ next_href }}" aria-label="Jour suivant"> | 54 | + <a class="btn btn-secondary btn-icon" href="{{ next_href }}" data-date-step="1" data-date-for="done-date" aria-label="Jour suivant"> |
| 55 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> | 55 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> |
| 56 | </a> | 56 | </a> |
| 57 | {% else %} | 57 | {% else %} |
| @@ -75,11 +75,11 @@ | |||
| 75 | <div class="done-body"> | 75 | <div class="done-body"> |
| 76 | <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p> | 76 | <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p> |
| 77 | <div class="date-row"> | 77 | <div class="date-row"> |
| 78 | - <a class="btn btn-secondary btn-icon" href="{{ next_prev }}" aria-label="Jour précédent"> | 78 | + <a class="btn btn-secondary btn-icon" href="{{ next_prev }}" data-date-step="-1" data-date-for="next-date" aria-label="Jour précédent"> |
| 79 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> | 79 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg> |
| 80 | </a> | 80 | </a> |
| 81 | - <input class="input" name="prochaine" type="date" value="{{ next }}" required aria-label="Prochaine activation"> | 81 | + <input class="input" id="next-date" name="prochaine" type="date" value="{{ next }}" required aria-label="Prochaine activation"> |
| 82 | - <a class="btn btn-secondary btn-icon" href="{{ next_next }}" aria-label="Jour suivant"> | 82 | + <a class="btn btn-secondary btn-icon" href="{{ next_next }}" data-date-step="1" data-date-for="next-date" aria-label="Jour suivant"> |
| 83 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> | 83 | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"></path></svg> |
| 84 | </a> | 84 | </a> |
| 85 | </div> | 85 | </div> |
| @@ -90,37 +90,4 @@ | |||
| 90 | <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px;margin-top:var(--space-8)">Valider</button> | 90 | <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px;margin-top:var(--space-8)">Valider</button> |
| 91 | </form> | 91 | </form> |
| 92 | 92 | ||
| 93 | -<script> | ||
| 94 | -// Amélioration progressive : ±5 min et presets sans recharger (mêmes liens | ||
| 95 | -// href en secours sans JS, comme l'étape 2 d'ajout de temps). | ||
| 96 | -(function () { | ||
| 97 | - var input = document.getElementById('minutes'); | ||
| 98 | - var pretty = document.getElementById('minutes-pretty'); | ||
| 99 | - function dur(m) { | ||
| 100 | - if (!m || m < 1) return '0 min'; | ||
| 101 | - var h = Math.floor(m / 60), r = m % 60; | ||
| 102 | - if (!h) return r + ' min'; | ||
| 103 | - return r ? h + ' h ' + (r < 10 ? '0' + r : r) : h + ' h'; | ||
| 104 | - } | ||
| 105 | - function refresh() { | ||
| 106 | - var v = Math.round(+input.value) || 0; | ||
| 107 | - pretty.textContent = dur(v); | ||
| 108 | - document.querySelectorAll('.preset[data-minutes]').forEach(function (p) { | ||
| 109 | - if (+p.dataset.minutes === v) { p.setAttribute('aria-current', 'true'); } | ||
| 110 | - else { p.removeAttribute('aria-current'); } | ||
| 111 | - }); | ||
| 112 | - } | ||
| 113 | - document.querySelectorAll('.stepper-btn[data-step]').forEach(function (btn) { | ||
| 114 | - btn.addEventListener('click', function (e) { | ||
| 115 | - e.preventDefault(); | ||
| 116 | - input.value = Math.max(5, (Math.round(+input.value) || 0) + (+btn.dataset.step)); | ||
| 117 | - refresh(); | ||
| 118 | - }); | ||
| 119 | - }); | ||
| 120 | - document.querySelectorAll('.preset[data-minutes]').forEach(function (p) { | ||
| 121 | - p.addEventListener('click', function (e) { e.preventDefault(); input.value = +p.dataset.minutes; refresh(); }); | ||
| 122 | - }); | ||
| 123 | - input.addEventListener('input', refresh); | ||
| 124 | -})(); | ||
| 125 | -</script> | ||
| 126 | {% endblock %} | 93 | {% endblock %} |
modified
templates/todos.html +60 -3 | @@ -30,7 +30,11 @@ | ||
| 30 | 30 | <div style="flex:1;min-width:0"> |
| 31 | 31 | <div class="todo-name">{{ t.name }}</div> |
| 32 | 32 | <div class="todo-meta">{{ t.meta }}</div> |
| 33 | - {% if !t.assignee_line.is_empty() %}<div class="todo-for{% if t.for_me %} forme{% endif %}">{{ t.assignee_line }}</div>{% endif %} | |
| 33 | + {% if !t.assignees.is_empty() %} | |
| 34 | + <div class="asg-chips"> | |
| 35 | + {% for a in t.assignees %}<span class="asg-chip{% if a.me %} me{% endif %}">{{ a.name }}</span>{% endfor %} | |
| 36 | + </div> | |
| 37 | + {% endif %} | |
| 34 | 38 | </div> |
| 35 | 39 | {% if t.mine %} |
| 36 | 40 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> |
| @@ -72,7 +76,11 @@ | ||
| 72 | 76 | <div style="flex:1;min-width:0"> |
| 73 | 77 | <div class="todo-name">{{ t.name }}</div> |
| 74 | 78 | <div class="todo-meta{% if t.late %} late{% endif %}">{{ t.meta }}</div> |
| 75 | - {% if !t.assignee_line.is_empty() %}<div class="todo-for{% if t.for_me %} forme{% endif %}">{{ t.assignee_line }}</div>{% endif %} | |
| 79 | + {% if !t.assignees.is_empty() %} | |
| 80 | + <div class="asg-chips"> | |
| 81 | + {% for a in t.assignees %}<span class="asg-chip{% if a.me %} me{% endif %}">{{ a.name }}</span>{% endfor %} | |
| 82 | + </div> | |
| 83 | + {% endif %} | |
| 76 | 84 | </div> |
| 77 | 85 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> |
| 78 | 86 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg> |
| @@ -101,7 +109,11 @@ | ||
| 101 | 109 | <div style="flex:1;min-width:0"> |
| 102 | 110 | <div style="font-size:15px">{{ t.name }}</div> |
| 103 | 111 | <div class="todo-meta">{{ t.meta }}</div> |
| 104 | - {% if !t.assignee_line.is_empty() %}<div class="todo-for{% if t.for_me %} forme{% endif %}">{{ t.assignee_line }}</div>{% endif %} | |
| 112 | + {% if !t.assignees.is_empty() %} | |
| 113 | + <div class="asg-chips"> | |
| 114 | + {% for a in t.assignees %}<span class="asg-chip{% if a.me %} me{% endif %}">{{ a.name }}</span>{% endfor %} | |
| 115 | + </div> | |
| 116 | + {% endif %} | |
| 105 | 117 | </div> |
| 106 | 118 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> |
| 107 | 119 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg> |
| @@ -111,4 +123,49 @@ | ||
| 111 | 123 | </div> |
| 112 | 124 | </details> |
| 113 | 125 | {% endif %} |
| 126 | +<script> | |
| 127 | +// Amélioration progressive : « Fait ✓ » ouvre le flux de complétion en | |
| 128 | +// bottom sheet par-dessus la liste (sans JS : navigation vers la page). | |
| 129 | +(function () { | |
| 130 | + var trigger = null; | |
| 131 | + function close(wrap) { | |
| 132 | + wrap.remove(); | |
| 133 | + document.removeEventListener('keydown', wrap.tt_esc); | |
| 134 | + if (trigger && document.contains(trigger)) trigger.focus(); | |
| 135 | + } | |
| 136 | + function open(html, url) { | |
| 137 | + var doc = new DOMParser().parseFromString(html, 'text/html'); | |
| 138 | + var title = doc.querySelector('.done-title'); | |
| 139 | + var form = doc.querySelector('form[data-done]'); | |
| 140 | + if (!form) { location.href = url; return; } | |
| 141 | + var wrap = document.createElement('div'); | |
| 142 | + wrap.className = 'sheet-wrap open'; | |
| 143 | + wrap.setAttribute('role', 'dialog'); | |
| 144 | + wrap.setAttribute('aria-label', 'Tâche effectuée'); | |
| 145 | + wrap.innerHTML = '<span class="sheet-backdrop"></span>' + | |
| 146 | + '<div class="sheet sheet-scroll"><div class="sheet-head">' + | |
| 147 | + '<span class="pname">Tâche effectuée</span>' + | |
| 148 | + '<button type="button" class="btn btn-ghost close">Fermer</button></div></div>'; | |
| 149 | + var sheet = wrap.querySelector('.sheet'); | |
| 150 | + if (title) sheet.appendChild(title); | |
| 151 | + sheet.appendChild(form); | |
| 152 | + wrap.tt_esc = function (e) { if (e.key === 'Escape') close(wrap); }; | |
| 153 | + wrap.querySelector('.sheet-backdrop').addEventListener('click', function () { close(wrap); }); | |
| 154 | + wrap.querySelector('.close').addEventListener('click', function () { close(wrap); }); | |
| 155 | + document.addEventListener('keydown', wrap.tt_esc); | |
| 156 | + document.querySelector('.shell').appendChild(wrap); | |
| 157 | + wrap.querySelector('.close').focus(); | |
| 158 | + } | |
| 159 | + document.addEventListener('click', function (e) { | |
| 160 | + var a = e.target.closest ? e.target.closest('a[href$="/terminer"]') : null; | |
| 161 | + if (!a) return; | |
| 162 | + e.preventDefault(); | |
| 163 | + trigger = a; | |
| 164 | + fetch(a.getAttribute('href')) | |
| 165 | + .then(function (r) { return r.ok ? r.text() : Promise.reject(); }) | |
| 166 | + .then(function (t) { open(t, a.getAttribute('href')); }) | |
| 167 | + .catch(function () { location.href = a.getAttribute('href'); }); | |
| 168 | + }); | |
| 169 | +})(); | |
| 170 | +</script> | |
| 114 | 171 | {% endblock %} |
| @@ -30,7 +30,11 @@ | |||
| 30 | <div style="flex:1;min-width:0"> | 30 | <div style="flex:1;min-width:0"> |
| 31 | <div class="todo-name">{{ t.name }}</div> | 31 | <div class="todo-name">{{ t.name }}</div> |
| 32 | <div class="todo-meta">{{ t.meta }}</div> | 32 | <div class="todo-meta">{{ t.meta }}</div> |
| 33 | - {% if !t.assignee_line.is_empty() %}<div class="todo-for{% if t.for_me %} forme{% endif %}">{{ t.assignee_line }}</div>{% endif %} | 33 | + {% if !t.assignees.is_empty() %} |
| 34 | + <div class="asg-chips"> | ||
| 35 | + {% for a in t.assignees %}<span class="asg-chip{% if a.me %} me{% endif %}">{{ a.name }}</span>{% endfor %} | ||
| 36 | + </div> | ||
| 37 | + {% endif %} | ||
| 34 | </div> | 38 | </div> |
| 35 | {% if t.mine %} | 39 | {% if t.mine %} |
| 36 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> | 40 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> |
| @@ -72,7 +76,11 @@ | |||
| 72 | <div style="flex:1;min-width:0"> | 76 | <div style="flex:1;min-width:0"> |
| 73 | <div class="todo-name">{{ t.name }}</div> | 77 | <div class="todo-name">{{ t.name }}</div> |
| 74 | <div class="todo-meta{% if t.late %} late{% endif %}">{{ t.meta }}</div> | 78 | <div class="todo-meta{% if t.late %} late{% endif %}">{{ t.meta }}</div> |
| 75 | - {% if !t.assignee_line.is_empty() %}<div class="todo-for{% if t.for_me %} forme{% endif %}">{{ t.assignee_line }}</div>{% endif %} | 79 | + {% if !t.assignees.is_empty() %} |
| 80 | + <div class="asg-chips"> | ||
| 81 | + {% for a in t.assignees %}<span class="asg-chip{% if a.me %} me{% endif %}">{{ a.name }}</span>{% endfor %} | ||
| 82 | + </div> | ||
| 83 | + {% endif %} | ||
| 76 | </div> | 84 | </div> |
| 77 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> | 85 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> |
| 78 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg> | 86 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg> |
| @@ -101,7 +109,11 @@ | |||
| 101 | <div style="flex:1;min-width:0"> | 109 | <div style="flex:1;min-width:0"> |
| 102 | <div style="font-size:15px">{{ t.name }}</div> | 110 | <div style="font-size:15px">{{ t.name }}</div> |
| 103 | <div class="todo-meta">{{ t.meta }}</div> | 111 | <div class="todo-meta">{{ t.meta }}</div> |
| 104 | - {% if !t.assignee_line.is_empty() %}<div class="todo-for{% if t.for_me %} forme{% endif %}">{{ t.assignee_line }}</div>{% endif %} | 112 | + {% if !t.assignees.is_empty() %} |
| 113 | + <div class="asg-chips"> | ||
| 114 | + {% for a in t.assignees %}<span class="asg-chip{% if a.me %} me{% endif %}">{{ a.name }}</span>{% endfor %} | ||
| 115 | + </div> | ||
| 116 | + {% endif %} | ||
| 105 | </div> | 117 | </div> |
| 106 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> | 118 | <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}"> |
| 107 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg> | 119 | <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"></path></svg> |
| @@ -111,4 +123,49 @@ | |||
| 111 | </div> | 123 | </div> |
| 112 | </details> | 124 | </details> |
| 113 | {% endif %} | 125 | {% endif %} |
| 126 | +<script> | ||
| 127 | +// Amélioration progressive : « Fait ✓ » ouvre le flux de complétion en | ||
| 128 | +// bottom sheet par-dessus la liste (sans JS : navigation vers la page). | ||
| 129 | +(function () { | ||
| 130 | + var trigger = null; | ||
| 131 | + function close(wrap) { | ||
| 132 | + wrap.remove(); | ||
| 133 | + document.removeEventListener('keydown', wrap.tt_esc); | ||
| 134 | + if (trigger && document.contains(trigger)) trigger.focus(); | ||
| 135 | + } | ||
| 136 | + function open(html, url) { | ||
| 137 | + var doc = new DOMParser().parseFromString(html, 'text/html'); | ||
| 138 | + var title = doc.querySelector('.done-title'); | ||
| 139 | + var form = doc.querySelector('form[data-done]'); | ||
| 140 | + if (!form) { location.href = url; return; } | ||
| 141 | + var wrap = document.createElement('div'); | ||
| 142 | + wrap.className = 'sheet-wrap open'; | ||
| 143 | + wrap.setAttribute('role', 'dialog'); | ||
| 144 | + wrap.setAttribute('aria-label', 'Tâche effectuée'); | ||
| 145 | + wrap.innerHTML = '<span class="sheet-backdrop"></span>' + | ||
| 146 | + '<div class="sheet sheet-scroll"><div class="sheet-head">' + | ||
| 147 | + '<span class="pname">Tâche effectuée</span>' + | ||
| 148 | + '<button type="button" class="btn btn-ghost close">Fermer</button></div></div>'; | ||
| 149 | + var sheet = wrap.querySelector('.sheet'); | ||
| 150 | + if (title) sheet.appendChild(title); | ||
| 151 | + sheet.appendChild(form); | ||
| 152 | + wrap.tt_esc = function (e) { if (e.key === 'Escape') close(wrap); }; | ||
| 153 | + wrap.querySelector('.sheet-backdrop').addEventListener('click', function () { close(wrap); }); | ||
| 154 | + wrap.querySelector('.close').addEventListener('click', function () { close(wrap); }); | ||
| 155 | + document.addEventListener('keydown', wrap.tt_esc); | ||
| 156 | + document.querySelector('.shell').appendChild(wrap); | ||
| 157 | + wrap.querySelector('.close').focus(); | ||
| 158 | + } | ||
| 159 | + document.addEventListener('click', function (e) { | ||
| 160 | + var a = e.target.closest ? e.target.closest('a[href$="/terminer"]') : null; | ||
| 161 | + if (!a) return; | ||
| 162 | + e.preventDefault(); | ||
| 163 | + trigger = a; | ||
| 164 | + fetch(a.getAttribute('href')) | ||
| 165 | + .then(function (r) { return r.ok ? r.text() : Promise.reject(); }) | ||
| 166 | + .then(function (t) { open(t, a.getAttribute('href')); }) | ||
| 167 | + .catch(function () { location.href = a.getAttribute('href'); }); | ||
| 168 | + }); | ||
| 169 | +})(); | ||
| 170 | +</script> | ||
| 114 | {% endblock %} | 171 | {% endblock %} |