judu/ex-egopublic Fork
⑂ f59263c
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(équipes): couleur d'équipe dérivée du hash de l'id — pastille dans la pill d'en-tête (agrandie) et le sélecteur ; palette étendue à 15 teintesUnverified

Claude-Session: https://claude.ai/code/session_01FPKHKGfqvnvXKY9tNhYmkh
Julien Durillon committed 2026-08-08T18:00:33+02:00 Browse files
f59263c parent: d4a0a1e
modified assets/style.css +4 -7
@@ -149,10 +149,12 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; }
149149 .app-header { padding: var(--space-6) var(--space-4) var(--space-3); }
150150 .app-topline { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; }
151151 .team-pill {
152- display: inline-flex; align-items: center; gap: 6px; font-size: 12px;
153- padding: 4px 11px 4px 8px; border-radius: 999px;
152+ display: inline-flex; align-items: center; gap: 8px; font-size: 14px;
153+ padding: 9px 15px 9px 12px; border-radius: 999px;
154154 background: var(--color-accent-2-200); color: var(--color-accent-2-900);
155155 }
156+/* Pastille couleur d'équipe (hash de l'id, posée en style inline). */
157+.team-dot { width: 12px; height: 12px; border-radius: 999px; flex: none; }
156158 /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */
157159 a.team-pill { text-decoration: none; cursor: pointer; }
158160 a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-200)); }
@@ -264,15 +266,10 @@ a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-2
264266 .team-choice:disabled { opacity: .55; cursor: default; }
265267 .team-choice:disabled:hover { border-color: transparent; }
266268 .team-choice.current:disabled:hover { border-color: var(--color-accent-2); }
267-.team-choice .team-dot {
268- width: 12px; height: 12px; border-radius: 999px; flex: none;
269- background: var(--color-neutral-300);
270-}
271269 .team-choice.current {
272270 background: var(--color-accent-2-200); border-color: var(--color-accent-2);
273271 color: var(--color-accent-2-900);
274272 }
275-.team-choice.current .team-dot { background: var(--color-accent-2); }
276273
277274 .invite-card {
278275 background: var(--color-accent-200); border-radius: var(--radius-lg);
@@ -149,10 +149,12 @@ input[type=number] { -moz-appearance: textfield; appearance: textfield; }
149 .app-header { padding: var(--space-6) var(--space-4) var(--space-3); }149 .app-header { padding: var(--space-6) var(--space-4) var(--space-3); }
150 .app-topline { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; }150 .app-topline { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 4px; }
151 .team-pill {151 .team-pill {
152- display: inline-flex; align-items: center; gap: 6px; font-size: 12px;152+ display: inline-flex; align-items: center; gap: 8px; font-size: 14px;
153- padding: 4px 11px 4px 8px; border-radius: 999px;153+ padding: 9px 15px 9px 12px; 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+/* Pastille couleur d'équipe (hash de l'id, posée en style inline). */
157+.team-dot { width: 12px; height: 12px; border-radius: 999px; flex: none; }
156 /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */158 /* Pill-sélecteur (plusieurs équipes) : ouvre la sheet #equipes. */
157 a.team-pill { text-decoration: none; cursor: pointer; }159 a.team-pill { text-decoration: none; cursor: pointer; }
158 a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-200)); }160 a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-200)); }
@@ -264,15 +266,10 @@ a.team-pill:hover { background: var(--color-accent-2-300, var(--color-accent-2-2
264 .team-choice:disabled { opacity: .55; cursor: default; }266 .team-choice:disabled { opacity: .55; cursor: default; }
265 .team-choice:disabled:hover { border-color: transparent; }267 .team-choice:disabled:hover { border-color: transparent; }
266 .team-choice.current:disabled:hover { border-color: var(--color-accent-2); }268 .team-choice.current:disabled:hover { border-color: var(--color-accent-2); }
267-.team-choice .team-dot {
268- width: 12px; height: 12px; border-radius: 999px; flex: none;
269- background: var(--color-neutral-300);
270-}
271 .team-choice.current {269 .team-choice.current {
272 background: var(--color-accent-2-200); border-color: var(--color-accent-2);270 background: var(--color-accent-2-200); border-color: var(--color-accent-2);
273 color: var(--color-accent-2-900);271 color: var(--color-accent-2-900);
274 }272 }
275-.team-choice.current .team-dot { background: var(--color-accent-2); }
276 273
277 .invite-card {274 .invite-card {
278 background: var(--color-accent-200); border-radius: var(--radius-lg);275 background: var(--color-accent-200); border-radius: var(--radius-lg);
modified src/charts.rs +19 -1
@@ -7,8 +7,12 @@ use crate::db::users::UserRow;
77
88 /// Palette du design system « Organic » — couleur par ordre de création de la
99 /// tâche (les UUIDv7 trient dans l'ordre de création), stable dans le temps.
10-pub const PALETTE: [&str; 7] = [
10+/// ⚠ Étendre uniquement par la fin : les 7 premières teintes sont historiques,
11+/// les réordonner changerait la couleur des tâches existantes.
12+pub const PALETTE: [&str; 15] = [
1113 "#d67f48", "#8fa073", "#f6a06b", "#56633f", "#a19786", "#b2622d", "#ccdbb2",
14+ "#d9b45f", "#5c7c65", "#c96f5a", "#8d7086", "#e3c088", "#6d4a33", "#7590a5",
15+ "#c98d8d",
1216 ];
1317
1418 /// task_id → couleur, à partir de la liste des tâches triée par id (création).
@@ -20,6 +24,12 @@ pub fn color_map(task_ids_in_creation_order: &[Uuid]) -> HashMap<Uuid, &'static
2024 .collect()
2125 }
2226
27+/// Couleur d'une équipe — dérivée du hash de son id (les bits bas d'un
28+/// UUIDv7 sont aléatoires), jamais stockée en base.
29+pub fn team_color(team_id: Uuid) -> &'static str {
30+ PALETTE[(team_id.as_u128() % PALETTE.len() as u128) as usize]
31+}
32+
2333 /// « 45 min », « 2 h », « 12 h 30 »
2434 pub fn fmt_minutes(total: i64) -> String {
2535 let (h, m) = (total / 60, total % 60);
@@ -256,4 +266,12 @@ mod tests {
256266 assert_eq!(m[&ids[0]], PALETTE[0]);
257267 assert_eq!(m[&ids[1]], PALETTE[1]);
258268 }
269+
270+ #[test]
271+ fn team_color_stable_and_in_palette() {
272+ let id = Uuid::from_u128(0x1234_5678_9abc_def0);
273+ assert_eq!(team_color(id), team_color(id));
274+ assert!(PALETTE.contains(&team_color(id)));
275+ assert_eq!(team_color(Uuid::from_u128(8)), PALETTE[8]);
276+ }
259277 }
@@ -7,8 +7,12 @@ use crate::db::users::UserRow;
7 7
8 /// Palette du design system « Organic » — couleur par ordre de création de la8 /// Palette du design system « Organic » — couleur par ordre de création de la
9 /// tâche (les UUIDv7 trient dans l'ordre de création), stable dans le temps.9 /// tâche (les UUIDv7 trient dans l'ordre de création), stable dans le temps.
10-pub const PALETTE: [&str; 7] = [10+/// ⚠ Étendre uniquement par la fin : les 7 premières teintes sont historiques,
11+/// les réordonner changerait la couleur des tâches existantes.
12+pub const PALETTE: [&str; 15] = [
11 "#d67f48", "#8fa073", "#f6a06b", "#56633f", "#a19786", "#b2622d", "#ccdbb2",13 "#d67f48", "#8fa073", "#f6a06b", "#56633f", "#a19786", "#b2622d", "#ccdbb2",
14+ "#d9b45f", "#5c7c65", "#c96f5a", "#8d7086", "#e3c088", "#6d4a33", "#7590a5",
15+ "#c98d8d",
12 ];16 ];
13 17
14 /// task_id → couleur, à partir de la liste des tâches triée par id (création).18 /// task_id → couleur, à partir de la liste des tâches triée par id (création).
@@ -20,6 +24,12 @@ pub fn color_map(task_ids_in_creation_order: &[Uuid]) -> HashMap<Uuid, &'static
20 .collect()24 .collect()
21 }25 }
22 26
27+/// Couleur d'une équipe — dérivée du hash de son id (les bits bas d'un
28+/// UUIDv7 sont aléatoires), jamais stockée en base.
29+pub fn team_color(team_id: Uuid) -> &'static str {
30+ PALETTE[(team_id.as_u128() % PALETTE.len() as u128) as usize]
31+}
32+
23 /// « 45 min », « 2 h », « 12 h 30 »33 /// « 45 min », « 2 h », « 12 h 30 »
24 pub fn fmt_minutes(total: i64) -> String {34 pub fn fmt_minutes(total: i64) -> String {
25 let (h, m) = (total / 60, total % 60);35 let (h, m) = (total / 60, total % 60);
@@ -256,4 +266,12 @@ mod tests {
256 assert_eq!(m[&ids[0]], PALETTE[0]);266 assert_eq!(m[&ids[0]], PALETTE[0]);
257 assert_eq!(m[&ids[1]], PALETTE[1]);267 assert_eq!(m[&ids[1]], PALETTE[1]);
258 }268 }
269+
270+ #[test]
271+ fn team_color_stable_and_in_palette() {
272+ let id = Uuid::from_u128(0x1234_5678_9abc_def0);
273+ assert_eq!(team_color(id), team_color(id));
274+ assert!(PALETTE.contains(&team_color(id)));
275+ assert_eq!(team_color(Uuid::from_u128(8)), PALETTE[8]);
276+ }
259 }277 }
modified src/render.rs +4 -0
@@ -23,6 +23,8 @@ pub struct TeamChoiceView {
2323 /// « 2 membres »
2424 pub meta: String,
2525 pub current: bool,
26+ /// Couleur dérivée du hash de l'id (charts::team_color).
27+ pub color: &'static str,
2628 }
2729
2830 /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶).
@@ -45,6 +47,8 @@ pub struct Shell {
4547 pub username: String,
4648 /// Nom de l'équipe active (pill de l'en-tête).
4749 pub team_name: String,
50+ /// Couleur de l'équipe active (pastille dans la pill).
51+ pub team_color: &'static str,
4852 /// "add" | "stats" | "times" | "settings"
4953 pub tab: &'static str,
5054 pub toast: Option<String>,
@@ -23,6 +23,8 @@ pub struct TeamChoiceView {
23 /// « 2 membres »23 /// « 2 membres »
24 pub meta: String,24 pub meta: String,
25 pub current: bool,25 pub current: bool,
26+ /// Couleur dérivée du hash de l'id (charts::team_color).
27+ pub color: &'static str,
26 }28 }
27 29
28 /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶).30 /// Pill timer flottante du shell — None dans Shell = repos (bouton ▶).
@@ -45,6 +47,8 @@ pub struct Shell {
45 pub username: String,47 pub username: String,
46 /// Nom de l'équipe active (pill de l'en-tête).48 /// Nom de l'équipe active (pill de l'en-tête).
47 pub team_name: String,49 pub team_name: String,
50+ /// Couleur de l'équipe active (pastille dans la pill).
51+ pub team_color: &'static str,
48 /// "add" | "stats" | "times" | "settings"52 /// "add" | "stats" | "times" | "settings"
49 pub tab: &'static str,53 pub tab: &'static str,
50 pub toast: Option<String>,54 pub toast: Option<String>,
modified src/routes/mod.rs +3 -1
@@ -14,7 +14,7 @@ use axum::response::{Redirect, Response};
1414 use axum::routing::{get, post};
1515
1616 use crate::auth::extractor::AuthUser;
17-use crate::charts::fmt_minutes;
17+use crate::charts::{fmt_minutes, team_color};
1818 use crate::db::{invitations, teams as teams_db, timers, users};
1919 use crate::error::AppError;
2020 use crate::render::{Shell, TeamChoiceView, TimerView};
@@ -155,6 +155,7 @@ pub async fn shell(
155155 title: title.to_string(),
156156 username,
157157 team_name: ctx.team_name.clone(),
158+ team_color: team_color(ctx.team_id),
158159 tab,
159160 toast,
160161 invite_count,
@@ -175,6 +176,7 @@ pub async fn shell(
175176 .map(|t| TeamChoiceView {
176177 current: t.id == ctx.team_id,
177178 meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }),
179+ color: team_color(t.id),
178180 id: t.id,
179181 name: t.name,
180182 })
@@ -14,7 +14,7 @@ use axum::response::{Redirect, Response};
14 use axum::routing::{get, post};14 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, team_color};
18 use crate::db::{invitations, teams as teams_db, 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, TeamChoiceView, TimerView};20 use crate::render::{Shell, TeamChoiceView, TimerView};
@@ -155,6 +155,7 @@ pub async fn shell(
155 title: title.to_string(),155 title: title.to_string(),
156 username,156 username,
157 team_name: ctx.team_name.clone(),157 team_name: ctx.team_name.clone(),
158+ team_color: team_color(ctx.team_id),
158 tab,159 tab,
159 toast,160 toast,
160 invite_count,161 invite_count,
@@ -175,6 +176,7 @@ pub async fn shell(
175 .map(|t| TeamChoiceView {176 .map(|t| TeamChoiceView {
176 current: t.id == ctx.team_id,177 current: t.id == ctx.team_id,
177 meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }),178 meta: format!("{} membre{}", t.members, if t.members > 1 { "s" } else { "" }),
179+ color: team_color(t.id),
178 id: t.id,180 id: t.id,
179 name: t.name,181 name: t.name,
180 })182 })
modified templates/app.html +3 -3
@@ -5,13 +5,13 @@
55 <div class="app-topline">
66 {% if !shell.teams.is_empty() %}
77 <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>
8+ <span class="team-dot" style="background:{{ shell.team_color }}"></span>
99 {{ shell.team_name }}
1010 <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>
1111 </a>
1212 {% else %}
1313 <span class="team-pill">
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>
14+ <span class="team-dot" style="background:{{ shell.team_color }}"></span>
1515 {{ shell.team_name }}
1616 </span>
1717 {% endif %}
@@ -74,7 +74,7 @@
7474 <input type="hidden" name="equipe" value="{{ t.id }}">
7575 <input type="hidden" name="retour" value="{{ shell.start_return }}">
7676 <button type="submit" class="team-choice{% if t.current %} current{% endif %}">
77- <span class="team-dot"></span>
77+ <span class="team-dot" style="background:{{ t.color }}"></span>
7878 <span style="flex:1;text-align:left">
7979 <span style="display:block;font-size:16px">{{ t.name }}</span>
8080 <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span>
@@ -5,13 +5,13 @@
5 <div class="app-topline">5 <div class="app-topline">
6 {% if !shell.teams.is_empty() %}6 {% if !shell.teams.is_empty() %}
7 <a class="team-pill" href="#equipes" aria-label="Changer d'équipe">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>8+ <span class="team-dot" style="background:{{ shell.team_color }}"></span>
9 {{ shell.team_name }}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>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>11 </a>
12 {% else %}12 {% else %}
13 <span class="team-pill">13 <span class="team-pill">
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>14+ <span class="team-dot" style="background:{{ shell.team_color }}"></span>
15 {{ shell.team_name }}15 {{ shell.team_name }}
16 </span>16 </span>
17 {% endif %}17 {% endif %}
@@ -74,7 +74,7 @@
74 <input type="hidden" name="equipe" value="{{ t.id }}">74 <input type="hidden" name="equipe" value="{{ t.id }}">
75 <input type="hidden" name="retour" value="{{ shell.start_return }}">75 <input type="hidden" name="retour" value="{{ shell.start_return }}">
76 <button type="submit" class="team-choice{% if t.current %} current{% endif %}">76 <button type="submit" class="team-choice{% if t.current %} current{% endif %}">
77- <span class="team-dot"></span>77+ <span class="team-dot" style="background:{{ t.color }}"></span>
78 <span style="flex:1;text-align:left">78 <span style="flex:1;text-align:left">
79 <span style="display:block;font-size:16px">{{ t.name }}</span>79 <span style="display:block;font-size:16px">{{ t.name }}</span>
80 <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span>80 <span style="display:block;font-size:12px;opacity:.6">{{ t.meta }}</span>