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
01c726d parent: a314c3e modified
a11y/audit.mjs +5 -4 | @@ -1,6 +1,6 @@ | ||
| 1 | 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 échoue | |
| 3 | +// Parcourt les 13 écrans de l'app (viewport mobile 390×844) et échoue | |
| 4 | 4 | // (exit 1) à la moindre violation axe « critical » ou « serious » ; |
| 5 | 5 | // les violations moindres sont listées en avertissement. |
| 6 | 6 | // |
| @@ -59,7 +59,7 @@ if (page.url().includes('/login')) { | ||
| 59 | 59 | |
| 60 | 60 | // Une catégorie, une saisie et une todo existantes servent aux pages paramétrées. |
| 61 | 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 | 63 | const times = await fetch('/temps').then((r) => r.text()); |
| 64 | 64 | const todos = await fetch('/taches').then((r) => r.text()); |
| 65 | 65 | return { |
| @@ -71,7 +71,8 @@ const ids = await page.evaluate(async () => { | ||
| 71 | 71 | |
| 72 | 72 | const pages = [ |
| 73 | 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 | 76 | ids.categorie && { name: 'add-step2', url: `/temps/nouveau?categorie=${ids.categorie}` }, |
| 76 | 77 | { name: 'stats', url: '/stats' }, |
| 77 | 78 | { name: 'times', url: '/temps' }, |
| @@ -83,7 +84,7 @@ const pages = [ | ||
| 83 | 84 | { name: 'todo-new', url: '/taches/nouvelle' }, |
| 84 | 85 | ids.todo && { name: 'todo-done', url: `/taches/${ids.todo}/terminer` }, |
| 85 | 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 | 89 | let blocking = 0; |
| 89 | 90 | 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 échoue | 3 | +// 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); } | ||
| 239 | 239 | .timer-banner .t-note { font-size: 13px; font-weight: 400; opacity: .85; } |
| 240 | 240 | .timer-running { background: var(--color-accent-deep); color: var(--color-bg); } |
| 241 | 241 | .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); } | |
| 247 | 267 | |
| 248 | 268 | /* Écran minuteur (onglet Ajouter pendant qu'un timer tourne). */ |
| 249 | 269 | .chrono-card { |
| @@ -293,6 +313,8 @@ a.team-pill:hover { filter: brightness(.96); } | ||
| 293 | 313 | border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg); |
| 294 | 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 | 318 | .tab { |
| 297 | 319 | flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; |
| 298 | 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 | 670 | .sheet { animation: none; } |
| 649 | 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 | +} | |
| @@ -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 { | ||
| 73 | 73 | pub member_one: &'static str, |
| 74 | 74 | pub member_many: &'static str, |
| 75 | 75 | pub start_timer: &'static str, |
| 76 | - pub start_timer_aria: &'static str, | |
| 77 | 76 | pub stop_timer_aria: &'static str, |
| 78 | 77 | pub timer_running_note: &'static str, |
| 79 | 78 | pub record_measured_aria: &'static str, |
| @@ -90,6 +89,11 @@ pub struct Tr { | ||
| 90 | 89 | pub bad_credentials: &'static str, |
| 91 | 90 | // ─── ajout ─── |
| 92 | 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 | 97 | pub step1_hint: &'static str, |
| 94 | 98 | pub no_categories_yet: &'static str, |
| 95 | 99 | pub manage_categories: &'static str, |
| @@ -332,7 +336,6 @@ pub static FR: Tr = Tr { | ||
| 332 | 336 | member_one: "membre", |
| 333 | 337 | member_many: "membres", |
| 334 | 338 | start_timer: "Lancer le minuteur", |
| 335 | - start_timer_aria: "Démarrer le timer", | |
| 336 | 339 | stop_timer_aria: "Arrêter le timer", |
| 337 | 340 | timer_running_note: "en cours", |
| 338 | 341 | record_measured_aria: "Enregistrer le temps mesuré", |
| @@ -347,6 +350,10 @@ pub static FR: Tr = Tr { | ||
| 347 | 350 | sign_in: "Se connecter", |
| 348 | 351 | bad_credentials: "Identifiants incorrects.", |
| 349 | 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 | 357 | step1_hint: "Étape 1 sur 2 — choisis la catégorie", |
| 351 | 358 | no_categories_yet: "Aucune catégorie pour l'instant — commence par en créer une.", |
| 352 | 359 | manage_categories: "Gérer les catégories", |
| @@ -550,7 +557,6 @@ pub static EN: Tr = Tr { | ||
| 550 | 557 | member_one: "member", |
| 551 | 558 | member_many: "members", |
| 552 | 559 | start_timer: "Start the timer", |
| 553 | - start_timer_aria: "Start timer", | |
| 554 | 560 | stop_timer_aria: "Stop timer", |
| 555 | 561 | timer_running_note: "running", |
| 556 | 562 | record_measured_aria: "Record the measured time", |
| @@ -565,6 +571,10 @@ pub static EN: Tr = Tr { | ||
| 565 | 571 | sign_in: "Sign in", |
| 566 | 572 | bad_credentials: "Incorrect credentials.", |
| 567 | 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 | 578 | step1_hint: "Step 1 of 2 — pick the category", |
| 569 | 579 | no_categories_yet: "No categories yet — start by creating one.", |
| 570 | 580 | manage_categories: "Manage categories", |
| @@ -768,7 +778,6 @@ pub static DE: Tr = Tr { | ||
| 768 | 778 | member_one: "Mitglied", |
| 769 | 779 | member_many: "Mitglieder", |
| 770 | 780 | start_timer: "Timer starten", |
| 771 | - start_timer_aria: "Timer starten", | |
| 772 | 781 | stop_timer_aria: "Timer stoppen", |
| 773 | 782 | timer_running_note: "läuft", |
| 774 | 783 | record_measured_aria: "Gemessene Zeit erfassen", |
| @@ -783,6 +792,10 @@ pub static DE: Tr = Tr { | ||
| 783 | 792 | sign_in: "Anmelden", |
| 784 | 793 | bad_credentials: "Zugangsdaten sind falsch.", |
| 785 | 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 | 799 | step1_hint: "Schritt 1 von 2 — wähle die Kategorie", |
| 787 | 800 | no_categories_yet: "Noch keine Kategorien — lege zuerst eine an.", |
| 788 | 801 | 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 { | ||
| 67 | 67 | /// Où revenir après un démarrage de timer (page courante, déduite du tab). |
| 68 | 68 | pub start_return: &'static str, |
| 69 | 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 | 70 | /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en |
| 74 | 71 | /// ai qu'une (pill inactive, pas de sheet). |
| 75 | 72 | 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'en | 70 | /// 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 { | ||
| 39 | 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 | 49 | #[derive(Template)] |
| 43 | 50 | #[template(path = "add_step1.html")] |
| 44 | 51 | pub struct AddStep1Page { |
| 45 | 52 | pub shell: Shell, |
| 46 | 53 | pub categories: Vec<PickCategory>, |
| 47 | 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 | 60 | /// Écran minuteur — remplace tout l'onglet Ajouter pendant qu'un timer |
| @@ -93,6 +103,8 @@ pub struct AddStep2Page { | ||
| 93 | 103 | #[derive(Deserialize)] |
| 94 | 104 | pub struct NewQuery { |
| 95 | 105 | categorie: Option<Uuid>, |
| 106 | + /// « manuel » = flux de saisie choisi sur l'écran d'atterrissage. | |
| 107 | + mode: Option<String>, | |
| 96 | 108 | minutes: Option<i32>, |
| 97 | 109 | date: Option<NaiveDate>, |
| 98 | 110 | /// minutes de la saisie qui vient d'aboutir (toast). |
| @@ -124,9 +136,6 @@ pub async fn entry_new( | ||
| 124 | 136 | let colors = color_map(&all_tasks.iter().map(|t| t.id).collect::<Vec<_>>(), ctx.hc); |
| 125 | 137 | |
| 126 | 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 | 140 | // Timer en cours dans l'équipe active : l'onglet Ajouter devient |
| 132 | 141 | // l'écran minuteur, le formulaire manuel est inaccessible. |
| @@ -149,6 +158,16 @@ pub async fn entry_new( | ||
| 149 | 158 | // Mesure de timer figée dans l'équipe active : elle préremplit l'étape 2. |
| 150 | 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 | 171 | // Étape 2 si une catégorie (active) est choisie. |
| 153 | 172 | if let Some(picked) = q |
| 154 | 173 | .categorie |
| @@ -180,8 +199,7 @@ pub async fn entry_new( | ||
| 180 | 199 | selected: minutes == m, |
| 181 | 200 | }) |
| 182 | 201 | .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?; | |
| 185 | 203 | return Ok(HtmlTemplate(AddStep2Page { |
| 186 | 204 | csrf: ctx.user.csrf_token(&state), |
| 187 | 205 | shell: sh, |
| @@ -224,16 +242,17 @@ pub async fn entry_new( | ||
| 224 | 242 | .unwrap_or_default(), |
| 225 | 243 | }) |
| 226 | 244 | .collect(); |
| 245 | + let can_back = pending.is_none(); | |
| 227 | 246 | let measured = pending.and_then(|t| t.measured_minutes()).map(|m| MeasuredView { |
| 228 | 247 | title: p1(tr.measured_pick_pat, fmt_minutes(m)), |
| 229 | 248 | warning: m > 240, |
| 230 | 249 | }); |
| 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?; | |
| 233 | 251 | Ok(HtmlTemplate(AddStep1Page { |
| 234 | 252 | shell: sh, |
| 235 | 253 | categories, |
| 236 | 254 | measured, |
| 255 | + can_back, | |
| 237 | 256 | }) |
| 238 | 257 | .into_response()) |
| 239 | 258 | } |
| @@ -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 timer | 60 | /// É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 devient | 140 | // 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) = q | 172 | if let Some(picked) = q |
| 154 | .categorie | 173 | .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( | ||
| 186 | 186 | }, |
| 187 | 187 | todo_count, |
| 188 | 188 | timer, |
| 189 | - show_start: false, | |
| 190 | 189 | badge, |
| 191 | 190 | teams: { |
| 192 | 191 | 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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | 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 | 6 | {% if let Some(m) = measured %} |
| 4 | 7 | <div class="measure-card"> |
| 5 | 8 | <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 @@ | ||
| 1 | 1 | {% extends "app.html" %} |
| 2 | 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 | 4 | <div class="picked"> |
| 5 | 5 | <span class="dot dot-16" style="background:{{ picked_color }}"></span> |
| 6 | 6 | <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 @@ | ||
| 56 | 56 | </a> |
| 57 | 57 | </div> |
| 58 | 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 | 59 | {% endif %} |
| 69 | 60 | {% if !shell.teams.is_empty() %} |
| 70 | 61 | <div class="sheet-wrap" id="equipes" role="dialog" aria-label="{{ shell.tr.change_team }}"> |
| @@ -96,6 +87,21 @@ | ||
| 96 | 87 | {% endif %} |
| 97 | 88 | {% if let Some(t) = shell.toast %}<div class="toast" role="status">{{ t }}</div>{% endif %} |
| 98 | 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 | 105 | <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}> |
| 100 | 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 | 107 | <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> |