feat(équipes): la pill d'en-tête devient sélecteur — sheet :target « Changer d'équipe », bascule avec retour, section retirée des RéglagesUnverified
ea3b762 parent: d3e8a4f modified
assets/style.css +3 -0 | @@ -153,6 +153,9 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | ||
| 153 | 153 | padding: 4px 11px 4px 8px; border-radius: 999px; |
| 154 | 154 | background: var(--color-accent-2-200); color: var(--color-accent-2-900); |
| 155 | 155 | } |
| 156 | +/* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */ | |
| 157 | +a.team-pill { text-decoration: none; cursor: pointer; } | |
| 158 | +a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-200)); } | |
| 156 | 159 | .app-topline .who { margin-left: auto; font-size: 12px; opacity: .6; } |
| 157 | 160 | .app-header h1 { margin: 0; font-size: 29px; letter-spacing: -0.02em; } |
| 158 | 161 | |
| @@ -153,6 +153,9 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; } | |||
| 153 | padding: 4px 11px 4px 8px; border-radius: 999px; | 153 | padding: 4px 11px 4px 8px; border-radius: 999px; |
| 154 | background: var(--color-accent-2-200); color: var(--color-accent-2-900); | 154 | background: var(--color-accent-2-200); color: var(--color-accent-2-900); |
| 155 | } | 155 | } |
| 156 | +/* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */ | ||
| 157 | +a.team-pill { text-decoration: none; cursor: pointer; } | ||
| 158 | +a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-200)); } | ||
| 156 | .app-topline .who { margin-left: auto; font-size: 12px; opacity: .6; } | 159 | .app-topline .who { margin-left: auto; font-size: 12px; opacity: .6; } |
| 157 | .app-header h1 { margin: 0; font-size: 29px; letter-spacing: -0.02em; } | 160 | .app-header h1 { margin: 0; font-size: 29px; letter-spacing: -0.02em; } |
| 158 | 161 | ||
modified
src/render.rs +12 -0 | @@ -16,6 +16,15 @@ impl<T: Template> IntoResponse for HtmlTemplate<T> { | ||
| 16 | 16 | } |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | +/// Entrée du sélecteur d'équipe (sheet ouverte par la pill de l'en-tête). | |
| 20 | +pub struct TeamChoiceView { | |
| 21 | + pub id: uuid::Uuid, | |
| 22 | + pub name: String, | |
| 23 | + /// « 2 membres » | |
| 24 | + pub meta: String, | |
| 25 | + pub current: bool, | |
| 26 | +} | |
| 27 | + | |
| 19 | 28 | /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶). |
| 20 | 29 | pub struct TimerView { |
| 21 | 30 | /// Timer en cours (sinon : mesure figée en attente d'enregistrement). |
| @@ -49,4 +58,7 @@ pub struct Shell { | ||
| 49 | 58 | /// Bandeau « Lancer le minuteur » — seulement sur l'écran Ajouter, |
| 50 | 59 | /// et seulement si aucun timer ni mesure n'existe (posé par entry_new). |
| 51 | 60 | pub show_start: bool, |
| 61 | + /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en | |
| 62 | + /// ai qu'une (pill inactive, pas de sheet). | |
| 63 | + pub teams: Vec<TeamChoiceView>, | |
| 52 | 64 | } |
| @@ -16,6 +16,15 @@ impl<T: Template> IntoResponse for HtmlTemplate<T> { | |||
| 16 | } | 16 | } |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | +/// Entrée du sélecteur d'équipe (sheet ouverte par la pill de l'en-tête). | ||
| 20 | +pub struct TeamChoiceView { | ||
| 21 | + pub id: uuid::Uuid, | ||
| 22 | + pub name: String, | ||
| 23 | + /// « 2 membres » | ||
| 24 | + pub meta: String, | ||
| 25 | + pub current: bool, | ||
| 26 | +} | ||
| 27 | + | ||
| 19 | /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶). | 28 | /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶). |
| 20 | pub struct TimerView { | 29 | pub struct TimerView { |
| 21 | /// Timer en cours (sinon : mesure figée en attente d'enregistrement). | 30 | /// Timer en cours (sinon : mesure figée en attente d'enregistrement). |
| @@ -49,4 +58,7 @@ pub struct Shell { | |||
| 49 | /// Bandeau « Lancer le minuteur » — seulement sur l'écran Ajouter, | 58 | /// Bandeau « Lancer le minuteur » — seulement sur l'écran Ajouter, |
| 50 | /// et seulement si aucun timer ni mesure n'existe (posé par entry_new). | 59 | /// et seulement si aucun timer ni mesure n'existe (posé par entry_new). |
| 51 | pub show_start: bool, | 60 | pub show_start: bool, |
| 61 | + /// Mes équipes pour le sélecteur de la pill d'en-tête — vide si je n'en | ||
| 62 | + /// ai qu'une (pill inactive, pas de sheet). | ||
| 63 | + pub teams: Vec<TeamChoiceView>, | ||
| 52 | } | 64 | } |
modified
src/routes/mod.rs +17 -2 | @@ -15,9 +15,9 @@ use axum::routing::{get, post}; | ||
| 15 | 15 | |
| 16 | 16 | use crate::auth::extractor::AuthUser; |
| 17 | 17 | use crate::charts::fmt_minutes; |
| 18 | -use crate::db::{invitations, timers, users}; | |
| 18 | +use crate::db::{invitations, teams as teams_db, timers, users}; | |
| 19 | 19 | use crate::error::AppError; |
| 20 | -use crate::render::{Shell, TimerView}; | |
| 20 | +use crate::render::{Shell, TeamChoiceView, TimerView}; | |
| 21 | 21 | use crate::state::AppState; |
| 22 | 22 | use crate::team::ActiveTeam; |
| 23 | 23 | |
| @@ -165,5 +165,20 @@ pub async fn shell( | ||
| 165 | 165 | }, |
| 166 | 166 | timer, |
| 167 | 167 | show_start: false, |
| 168 | + teams: { | |
| 169 | + let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?; | |
| 170 | + if mine.len() > 1 { | |
| 171 | + mine.into_iter() | |
| 172 | + .map(|t| TeamChoiceView { | |
| 173 | + current: t.id == ctx.team_id, | |
| 174 | + meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }), | |
| 175 | + id: t.id, | |
| 176 | + name: t.name, | |
| 177 | + }) | |
| 178 | + .collect() | |
| 179 | + } else { | |
| 180 | + Vec::new() | |
| 181 | + } | |
| 182 | + }, | |
| 168 | 183 | }) |
| 169 | 184 | } |
| @@ -15,9 +15,9 @@ use axum::routing::{get, post}; | |||
| 15 | 15 | ||
| 16 | use crate::auth::extractor::AuthUser; | 16 | use crate::auth::extractor::AuthUser; |
| 17 | use crate::charts::fmt_minutes; | 17 | use crate::charts::fmt_minutes; |
| 18 | -use crate::db::{invitations, timers, users}; | 18 | +use crate::db::{invitations, teams as teams_db, timers, users}; |
| 19 | use crate::error::AppError; | 19 | use crate::error::AppError; |
| 20 | -use crate::render::{Shell, TimerView}; | 20 | +use crate::render::{Shell, TeamChoiceView, TimerView}; |
| 21 | use crate::state::AppState; | 21 | use crate::state::AppState; |
| 22 | use crate::team::ActiveTeam; | 22 | use crate::team::ActiveTeam; |
| 23 | 23 | ||
| @@ -165,5 +165,20 @@ pub async fn shell( | |||
| 165 | }, | 165 | }, |
| 166 | timer, | 166 | timer, |
| 167 | show_start: false, | 167 | show_start: false, |
| 168 | + teams: { | ||
| 169 | + let mine = teams_db::teams_of(&state.pool, ctx.user.id).await?; | ||
| 170 | + if mine.len() > 1 { | ||
| 171 | + mine.into_iter() | ||
| 172 | + .map(|t| TeamChoiceView { | ||
| 173 | + current: t.id == ctx.team_id, | ||
| 174 | + meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }), | ||
| 175 | + id: t.id, | ||
| 176 | + name: t.name, | ||
| 177 | + }) | ||
| 178 | + .collect() | ||
| 179 | + } else { | ||
| 180 | + Vec::new() | ||
| 181 | + } | ||
| 182 | + }, | ||
| 168 | }) | 183 | }) |
| 169 | } | 184 | } |
modified
src/routes/settings.rs +4 -25 | @@ -22,13 +22,6 @@ pub struct InviteView { | ||
| 22 | 22 | pub refuse_action: String, |
| 23 | 23 | } |
| 24 | 24 | |
| 25 | -pub struct TeamChoice { | |
| 26 | - pub id: Uuid, | |
| 27 | - pub name: String, | |
| 28 | - pub meta: String, | |
| 29 | - pub current: bool, | |
| 30 | -} | |
| 31 | - | |
| 32 | 25 | pub struct SentView { |
| 33 | 26 | pub name: String, |
| 34 | 27 | pub meta: String, |
| @@ -44,7 +37,6 @@ pub struct SettingsPage { | ||
| 44 | 37 | pub error: Option<&'static str>, |
| 45 | 38 | pub invites: Vec<InviteView>, |
| 46 | 39 | pub account_line: String, |
| 47 | - pub my_teams: Vec<TeamChoice>, | |
| 48 | 40 | pub members: Vec<String>, |
| 49 | 41 | /// Erreur inline du bloc invitation. |
| 50 | 42 | pub invite_error: Option<String>, |
| @@ -105,22 +97,10 @@ pub async fn settings_page( | ||
| 105 | 97 | }) |
| 106 | 98 | .collect(); |
| 107 | 99 | |
| 108 | - let my_teams: Vec<TeamChoice> = teams::teams_of(&state.pool, ctx.user.id) | |
| 109 | - .await? | |
| 110 | - .into_iter() | |
| 111 | - .map(|t| TeamChoice { | |
| 112 | - current: t.id == ctx.team_id, | |
| 113 | - meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }), | |
| 114 | - id: t.id, | |
| 115 | - name: t.name, | |
| 116 | - }) | |
| 117 | - .collect(); | |
| 118 | - let can_leave = my_teams.len() > 1; | |
| 119 | - let account_line = if my_teams.len() > 1 { | |
| 120 | - format!( | |
| 121 | - "Membre de {} équipes. Les comptes sont créés en ligne de commande.", | |
| 122 | - my_teams.len() | |
| 123 | - ) | |
| 100 | + let team_count = teams::teams_of(&state.pool, ctx.user.id).await?.len(); | |
| 101 | + let can_leave = team_count > 1; | |
| 102 | + let account_line = if team_count > 1 { | |
| 103 | + format!("Membre de {team_count} équipes. Les comptes sont créés en ligne de commande.") | |
| 124 | 104 | } else { |
| 125 | 105 | "Les comptes sont créés en ligne de commande.".to_string() |
| 126 | 106 | }; |
| @@ -147,7 +127,6 @@ pub async fn settings_page( | ||
| 147 | 127 | error, |
| 148 | 128 | invites, |
| 149 | 129 | account_line, |
| 150 | - my_teams, | |
| 151 | 130 | members, |
| 152 | 131 | invite_error, |
| 153 | 132 | sent, |
| @@ -22,13 +22,6 @@ pub struct InviteView { | |||
| 22 | pub refuse_action: String, | 22 | pub refuse_action: String, |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | -pub struct TeamChoice { | ||
| 26 | - pub id: Uuid, | ||
| 27 | - pub name: String, | ||
| 28 | - pub meta: String, | ||
| 29 | - pub current: bool, | ||
| 30 | -} | ||
| 31 | - | ||
| 32 | pub struct SentView { | 25 | pub struct SentView { |
| 33 | pub name: String, | 26 | pub name: String, |
| 34 | pub meta: String, | 27 | pub meta: String, |
| @@ -44,7 +37,6 @@ pub struct SettingsPage { | |||
| 44 | pub error: Option<&'static str>, | 37 | pub error: Option<&'static str>, |
| 45 | pub invites: Vec<InviteView>, | 38 | pub invites: Vec<InviteView>, |
| 46 | pub account_line: String, | 39 | pub account_line: String, |
| 47 | - pub my_teams: Vec<TeamChoice>, | ||
| 48 | pub members: Vec<String>, | 40 | pub members: Vec<String>, |
| 49 | /// Erreur inline du bloc invitation. | 41 | /// Erreur inline du bloc invitation. |
| 50 | pub invite_error: Option<String>, | 42 | pub invite_error: Option<String>, |
| @@ -105,22 +97,10 @@ pub async fn settings_page( | |||
| 105 | }) | 97 | }) |
| 106 | .collect(); | 98 | .collect(); |
| 107 | 99 | ||
| 108 | - let my_teams: Vec<TeamChoice> = teams::teams_of(&state.pool, ctx.user.id) | 100 | + let team_count = teams::teams_of(&state.pool, ctx.user.id).await?.len(); |
| 109 | - .await? | 101 | + let can_leave = team_count > 1; |
| 110 | - .into_iter() | 102 | + let account_line = if team_count > 1 { |
| 111 | - .map(|t| TeamChoice { | 103 | + format!("Membre de {team_count} équipes. Les comptes sont créés en ligne de commande.") |
| 112 | - current: t.id == ctx.team_id, | ||
| 113 | - meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }), | ||
| 114 | - id: t.id, | ||
| 115 | - name: t.name, | ||
| 116 | - }) | ||
| 117 | - .collect(); | ||
| 118 | - let can_leave = my_teams.len() > 1; | ||
| 119 | - let account_line = if my_teams.len() > 1 { | ||
| 120 | - format!( | ||
| 121 | - "Membre de {} équipes. Les comptes sont créés en ligne de commande.", | ||
| 122 | - my_teams.len() | ||
| 123 | - ) | ||
| 124 | } else { | 104 | } else { |
| 125 | "Les comptes sont créés en ligne de commande.".to_string() | 105 | "Les comptes sont créés en ligne de commande.".to_string() |
| 126 | }; | 106 | }; |
| @@ -147,7 +127,6 @@ pub async fn settings_page( | |||
| 147 | error, | 127 | error, |
| 148 | invites, | 128 | invites, |
| 149 | account_line, | 129 | account_line, |
| 150 | - my_teams, | ||
| 151 | members, | 130 | members, |
| 152 | invite_error, | 131 | invite_error, |
| 153 | sent, | 132 | sent, |
modified
src/routes/teams.rs +8 -1 | @@ -65,6 +65,8 @@ pub async fn team_create( | ||
| 65 | 65 | pub struct SwitchForm { |
| 66 | 66 | equipe: Uuid, |
| 67 | 67 | csrf: String, |
| 68 | + /// Page d'origine du sélecteur (la sheet est présente sur tous les écrans). | |
| 69 | + retour: Option<String>, | |
| 68 | 70 | } |
| 69 | 71 | |
| 70 | 72 | /// POST /equipes/basculer — change l'équipe active (cookie par appareil). |
| @@ -83,7 +85,12 @@ pub async fn team_switch( | ||
| 83 | 85 | { |
| 84 | 86 | return Err(AppError::NotFound); |
| 85 | 87 | } |
| 86 | - Ok((jar.add(team_cookie(form.equipe)), Redirect::to("/parametres")).into_response()) | |
| 88 | + let back = form | |
| 89 | + .retour | |
| 90 | + .as_deref() | |
| 91 | + .filter(|p| p.starts_with('/') && !p.starts_with("//")) | |
| 92 | + .unwrap_or("/parametres"); | |
| 93 | + Ok((jar.add(team_cookie(form.equipe)), Redirect::to(back)).into_response()) | |
| 87 | 94 | } |
| 88 | 95 | |
| 89 | 96 | /// POST /equipes/renommer — tout membre peut renommer l'équipe active. |
| @@ -65,6 +65,8 @@ pub async fn team_create( | |||
| 65 | pub struct SwitchForm { | 65 | pub struct SwitchForm { |
| 66 | equipe: Uuid, | 66 | equipe: Uuid, |
| 67 | csrf: String, | 67 | csrf: String, |
| 68 | + /// Page d'origine du sélecteur (la sheet est présente sur tous les écrans). | ||
| 69 | + retour: Option<String>, | ||
| 68 | } | 70 | } |
| 69 | 71 | ||
| 70 | /// POST /equipes/basculer — change l'équipe active (cookie par appareil). | 72 | /// POST /equipes/basculer — change l'équipe active (cookie par appareil). |
| @@ -83,7 +85,12 @@ pub async fn team_switch( | |||
| 83 | { | 85 | { |
| 84 | return Err(AppError::NotFound); | 86 | return Err(AppError::NotFound); |
| 85 | } | 87 | } |
| 86 | - Ok((jar.add(team_cookie(form.equipe)), Redirect::to("/parametres")).into_response()) | 88 | + let back = form |
| 89 | + .retour | ||
| 90 | + .as_deref() | ||
| 91 | + .filter(|p| p.starts_with('/') && !p.starts_with("//")) | ||
| 92 | + .unwrap_or("/parametres"); | ||
| 93 | + Ok((jar.add(team_cookie(form.equipe)), Redirect::to(back)).into_response()) | ||
| 87 | } | 94 | } |
| 88 | 95 | ||
| 89 | /// POST /equipes/renommer — tout membre peut renommer l'équipe active. | 96 | /// POST /equipes/renommer — tout membre peut renommer l'équipe active. |
modified
templates/app.html +36 -0 | @@ -3,10 +3,18 @@ | ||
| 3 | 3 | {% block body %} |
| 4 | 4 | <header class="app-header"> |
| 5 | 5 | <div class="app-topline"> |
| 6 | + {% if !shell.teams.is_empty() %} | |
| 7 | + <a class="team-pill" href="#equipes" aria-label="Changer d'équipe"> | |
| 8 | + <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="3.2"></circle><path d="M22 20v-2a4 4 0 0 0-3-3.8"></path><path d="M16.5 4.2a4 4 0 0 1 0 5.6"></path></svg> | |
| 9 | + {{ shell.team_name }} | |
| 10 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"></path></svg> | |
| 11 | + </a> | |
| 12 | + {% else %} | |
| 6 | 13 | <span class="team-pill"> |
| 7 | 14 | <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="3.2"></circle><path d="M22 20v-2a4 4 0 0 0-3-3.8"></path><path d="M16.5 4.2a4 4 0 0 1 0 5.6"></path></svg> |
| 8 | 15 | {{ shell.team_name }} |
| 9 | 16 | </span> |
| 17 | + {% endif %} | |
| 10 | 18 | <span class="who">{{ shell.username }}</span> |
| 11 | 19 | </div> |
| 12 | 20 | <h1>{{ shell.title }}</h1> |
| @@ -51,6 +59,34 @@ | ||
| 51 | 59 | </button> |
| 52 | 60 | </form> |
| 53 | 61 | {% endif %} |
| 62 | +{% if !shell.teams.is_empty() %} | |
| 63 | +<div class="sheet-wrap" id="equipes"> | |
| 64 | + <a class="sheet-backdrop" href="#fermer" aria-label="Fermer"></a> | |
| 65 | + <div class="sheet"> | |
| 66 | + <div class="sheet-head"> | |
| 67 | + <span class="pname">Changer d'équipe</span> | |
| 68 | + <a class="btn btn-ghost close" href="#fermer">Fermer</a> | |
| 69 | + </div> | |
| 70 | + <div style="display:grid;gap:var(--space-2)"> | |
| 71 | + {% for t in shell.teams %} | |
| 72 | + <form method="post" action="/equipes/basculer" style="margin:0"> | |
| 73 | + <input type="hidden" name="csrf" value="{{ shell.csrf }}"> | |
| 74 | + <input type="hidden" name="equipe" value="{{ t.id }}"> | |
| 75 | + <input type="hidden" name="retour" value="{{ shell.start_return }}"> | |
| 76 | + <button type="submit" class="team-choice{% if t.current %} current{% endif %}"> | |
| 77 | + <span class="team-dot"></span> | |
| 78 | + <span style="flex:1;text-align:left"> | |
| 79 | + <span style="display:block;font-size:16px">{{ t.name }}</span> | |
| 80 | + <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span> | |
| 81 | + </span> | |
| 82 | + {% if t.current %}<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"></path></svg>{% endif %} | |
| 83 | + </button> | |
| 84 | + </form> | |
| 85 | + {% endfor %} | |
| 86 | + </div> | |
| 87 | + </div> | |
| 88 | +</div> | |
| 89 | +{% endif %} | |
| 54 | 90 | {% if let Some(t) = shell.toast %}<div class="toast">{{ t }}</div>{% endif %} |
| 55 | 91 | <nav class="tabbar"> |
| 56 | 92 | <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}> |
| @@ -3,10 +3,18 @@ | |||
| 3 | {% block body %} | 3 | {% block body %} |
| 4 | <header class="app-header"> | 4 | <header class="app-header"> |
| 5 | <div class="app-topline"> | 5 | <div class="app-topline"> |
| 6 | + {% if !shell.teams.is_empty() %} | ||
| 7 | + <a class="team-pill" href="#equipes" aria-label="Changer d'équipe"> | ||
| 8 | + <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="3.2"></circle><path d="M22 20v-2a4 4 0 0 0-3-3.8"></path><path d="M16.5 4.2a4 4 0 0 1 0 5.6"></path></svg> | ||
| 9 | + {{ shell.team_name }} | ||
| 10 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"></path></svg> | ||
| 11 | + </a> | ||
| 12 | + {% else %} | ||
| 6 | <span class="team-pill"> | 13 | <span class="team-pill"> |
| 7 | <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="3.2"></circle><path d="M22 20v-2a4 4 0 0 0-3-3.8"></path><path d="M16.5 4.2a4 4 0 0 1 0 5.6"></path></svg> | 14 | <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="3.2"></circle><path d="M22 20v-2a4 4 0 0 0-3-3.8"></path><path d="M16.5 4.2a4 4 0 0 1 0 5.6"></path></svg> |
| 8 | {{ shell.team_name }} | 15 | {{ shell.team_name }} |
| 9 | </span> | 16 | </span> |
| 17 | + {% endif %} | ||
| 10 | <span class="who">{{ shell.username }}</span> | 18 | <span class="who">{{ shell.username }}</span> |
| 11 | </div> | 19 | </div> |
| 12 | <h1>{{ shell.title }}</h1> | 20 | <h1>{{ shell.title }}</h1> |
| @@ -51,6 +59,34 @@ | |||
| 51 | </button> | 59 | </button> |
| 52 | </form> | 60 | </form> |
| 53 | {% endif %} | 61 | {% endif %} |
| 62 | +{% if !shell.teams.is_empty() %} | ||
| 63 | +<div class="sheet-wrap" id="equipes"> | ||
| 64 | + <a class="sheet-backdrop" href="#fermer" aria-label="Fermer"></a> | ||
| 65 | + <div class="sheet"> | ||
| 66 | + <div class="sheet-head"> | ||
| 67 | + <span class="pname">Changer d'équipe</span> | ||
| 68 | + <a class="btn btn-ghost close" href="#fermer">Fermer</a> | ||
| 69 | + </div> | ||
| 70 | + <div style="display:grid;gap:var(--space-2)"> | ||
| 71 | + {% for t in shell.teams %} | ||
| 72 | + <form method="post" action="/equipes/basculer" style="margin:0"> | ||
| 73 | + <input type="hidden" name="csrf" value="{{ shell.csrf }}"> | ||
| 74 | + <input type="hidden" name="equipe" value="{{ t.id }}"> | ||
| 75 | + <input type="hidden" name="retour" value="{{ shell.start_return }}"> | ||
| 76 | + <button type="submit" class="team-choice{% if t.current %} current{% endif %}"> | ||
| 77 | + <span class="team-dot"></span> | ||
| 78 | + <span style="flex:1;text-align:left"> | ||
| 79 | + <span style="display:block;font-size:16px">{{ t.name }}</span> | ||
| 80 | + <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span> | ||
| 81 | + </span> | ||
| 82 | + {% if t.current %}<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"></path></svg>{% endif %} | ||
| 83 | + </button> | ||
| 84 | + </form> | ||
| 85 | + {% endfor %} | ||
| 86 | + </div> | ||
| 87 | + </div> | ||
| 88 | +</div> | ||
| 89 | +{% endif %} | ||
| 54 | {% if let Some(t) = shell.toast %}<div class="toast">{{ t }}</div>{% endif %} | 90 | {% if let Some(t) = shell.toast %}<div class="toast">{{ t }}</div>{% endif %} |
| 55 | <nav class="tabbar"> | 91 | <nav class="tabbar"> |
| 56 | <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}> | 92 | <a class="tab" href="/temps/nouveau" {% if shell.tab == "add" %}aria-current="page"{% endif %}> |
modified
templates/settings.html +0 -19 | @@ -27,25 +27,6 @@ | ||
| 27 | 27 | <p class="card-body">{{ account_line }}</p> |
| 28 | 28 | </div> |
| 29 | 29 | |
| 30 | -<h4 style="margin:0 0 var(--space-2)">Équipe active</h4> | |
| 31 | -<p style="font-size:13px;opacity:.6;margin:0 0 var(--space-3)">Tâches, temps et statistiques sont propres à l'équipe active.</p> | |
| 32 | -<div style="display:grid;gap:var(--space-2);margin-bottom:var(--space-4)"> | |
| 33 | - {% for t in my_teams %} | |
| 34 | - <form method="post" action="/equipes/basculer" style="margin:0"> | |
| 35 | - <input type="hidden" name="csrf" value="{{ csrf }}"> | |
| 36 | - <input type="hidden" name="equipe" value="{{ t.id }}"> | |
| 37 | - <button type="submit" class="team-choice{% if t.current %} current{% endif %}"> | |
| 38 | - <span class="team-dot"></span> | |
| 39 | - <span style="flex:1;text-align:left"> | |
| 40 | - <span style="display:block;font-size:16px">{{ t.name }}</span> | |
| 41 | - <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span> | |
| 42 | - </span> | |
| 43 | - {% if t.current %}<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"></path></svg>{% endif %} | |
| 44 | - </button> | |
| 45 | - </form> | |
| 46 | - {% endfor %} | |
| 47 | -</div> | |
| 48 | - | |
| 49 | 30 | <form method="post" action="/equipes/renommer"> |
| 50 | 31 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 51 | 32 | <div class="field" style="margin-bottom:var(--space-2)"> |
| @@ -27,25 +27,6 @@ | |||
| 27 | <p class="card-body">{{ account_line }}</p> | 27 | <p class="card-body">{{ account_line }}</p> |
| 28 | </div> | 28 | </div> |
| 29 | 29 | ||
| 30 | -<h4 style="margin:0 0 var(--space-2)">Équipe active</h4> | ||
| 31 | -<p style="font-size:13px;opacity:.6;margin:0 0 var(--space-3)">Tâches, temps et statistiques sont propres à l'équipe active.</p> | ||
| 32 | -<div style="display:grid;gap:var(--space-2);margin-bottom:var(--space-4)"> | ||
| 33 | - {% for t in my_teams %} | ||
| 34 | - <form method="post" action="/equipes/basculer" style="margin:0"> | ||
| 35 | - <input type="hidden" name="csrf" value="{{ csrf }}"> | ||
| 36 | - <input type="hidden" name="equipe" value="{{ t.id }}"> | ||
| 37 | - <button type="submit" class="team-choice{% if t.current %} current{% endif %}"> | ||
| 38 | - <span class="team-dot"></span> | ||
| 39 | - <span style="flex:1;text-align:left"> | ||
| 40 | - <span style="display:block;font-size:16px">{{ t.name }}</span> | ||
| 41 | - <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span> | ||
| 42 | - </span> | ||
| 43 | - {% if t.current %}<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"></path></svg>{% endif %} | ||
| 44 | - </button> | ||
| 45 | - </form> | ||
| 46 | - {% endfor %} | ||
| 47 | -</div> | ||
| 48 | - | ||
| 49 | <form method="post" action="/equipes/renommer"> | 30 | <form method="post" action="/equipes/renommer"> |
| 50 | <input type="hidden" name="csrf" value="{{ csrf }}"> | 31 | <input type="hidden" name="csrf" value="{{ csrf }}"> |
| 51 | <div class="field" style="margin-bottom:var(--space-2)"> | 32 | <div class="field" style="margin-bottom:var(--space-2)"> |