judu/ex-egopublic Fork
⑂ 01c726d
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(ui): écran de choix « Ajouter » + mode large ≥ 700 px (design resync)Unverified

Onglet Ajouter : atterrissage à deux cartes (lancer le minuteur / saisie
manuelle en deux étapes) conformément au design (addLanding « deux
boutons ») ; l'étape 1 passe par ?mode=manuel avec « ← Retour », le
bandeau flottant « Lancer le minuteur » disparaît (remplacé par la
carte). Mode large : rail de navigation 232 px avec logo, cadre 960 px
centré, sheets en modales centrées — pur CSS (@media), zéro variante de
markup. Audit a11y : 13 écrans, carte accent-deep (4.5:1), vert.

Claude-Session: https://claude.ai/code/session_01D114WAMbcTKTZShiyksS1z
Julien Durillon committed 2026-08-10T20:24:50+02:00 Browse files
01c726d parent: a314c3e
modified a11y/audit.mjs +5 -4
@@ -1,6 +1,6 @@
11 // Audit d'accessibilité récurrent — axe-core dans Chrome headless.
22 //
3-// Parcourt les 12 écrans de l'app (viewport mobile 390×844) et échoue
3+// Parcourt les 13 écrans de l'app (viewport mobile 390×844) et échoue
44 // (exit 1) à la moindre violation axe « critical » ou « serious » ;
55 // les violations moindres sont listées en avertissement.
66 //
@@ -59,7 +59,7 @@ if (page.url().includes('/login')) {
5959
6060 // Une catégorie, une saisie et une todo existantes servent aux pages paramétrées.
6161 const ids = await page.evaluate(async () => {
62- const step1 = await fetch('/temps/nouveau').then((r) => r.text());
62+ const step1 = await fetch('/temps/nouveau?mode=manuel').then((r) => r.text());
6363 const times = await fetch('/temps').then((r) => r.text());
6464 const todos = await fetch('/taches').then((r) => r.text());
6565 return {
@@ -71,7 +71,8 @@ const ids = await page.evaluate(async () => {
7171
7272 const pages = [
7373 { name: 'login', url: '/login', anon: true },
74- { name: 'add-step1', url: '/temps/nouveau' },
74+ { name: 'add-choice', url: '/temps/nouveau' },
75+ { name: 'add-step1', url: '/temps/nouveau?mode=manuel' },
7576 ids.categorie && { name: 'add-step2', url: `/temps/nouveau?categorie=${ids.categorie}` },
7677 { name: 'stats', url: '/stats' },
7778 { name: 'times', url: '/temps' },
@@ -83,7 +84,7 @@ const pages = [
8384 { name: 'todo-new', url: '/taches/nouvelle' },
8485 ids.todo && { name: 'todo-done', url: `/taches/${ids.todo}/terminer` },
8586 ].filter(Boolean);
86-if (pages.length < 12) console.warn('⚠ catégorie, saisie ou todo manquante : certains écrans sont sautés.');
87+if (pages.length < 13) console.warn('⚠ catégorie, saisie ou todo manquante : certains écrans sont sautés.');
8788
8889 let blocking = 0;
8990 for (const p of pages) {
@@ -1,6 +1,6 @@
1 // Audit d'accessibilité récurrent — axe-core dans Chrome headless.1 // Audit d'accessibilité récurrent — axe-core dans Chrome headless.
2 //2 //
3-// Parcourt les 12 écrans de l'app (viewport mobile 390×844) et échoue3+// Parcourt les 13 écrans de l'app (viewport mobile 390×844) et échoue
4 // (exit 1) à la moindre violation axe « critical » ou « serious » ;4 // (exit 1) à la moindre violation axe « critical » ou « serious » ;
5 // les violations moindres sont listées en avertissement.5 // les violations moindres sont listées en avertissement.
6 //6 //
@@ -59,7 +59,7 @@ if (page.url().includes('/login')) {
59 59
60 // Une catégorie, une saisie et une todo existantes servent aux pages paramétrées.60 // Une catégorie, une saisie et une todo existantes servent aux pages paramétrées.
61 const ids = await page.evaluate(async () => {61 const ids = await page.evaluate(async () => {
62- const step1 = await fetch('/temps/nouveau').then((r) => r.text());62+ const step1 = await fetch('/temps/nouveau?mode=manuel').then((r) => r.text());
63 const times = await fetch('/temps').then((r) => r.text());63 const times = await fetch('/temps').then((r) => r.text());
64 const todos = await fetch('/taches').then((r) => r.text());64 const todos = await fetch('/taches').then((r) => r.text());
65 return {65 return {
@@ -71,7 +71,8 @@ const ids = await page.evaluate(async () => {
71 71
72 const pages = [72 const pages = [
73 { name: 'login', url: '/login', anon: true },73 { name: 'login', url: '/login', anon: true },
74- { name: 'add-step1', url: '/temps/nouveau' },74+ { name: 'add-choice', url: '/temps/nouveau' },
75+ { name: 'add-step1', url: '/temps/nouveau?mode=manuel' },
75 ids.categorie && { name: 'add-step2', url: `/temps/nouveau?categorie=${ids.categorie}` },76 ids.categorie && { name: 'add-step2', url: `/temps/nouveau?categorie=${ids.categorie}` },
76 { name: 'stats', url: '/stats' },77 { name: 'stats', url: '/stats' },
77 { name: 'times', url: '/temps' },78 { name: 'times', url: '/temps' },
@@ -83,7 +84,7 @@ const pages = [
83 { name: 'todo-new', url: '/taches/nouvelle' },84 { name: 'todo-new', url: '/taches/nouvelle' },
84 ids.todo && { name: 'todo-done', url: `/taches/${ids.todo}/terminer` },85 ids.todo && { name: 'todo-done', url: `/taches/${ids.todo}/terminer` },
85 ].filter(Boolean);86 ].filter(Boolean);
86-if (pages.length < 12) console.warn('⚠ catégorie, saisie ou todo manquante : certains écrans sont sautés.');87+if (pages.length < 13) console.warn('⚠ catégorie, saisie ou todo manquante : certains écrans sont sautés.');
87 88
88 let blocking = 0;89 let blocking = 0;
89 for (const p of pages) {90 for (const p of pages) {
modified assets/style.css +68 -5
@@ -239,11 +239,31 @@ a.team-pill:hover { filter: brightness(.96); }
239239 .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; }
240240 .timer-running { background: var(--color-accent-deep); color: var(--color-bg); }
241241 .timer-waiting { background: var(--color-accent-200); color: var(--color-accent-900); }
242-.timer-start {
243- background: var(--color-surface); color: var(--color-accent-700);
244- transition: background .15s ease, color .15s ease;
245-}
246-.timer-start:hover { background: var(--color-accent-2); color: var(--color-bg); }
242+/* Atterrissage de l'onglet Ajouter : minuteur ou saisie manuelle. */
243+.choice-hint { font-size: 15px; opacity: .7; margin: 0 0 var(--space-6); text-wrap: pretty; }
244+.choice-stack { display: grid; gap: var(--space-3); }
245+.choice-card {
246+ display: flex; align-items: center; gap: var(--space-4); width: 100%;
247+ text-align: left; padding: var(--space-6) var(--space-4);
248+ border: none; border-radius: var(--radius-lg); cursor: pointer;
249+ font: inherit; text-decoration: none;
250+}
251+.choice-card svg { flex: none; }
252+.choice-title {
253+ display: block; font-family: var(--font-heading);
254+ font-weight: var(--font-heading-weight); font-size: 21px; letter-spacing: -0.01em;
255+}
256+.choice-sub { display: block; font-size: 14px; opacity: .9; margin-top: 2px; }
257+/* accent-deep est calibré pour 4.5:1 avec la crème — sans marge pour une
258+ estompe : sous-titre en pleine opacité sur la carte foncée. */
259+.choice-timer .choice-sub { opacity: 1; }
260+/* accent-deep, pas accent : le sous-titre crème doit tenir 4.5:1 (convention
261+ a11y du projet — écart assumé avec le design plein accent). */
262+.choice-timer { background: var(--color-accent-deep); color: var(--color-bg); box-shadow: var(--shadow-md); }
263+.choice-timer:hover { background: var(--color-accent-700); }
264+.choice-manual { background: var(--color-surface); color: var(--color-text); border: 1.5px solid var(--color-accent-2); }
265+.choice-manual:hover { background: color-mix(in srgb, var(--color-accent-2) 12%, var(--color-surface)); }
266+.choice-manage { justify-self: start; margin-top: var(--space-2); }
247267
248268 /* Écran minuteur (onglet Ajouter pendant qu'un timer tourne). */
249269 .chrono-card {
@@ -293,6 +313,8 @@ a.team-pill:hover { filter: brightness(.96); }
293313 border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg);
294314 box-shadow: var(--shadow-md);
295315 }
316+/* Logo + nom dans le rail — n'existe qu'en mode large. */
317+.rail-brand { display: none; }
296318 .tab {
297319 flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
298320 padding: 8px 2px; border-radius: 999px; text-decoration: none;
@@ -648,3 +670,44 @@ a.times-link { flex-direction: row; align-items: center; text-decoration: none;
648670 .sheet { animation: none; }
649671 .toast { animation: tt-fade .4s ease-in 1.5s forwards; }
650672 }
673+
674+/* ═══ Mode large (≥ 700 px) — source : projet Design (wide) ═══
675+ Le rail de navigation remplace la tab bar, le cadre devient une carte
676+ centrée, les sheets deviennent des modales centrées. Tout est piloté
677+ par les mêmes classes : aucune variante de markup. */
678+@media (min-width: 700px) {
679+ .viewport { align-items: center; }
680+ .shell {
681+ max-width: 960px; height: min(100dvh, 820px);
682+ border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
683+ padding-top: 0;
684+ }
685+ .app-header { margin-left: 232px; padding: var(--space-8) var(--space-8) var(--space-4); }
686+ .app-main { margin-left: 232px; max-width: 700px; padding: 0 var(--space-8) 128px; }
687+ .tabbar {
688+ top: 0; bottom: 0; right: auto; width: 232px;
689+ flex-direction: column; gap: 4px; align-items: stretch;
690+ padding: var(--space-8) var(--space-4);
691+ border-right: 1px solid var(--color-divider);
692+ border-radius: 0; box-shadow: none;
693+ }
694+ .rail-brand {
695+ display: flex; align-items: center; gap: 10px;
696+ padding: 0 var(--space-3); margin-bottom: var(--space-6);
697+ font-family: var(--font-heading); font-size: 22px; letter-spacing: -0.02em;
698+ }
699+ .rail-brand svg { display: block; flex: none; }
700+ .tab {
701+ flex: 0 0 auto; flex-direction: row; justify-content: flex-start;
702+ gap: var(--space-3); padding: 11px var(--space-3); font-size: 15px;
703+ color: color-mix(in srgb, var(--color-text) 70%, transparent);
704+ }
705+ .tab[aria-current="page"] { background: var(--color-accent-2-200); color: var(--color-accent-700); }
706+ .timer-slot { left: 232px; bottom: var(--space-6); padding: 0 var(--space-8); }
707+ .sheet-wrap:target, .sheet-wrap.open { align-items: center; padding: var(--space-6); }
708+ .sheet {
709+ max-width: 520px; max-height: 100%; overflow-y: auto;
710+ border-radius: var(--radius-lg); padding: var(--space-8);
711+ animation: tt-drop .18s ease-out;
712+ }
713+}
@@ -239,11 +239,31 @@ a.team-pill:hover { filter: brightness(.96); }
239 .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; }239 .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; }
240 .timer-running { background: var(--color-accent-deep); color: var(--color-bg); }240 .timer-running { background: var(--color-accent-deep); color: var(--color-bg); }
241 .timer-waiting { background: var(--color-accent-200); color: var(--color-accent-900); }241 .timer-waiting { background: var(--color-accent-200); color: var(--color-accent-900); }
242-.timer-start {242+/* Atterrissage de l'onglet Ajouter : minuteur ou saisie manuelle. */
243- background: var(--color-surface); color: var(--color-accent-700);243+.choice-hint { font-size: 15px; opacity: .7; margin: 0 0 var(--space-6); text-wrap: pretty; }
244- transition: background .15s ease, color .15s ease;244+.choice-stack { display: grid; gap: var(--space-3); }
245-}245+.choice-card {
246-.timer-start:hover { background: var(--color-accent-2); color: var(--color-bg); }246+ display: flex; align-items: center; gap: var(--space-4); width: 100%;
247+ text-align: left; padding: var(--space-6) var(--space-4);
248+ border: none; border-radius: var(--radius-lg); cursor: pointer;
249+ font: inherit; text-decoration: none;
250+}
251+.choice-card svg { flex: none; }
252+.choice-title {
253+ display: block; font-family: var(--font-heading);
254+ font-weight: var(--font-heading-weight); font-size: 21px; letter-spacing: -0.01em;
255+}
256+.choice-sub { display: block; font-size: 14px; opacity: .9; margin-top: 2px; }
257+/* accent-deep est calibré pour 4.5:1 avec la crème — sans marge pour une
258+ estompe : sous-titre en pleine opacité sur la carte foncée. */
259+.choice-timer .choice-sub { opacity: 1; }
260+/* accent-deep, pas accent : le sous-titre crème doit tenir 4.5:1 (convention
261+ a11y du projet — écart assumé avec le design plein accent). */
262+.choice-timer { background: var(--color-accent-deep); color: var(--color-bg); box-shadow: var(--shadow-md); }
263+.choice-timer:hover { background: var(--color-accent-700); }
264+.choice-manual { background: var(--color-surface); color: var(--color-text); border: 1.5px solid var(--color-accent-2); }
265+.choice-manual:hover { background: color-mix(in srgb, var(--color-accent-2) 12%, var(--color-surface)); }
266+.choice-manage { justify-self: start; margin-top: var(--space-2); }
247 267
248 /* Écran minuteur (onglet Ajouter pendant qu'un timer tourne). */268 /* Écran minuteur (onglet Ajouter pendant qu'un timer tourne). */
249 .chrono-card {269 .chrono-card {
@@ -293,6 +313,8 @@ a.team-pill:hover { filter: brightness(.96); }
293 border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg);313 border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg);
294 box-shadow: var(--shadow-md);314 box-shadow: var(--shadow-md);
295 }315 }
316+/* Logo + nom dans le rail — n'existe qu'en mode large. */
317+.rail-brand { display: none; }
296 .tab {318 .tab {
297 flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;319 flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
298 padding: 8px 2px; border-radius: 999px; text-decoration: none;320 padding: 8px 2px; border-radius: 999px; text-decoration: none;
@@ -648,3 +670,44 @@ a.times-link { flex-direction: row; align-items: center; text-decoration: none;
648 .sheet { animation: none; }670 .sheet { animation: none; }
649 .toast { animation: tt-fade .4s ease-in 1.5s forwards; }671 .toast { animation: tt-fade .4s ease-in 1.5s forwards; }
650 }672 }
673+
674+/* ═══ Mode large (≥ 700 px) — source : projet Design (wide) ═══
675+ Le rail de navigation remplace la tab bar, le cadre devient une carte
676+ centrée, les sheets deviennent des modales centrées. Tout est piloté
677+ par les mêmes classes : aucune variante de markup. */
678+@media (min-width: 700px) {
679+ .viewport { align-items: center; }
680+ .shell {
681+ max-width: 960px; height: min(100dvh, 820px);
682+ border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
683+ padding-top: 0;
684+ }
685+ .app-header { margin-left: 232px; padding: var(--space-8) var(--space-8) var(--space-4); }
686+ .app-main { margin-left: 232px; max-width: 700px; padding: 0 var(--space-8) 128px; }
687+ .tabbar {
688+ top: 0; bottom: 0; right: auto; width: 232px;
689+ flex-direction: column; gap: 4px; align-items: stretch;
690+ padding: var(--space-8) var(--space-4);
691+ border-right: 1px solid var(--color-divider);
692+ border-radius: 0; box-shadow: none;
693+ }
694+ .rail-brand {
695+ display: flex; align-items: center; gap: 10px;
696+ padding: 0 var(--space-3); margin-bottom: var(--space-6);
697+ font-family: var(--font-heading); font-size: 22px; letter-spacing: -0.02em;
698+ }
699+ .rail-brand svg { display: block; flex: none; }
700+ .tab {
701+ flex: 0 0 auto; flex-direction: row; justify-content: flex-start;
702+ gap: var(--space-3); padding: 11px var(--space-3); font-size: 15px;
703+ color: color-mix(in srgb, var(--color-text) 70%, transparent);
704+ }
705+ .tab[aria-current="page"] { background: var(--color-accent-2-200); color: var(--color-accent-700); }
706+ .timer-slot { left: 232px; bottom: var(--space-6); padding: 0 var(--space-8); }
707+ .sheet-wrap:target, .sheet-wrap.open { align-items: center; padding: var(--space-6); }
708+ .sheet {
709+ max-width: 520px; max-height: 100%; overflow-y: auto;
710+ border-radius: var(--radius-lg); padding: var(--space-8);
711+ animation: tt-drop .18s ease-out;
712+ }
713+}
modified src/i18n.rs +17 -4
@@ -73,7 +73,6 @@ pub struct Tr {
7373 pub member_one: &'static str,
7474 pub member_many: &'static str,
7575 pub start_timer: &'static str,
76- pub start_timer_aria: &'static str,
7776 pub stop_timer_aria: &'static str,
7877 pub timer_running_note: &'static str,
7978 pub record_measured_aria: &'static str,
@@ -90,6 +89,11 @@ pub struct Tr {
9089 pub bad_credentials: &'static str,
9190 // ─── ajout ───
9291 pub title_add: &'static str,
92+ /// Écran de choix (deux boutons) de l'onglet Ajouter.
93+ pub add_choice_hint: &'static str,
94+ pub choice_timer_sub: &'static str,
95+ pub choice_manual_sub: &'static str,
96+ pub back: &'static str,
9397 pub step1_hint: &'static str,
9498 pub no_categories_yet: &'static str,
9599 pub manage_categories: &'static str,
@@ -332,7 +336,6 @@ pub static FR: Tr = Tr {
332336 member_one: "membre",
333337 member_many: "membres",
334338 start_timer: "Lancer le minuteur",
335- start_timer_aria: "Démarrer le timer",
336339 stop_timer_aria: "Arrêter le timer",
337340 timer_running_note: "en cours",
338341 record_measured_aria: "Enregistrer le temps mesuré",
@@ -347,6 +350,10 @@ pub static FR: Tr = Tr {
347350 sign_in: "Se connecter",
348351 bad_credentials: "Identifiants incorrects.",
349352 title_add: "Ajouter du temps",
353+ add_choice_hint: "Compte le temps au fur et à mesure, ou saisis une durée que tu as déjà passée.",
354+ choice_timer_sub: "Tu choisiras la catégorie en l'arrêtant.",
355+ choice_manual_sub: "Une durée déjà passée, en deux étapes.",
356+ back: "← Retour",
350357 step1_hint: "Étape 1 sur 2 — choisis la catégorie",
351358 no_categories_yet: "Aucune catégorie pour l'instant — commence par en créer une.",
352359 manage_categories: "Gérer les catégories",
@@ -550,7 +557,6 @@ pub static EN: Tr = Tr {
550557 member_one: "member",
551558 member_many: "members",
552559 start_timer: "Start the timer",
553- start_timer_aria: "Start timer",
554560 stop_timer_aria: "Stop timer",
555561 timer_running_note: "running",
556562 record_measured_aria: "Record the measured time",
@@ -565,6 +571,10 @@ pub static EN: Tr = Tr {
565571 sign_in: "Sign in",
566572 bad_credentials: "Incorrect credentials.",
567573 title_add: "Add time",
574+ add_choice_hint: "Track time as it passes, or enter a duration you already spent.",
575+ choice_timer_sub: "You'll pick the category when stopping it.",
576+ choice_manual_sub: "A duration already spent, in two steps.",
577+ back: "← Back",
568578 step1_hint: "Step 1 of 2 — pick the category",
569579 no_categories_yet: "No categories yet — start by creating one.",
570580 manage_categories: "Manage categories",
@@ -768,7 +778,6 @@ pub static DE: Tr = Tr {
768778 member_one: "Mitglied",
769779 member_many: "Mitglieder",
770780 start_timer: "Timer starten",
771- start_timer_aria: "Timer starten",
772781 stop_timer_aria: "Timer stoppen",
773782 timer_running_note: "läuft",
774783 record_measured_aria: "Gemessene Zeit erfassen",
@@ -783,6 +792,10 @@ pub static DE: Tr = Tr {
783792 sign_in: "Anmelden",
784793 bad_credentials: "Zugangsdaten sind falsch.",
785794 title_add: "Zeit erfassen",
795+ add_choice_hint: "Miss die Zeit, während sie läuft, oder erfasse eine bereits verbrachte Dauer.",
796+ choice_timer_sub: "Die Kategorie wählst du beim Stoppen.",
797+ choice_manual_sub: "Eine bereits verbrachte Dauer, in zwei Schritten.",
798+ back: "← Zurück",
786799 step1_hint: "Schritt 1 von 2 — wähle die Kategorie",
787800 no_categories_yet: "Noch keine Kategorien — lege zuerst eine an.",
788801 manage_categories: "Kategorien verwalten",
@@ -73,7 +73,6 @@ pub struct Tr {
73 pub member_one: &'static str,73 pub member_one: &'static str,
74 pub member_many: &'static str,74 pub member_many: &'static str,
75 pub start_timer: &'static str,75 pub start_timer: &'static str,
76- pub start_timer_aria: &'static str,
77 pub stop_timer_aria: &'static str,76 pub stop_timer_aria: &'static str,
78 pub timer_running_note: &'static str,77 pub timer_running_note: &'static str,
79 pub record_measured_aria: &'static str,78 pub record_measured_aria: &'static str,
@@ -90,6 +89,11 @@ pub struct Tr {
90 pub bad_credentials: &'static str,89 pub bad_credentials: &'static str,
91 // ─── ajout ───90 // ─── ajout ───
92 pub title_add: &'static str,91 pub title_add: &'static str,
92+ /// Écran de choix (deux boutons) de l'onglet Ajouter.
93+ pub add_choice_hint: &'static str,
94+ pub choice_timer_sub: &'static str,
95+ pub choice_manual_sub: &'static str,
96+ pub back: &'static str,
93 pub step1_hint: &'static str,97 pub step1_hint: &'static str,
94 pub no_categories_yet: &'static str,98 pub no_categories_yet: &'static str,
95 pub manage_categories: &'static str,99 pub manage_categories: &'static str,
@@ -332,7 +336,6 @@ pub static FR: Tr = Tr {
332 member_one: "membre",336 member_one: "membre",
333 member_many: "membres",337 member_many: "membres",
334 start_timer: "Lancer le minuteur",338 start_timer: "Lancer le minuteur",
335- start_timer_aria: "Démarrer le timer",
336 stop_timer_aria: "Arrêter le timer",339 stop_timer_aria: "Arrêter le timer",
337 timer_running_note: "en cours",340 timer_running_note: "en cours",
338 record_measured_aria: "Enregistrer le temps mesuré",341 record_measured_aria: "Enregistrer le temps mesuré",
@@ -347,6 +350,10 @@ pub static FR: Tr = Tr {
347 sign_in: "Se connecter",350 sign_in: "Se connecter",
348 bad_credentials: "Identifiants incorrects.",351 bad_credentials: "Identifiants incorrects.",
349 title_add: "Ajouter du temps",352 title_add: "Ajouter du temps",
353+ add_choice_hint: "Compte le temps au fur et à mesure, ou saisis une durée que tu as déjà passée.",
354+ choice_timer_sub: "Tu choisiras la catégorie en l'arrêtant.",
355+ choice_manual_sub: "Une durée déjà passée, en deux étapes.",
356+ back: "← Retour",
350 step1_hint: "Étape 1 sur 2 — choisis la catégorie",357 step1_hint: "Étape 1 sur 2 — choisis la catégorie",
351 no_categories_yet: "Aucune catégorie pour l'instant — commence par en créer une.",358 no_categories_yet: "Aucune catégorie pour l'instant — commence par en créer une.",
352 manage_categories: "Gérer les catégories",359 manage_categories: "Gérer les catégories",
@@ -550,7 +557,6 @@ pub static EN: Tr = Tr {
550 member_one: "member",557 member_one: "member",
551 member_many: "members",558 member_many: "members",
552 start_timer: "Start the timer",559 start_timer: "Start the timer",
553- start_timer_aria: "Start timer",
554 stop_timer_aria: "Stop timer",560 stop_timer_aria: "Stop timer",
555 timer_running_note: "running",561 timer_running_note: "running",
556 record_measured_aria: "Record the measured time",562 record_measured_aria: "Record the measured time",
@@ -565,6 +571,10 @@ pub static EN: Tr = Tr {
565 sign_in: "Sign in",571 sign_in: "Sign in",
566 bad_credentials: "Incorrect credentials.",572 bad_credentials: "Incorrect credentials.",
567 title_add: "Add time",573 title_add: "Add time",
574+ add_choice_hint: "Track time as it passes, or enter a duration you already spent.",
575+ choice_timer_sub: "You'll pick the category when stopping it.",
576+ choice_manual_sub: "A duration already spent, in two steps.",
577+ back: "← Back",
568 step1_hint: "Step 1 of 2 — pick the category",578 step1_hint: "Step 1 of 2 — pick the category",
569 no_categories_yet: "No categories yet — start by creating one.",579 no_categories_yet: "No categories yet — start by creating one.",
570 manage_categories: "Manage categories",580 manage_categories: "Manage categories",
@@ -768,7 +778,6 @@ pub static DE: Tr = Tr {
768 member_one: "Mitglied",778 member_one: "Mitglied",
769 member_many: "Mitglieder",779 member_many: "Mitglieder",
770 start_timer: "Timer starten",780 start_timer: "Timer starten",
771- start_timer_aria: "Timer starten",
772 stop_timer_aria: "Timer stoppen",781 stop_timer_aria: "Timer stoppen",
773 timer_running_note: "läuft",782 timer_running_note: "läuft",
774 record_measured_aria: "Gemessene Zeit erfassen",783 record_measured_aria: "Gemessene Zeit erfassen",
@@ -783,6 +792,10 @@ pub static DE: Tr = Tr {
783 sign_in: "Anmelden",792 sign_in: "Anmelden",
784 bad_credentials: "Zugangsdaten sind falsch.",793 bad_credentials: "Zugangsdaten sind falsch.",
785 title_add: "Zeit erfassen",794 title_add: "Zeit erfassen",
795+ add_choice_hint: "Miss die Zeit, während sie läuft, oder erfasse eine bereits verbrachte Dauer.",
796+ choice_timer_sub: "Die Kategorie wählst du beim Stoppen.",
797+ choice_manual_sub: "Eine bereits verbrachte Dauer, in zwei Schritten.",
798+ back: "← Zurück",
786 step1_hint: "Schritt 1 von 2 — wähle die Kategorie",799 step1_hint: "Schritt 1 von 2 — wähle die Kategorie",
787 no_categories_yet: "Noch keine Kategorien — lege zuerst eine an.",800 no_categories_yet: "Noch keine Kategorien — lege zuerst eine an.",
788 manage_categories: "Kategorien verwalten",801 manage_categories: "Kategorien verwalten",
modified src/render.rs +0 -3
@@ -67,9 +67,6 @@ pub struct Shell {
6767 /// Où revenir après un démarrage de timer (page courante, déduite du tab).
6868 pub start_return: &'static str,
6969 pub timer: Option<TimerView>,
70- /// Bandeau « Lancer le minuteur » — seulement sur l'écran Ajouter,
71- /// et seulement si aucun timer ni mesure n'existe (posé par entry_new).
72- pub show_start: bool,
7370 /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en
7471 /// ai qu'une (pill inactive, pas de sheet).
7572 pub teams: Vec<TeamChoiceView>,
@@ -67,9 +67,6 @@ pub struct Shell {
67 /// Où revenir après un démarrage de timer (page courante, déduite du tab).67 /// Où revenir après un démarrage de timer (page courante, déduite du tab).
68 pub start_return: &'static str,68 pub start_return: &'static str,
69 pub timer: Option<TimerView>,69 pub timer: Option<TimerView>,
70- /// Bandeau « Lancer le minuteur » — seulement sur l'écran Ajouter,
71- /// et seulement si aucun timer ni mesure n'existe (posé par entry_new).
72- pub show_start: bool,
73 /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en70 /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en
74 /// ai qu'une (pill inactive, pas de sheet).71 /// ai qu'une (pill inactive, pas de sheet).
75 pub teams: Vec<TeamChoiceView>,72 pub teams: Vec<TeamChoiceView>,
modified src/routes/entries.rs +26 -7
@@ -39,12 +39,22 @@ pub struct MeasuredView {
3939 pub warning: bool,
4040 }
4141
42+/// Atterrissage de l'onglet Ajouter : minuteur ou saisie manuelle.
43+#[derive(Template)]
44+#[template(path = "add_choice.html")]
45+pub struct AddChoicePage {
46+ pub shell: Shell,
47+}
48+
4249 #[derive(Template)]
4350 #[template(path = "add_step1.html")]
4451 pub struct AddStep1Page {
4552 pub shell: Shell,
4653 pub categories: Vec<PickCategory>,
4754 pub measured: Option<MeasuredView>,
55+ /// « ← Retour » vers l'écran de choix — absent quand une mesure attend
56+ /// (on arrive alors directement ici, il n'y a pas de choix à refaire).
57+ pub can_back: bool,
4858 }
4959
5060 /// Écran minuteur — remplace tout l'onglet Ajouter pendant qu'un timer
@@ -93,6 +103,8 @@ pub struct AddStep2Page {
93103 #[derive(Deserialize)]
94104 pub struct NewQuery {
95105 categorie: Option<Uuid>,
106+ /// « manuel » = flux de saisie choisi sur l'écran d'atterrissage.
107+ mode: Option<String>,
96108 minutes: Option<i32>,
97109 date: Option<NaiveDate>,
98110 /// minutes de la saisie qui vient d'aboutir (toast).
@@ -124,9 +136,6 @@ pub async fn entry_new(
124136 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc);
125137
126138 let timer_row = timers::get(&state.pool, ctx.user.id).await?;
127- // Le bandeau « Lancer le minuteur » n'existe que sur cet écran, et
128- // seulement quand rien ne tourne ni n'attend (dans aucune équipe).
129- let can_start = timer_row.is_none();
130139
131140 // Timer en cours dans l'équipe active : l'onglet Ajouter devient
132141 // l'écran minuteur, le formulaire manuel est inaccessible.
@@ -149,6 +158,16 @@ pub async fn entry_new(
149158 // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2.
150159 let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some());
151160
161+ // Atterrissage : choix minuteur / saisie manuelle. Sauté si une mesure
162+ // attend (direction l'étape 1), si le flux manuel est engagé (?mode=)
163+ // ou si une catégorie est déjà choisie (étape 2).
164+ if q.categorie.is_none() && q.mode.is_none() && pending.is_none() {
165+ return Ok(HtmlTemplate(AddChoicePage {
166+ shell: shell(&state, &ctx, tr.title_add, "add", toast).await?,
167+ })
168+ .into_response());
169+ }
170+
152171 // Étape 2 si une catégorie (active) est choisie.
153172 if let Some(picked) = q
154173 .categorie
@@ -180,8 +199,7 @@ pub async fn entry_new(
180199 selected: minutes == m,
181200 })
182201 .collect();
183- let mut sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
184- sh.show_start = can_start;
202+ let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
185203 return Ok(HtmlTemplate(AddStep2Page {
186204 csrf: ctx.user.csrf_token(&state),
187205 shell: sh,
@@ -224,16 +242,17 @@ pub async fn entry_new(
224242 .unwrap_or_default(),
225243 })
226244 .collect();
245+ let can_back = pending.is_none();
227246 let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView {
228247 title: p1(tr.measured_pick_pat, fmt_minutes(m)),
229248 warning: m > 240,
230249 });
231- let mut sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
232- sh.show_start = can_start;
250+ let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
233251 Ok(HtmlTemplate(AddStep1Page {
234252 shell: sh,
235253 categories,
236254 measured,
255+ can_back,
237256 })
238257 .into_response())
239258 }
@@ -39,12 +39,22 @@ pub struct MeasuredView {
39 pub warning: bool,39 pub warning: bool,
40 }40 }
41 41
42+/// Atterrissage de l'onglet Ajouter : minuteur ou saisie manuelle.
43+#[derive(Template)]
44+#[template(path = "add_choice.html")]
45+pub struct AddChoicePage {
46+ pub shell: Shell,
47+}
48+
42 #[derive(Template)]49 #[derive(Template)]
43 #[template(path = "add_step1.html")]50 #[template(path = "add_step1.html")]
44 pub struct AddStep1Page {51 pub struct AddStep1Page {
45 pub shell: Shell,52 pub shell: Shell,
46 pub categories: Vec<PickCategory>,53 pub categories: Vec<PickCategory>,
47 pub measured: Option<MeasuredView>,54 pub measured: Option<MeasuredView>,
55+ /// « ← Retour » vers l'écran de choix — absent quand une mesure attend
56+ /// (on arrive alors directement ici, il n'y a pas de choix à refaire).
57+ pub can_back: bool,
48 }58 }
49 59
50 /// Écran minuteur — remplace tout l'onglet Ajouter pendant qu'un timer60 /// Écran minuteur — remplace tout l'onglet Ajouter pendant qu'un timer
@@ -93,6 +103,8 @@ pub struct AddStep2Page {
93 #[derive(Deserialize)]103 #[derive(Deserialize)]
94 pub struct NewQuery {104 pub struct NewQuery {
95 categorie: Option<Uuid>,105 categorie: Option<Uuid>,
106+ /// « manuel » = flux de saisie choisi sur l'écran d'atterrissage.
107+ mode: Option<String>,
96 minutes: Option<i32>,108 minutes: Option<i32>,
97 date: Option<NaiveDate>,109 date: Option<NaiveDate>,
98 /// minutes de la saisie qui vient d'aboutir (toast).110 /// minutes de la saisie qui vient d'aboutir (toast).
@@ -124,9 +136,6 @@ pub async fn entry_new(
124 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc);136 let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc);
125 137
126 let timer_row = timers::get(&state.pool, ctx.user.id).await?;138 let timer_row = timers::get(&state.pool, ctx.user.id).await?;
127- // Le bandeau « Lancer le minuteur » n'existe que sur cet écran, et
128- // seulement quand rien ne tourne ni n'attend (dans aucune équipe).
129- let can_start = timer_row.is_none();
130 139
131 // Timer en cours dans l'équipe active : l'onglet Ajouter devient140 // Timer en cours dans l'équipe active : l'onglet Ajouter devient
132 // l'écran minuteur, le formulaire manuel est inaccessible.141 // l'écran minuteur, le formulaire manuel est inaccessible.
@@ -149,6 +158,16 @@ pub async fn entry_new(
149 // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2.158 // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2.
150 let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some());159 let pending = timer_row.filter(|t| t.team_id == ctx.team_id && t.stopped_at.is_some());
151 160
161+ // Atterrissage : choix minuteur / saisie manuelle. Sauté si une mesure
162+ // attend (direction l'étape 1), si le flux manuel est engagé (?mode=)
163+ // ou si une catégorie est déjà choisie (étape 2).
164+ if q.categorie.is_none() && q.mode.is_none() && pending.is_none() {
165+ return Ok(HtmlTemplate(AddChoicePage {
166+ shell: shell(&state, &ctx, tr.title_add, "add", toast).await?,
167+ })
168+ .into_response());
169+ }
170+
152 // Étape 2 si une catégorie (active) est choisie.171 // Étape 2 si une catégorie (active) est choisie.
153 if let Some(picked) = q172 if let Some(picked) = q
154 .categorie173 .categorie
@@ -180,8 +199,7 @@ pub async fn entry_new(
180 selected: minutes == m,199 selected: minutes == m,
181 })200 })
182 .collect();201 .collect();
183- let mut sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;202+ let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
184- sh.show_start = can_start;
185 return Ok(HtmlTemplate(AddStep2Page {203 return Ok(HtmlTemplate(AddStep2Page {
186 csrf: ctx.user.csrf_token(&state),204 csrf: ctx.user.csrf_token(&state),
187 shell: sh,205 shell: sh,
@@ -224,16 +242,17 @@ pub async fn entry_new(
224 .unwrap_or_default(),242 .unwrap_or_default(),
225 })243 })
226 .collect();244 .collect();
245+ let can_back = pending.is_none();
227 let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView {246 let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView {
228 title: p1(tr.measured_pick_pat, fmt_minutes(m)),247 title: p1(tr.measured_pick_pat, fmt_minutes(m)),
229 warning: m > 240,248 warning: m > 240,
230 });249 });
231- let mut sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;250+ let sh = shell(&state, &ctx, tr.title_add, "add", toast).await?;
232- sh.show_start = can_start;
233 Ok(HtmlTemplate(AddStep1Page {251 Ok(HtmlTemplate(AddStep1Page {
234 shell: sh,252 shell: sh,
235 categories,253 categories,
236 measured,254 measured,
255+ can_back,
237 })256 })
238 .into_response())257 .into_response())
239 }258 }
modified src/routes/mod.rs +0 -1
@@ -186,7 +186,6 @@ pub async fn shell(
186186 },
187187 todo_count,
188188 timer,
189- show_start: false,
190189 badge,
191190 teams: {
192191 let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?;
@@ -186,7 +186,6 @@ pub async fn shell(
186 },186 },
187 todo_count,187 todo_count,
188 timer,188 timer,
189- show_start: false,
190 badge,189 badge,
191 teams: {190 teams: {
192 let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?;191 let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?;
added templates/add_choice.html +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+{% extends "app.html" %}
2+{% block content %}
3+<p class="choice-hint">{{ shell.tr.add_choice_hint }}</p>
4+<div class="choice-stack">
5+ <form method="post" action="/timer/demarrer" style="margin:0">
6+ <input type="hidden" name="csrf" value="{{ shell.csrf }}">
7+ <input type="hidden" name="retour" value="{{ shell.start_return }}">
8+ <button type="submit" class="choice-card choice-timer">
9+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="13.5" r="7.5"></circle><path d="M12 10v3.5l2.2 1.6"></path><path d="M9.4 2.5h5.2"></path></svg>
10+ <span>
11+ <span class="choice-title">{{ shell.tr.start_timer }}</span>
12+ <span class="choice-sub">{{ shell.tr.choice_timer_sub }}</span>
13+ </span>
14+ </button>
15+ </form>
16+ <a class="choice-card choice-manual" href="/temps/nouveau?mode=manuel">
17+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v8M8 12h8"></path></svg>
18+ <span>
19+ <span class="choice-title">{{ shell.tr.title_add }}</span>
20+ <span class="choice-sub">{{ shell.tr.choice_manual_sub }}</span>
21+ </span>
22+ </a>
23+ <a class="btn btn-ghost choice-manage" href="/parametres/categories">{{ shell.tr.manage_categories }}</a>
24+</div>
25+{% endblock %}
new file mode 100644
@@ -0,0 +1,25 @@
1+{% extends "app.html" %}
2+{% block content %}
3+<p class="choice-hint">{{ shell.tr.add_choice_hint }}</p>
4+<div class="choice-stack">
5+ <form method="post" action="/timer/demarrer" style="margin:0">
6+ <input type="hidden" name="csrf" value="{{ shell.csrf }}">
7+ <input type="hidden" name="retour" value="{{ shell.start_return }}">
8+ <button type="submit" class="choice-card choice-timer">
9+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="13.5" r="7.5"></circle><path d="M12 10v3.5l2.2 1.6"></path><path d="M9.4 2.5h5.2"></path></svg>
10+ <span>
11+ <span class="choice-title">{{ shell.tr.start_timer }}</span>
12+ <span class="choice-sub">{{ shell.tr.choice_timer_sub }}</span>
13+ </span>
14+ </button>
15+ </form>
16+ <a class="choice-card choice-manual" href="/temps/nouveau?mode=manuel">
17+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v8M8 12h8"></path></svg>
18+ <span>
19+ <span class="choice-title">{{ shell.tr.title_add }}</span>
20+ <span class="choice-sub">{{ shell.tr.choice_manual_sub }}</span>
21+ </span>
22+ </a>
23+ <a class="btn btn-ghost choice-manage" href="/parametres/categories">{{ shell.tr.manage_categories }}</a>
24+</div>
25+{% endblock %}
modified templates/add_step1.html +3 -0
@@ -1,5 +1,8 @@
11 {% extends "app.html" %}
22 {% block content %}
3+{% if can_back %}
4+<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">{{ shell.tr.back }}</a>
5+{% endif %}
36 {% if let Some(m) = measured %}
47 <div class="measure-card">
58 <p>{{ m.title }}</p>
@@ -1,5 +1,8 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2 {% block content %}2 {% block content %}
3+{% if can_back %}
4+<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">{{ shell.tr.back }}</a>
5+{% endif %}
3 {% if let Some(m) = measured %}6 {% if let Some(m) = measured %}
4 <div class="measure-card">7 <div class="measure-card">
5 <p>{{ m.title }}</p>8 <p>{{ m.title }}</p>
modified templates/add_step2.html +1 -1
@@ -1,6 +1,6 @@
11 {% extends "app.html" %}
22 {% block content %}
3-<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">{{ shell.tr.change_category }}</a>
3+<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau?mode=manuel">{{ shell.tr.change_category }}</a>
44 <div class="picked">
55 <span class="dot dot-16" style="background:{{ picked_color }}"></span>
66 <span class="pname">{{ picked_name }}</span>
@@ -1,6 +1,6 @@
1 {% extends "app.html" %}1 {% extends "app.html" %}
2 {% block content %}2 {% block content %}
3-<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau">{{ shell.tr.change_category }}</a>3+<a class="btn btn-ghost" style="margin:0 0 var(--space-3);padding-left:0" href="/temps/nouveau?mode=manuel">{{ shell.tr.change_category }}</a>
4 <div class="picked">4 <div class="picked">
5 <span class="dot dot-16" style="background:{{ picked_color }}"></span>5 <span class="dot dot-16" style="background:{{ picked_color }}"></span>
6 <span class="pname">{{ picked_name }}</span>6 <span class="pname">{{ picked_name }}</span>
modified templates/app.html +15 -9
@@ -56,15 +56,6 @@
5656 </a>
5757 </div>
5858 {% endif %}
59-{% else if shell.show_start %}
60-<form class="timer-slot" method="post" action="/timer/demarrer">
61- <input type="hidden" name="csrf" value="{{ shell.csrf }}">
62- <input type="hidden" name="retour" value="{{ shell.start_return }}">
63- <button type="submit" class="timer-banner timer-start" aria-label="{{ shell.tr.start_timer_aria }}">
64- <svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M10.2 8.6l5.6 3.4-5.6 3.4z"></path></svg>
65- <span>{{ shell.tr.start_timer }}</span>
66- </button>
67-</form>
6859 {% endif %}
6960 {% if !shell.teams.is_empty() %}
7061 <div class="sheet-wrap" id="equipes" role="dialog" aria-label="{{ shell.tr.change_team }}">
@@ -96,6 +87,21 @@
9687 {% endif %}
9788 {% if let Some(t) = shell.toast %}<div class="toast" role="status">{{ t }}</div>{% endif %}
9889 <nav class="tabbar">
90+ <span class="rail-brand">
91+ <svg width="30" height="30" viewBox="0 0 96 96" aria-hidden="true">
92+ <circle cx="48" cy="48" r="44" fill="var(--color-surface)"></circle>
93+ <circle cx="48" cy="48" r="40" fill="none" stroke="var(--color-text)" stroke-width="2.4" opacity=".38"></circle>
94+ <circle cx="48" cy="14" r="3.3" fill="var(--color-text)" opacity=".7"></circle>
95+ <circle cx="48" cy="82" r="3.3" fill="var(--color-text)" opacity=".5"></circle>
96+ <circle cx="14" cy="48" r="3.3" fill="var(--color-text)" opacity=".5"></circle>
97+ <circle cx="82" cy="48" r="3.3" fill="var(--color-text)" opacity=".5"></circle>
98+ <rect x="27" y="35.5" width="42" height="10.5" rx="5.25" fill="var(--color-accent)"></rect>
99+ <path d="M53.5 35.5 h10.25 a5.25 5.25 0 0 1 0 10.5 h-10.25 z" fill="var(--color-accent-2)"></path>
100+ <rect x="27" y="50" width="42" height="10.5" rx="5.25" fill="var(--color-accent-2)"></rect>
101+ <path d="M43.5 50 h20.25 a5.25 5.25 0 0 1 0 10.5 h-20.25 z" fill="var(--color-accent)"></path>
102+ </svg>
103+ Ex Ego
104+ </span>
99105 <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}>
100106 <svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v8M8 12h8"></path></svg>
101107 <span>{{ shell.tr.tab_add }}</span>
@@ -56,15 +56,6 @@
56 </a>56 </a>
57 </div>57 </div>
58 {% endif %}58 {% endif %}
59-{% else if shell.show_start %}
60-<form class="timer-slot" method="post" action="/timer/demarrer">
61- <input type="hidden" name="csrf" value="{{ shell.csrf }}">
62- <input type="hidden" name="retour" value="{{ shell.start_return }}">
63- <button type="submit" class="timer-banner timer-start" aria-label="{{ shell.tr.start_timer_aria }}">
64- <svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M10.2 8.6l5.6 3.4-5.6 3.4z"></path></svg>
65- <span>{{ shell.tr.start_timer }}</span>
66- </button>
67-</form>
68 {% endif %}59 {% endif %}
69 {% if !shell.teams.is_empty() %}60 {% if !shell.teams.is_empty() %}
70 <div class="sheet-wrap" id="equipes" role="dialog" aria-label="{{ shell.tr.change_team }}">61 <div class="sheet-wrap" id="equipes" role="dialog" aria-label="{{ shell.tr.change_team }}">
@@ -96,6 +87,21 @@
96 {% endif %}87 {% endif %}
97 {% if let Some(t) = shell.toast %}<div class="toast" role="status">{{ t }}</div>{% endif %}88 {% if let Some(t) = shell.toast %}<div class="toast" role="status">{{ t }}</div>{% endif %}
98 <nav class="tabbar">89 <nav class="tabbar">
90+ <span class="rail-brand">
91+ <svg width="30" height="30" viewBox="0 0 96 96" aria-hidden="true">
92+ <circle cx="48" cy="48" r="44" fill="var(--color-surface)"></circle>
93+ <circle cx="48" cy="48" r="40" fill="none" stroke="var(--color-text)" stroke-width="2.4" opacity=".38"></circle>
94+ <circle cx="48" cy="14" r="3.3" fill="var(--color-text)" opacity=".7"></circle>
95+ <circle cx="48" cy="82" r="3.3" fill="var(--color-text)" opacity=".5"></circle>
96+ <circle cx="14" cy="48" r="3.3" fill="var(--color-text)" opacity=".5"></circle>
97+ <circle cx="82" cy="48" r="3.3" fill="var(--color-text)" opacity=".5"></circle>
98+ <rect x="27" y="35.5" width="42" height="10.5" rx="5.25" fill="var(--color-accent)"></rect>
99+ <path d="M53.5 35.5 h10.25 a5.25 5.25 0 0 1 0 10.5 h-10.25 z" fill="var(--color-accent-2)"></path>
100+ <rect x="27" y="50" width="42" height="10.5" rx="5.25" fill="var(--color-accent-2)"></rect>
101+ <path d="M43.5 50 h20.25 a5.25 5.25 0 0 1 0 10.5 h-20.25 z" fill="var(--color-accent)"></path>
102+ </svg>
103+ Ex Ego
104+ </span>
99 <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}>105 <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}>
100 <svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v8M8 12h8"></path></svg>106 <svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 8v8M8 12h8"></path></svg>
101 <span>{{ shell.tr.tab_add }}</span>107 <span>{{ shell.tr.tab_add }}</span>