judu/ex-egopublic Fork
⑂ c538051
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

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
Julien Durillon committed 2026-08-08T23:15:27+02:00 Browse files
c538051 parent: 225c26f
modified assets/style.css +15 -4
@@ -392,9 +392,12 @@ a.team-pill:hover { filter: brightness(.96); }
392392 légèrement sur la proportion visuelle pour les petits segments. */
393393 .col-bar a { display: block; min-height: 24px; width: 100%; }
394394
395-/* — bottom sheet (zéro JS : :target) — */
395+/* — bottom sheet (zéro JS : :target ; .open = variante injectée en JS) — */
396396 .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); }
398401 .sheet-backdrop {
399402 position: absolute; inset: 0;
400403 background: color-mix(in srgb, var(--color-neutral-900) 45%, transparent);
@@ -478,8 +481,16 @@ a.team-pill:hover { filter: brightness(.96); }
478481 .todo-name { font-size: 16px; }
479482 .todo-meta { font-size: 13px; opacity: .7; }
480483 .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); }
483494 .todo-actions { display: flex; gap: var(--space-2); margin-top: var(--space-3); }
484495 .todo-actions form { display: contents; }
485496 .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;
2323
2424 /// ─── Écran liste ───
2525
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+
2633 pub struct TodoCard {
2734 pub id: Uuid,
2835 pub name: String,
@@ -35,9 +42,9 @@ pub struct TodoCard {
3542 pub mine: bool,
3643 /// Bloc en cours : prise par quelqu'un d'autre (carte estompée).
3744 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.
4148 pub for_me: bool,
4249 }
4350
@@ -92,16 +99,6 @@ fn list_href(doing_on: bool, open_on: bool) -> String {
9299 }
93100 }
94101
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-
105102 pub async fn todos_page(
106103 State(state): State<AppState>,
107104 ctx: ActiveTeam,
@@ -123,11 +120,6 @@ pub async fn todos_page(
123120 let card = |t: &TodoRow| {
124121 let assignees = by_todo.get(&t.id).map(Vec::as_slice).unwrap_or(&[]);
125122 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();
131123 let mine = t.taker_id == Some(me);
132124 let late = t.state_id == STATE_TODO && t.activation_date < today;
133125 let meta = if t.state_id == STATE_IN_PROGRESS {
@@ -151,7 +143,13 @@ pub async fn todos_page(
151143 late,
152144 mine,
153145 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(),
155153 for_me,
156154 }
157155 };
@@ -450,6 +448,8 @@ pub struct TodoDonePage {
450448 pub plus_href: String,
451449 pub presets: Vec<DonePreset>,
452450 pub date: NaiveDate,
451+ /// Borne haute de la date du temps (aujourd'hui) — attribut max de l'input.
452+ pub today: NaiveDate,
453453 pub day_prev: String,
454454 /// None = aujourd'hui (flèche désactivée).
455455 pub day_next: Option<String>,
@@ -530,6 +530,7 @@ pub async fn todo_done_page(
530530 })
531531 .collect(),
532532 date,
533+ today,
533534 day_prev: done_href(id, minutes, date - Duration::days(1), next),
534535 day_next: (date < today).then(|| done_href(id, minutes, date + Duration::days(1), next)),
535536 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 @@
158158 if (e.key === 'Escape' && openSheet()) location.hash = 'fermer';
159159 });
160160 })();
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+})();
161203 // Amélioration progressive : le temps écoulé rendu par le serveur reste
162204 // juste sans JS, ceci le rafraîchit toutes les 10 s sans recharger.
163205 (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 reste203 // 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 @@
1010 <span class="done-name">{{ name }}</span>
1111 </div>
1212
13-<form method="post" action="/taches/{{ id }}/terminer">
13+<form method="post" action="/taches/{{ id }}/terminer" data-done>
1414 <input type="hidden" name="csrf" value="{{ csrf }}">
1515
1616 <section class="done-sec">
@@ -46,12 +46,12 @@
4646 </div>
4747 <label class="mini-label" for="done-date" style="margin-top:var(--space-3)">Date du temps</label>
4848 <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">
5050 <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>
5151 </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>
5353 {% 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">
5555 <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>
5656 </a>
5757 {% else %}
@@ -75,11 +75,11 @@
7575 <div class="done-body">
7676 <p style="font-size:13px;opacity:.7;margin:0 0 var(--space-3)">{{ rhythm }}</p>
7777 <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">
7979 <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>
8080 </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">
8383 <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>
8484 </a>
8585 </div>
@@ -90,37 +90,4 @@
9090 <button type="submit" class="btn btn-primary btn-block" style="min-height:52px;font-size:16px;margin-top:var(--space-8)">Valider</button>
9191 </form>
9292
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>
12693 {% 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 @@
3030 <div style="flex:1;min-width:0">
3131 <div class="todo-name">{{ t.name }}</div>
3232 <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 %}
3438 </div>
3539 {% if t.mine %}
3640 <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 @@
7276 <div style="flex:1;min-width:0">
7377 <div class="todo-name">{{ t.name }}</div>
7478 <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 %}
7684 </div>
7785 <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}">
7886 <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 @@
101109 <div style="flex:1;min-width:0">
102110 <div style="font-size:15px">{{ t.name }}</div>
103111 <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 %}
105117 </div>
106118 <a class="btn btn-secondary btn-icon" style="flex:none" href="/taches/{{ t.id }}/modifier" aria-label="Modifier la tâche {{ t.name }}">
107119 <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 @@
111123 </div>
112124 </details>
113125 {% 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>
114171 {% 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 %}